@jaggerxtrm/specialists 3.2.1 → 3.3.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
@@ -17423,7 +17423,7 @@ async function parseSpecialist(yamlContent) {
17423
17423
  const raw = $parse(yamlContent);
17424
17424
  return SpecialistSchema.parseAsync(raw);
17425
17425
  }
17426
- var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
17426
+ var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
17427
17427
  var init_schema = __esm(() => {
17428
17428
  init_zod();
17429
17429
  init_dist();
@@ -17447,6 +17447,7 @@ var init_schema = __esm(() => {
17447
17447
  stall_timeout_ms: numberType().optional(),
17448
17448
  response_format: enumType(["text", "json", "markdown"]).default("text"),
17449
17449
  permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
17450
+ thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
17450
17451
  preferred_profile: stringType().optional(),
17451
17452
  approval_mode: stringType().optional()
17452
17453
  });
@@ -17458,27 +17459,26 @@ var init_schema = __esm(() => {
17458
17459
  examples: arrayType(unknownType()).optional(),
17459
17460
  skill_inherit: stringType().optional()
17460
17461
  });
17462
+ ScriptEntrySchema = objectType({
17463
+ run: stringType().optional(),
17464
+ path: stringType().optional(),
17465
+ phase: enumType(["pre", "post"]),
17466
+ inject_output: booleanType().default(false)
17467
+ }).transform((s) => ({
17468
+ run: s.run ?? s.path ?? "",
17469
+ phase: s.phase,
17470
+ inject_output: s.inject_output
17471
+ }));
17461
17472
  SkillsSchema = objectType({
17462
- scripts: arrayType(objectType({
17463
- path: stringType(),
17464
- phase: enumType(["pre", "post"]),
17465
- inject_output: booleanType().default(false)
17466
- })).optional(),
17467
- references: arrayType(unknownType()).optional(),
17468
- tools: arrayType(stringType()).optional(),
17469
- paths: arrayType(stringType()).optional()
17473
+ paths: arrayType(stringType()).optional(),
17474
+ scripts: arrayType(ScriptEntrySchema).optional()
17470
17475
  }).optional();
17471
17476
  CapabilitiesSchema = objectType({
17472
- file_scope: arrayType(stringType()).optional(),
17473
- blocked_tools: arrayType(stringType()).optional(),
17474
- can_spawn: booleanType().optional(),
17475
- tools: arrayType(objectType({ name: stringType(), purpose: stringType() })).optional(),
17476
- diagnostic_scripts: arrayType(stringType()).optional()
17477
+ required_tools: arrayType(stringType()).optional(),
17478
+ external_commands: arrayType(stringType()).optional()
17477
17479
  }).optional();
17478
17480
  CommunicationSchema = objectType({
17479
- publishes: arrayType(stringType()).optional(),
17480
- subscribes: arrayType(stringType()).optional(),
17481
- output_to: stringType().optional()
17481
+ next_specialists: unionType([stringType(), arrayType(stringType())]).optional()
17482
17482
  }).optional();
17483
17483
  ValidationSchema = objectType({
17484
17484
  files_to_watch: arrayType(stringType()).optional(),
@@ -17494,6 +17494,7 @@ var init_schema = __esm(() => {
17494
17494
  capabilities: CapabilitiesSchema,
17495
17495
  communication: CommunicationSchema,
17496
17496
  validation: ValidationSchema,
17497
+ output_file: stringType().optional(),
17497
17498
  beads_integration: enumType(["auto", "always", "never"]).default("auto"),
17498
17499
  heartbeat: unknownType().optional()
17499
17500
  })
@@ -17503,7 +17504,6 @@ var init_schema = __esm(() => {
17503
17504
  // src/specialist/loader.ts
17504
17505
  import { readdir, readFile, stat } from "node:fs/promises";
17505
17506
  import { join } from "node:path";
17506
- import { homedir } from "node:os";
17507
17507
  import { existsSync } from "node:fs";
17508
17508
  async function checkStaleness(summary) {
17509
17509
  if (!summary.filestoWatch?.length || !summary.updated)
@@ -17527,17 +17527,13 @@ async function checkStaleness(summary) {
17527
17527
  class SpecialistLoader {
17528
17528
  cache = new Map;
17529
17529
  projectDir;
17530
- userDir;
17531
17530
  constructor(options = {}) {
17532
17531
  this.projectDir = options.projectDir ?? process.cwd();
17533
- this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
17534
17532
  }
17535
17533
  getScanDirs() {
17536
17534
  const dirs = [
17537
- { path: join(this.projectDir, "specialists"), scope: "project" },
17538
- { path: join(this.projectDir, ".claude", "specialists"), scope: "project" },
17539
- { path: join(this.projectDir, ".agent-forge", "specialists"), scope: "project" },
17540
- { path: this.userDir, scope: "user" }
17535
+ { path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
17536
+ { path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" }
17541
17537
  ];
17542
17538
  return dirs.filter((d) => existsSync(d.path));
17543
17539
  }
@@ -17569,7 +17565,11 @@ class SpecialistLoader {
17569
17565
  filestoWatch: spec.specialist.validation?.files_to_watch,
17570
17566
  staleThresholdDays: spec.specialist.validation?.stale_threshold_days
17571
17567
  });
17572
- } catch {}
17568
+ } catch (e) {
17569
+ const reason = e instanceof Error ? e.message : String(e);
17570
+ process.stderr.write(`[specialists] skipping ${filePath}: ${reason}
17571
+ `);
17572
+ }
17573
17573
  }
17574
17574
  }
17575
17575
  return results;
@@ -17584,11 +17584,10 @@ class SpecialistLoader {
17584
17584
  const spec = await parseSpecialist(content);
17585
17585
  const rawPaths = spec.specialist.skills?.paths;
17586
17586
  if (rawPaths?.length) {
17587
- const home = homedir();
17588
17587
  const fileDir = dir.path;
17589
17588
  const resolved = rawPaths.map((p) => {
17590
17589
  if (p.startsWith("~/"))
17591
- return join(home, p.slice(2));
17590
+ return join(process.env.HOME || "", p.slice(2));
17592
17591
  if (p.startsWith("./"))
17593
17592
  return join(fileDir, p.slice(2));
17594
17593
  return p;
@@ -17651,16 +17650,16 @@ var init_backendMap = __esm(() => {
17651
17650
  // src/pi/session.ts
17652
17651
  import { spawn } from "node:child_process";
17653
17652
  import { existsSync as existsSync2 } from "node:fs";
17654
- import { homedir as homedir2 } from "node:os";
17653
+ import { homedir } from "node:os";
17655
17654
  import { join as join2 } from "node:path";
17656
17655
  function mapPermissionToTools(level) {
17657
17656
  switch (level?.toUpperCase()) {
17658
17657
  case "READ_ONLY":
17659
- return "read,bash,grep,find,ls";
17660
- case "BASH_ONLY":
17661
- return "bash";
17658
+ return "read,grep,find,ls";
17662
17659
  case "LOW":
17660
+ return "read,bash,grep,find,ls";
17663
17661
  case "MEDIUM":
17662
+ return "read,bash,edit,grep,find,ls";
17664
17663
  case "HIGH":
17665
17664
  return "read,bash,edit,write,grep,find,ls";
17666
17665
  default:
@@ -17708,7 +17707,13 @@ class PiAgentSession {
17708
17707
  const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
17709
17708
  if (toolsFlag)
17710
17709
  args.push("--tools", toolsFlag);
17711
- const piExtDir = join2(homedir2(), ".pi", "agent", "extensions");
17710
+ if (this.options.thinkingLevel) {
17711
+ args.push("--thinking", this.options.thinkingLevel);
17712
+ }
17713
+ for (const skillPath of this.options.skillPaths ?? []) {
17714
+ args.push("--skill", skillPath);
17715
+ }
17716
+ const piExtDir = join2(homedir(), ".pi", "agent", "extensions");
17712
17717
  const permLevel = (this.options.permissionLevel ?? "").toUpperCase();
17713
17718
  if (permLevel !== "READ_ONLY") {
17714
17719
  const qgPath = join2(piExtDir, "quality-gates");
@@ -17902,6 +17907,30 @@ class PiAgentSession {
17902
17907
  this.proc = undefined;
17903
17908
  this._doneReject?.(new SessionKilledError);
17904
17909
  }
17910
+ async steer(message) {
17911
+ if (this._killed || !this.proc?.stdin) {
17912
+ throw new Error("Session is not active");
17913
+ }
17914
+ const cmd = JSON.stringify({ type: "steer", message }) + `
17915
+ `;
17916
+ await new Promise((resolve, reject) => {
17917
+ this.proc.stdin.write(cmd, (err) => err ? reject(err) : resolve());
17918
+ });
17919
+ }
17920
+ async resume(task, timeout) {
17921
+ if (this._killed || !this.proc?.stdin) {
17922
+ throw new Error("Session is not active");
17923
+ }
17924
+ this._agentEndReceived = false;
17925
+ const donePromise = new Promise((resolve, reject) => {
17926
+ this._doneResolve = resolve;
17927
+ this._doneReject = reject;
17928
+ });
17929
+ donePromise.catch(() => {});
17930
+ this._donePromise = donePromise;
17931
+ await this.prompt(task);
17932
+ await this.waitForDone(timeout);
17933
+ }
17905
17934
  }
17906
17935
  var SessionKilledError;
17907
17936
  var init_session = __esm(() => {
@@ -18055,14 +18084,16 @@ var init_beads = () => {};
18055
18084
  // src/specialist/runner.ts
18056
18085
  import { createHash } from "node:crypto";
18057
18086
  import { writeFile } from "node:fs/promises";
18058
- import { execSync } from "node:child_process";
18059
- import { basename } from "node:path";
18060
- function runScript(scriptPath) {
18087
+ import { execSync, spawnSync as spawnSync2 } from "node:child_process";
18088
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
18089
+ import { basename, resolve } from "node:path";
18090
+ import { homedir as homedir2 } from "node:os";
18091
+ function runScript(run) {
18061
18092
  try {
18062
- const output = execSync(scriptPath, { encoding: "utf8", timeout: 30000 });
18063
- return { name: basename(scriptPath), output, exitCode: 0 };
18093
+ const output = execSync(run, { encoding: "utf8", timeout: 30000 });
18094
+ return { name: basename(run.split(" ")[0]), output, exitCode: 0 };
18064
18095
  } catch (e) {
18065
- return { name: basename(scriptPath), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
18096
+ return { name: basename(run.split(" ")[0]), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
18066
18097
  }
18067
18098
  }
18068
18099
  function formatScriptOutput(results) {
@@ -18080,6 +18111,77 @@ ${r.output.trim()}
18080
18111
  ${blocks}
18081
18112
  </pre_flight_context>`;
18082
18113
  }
18114
+ function resolvePath(p) {
18115
+ return p.startsWith("~/") ? resolve(homedir2(), p.slice(2)) : resolve(p);
18116
+ }
18117
+ function commandExists(cmd) {
18118
+ const result = spawnSync2("which", [cmd], { stdio: "ignore" });
18119
+ return result.status === 0;
18120
+ }
18121
+ function validateShebang(filePath, errors5) {
18122
+ try {
18123
+ const head = readFileSync(filePath, "utf-8").slice(0, 120);
18124
+ if (!head.startsWith("#!"))
18125
+ return;
18126
+ const shebang = head.split(`
18127
+ `)[0].toLowerCase();
18128
+ const typos = [
18129
+ [/pytho[^n]|pyton|pyhon/, "python"],
18130
+ [/nod[^e]b/, "node"],
18131
+ [/bsh$|bas$/, "bash"],
18132
+ [/rub[^y]/, "ruby"]
18133
+ ];
18134
+ for (const [pattern, correct] of typos) {
18135
+ if (pattern.test(shebang)) {
18136
+ errors5.push(` ✗ ${filePath}: shebang looks wrong — did you mean '${correct}'? (got: ${shebang})`);
18137
+ }
18138
+ }
18139
+ } catch {}
18140
+ }
18141
+ function validateBeforeRun(spec) {
18142
+ const errors5 = [];
18143
+ const warnings = [];
18144
+ for (const p of spec.specialist.skills?.paths ?? []) {
18145
+ const abs = resolvePath(p);
18146
+ if (!existsSync3(abs))
18147
+ warnings.push(` ⚠ skills.paths: file not found: ${p}`);
18148
+ }
18149
+ for (const script of spec.specialist.skills?.scripts ?? []) {
18150
+ const { run } = script;
18151
+ if (!run)
18152
+ continue;
18153
+ const isFilePath = run.startsWith("./") || run.startsWith("../") || run.startsWith("/") || run.startsWith("~/");
18154
+ if (isFilePath) {
18155
+ const abs = resolvePath(run);
18156
+ if (!existsSync3(abs)) {
18157
+ errors5.push(` ✗ skills.scripts: script not found: ${run}`);
18158
+ } else {
18159
+ validateShebang(abs, errors5);
18160
+ }
18161
+ } else {
18162
+ const binary = run.split(" ")[0];
18163
+ if (!commandExists(binary)) {
18164
+ errors5.push(` ✗ skills.scripts: command not found on PATH: ${binary}`);
18165
+ }
18166
+ }
18167
+ }
18168
+ for (const cmd of spec.specialist.capabilities?.external_commands ?? []) {
18169
+ if (!commandExists(cmd)) {
18170
+ errors5.push(` ✗ capabilities.external_commands: not found on PATH: ${cmd}`);
18171
+ }
18172
+ }
18173
+ if (warnings.length > 0) {
18174
+ process.stderr.write(`[specialists] pre-run warnings:
18175
+ ${warnings.join(`
18176
+ `)}
18177
+ `);
18178
+ }
18179
+ if (errors5.length > 0) {
18180
+ throw new Error(`Specialist pre-run validation failed:
18181
+ ${errors5.join(`
18182
+ `)}`);
18183
+ }
18184
+ }
18083
18185
 
18084
18186
  class SpecialistRunner {
18085
18187
  deps;
@@ -18088,12 +18190,12 @@ class SpecialistRunner {
18088
18190
  this.deps = deps;
18089
18191
  this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
18090
18192
  }
18091
- async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated) {
18193
+ async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
18092
18194
  const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
18093
18195
  const invocationId = crypto.randomUUID();
18094
18196
  const start = Date.now();
18095
18197
  const spec = await loader.get(options.name);
18096
- const { metadata, execution, prompt, communication } = spec.specialist;
18198
+ const { metadata, execution, prompt, output_file } = spec.specialist;
18097
18199
  const primaryModel = options.backendOverride ?? execution.model;
18098
18200
  const model = circuitBreaker.isAvailable(primaryModel) ? primaryModel : execution.fallback_model ?? primaryModel;
18099
18201
  const fallbackUsed = model !== primaryModel;
@@ -18104,8 +18206,9 @@ class SpecialistRunner {
18104
18206
  circuit_breaker_state: circuitBreaker.getState(model),
18105
18207
  scope: "project"
18106
18208
  });
18209
+ validateBeforeRun(spec);
18107
18210
  const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
18108
- const preResults = preScripts.map((s) => runScript(s.path)).filter((_, i) => preScripts[i].inject_output);
18211
+ const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
18109
18212
  const preScriptOutput = formatScriptOutput(preResults);
18110
18213
  const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
18111
18214
  const variables = {
@@ -18123,40 +18226,32 @@ class SpecialistRunner {
18123
18226
  estimated_tokens: Math.ceil(renderedTask.length / 4),
18124
18227
  system_prompt_present: !!prompt.system
18125
18228
  });
18126
- const { readFile: readFile2 } = await import("node:fs/promises");
18127
- let agentsMd = prompt.system ?? "";
18128
- if (prompt.skill_inherit) {
18129
- const skillContent = await readFile2(prompt.skill_inherit, "utf-8").catch(() => "");
18130
- if (skillContent)
18131
- agentsMd += `
18132
-
18133
- ---
18134
- # Service Knowledge
18135
-
18136
- ${skillContent}`;
18137
- }
18138
- const skillPaths = spec.specialist.skills?.paths ?? [];
18139
- for (const skillPath of skillPaths) {
18140
- const skillContent = await readFile2(skillPath, "utf-8").catch(() => "");
18141
- if (skillContent)
18142
- agentsMd += `
18143
-
18144
- ---
18145
- # Skill: ${skillPath}
18146
-
18147
- ${skillContent}`;
18148
- }
18149
- if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
18150
- agentsMd += `
18151
-
18152
- ---
18153
- # Diagnostic Scripts
18154
- You have access via Bash:
18155
- `;
18156
- for (const s of spec.specialist.capabilities.diagnostic_scripts) {
18157
- agentsMd += `- \`${s}\`
18158
- `;
18229
+ const agentsMd = prompt.system ?? "";
18230
+ const skillPaths = [];
18231
+ if (prompt.skill_inherit)
18232
+ skillPaths.push(prompt.skill_inherit);
18233
+ skillPaths.push(...spec.specialist.skills?.paths ?? []);
18234
+ if (skillPaths.length > 0 || preScripts.length > 0) {
18235
+ const line = "━".repeat(56);
18236
+ onProgress?.(`
18237
+ ${line}
18238
+ ◆ AUTO INJECTED
18239
+ `);
18240
+ if (skillPaths.length > 0) {
18241
+ onProgress?.(` skills (--skill):
18242
+ ${skillPaths.map((p) => ` • ${p}`).join(`
18243
+ `)}
18244
+ `);
18159
18245
  }
18246
+ if (preScripts.length > 0) {
18247
+ onProgress?.(` pre scripts/commands:
18248
+ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
18249
+ `)}
18250
+ `);
18251
+ }
18252
+ onProgress?.(`${line}
18253
+
18254
+ `);
18160
18255
  }
18161
18256
  const permissionLevel = options.autonomyLevel ?? execution.permission_required;
18162
18257
  await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
@@ -18180,10 +18275,13 @@ You have access via Bash:
18180
18275
  let output;
18181
18276
  let sessionBackend = model;
18182
18277
  let session;
18278
+ let keepAliveActive = false;
18183
18279
  try {
18184
18280
  session = await this.sessionFactory({
18185
18281
  model,
18186
18282
  systemPrompt: agentsMd || undefined,
18283
+ skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
18284
+ thinkingLevel: execution.thinking_level,
18187
18285
  permissionLevel,
18188
18286
  cwd: process.cwd(),
18189
18287
  onToken: (delta) => onProgress?.(delta),
@@ -18197,15 +18295,29 @@ You have access via Bash:
18197
18295
  });
18198
18296
  await session.start();
18199
18297
  onKillRegistered?.(session.kill.bind(session));
18298
+ onSteerRegistered?.((msg) => session.steer(msg));
18200
18299
  await session.prompt(renderedTask);
18201
18300
  await session.waitForDone(execution.timeout_ms);
18202
18301
  sessionBackend = session.meta.backend;
18203
18302
  output = await session.getLastOutput();
18204
18303
  sessionBackend = session.meta.backend;
18205
- await session.close();
18304
+ if (options.keepAlive && onResumeReady) {
18305
+ keepAliveActive = true;
18306
+ const resumeFn = async (msg) => {
18307
+ await session.resume(msg, execution.timeout_ms);
18308
+ return session.getLastOutput();
18309
+ };
18310
+ const closeFn = async () => {
18311
+ keepAliveActive = false;
18312
+ await session.close();
18313
+ };
18314
+ onResumeReady(resumeFn, closeFn);
18315
+ } else {
18316
+ await session.close();
18317
+ }
18206
18318
  const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
18207
18319
  for (const script of postScripts)
18208
- runScript(script.path);
18320
+ runScript(script.run);
18209
18321
  circuitBreaker.recordSuccess(model);
18210
18322
  } catch (err) {
18211
18323
  const isCancelled = err instanceof SessionKilledError;
@@ -18226,11 +18338,13 @@ You have access via Bash:
18226
18338
  });
18227
18339
  throw err;
18228
18340
  } finally {
18229
- session?.kill();
18341
+ if (!keepAliveActive) {
18342
+ session?.kill();
18343
+ }
18230
18344
  }
18231
18345
  const durationMs = Date.now() - start;
18232
- if (communication?.output_to) {
18233
- await writeFile(communication.output_to, output, "utf-8").catch(() => {});
18346
+ if (output_file) {
18347
+ await writeFile(output_file, output, "utf-8").catch(() => {});
18234
18348
  }
18235
18349
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
18236
18350
  status: "COMPLETE",
@@ -18264,7 +18378,7 @@ You have access via Bash:
18264
18378
  model: "?",
18265
18379
  specialistVersion
18266
18380
  });
18267
- this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
18381
+ this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId), (steerFn) => registry2.setSteerFn(jobId, steerFn), (resumeFn, closeFn) => registry2.setResumeFn(jobId, resumeFn, closeFn)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
18268
18382
  return jobId;
18269
18383
  }
18270
18384
  }
@@ -18344,656 +18458,198 @@ class CircuitBreaker {
18344
18458
  }
18345
18459
  }
18346
18460
 
18347
- // src/cli/install.ts
18348
- var exports_install = {};
18349
- __export(exports_install, {
18350
- run: () => run
18351
- });
18352
- import { execFileSync } from "node:child_process";
18353
- import { fileURLToPath } from "node:url";
18354
- import { dirname as dirname2, join as join5 } from "node:path";
18355
- async function run() {
18356
- const installerPath = join5(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18357
- execFileSync(process.execPath, [installerPath], { stdio: "inherit" });
18461
+ // src/specialist/timeline-events.ts
18462
+ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18463
+ const t = Date.now();
18464
+ switch (callbackEvent) {
18465
+ case "thinking":
18466
+ return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18467
+ case "toolcall":
18468
+ return {
18469
+ t,
18470
+ type: TIMELINE_EVENT_TYPES.TOOL,
18471
+ tool: context.tool ?? "unknown",
18472
+ phase: "start",
18473
+ tool_call_id: context.toolCallId
18474
+ };
18475
+ case "tool_execution_end":
18476
+ return {
18477
+ t,
18478
+ type: TIMELINE_EVENT_TYPES.TOOL,
18479
+ tool: context.tool ?? "unknown",
18480
+ phase: "end",
18481
+ tool_call_id: context.toolCallId,
18482
+ is_error: context.isError
18483
+ };
18484
+ case "text":
18485
+ return { t, type: TIMELINE_EVENT_TYPES.TEXT };
18486
+ case "agent_end":
18487
+ case "message_done":
18488
+ case "done":
18489
+ return null;
18490
+ default:
18491
+ return null;
18492
+ }
18358
18493
  }
18359
- var init_install = () => {};
18360
-
18361
- // src/cli/version.ts
18362
- var exports_version = {};
18363
- __export(exports_version, {
18364
- run: () => run2
18365
- });
18366
- import { createRequire as createRequire2 } from "node:module";
18367
- async function run2() {
18368
- const req = createRequire2(import.meta.url);
18369
- const pkg = req("../package.json");
18370
- console.log(`${pkg.name} v${pkg.version}`);
18494
+ function createRunStartEvent(specialist, beadId) {
18495
+ return {
18496
+ t: Date.now(),
18497
+ type: TIMELINE_EVENT_TYPES.RUN_START,
18498
+ specialist,
18499
+ bead_id: beadId
18500
+ };
18371
18501
  }
18372
- var init_version = () => {};
18373
-
18374
- // src/cli/list.ts
18375
- var exports_list = {};
18376
- __export(exports_list, {
18377
- run: () => run3,
18378
- parseArgs: () => parseArgs,
18379
- ArgParseError: () => ArgParseError
18380
- });
18381
- function parseArgs(argv) {
18382
- const result = {};
18383
- for (let i = 0;i < argv.length; i++) {
18384
- const token = argv[i];
18385
- if (token === "--category") {
18386
- const value = argv[++i];
18387
- if (!value || value.startsWith("--")) {
18388
- throw new ArgParseError("--category requires a value");
18389
- }
18390
- result.category = value;
18391
- continue;
18392
- }
18393
- if (token === "--scope") {
18394
- const value = argv[++i];
18395
- if (value !== "project" && value !== "user") {
18396
- throw new ArgParseError(`--scope must be "project" or "user", got: "${value ?? ""}"`);
18397
- }
18398
- result.scope = value;
18399
- continue;
18400
- }
18401
- if (token === "--json") {
18402
- result.json = true;
18403
- continue;
18404
- }
18405
- }
18406
- return result;
18502
+ function createMetaEvent(model, backend) {
18503
+ return {
18504
+ t: Date.now(),
18505
+ type: TIMELINE_EVENT_TYPES.META,
18506
+ model,
18507
+ backend
18508
+ };
18407
18509
  }
18408
- async function run3() {
18409
- let args;
18510
+ function createRunCompleteEvent(status, elapsed_s, options) {
18511
+ return {
18512
+ t: Date.now(),
18513
+ type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
18514
+ status,
18515
+ elapsed_s,
18516
+ ...options
18517
+ };
18518
+ }
18519
+ function parseTimelineEvent(line) {
18410
18520
  try {
18411
- args = parseArgs(process.argv.slice(3));
18412
- } catch (err) {
18413
- if (err instanceof ArgParseError) {
18414
- console.error(`Error: ${err.message}`);
18415
- process.exit(1);
18521
+ const parsed = JSON.parse(line);
18522
+ if (!parsed || typeof parsed !== "object")
18523
+ return null;
18524
+ if (typeof parsed.t !== "number")
18525
+ return null;
18526
+ if (typeof parsed.type !== "string")
18527
+ return null;
18528
+ if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
18529
+ return {
18530
+ t: parsed.t,
18531
+ type: TIMELINE_EVENT_TYPES.DONE,
18532
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18533
+ };
18416
18534
  }
18417
- throw err;
18418
- }
18419
- const loader = new SpecialistLoader;
18420
- let specialists = await loader.list(args.category);
18421
- if (args.scope) {
18422
- specialists = specialists.filter((s) => s.scope === args.scope);
18423
- }
18424
- if (args.json) {
18425
- console.log(JSON.stringify(specialists, null, 2));
18426
- return;
18427
- }
18428
- if (specialists.length === 0) {
18429
- console.log("No specialists found.");
18430
- return;
18431
- }
18432
- const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
18433
- console.log(`
18434
- ${bold(`Specialists (${specialists.length})`)}
18435
- `);
18436
- for (const s of specialists) {
18437
- const name = cyan(s.name.padEnd(nameWidth));
18438
- const scopeTag = yellow(`[${s.scope}]`);
18439
- const model = dim(s.model);
18440
- const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
18441
- console.log(` ${name} ${scopeTag} ${model}`);
18442
- console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
18443
- console.log();
18444
- }
18445
- }
18446
- var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
18447
- var init_list = __esm(() => {
18448
- init_loader();
18449
- ArgParseError = class ArgParseError extends Error {
18450
- constructor(message) {
18451
- super(message);
18452
- this.name = "ArgParseError";
18535
+ if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
18536
+ return {
18537
+ t: parsed.t,
18538
+ type: TIMELINE_EVENT_TYPES.AGENT_END,
18539
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18540
+ };
18453
18541
  }
18454
- };
18455
- });
18456
-
18457
- // src/cli/models.ts
18458
- var exports_models = {};
18459
- __export(exports_models, {
18460
- run: () => run4
18461
- });
18462
- import { spawnSync as spawnSync3 } from "node:child_process";
18463
- function parsePiModels() {
18464
- const r = spawnSync3("pi", ["--list-models"], {
18465
- encoding: "utf8",
18466
- stdio: "pipe",
18467
- timeout: 8000
18468
- });
18469
- if (r.status !== 0 || r.error)
18542
+ const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
18543
+ if (!knownTypes.includes(parsed.type))
18544
+ return null;
18545
+ return parsed;
18546
+ } catch {
18470
18547
  return null;
18471
- return r.stdout.split(`
18472
- `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
18473
- const cols = line.split(/\s+/);
18474
- return {
18475
- provider: cols[0] ?? "",
18476
- model: cols[1] ?? "",
18477
- context: cols[2] ?? "",
18478
- maxOut: cols[3] ?? "",
18479
- thinking: cols[4] === "yes",
18480
- images: cols[5] === "yes"
18481
- };
18482
- }).filter((m) => m.provider && m.model);
18483
- }
18484
- function parseArgs2(argv) {
18485
- const out = {};
18486
- for (let i = 0;i < argv.length; i++) {
18487
- if (argv[i] === "--provider" && argv[i + 1]) {
18488
- out.provider = argv[++i];
18489
- continue;
18490
- }
18491
- if (argv[i] === "--used") {
18492
- out.used = true;
18493
- continue;
18494
- }
18495
18548
  }
18496
- return out;
18497
18549
  }
18498
- async function run4() {
18499
- const args = parseArgs2(process.argv.slice(3));
18500
- const loader = new SpecialistLoader;
18501
- const specialists = await loader.list();
18502
- const usedBy = new Map;
18503
- for (const s of specialists) {
18504
- const key = s.model;
18505
- if (!usedBy.has(key))
18506
- usedBy.set(key, []);
18507
- usedBy.get(key).push(s.name);
18508
- }
18509
- const allModels = parsePiModels();
18510
- if (!allModels) {
18511
- console.error("pi not found or failed — install and configure pi first");
18512
- process.exit(1);
18513
- }
18514
- let models = allModels;
18515
- if (args.provider) {
18516
- models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
18517
- }
18518
- if (args.used) {
18519
- models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
18520
- }
18521
- if (models.length === 0) {
18522
- console.log("No models match.");
18523
- return;
18524
- }
18525
- const byProvider = new Map;
18526
- for (const m of models) {
18527
- if (!byProvider.has(m.provider))
18528
- byProvider.set(m.provider, []);
18529
- byProvider.get(m.provider).push(m);
18530
- }
18531
- const total = models.length;
18532
- console.log(`
18533
- ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
18534
- `);
18535
- for (const [provider, pModels] of byProvider) {
18536
- console.log(` ${cyan2(provider)} ${dim2(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
18537
- const modelWidth = Math.max(...pModels.map((m) => m.model.length));
18538
- for (const m of pModels) {
18539
- const key = `${m.provider}/${m.model}`;
18540
- const inUse = usedBy.get(key);
18541
- const flags = [
18542
- m.thinking ? green("thinking") : dim2("·"),
18543
- m.images ? dim2("images") : ""
18544
- ].filter(Boolean).join(" ");
18545
- const ctx = dim2(`ctx ${m.context}`);
18546
- const usedLabel = inUse ? ` ${yellow2("←")} ${dim2(inUse.join(", "))}` : "";
18547
- console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
18548
- }
18549
- console.log();
18550
- }
18551
- if (!args.used) {
18552
- console.log(dim2(` --provider <name> filter by provider`));
18553
- console.log(dim2(` --used only show models used by your specialists`));
18554
- console.log();
18555
- }
18550
+ function isRunCompleteEvent(event) {
18551
+ return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
18556
18552
  }
18557
- var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`;
18558
- var init_models = __esm(() => {
18559
- init_loader();
18553
+ function compareTimelineEvents(a, b) {
18554
+ return a.t - b.t;
18555
+ }
18556
+ var TIMELINE_EVENT_TYPES;
18557
+ var init_timeline_events = __esm(() => {
18558
+ TIMELINE_EVENT_TYPES = {
18559
+ RUN_START: "run_start",
18560
+ META: "meta",
18561
+ THINKING: "thinking",
18562
+ TOOL: "tool",
18563
+ TEXT: "text",
18564
+ RUN_COMPLETE: "run_complete",
18565
+ DONE: "done",
18566
+ AGENT_END: "agent_end"
18567
+ };
18560
18568
  });
18561
18569
 
18562
- // src/cli/init.ts
18563
- var exports_init = {};
18564
- __export(exports_init, {
18565
- run: () => run5
18566
- });
18567
- import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18568
- import { join as join6 } from "node:path";
18569
- function ok(msg) {
18570
- console.log(` ${green2("✓")} ${msg}`);
18571
- }
18572
- function skip(msg) {
18573
- console.log(` ${yellow3("○")} ${msg}`);
18574
- }
18575
- function loadJson(path, fallback) {
18576
- if (!existsSync4(path))
18577
- return structuredClone(fallback);
18578
- try {
18579
- return JSON.parse(readFileSync(path, "utf-8"));
18580
- } catch {
18581
- return structuredClone(fallback);
18582
- }
18583
- }
18584
- function saveJson(path, value) {
18585
- writeFileSync(path, JSON.stringify(value, null, 2) + `
18586
- `, "utf-8");
18587
- }
18588
- function ensureProjectMcp(cwd) {
18589
- const mcpPath = join6(cwd, MCP_FILE);
18590
- const mcp = loadJson(mcpPath, { mcpServers: {} });
18591
- mcp.mcpServers ??= {};
18592
- const existing = mcp.mcpServers[MCP_SERVER_NAME];
18593
- if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
18594
- skip(".mcp.json already registers specialists");
18570
+ // src/specialist/supervisor.ts
18571
+ import {
18572
+ closeSync,
18573
+ existsSync as existsSync4,
18574
+ mkdirSync,
18575
+ openSync,
18576
+ readdirSync,
18577
+ readFileSync as readFileSync2,
18578
+ renameSync,
18579
+ rmSync,
18580
+ statSync,
18581
+ writeFileSync,
18582
+ writeSync
18583
+ } from "node:fs";
18584
+ import { join as join3 } from "node:path";
18585
+ import { createInterface } from "node:readline";
18586
+ import { createReadStream } from "node:fs";
18587
+ import { spawnSync as spawnSync3, execFileSync } from "node:child_process";
18588
+ function getCurrentGitSha() {
18589
+ const result = spawnSync3("git", ["rev-parse", "HEAD"], {
18590
+ encoding: "utf-8",
18591
+ stdio: ["ignore", "pipe", "ignore"]
18592
+ });
18593
+ if (result.status !== 0)
18595
18594
  return;
18596
- }
18597
- mcp.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
18598
- saveJson(mcpPath, mcp);
18599
- ok("registered specialists in project .mcp.json");
18595
+ const sha = result.stdout?.trim();
18596
+ return sha || undefined;
18600
18597
  }
18601
- async function run5() {
18602
- const cwd = process.cwd();
18603
- console.log(`
18604
- ${bold3("specialists init")}
18598
+ function formatBeadNotes(result) {
18599
+ const metadata = [
18600
+ `prompt_hash=${result.promptHash}`,
18601
+ `git_sha=${getCurrentGitSha() ?? "unknown"}`,
18602
+ `elapsed_ms=${Math.round(result.durationMs)}`,
18603
+ `model=${result.model}`,
18604
+ `backend=${result.backend}`
18605
+ ].join(`
18605
18606
  `);
18606
- const specialistsDir = join6(cwd, "specialists");
18607
- if (existsSync4(specialistsDir)) {
18608
- skip("specialists/ already exists");
18609
- } else {
18610
- mkdirSync(specialistsDir, { recursive: true });
18611
- ok("created specialists/");
18612
- }
18613
- const runtimeDir = join6(cwd, ".specialists");
18614
- if (existsSync4(runtimeDir)) {
18615
- skip(".specialists/ already exists");
18616
- } else {
18617
- mkdirSync(join6(runtimeDir, "jobs"), { recursive: true });
18618
- mkdirSync(join6(runtimeDir, "ready"), { recursive: true });
18619
- ok("created .specialists/ (jobs/, ready/)");
18620
- }
18621
- const gitignorePath = join6(cwd, ".gitignore");
18622
- if (existsSync4(gitignorePath)) {
18623
- const existing = readFileSync(gitignorePath, "utf-8");
18624
- if (existing.includes(GITIGNORE_ENTRY)) {
18625
- skip(".gitignore already has .specialists/ entry");
18626
- } else {
18627
- const separator = existing.endsWith(`
18628
- `) ? "" : `
18629
- `;
18630
- writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
18631
- `, "utf-8");
18632
- ok("added .specialists/ to .gitignore");
18633
- }
18634
- } else {
18635
- writeFileSync(gitignorePath, GITIGNORE_ENTRY + `
18636
- `, "utf-8");
18637
- ok("created .gitignore with .specialists/ entry");
18638
- }
18639
- const agentsPath = join6(cwd, "AGENTS.md");
18640
- if (existsSync4(agentsPath)) {
18641
- const existing = readFileSync(agentsPath, "utf-8");
18642
- if (existing.includes(AGENTS_MARKER)) {
18643
- skip("AGENTS.md already has Specialists section");
18644
- } else {
18645
- writeFileSync(agentsPath, existing.trimEnd() + `
18607
+ return `${result.output}
18646
18608
 
18647
- ` + AGENTS_BLOCK, "utf-8");
18648
- ok("appended Specialists section to AGENTS.md");
18649
- }
18650
- } else {
18651
- writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
18652
- ok("created AGENTS.md with Specialists section");
18653
- }
18654
- ensureProjectMcp(cwd);
18655
- console.log(`
18656
- ${bold3("Done!")}
18657
- `);
18658
- console.log(` ${dim3("Next steps:")}`);
18659
- console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
18660
- console.log(` 2. Run ${yellow3("specialists list")} to verify they are discovered`);
18661
- console.log(` 3. Restart Claude Code to pick up AGENTS.md / .mcp.json changes
18662
- `);
18609
+ ---
18610
+ ${metadata}`;
18663
18611
  }
18664
- var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/", MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
18665
- var init_init = __esm(() => {
18666
- AGENTS_BLOCK = `
18667
- ## Specialists
18668
18612
 
18669
- Call \`specialist_init\` at the start of every session to bootstrap context and
18670
- see available specialists. Use \`use_specialist\` or \`start_specialist\` to
18671
- delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
18672
- specialist without user intervention.
18673
- `.trimStart();
18674
- MCP_SERVER_CONFIG = { command: "specialists", args: [] };
18675
- });
18676
-
18677
- // src/cli/edit.ts
18678
- var exports_edit = {};
18679
- __export(exports_edit, {
18680
- run: () => run6
18681
- });
18682
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
18683
- function parseArgs3(argv) {
18684
- const name = argv[0];
18685
- if (!name || name.startsWith("--")) {
18686
- console.error("Usage: specialists edit <name> --<field> <value> [--dry-run]");
18687
- console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
18688
- process.exit(1);
18613
+ class Supervisor {
18614
+ opts;
18615
+ constructor(opts) {
18616
+ this.opts = opts;
18689
18617
  }
18690
- let field;
18691
- let value;
18692
- let dryRun = false;
18693
- let scope;
18694
- for (let i = 1;i < argv.length; i++) {
18695
- const token = argv[i];
18696
- if (token === "--dry-run") {
18697
- dryRun = true;
18698
- continue;
18699
- }
18700
- if (token === "--scope") {
18701
- const v = argv[++i];
18702
- if (v !== "project" && v !== "user") {
18703
- console.error(`Error: --scope must be "project" or "user", got: "${v ?? ""}"`);
18704
- process.exit(1);
18705
- }
18706
- scope = v;
18707
- continue;
18708
- }
18709
- if (token.startsWith("--") && !field) {
18710
- field = token.slice(2);
18711
- value = argv[++i];
18712
- continue;
18713
- }
18618
+ jobDir(id) {
18619
+ return join3(this.opts.jobsDir, id);
18714
18620
  }
18715
- if (!field || !FIELD_MAP[field]) {
18716
- console.error(`Error: unknown or missing field. Valid fields: ${Object.keys(FIELD_MAP).join(", ")}`);
18717
- process.exit(1);
18621
+ statusPath(id) {
18622
+ return join3(this.jobDir(id), "status.json");
18718
18623
  }
18719
- if (value === undefined || value === "") {
18720
- console.error(`Error: --${field} requires a value`);
18721
- process.exit(1);
18624
+ resultPath(id) {
18625
+ return join3(this.jobDir(id), "result.txt");
18722
18626
  }
18723
- if (field === "permission" && !VALID_PERMISSIONS.includes(value)) {
18724
- console.error(`Error: --permission must be one of: ${VALID_PERMISSIONS.join(", ")}`);
18725
- process.exit(1);
18627
+ eventsPath(id) {
18628
+ return join3(this.jobDir(id), "events.jsonl");
18726
18629
  }
18727
- if (field === "timeout" && !/^\d+$/.test(value)) {
18728
- console.error("Error: --timeout must be a number (milliseconds)");
18729
- process.exit(1);
18730
- }
18731
- return { name, field, value, dryRun, scope };
18732
- }
18733
- function setIn(doc2, path, value) {
18734
- let node = doc2;
18735
- for (let i = 0;i < path.length - 1; i++) {
18736
- node = node.get(path[i], true);
18737
- }
18738
- const leaf = path[path.length - 1];
18739
- if (Array.isArray(value)) {
18740
- node.set(leaf, value);
18741
- } else {
18742
- node.set(leaf, value);
18743
- }
18744
- }
18745
- async function run6() {
18746
- const args = parseArgs3(process.argv.slice(3));
18747
- const { name, field, value, dryRun, scope } = args;
18748
- const loader = new SpecialistLoader;
18749
- const all = await loader.list();
18750
- const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
18751
- if (!match) {
18752
- const hint = scope ? ` (scope: ${scope})` : "";
18753
- console.error(`Error: specialist "${name}" not found${hint}`);
18754
- console.error(` Run ${yellow4("specialists list")} to see available specialists`);
18755
- process.exit(1);
18756
- }
18757
- const raw = readFileSync2(match.filePath, "utf-8");
18758
- const doc2 = $parseDocument(raw);
18759
- const yamlPath = FIELD_MAP[field];
18760
- let typedValue = value;
18761
- if (field === "timeout") {
18762
- typedValue = parseInt(value, 10);
18763
- } else if (field === "tags") {
18764
- typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
18765
- }
18766
- setIn(doc2, yamlPath, typedValue);
18767
- const updated = doc2.toString();
18768
- if (dryRun) {
18769
- console.log(`
18770
- ${bold4(`[dry-run] ${match.filePath}`)}
18771
- `);
18772
- console.log(dim4("--- current"));
18773
- console.log(dim4(`+++ updated`));
18774
- const oldLines = raw.split(`
18775
- `);
18776
- const newLines = updated.split(`
18777
- `);
18778
- newLines.forEach((line, i) => {
18779
- if (line !== oldLines[i]) {
18780
- if (oldLines[i] !== undefined)
18781
- console.log(dim4(`- ${oldLines[i]}`));
18782
- console.log(green3(`+ ${line}`));
18783
- }
18784
- });
18785
- console.log();
18786
- return;
18787
- }
18788
- writeFileSync2(match.filePath, updated, "utf-8");
18789
- const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
18790
- console.log(`${green3("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
18791
- }
18792
- var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
18793
- var init_edit = __esm(() => {
18794
- init_dist();
18795
- init_loader();
18796
- FIELD_MAP = {
18797
- model: ["specialist", "execution", "model"],
18798
- "fallback-model": ["specialist", "execution", "fallback_model"],
18799
- description: ["specialist", "metadata", "description"],
18800
- permission: ["specialist", "execution", "permission_required"],
18801
- timeout: ["specialist", "execution", "timeout_ms"],
18802
- tags: ["specialist", "metadata", "tags"]
18803
- };
18804
- VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
18805
- });
18806
-
18807
- // src/specialist/timeline-events.ts
18808
- function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18809
- const t = Date.now();
18810
- switch (callbackEvent) {
18811
- case "thinking":
18812
- return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18813
- case "toolcall":
18814
- return {
18815
- t,
18816
- type: TIMELINE_EVENT_TYPES.TOOL,
18817
- tool: context.tool ?? "unknown",
18818
- phase: "start",
18819
- tool_call_id: context.toolCallId
18820
- };
18821
- case "tool_execution_end":
18822
- return {
18823
- t,
18824
- type: TIMELINE_EVENT_TYPES.TOOL,
18825
- tool: context.tool ?? "unknown",
18826
- phase: "end",
18827
- tool_call_id: context.toolCallId,
18828
- is_error: context.isError
18829
- };
18830
- case "text":
18831
- return { t, type: TIMELINE_EVENT_TYPES.TEXT };
18832
- case "agent_end":
18833
- case "message_done":
18834
- case "done":
18835
- return null;
18836
- default:
18837
- return null;
18838
- }
18839
- }
18840
- function createRunStartEvent(specialist, beadId) {
18841
- return {
18842
- t: Date.now(),
18843
- type: TIMELINE_EVENT_TYPES.RUN_START,
18844
- specialist,
18845
- bead_id: beadId
18846
- };
18847
- }
18848
- function createMetaEvent(model, backend) {
18849
- return {
18850
- t: Date.now(),
18851
- type: TIMELINE_EVENT_TYPES.META,
18852
- model,
18853
- backend
18854
- };
18855
- }
18856
- function createRunCompleteEvent(status, elapsed_s, options) {
18857
- return {
18858
- t: Date.now(),
18859
- type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
18860
- status,
18861
- elapsed_s,
18862
- ...options
18863
- };
18864
- }
18865
- function parseTimelineEvent(line) {
18866
- try {
18867
- const parsed = JSON.parse(line);
18868
- if (!parsed || typeof parsed !== "object")
18869
- return null;
18870
- if (typeof parsed.t !== "number")
18871
- return null;
18872
- if (typeof parsed.type !== "string")
18873
- return null;
18874
- if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
18875
- return {
18876
- t: parsed.t,
18877
- type: TIMELINE_EVENT_TYPES.DONE,
18878
- elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18879
- };
18880
- }
18881
- if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
18882
- return {
18883
- t: parsed.t,
18884
- type: TIMELINE_EVENT_TYPES.AGENT_END,
18885
- elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18886
- };
18887
- }
18888
- const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
18889
- if (!knownTypes.includes(parsed.type))
18890
- return null;
18891
- return parsed;
18892
- } catch {
18893
- return null;
18894
- }
18895
- }
18896
- function isRunCompleteEvent(event) {
18897
- return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
18898
- }
18899
- function compareTimelineEvents(a, b) {
18900
- return a.t - b.t;
18901
- }
18902
- var TIMELINE_EVENT_TYPES;
18903
- var init_timeline_events = __esm(() => {
18904
- TIMELINE_EVENT_TYPES = {
18905
- RUN_START: "run_start",
18906
- META: "meta",
18907
- THINKING: "thinking",
18908
- TOOL: "tool",
18909
- TEXT: "text",
18910
- RUN_COMPLETE: "run_complete",
18911
- DONE: "done",
18912
- AGENT_END: "agent_end"
18913
- };
18914
- });
18915
-
18916
- // src/specialist/supervisor.ts
18917
- import {
18918
- closeSync,
18919
- existsSync as existsSync5,
18920
- mkdirSync as mkdirSync2,
18921
- openSync,
18922
- readdirSync,
18923
- readFileSync as readFileSync3,
18924
- renameSync,
18925
- rmSync,
18926
- statSync,
18927
- writeFileSync as writeFileSync3,
18928
- writeSync
18929
- } from "node:fs";
18930
- import { join as join7 } from "node:path";
18931
- import { spawnSync as spawnSync4 } from "node:child_process";
18932
- function getCurrentGitSha() {
18933
- const result = spawnSync4("git", ["rev-parse", "HEAD"], {
18934
- encoding: "utf-8",
18935
- stdio: ["ignore", "pipe", "ignore"]
18936
- });
18937
- if (result.status !== 0)
18938
- return;
18939
- const sha = result.stdout?.trim();
18940
- return sha || undefined;
18941
- }
18942
- function formatBeadNotes(result) {
18943
- const metadata = [
18944
- `prompt_hash=${result.promptHash}`,
18945
- `git_sha=${getCurrentGitSha() ?? "unknown"}`,
18946
- `elapsed_ms=${Math.round(result.durationMs)}`,
18947
- `model=${result.model}`,
18948
- `backend=${result.backend}`
18949
- ].join(`
18950
- `);
18951
- return `${result.output}
18952
-
18953
- ---
18954
- ${metadata}`;
18955
- }
18956
-
18957
- class Supervisor {
18958
- opts;
18959
- constructor(opts) {
18960
- this.opts = opts;
18961
- }
18962
- jobDir(id) {
18963
- return join7(this.opts.jobsDir, id);
18964
- }
18965
- statusPath(id) {
18966
- return join7(this.jobDir(id), "status.json");
18967
- }
18968
- resultPath(id) {
18969
- return join7(this.jobDir(id), "result.txt");
18970
- }
18971
- eventsPath(id) {
18972
- return join7(this.jobDir(id), "events.jsonl");
18973
- }
18974
- readyDir() {
18975
- return join7(this.opts.jobsDir, "..", "ready");
18630
+ readyDir() {
18631
+ return join3(this.opts.jobsDir, "..", "ready");
18976
18632
  }
18977
18633
  readStatus(id) {
18978
18634
  const path = this.statusPath(id);
18979
- if (!existsSync5(path))
18635
+ if (!existsSync4(path))
18980
18636
  return null;
18981
18637
  try {
18982
- return JSON.parse(readFileSync3(path, "utf-8"));
18638
+ return JSON.parse(readFileSync2(path, "utf-8"));
18983
18639
  } catch {
18984
18640
  return null;
18985
18641
  }
18986
18642
  }
18987
18643
  listJobs() {
18988
- if (!existsSync5(this.opts.jobsDir))
18644
+ if (!existsSync4(this.opts.jobsDir))
18989
18645
  return [];
18990
18646
  const jobs = [];
18991
18647
  for (const entry of readdirSync(this.opts.jobsDir)) {
18992
- const path = join7(this.opts.jobsDir, entry, "status.json");
18993
- if (!existsSync5(path))
18648
+ const path = join3(this.opts.jobsDir, entry, "status.json");
18649
+ if (!existsSync4(path))
18994
18650
  continue;
18995
18651
  try {
18996
- jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
18652
+ jobs.push(JSON.parse(readFileSync2(path, "utf-8")));
18997
18653
  } catch {}
18998
18654
  }
18999
18655
  return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
@@ -19001,7 +18657,7 @@ class Supervisor {
19001
18657
  writeStatusFile(id, data) {
19002
18658
  const path = this.statusPath(id);
19003
18659
  const tmp = path + ".tmp";
19004
- writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
18660
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
19005
18661
  renameSync(tmp, path);
19006
18662
  }
19007
18663
  updateStatus(id, updates) {
@@ -19011,11 +18667,11 @@ class Supervisor {
19011
18667
  this.writeStatusFile(id, { ...current, ...updates });
19012
18668
  }
19013
18669
  gc() {
19014
- if (!existsSync5(this.opts.jobsDir))
18670
+ if (!existsSync4(this.opts.jobsDir))
19015
18671
  return;
19016
18672
  const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
19017
18673
  for (const entry of readdirSync(this.opts.jobsDir)) {
19018
- const dir = join7(this.opts.jobsDir, entry);
18674
+ const dir = join3(this.opts.jobsDir, entry);
19019
18675
  try {
19020
18676
  const stat2 = statSync(dir);
19021
18677
  if (!stat2.isDirectory())
@@ -19026,14 +18682,14 @@ class Supervisor {
19026
18682
  }
19027
18683
  }
19028
18684
  crashRecovery() {
19029
- if (!existsSync5(this.opts.jobsDir))
18685
+ if (!existsSync4(this.opts.jobsDir))
19030
18686
  return;
19031
18687
  for (const entry of readdirSync(this.opts.jobsDir)) {
19032
- const statusPath = join7(this.opts.jobsDir, entry, "status.json");
19033
- if (!existsSync5(statusPath))
18688
+ const statusPath = join3(this.opts.jobsDir, entry, "status.json");
18689
+ if (!existsSync4(statusPath))
19034
18690
  continue;
19035
18691
  try {
19036
- const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
18692
+ const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
19037
18693
  if (s.status !== "running" && s.status !== "starting")
19038
18694
  continue;
19039
18695
  if (!s.pid)
@@ -19043,7 +18699,7 @@ class Supervisor {
19043
18699
  } catch {
19044
18700
  const tmp = statusPath + ".tmp";
19045
18701
  const updated = { ...s, status: "error", error: "Process crashed or was killed" };
19046
- writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
18702
+ writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
19047
18703
  renameSync(tmp, statusPath);
19048
18704
  }
19049
18705
  } catch {}
@@ -19056,8 +18712,8 @@ class Supervisor {
19056
18712
  const id = crypto.randomUUID().slice(0, 6);
19057
18713
  const dir = this.jobDir(id);
19058
18714
  const startedAtMs = Date.now();
19059
- mkdirSync2(dir, { recursive: true });
19060
- mkdirSync2(this.readyDir(), { recursive: true });
18715
+ mkdirSync(dir, { recursive: true });
18716
+ mkdirSync(this.readyDir(), { recursive: true });
19061
18717
  const initialStatus = {
19062
18718
  id,
19063
18719
  specialist: runOptions.name,
@@ -19074,10 +18730,18 @@ class Supervisor {
19074
18730
  } catch {}
19075
18731
  };
19076
18732
  appendTimelineEvent(createRunStartEvent(runOptions.name));
18733
+ const fifoPath = join3(dir, "steer.pipe");
18734
+ try {
18735
+ execFileSync("mkfifo", [fifoPath]);
18736
+ this.updateStatus(id, { fifo_path: fifoPath });
18737
+ } catch {}
19077
18738
  let textLogged = false;
19078
18739
  let currentTool = "";
19079
18740
  let currentToolCallId = "";
19080
18741
  let killFn;
18742
+ let steerFn;
18743
+ let resumeFn;
18744
+ let closeFn;
19081
18745
  const sigtermHandler = () => killFn?.();
19082
18746
  process.once("SIGTERM", sigtermHandler);
19083
18747
  try {
@@ -19112,9 +18776,44 @@ class Supervisor {
19112
18776
  killFn = fn;
19113
18777
  }, (beadId) => {
19114
18778
  this.updateStatus(id, { bead_id: beadId });
18779
+ }, (fn) => {
18780
+ steerFn = fn;
18781
+ if (existsSync4(fifoPath)) {
18782
+ const rl = createInterface({ input: createReadStream(fifoPath, { flags: "r+" }) });
18783
+ rl.on("line", (line) => {
18784
+ try {
18785
+ const parsed = JSON.parse(line);
18786
+ if (parsed?.type === "steer" && typeof parsed.message === "string") {
18787
+ steerFn?.(parsed.message).catch(() => {});
18788
+ } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
18789
+ if (resumeFn) {
18790
+ this.updateStatus(id, { status: "running", current_event: "starting" });
18791
+ resumeFn(parsed.message).then((output) => {
18792
+ writeFileSync(this.resultPath(id), output, "utf-8");
18793
+ this.updateStatus(id, {
18794
+ status: "waiting",
18795
+ current_event: "waiting",
18796
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
18797
+ last_event_at_ms: Date.now()
18798
+ });
18799
+ }).catch((err) => {
18800
+ this.updateStatus(id, { status: "error", error: err?.message ?? String(err) });
18801
+ });
18802
+ }
18803
+ } else if (parsed?.type === "close") {
18804
+ closeFn?.().catch(() => {});
18805
+ }
18806
+ } catch {}
18807
+ });
18808
+ rl.on("error", () => {});
18809
+ }
18810
+ }, (rFn, cFn) => {
18811
+ resumeFn = rFn;
18812
+ closeFn = cFn;
18813
+ this.updateStatus(id, { status: "waiting", current_event: "waiting" });
19115
18814
  });
19116
18815
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
19117
- writeFileSync3(this.resultPath(id), result.output, "utf-8");
18816
+ writeFileSync(this.resultPath(id), result.output, "utf-8");
19118
18817
  if (result.beadId) {
19119
18818
  this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19120
18819
  }
@@ -19131,7 +18830,7 @@ class Supervisor {
19131
18830
  backend: result.backend,
19132
18831
  bead_id: result.beadId
19133
18832
  }));
19134
- writeFileSync3(join7(this.readyDir(), id), "", "utf-8");
18833
+ writeFileSync(join3(this.readyDir(), id), "", "utf-8");
19135
18834
  return id;
19136
18835
  } catch (err) {
19137
18836
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
@@ -19148,13 +18847,663 @@ class Supervisor {
19148
18847
  } finally {
19149
18848
  process.removeListener("SIGTERM", sigtermHandler);
19150
18849
  closeSync(eventsFd);
18850
+ try {
18851
+ if (existsSync4(fifoPath))
18852
+ rmSync(fifoPath);
18853
+ } catch {}
19151
18854
  }
19152
18855
  }
19153
18856
  }
19154
- var JOB_TTL_DAYS;
19155
- var init_supervisor = __esm(() => {
19156
- init_timeline_events();
19157
- JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18857
+ var JOB_TTL_DAYS;
18858
+ var init_supervisor = __esm(() => {
18859
+ init_timeline_events();
18860
+ JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
18861
+ });
18862
+
18863
+ // src/cli/install.ts
18864
+ var exports_install = {};
18865
+ __export(exports_install, {
18866
+ run: () => run
18867
+ });
18868
+ import { execFileSync as execFileSync2 } from "node:child_process";
18869
+ import { fileURLToPath } from "node:url";
18870
+ import { dirname as dirname2, join as join8 } from "node:path";
18871
+ async function run() {
18872
+ const installerPath = join8(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
18873
+ execFileSync2(process.execPath, [installerPath], { stdio: "inherit" });
18874
+ }
18875
+ var init_install = () => {};
18876
+
18877
+ // src/cli/version.ts
18878
+ var exports_version = {};
18879
+ __export(exports_version, {
18880
+ run: () => run2
18881
+ });
18882
+ import { createRequire as createRequire2 } from "node:module";
18883
+ async function run2() {
18884
+ const req = createRequire2(import.meta.url);
18885
+ const pkg = req("../package.json");
18886
+ console.log(`${pkg.name} v${pkg.version}`);
18887
+ }
18888
+ var init_version = () => {};
18889
+
18890
+ // src/cli/list.ts
18891
+ var exports_list = {};
18892
+ __export(exports_list, {
18893
+ run: () => run3,
18894
+ parseArgs: () => parseArgs,
18895
+ ArgParseError: () => ArgParseError
18896
+ });
18897
+ function parseArgs(argv) {
18898
+ const result = {};
18899
+ for (let i = 0;i < argv.length; i++) {
18900
+ const token = argv[i];
18901
+ if (token === "--category") {
18902
+ const value = argv[++i];
18903
+ if (!value || value.startsWith("--")) {
18904
+ throw new ArgParseError("--category requires a value");
18905
+ }
18906
+ result.category = value;
18907
+ continue;
18908
+ }
18909
+ if (token === "--scope") {
18910
+ const value = argv[++i];
18911
+ if (value !== "default" && value !== "user") {
18912
+ throw new ArgParseError(`--scope must be "default" or "user", got: "${value ?? ""}"`);
18913
+ }
18914
+ result.scope = value;
18915
+ continue;
18916
+ }
18917
+ if (token === "--json") {
18918
+ result.json = true;
18919
+ continue;
18920
+ }
18921
+ }
18922
+ return result;
18923
+ }
18924
+ async function run3() {
18925
+ let args;
18926
+ try {
18927
+ args = parseArgs(process.argv.slice(3));
18928
+ } catch (err) {
18929
+ if (err instanceof ArgParseError) {
18930
+ console.error(`Error: ${err.message}`);
18931
+ process.exit(1);
18932
+ }
18933
+ throw err;
18934
+ }
18935
+ const loader = new SpecialistLoader;
18936
+ let specialists = await loader.list(args.category);
18937
+ if (args.scope) {
18938
+ specialists = specialists.filter((s) => s.scope === args.scope);
18939
+ }
18940
+ if (args.json) {
18941
+ console.log(JSON.stringify(specialists, null, 2));
18942
+ return;
18943
+ }
18944
+ if (specialists.length === 0) {
18945
+ console.log("No specialists found.");
18946
+ return;
18947
+ }
18948
+ const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
18949
+ console.log(`
18950
+ ${bold(`Specialists (${specialists.length})`)}
18951
+ `);
18952
+ for (const s of specialists) {
18953
+ const name = cyan(s.name.padEnd(nameWidth));
18954
+ const scopeTag = s.scope === "default" ? green("[default]") : yellow("[user]");
18955
+ const model = dim(s.model);
18956
+ const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
18957
+ console.log(` ${name} ${scopeTag} ${model}`);
18958
+ console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
18959
+ console.log();
18960
+ }
18961
+ }
18962
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
18963
+ var init_list = __esm(() => {
18964
+ init_loader();
18965
+ ArgParseError = class ArgParseError extends Error {
18966
+ constructor(message) {
18967
+ super(message);
18968
+ this.name = "ArgParseError";
18969
+ }
18970
+ };
18971
+ });
18972
+
18973
+ // src/cli/models.ts
18974
+ var exports_models = {};
18975
+ __export(exports_models, {
18976
+ run: () => run4
18977
+ });
18978
+ import { spawnSync as spawnSync5 } from "node:child_process";
18979
+ function parsePiModels() {
18980
+ const r = spawnSync5("pi", ["--list-models"], {
18981
+ encoding: "utf8",
18982
+ stdio: "pipe",
18983
+ timeout: 8000
18984
+ });
18985
+ if (r.status !== 0 || r.error)
18986
+ return null;
18987
+ return r.stdout.split(`
18988
+ `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
18989
+ const cols = line.split(/\s+/);
18990
+ return {
18991
+ provider: cols[0] ?? "",
18992
+ model: cols[1] ?? "",
18993
+ context: cols[2] ?? "",
18994
+ maxOut: cols[3] ?? "",
18995
+ thinking: cols[4] === "yes",
18996
+ images: cols[5] === "yes"
18997
+ };
18998
+ }).filter((m) => m.provider && m.model);
18999
+ }
19000
+ function parseArgs2(argv) {
19001
+ const out = {};
19002
+ for (let i = 0;i < argv.length; i++) {
19003
+ if (argv[i] === "--provider" && argv[i + 1]) {
19004
+ out.provider = argv[++i];
19005
+ continue;
19006
+ }
19007
+ if (argv[i] === "--used") {
19008
+ out.used = true;
19009
+ continue;
19010
+ }
19011
+ }
19012
+ return out;
19013
+ }
19014
+ async function run4() {
19015
+ const args = parseArgs2(process.argv.slice(3));
19016
+ const loader = new SpecialistLoader;
19017
+ const specialists = await loader.list();
19018
+ const usedBy = new Map;
19019
+ for (const s of specialists) {
19020
+ const key = s.model;
19021
+ if (!usedBy.has(key))
19022
+ usedBy.set(key, []);
19023
+ usedBy.get(key).push(s.name);
19024
+ }
19025
+ const allModels = parsePiModels();
19026
+ if (!allModels) {
19027
+ console.error("pi not found or failed — install and configure pi first");
19028
+ process.exit(1);
19029
+ }
19030
+ let models = allModels;
19031
+ if (args.provider) {
19032
+ models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
19033
+ }
19034
+ if (args.used) {
19035
+ models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
19036
+ }
19037
+ if (models.length === 0) {
19038
+ console.log("No models match.");
19039
+ return;
19040
+ }
19041
+ const byProvider = new Map;
19042
+ for (const m of models) {
19043
+ if (!byProvider.has(m.provider))
19044
+ byProvider.set(m.provider, []);
19045
+ byProvider.get(m.provider).push(m);
19046
+ }
19047
+ const total = models.length;
19048
+ console.log(`
19049
+ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
19050
+ `);
19051
+ for (const [provider, pModels] of byProvider) {
19052
+ console.log(` ${cyan2(provider)} ${dim2(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
19053
+ const modelWidth = Math.max(...pModels.map((m) => m.model.length));
19054
+ for (const m of pModels) {
19055
+ const key = `${m.provider}/${m.model}`;
19056
+ const inUse = usedBy.get(key);
19057
+ const flags = [
19058
+ m.thinking ? green2("thinking") : dim2("·"),
19059
+ m.images ? dim2("images") : ""
19060
+ ].filter(Boolean).join(" ");
19061
+ const ctx = dim2(`ctx ${m.context}`);
19062
+ const usedLabel = inUse ? ` ${yellow2("←")} ${dim2(inUse.join(", "))}` : "";
19063
+ console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
19064
+ }
19065
+ console.log();
19066
+ }
19067
+ if (!args.used) {
19068
+ console.log(dim2(` --provider <name> filter by provider`));
19069
+ console.log(dim2(` --used only show models used by your specialists`));
19070
+ console.log();
19071
+ }
19072
+ }
19073
+ var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`;
19074
+ var init_models = __esm(() => {
19075
+ init_loader();
19076
+ });
19077
+
19078
+ // src/cli/init.ts
19079
+ var exports_init = {};
19080
+ __export(exports_init, {
19081
+ run: () => run5
19082
+ });
19083
+ import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
19084
+ import { join as join9 } from "node:path";
19085
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19086
+ function ok(msg) {
19087
+ console.log(` ${green3("✓")} ${msg}`);
19088
+ }
19089
+ function skip(msg) {
19090
+ console.log(` ${yellow3("○")} ${msg}`);
19091
+ }
19092
+ function loadJson(path, fallback) {
19093
+ if (!existsSync6(path))
19094
+ return structuredClone(fallback);
19095
+ try {
19096
+ return JSON.parse(readFileSync3(path, "utf-8"));
19097
+ } catch {
19098
+ return structuredClone(fallback);
19099
+ }
19100
+ }
19101
+ function saveJson(path, value) {
19102
+ writeFileSync4(path, JSON.stringify(value, null, 2) + `
19103
+ `, "utf-8");
19104
+ }
19105
+ function resolvePackagePath(relativePath) {
19106
+ const configPath = `config/${relativePath}`;
19107
+ let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19108
+ if (existsSync6(resolved))
19109
+ return resolved;
19110
+ resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19111
+ if (existsSync6(resolved))
19112
+ return resolved;
19113
+ return null;
19114
+ }
19115
+ function copyCanonicalSpecialists(cwd) {
19116
+ const sourceDir = resolvePackagePath("specialists");
19117
+ if (!sourceDir) {
19118
+ skip("no canonical specialists found in package");
19119
+ return;
19120
+ }
19121
+ const targetDir = join9(cwd, ".specialists", "default", "specialists");
19122
+ const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19123
+ if (files.length === 0) {
19124
+ skip("no specialist files found in package");
19125
+ return;
19126
+ }
19127
+ if (!existsSync6(targetDir)) {
19128
+ mkdirSync2(targetDir, { recursive: true });
19129
+ }
19130
+ let copied = 0;
19131
+ let skipped = 0;
19132
+ for (const file of files) {
19133
+ const src = join9(sourceDir, file);
19134
+ const dest = join9(targetDir, file);
19135
+ if (existsSync6(dest)) {
19136
+ skipped++;
19137
+ } else {
19138
+ copyFileSync(src, dest);
19139
+ copied++;
19140
+ }
19141
+ }
19142
+ if (copied > 0) {
19143
+ ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/specialists/`);
19144
+ }
19145
+ if (skipped > 0) {
19146
+ skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19147
+ }
19148
+ }
19149
+ function copyCanonicalHooks(cwd) {
19150
+ const sourceDir = resolvePackagePath("hooks");
19151
+ if (!sourceDir) {
19152
+ skip("no canonical hooks found in package");
19153
+ return;
19154
+ }
19155
+ const targetDir = join9(cwd, ".specialists", "default", "hooks");
19156
+ const hooks = readdirSync2(sourceDir).filter((f) => f.endsWith(".mjs"));
19157
+ if (hooks.length === 0) {
19158
+ skip("no hook files found in package");
19159
+ return;
19160
+ }
19161
+ if (!existsSync6(targetDir)) {
19162
+ mkdirSync2(targetDir, { recursive: true });
19163
+ }
19164
+ let copied = 0;
19165
+ let skipped = 0;
19166
+ for (const file of hooks) {
19167
+ const src = join9(sourceDir, file);
19168
+ const dest = join9(targetDir, file);
19169
+ if (existsSync6(dest)) {
19170
+ skipped++;
19171
+ } else {
19172
+ copyFileSync(src, dest);
19173
+ copied++;
19174
+ }
19175
+ }
19176
+ if (copied > 0) {
19177
+ ok(`copied ${copied} hook${copied === 1 ? "" : "s"} to .specialists/default/hooks/`);
19178
+ }
19179
+ if (skipped > 0) {
19180
+ skip(`${skipped} hook${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19181
+ }
19182
+ }
19183
+ function ensureProjectHooks(cwd) {
19184
+ const settingsPath = join9(cwd, ".claude", "settings.json");
19185
+ const settingsDir = join9(cwd, ".claude");
19186
+ if (!existsSync6(settingsDir)) {
19187
+ mkdirSync2(settingsDir, { recursive: true });
19188
+ }
19189
+ const settings = loadJson(settingsPath, {});
19190
+ let changed = false;
19191
+ function addHook(event, command) {
19192
+ const eventList = settings[event] ?? [];
19193
+ settings[event] = eventList;
19194
+ const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
19195
+ if (!alreadyWired) {
19196
+ eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
19197
+ changed = true;
19198
+ }
19199
+ }
19200
+ addHook("UserPromptSubmit", "node .specialists/default/hooks/specialists-complete.mjs");
19201
+ addHook("SessionStart", "node .specialists/default/hooks/specialists-session-start.mjs");
19202
+ if (changed) {
19203
+ saveJson(settingsPath, settings);
19204
+ ok("wired specialists hooks in .claude/settings.json");
19205
+ } else {
19206
+ skip(".claude/settings.json already has specialists hooks");
19207
+ }
19208
+ }
19209
+ function copyCanonicalSkills(cwd) {
19210
+ const sourceDir = resolvePackagePath("skills");
19211
+ if (!sourceDir) {
19212
+ skip("no canonical skills found in package");
19213
+ return;
19214
+ }
19215
+ const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19216
+ if (skills.length === 0) {
19217
+ skip("no skill directories found in package");
19218
+ return;
19219
+ }
19220
+ const targetDir = join9(cwd, ".specialists", "default", "skills");
19221
+ if (!existsSync6(targetDir)) {
19222
+ mkdirSync2(targetDir, { recursive: true });
19223
+ }
19224
+ let copied = 0;
19225
+ let skipped = 0;
19226
+ for (const skill of skills) {
19227
+ const src = join9(sourceDir, skill);
19228
+ const dest = join9(targetDir, skill);
19229
+ if (existsSync6(dest)) {
19230
+ skipped++;
19231
+ } else {
19232
+ cpSync(src, dest, { recursive: true });
19233
+ copied++;
19234
+ }
19235
+ }
19236
+ if (copied > 0) {
19237
+ ok(`copied ${copied} skill${copied === 1 ? "" : "s"} to .specialists/default/skills/`);
19238
+ }
19239
+ if (skipped > 0) {
19240
+ skip(`${skipped} skill${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19241
+ }
19242
+ }
19243
+ function createUserDirs(cwd) {
19244
+ const userDirs = [
19245
+ join9(cwd, ".specialists", "user", "specialists"),
19246
+ join9(cwd, ".specialists", "user", "hooks"),
19247
+ join9(cwd, ".specialists", "user", "skills")
19248
+ ];
19249
+ let created = 0;
19250
+ for (const dir of userDirs) {
19251
+ if (!existsSync6(dir)) {
19252
+ mkdirSync2(dir, { recursive: true });
19253
+ created++;
19254
+ }
19255
+ }
19256
+ if (created > 0) {
19257
+ ok("created .specialists/user/ directories for custom assets");
19258
+ }
19259
+ }
19260
+ function createRuntimeDirs(cwd) {
19261
+ const runtimeDirs = [
19262
+ join9(cwd, ".specialists", "jobs"),
19263
+ join9(cwd, ".specialists", "ready")
19264
+ ];
19265
+ let created = 0;
19266
+ for (const dir of runtimeDirs) {
19267
+ if (!existsSync6(dir)) {
19268
+ mkdirSync2(dir, { recursive: true });
19269
+ created++;
19270
+ }
19271
+ }
19272
+ if (created > 0) {
19273
+ ok("created .specialists/jobs/ and .specialists/ready/");
19274
+ }
19275
+ }
19276
+ function ensureProjectMcp(cwd) {
19277
+ const mcpPath = join9(cwd, MCP_FILE);
19278
+ const mcp = loadJson(mcpPath, { mcpServers: {} });
19279
+ mcp.mcpServers ??= {};
19280
+ const existing = mcp.mcpServers[MCP_SERVER_NAME];
19281
+ if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
19282
+ skip(".mcp.json already registers specialists");
19283
+ return;
19284
+ }
19285
+ mcp.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
19286
+ saveJson(mcpPath, mcp);
19287
+ ok("registered specialists in project .mcp.json");
19288
+ }
19289
+ function ensureGitignore(cwd) {
19290
+ const gitignorePath = join9(cwd, ".gitignore");
19291
+ const existing = existsSync6(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
19292
+ let added = 0;
19293
+ const lines = existing.split(`
19294
+ `);
19295
+ for (const entry of GITIGNORE_ENTRIES) {
19296
+ if (!lines.includes(entry)) {
19297
+ lines.push(entry);
19298
+ added++;
19299
+ }
19300
+ }
19301
+ if (added > 0) {
19302
+ writeFileSync4(gitignorePath, lines.join(`
19303
+ `) + `
19304
+ `, "utf-8");
19305
+ ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
19306
+ } else {
19307
+ skip(".gitignore already has runtime entries");
19308
+ }
19309
+ }
19310
+ function ensureAgentsMd(cwd) {
19311
+ const agentsPath = join9(cwd, "AGENTS.md");
19312
+ if (existsSync6(agentsPath)) {
19313
+ const existing = readFileSync3(agentsPath, "utf-8");
19314
+ if (existing.includes(AGENTS_MARKER)) {
19315
+ skip("AGENTS.md already has Specialists section");
19316
+ } else {
19317
+ writeFileSync4(agentsPath, existing.trimEnd() + `
19318
+
19319
+ ` + AGENTS_BLOCK, "utf-8");
19320
+ ok("appended Specialists section to AGENTS.md");
19321
+ }
19322
+ } else {
19323
+ writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
19324
+ ok("created AGENTS.md with Specialists section");
19325
+ }
19326
+ }
19327
+ async function run5() {
19328
+ const cwd = process.cwd();
19329
+ console.log(`
19330
+ ${bold3("specialists init")}
19331
+ `);
19332
+ copyCanonicalSpecialists(cwd);
19333
+ copyCanonicalHooks(cwd);
19334
+ copyCanonicalSkills(cwd);
19335
+ createUserDirs(cwd);
19336
+ createRuntimeDirs(cwd);
19337
+ ensureGitignore(cwd);
19338
+ ensureAgentsMd(cwd);
19339
+ ensureProjectMcp(cwd);
19340
+ ensureProjectHooks(cwd);
19341
+ console.log(`
19342
+ ${bold3("Done!")}
19343
+ `);
19344
+ console.log(` ${dim3("Directory structure:")}`);
19345
+ console.log(` .specialists/`);
19346
+ console.log(` ├── default/ ${dim3("# canonical assets (from init)")}`);
19347
+ console.log(` │ ├── specialists/`);
19348
+ console.log(` │ ├── hooks/`);
19349
+ console.log(` │ └── skills/`);
19350
+ console.log(` ├── user/ ${dim3("# your custom additions")}`);
19351
+ console.log(` │ ├── specialists/`);
19352
+ console.log(` │ ├── hooks/`);
19353
+ console.log(` │ └── skills/`);
19354
+ console.log(` ├── jobs/ ${dim3("# runtime (gitignored)")}`);
19355
+ console.log(` └── ready/ ${dim3("# runtime (gitignored)")}`);
19356
+ console.log(`
19357
+ ${dim3("Next steps:")}`);
19358
+ console.log(` 1. Run ${yellow3("specialists list")} to see available specialists`);
19359
+ console.log(` 2. Add custom specialists to ${yellow3(".specialists/user/specialists/")}`);
19360
+ console.log(` 3. Restart Claude Code to pick up changes
19361
+ `);
19362
+ }
19363
+ var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRIES, MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
19364
+ var init_init = __esm(() => {
19365
+ AGENTS_BLOCK = `
19366
+ ## Specialists
19367
+
19368
+ Call \`specialist_init\` at the start of every session to bootstrap context and
19369
+ see available specialists. Use \`use_specialist\` or \`start_specialist\` to
19370
+ delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
19371
+ specialist without user intervention.
19372
+
19373
+ Add custom specialists to \`.specialists/user/specialists/\` to extend the defaults.
19374
+ `.trimStart();
19375
+ GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
19376
+ MCP_SERVER_CONFIG = { command: "specialists", args: [] };
19377
+ });
19378
+
19379
+ // src/cli/edit.ts
19380
+ var exports_edit = {};
19381
+ __export(exports_edit, {
19382
+ run: () => run6
19383
+ });
19384
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
19385
+ function parseArgs3(argv) {
19386
+ const name = argv[0];
19387
+ if (!name || name.startsWith("--")) {
19388
+ console.error("Usage: specialists|sp edit <name> --<field> <value> [--dry-run]");
19389
+ console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
19390
+ process.exit(1);
19391
+ }
19392
+ let field;
19393
+ let value;
19394
+ let dryRun = false;
19395
+ let scope;
19396
+ for (let i = 1;i < argv.length; i++) {
19397
+ const token = argv[i];
19398
+ if (token === "--dry-run") {
19399
+ dryRun = true;
19400
+ continue;
19401
+ }
19402
+ if (token === "--scope") {
19403
+ const v = argv[++i];
19404
+ if (v !== "project" && v !== "user") {
19405
+ console.error(`Error: --scope must be "project" or "user", got: "${v ?? ""}"`);
19406
+ process.exit(1);
19407
+ }
19408
+ scope = v;
19409
+ continue;
19410
+ }
19411
+ if (token.startsWith("--") && !field) {
19412
+ field = token.slice(2);
19413
+ value = argv[++i];
19414
+ continue;
19415
+ }
19416
+ }
19417
+ if (!field || !FIELD_MAP[field]) {
19418
+ console.error(`Error: unknown or missing field. Valid fields: ${Object.keys(FIELD_MAP).join(", ")}`);
19419
+ process.exit(1);
19420
+ }
19421
+ if (value === undefined || value === "") {
19422
+ console.error(`Error: --${field} requires a value`);
19423
+ process.exit(1);
19424
+ }
19425
+ if (field === "permission" && !VALID_PERMISSIONS.includes(value)) {
19426
+ console.error(`Error: --permission must be one of: ${VALID_PERMISSIONS.join(", ")}`);
19427
+ process.exit(1);
19428
+ }
19429
+ if (field === "timeout" && !/^\d+$/.test(value)) {
19430
+ console.error("Error: --timeout must be a number (milliseconds)");
19431
+ process.exit(1);
19432
+ }
19433
+ return { name, field, value, dryRun, scope };
19434
+ }
19435
+ function setIn(doc2, path, value) {
19436
+ let node = doc2;
19437
+ for (let i = 0;i < path.length - 1; i++) {
19438
+ node = node.get(path[i], true);
19439
+ }
19440
+ const leaf = path[path.length - 1];
19441
+ if (Array.isArray(value)) {
19442
+ node.set(leaf, value);
19443
+ } else {
19444
+ node.set(leaf, value);
19445
+ }
19446
+ }
19447
+ async function run6() {
19448
+ const args = parseArgs3(process.argv.slice(3));
19449
+ const { name, field, value, dryRun, scope } = args;
19450
+ const loader = new SpecialistLoader;
19451
+ const all = await loader.list();
19452
+ const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
19453
+ if (!match) {
19454
+ const hint = scope ? ` (scope: ${scope})` : "";
19455
+ console.error(`Error: specialist "${name}" not found${hint}`);
19456
+ console.error(` Run ${yellow4("specialists list")} to see available specialists`);
19457
+ process.exit(1);
19458
+ }
19459
+ const raw = readFileSync4(match.filePath, "utf-8");
19460
+ const doc2 = $parseDocument(raw);
19461
+ const yamlPath = FIELD_MAP[field];
19462
+ let typedValue = value;
19463
+ if (field === "timeout") {
19464
+ typedValue = parseInt(value, 10);
19465
+ } else if (field === "tags") {
19466
+ typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
19467
+ }
19468
+ setIn(doc2, yamlPath, typedValue);
19469
+ const updated = doc2.toString();
19470
+ if (dryRun) {
19471
+ console.log(`
19472
+ ${bold4(`[dry-run] ${match.filePath}`)}
19473
+ `);
19474
+ console.log(dim4("--- current"));
19475
+ console.log(dim4(`+++ updated`));
19476
+ const oldLines = raw.split(`
19477
+ `);
19478
+ const newLines = updated.split(`
19479
+ `);
19480
+ newLines.forEach((line, i) => {
19481
+ if (line !== oldLines[i]) {
19482
+ if (oldLines[i] !== undefined)
19483
+ console.log(dim4(`- ${oldLines[i]}`));
19484
+ console.log(green4(`+ ${line}`));
19485
+ }
19486
+ });
19487
+ console.log();
19488
+ return;
19489
+ }
19490
+ writeFileSync5(match.filePath, updated, "utf-8");
19491
+ const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
19492
+ console.log(`${green4("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
19493
+ }
19494
+ var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
19495
+ var init_edit = __esm(() => {
19496
+ init_dist();
19497
+ init_loader();
19498
+ FIELD_MAP = {
19499
+ model: ["specialist", "execution", "model"],
19500
+ "fallback-model": ["specialist", "execution", "fallback_model"],
19501
+ description: ["specialist", "metadata", "description"],
19502
+ permission: ["specialist", "execution", "permission_required"],
19503
+ timeout: ["specialist", "execution", "timeout_ms"],
19504
+ tags: ["specialist", "metadata", "tags"]
19505
+ };
19506
+ VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
19158
19507
  });
19159
19508
 
19160
19509
  // src/cli/run.ts
@@ -19162,11 +19511,12 @@ var exports_run = {};
19162
19511
  __export(exports_run, {
19163
19512
  run: () => run7
19164
19513
  });
19165
- import { join as join8 } from "node:path";
19514
+ import { spawn as spawn2 } from "node:child_process";
19515
+ import { join as join10 } from "node:path";
19166
19516
  async function parseArgs4(argv) {
19167
19517
  const name = argv[0];
19168
19518
  if (!name || name.startsWith("--")) {
19169
- console.error('Usage: specialists run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--background]');
19519
+ console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--background] [--follow]');
19170
19520
  process.exit(1);
19171
19521
  }
19172
19522
  let prompt = "";
@@ -19174,6 +19524,8 @@ async function parseArgs4(argv) {
19174
19524
  let model;
19175
19525
  let noBeads = false;
19176
19526
  let background = false;
19527
+ let follow = false;
19528
+ let keepAlive = false;
19177
19529
  let contextDepth = 1;
19178
19530
  for (let i = 1;i < argv.length; i++) {
19179
19531
  const token = argv[i];
@@ -19201,32 +19553,40 @@ async function parseArgs4(argv) {
19201
19553
  background = true;
19202
19554
  continue;
19203
19555
  }
19556
+ if (token === "--follow") {
19557
+ follow = true;
19558
+ continue;
19559
+ }
19560
+ if (token === "--keep-alive") {
19561
+ keepAlive = true;
19562
+ continue;
19563
+ }
19204
19564
  }
19205
19565
  if (prompt && beadId) {
19206
19566
  console.error("Error: use either --prompt or --bead, not both.");
19207
19567
  process.exit(1);
19208
19568
  }
19209
19569
  if (!prompt && !beadId && !process.stdin.isTTY) {
19210
- prompt = await new Promise((resolve) => {
19570
+ prompt = await new Promise((resolve2) => {
19211
19571
  let buf = "";
19212
19572
  process.stdin.setEncoding("utf-8");
19213
19573
  process.stdin.on("data", (chunk) => {
19214
19574
  buf += chunk;
19215
19575
  });
19216
- process.stdin.on("end", () => resolve(buf.trim()));
19576
+ process.stdin.on("end", () => resolve2(buf.trim()));
19217
19577
  });
19218
19578
  }
19219
19579
  if (!prompt && !beadId) {
19220
19580
  console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
19221
19581
  process.exit(1);
19222
19582
  }
19223
- return { name, prompt, beadId, model, noBeads, background, contextDepth };
19583
+ return { name, prompt, beadId, model, noBeads, background, follow, keepAlive, contextDepth };
19224
19584
  }
19225
19585
  async function run7() {
19226
19586
  const args = await parseArgs4(process.argv.slice(3));
19227
19587
  const loader = new SpecialistLoader;
19228
19588
  const circuitBreaker = new CircuitBreaker;
19229
- const hooks = new HookEmitter({ tracePath: join8(process.cwd(), ".specialists", "trace.jsonl") });
19589
+ const hooks = new HookEmitter({ tracePath: join10(process.cwd(), ".specialists", "trace.jsonl") });
19230
19590
  const beadsClient = args.noBeads ? undefined : new BeadsClient;
19231
19591
  const beadReader = beadsClient ?? new BeadsClient;
19232
19592
  let prompt = args.prompt;
@@ -19255,8 +19615,8 @@ async function run7() {
19255
19615
  circuitBreaker,
19256
19616
  beadsClient
19257
19617
  });
19258
- if (args.background) {
19259
- const jobsDir = join8(process.cwd(), ".specialists", "jobs");
19618
+ if (args.background || args.follow) {
19619
+ const jobsDir = join10(process.cwd(), ".specialists", "jobs");
19260
19620
  const supervisor = new Supervisor({
19261
19621
  runner,
19262
19622
  runOptions: {
@@ -19264,20 +19624,46 @@ async function run7() {
19264
19624
  prompt,
19265
19625
  variables,
19266
19626
  backendOverride: args.model,
19267
- inputBeadId: args.beadId
19627
+ inputBeadId: args.beadId,
19628
+ keepAlive: args.keepAlive
19268
19629
  },
19269
19630
  jobsDir,
19270
19631
  beadsClient
19271
19632
  });
19633
+ let jobId;
19272
19634
  try {
19273
- const jobId = await supervisor.run();
19274
- process.stdout.write(`Job started: ${jobId}
19635
+ jobId = await supervisor.run();
19636
+ if (!args.follow) {
19637
+ process.stdout.write(`Job started: ${jobId}
19275
19638
  `);
19639
+ }
19276
19640
  } catch (err) {
19277
19641
  process.stderr.write(`Error: ${err?.message ?? err}
19278
19642
  `);
19279
19643
  process.exit(1);
19280
19644
  }
19645
+ if (args.follow) {
19646
+ await new Promise((resolve2, reject) => {
19647
+ const feed = spawn2("specialists", ["feed", "--job", jobId, "--follow"], {
19648
+ cwd: process.cwd(),
19649
+ stdio: "inherit"
19650
+ });
19651
+ feed.on("close", (code) => {
19652
+ if (code === 0) {
19653
+ resolve2();
19654
+ } else {
19655
+ reject(new Error(`Feed exited with code ${code}`));
19656
+ }
19657
+ });
19658
+ feed.on("error", (err) => {
19659
+ reject(err);
19660
+ });
19661
+ }).catch((err) => {
19662
+ process.stderr.write(`Error: ${err.message}
19663
+ `);
19664
+ process.exit(1);
19665
+ });
19666
+ }
19281
19667
  return;
19282
19668
  }
19283
19669
  process.stderr.write(`
@@ -19321,11 +19707,11 @@ Interrupted.
19321
19707
  dim5(result.model)
19322
19708
  ].filter(Boolean).join(" ");
19323
19709
  process.stderr.write(`
19324
- ${green4("✓")} ${footer}
19710
+ ${green5("✓")} ${footer}
19325
19711
 
19326
19712
  `);
19327
19713
  }
19328
- var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, cyan3 = (s) => `\x1B[36m${s}\x1B[0m`;
19714
+ var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, cyan3 = (s) => `\x1B[36m${s}\x1B[0m`;
19329
19715
  var init_run = __esm(() => {
19330
19716
  init_loader();
19331
19717
  init_runner();
@@ -19407,9 +19793,9 @@ function formatEventLine(event, options) {
19407
19793
  const detail = detailParts.length > 0 ? dim6(detailParts.join(" ")) : "";
19408
19794
  return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
19409
19795
  }
19410
- var dim6 = (s) => `\x1B[2m${s}\x1B[0m`, bold6 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
19796
+ var dim6 = (s) => `\x1B[2m${s}\x1B[0m`, bold6 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, green6 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
19411
19797
  var init_format_helpers = __esm(() => {
19412
- JOB_COLORS = [cyan4, yellow5, magenta, green5, blue, red];
19798
+ JOB_COLORS = [cyan4, yellow5, magenta, green6, blue, red];
19413
19799
  EVENT_LABELS = {
19414
19800
  run_start: "START",
19415
19801
  meta: "META",
@@ -19428,17 +19814,17 @@ var exports_status = {};
19428
19814
  __export(exports_status, {
19429
19815
  run: () => run8
19430
19816
  });
19431
- import { spawnSync as spawnSync5 } from "node:child_process";
19432
- import { existsSync as existsSync6 } from "node:fs";
19433
- import { join as join9 } from "node:path";
19817
+ import { spawnSync as spawnSync6 } from "node:child_process";
19818
+ import { existsSync as existsSync7 } from "node:fs";
19819
+ import { join as join11 } from "node:path";
19434
19820
  function ok2(msg) {
19435
- console.log(` ${green5("✓")} ${msg}`);
19821
+ console.log(` ${green6("✓")} ${msg}`);
19436
19822
  }
19437
19823
  function warn(msg) {
19438
19824
  console.log(` ${yellow5("○")} ${msg}`);
19439
19825
  }
19440
19826
  function fail(msg) {
19441
- console.log(` ${red2("✗")} ${msg}`);
19827
+ console.log(` ${red("✗")} ${msg}`);
19442
19828
  }
19443
19829
  function info(msg) {
19444
19830
  console.log(` ${dim6(msg)}`);
@@ -19449,7 +19835,7 @@ function section(label) {
19449
19835
  ${bold6(`── ${label} ${line}`)}`);
19450
19836
  }
19451
19837
  function cmd(bin, args) {
19452
- const r = spawnSync5(bin, args, {
19838
+ const r = spawnSync6(bin, args, {
19453
19839
  encoding: "utf8",
19454
19840
  stdio: "pipe",
19455
19841
  timeout: 5000
@@ -19457,7 +19843,7 @@ function cmd(bin, args) {
19457
19843
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
19458
19844
  }
19459
19845
  function isInstalled(bin) {
19460
- return spawnSync5("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19846
+ return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
19461
19847
  }
19462
19848
  function formatElapsed2(s) {
19463
19849
  if (s.elapsed_s === undefined)
@@ -19469,11 +19855,11 @@ function formatElapsed2(s) {
19469
19855
  function statusColor(status) {
19470
19856
  switch (status) {
19471
19857
  case "running":
19472
- return cyan5(status);
19858
+ return cyan4(status);
19473
19859
  case "done":
19474
- return green5(status);
19860
+ return green6(status);
19475
19861
  case "error":
19476
- return red2(status);
19862
+ return red(status);
19477
19863
  case "starting":
19478
19864
  return yellow5(status);
19479
19865
  default:
@@ -19492,11 +19878,11 @@ async function run8() {
19492
19878
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
19493
19879
  const bdInstalled = isInstalled("bd");
19494
19880
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
19495
- const beadsPresent = existsSync6(join9(process.cwd(), ".beads"));
19881
+ const beadsPresent = existsSync7(join11(process.cwd(), ".beads"));
19496
19882
  const specialistsBin = cmd("which", ["specialists"]);
19497
- const jobsDir = join9(process.cwd(), ".specialists", "jobs");
19883
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
19498
19884
  let jobs = [];
19499
- if (existsSync6(jobsDir)) {
19885
+ if (existsSync7(jobsDir)) {
19500
19886
  const supervisor = new Supervisor({
19501
19887
  runner: null,
19502
19888
  runOptions: null,
@@ -19562,7 +19948,7 @@ ${bold6("specialists status")}
19562
19948
  for (const s of allSpecialists) {
19563
19949
  const staleness = stalenessMap[s.name];
19564
19950
  if (staleness === "AGED") {
19565
- warn(`${s.name} ${red2("AGED")} ${dim6(s.scope)}`);
19951
+ warn(`${s.name} ${red("AGED")} ${dim6(s.scope)}`);
19566
19952
  } else if (staleness === "STALE") {
19567
19953
  warn(`${s.name} ${yellow5("STALE")} ${dim6(s.scope)}`);
19568
19954
  }
@@ -19599,13 +19985,12 @@ ${bold6("specialists status")}
19599
19985
  section("Active Jobs");
19600
19986
  for (const job of jobs) {
19601
19987
  const elapsed = formatElapsed2(job);
19602
- const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19988
+ const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
19603
19989
  console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
19604
19990
  }
19605
19991
  }
19606
19992
  console.log();
19607
19993
  }
19608
- var red2 = (s) => `\x1B[31m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
19609
19994
  var init_status = __esm(() => {
19610
19995
  init_loader();
19611
19996
  init_supervisor();
@@ -19617,15 +20002,15 @@ var exports_result = {};
19617
20002
  __export(exports_result, {
19618
20003
  run: () => run9
19619
20004
  });
19620
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
19621
- import { join as join10 } from "node:path";
20005
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
20006
+ import { join as join12 } from "node:path";
19622
20007
  async function run9() {
19623
20008
  const jobId = process.argv[3];
19624
20009
  if (!jobId) {
19625
- console.error("Usage: specialists result <job-id>");
20010
+ console.error("Usage: specialists|sp result <job-id>");
19626
20011
  process.exit(1);
19627
20012
  }
19628
- const jobsDir = join10(process.cwd(), ".specialists", "jobs");
20013
+ const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19629
20014
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19630
20015
  const status = supervisor.readStatus(jobId);
19631
20016
  if (!status) {
@@ -19638,30 +20023,30 @@ async function run9() {
19638
20023
  process.exit(1);
19639
20024
  }
19640
20025
  if (status.status === "error") {
19641
- process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
20026
+ process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
19642
20027
  `);
19643
20028
  process.exit(1);
19644
20029
  }
19645
- const resultPath = join10(jobsDir, jobId, "result.txt");
19646
- if (!existsSync7(resultPath)) {
20030
+ const resultPath = join12(jobsDir, jobId, "result.txt");
20031
+ if (!existsSync8(resultPath)) {
19647
20032
  console.error(`Result file not found for job ${jobId}`);
19648
20033
  process.exit(1);
19649
20034
  }
19650
- process.stdout.write(readFileSync4(resultPath, "utf-8"));
20035
+ process.stdout.write(readFileSync5(resultPath, "utf-8"));
19651
20036
  }
19652
- var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
20037
+ var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
19653
20038
  var init_result = __esm(() => {
19654
20039
  init_supervisor();
19655
20040
  });
19656
20041
 
19657
20042
  // src/specialist/timeline-query.ts
19658
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
19659
- import { join as join11 } from "node:path";
20043
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
20044
+ import { join as join13 } from "node:path";
19660
20045
  function readJobEvents(jobDir) {
19661
- const eventsPath = join11(jobDir, "events.jsonl");
19662
- if (!existsSync8(eventsPath))
20046
+ const eventsPath = join13(jobDir, "events.jsonl");
20047
+ if (!existsSync9(eventsPath))
19663
20048
  return [];
19664
- const content = readFileSync5(eventsPath, "utf-8");
20049
+ const content = readFileSync6(eventsPath, "utf-8");
19665
20050
  const lines = content.split(`
19666
20051
  `).filter(Boolean);
19667
20052
  const events = [];
@@ -19674,12 +20059,12 @@ function readJobEvents(jobDir) {
19674
20059
  return events;
19675
20060
  }
19676
20061
  function readAllJobEvents(jobsDir) {
19677
- if (!existsSync8(jobsDir))
20062
+ if (!existsSync9(jobsDir))
19678
20063
  return [];
19679
20064
  const batches = [];
19680
- const entries = readdirSync2(jobsDir);
20065
+ const entries = readdirSync3(jobsDir);
19681
20066
  for (const entry of entries) {
19682
- const jobDir = join11(jobsDir, entry);
20067
+ const jobDir = join13(jobsDir, entry);
19683
20068
  try {
19684
20069
  const stat2 = __require("node:fs").statSync(jobDir);
19685
20070
  if (!stat2.isDirectory())
@@ -19688,12 +20073,12 @@ function readAllJobEvents(jobsDir) {
19688
20073
  continue;
19689
20074
  }
19690
20075
  const jobId = entry;
19691
- const statusPath = join11(jobDir, "status.json");
20076
+ const statusPath = join13(jobDir, "status.json");
19692
20077
  let specialist = "unknown";
19693
20078
  let beadId;
19694
- if (existsSync8(statusPath)) {
20079
+ if (existsSync9(statusPath)) {
19695
20080
  try {
19696
- const status = JSON.parse(readFileSync5(statusPath, "utf-8"));
20081
+ const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
19697
20082
  specialist = status.specialist ?? "unknown";
19698
20083
  beadId = status.bead_id;
19699
20084
  } catch {}
@@ -19756,8 +20141,8 @@ var exports_feed = {};
19756
20141
  __export(exports_feed, {
19757
20142
  run: () => run10
19758
20143
  });
19759
- import { existsSync as existsSync9 } from "node:fs";
19760
- import { join as join12 } from "node:path";
20144
+ import { existsSync as existsSync10 } from "node:fs";
20145
+ import { join as join14 } from "node:path";
19761
20146
  function getHumanEventKey(event) {
19762
20147
  switch (event.type) {
19763
20148
  case "meta":
@@ -19907,7 +20292,7 @@ async function followMerged(jobsDir, options) {
19907
20292
  }
19908
20293
  const lastPrintedEventKey = new Map;
19909
20294
  const seenMetaKey = new Map;
19910
- await new Promise((resolve) => {
20295
+ await new Promise((resolve2) => {
19911
20296
  const interval = setInterval(() => {
19912
20297
  const batches = filteredBatches();
19913
20298
  const newEvents = [];
@@ -19944,15 +20329,15 @@ async function followMerged(jobsDir, options) {
19944
20329
  }
19945
20330
  if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
19946
20331
  clearInterval(interval);
19947
- resolve();
20332
+ resolve2();
19948
20333
  }
19949
20334
  }, 500);
19950
20335
  });
19951
20336
  }
19952
20337
  async function run10() {
19953
20338
  const options = parseArgs5(process.argv.slice(3));
19954
- const jobsDir = join12(process.cwd(), ".specialists", "jobs");
19955
- if (!existsSync9(jobsDir)) {
20339
+ const jobsDir = join14(process.cwd(), ".specialists", "jobs");
20340
+ if (!existsSync10(jobsDir)) {
19956
20341
  console.log(dim6("No jobs directory found."));
19957
20342
  return;
19958
20343
  }
@@ -19974,19 +20359,121 @@ var init_feed = __esm(() => {
19974
20359
  init_format_helpers();
19975
20360
  });
19976
20361
 
20362
+ // src/cli/steer.ts
20363
+ var exports_steer = {};
20364
+ __export(exports_steer, {
20365
+ run: () => run11
20366
+ });
20367
+ import { join as join15 } from "node:path";
20368
+ import { writeFileSync as writeFileSync6 } from "node:fs";
20369
+ async function run11() {
20370
+ const jobId = process.argv[3];
20371
+ const message = process.argv[4];
20372
+ if (!jobId || !message) {
20373
+ console.error('Usage: specialists|sp steer <job-id> "<message>"');
20374
+ process.exit(1);
20375
+ }
20376
+ const jobsDir = join15(process.cwd(), ".specialists", "jobs");
20377
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20378
+ const status = supervisor.readStatus(jobId);
20379
+ if (!status) {
20380
+ console.error(`No job found: ${jobId}`);
20381
+ process.exit(1);
20382
+ }
20383
+ if (status.status === "done" || status.status === "error") {
20384
+ process.stderr.write(`Job ${jobId} is already ${status.status}.
20385
+ `);
20386
+ process.exit(1);
20387
+ }
20388
+ if (!status.fifo_path) {
20389
+ process.stderr.write(`${red3("Error:")} Job ${jobId} has no steer pipe.
20390
+ `);
20391
+ process.stderr.write(`Only jobs started with --background support mid-run steering.
20392
+ `);
20393
+ process.exit(1);
20394
+ }
20395
+ try {
20396
+ const payload = JSON.stringify({ type: "steer", message }) + `
20397
+ `;
20398
+ writeFileSync6(status.fifo_path, payload, { flag: "a" });
20399
+ process.stdout.write(`${green7("✓")} Steer message sent to job ${jobId}
20400
+ `);
20401
+ } catch (err) {
20402
+ process.stderr.write(`${red3("Error:")} Failed to write to steer pipe: ${err?.message}
20403
+ `);
20404
+ process.exit(1);
20405
+ }
20406
+ }
20407
+ var green7 = (s) => `\x1B[32m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
20408
+ var init_steer = __esm(() => {
20409
+ init_supervisor();
20410
+ });
20411
+
20412
+ // src/cli/follow-up.ts
20413
+ var exports_follow_up = {};
20414
+ __export(exports_follow_up, {
20415
+ run: () => run12
20416
+ });
20417
+ import { join as join16 } from "node:path";
20418
+ import { writeFileSync as writeFileSync7 } from "node:fs";
20419
+ async function run12() {
20420
+ const jobId = process.argv[3];
20421
+ const message = process.argv[4];
20422
+ if (!jobId || !message) {
20423
+ console.error('Usage: specialists|sp follow-up <job-id> "<message>"');
20424
+ process.exit(1);
20425
+ }
20426
+ const jobsDir = join16(process.cwd(), ".specialists", "jobs");
20427
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
20428
+ const status = supervisor.readStatus(jobId);
20429
+ if (!status) {
20430
+ console.error(`No job found: ${jobId}`);
20431
+ process.exit(1);
20432
+ }
20433
+ if (status.status !== "waiting") {
20434
+ process.stderr.write(`${red4("Error:")} Job ${jobId} is not in waiting state (status: ${status.status}).
20435
+ `);
20436
+ process.stderr.write(`Only jobs started with --keep-alive and --background support follow-up prompts.
20437
+ `);
20438
+ process.exit(1);
20439
+ }
20440
+ if (!status.fifo_path) {
20441
+ process.stderr.write(`${red4("Error:")} Job ${jobId} has no steer pipe.
20442
+ `);
20443
+ process.exit(1);
20444
+ }
20445
+ try {
20446
+ const payload = JSON.stringify({ type: "prompt", message }) + `
20447
+ `;
20448
+ writeFileSync7(status.fifo_path, payload, { flag: "a" });
20449
+ process.stdout.write(`${green8("✓")} Follow-up sent to job ${jobId}
20450
+ `);
20451
+ process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
20452
+ `);
20453
+ } catch (err) {
20454
+ process.stderr.write(`${red4("Error:")} Failed to write to steer pipe: ${err?.message}
20455
+ `);
20456
+ process.exit(1);
20457
+ }
20458
+ }
20459
+ var green8 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
20460
+ var init_follow_up = __esm(() => {
20461
+ init_supervisor();
20462
+ });
20463
+
19977
20464
  // src/cli/stop.ts
19978
20465
  var exports_stop = {};
19979
20466
  __export(exports_stop, {
19980
- run: () => run11
20467
+ run: () => run13
19981
20468
  });
19982
- import { join as join13 } from "node:path";
19983
- async function run11() {
20469
+ import { join as join17 } from "node:path";
20470
+ async function run13() {
19984
20471
  const jobId = process.argv[3];
19985
20472
  if (!jobId) {
19986
- console.error("Usage: specialists stop <job-id>");
20473
+ console.error("Usage: specialists|sp stop <job-id>");
19987
20474
  process.exit(1);
19988
20475
  }
19989
- const jobsDir = join13(process.cwd(), ".specialists", "jobs");
20476
+ const jobsDir = join17(process.cwd(), ".specialists", "jobs");
19990
20477
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
19991
20478
  const status = supervisor.readStatus(jobId);
19992
20479
  if (!status) {
@@ -20005,7 +20492,7 @@ async function run11() {
20005
20492
  }
20006
20493
  try {
20007
20494
  process.kill(status.pid, "SIGTERM");
20008
- process.stdout.write(`${green7("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
20495
+ process.stdout.write(`${green9("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
20009
20496
  `);
20010
20497
  } catch (err) {
20011
20498
  if (err.code === "ESRCH") {
@@ -20018,7 +20505,7 @@ async function run11() {
20018
20505
  }
20019
20506
  }
20020
20507
  }
20021
- var green7 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
20508
+ var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
20022
20509
  var init_stop = __esm(() => {
20023
20510
  init_supervisor();
20024
20511
  });
@@ -20026,25 +20513,26 @@ var init_stop = __esm(() => {
20026
20513
  // src/cli/quickstart.ts
20027
20514
  var exports_quickstart = {};
20028
20515
  __export(exports_quickstart, {
20029
- run: () => run12
20516
+ run: () => run14
20030
20517
  });
20031
20518
  function section2(title) {
20032
20519
  const bar = "─".repeat(60);
20033
20520
  return `
20034
- ${bold7(cyan7(title))}
20521
+ ${bold7(cyan5(title))}
20035
20522
  ${dim9(bar)}`;
20036
20523
  }
20037
20524
  function cmd2(s) {
20038
20525
  return yellow6(s);
20039
20526
  }
20040
20527
  function flag(s) {
20041
- return green8(s);
20528
+ return green10(s);
20042
20529
  }
20043
- async function run12() {
20530
+ async function run14() {
20044
20531
  const lines = [
20045
20532
  "",
20046
20533
  bold7("specialists · Quick Start Guide"),
20047
20534
  dim9("One MCP server. Multiple AI backends. Intelligent orchestration."),
20535
+ dim9("Tip: sp is a shorter alias — sp run, sp list, sp feed etc. work identically."),
20048
20536
  ""
20049
20537
  ];
20050
20538
  lines.push(section2("1. Installation"));
@@ -20074,10 +20562,9 @@ async function run12() {
20074
20562
  lines.push(` ${cmd2("specialists list")} ${flag("--category analysis")} # filter by category`);
20075
20563
  lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
20076
20564
  lines.push("");
20077
- lines.push(` Scopes (searched in order):`);
20078
- lines.push(` ${blue2("project")} ./specialists/*.specialist.yaml`);
20079
- lines.push(` ${blue2("user")} ~/.specialists/*.specialist.yaml`);
20080
- lines.push(` ${blue2("system")} bundled specialists (shipped with the package)`);
20565
+ lines.push(` Scopes (searched in order, user wins on name collision):`);
20566
+ lines.push(` ${blue2("user")} .specialists/user/specialists/*.specialist.yaml`);
20567
+ lines.push(` ${blue2("default")} .specialists/default/specialists/*.specialist.yaml`);
20081
20568
  lines.push("");
20082
20569
  lines.push(section2("4. Running a Specialist"));
20083
20570
  lines.push("");
@@ -20088,6 +20575,10 @@ async function run12() {
20088
20575
  lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--background")}`);
20089
20576
  lines.push(` ${dim9(" # → Job started: job_a1b2c3d4")}`);
20090
20577
  lines.push("");
20578
+ lines.push(` ${bold7("Follow")} (background + stream live output in one command):`);
20579
+ lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--follow")}`);
20580
+ lines.push(` ${dim9(" # starts in background, streams output live, exits when complete")}`);
20581
+ lines.push("");
20091
20582
  lines.push(` Override model for one run:`);
20092
20583
  lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim9("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim9('"..."')}`);
20093
20584
  lines.push("");
@@ -20106,6 +20597,17 @@ async function run12() {
20106
20597
  lines.push(` ${bold7("Read results")} — print the final output:`);
20107
20598
  lines.push(` ${cmd2("specialists result job_a1b2c3d4")} # exits 1 if still running`);
20108
20599
  lines.push("");
20600
+ lines.push(` ${bold7("Steer a running job")} — redirect the agent mid-run without cancelling:`);
20601
+ lines.push(` ${cmd2("specialists steer job_a1b2c3d4")} ${flag('"focus only on supervisor.ts"')}`);
20602
+ lines.push(` ${dim9(" # delivered after current tool calls finish, before the next LLM call")}`);
20603
+ lines.push("");
20604
+ lines.push(` ${bold7("Keep-alive multi-turn")} — start with ${flag("--keep-alive")}, then follow up:`);
20605
+ lines.push(` ${cmd2("specialists run bug-hunt")} ${flag("--bead unitAI-abc --keep-alive --background")}`);
20606
+ lines.push(` ${dim9(" # → Job started: a1b2c3 (status: waiting after first turn)")}`);
20607
+ lines.push(` ${cmd2("specialists result a1b2c3")} # read first turn`);
20608
+ lines.push(` ${cmd2("specialists follow-up a1b2c3")} ${flag('"now write the fix"')} # next turn, same Pi context`);
20609
+ lines.push(` ${cmd2("specialists feed a1b2c3")} ${flag("--follow")} # watch response`);
20610
+ lines.push("");
20109
20611
  lines.push(` ${bold7("Cancel a job")}:`);
20110
20612
  lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
20111
20613
  lines.push("");
@@ -20113,6 +20615,7 @@ async function run12() {
20113
20615
  lines.push(` ${dim9("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
20114
20616
  lines.push(` ${dim9("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
20115
20617
  lines.push(` ${dim9("result.txt")} — final output (written when status=done)`);
20618
+ lines.push(` ${dim9("steer.pipe")} — named FIFO for mid-run steering (removed on job completion)`);
20116
20619
  lines.push("");
20117
20620
  lines.push(section2("6. Editing Specialists"));
20118
20621
  lines.push("");
@@ -20200,7 +20703,9 @@ async function run12() {
20200
20703
  lines.push(` ${bold7("run_parallel")} — concurrent or pipeline execution`);
20201
20704
  lines.push(` ${bold7("start_specialist")} — async job start, returns job ID`);
20202
20705
  lines.push(` ${bold7("poll_specialist")} — poll job status/output by ID`);
20203
- lines.push(` ${bold7("stop_specialist")} cancel a running job by ID`);
20706
+ lines.push(` ${bold7("steer_specialist")} send a mid-run message to a running job`);
20707
+ lines.push(` ${bold7("follow_up_specialist")} — send a next-turn prompt to a keep-alive session`);
20708
+ lines.push(` ${bold7("stop_specialist")} — cancel a running job by ID`);
20204
20709
  lines.push(` ${bold7("specialist_status")} — circuit breaker health + staleness`);
20205
20710
  lines.push("");
20206
20711
  lines.push(section2("10. Common Workflows"));
@@ -20213,6 +20718,17 @@ async function run12() {
20213
20718
  lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
20214
20719
  lines.push(` ${cmd2("specialists result <job-id> > analysis.md")}`);
20215
20720
  lines.push("");
20721
+ lines.push(` ${bold7("Steer a job mid-run:")}`);
20722
+ lines.push(` ${cmd2('specialists run deep-analysis --prompt "..." --background')}`);
20723
+ lines.push(` ${cmd2('specialists steer <job-id> "focus only on the auth module"')}`);
20724
+ lines.push(` ${cmd2("specialists result <job-id>")}`);
20725
+ lines.push("");
20726
+ lines.push(` ${bold7("Multi-turn keep-alive (iterative work):")}`);
20727
+ lines.push(` ${cmd2("specialists run bug-hunt --bead unitAI-abc --keep-alive --background")}`);
20728
+ lines.push(` ${cmd2("specialists result <job-id>")}`);
20729
+ lines.push(` ${cmd2('specialists follow-up <job-id> "now write the fix for the root cause"')}`);
20730
+ lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
20731
+ lines.push("");
20216
20732
  lines.push(` ${bold7("Override model for a single run:")}`);
20217
20733
  lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
20218
20734
  lines.push("");
@@ -20223,18 +20739,18 @@ async function run12() {
20223
20739
  console.log(lines.join(`
20224
20740
  `));
20225
20741
  }
20226
- var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, cyan7 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green8 = (s) => `\x1B[32m${s}\x1B[0m`;
20742
+ var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green10 = (s) => `\x1B[32m${s}\x1B[0m`;
20227
20743
 
20228
20744
  // src/cli/doctor.ts
20229
20745
  var exports_doctor = {};
20230
20746
  __export(exports_doctor, {
20231
- run: () => run13
20747
+ run: () => run15
20232
20748
  });
20233
- import { spawnSync as spawnSync6 } from "node:child_process";
20234
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "node:fs";
20235
- import { join as join14 } from "node:path";
20749
+ import { spawnSync as spawnSync7 } from "node:child_process";
20750
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
20751
+ import { join as join18 } from "node:path";
20236
20752
  function ok3(msg) {
20237
- console.log(` ${green9("✓")} ${msg}`);
20753
+ console.log(` ${green11("✓")} ${msg}`);
20238
20754
  }
20239
20755
  function warn2(msg) {
20240
20756
  console.log(` ${yellow7("○")} ${msg}`);
@@ -20254,17 +20770,17 @@ function section3(label) {
20254
20770
  ${bold8(`── ${label} ${line}`)}`);
20255
20771
  }
20256
20772
  function sp(bin, args) {
20257
- const r = spawnSync6(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
20773
+ const r = spawnSync7(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
20258
20774
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
20259
20775
  }
20260
20776
  function isInstalled2(bin) {
20261
- return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20777
+ return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
20262
20778
  }
20263
20779
  function loadJson2(path) {
20264
- if (!existsSync10(path))
20780
+ if (!existsSync11(path))
20265
20781
  return null;
20266
20782
  try {
20267
- return JSON.parse(readFileSync6(path, "utf8"));
20783
+ return JSON.parse(readFileSync7(path, "utf8"));
20268
20784
  } catch {
20269
20785
  return null;
20270
20786
  }
@@ -20297,7 +20813,7 @@ function checkBd() {
20297
20813
  return false;
20298
20814
  }
20299
20815
  ok3(`bd installed ${dim10(sp("bd", ["--version"]).stdout || "")}`);
20300
- if (existsSync10(join14(CWD, ".beads")))
20816
+ if (existsSync11(join18(CWD, ".beads")))
20301
20817
  ok3(".beads/ present in project");
20302
20818
  else
20303
20819
  warn2(".beads/ not found in project");
@@ -20317,8 +20833,8 @@ function checkHooks() {
20317
20833
  section3("Claude Code hooks (2 expected)");
20318
20834
  let allPresent = true;
20319
20835
  for (const name of HOOK_NAMES) {
20320
- const dest = join14(HOOKS_DIR, name);
20321
- if (!existsSync10(dest)) {
20836
+ const dest = join18(HOOKS_DIR, name);
20837
+ if (!existsSync11(dest)) {
20322
20838
  fail2(`${name} ${red6("missing")}`);
20323
20839
  fix("specialists install");
20324
20840
  allPresent = false;
@@ -20332,14 +20848,13 @@ function checkHooks() {
20332
20848
  fix("specialists install");
20333
20849
  return false;
20334
20850
  }
20335
- const hooks = settings.hooks ?? {};
20336
20851
  const wiredCommands = new Set([
20337
- ...hooks.UserPromptSubmit ?? [],
20338
- ...hooks.SessionStart ?? []
20852
+ ...settings.UserPromptSubmit ?? [],
20853
+ ...settings.SessionStart ?? []
20339
20854
  ].flatMap((entry) => (entry.hooks ?? []).map((h) => h.command ?? "")));
20340
20855
  for (const name of HOOK_NAMES) {
20341
- const expected = join14(HOOKS_DIR, name);
20342
- if (!wiredCommands.has(expected)) {
20856
+ const expectedRelative = `node .specialists/default/hooks/${name}`;
20857
+ if (!wiredCommands.has(expectedRelative)) {
20343
20858
  warn2(`${name} not wired in settings.json`);
20344
20859
  fix("specialists install");
20345
20860
  allPresent = false;
@@ -20363,18 +20878,18 @@ function checkMCP() {
20363
20878
  }
20364
20879
  function checkRuntimeDirs() {
20365
20880
  section3(".specialists/ runtime directories");
20366
- const rootDir = join14(CWD, ".specialists");
20367
- const jobsDir = join14(rootDir, "jobs");
20368
- const readyDir = join14(rootDir, "ready");
20881
+ const rootDir = join18(CWD, ".specialists");
20882
+ const jobsDir = join18(rootDir, "jobs");
20883
+ const readyDir = join18(rootDir, "ready");
20369
20884
  let allOk = true;
20370
- if (!existsSync10(rootDir)) {
20885
+ if (!existsSync11(rootDir)) {
20371
20886
  warn2(".specialists/ not found in current project");
20372
20887
  fix("specialists init");
20373
20888
  allOk = false;
20374
20889
  } else {
20375
20890
  ok3(".specialists/ present");
20376
20891
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
20377
- if (!existsSync10(subDir)) {
20892
+ if (!existsSync11(subDir)) {
20378
20893
  warn2(`.specialists/${label}/ missing — auto-creating`);
20379
20894
  mkdirSync3(subDir, { recursive: true });
20380
20895
  ok3(`.specialists/${label}/ created`);
@@ -20387,14 +20902,14 @@ function checkRuntimeDirs() {
20387
20902
  }
20388
20903
  function checkZombieJobs() {
20389
20904
  section3("Background jobs");
20390
- const jobsDir = join14(CWD, ".specialists", "jobs");
20391
- if (!existsSync10(jobsDir)) {
20905
+ const jobsDir = join18(CWD, ".specialists", "jobs");
20906
+ if (!existsSync11(jobsDir)) {
20392
20907
  hint("No .specialists/jobs/ — skipping");
20393
20908
  return true;
20394
20909
  }
20395
20910
  let entries;
20396
20911
  try {
20397
- entries = readdirSync3(jobsDir);
20912
+ entries = readdirSync4(jobsDir);
20398
20913
  } catch {
20399
20914
  entries = [];
20400
20915
  }
@@ -20406,11 +20921,11 @@ function checkZombieJobs() {
20406
20921
  let total = 0;
20407
20922
  let running = 0;
20408
20923
  for (const jobId of entries) {
20409
- const statusPath = join14(jobsDir, jobId, "status.json");
20410
- if (!existsSync10(statusPath))
20924
+ const statusPath = join18(jobsDir, jobId, "status.json");
20925
+ if (!existsSync11(statusPath))
20411
20926
  continue;
20412
20927
  try {
20413
- const status = JSON.parse(readFileSync6(statusPath, "utf8"));
20928
+ const status = JSON.parse(readFileSync7(statusPath, "utf8"));
20414
20929
  total++;
20415
20930
  if (status.status === "running" || status.status === "starting") {
20416
20931
  const pid = status.pid;
@@ -20437,7 +20952,7 @@ function checkZombieJobs() {
20437
20952
  }
20438
20953
  return zombies === 0;
20439
20954
  }
20440
- async function run13() {
20955
+ async function run15() {
20441
20956
  console.log(`
20442
20957
  ${bold8("specialists doctor")}
20443
20958
  `);
@@ -20451,20 +20966,21 @@ ${bold8("specialists doctor")}
20451
20966
  const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
20452
20967
  console.log("");
20453
20968
  if (allOk) {
20454
- console.log(` ${green9("✓")} ${bold8("All checks passed")} — specialists is healthy`);
20969
+ console.log(` ${green11("✓")} ${bold8("All checks passed")} — specialists is healthy`);
20455
20970
  } else {
20456
20971
  console.log(` ${yellow7("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
20457
20972
  console.log(` ${dim10("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
20458
20973
  }
20459
20974
  console.log("");
20460
20975
  }
20461
- var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
20976
+ var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, green11 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
20462
20977
  var init_doctor = __esm(() => {
20463
20978
  CWD = process.cwd();
20464
- CLAUDE_DIR = join14(CWD, ".claude");
20465
- HOOKS_DIR = join14(CLAUDE_DIR, "hooks");
20466
- SETTINGS_FILE = join14(CLAUDE_DIR, "settings.json");
20467
- MCP_FILE2 = join14(CWD, ".mcp.json");
20979
+ CLAUDE_DIR = join18(CWD, ".claude");
20980
+ SPECIALISTS_DIR = join18(CWD, ".specialists");
20981
+ HOOKS_DIR = join18(SPECIALISTS_DIR, "default", "hooks");
20982
+ SETTINGS_FILE = join18(CLAUDE_DIR, "settings.json");
20983
+ MCP_FILE2 = join18(CWD, ".mcp.json");
20468
20984
  HOOK_NAMES = [
20469
20985
  "specialists-complete.mjs",
20470
20986
  "specialists-session-start.mjs"
@@ -20474,13 +20990,13 @@ var init_doctor = __esm(() => {
20474
20990
  // src/cli/setup.ts
20475
20991
  var exports_setup = {};
20476
20992
  __export(exports_setup, {
20477
- run: () => run14
20993
+ run: () => run16
20478
20994
  });
20479
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
20995
+ import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
20480
20996
  import { homedir as homedir3 } from "node:os";
20481
- import { join as join15, resolve } from "node:path";
20997
+ import { join as join19, resolve as resolve2 } from "node:path";
20482
20998
  function ok4(msg) {
20483
- console.log(` ${green10("✓")} ${msg}`);
20999
+ console.log(` ${green12("✓")} ${msg}`);
20484
21000
  }
20485
21001
  function skip2(msg) {
20486
21002
  console.log(` ${yellow8("○")} ${msg}`);
@@ -20488,12 +21004,12 @@ function skip2(msg) {
20488
21004
  function resolveTarget(target) {
20489
21005
  switch (target) {
20490
21006
  case "global":
20491
- return join15(homedir3(), ".claude", "CLAUDE.md");
21007
+ return join19(homedir3(), ".claude", "CLAUDE.md");
20492
21008
  case "agents":
20493
- return join15(process.cwd(), "AGENTS.md");
21009
+ return join19(process.cwd(), "AGENTS.md");
20494
21010
  case "project":
20495
21011
  default:
20496
- return join15(process.cwd(), "CLAUDE.md");
21012
+ return join19(process.cwd(), "CLAUDE.md");
20497
21013
  }
20498
21014
  }
20499
21015
  function parseArgs6() {
@@ -20521,17 +21037,17 @@ function parseArgs6() {
20521
21037
  }
20522
21038
  return { target, dryRun };
20523
21039
  }
20524
- async function run14() {
21040
+ async function run16() {
20525
21041
  const { target, dryRun } = parseArgs6();
20526
- const filePath = resolve(resolveTarget(target));
21042
+ const filePath = resolve2(resolveTarget(target));
20527
21043
  const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
20528
21044
  console.log(`
20529
21045
  ${bold9("specialists setup")}
20530
21046
  `);
20531
21047
  console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
20532
21048
  `);
20533
- if (existsSync11(filePath)) {
20534
- const existing = readFileSync7(filePath, "utf8");
21049
+ if (existsSync12(filePath)) {
21050
+ const existing = readFileSync8(filePath, "utf8");
20535
21051
  if (existing.includes(MARKER)) {
20536
21052
  skip2(`${label} already contains Specialists Workflow section`);
20537
21053
  console.log(`
@@ -20552,7 +21068,7 @@ ${bold9("specialists setup")}
20552
21068
  ` : `
20553
21069
 
20554
21070
  `;
20555
- writeFileSync4(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
21071
+ writeFileSync8(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
20556
21072
  ok4(`Appended Specialists Workflow section to ${label}`);
20557
21073
  } else {
20558
21074
  if (dryRun) {
@@ -20563,7 +21079,7 @@ ${bold9("specialists setup")}
20563
21079
  console.log(dim11("─".repeat(60)));
20564
21080
  return;
20565
21081
  }
20566
- writeFileSync4(filePath, WORKFLOW_BLOCK, "utf8");
21082
+ writeFileSync8(filePath, WORKFLOW_BLOCK, "utf8");
20567
21083
  ok4(`Created ${label} with Specialists Workflow section`);
20568
21084
  }
20569
21085
  console.log("");
@@ -20573,7 +21089,7 @@ ${bold9("specialists setup")}
20573
21089
  console.log(` • Run ${yellow8("specialist_init")} in a new session to bootstrap context`);
20574
21090
  console.log("");
20575
21091
  }
20576
- var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green10 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
21092
+ var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
20577
21093
 
20578
21094
  > Injected by \`specialists setup\`. Keep this section — agents use it for context.
20579
21095
 
@@ -20641,26 +21157,28 @@ var init_setup = () => {};
20641
21157
  // src/cli/help.ts
20642
21158
  var exports_help = {};
20643
21159
  __export(exports_help, {
20644
- run: () => run15
21160
+ run: () => run17
20645
21161
  });
20646
21162
  function formatCommands(entries) {
20647
21163
  const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
20648
21164
  return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
20649
21165
  }
20650
- async function run15() {
21166
+ async function run17() {
20651
21167
  const lines = [
20652
21168
  "",
20653
21169
  "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
20654
21170
  "",
20655
21171
  bold10("Usage:"),
20656
- " specialists [command]",
20657
- " specialists [command] --help",
21172
+ " specialists|sp [command]",
21173
+ " specialists|sp [command] --help",
21174
+ "",
21175
+ dim12(" sp is a shorter alias — sp run, sp list, sp feed etc. all work identically."),
20658
21176
  "",
20659
21177
  bold10("Common flows:"),
20660
21178
  "",
20661
21179
  " Tracked work (primary)",
20662
21180
  ' bd create "Task title" -t task -p 1 --json',
20663
- " specialists run <name> --bead <id> [--context-depth N] [--background]",
21181
+ " specialists run <name> --bead <id> [--context-depth N] [--background] [--follow]",
20664
21182
  " specialists feed -f",
20665
21183
  ' bd close <id> --reason "Done"',
20666
21184
  "",
@@ -20672,6 +21190,7 @@ async function run15() {
20672
21190
  " --prompt is for quick untracked work",
20673
21191
  " --context-depth defaults to 1 with --bead",
20674
21192
  " --no-beads does not disable bead reading",
21193
+ " --follow runs in background and streams output live",
20675
21194
  "",
20676
21195
  bold10("Core commands:"),
20677
21196
  ...formatCommands(CORE_COMMANDS),
@@ -20686,15 +21205,20 @@ async function run15() {
20686
21205
  " specialists init",
20687
21206
  " specialists list",
20688
21207
  " specialists run bug-hunt --bead unitAI-123 --background",
21208
+ " specialists run sync-docs --follow # run + stream live output",
20689
21209
  ' specialists run codebase-explorer --prompt "Map the CLI architecture"',
20690
21210
  " specialists feed -f",
21211
+ ' specialists steer <job-id> "focus only on supervisor.ts"',
21212
+ ' specialists follow-up <job-id> "now write the fix"',
20691
21213
  " specialists result <job-id>",
20692
21214
  "",
20693
21215
  bold10("More help:"),
20694
- " specialists quickstart Full guide and workflow reference",
20695
- " specialists run --help Run command details and flags",
20696
- " specialists init --help Bootstrap behavior and workflow injection",
20697
- " specialists feed --help Background job monitoring details",
21216
+ " specialists quickstart Full guide and workflow reference",
21217
+ " specialists run --help Run command details and flags",
21218
+ " specialists steer --help Mid-run steering details",
21219
+ " specialists follow-up --help Multi-turn keep-alive details",
21220
+ " specialists init --help Bootstrap behavior and workflow injection",
21221
+ " specialists feed --help Background job monitoring details",
20698
21222
  "",
20699
21223
  dim12("Project model: specialists are project-only; user-scope discovery is deprecated."),
20700
21224
  ""
@@ -20710,6 +21234,8 @@ var init_help = __esm(() => {
20710
21234
  ["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
20711
21235
  ["feed", "Tail job events; use -f to follow all jobs"],
20712
21236
  ["result", "Print final output of a completed background job"],
21237
+ ["steer", "Send a mid-run message to a running background job"],
21238
+ ["follow-up", "Send a next-turn prompt to a keep-alive session (retains full context)"],
20713
21239
  ["stop", "Stop a running background job"],
20714
21240
  ["status", "Show health, MCP state, and active jobs"],
20715
21241
  ["doctor", "Diagnose installation/runtime problems"],
@@ -28002,7 +28528,7 @@ class StdioServerTransport {
28002
28528
  }
28003
28529
 
28004
28530
  // src/server.ts
28005
- import { join as join4 } from "node:path";
28531
+ import { join as join7 } from "node:path";
28006
28532
 
28007
28533
  // src/constants.ts
28008
28534
  var LOG_PREFIX = "[specialists]";
@@ -28118,8 +28644,7 @@ function createUseSpecialistTool(runner) {
28118
28644
  if (!bead) {
28119
28645
  throw new Error(`Unable to read bead '${input.bead_id}' via bd show --json`);
28120
28646
  }
28121
- const blockers = input.context_depth && input.context_depth > 0 ? beadsClient.getBlockers(input.bead_id, input.context_depth) : [];
28122
- const beadContext = buildBeadContext(bead, { blockers, depth: input.context_depth ?? 0 });
28647
+ const beadContext = buildBeadContext(bead);
28123
28648
  prompt = beadContext;
28124
28649
  variables = {
28125
28650
  ...input.variables ?? {},
@@ -28239,17 +28764,17 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
28239
28764
  async execute(_) {
28240
28765
  const list = await loader.list();
28241
28766
  const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
28242
- const { existsSync: existsSync3, readdirSync, readFileSync } = await import("node:fs");
28767
+ const { existsSync: existsSync4, readdirSync, readFileSync: readFileSync2 } = await import("node:fs");
28243
28768
  const { join: join3 } = await import("node:path");
28244
28769
  const jobsDir = join3(process.cwd(), ".specialists", "jobs");
28245
28770
  const jobs = [];
28246
- if (existsSync3(jobsDir)) {
28771
+ if (existsSync4(jobsDir)) {
28247
28772
  for (const entry of readdirSync(jobsDir)) {
28248
28773
  const statusPath = join3(jobsDir, entry, "status.json");
28249
- if (!existsSync3(statusPath))
28774
+ if (!existsSync4(statusPath))
28250
28775
  continue;
28251
28776
  try {
28252
- jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
28777
+ jobs.push(JSON.parse(readFileSync2(statusPath, "utf-8")));
28253
28778
  } catch {}
28254
28779
  }
28255
28780
  jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
@@ -28328,6 +28853,74 @@ class JobRegistry {
28328
28853
  }
28329
28854
  job.killFn = killFn;
28330
28855
  }
28856
+ setSteerFn(id, steerFn) {
28857
+ const job = this.jobs.get(id);
28858
+ if (!job)
28859
+ return;
28860
+ job.steerFn = steerFn;
28861
+ }
28862
+ setResumeFn(id, resumeFn, closeFn) {
28863
+ const job = this.jobs.get(id);
28864
+ if (!job)
28865
+ return;
28866
+ job.resumeFn = resumeFn;
28867
+ job.closeFn = closeFn;
28868
+ job.status = "waiting";
28869
+ job.currentEvent = "waiting";
28870
+ }
28871
+ async followUp(id, message) {
28872
+ const job = this.jobs.get(id);
28873
+ if (!job)
28874
+ return { ok: false, error: `Job not found: ${id}` };
28875
+ if (job.status !== "waiting")
28876
+ return { ok: false, error: `Job is not waiting (status: ${job.status})` };
28877
+ if (!job.resumeFn)
28878
+ return { ok: false, error: "Job has no resume function" };
28879
+ job.status = "running";
28880
+ job.currentEvent = "starting";
28881
+ try {
28882
+ const output = await job.resumeFn(message);
28883
+ job.outputBuffer = output;
28884
+ job.status = "waiting";
28885
+ job.currentEvent = "waiting";
28886
+ return { ok: true, output };
28887
+ } catch (err) {
28888
+ job.status = "error";
28889
+ job.error = err?.message ?? String(err);
28890
+ return { ok: false, error: job.error };
28891
+ }
28892
+ }
28893
+ async closeSession(id) {
28894
+ const job = this.jobs.get(id);
28895
+ if (!job)
28896
+ return { ok: false, error: `Job not found: ${id}` };
28897
+ if (job.status !== "waiting")
28898
+ return { ok: false, error: `Job is not in waiting state` };
28899
+ try {
28900
+ await job.closeFn?.();
28901
+ job.status = "done";
28902
+ job.currentEvent = "done";
28903
+ job.endedAtMs = Date.now();
28904
+ return { ok: true };
28905
+ } catch (err) {
28906
+ return { ok: false, error: err?.message ?? String(err) };
28907
+ }
28908
+ }
28909
+ async steer(id, message) {
28910
+ const job = this.jobs.get(id);
28911
+ if (!job)
28912
+ return { ok: false, error: `Job not found: ${id}` };
28913
+ if (job.status !== "running")
28914
+ return { ok: false, error: `Job is not running (status: ${job.status})` };
28915
+ if (!job.steerFn)
28916
+ return { ok: false, error: "Job session not ready for steering yet" };
28917
+ try {
28918
+ await job.steerFn(message);
28919
+ return { ok: true };
28920
+ } catch (err) {
28921
+ return { ok: false, error: err?.message ?? String(err) };
28922
+ }
28923
+ }
28331
28924
  complete(id, result) {
28332
28925
  const job = this.jobs.get(id);
28333
28926
  if (!job || job.status !== "running")
@@ -28452,20 +29045,117 @@ function createStopSpecialistTool(registry2) {
28452
29045
  };
28453
29046
  }
28454
29047
 
29048
+ // src/tools/specialist/steer_specialist.tool.ts
29049
+ init_zod();
29050
+ init_supervisor();
29051
+ import { writeFileSync as writeFileSync2 } from "node:fs";
29052
+ import { join as join4 } from "node:path";
29053
+ var steerSpecialistSchema = exports_external.object({
29054
+ job_id: exports_external.string().describe("Job ID returned by start_specialist or specialists run --background"),
29055
+ message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
29056
+ });
29057
+ function createSteerSpecialistTool(registry2) {
29058
+ return {
29059
+ name: "steer_specialist",
29060
+ 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).",
29061
+ inputSchema: steerSpecialistSchema,
29062
+ async execute(input) {
29063
+ const snap = registry2.snapshot(input.job_id);
29064
+ if (snap) {
29065
+ const result = await registry2.steer(input.job_id, input.message);
29066
+ if (result.ok) {
29067
+ return { status: "steered", job_id: input.job_id, message: input.message };
29068
+ }
29069
+ return { status: "error", error: result.error, job_id: input.job_id };
29070
+ }
29071
+ const jobsDir = join4(process.cwd(), ".specialists", "jobs");
29072
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29073
+ const status = supervisor.readStatus(input.job_id);
29074
+ if (!status) {
29075
+ return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29076
+ }
29077
+ if (status.status === "done" || status.status === "error") {
29078
+ return { status: "error", error: `Job is already ${status.status}`, job_id: input.job_id };
29079
+ }
29080
+ if (!status.fifo_path) {
29081
+ return { status: "error", error: "Job has no steer pipe (may have been started without FIFO support)", job_id: input.job_id };
29082
+ }
29083
+ try {
29084
+ const payload = JSON.stringify({ type: "steer", message: input.message }) + `
29085
+ `;
29086
+ writeFileSync2(status.fifo_path, payload, { flag: "a" });
29087
+ return { status: "steered", job_id: input.job_id, message: input.message };
29088
+ } catch (err) {
29089
+ return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
29090
+ }
29091
+ }
29092
+ };
29093
+ }
29094
+
29095
+ // src/tools/specialist/follow_up_specialist.tool.ts
29096
+ init_zod();
29097
+ init_supervisor();
29098
+ import { writeFileSync as writeFileSync3 } from "node:fs";
29099
+ import { join as join5 } from "node:path";
29100
+ var followUpSpecialistSchema = exports_external.object({
29101
+ job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
29102
+ message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
29103
+ });
29104
+ function createFollowUpSpecialistTool(registry2) {
29105
+ return {
29106
+ name: "follow_up_specialist",
29107
+ 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).",
29108
+ inputSchema: followUpSpecialistSchema,
29109
+ async execute(input) {
29110
+ const snap = registry2.snapshot(input.job_id);
29111
+ if (snap) {
29112
+ if (snap.status !== "waiting") {
29113
+ return { status: "error", error: `Job is not waiting (status: ${snap.status})`, job_id: input.job_id };
29114
+ }
29115
+ const result = await registry2.followUp(input.job_id, input.message);
29116
+ if (result.ok) {
29117
+ return { status: "resumed", job_id: input.job_id, output: result.output };
29118
+ }
29119
+ return { status: "error", error: result.error, job_id: input.job_id };
29120
+ }
29121
+ const jobsDir = join5(process.cwd(), ".specialists", "jobs");
29122
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
29123
+ const status = supervisor.readStatus(input.job_id);
29124
+ if (!status) {
29125
+ return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
29126
+ }
29127
+ if (status.status !== "waiting") {
29128
+ return { status: "error", error: `Job is not waiting (status: ${status.status})`, job_id: input.job_id };
29129
+ }
29130
+ if (!status.fifo_path) {
29131
+ return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
29132
+ }
29133
+ try {
29134
+ const payload = JSON.stringify({ type: "prompt", message: input.message }) + `
29135
+ `;
29136
+ writeFileSync3(status.fifo_path, payload, { flag: "a" });
29137
+ return { status: "sent", job_id: input.job_id, message: input.message };
29138
+ } catch (err) {
29139
+ return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
29140
+ }
29141
+ }
29142
+ };
29143
+ }
29144
+
28455
29145
  // src/server.ts
28456
29146
  init_zod();
28457
29147
 
28458
29148
  // src/tools/specialist/specialist_init.tool.ts
28459
29149
  init_zod();
28460
- import { spawnSync as spawnSync2 } from "node:child_process";
28461
- import { existsSync as existsSync3 } from "node:fs";
28462
- import { join as join3 } from "node:path";
29150
+ import { spawnSync as spawnSync4 } from "node:child_process";
29151
+ import { existsSync as existsSync5 } from "node:fs";
29152
+ import { join as join6 } from "node:path";
28463
29153
  var specialistInitSchema = objectType({});
28464
29154
  function createSpecialistInitTool(loader, deps) {
28465
29155
  const resolved = deps ?? {
28466
- bdAvailable: () => spawnSync2("bd", ["--version"], { stdio: "ignore" }).status === 0,
28467
- beadsExists: () => existsSync3(join3(process.cwd(), ".beads")),
28468
- bdInit: () => spawnSync2("bd", ["init"], { stdio: "ignore" })
29156
+ bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
29157
+ beadsExists: () => existsSync5(join6(process.cwd(), ".beads")),
29158
+ bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
28469
29159
  };
28470
29160
  return {
28471
29161
  name: "specialist_init",
@@ -28499,7 +29189,7 @@ class SpecialistsServer {
28499
29189
  const circuitBreaker = new CircuitBreaker;
28500
29190
  const loader = new SpecialistLoader;
28501
29191
  const hooks = new HookEmitter({
28502
- tracePath: join4(process.cwd(), ".specialists", "trace.jsonl")
29192
+ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl")
28503
29193
  });
28504
29194
  const beadsClient = new BeadsClient;
28505
29195
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
@@ -28512,6 +29202,8 @@ class SpecialistsServer {
28512
29202
  createStartSpecialistTool(runner, registry2),
28513
29203
  createPollSpecialistTool(registry2),
28514
29204
  createStopSpecialistTool(registry2),
29205
+ createSteerSpecialistTool(registry2),
29206
+ createFollowUpSpecialistTool(registry2),
28515
29207
  createSpecialistInitTool(loader)
28516
29208
  ];
28517
29209
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
@@ -28527,6 +29219,8 @@ class SpecialistsServer {
28527
29219
  start_specialist: startSpecialistSchema,
28528
29220
  poll_specialist: pollSpecialistSchema,
28529
29221
  stop_specialist: stopSpecialistSchema,
29222
+ steer_specialist: steerSpecialistSchema,
29223
+ follow_up_specialist: followUpSpecialistSchema,
28530
29224
  specialist_init: specialistInitSchema
28531
29225
  };
28532
29226
  this.toolSchemas = schemaMap;
@@ -28599,7 +29293,7 @@ var next = process.argv[3];
28599
29293
  function wantsHelp() {
28600
29294
  return next === "--help" || next === "-h";
28601
29295
  }
28602
- async function run16() {
29296
+ async function run18() {
28603
29297
  if (sub === "install") {
28604
29298
  if (wantsHelp()) {
28605
29299
  console.log([
@@ -28754,11 +29448,13 @@ async function run16() {
28754
29448
  " --context-depth <n> Dependency context depth when using --bead (default: 1)",
28755
29449
  " --no-beads Do not create a new tracking bead (does not disable bead reading)",
28756
29450
  " --background Start async and return a job id",
29451
+ " --follow Run in background and stream output live",
28757
29452
  " --model <model> Override the configured model for this run",
28758
29453
  "",
28759
29454
  "Examples:",
28760
29455
  " specialists run bug-hunt --bead unitAI-55d",
28761
29456
  " specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
29457
+ " specialists run sync-docs --follow",
28762
29458
  ' specialists run code-review --prompt "Audit src/api.ts"',
28763
29459
  " cat brief.md | specialists run report-generator",
28764
29460
  "",
@@ -28857,6 +29553,65 @@ async function run16() {
28857
29553
  const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
28858
29554
  return handler();
28859
29555
  }
29556
+ if (sub === "steer") {
29557
+ if (wantsHelp()) {
29558
+ console.log([
29559
+ "",
29560
+ 'Usage: specialists steer <job-id> "<message>"',
29561
+ "",
29562
+ "Send a mid-run steering message to a running background specialist job.",
29563
+ "The agent receives the message after its current tool calls finish,",
29564
+ "before the next LLM call.",
29565
+ "",
29566
+ 'Pi RPC steer command: {"type":"steer","message":"..."}',
29567
+ 'Response: {"type":"response","command":"steer","success":true}',
29568
+ "",
29569
+ "Examples:",
29570
+ ' specialists steer a1b2c3 "focus only on supervisor.ts"',
29571
+ ' specialists steer a1b2c3 "skip tests, just fix the bug"',
29572
+ "",
29573
+ "Notes:",
29574
+ " - Only works for jobs started with --background.",
29575
+ " - Delivery is best-effort: the agent processes it on its next turn.",
29576
+ ""
29577
+ ].join(`
29578
+ `));
29579
+ return;
29580
+ }
29581
+ const { run: handler } = await Promise.resolve().then(() => (init_steer(), exports_steer));
29582
+ return handler();
29583
+ }
29584
+ if (sub === "follow-up") {
29585
+ if (wantsHelp()) {
29586
+ console.log([
29587
+ "",
29588
+ 'Usage: specialists follow-up <job-id> "<message>"',
29589
+ "",
29590
+ "Send a follow-up prompt to a waiting keep-alive specialist session.",
29591
+ "The Pi session retains full conversation history between turns.",
29592
+ "",
29593
+ "Requires: job started with --keep-alive --background.",
29594
+ "",
29595
+ "Examples:",
29596
+ ' specialists follow-up a1b2c3 "Now write the fix for the bug you found"',
29597
+ ' specialists follow-up a1b2c3 "Focus only on the auth module"',
29598
+ "",
29599
+ "Workflow:",
29600
+ " specialists run bug-hunt --bead <id> --keep-alive --background",
29601
+ " # → Job started: a1b2c3 (status: waiting after first turn)",
29602
+ " specialists result a1b2c3 # read first turn output",
29603
+ ' specialists follow-up a1b2c3 "..." # send next prompt',
29604
+ " specialists feed a1b2c3 --follow # watch response",
29605
+ "",
29606
+ "See also: specialists steer (mid-run redirect)",
29607
+ ""
29608
+ ].join(`
29609
+ `));
29610
+ return;
29611
+ }
29612
+ const { run: handler } = await Promise.resolve().then(() => (init_follow_up(), exports_follow_up));
29613
+ return handler();
29614
+ }
28860
29615
  if (sub === "stop") {
28861
29616
  if (wantsHelp()) {
28862
29617
  console.log([
@@ -28952,7 +29707,7 @@ Run 'specialists help' to see available commands.`);
28952
29707
  const server = new SpecialistsServer;
28953
29708
  await server.start();
28954
29709
  }
28955
- run16().catch((error2) => {
29710
+ run18().catch((error2) => {
28956
29711
  logger.error(`Fatal error: ${error2}`);
28957
29712
  process.exit(1);
28958
29713
  });