@jaggerxtrm/specialists 3.4.2 → 3.4.3

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
@@ -18774,1278 +18774,526 @@ class HookEmitter {
18774
18774
  }
18775
18775
  var init_hooks = () => {};
18776
18776
 
18777
- // src/specialist/timeline-events.ts
18778
- function mapCallbackEventToTimelineEvent(callbackEvent, context) {
18779
- const t = Date.now();
18780
- switch (callbackEvent) {
18781
- case "thinking":
18782
- return { t, type: TIMELINE_EVENT_TYPES.THINKING };
18783
- case "tool_execution_start":
18784
- return {
18785
- t,
18786
- type: TIMELINE_EVENT_TYPES.TOOL,
18787
- tool: context.tool ?? "unknown",
18788
- phase: "start",
18789
- tool_call_id: context.toolCallId,
18790
- args: context.args,
18791
- started_at: new Date(t).toISOString()
18792
- };
18793
- case "tool_execution_update":
18794
- case "tool_execution":
18795
- return {
18796
- t,
18797
- type: TIMELINE_EVENT_TYPES.TOOL,
18798
- tool: context.tool ?? "unknown",
18799
- phase: "update",
18800
- tool_call_id: context.toolCallId
18801
- };
18802
- case "tool_execution_end":
18803
- return {
18804
- t,
18805
- type: TIMELINE_EVENT_TYPES.TOOL,
18806
- tool: context.tool ?? "unknown",
18807
- phase: "end",
18808
- tool_call_id: context.toolCallId,
18809
- is_error: context.isError
18810
- };
18811
- case "message_start_assistant":
18812
- return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "assistant" };
18813
- case "message_end_assistant":
18814
- return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "assistant" };
18815
- case "message_start_tool_result":
18816
- return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "toolResult" };
18817
- case "message_end_tool_result":
18818
- return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "toolResult" };
18819
- case "turn_start":
18820
- return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "start" };
18821
- case "turn_end":
18822
- return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "end" };
18823
- case "text":
18824
- return { t, type: TIMELINE_EVENT_TYPES.TEXT };
18825
- case "agent_end":
18826
- case "message_done":
18827
- case "done":
18828
- return null;
18829
- default:
18830
- return null;
18831
- }
18832
- }
18833
- function createRunStartEvent(specialist, beadId) {
18834
- return {
18835
- t: Date.now(),
18836
- type: TIMELINE_EVENT_TYPES.RUN_START,
18837
- specialist,
18838
- bead_id: beadId
18839
- };
18840
- }
18841
- function createMetaEvent(model, backend) {
18842
- return {
18843
- t: Date.now(),
18844
- type: TIMELINE_EVENT_TYPES.META,
18845
- model,
18846
- backend
18847
- };
18777
+ // src/cli/install.ts
18778
+ var exports_install = {};
18779
+ __export(exports_install, {
18780
+ run: () => run
18781
+ });
18782
+ async function run() {
18783
+ console.log("");
18784
+ console.log(yellow("⚠ DEPRECATED: `specialists install` is deprecated"));
18785
+ console.log("");
18786
+ console.log(` Use ${bold("specialists init")} instead.`);
18787
+ console.log("");
18788
+ console.log(" The init command:");
18789
+ console.log(" • creates specialists/ and .specialists/ directories");
18790
+ console.log(" • registers the MCP server in .mcp.json");
18791
+ console.log(" • injects workflow context into AGENTS.md/CLAUDE.md");
18792
+ console.log("");
18793
+ console.log(` ${dim("Run: specialists init --help for full details")}`);
18794
+ console.log("");
18848
18795
  }
18849
- function createStaleWarningEvent(reason, options) {
18850
- return {
18851
- t: Date.now(),
18852
- type: TIMELINE_EVENT_TYPES.STALE_WARNING,
18853
- reason,
18854
- silence_ms: options.silence_ms,
18855
- threshold_ms: options.threshold_ms,
18856
- ...options.tool !== undefined ? { tool: options.tool } : {}
18857
- };
18796
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, dim = (s) => `\x1B[2m${s}\x1B[0m`;
18797
+
18798
+ // src/cli/version.ts
18799
+ var exports_version = {};
18800
+ __export(exports_version, {
18801
+ run: () => run2
18802
+ });
18803
+ import { createRequire as createRequire2 } from "node:module";
18804
+ import { fileURLToPath } from "node:url";
18805
+ import { dirname as dirname2, join as join4 } from "node:path";
18806
+ import { existsSync as existsSync4 } from "node:fs";
18807
+ async function run2() {
18808
+ const req = createRequire2(import.meta.url);
18809
+ const here = dirname2(fileURLToPath(import.meta.url));
18810
+ const bundlePkgPath = join4(here, "..", "package.json");
18811
+ const sourcePkgPath = join4(here, "..", "..", "package.json");
18812
+ let pkg;
18813
+ if (existsSync4(bundlePkgPath)) {
18814
+ pkg = req("../package.json");
18815
+ } else if (existsSync4(sourcePkgPath)) {
18816
+ pkg = req("../../package.json");
18817
+ } else {
18818
+ console.error("Cannot find package.json");
18819
+ process.exit(1);
18820
+ }
18821
+ console.log(`${pkg.name} v${pkg.version}`);
18858
18822
  }
18859
- function createRunCompleteEvent(status, elapsed_s, options) {
18823
+ var init_version = () => {};
18824
+
18825
+ // src/cli/list.ts
18826
+ var exports_list = {};
18827
+ __export(exports_list, {
18828
+ run: () => run3,
18829
+ parseArgs: () => parseArgs,
18830
+ ArgParseError: () => ArgParseError
18831
+ });
18832
+ import { spawnSync as spawnSync3 } from "node:child_process";
18833
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2 } from "node:fs";
18834
+ import { join as join5 } from "node:path";
18835
+ import readline from "node:readline";
18836
+ function toLiveJob(status) {
18837
+ if (!status)
18838
+ return null;
18839
+ if (status.status !== "running" && status.status !== "waiting" || !status.tmux_session) {
18840
+ return null;
18841
+ }
18842
+ const elapsedS = status.elapsed_s ?? Math.max(0, Math.floor((Date.now() - status.started_at_ms) / 1000));
18860
18843
  return {
18861
- t: Date.now(),
18862
- type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
18863
- status,
18864
- elapsed_s,
18865
- ...options
18844
+ id: status.id,
18845
+ specialist: status.specialist,
18846
+ status: status.status,
18847
+ tmuxSession: status.tmux_session,
18848
+ elapsedS,
18849
+ startedAtMs: status.started_at_ms
18866
18850
  };
18867
18851
  }
18868
- function parseTimelineEvent(line) {
18852
+ function readJobStatus(statusPath) {
18869
18853
  try {
18870
- const parsed = JSON.parse(line);
18871
- if (!parsed || typeof parsed !== "object")
18872
- return null;
18873
- if (typeof parsed.t !== "number")
18874
- return null;
18875
- if (typeof parsed.type !== "string")
18876
- return null;
18877
- if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
18878
- return {
18879
- t: parsed.t,
18880
- type: TIMELINE_EVENT_TYPES.DONE,
18881
- elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18882
- };
18883
- }
18884
- if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
18885
- return {
18886
- t: parsed.t,
18887
- type: TIMELINE_EVENT_TYPES.AGENT_END,
18888
- elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
18889
- };
18890
- }
18891
- const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
18892
- if (!knownTypes.includes(parsed.type))
18893
- return null;
18894
- return parsed;
18854
+ return JSON.parse(readFileSync2(statusPath, "utf-8"));
18895
18855
  } catch {
18896
18856
  return null;
18897
18857
  }
18898
18858
  }
18899
- function isRunCompleteEvent(event) {
18900
- return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
18901
- }
18902
- function compareTimelineEvents(a, b) {
18903
- return a.t - b.t;
18859
+ function listLiveJobs() {
18860
+ const jobsDir = join5(process.cwd(), ".specialists", "jobs");
18861
+ if (!existsSync5(jobsDir))
18862
+ return [];
18863
+ const jobs = readdirSync(jobsDir).map((entry) => toLiveJob(readJobStatus(join5(jobsDir, entry, "status.json")))).filter((job) => job !== null).sort((a, b) => b.startedAtMs - a.startedAtMs);
18864
+ return jobs;
18904
18865
  }
18905
- var TIMELINE_EVENT_TYPES;
18906
- var init_timeline_events = __esm(() => {
18907
- TIMELINE_EVENT_TYPES = {
18908
- RUN_START: "run_start",
18909
- META: "meta",
18910
- THINKING: "thinking",
18911
- TOOL: "tool",
18912
- TEXT: "text",
18913
- MESSAGE: "message",
18914
- TURN: "turn",
18915
- RUN_COMPLETE: "run_complete",
18916
- STALE_WARNING: "stale_warning",
18917
- DONE: "done",
18918
- AGENT_END: "agent_end"
18919
- };
18920
- });
18921
-
18922
- // src/specialist/supervisor.ts
18923
- import {
18924
- appendFileSync,
18925
- closeSync,
18926
- existsSync as existsSync4,
18927
- fsyncSync,
18928
- mkdirSync,
18929
- openSync,
18930
- readdirSync,
18931
- readFileSync as readFileSync2,
18932
- renameSync,
18933
- rmSync,
18934
- statSync,
18935
- writeFileSync,
18936
- writeSync
18937
- } from "node:fs";
18938
- import { join as join3 } from "node:path";
18939
- import { createInterface } from "node:readline";
18940
- import { createReadStream } from "node:fs";
18941
- import { spawnSync as spawnSync3, execFileSync } from "node:child_process";
18942
- function getCurrentGitSha() {
18943
- const result = spawnSync3("git", ["rev-parse", "HEAD"], {
18944
- encoding: "utf-8",
18945
- stdio: ["ignore", "pipe", "ignore"]
18946
- });
18947
- if (result.status !== 0)
18948
- return;
18949
- const sha = result.stdout?.trim();
18950
- return sha || undefined;
18866
+ function formatLiveChoice(job) {
18867
+ return `${job.tmuxSession} ${job.specialist} ${job.elapsedS}s ${job.status}`;
18951
18868
  }
18952
- function formatBeadNotes(result) {
18953
- const metadata = [
18954
- `prompt_hash=${result.promptHash}`,
18955
- `git_sha=${getCurrentGitSha() ?? "unknown"}`,
18956
- `elapsed_ms=${Math.round(result.durationMs)}`,
18957
- `model=${result.model}`,
18958
- `backend=${result.backend}`
18959
- ].join(`
18960
- `);
18961
- return `${result.output}
18962
-
18963
- ---
18964
- ${metadata}`;
18869
+ function renderLiveSelector(jobs, selectedIndex) {
18870
+ return [
18871
+ "",
18872
+ bold2("Select tmux session (↑/↓, Enter to attach, Ctrl+C to cancel)"),
18873
+ "",
18874
+ ...jobs.map((job, index) => `${index === selectedIndex ? cyan("❯") : " "} ${formatLiveChoice(job)}`),
18875
+ ""
18876
+ ];
18965
18877
  }
18966
-
18967
- class Supervisor {
18968
- opts;
18969
- constructor(opts) {
18970
- this.opts = opts;
18971
- }
18972
- jobDir(id) {
18973
- return join3(this.opts.jobsDir, id);
18974
- }
18975
- statusPath(id) {
18976
- return join3(this.jobDir(id), "status.json");
18977
- }
18978
- resultPath(id) {
18979
- return join3(this.jobDir(id), "result.txt");
18878
+ function selectLiveJob(jobs) {
18879
+ return new Promise((resolve2) => {
18880
+ const input = process.stdin;
18881
+ const output = process.stdout;
18882
+ const wasRawMode = input.isTTY ? input.isRaw : false;
18883
+ let selectedIndex = 0;
18884
+ let renderedLineCount = 0;
18885
+ const cleanup = (selected) => {
18886
+ input.off("keypress", onKeypress);
18887
+ if (input.isTTY && !wasRawMode) {
18888
+ input.setRawMode(false);
18889
+ }
18890
+ output.write("\x1B[?25h");
18891
+ if (renderedLineCount > 0) {
18892
+ readline.moveCursor(output, 0, -renderedLineCount);
18893
+ readline.clearScreenDown(output);
18894
+ }
18895
+ resolve2(selected);
18896
+ };
18897
+ const render = () => {
18898
+ if (renderedLineCount > 0) {
18899
+ readline.moveCursor(output, 0, -renderedLineCount);
18900
+ readline.clearScreenDown(output);
18901
+ }
18902
+ const lines = renderLiveSelector(jobs, selectedIndex);
18903
+ output.write(lines.join(`
18904
+ `));
18905
+ renderedLineCount = lines.length;
18906
+ };
18907
+ const onKeypress = (_, key) => {
18908
+ if (key.ctrl && key.name === "c") {
18909
+ cleanup(null);
18910
+ return;
18911
+ }
18912
+ if (key.name === "up") {
18913
+ selectedIndex = (selectedIndex - 1 + jobs.length) % jobs.length;
18914
+ render();
18915
+ return;
18916
+ }
18917
+ if (key.name === "down") {
18918
+ selectedIndex = (selectedIndex + 1) % jobs.length;
18919
+ render();
18920
+ return;
18921
+ }
18922
+ if (key.name === "return") {
18923
+ cleanup(jobs[selectedIndex]);
18924
+ }
18925
+ };
18926
+ readline.emitKeypressEvents(input);
18927
+ if (input.isTTY && !wasRawMode) {
18928
+ input.setRawMode(true);
18929
+ }
18930
+ output.write("\x1B[?25l");
18931
+ input.on("keypress", onKeypress);
18932
+ render();
18933
+ });
18934
+ }
18935
+ async function runLiveMode() {
18936
+ const jobs = listLiveJobs();
18937
+ if (jobs.length === 0) {
18938
+ console.log("No running tmux sessions found.");
18939
+ return;
18980
18940
  }
18981
- eventsPath(id) {
18982
- return join3(this.jobDir(id), "events.jsonl");
18941
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
18942
+ for (const job of jobs) {
18943
+ console.log(`${job.id} ${job.tmuxSession} ${job.status}`);
18944
+ }
18945
+ return;
18983
18946
  }
18984
- readyDir() {
18985
- return join3(this.opts.jobsDir, "..", "ready");
18947
+ const selected = await selectLiveJob(jobs);
18948
+ if (!selected)
18949
+ return;
18950
+ const attach = spawnSync3("tmux", ["attach-session", "-t", selected.tmuxSession], {
18951
+ stdio: "inherit"
18952
+ });
18953
+ if (attach.error) {
18954
+ console.error(`Failed to attach tmux session ${selected.tmuxSession}: ${attach.error.message}`);
18955
+ process.exit(1);
18986
18956
  }
18987
- readStatus(id) {
18988
- const path = this.statusPath(id);
18989
- if (!existsSync4(path))
18990
- return null;
18991
- try {
18992
- return JSON.parse(readFileSync2(path, "utf-8"));
18993
- } catch {
18994
- return null;
18957
+ }
18958
+ function parseArgs(argv) {
18959
+ const result = {};
18960
+ for (let i = 0;i < argv.length; i++) {
18961
+ const token = argv[i];
18962
+ if (token === "--category") {
18963
+ const value = argv[++i];
18964
+ if (!value || value.startsWith("--")) {
18965
+ throw new ArgParseError("--category requires a value");
18966
+ }
18967
+ result.category = value;
18968
+ continue;
18969
+ }
18970
+ if (token === "--scope") {
18971
+ const value = argv[++i];
18972
+ if (value !== "default" && value !== "user") {
18973
+ throw new ArgParseError(`--scope must be "default" or "user", got: "${value ?? ""}"`);
18974
+ }
18975
+ result.scope = value;
18976
+ continue;
18977
+ }
18978
+ if (token === "--json") {
18979
+ result.json = true;
18980
+ continue;
18981
+ }
18982
+ if (token === "--live") {
18983
+ result.live = true;
18984
+ continue;
18995
18985
  }
18996
18986
  }
18997
- listJobs() {
18998
- if (!existsSync4(this.opts.jobsDir))
18999
- return [];
19000
- const jobs = [];
19001
- for (const entry of readdirSync(this.opts.jobsDir)) {
19002
- const path = join3(this.opts.jobsDir, entry, "status.json");
19003
- if (!existsSync4(path))
19004
- continue;
19005
- try {
19006
- jobs.push(JSON.parse(readFileSync2(path, "utf-8")));
19007
- } catch {}
18987
+ return result;
18988
+ }
18989
+ async function run3() {
18990
+ let args;
18991
+ try {
18992
+ args = parseArgs(process.argv.slice(3));
18993
+ } catch (err) {
18994
+ if (err instanceof ArgParseError) {
18995
+ console.error(`Error: ${err.message}`);
18996
+ process.exit(1);
19008
18997
  }
19009
- return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
18998
+ throw err;
19010
18999
  }
19011
- writeStatusFile(id, data) {
19012
- mkdirSync(this.jobDir(id), { recursive: true });
19013
- const path = this.statusPath(id);
19014
- const tmp = path + ".tmp";
19015
- writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
19016
- renameSync(tmp, path);
19000
+ if (args.live) {
19001
+ await runLiveMode();
19002
+ return;
19017
19003
  }
19018
- updateStatus(id, updates) {
19019
- const current = this.readStatus(id);
19020
- if (!current)
19021
- return;
19022
- this.writeStatusFile(id, { ...current, ...updates });
19004
+ const loader = new SpecialistLoader;
19005
+ let specialists = await loader.list(args.category);
19006
+ if (args.scope) {
19007
+ specialists = specialists.filter((s) => s.scope === args.scope);
19023
19008
  }
19024
- gc() {
19025
- if (!existsSync4(this.opts.jobsDir))
19026
- return;
19027
- const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
19028
- for (const entry of readdirSync(this.opts.jobsDir)) {
19029
- const dir = join3(this.opts.jobsDir, entry);
19030
- try {
19031
- const stat2 = statSync(dir);
19032
- if (!stat2.isDirectory())
19033
- continue;
19034
- if (stat2.mtimeMs < cutoff)
19035
- rmSync(dir, { recursive: true, force: true });
19036
- } catch {}
19037
- }
19009
+ if (args.json) {
19010
+ console.log(JSON.stringify(specialists, null, 2));
19011
+ return;
19038
19012
  }
19039
- crashRecovery() {
19040
- if (!existsSync4(this.opts.jobsDir))
19041
- return;
19042
- const thresholds = {
19043
- ...STALL_DETECTION_DEFAULTS,
19044
- ...this.opts.stallDetection
19045
- };
19046
- const now = Date.now();
19047
- for (const entry of readdirSync(this.opts.jobsDir)) {
19048
- const statusPath = join3(this.opts.jobsDir, entry, "status.json");
19049
- if (!existsSync4(statusPath))
19050
- continue;
19051
- try {
19052
- const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
19053
- if (s.status === "running" || s.status === "starting") {
19054
- if (!s.pid)
19055
- continue;
19056
- let pidAlive = true;
19057
- try {
19058
- process.kill(s.pid, 0);
19059
- } catch {
19060
- pidAlive = false;
19061
- }
19062
- if (!pidAlive) {
19063
- const tmp = statusPath + ".tmp";
19064
- const updated = { ...s, status: "error", error: "Process crashed or was killed" };
19065
- writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
19066
- renameSync(tmp, statusPath);
19067
- } else if (s.status === "running") {
19068
- const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
19069
- const silenceMs = now - lastEventAt;
19070
- if (silenceMs > thresholds.running_silence_error_ms) {
19071
- const tmp = statusPath + ".tmp";
19072
- const updated = {
19073
- ...s,
19074
- status: "error",
19075
- error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
19076
- };
19077
- writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
19078
- renameSync(tmp, statusPath);
19079
- }
19080
- }
19081
- } else if (s.status === "waiting") {
19082
- const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
19083
- const silenceMs = now - lastEventAt;
19084
- if (silenceMs > thresholds.waiting_stale_ms) {
19085
- const eventsPath = join3(this.opts.jobsDir, entry, "events.jsonl");
19086
- const event = createStaleWarningEvent("waiting_stale", {
19087
- silence_ms: silenceMs,
19088
- threshold_ms: thresholds.waiting_stale_ms
19089
- });
19090
- try {
19091
- appendFileSync(eventsPath, JSON.stringify(event) + `
19092
- `);
19093
- } catch {}
19094
- }
19095
- }
19096
- } catch {}
19097
- }
19013
+ if (specialists.length === 0) {
19014
+ console.log("No specialists found.");
19015
+ return;
19098
19016
  }
19099
- async run() {
19100
- const { runner, runOptions, jobsDir } = this.opts;
19101
- this.gc();
19102
- this.crashRecovery();
19103
- const id = crypto.randomUUID().slice(0, 6);
19104
- const dir = this.jobDir(id);
19105
- const startedAtMs = Date.now();
19106
- mkdirSync(dir, { recursive: true });
19107
- mkdirSync(this.readyDir(), { recursive: true });
19108
- const initialStatus = {
19109
- id,
19110
- specialist: runOptions.name,
19111
- status: "starting",
19112
- started_at_ms: startedAtMs,
19113
- pid: process.pid,
19114
- ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
19115
- ...process.env.SPECIALISTS_TMUX_SESSION ? { tmux_session: process.env.SPECIALISTS_TMUX_SESSION } : {}
19116
- };
19117
- this.writeStatusFile(id, initialStatus);
19118
- writeFileSync(join3(this.opts.jobsDir, "latest"), `${id}
19119
- `, "utf-8");
19120
- this.opts.onJobStarted?.({ id });
19121
- let statusSnapshot = initialStatus;
19122
- const setStatus = (updates) => {
19123
- statusSnapshot = { ...statusSnapshot, ...updates };
19124
- this.writeStatusFile(id, statusSnapshot);
19125
- };
19126
- const eventsFd = openSync(this.eventsPath(id), "a");
19127
- const appendTimelineEvent = (event) => {
19128
- try {
19129
- writeSync(eventsFd, JSON.stringify(event) + `
19017
+ const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
19018
+ console.log(`
19019
+ ${bold2(`Specialists (${specialists.length})`)}
19130
19020
  `);
19131
- } catch (err) {
19132
- console.error(`[supervisor] Failed to write event: ${err?.message ?? err}`);
19133
- }
19134
- };
19135
- appendTimelineEvent(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
19136
- const fifoPath = join3(dir, "steer.pipe");
19137
- try {
19138
- execFileSync("mkfifo", [fifoPath]);
19139
- setStatus({ fifo_path: fifoPath });
19140
- } catch {}
19141
- let textLogged = false;
19142
- let currentTool = "";
19143
- let currentToolCallId = "";
19144
- let currentToolArgs;
19145
- let currentToolIsError = false;
19146
- const activeToolCalls = new Map;
19147
- let killFn;
19148
- let steerFn;
19149
- let resumeFn;
19150
- let closeFn;
19151
- let fifoReadStream;
19152
- let fifoReadline;
19153
- const thresholds = {
19154
- ...STALL_DETECTION_DEFAULTS,
19155
- ...this.opts.stallDetection
19156
- };
19157
- let lastActivityMs = startedAtMs;
19158
- let silenceWarnEmitted = false;
19159
- let toolStartMs;
19160
- let toolDurationWarnEmitted = false;
19161
- let stuckIntervalId;
19162
- stuckIntervalId = setInterval(() => {
19163
- const now = Date.now();
19164
- if (statusSnapshot.status === "running") {
19165
- const silenceMs = now - lastActivityMs;
19166
- if (!silenceWarnEmitted && silenceMs > thresholds.running_silence_warn_ms) {
19167
- silenceWarnEmitted = true;
19168
- appendTimelineEvent(createStaleWarningEvent("running_silence", {
19169
- silence_ms: silenceMs,
19170
- threshold_ms: thresholds.running_silence_warn_ms
19171
- }));
19172
- }
19173
- if (silenceMs > thresholds.running_silence_error_ms) {
19174
- appendTimelineEvent(createStaleWarningEvent("running_silence_error", {
19175
- silence_ms: silenceMs,
19176
- threshold_ms: thresholds.running_silence_error_ms
19177
- }));
19178
- setStatus({
19179
- status: "error",
19180
- error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
19181
- });
19182
- killFn?.();
19183
- clearInterval(stuckIntervalId);
19184
- }
19185
- }
19186
- if (toolStartMs !== undefined && !toolDurationWarnEmitted) {
19187
- const toolDurationMs = now - toolStartMs;
19188
- if (toolDurationMs > thresholds.tool_duration_warn_ms) {
19189
- toolDurationWarnEmitted = true;
19190
- appendTimelineEvent(createStaleWarningEvent("tool_duration", {
19191
- silence_ms: toolDurationMs,
19192
- threshold_ms: thresholds.tool_duration_warn_ms,
19193
- tool: currentTool
19194
- }));
19195
- }
19196
- }
19197
- }, 1e4);
19198
- const sigtermHandler = () => killFn?.();
19199
- process.once("SIGTERM", sigtermHandler);
19200
- try {
19201
- const result = await runner.run(runOptions, (delta) => {
19202
- const toolMatch = delta.match(/⚙ (.+?)…/);
19203
- if (toolMatch) {
19204
- currentTool = toolMatch[1];
19205
- setStatus({ current_tool: currentTool });
19206
- }
19207
- this.opts.onProgress?.(delta);
19208
- }, (eventType) => {
19209
- const now = Date.now();
19210
- lastActivityMs = now;
19211
- silenceWarnEmitted = false;
19212
- setStatus({
19213
- status: "running",
19214
- current_event: eventType,
19215
- last_event_at_ms: now,
19216
- elapsed_s: Math.round((now - startedAtMs) / 1000)
19217
- });
19218
- const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
19219
- tool: currentTool,
19220
- toolCallId: currentToolCallId || undefined,
19221
- args: currentToolArgs,
19222
- isError: currentToolIsError
19223
- });
19224
- if (timelineEvent) {
19225
- appendTimelineEvent(timelineEvent);
19226
- } else if (eventType === "text" && !textLogged) {
19227
- textLogged = true;
19228
- appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
19229
- }
19230
- }, (meta) => {
19231
- setStatus({ model: meta.model, backend: meta.backend });
19232
- appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
19233
- this.opts.onMeta?.(meta);
19234
- }, (fn) => {
19235
- killFn = fn;
19236
- }, (beadId) => {
19237
- setStatus({ bead_id: beadId });
19238
- }, (fn) => {
19239
- steerFn = fn;
19240
- if (!existsSync4(fifoPath))
19241
- return;
19242
- fifoReadStream = createReadStream(fifoPath, { flags: "r+" });
19243
- fifoReadline = createInterface({ input: fifoReadStream });
19244
- fifoReadline.on("line", (line) => {
19245
- try {
19246
- const parsed = JSON.parse(line);
19247
- if (parsed?.type === "steer" && typeof parsed.message === "string") {
19248
- steerFn?.(parsed.message).catch(() => {});
19249
- } else if (parsed?.type === "resume" && typeof parsed.task === "string") {
19250
- if (resumeFn) {
19251
- setStatus({ status: "running", current_event: "starting" });
19252
- resumeFn(parsed.task).then((output) => {
19253
- mkdirSync(this.jobDir(id), { recursive: true });
19254
- writeFileSync(this.resultPath(id), output, "utf-8");
19255
- setStatus({
19256
- status: "waiting",
19257
- current_event: "waiting",
19258
- elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
19259
- last_event_at_ms: Date.now()
19260
- });
19261
- }).catch((err) => {
19262
- setStatus({ status: "error", error: err?.message ?? String(err) });
19263
- });
19264
- }
19265
- } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
19266
- console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
19267
- if (resumeFn) {
19268
- setStatus({ status: "running", current_event: "starting" });
19269
- resumeFn(parsed.message).then((output) => {
19270
- mkdirSync(this.jobDir(id), { recursive: true });
19271
- writeFileSync(this.resultPath(id), output, "utf-8");
19272
- setStatus({
19273
- status: "waiting",
19274
- current_event: "waiting",
19275
- elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
19276
- last_event_at_ms: Date.now()
19277
- });
19278
- }).catch((err) => {
19279
- setStatus({ status: "error", error: err?.message ?? String(err) });
19280
- });
19281
- }
19282
- } else if (parsed?.type === "close") {
19283
- closeFn?.().catch(() => {});
19284
- }
19285
- } catch {}
19286
- });
19287
- fifoReadline.on("error", () => {});
19288
- }, (rFn, cFn) => {
19289
- resumeFn = rFn;
19290
- closeFn = cFn;
19291
- setStatus({ status: "waiting", current_event: "waiting" });
19292
- }, (tool, args, toolCallId) => {
19293
- currentTool = tool;
19294
- currentToolArgs = args;
19295
- currentToolCallId = toolCallId ?? "";
19296
- currentToolIsError = false;
19297
- toolStartMs = Date.now();
19298
- toolDurationWarnEmitted = false;
19299
- setStatus({ current_tool: tool });
19300
- if (toolCallId) {
19301
- activeToolCalls.set(toolCallId, { tool, args });
19302
- }
19303
- }, (tool, isError, toolCallId) => {
19304
- if (toolCallId && activeToolCalls.has(toolCallId)) {
19305
- const entry = activeToolCalls.get(toolCallId);
19306
- currentTool = entry.tool;
19307
- currentToolArgs = entry.args;
19308
- currentToolCallId = toolCallId;
19309
- activeToolCalls.delete(toolCallId);
19310
- } else {
19311
- currentTool = tool;
19312
- }
19313
- currentToolIsError = isError;
19314
- toolStartMs = undefined;
19315
- toolDurationWarnEmitted = false;
19316
- });
19317
- const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
19318
- mkdirSync(this.jobDir(id), { recursive: true });
19319
- writeFileSync(this.resultPath(id), result.output, "utf-8");
19320
- const inputBeadId = runOptions.inputBeadId;
19321
- const ownsBead = Boolean(result.beadId && !inputBeadId);
19322
- const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
19323
- const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && result.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
19324
- if (ownsBead && result.beadId) {
19325
- this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19326
- } else if (shouldWriteExternalBeadNotes) {
19327
- if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
19328
- this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(result));
19329
- } else if (result.beadId) {
19330
- this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
19331
- }
19332
- }
19333
- if (result.beadId) {
19334
- if (!inputBeadId) {
19335
- this.opts.beadsClient?.closeBead(result.beadId, "COMPLETE", result.durationMs, result.model);
19336
- }
19337
- }
19338
- setStatus({
19339
- status: "done",
19340
- elapsed_s: elapsed,
19341
- last_event_at_ms: Date.now(),
19342
- model: result.model,
19343
- backend: result.backend,
19344
- bead_id: result.beadId
19345
- });
19346
- appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
19347
- model: result.model,
19348
- backend: result.backend,
19349
- bead_id: result.beadId,
19350
- output: result.output
19351
- }));
19352
- mkdirSync(this.readyDir(), { recursive: true });
19353
- writeFileSync(join3(this.readyDir(), id), "", "utf-8");
19354
- return id;
19355
- } catch (err) {
19356
- const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
19357
- const errorMsg = err?.message ?? String(err);
19358
- setStatus({
19359
- status: "error",
19360
- elapsed_s: elapsed,
19361
- error: errorMsg
19362
- });
19363
- appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
19364
- error: errorMsg
19365
- }));
19366
- throw err;
19367
- } finally {
19368
- if (stuckIntervalId !== undefined)
19369
- clearInterval(stuckIntervalId);
19370
- process.removeListener("SIGTERM", sigtermHandler);
19371
- try {
19372
- fifoReadline?.close();
19373
- } catch {}
19374
- try {
19375
- fifoReadStream?.destroy();
19376
- } catch {}
19377
- try {
19378
- fsyncSync(eventsFd);
19379
- } catch {}
19380
- closeSync(eventsFd);
19381
- try {
19382
- if (existsSync4(fifoPath))
19383
- rmSync(fifoPath);
19384
- } catch {}
19385
- if (statusSnapshot.tmux_session) {
19386
- spawnSync3("tmux", ["kill-session", "-t", statusSnapshot.tmux_session], { stdio: "ignore" });
19387
- }
19388
- }
19021
+ for (const s of specialists) {
19022
+ const name = cyan(s.name.padEnd(nameWidth));
19023
+ const scopeTag = s.scope === "default" ? green("[default]") : yellow2("[user]");
19024
+ const model = dim2(s.model);
19025
+ const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
19026
+ console.log(` ${name} ${scopeTag} ${model}`);
19027
+ console.log(` ${" ".repeat(nameWidth)} ${dim2(desc)}`);
19028
+ console.log();
19389
19029
  }
19390
19030
  }
19391
- var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS;
19392
- var init_supervisor = __esm(() => {
19393
- init_timeline_events();
19394
- JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
19395
- STALL_DETECTION_DEFAULTS = {
19396
- running_silence_warn_ms: 60000,
19397
- running_silence_error_ms: 300000,
19398
- waiting_stale_ms: 3600000,
19399
- tool_duration_warn_ms: 120000
19031
+ var dim2 = (s) => `\x1B[2m${s}\x1B[0m`, bold2 = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
19032
+ var init_list = __esm(() => {
19033
+ init_loader();
19034
+ ArgParseError = class ArgParseError extends Error {
19035
+ constructor(message) {
19036
+ super(message);
19037
+ this.name = "ArgParseError";
19038
+ }
19400
19039
  };
19401
19040
  });
19402
19041
 
19403
- // src/specialist/timeline-query.ts
19404
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
19405
- import { join as join8 } from "node:path";
19406
- function readJobEvents(jobDir) {
19407
- const eventsPath = join8(jobDir, "events.jsonl");
19408
- if (!existsSync5(eventsPath))
19409
- return [];
19410
- const content = readFileSync3(eventsPath, "utf-8");
19411
- const lines = content.split(`
19412
- `).filter(Boolean);
19413
- const events = [];
19414
- for (const line of lines) {
19415
- const event = parseTimelineEvent(line);
19416
- if (event)
19417
- events.push(event);
19418
- }
19419
- events.sort(compareTimelineEvents);
19420
- return events;
19421
- }
19422
- function readJobEventsById(jobsDir, jobId) {
19423
- return readJobEvents(join8(jobsDir, jobId));
19042
+ // src/cli/models.ts
19043
+ var exports_models = {};
19044
+ __export(exports_models, {
19045
+ run: () => run4
19046
+ });
19047
+ import { spawnSync as spawnSync4 } from "node:child_process";
19048
+ function parsePiModels() {
19049
+ const r = spawnSync4("pi", ["--list-models"], {
19050
+ encoding: "utf8",
19051
+ stdio: "pipe",
19052
+ timeout: 8000
19053
+ });
19054
+ if (r.status !== 0 || r.error)
19055
+ return null;
19056
+ return r.stdout.split(`
19057
+ `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
19058
+ const cols = line.split(/\s+/);
19059
+ return {
19060
+ provider: cols[0] ?? "",
19061
+ model: cols[1] ?? "",
19062
+ context: cols[2] ?? "",
19063
+ maxOut: cols[3] ?? "",
19064
+ thinking: cols[4] === "yes",
19065
+ images: cols[5] === "yes"
19066
+ };
19067
+ }).filter((m) => m.provider && m.model);
19424
19068
  }
19425
- function readAllJobEvents(jobsDir) {
19426
- if (!existsSync5(jobsDir))
19427
- return [];
19428
- const batches = [];
19429
- const entries = readdirSync2(jobsDir);
19430
- for (const entry of entries) {
19431
- const jobDir = join8(jobsDir, entry);
19432
- try {
19433
- const stat2 = __require("node:fs").statSync(jobDir);
19434
- if (!stat2.isDirectory())
19435
- continue;
19436
- } catch {
19069
+ function parseArgs2(argv) {
19070
+ const out = {};
19071
+ for (let i = 0;i < argv.length; i++) {
19072
+ if (argv[i] === "--provider" && argv[i + 1]) {
19073
+ out.provider = argv[++i];
19437
19074
  continue;
19438
19075
  }
19439
- const jobId = entry;
19440
- const statusPath = join8(jobDir, "status.json");
19441
- let specialist = "unknown";
19442
- let beadId;
19443
- if (existsSync5(statusPath)) {
19444
- try {
19445
- const status = JSON.parse(readFileSync3(statusPath, "utf-8"));
19446
- specialist = status.specialist ?? "unknown";
19447
- beadId = status.bead_id;
19448
- } catch {}
19449
- }
19450
- const events = readJobEvents(jobDir);
19451
- if (events.length > 0) {
19452
- batches.push({ jobId, specialist, beadId, events });
19076
+ if (argv[i] === "--used") {
19077
+ out.used = true;
19078
+ continue;
19453
19079
  }
19454
19080
  }
19455
- return batches;
19081
+ return out;
19456
19082
  }
19457
- function mergeTimelineEvents(batches) {
19458
- const merged = [];
19459
- for (const batch of batches) {
19460
- for (const event of batch.events) {
19461
- merged.push({
19462
- jobId: batch.jobId,
19463
- specialist: batch.specialist,
19464
- beadId: batch.beadId,
19465
- event
19466
- });
19467
- }
19083
+ async function run4() {
19084
+ const args = parseArgs2(process.argv.slice(3));
19085
+ const loader = new SpecialistLoader;
19086
+ const specialists = await loader.list();
19087
+ const usedBy = new Map;
19088
+ for (const s of specialists) {
19089
+ const key = s.model;
19090
+ if (!usedBy.has(key))
19091
+ usedBy.set(key, []);
19092
+ usedBy.get(key).push(s.name);
19468
19093
  }
19469
- merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
19470
- return merged;
19471
- }
19472
- function filterTimelineEvents(merged, filter) {
19473
- let result = merged;
19474
- if (filter.since !== undefined) {
19475
- result = result.filter(({ event }) => event.t >= filter.since);
19094
+ const allModels = parsePiModels();
19095
+ if (!allModels) {
19096
+ console.error("pi not found or failed — install and configure pi first");
19097
+ process.exit(1);
19476
19098
  }
19477
- if (filter.jobId !== undefined) {
19478
- result = result.filter(({ jobId }) => jobId === filter.jobId);
19099
+ let models = allModels;
19100
+ if (args.provider) {
19101
+ models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
19479
19102
  }
19480
- if (filter.specialist !== undefined) {
19481
- result = result.filter(({ specialist }) => specialist === filter.specialist);
19103
+ if (args.used) {
19104
+ models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
19482
19105
  }
19483
- if (filter.limit !== undefined && filter.limit > 0) {
19484
- result = result.slice(0, filter.limit);
19106
+ if (models.length === 0) {
19107
+ console.log("No models match.");
19108
+ return;
19485
19109
  }
19486
- return result;
19487
- }
19488
- function queryTimeline(jobsDir, filter = {}) {
19489
- let batches = readAllJobEvents(jobsDir);
19490
- if (filter.jobId !== undefined) {
19491
- batches = batches.filter((b) => b.jobId === filter.jobId);
19110
+ const byProvider = new Map;
19111
+ for (const m of models) {
19112
+ if (!byProvider.has(m.provider))
19113
+ byProvider.set(m.provider, []);
19114
+ byProvider.get(m.provider).push(m);
19492
19115
  }
19493
- if (filter.specialist !== undefined) {
19494
- batches = batches.filter((b) => b.specialist === filter.specialist);
19116
+ const total = models.length;
19117
+ console.log(`
19118
+ ${bold3(`Models on pi`)} ${dim3(`(${total} total)`)}
19119
+ `);
19120
+ for (const [provider, pModels] of byProvider) {
19121
+ console.log(` ${cyan2(provider)} ${dim3(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
19122
+ const modelWidth = Math.max(...pModels.map((m) => m.model.length));
19123
+ for (const m of pModels) {
19124
+ const key = `${m.provider}/${m.model}`;
19125
+ const inUse = usedBy.get(key);
19126
+ const flags = [
19127
+ m.thinking ? green2("thinking") : dim3("·"),
19128
+ m.images ? dim3("images") : ""
19129
+ ].filter(Boolean).join(" ");
19130
+ const ctx = dim3(`ctx ${m.context}`);
19131
+ const usedLabel = inUse ? ` ${yellow3("←")} ${dim3(inUse.join(", "))}` : "";
19132
+ console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
19133
+ }
19134
+ console.log();
19135
+ }
19136
+ if (!args.used) {
19137
+ console.log(dim3(` --provider <name> filter by provider`));
19138
+ console.log(dim3(` --used only show models used by your specialists`));
19139
+ console.log();
19495
19140
  }
19496
- const merged = mergeTimelineEvents(batches);
19497
- return filterTimelineEvents(merged, filter);
19498
- }
19499
- function isJobComplete(events) {
19500
- return events.some((e) => e.type === "run_complete");
19501
19141
  }
19502
- var init_timeline_query = __esm(() => {
19503
- init_timeline_events();
19142
+ var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`;
19143
+ var init_models = __esm(() => {
19144
+ init_loader();
19504
19145
  });
19505
19146
 
19506
- // src/specialist/model-display.ts
19507
- function extractModelId(model) {
19508
- if (!model)
19509
- return;
19510
- const trimmed = model.trim();
19511
- if (!trimmed)
19512
- return;
19513
- return trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
19147
+ // src/cli/init.ts
19148
+ var exports_init = {};
19149
+ __export(exports_init, {
19150
+ run: () => run5
19151
+ });
19152
+ import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
19153
+ import { basename as basename2, join as join6 } from "node:path";
19154
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19155
+ function ok(msg) {
19156
+ console.log(` ${green3("✓")} ${msg}`);
19514
19157
  }
19515
- function toModelAlias(model) {
19516
- const modelId = extractModelId(model);
19517
- if (!modelId)
19518
- return;
19519
- if (modelId.startsWith("claude-")) {
19520
- return modelId.slice("claude-".length);
19158
+ function skip(msg) {
19159
+ console.log(` ${yellow4("○")} ${msg}`);
19160
+ }
19161
+ function loadJson(path, fallback) {
19162
+ if (!existsSync6(path))
19163
+ return structuredClone(fallback);
19164
+ try {
19165
+ return JSON.parse(readFileSync3(path, "utf-8"));
19166
+ } catch {
19167
+ return structuredClone(fallback);
19521
19168
  }
19522
- return modelId;
19523
19169
  }
19524
- function formatSpecialistModel(specialist, model) {
19525
- const alias = toModelAlias(model);
19526
- return alias ? `${specialist}/${alias}` : specialist;
19170
+ function saveJson(path, value) {
19171
+ writeFileSync(path, JSON.stringify(value, null, 2) + `
19172
+ `, "utf-8");
19527
19173
  }
19528
-
19529
- // src/cli/install.ts
19530
- var exports_install = {};
19531
- __export(exports_install, {
19532
- run: () => run
19533
- });
19534
- async function run() {
19535
- console.log("");
19536
- console.log(yellow("⚠ DEPRECATED: `specialists install` is deprecated"));
19537
- console.log("");
19538
- console.log(` Use ${bold("specialists init")} instead.`);
19539
- console.log("");
19540
- console.log(" The init command:");
19541
- console.log(" • creates specialists/ and .specialists/ directories");
19542
- console.log(" • registers the MCP server in .mcp.json");
19543
- console.log(" • injects workflow context into AGENTS.md/CLAUDE.md");
19544
- console.log("");
19545
- console.log(` ${dim("Run: specialists init --help for full details")}`);
19546
- console.log("");
19174
+ function resolvePackagePath(relativePath) {
19175
+ const configPath = `config/${relativePath}`;
19176
+ let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19177
+ if (existsSync6(resolved))
19178
+ return resolved;
19179
+ resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19180
+ if (existsSync6(resolved))
19181
+ return resolved;
19182
+ return null;
19547
19183
  }
19548
- var bold = (s) => `\x1B[1m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, dim = (s) => `\x1B[2m${s}\x1B[0m`;
19549
-
19550
- // src/cli/version.ts
19551
- var exports_version = {};
19552
- __export(exports_version, {
19553
- run: () => run2
19554
- });
19555
- import { createRequire as createRequire2 } from "node:module";
19556
- import { fileURLToPath } from "node:url";
19557
- import { dirname as dirname2, join as join12 } from "node:path";
19558
- import { existsSync as existsSync8 } from "node:fs";
19559
- async function run2() {
19560
- const req = createRequire2(import.meta.url);
19561
- const here = dirname2(fileURLToPath(import.meta.url));
19562
- const bundlePkgPath = join12(here, "..", "package.json");
19563
- const sourcePkgPath = join12(here, "..", "..", "package.json");
19564
- let pkg;
19565
- if (existsSync8(bundlePkgPath)) {
19566
- pkg = req("../package.json");
19567
- } else if (existsSync8(sourcePkgPath)) {
19568
- pkg = req("../../package.json");
19569
- } else {
19570
- console.error("Cannot find package.json");
19571
- process.exit(1);
19184
+ function migrateLegacySpecialists(cwd, scope) {
19185
+ const sourceDir = join6(cwd, ".specialists", scope, "specialists");
19186
+ if (!existsSync6(sourceDir))
19187
+ return;
19188
+ const targetDir = join6(cwd, ".specialists", scope);
19189
+ if (!existsSync6(targetDir)) {
19190
+ mkdirSync(targetDir, { recursive: true });
19572
19191
  }
19573
- console.log(`${pkg.name} v${pkg.version}`);
19574
- }
19575
- var init_version = () => {};
19576
-
19577
- // src/cli/list.ts
19578
- var exports_list = {};
19579
- __export(exports_list, {
19580
- run: () => run3,
19581
- parseArgs: () => parseArgs,
19582
- ArgParseError: () => ArgParseError
19583
- });
19584
- import { spawnSync as spawnSync5 } from "node:child_process";
19585
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
19586
- import { join as join13 } from "node:path";
19587
- import readline from "node:readline";
19588
- function toLiveJob(status) {
19589
- if (!status)
19590
- return null;
19591
- if (status.status !== "running" && status.status !== "waiting" || !status.tmux_session) {
19592
- return null;
19192
+ const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19193
+ if (files.length === 0)
19194
+ return;
19195
+ let moved = 0;
19196
+ let skipped = 0;
19197
+ for (const file of files) {
19198
+ const src = join6(sourceDir, file);
19199
+ const dest = join6(targetDir, file);
19200
+ if (existsSync6(dest)) {
19201
+ skipped++;
19202
+ continue;
19203
+ }
19204
+ renameSync(src, dest);
19205
+ moved++;
19593
19206
  }
19594
- const elapsedS = status.elapsed_s ?? Math.max(0, Math.floor((Date.now() - status.started_at_ms) / 1000));
19595
- return {
19596
- id: status.id,
19597
- specialist: status.specialist,
19598
- status: status.status,
19599
- tmuxSession: status.tmux_session,
19600
- elapsedS,
19601
- startedAtMs: status.started_at_ms
19602
- };
19603
- }
19604
- function readJobStatus(statusPath) {
19605
- try {
19606
- return JSON.parse(readFileSync5(statusPath, "utf-8"));
19607
- } catch {
19608
- return null;
19207
+ if (moved > 0) {
19208
+ ok(`migrated ${moved} specialist${moved === 1 ? "" : "s"} from .specialists/${scope}/specialists/ to .specialists/${scope}/`);
19209
+ }
19210
+ if (skipped > 0) {
19211
+ skip(`${skipped} legacy specialist${skipped === 1 ? "" : "s"} already exist in .specialists/${scope}/ (not moved)`);
19609
19212
  }
19610
19213
  }
19611
- function listLiveJobs() {
19612
- const jobsDir = join13(process.cwd(), ".specialists", "jobs");
19613
- if (!existsSync9(jobsDir))
19614
- return [];
19615
- const jobs = readdirSync3(jobsDir).map((entry) => toLiveJob(readJobStatus(join13(jobsDir, entry, "status.json")))).filter((job) => job !== null).sort((a, b) => b.startedAtMs - a.startedAtMs);
19616
- return jobs;
19617
- }
19618
- function formatLiveChoice(job) {
19619
- return `${job.tmuxSession} ${job.specialist} ${job.elapsedS}s ${job.status}`;
19620
- }
19621
- function renderLiveSelector(jobs, selectedIndex) {
19622
- return [
19623
- "",
19624
- bold2("Select tmux session (↑/↓, Enter to attach, Ctrl+C to cancel)"),
19625
- "",
19626
- ...jobs.map((job, index) => `${index === selectedIndex ? cyan("❯") : " "} ${formatLiveChoice(job)}`),
19627
- ""
19628
- ];
19629
- }
19630
- function selectLiveJob(jobs) {
19631
- return new Promise((resolve2) => {
19632
- const input = process.stdin;
19633
- const output = process.stdout;
19634
- const wasRawMode = input.isTTY ? input.isRaw : false;
19635
- let selectedIndex = 0;
19636
- let renderedLineCount = 0;
19637
- const cleanup = (selected) => {
19638
- input.off("keypress", onKeypress);
19639
- if (input.isTTY && !wasRawMode) {
19640
- input.setRawMode(false);
19641
- }
19642
- output.write("\x1B[?25h");
19643
- if (renderedLineCount > 0) {
19644
- readline.moveCursor(output, 0, -renderedLineCount);
19645
- readline.clearScreenDown(output);
19646
- }
19647
- resolve2(selected);
19648
- };
19649
- const render = () => {
19650
- if (renderedLineCount > 0) {
19651
- readline.moveCursor(output, 0, -renderedLineCount);
19652
- readline.clearScreenDown(output);
19653
- }
19654
- const lines = renderLiveSelector(jobs, selectedIndex);
19655
- output.write(lines.join(`
19656
- `));
19657
- renderedLineCount = lines.length;
19658
- };
19659
- const onKeypress = (_, key) => {
19660
- if (key.ctrl && key.name === "c") {
19661
- cleanup(null);
19662
- return;
19663
- }
19664
- if (key.name === "up") {
19665
- selectedIndex = (selectedIndex - 1 + jobs.length) % jobs.length;
19666
- render();
19667
- return;
19668
- }
19669
- if (key.name === "down") {
19670
- selectedIndex = (selectedIndex + 1) % jobs.length;
19671
- render();
19672
- return;
19673
- }
19674
- if (key.name === "return") {
19675
- cleanup(jobs[selectedIndex]);
19676
- }
19677
- };
19678
- readline.emitKeypressEvents(input);
19679
- if (input.isTTY && !wasRawMode) {
19680
- input.setRawMode(true);
19681
- }
19682
- output.write("\x1B[?25l");
19683
- input.on("keypress", onKeypress);
19684
- render();
19685
- });
19686
- }
19687
- async function runLiveMode() {
19688
- const jobs = listLiveJobs();
19689
- if (jobs.length === 0) {
19690
- console.log("No running tmux sessions found.");
19214
+ function copyCanonicalSpecialists(cwd) {
19215
+ const sourceDir = resolvePackagePath("specialists");
19216
+ if (!sourceDir) {
19217
+ skip("no canonical specialists found in package");
19691
19218
  return;
19692
19219
  }
19693
- if (!process.stdout.isTTY || !process.stdin.isTTY) {
19694
- for (const job of jobs) {
19695
- console.log(`${job.id} ${job.tmuxSession} ${job.status}`);
19696
- }
19220
+ const targetDir = join6(cwd, ".specialists", "default");
19221
+ const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19222
+ if (files.length === 0) {
19223
+ skip("no specialist files found in package");
19697
19224
  return;
19698
19225
  }
19699
- const selected = await selectLiveJob(jobs);
19700
- if (!selected)
19701
- return;
19702
- const attach = spawnSync5("tmux", ["attach-session", "-t", selected.tmuxSession], {
19703
- stdio: "inherit"
19704
- });
19705
- if (attach.error) {
19706
- console.error(`Failed to attach tmux session ${selected.tmuxSession}: ${attach.error.message}`);
19707
- process.exit(1);
19226
+ if (!existsSync6(targetDir)) {
19227
+ mkdirSync(targetDir, { recursive: true });
19708
19228
  }
19709
- }
19710
- function parseArgs(argv) {
19711
- const result = {};
19712
- for (let i = 0;i < argv.length; i++) {
19713
- const token = argv[i];
19714
- if (token === "--category") {
19715
- const value = argv[++i];
19716
- if (!value || value.startsWith("--")) {
19717
- throw new ArgParseError("--category requires a value");
19718
- }
19719
- result.category = value;
19720
- continue;
19721
- }
19722
- if (token === "--scope") {
19723
- const value = argv[++i];
19724
- if (value !== "default" && value !== "user") {
19725
- throw new ArgParseError(`--scope must be "default" or "user", got: "${value ?? ""}"`);
19726
- }
19727
- result.scope = value;
19728
- continue;
19729
- }
19730
- if (token === "--json") {
19731
- result.json = true;
19732
- continue;
19733
- }
19734
- if (token === "--live") {
19735
- result.live = true;
19736
- continue;
19229
+ let copied = 0;
19230
+ let skipped = 0;
19231
+ for (const file of files) {
19232
+ const src = join6(sourceDir, file);
19233
+ const dest = join6(targetDir, file);
19234
+ if (existsSync6(dest)) {
19235
+ skipped++;
19236
+ } else {
19237
+ copyFileSync(src, dest);
19238
+ copied++;
19737
19239
  }
19738
19240
  }
19739
- return result;
19241
+ if (copied > 0) {
19242
+ ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/`);
19243
+ }
19244
+ if (skipped > 0) {
19245
+ skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19246
+ }
19740
19247
  }
19741
- async function run3() {
19742
- let args;
19743
- try {
19744
- args = parseArgs(process.argv.slice(3));
19745
- } catch (err) {
19746
- if (err instanceof ArgParseError) {
19747
- console.error(`Error: ${err.message}`);
19748
- process.exit(1);
19749
- }
19750
- throw err;
19248
+ function installProjectHooks(cwd) {
19249
+ const sourceDir = resolvePackagePath("hooks");
19250
+ if (!sourceDir) {
19251
+ skip("no canonical hooks found in package");
19252
+ return;
19751
19253
  }
19752
- if (args.live) {
19753
- await runLiveMode();
19254
+ const targetDir = join6(cwd, ".claude", "hooks");
19255
+ const hooks = readdirSync2(sourceDir).filter((f) => f.endsWith(".mjs"));
19256
+ if (hooks.length === 0) {
19257
+ skip("no hook files found in package");
19754
19258
  return;
19755
19259
  }
19756
- const loader = new SpecialistLoader;
19757
- let specialists = await loader.list(args.category);
19758
- if (args.scope) {
19759
- specialists = specialists.filter((s) => s.scope === args.scope);
19260
+ if (!existsSync6(targetDir)) {
19261
+ mkdirSync(targetDir, { recursive: true });
19760
19262
  }
19761
- if (args.json) {
19762
- console.log(JSON.stringify(specialists, null, 2));
19763
- return;
19263
+ let copied = 0;
19264
+ let skipped = 0;
19265
+ for (const file of hooks) {
19266
+ const src = join6(sourceDir, file);
19267
+ const dest = join6(targetDir, file);
19268
+ if (existsSync6(dest)) {
19269
+ skipped++;
19270
+ } else {
19271
+ copyFileSync(src, dest);
19272
+ copied++;
19273
+ }
19764
19274
  }
19765
- if (specialists.length === 0) {
19766
- console.log("No specialists found.");
19767
- return;
19275
+ if (copied > 0) {
19276
+ ok(`installed ${copied} hook${copied === 1 ? "" : "s"} to .claude/hooks/`);
19768
19277
  }
19769
- const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
19770
- console.log(`
19771
- ${bold2(`Specialists (${specialists.length})`)}
19772
- `);
19773
- for (const s of specialists) {
19774
- const name = cyan(s.name.padEnd(nameWidth));
19775
- const scopeTag = s.scope === "default" ? green("[default]") : yellow2("[user]");
19776
- const model = dim2(s.model);
19777
- const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
19778
- console.log(` ${name} ${scopeTag} ${model}`);
19779
- console.log(` ${" ".repeat(nameWidth)} ${dim2(desc)}`);
19780
- console.log();
19278
+ if (skipped > 0) {
19279
+ skip(`${skipped} hook${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19781
19280
  }
19782
19281
  }
19783
- var dim2 = (s) => `\x1B[2m${s}\x1B[0m`, bold2 = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
19784
- var init_list = __esm(() => {
19785
- init_loader();
19786
- ArgParseError = class ArgParseError extends Error {
19787
- constructor(message) {
19788
- super(message);
19789
- this.name = "ArgParseError";
19790
- }
19791
- };
19792
- });
19793
-
19794
- // src/cli/models.ts
19795
- var exports_models = {};
19796
- __export(exports_models, {
19797
- run: () => run4
19798
- });
19799
- import { spawnSync as spawnSync6 } from "node:child_process";
19800
- function parsePiModels() {
19801
- const r = spawnSync6("pi", ["--list-models"], {
19802
- encoding: "utf8",
19803
- stdio: "pipe",
19804
- timeout: 8000
19805
- });
19806
- if (r.status !== 0 || r.error)
19807
- return null;
19808
- return r.stdout.split(`
19809
- `).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
19810
- const cols = line.split(/\s+/);
19811
- return {
19812
- provider: cols[0] ?? "",
19813
- model: cols[1] ?? "",
19814
- context: cols[2] ?? "",
19815
- maxOut: cols[3] ?? "",
19816
- thinking: cols[4] === "yes",
19817
- images: cols[5] === "yes"
19818
- };
19819
- }).filter((m) => m.provider && m.model);
19820
- }
19821
- function parseArgs2(argv) {
19822
- const out = {};
19823
- for (let i = 0;i < argv.length; i++) {
19824
- if (argv[i] === "--provider" && argv[i + 1]) {
19825
- out.provider = argv[++i];
19826
- continue;
19827
- }
19828
- if (argv[i] === "--used") {
19829
- out.used = true;
19830
- continue;
19831
- }
19832
- }
19833
- return out;
19834
- }
19835
- async function run4() {
19836
- const args = parseArgs2(process.argv.slice(3));
19837
- const loader = new SpecialistLoader;
19838
- const specialists = await loader.list();
19839
- const usedBy = new Map;
19840
- for (const s of specialists) {
19841
- const key = s.model;
19842
- if (!usedBy.has(key))
19843
- usedBy.set(key, []);
19844
- usedBy.get(key).push(s.name);
19845
- }
19846
- const allModels = parsePiModels();
19847
- if (!allModels) {
19848
- console.error("pi not found or failed — install and configure pi first");
19849
- process.exit(1);
19850
- }
19851
- let models = allModels;
19852
- if (args.provider) {
19853
- models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
19854
- }
19855
- if (args.used) {
19856
- models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
19857
- }
19858
- if (models.length === 0) {
19859
- console.log("No models match.");
19860
- return;
19861
- }
19862
- const byProvider = new Map;
19863
- for (const m of models) {
19864
- if (!byProvider.has(m.provider))
19865
- byProvider.set(m.provider, []);
19866
- byProvider.get(m.provider).push(m);
19867
- }
19868
- const total = models.length;
19869
- console.log(`
19870
- ${bold3(`Models on pi`)} ${dim3(`(${total} total)`)}
19871
- `);
19872
- for (const [provider, pModels] of byProvider) {
19873
- console.log(` ${cyan2(provider)} ${dim3(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
19874
- const modelWidth = Math.max(...pModels.map((m) => m.model.length));
19875
- for (const m of pModels) {
19876
- const key = `${m.provider}/${m.model}`;
19877
- const inUse = usedBy.get(key);
19878
- const flags = [
19879
- m.thinking ? green2("thinking") : dim3("·"),
19880
- m.images ? dim3("images") : ""
19881
- ].filter(Boolean).join(" ");
19882
- const ctx = dim3(`ctx ${m.context}`);
19883
- const usedLabel = inUse ? ` ${yellow3("←")} ${dim3(inUse.join(", "))}` : "";
19884
- console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
19885
- }
19886
- console.log();
19887
- }
19888
- if (!args.used) {
19889
- console.log(dim3(` --provider <name> filter by provider`));
19890
- console.log(dim3(` --used only show models used by your specialists`));
19891
- console.log();
19892
- }
19893
- }
19894
- var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`;
19895
- var init_models = __esm(() => {
19896
- init_loader();
19897
- });
19898
-
19899
- // src/cli/init.ts
19900
- var exports_init = {};
19901
- __export(exports_init, {
19902
- run: () => run5
19903
- });
19904
- import { copyFileSync, cpSync, existsSync as existsSync10, mkdirSync as mkdirSync2, readdirSync as readdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19905
- import { join as join14 } from "node:path";
19906
- import { fileURLToPath as fileURLToPath2 } from "node:url";
19907
- function ok(msg) {
19908
- console.log(` ${green3("✓")} ${msg}`);
19909
- }
19910
- function skip(msg) {
19911
- console.log(` ${yellow4("○")} ${msg}`);
19912
- }
19913
- function loadJson(path, fallback) {
19914
- if (!existsSync10(path))
19915
- return structuredClone(fallback);
19916
- try {
19917
- return JSON.parse(readFileSync6(path, "utf-8"));
19918
- } catch {
19919
- return structuredClone(fallback);
19920
- }
19921
- }
19922
- function saveJson(path, value) {
19923
- writeFileSync4(path, JSON.stringify(value, null, 2) + `
19924
- `, "utf-8");
19925
- }
19926
- function resolvePackagePath(relativePath) {
19927
- const configPath = `config/${relativePath}`;
19928
- let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19929
- if (existsSync10(resolved))
19930
- return resolved;
19931
- resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19932
- if (existsSync10(resolved))
19933
- return resolved;
19934
- return null;
19935
- }
19936
- function migrateLegacySpecialists(cwd, scope) {
19937
- const sourceDir = join14(cwd, ".specialists", scope, "specialists");
19938
- if (!existsSync10(sourceDir))
19939
- return;
19940
- const targetDir = join14(cwd, ".specialists", scope);
19941
- if (!existsSync10(targetDir)) {
19942
- mkdirSync2(targetDir, { recursive: true });
19943
- }
19944
- const files = readdirSync4(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19945
- if (files.length === 0)
19946
- return;
19947
- let moved = 0;
19948
- let skipped = 0;
19949
- for (const file of files) {
19950
- const src = join14(sourceDir, file);
19951
- const dest = join14(targetDir, file);
19952
- if (existsSync10(dest)) {
19953
- skipped++;
19954
- continue;
19955
- }
19956
- renameSync2(src, dest);
19957
- moved++;
19958
- }
19959
- if (moved > 0) {
19960
- ok(`migrated ${moved} specialist${moved === 1 ? "" : "s"} from .specialists/${scope}/specialists/ to .specialists/${scope}/`);
19961
- }
19962
- if (skipped > 0) {
19963
- skip(`${skipped} legacy specialist${skipped === 1 ? "" : "s"} already exist in .specialists/${scope}/ (not moved)`);
19964
- }
19965
- }
19966
- function copyCanonicalSpecialists(cwd) {
19967
- const sourceDir = resolvePackagePath("specialists");
19968
- if (!sourceDir) {
19969
- skip("no canonical specialists found in package");
19970
- return;
19971
- }
19972
- const targetDir = join14(cwd, ".specialists", "default");
19973
- const files = readdirSync4(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19974
- if (files.length === 0) {
19975
- skip("no specialist files found in package");
19976
- return;
19977
- }
19978
- if (!existsSync10(targetDir)) {
19979
- mkdirSync2(targetDir, { recursive: true });
19980
- }
19981
- let copied = 0;
19982
- let skipped = 0;
19983
- for (const file of files) {
19984
- const src = join14(sourceDir, file);
19985
- const dest = join14(targetDir, file);
19986
- if (existsSync10(dest)) {
19987
- skipped++;
19988
- } else {
19989
- copyFileSync(src, dest);
19990
- copied++;
19991
- }
19992
- }
19993
- if (copied > 0) {
19994
- ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/`);
19995
- }
19996
- if (skipped > 0) {
19997
- skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
19998
- }
19999
- }
20000
- function installProjectHooks(cwd) {
20001
- const sourceDir = resolvePackagePath("hooks");
20002
- if (!sourceDir) {
20003
- skip("no canonical hooks found in package");
20004
- return;
20005
- }
20006
- const targetDir = join14(cwd, ".claude", "hooks");
20007
- const hooks = readdirSync4(sourceDir).filter((f) => f.endsWith(".mjs"));
20008
- if (hooks.length === 0) {
20009
- skip("no hook files found in package");
20010
- return;
20011
- }
20012
- if (!existsSync10(targetDir)) {
20013
- mkdirSync2(targetDir, { recursive: true });
20014
- }
20015
- let copied = 0;
20016
- let skipped = 0;
20017
- for (const file of hooks) {
20018
- const src = join14(sourceDir, file);
20019
- const dest = join14(targetDir, file);
20020
- if (existsSync10(dest)) {
20021
- skipped++;
20022
- } else {
20023
- copyFileSync(src, dest);
20024
- copied++;
20025
- }
20026
- }
20027
- if (copied > 0) {
20028
- ok(`installed ${copied} hook${copied === 1 ? "" : "s"} to .claude/hooks/`);
20029
- }
20030
- if (skipped > 0) {
20031
- skip(`${skipped} hook${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
20032
- }
20033
- }
20034
- function ensureProjectHookWiring(cwd) {
20035
- const settingsPath = join14(cwd, ".claude", "settings.json");
20036
- const settingsDir = join14(cwd, ".claude");
20037
- if (!existsSync10(settingsDir)) {
20038
- mkdirSync2(settingsDir, { recursive: true });
20039
- }
20040
- const settings = loadJson(settingsPath, {});
20041
- let changed = false;
20042
- function addHook(event, command) {
20043
- const eventList = settings[event] ?? [];
20044
- settings[event] = eventList;
20045
- const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
20046
- if (!alreadyWired) {
20047
- eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
20048
- changed = true;
19282
+ function ensureProjectHookWiring(cwd) {
19283
+ const settingsPath = join6(cwd, ".claude", "settings.json");
19284
+ const settingsDir = join6(cwd, ".claude");
19285
+ if (!existsSync6(settingsDir)) {
19286
+ mkdirSync(settingsDir, { recursive: true });
19287
+ }
19288
+ const settings = loadJson(settingsPath, {});
19289
+ let changed = false;
19290
+ function addHook(event, command) {
19291
+ const eventList = settings[event] ?? [];
19292
+ settings[event] = eventList;
19293
+ const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
19294
+ if (!alreadyWired) {
19295
+ eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
19296
+ changed = true;
20049
19297
  }
20050
19298
  }
20051
19299
  addHook("UserPromptSubmit", "node .claude/hooks/specialists-complete.mjs");
@@ -20063,25 +19311,25 @@ function installProjectSkills(cwd) {
20063
19311
  skip("no canonical skills found in package");
20064
19312
  return;
20065
19313
  }
20066
- const skills = readdirSync4(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19314
+ const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
20067
19315
  if (skills.length === 0) {
20068
19316
  skip("no skill directories found in package");
20069
19317
  return;
20070
19318
  }
20071
19319
  const targetDirs = [
20072
- join14(cwd, ".claude", "skills"),
20073
- join14(cwd, ".pi", "skills")
19320
+ join6(cwd, ".claude", "skills"),
19321
+ join6(cwd, ".pi", "skills")
20074
19322
  ];
20075
19323
  let totalCopied = 0;
20076
19324
  let totalSkipped = 0;
20077
19325
  for (const targetDir of targetDirs) {
20078
- if (!existsSync10(targetDir)) {
20079
- mkdirSync2(targetDir, { recursive: true });
19326
+ if (!existsSync6(targetDir)) {
19327
+ mkdirSync(targetDir, { recursive: true });
20080
19328
  }
20081
19329
  for (const skill of skills) {
20082
- const src = join14(sourceDir, skill);
20083
- const dest = join14(targetDir, skill);
20084
- if (existsSync10(dest)) {
19330
+ const src = join6(sourceDir, skill);
19331
+ const dest = join6(targetDir, skill);
19332
+ if (existsSync6(dest)) {
20085
19333
  totalSkipped++;
20086
19334
  } else {
20087
19335
  cpSync(src, dest, { recursive: true });
@@ -20097,21 +19345,21 @@ function installProjectSkills(cwd) {
20097
19345
  }
20098
19346
  }
20099
19347
  function createUserDirs(cwd) {
20100
- const userDir = join14(cwd, ".specialists", "user");
20101
- if (!existsSync10(userDir)) {
20102
- mkdirSync2(userDir, { recursive: true });
19348
+ const userDir = join6(cwd, ".specialists", "user");
19349
+ if (!existsSync6(userDir)) {
19350
+ mkdirSync(userDir, { recursive: true });
20103
19351
  ok("created .specialists/user/ for custom specialists");
20104
19352
  }
20105
19353
  }
20106
19354
  function createRuntimeDirs(cwd) {
20107
19355
  const runtimeDirs = [
20108
- join14(cwd, ".specialists", "jobs"),
20109
- join14(cwd, ".specialists", "ready")
19356
+ join6(cwd, ".specialists", "jobs"),
19357
+ join6(cwd, ".specialists", "ready")
20110
19358
  ];
20111
19359
  let created = 0;
20112
19360
  for (const dir of runtimeDirs) {
20113
- if (!existsSync10(dir)) {
20114
- mkdirSync2(dir, { recursive: true });
19361
+ if (!existsSync6(dir)) {
19362
+ mkdirSync(dir, { recursive: true });
20115
19363
  created++;
20116
19364
  }
20117
19365
  }
@@ -20120,7 +19368,7 @@ function createRuntimeDirs(cwd) {
20120
19368
  }
20121
19369
  }
20122
19370
  function ensureProjectMcp(cwd) {
20123
- const mcpPath = join14(cwd, MCP_FILE);
19371
+ const mcpPath = join6(cwd, MCP_FILE);
20124
19372
  const mcp = loadJson(mcpPath, { mcpServers: {} });
20125
19373
  mcp.mcpServers ??= {};
20126
19374
  const existing = mcp.mcpServers[MCP_SERVER_NAME];
@@ -20133,8 +19381,8 @@ function ensureProjectMcp(cwd) {
20133
19381
  ok("registered specialists in project .mcp.json");
20134
19382
  }
20135
19383
  function ensureGitignore(cwd) {
20136
- const gitignorePath = join14(cwd, ".gitignore");
20137
- const existing = existsSync10(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
19384
+ const gitignorePath = join6(cwd, ".gitignore");
19385
+ const existing = existsSync6(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
20138
19386
  let added = 0;
20139
19387
  const lines = existing.split(`
20140
19388
  `);
@@ -20145,7 +19393,7 @@ function ensureGitignore(cwd) {
20145
19393
  }
20146
19394
  }
20147
19395
  if (added > 0) {
20148
- writeFileSync4(gitignorePath, lines.join(`
19396
+ writeFileSync(gitignorePath, lines.join(`
20149
19397
  `) + `
20150
19398
  `, "utf-8");
20151
19399
  ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
@@ -20154,30 +19402,89 @@ function ensureGitignore(cwd) {
20154
19402
  }
20155
19403
  }
20156
19404
  function ensureAgentsMd(cwd) {
20157
- const agentsPath = join14(cwd, "AGENTS.md");
20158
- if (existsSync10(agentsPath)) {
20159
- const existing = readFileSync6(agentsPath, "utf-8");
19405
+ const agentsPath = join6(cwd, "AGENTS.md");
19406
+ if (existsSync6(agentsPath)) {
19407
+ const existing = readFileSync3(agentsPath, "utf-8");
20160
19408
  if (existing.includes(AGENTS_MARKER)) {
20161
19409
  skip("AGENTS.md already has Specialists section");
20162
19410
  } else {
20163
- writeFileSync4(agentsPath, existing.trimEnd() + `
19411
+ writeFileSync(agentsPath, existing.trimEnd() + `
20164
19412
 
20165
19413
  ` + AGENTS_BLOCK, "utf-8");
20166
19414
  ok("appended Specialists section to AGENTS.md");
20167
19415
  }
20168
19416
  } else {
20169
- writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
19417
+ writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
20170
19418
  ok("created AGENTS.md with Specialists section");
20171
19419
  }
20172
19420
  }
19421
+ function hasPiSessionEnv() {
19422
+ return Boolean(process.env.PI_SESSION_ID || process.env.PI_RPC_SOCKET || process.env.PI_AGENT_SESSION || process.env.PI_CODING_AGENT);
19423
+ }
19424
+ function readLinuxProcFile(path) {
19425
+ try {
19426
+ return readFileSync3(path, "utf-8");
19427
+ } catch {
19428
+ return null;
19429
+ }
19430
+ }
19431
+ function getLinuxParentPid(pid) {
19432
+ const status = readLinuxProcFile(`/proc/${pid}/status`);
19433
+ if (!status)
19434
+ return null;
19435
+ const ppidLine = status.split(`
19436
+ `).find((line) => line.startsWith("PPid:"));
19437
+ if (!ppidLine)
19438
+ return null;
19439
+ const value = Number(ppidLine.replace("PPid:", "").trim());
19440
+ return Number.isFinite(value) && value > 0 ? value : null;
19441
+ }
19442
+ function hasPiAncestorProcess(maxDepth = 8) {
19443
+ let pid = process.ppid;
19444
+ let depth = 0;
19445
+ while (pid && depth < maxDepth) {
19446
+ const cmdline = readLinuxProcFile(`/proc/${pid}/cmdline`);
19447
+ if (!cmdline)
19448
+ break;
19449
+ const command = cmdline.replace(/\0/g, " ").trim();
19450
+ const executable = basename2(command.split(" ")[0] ?? "");
19451
+ const isPiExecutable = executable === "pi" || executable === "pi-coding-agent" || executable.startsWith("pi-");
19452
+ if (isPiExecutable || command.includes("@mariozechner/pi-coding-agent")) {
19453
+ return true;
19454
+ }
19455
+ pid = getLinuxParentPid(pid);
19456
+ depth++;
19457
+ }
19458
+ return false;
19459
+ }
19460
+ function hasExistingDefaultSpecialists(cwd) {
19461
+ const defaultDir = join6(cwd, ".specialists", "default");
19462
+ const legacyNestedDir = join6(defaultDir, "specialists");
19463
+ const hasFlat = existsSync6(defaultDir) && readdirSync2(defaultDir).some((file) => file.endsWith(".specialist.yaml"));
19464
+ if (hasFlat)
19465
+ return true;
19466
+ return existsSync6(legacyNestedDir) && readdirSync2(legacyNestedDir).some((file) => file.endsWith(".specialist.yaml"));
19467
+ }
19468
+ function shouldSkipDefaultSyncInPiSession(cwd) {
19469
+ if (process.env.SPECIALISTS_INIT_FORCE_DEFAULT_SYNC === "1")
19470
+ return false;
19471
+ if (!hasExistingDefaultSpecialists(cwd))
19472
+ return false;
19473
+ return hasPiSessionEnv() || hasPiAncestorProcess();
19474
+ }
20173
19475
  async function run5() {
20174
19476
  const cwd = process.cwd();
20175
19477
  console.log(`
20176
19478
  ${bold4("specialists init")}
20177
19479
  `);
20178
- migrateLegacySpecialists(cwd, "default");
19480
+ const skipDefaultSync = shouldSkipDefaultSyncInPiSession(cwd);
19481
+ if (skipDefaultSync) {
19482
+ skip("pi session detected with existing default specialists; skipped .specialists/default sync");
19483
+ } else {
19484
+ migrateLegacySpecialists(cwd, "default");
19485
+ copyCanonicalSpecialists(cwd);
19486
+ }
20179
19487
  migrateLegacySpecialists(cwd, "user");
20180
- copyCanonicalSpecialists(cwd);
20181
19488
  createUserDirs(cwd);
20182
19489
  createRuntimeDirs(cwd);
20183
19490
  ensureGitignore(cwd);
@@ -20249,8 +19556,8 @@ __export(exports_validate, {
20249
19556
  ArgParseError: () => ArgParseError2
20250
19557
  });
20251
19558
  import { readFile as readFile2 } from "node:fs/promises";
20252
- import { existsSync as existsSync11 } from "node:fs";
20253
- import { join as join15 } from "node:path";
19559
+ import { existsSync as existsSync7 } from "node:fs";
19560
+ import { join as join7 } from "node:path";
20254
19561
  function parseArgs3(argv) {
20255
19562
  const name = argv[0];
20256
19563
  if (!name || name.startsWith("--")) {
@@ -20261,15 +19568,15 @@ function parseArgs3(argv) {
20261
19568
  }
20262
19569
  function findSpecialistFile(name) {
20263
19570
  const scanDirs = [
20264
- join15(process.cwd(), ".specialists", "user"),
20265
- join15(process.cwd(), ".specialists", "user", "specialists"),
20266
- join15(process.cwd(), ".specialists", "default"),
20267
- join15(process.cwd(), ".specialists", "default", "specialists"),
20268
- join15(process.cwd(), "specialists")
19571
+ join7(process.cwd(), ".specialists", "user"),
19572
+ join7(process.cwd(), ".specialists", "user", "specialists"),
19573
+ join7(process.cwd(), ".specialists", "default"),
19574
+ join7(process.cwd(), ".specialists", "default", "specialists"),
19575
+ join7(process.cwd(), "specialists")
20269
19576
  ];
20270
19577
  for (const dir of scanDirs) {
20271
- const candidate = join15(dir, `${name}.specialist.yaml`);
20272
- if (existsSync11(candidate)) {
19578
+ const candidate = join7(dir, `${name}.specialist.yaml`);
19579
+ if (existsSync7(candidate)) {
20273
19580
  return candidate;
20274
19581
  }
20275
19582
  }
@@ -20361,9 +19668,9 @@ var exports_edit = {};
20361
19668
  __export(exports_edit, {
20362
19669
  run: () => run7
20363
19670
  });
20364
- import { spawnSync as spawnSync7 } from "node:child_process";
20365
- import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
20366
- import { join as join16 } from "node:path";
19671
+ import { spawnSync as spawnSync5 } from "node:child_process";
19672
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
19673
+ import { join as join8 } from "node:path";
20367
19674
  function parseArgs4(argv) {
20368
19675
  const name = argv[0];
20369
19676
  if (!name || name.startsWith("--")) {
@@ -20427,18 +19734,18 @@ function setIn(doc2, path, value) {
20427
19734
  }
20428
19735
  }
20429
19736
  function openAllConfigSpecialistsInEditor() {
20430
- const configDir = join16(process.cwd(), "config", "specialists");
20431
- if (!existsSync12(configDir)) {
19737
+ const configDir = join8(process.cwd(), "config", "specialists");
19738
+ if (!existsSync8(configDir)) {
20432
19739
  console.error(`Error: missing directory: ${configDir}`);
20433
19740
  process.exit(1);
20434
19741
  }
20435
- const files = readdirSync5(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join16(configDir, file));
19742
+ const files = readdirSync3(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join8(configDir, file));
20436
19743
  if (files.length === 0) {
20437
19744
  console.error("Error: no specialist YAML files found in config/specialists/");
20438
19745
  process.exit(1);
20439
19746
  }
20440
19747
  const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
20441
- const result = spawnSync7(editor, files, { stdio: "inherit", shell: true });
19748
+ const result = spawnSync5(editor, files, { stdio: "inherit", shell: true });
20442
19749
  if (result.status !== 0) {
20443
19750
  process.exit(result.status ?? 1);
20444
19751
  }
@@ -20460,7 +19767,7 @@ async function run7() {
20460
19767
  console.error(` Run ${yellow6("specialists list")} to see available specialists`);
20461
19768
  process.exit(1);
20462
19769
  }
20463
- const raw = readFileSync7(match.filePath, "utf-8");
19770
+ const raw = readFileSync4(match.filePath, "utf-8");
20464
19771
  const doc2 = $parseDocument(raw);
20465
19772
  const yamlPath = FIELD_MAP[field];
20466
19773
  let typedValue = value;
@@ -20491,7 +19798,7 @@ ${bold6(`[dry-run] ${match.filePath}`)}
20491
19798
  console.log();
20492
19799
  return;
20493
19800
  }
20494
- writeFileSync5(match.filePath, updated, "utf-8");
19801
+ writeFileSync2(match.filePath, updated, "utf-8");
20495
19802
  const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
20496
19803
  console.log(`${green5("✓")} ${bold6(name)}: ${yellow6(field)} = ${displayValue}` + dim6(` (${match.filePath})`));
20497
19804
  }
@@ -20515,9 +19822,9 @@ var exports_config = {};
20515
19822
  __export(exports_config, {
20516
19823
  run: () => run8
20517
19824
  });
20518
- import { existsSync as existsSync13 } from "node:fs";
19825
+ import { existsSync as existsSync9 } from "node:fs";
20519
19826
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
20520
- import { basename as basename2, join as join17 } from "node:path";
19827
+ import { basename as basename3, join as join9 } from "node:path";
20521
19828
  function usage() {
20522
19829
  return [
20523
19830
  "Usage:",
@@ -20566,121 +19873,747 @@ ${usage()}`);
20566
19873
  if (!next || next.startsWith("--")) {
20567
19874
  throw new ArgParseError3("--name requires a specialist name");
20568
19875
  }
20569
- name = next;
20570
- continue;
20571
- }
20572
- throw new ArgParseError3(`Unknown option: ${token}`);
20573
- }
20574
- if (name && all) {
20575
- throw new ArgParseError3("Use either --name or --all, not both");
20576
- }
20577
- if (!name) {
20578
- all = true;
20579
- }
20580
- return { command, key, value, name, all };
20581
- }
20582
- function splitKeyPath(key) {
20583
- const path = key.split(".").map((part) => part.trim()).filter(Boolean);
20584
- if (path.length === 0) {
20585
- throw new ArgParseError3(`Invalid key: ${key}`);
20586
- }
20587
- return path;
20588
- }
20589
- function getSpecialistDir(projectDir) {
20590
- return join17(projectDir, "config", "specialists");
20591
- }
20592
- function getSpecialistNameFromPath(path) {
20593
- return path.replace(/\.specialist\.yaml$/, "");
20594
- }
20595
- async function listSpecialistFiles(projectDir) {
20596
- const specialistDir = getSpecialistDir(projectDir);
20597
- if (!existsSync13(specialistDir)) {
20598
- throw new Error(`Missing directory: ${specialistDir}`);
20599
- }
20600
- const entries = await readdir2(specialistDir);
20601
- return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join17(specialistDir, entry));
20602
- }
20603
- async function findNamedSpecialistFile(projectDir, name) {
20604
- const path = join17(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
20605
- if (!existsSync13(path)) {
20606
- throw new Error(`Specialist not found in config/specialists/: ${name}`);
20607
- }
20608
- return path;
20609
- }
20610
- function parseValue(rawValue) {
20611
- try {
20612
- return $parse(rawValue);
20613
- } catch {
20614
- return rawValue;
20615
- }
20616
- }
20617
- function formatValue(value) {
20618
- if (value === undefined)
20619
- return "<unset>";
20620
- if (typeof value === "string")
20621
- return value;
20622
- return JSON.stringify(value);
20623
- }
20624
- async function getAcrossFiles(files, keyPath) {
20625
- for (const file of files) {
20626
- const content = await readFile3(file, "utf-8");
20627
- const doc2 = $parseDocument(content);
20628
- const value = doc2.getIn(keyPath);
20629
- const name = getSpecialistNameFromPath(basename2(file));
20630
- console.log(`${yellow7(name)}: ${formatValue(value)}`);
20631
- }
20632
- }
20633
- async function setAcrossFiles(files, keyPath, rawValue) {
20634
- const typedValue = parseValue(rawValue);
20635
- for (const file of files) {
20636
- const content = await readFile3(file, "utf-8");
20637
- const doc2 = $parseDocument(content);
20638
- doc2.setIn(keyPath, typedValue);
20639
- await writeFile2(file, doc2.toString(), "utf-8");
20640
- }
20641
- console.log(`${green6("✓")} updated ${files.length} specialist${files.length === 1 ? "" : "s"}: ` + `${keyPath.join(".")} = ${formatValue(typedValue)}`);
20642
- }
20643
- async function run8() {
20644
- let args;
20645
- try {
20646
- args = parseArgs5(process.argv.slice(3));
20647
- } catch (error2) {
20648
- if (error2 instanceof ArgParseError3) {
20649
- console.error(error2.message);
20650
- process.exit(1);
19876
+ name = next;
19877
+ continue;
19878
+ }
19879
+ throw new ArgParseError3(`Unknown option: ${token}`);
19880
+ }
19881
+ if (name && all) {
19882
+ throw new ArgParseError3("Use either --name or --all, not both");
19883
+ }
19884
+ if (!name) {
19885
+ all = true;
19886
+ }
19887
+ return { command, key, value, name, all };
19888
+ }
19889
+ function splitKeyPath(key) {
19890
+ const path = key.split(".").map((part) => part.trim()).filter(Boolean);
19891
+ if (path.length === 0) {
19892
+ throw new ArgParseError3(`Invalid key: ${key}`);
19893
+ }
19894
+ return path;
19895
+ }
19896
+ function getSpecialistDir(projectDir) {
19897
+ return join9(projectDir, "config", "specialists");
19898
+ }
19899
+ function getSpecialistNameFromPath(path) {
19900
+ return path.replace(/\.specialist\.yaml$/, "");
19901
+ }
19902
+ async function listSpecialistFiles(projectDir) {
19903
+ const specialistDir = getSpecialistDir(projectDir);
19904
+ if (!existsSync9(specialistDir)) {
19905
+ throw new Error(`Missing directory: ${specialistDir}`);
19906
+ }
19907
+ const entries = await readdir2(specialistDir);
19908
+ return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join9(specialistDir, entry));
19909
+ }
19910
+ async function findNamedSpecialistFile(projectDir, name) {
19911
+ const path = join9(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
19912
+ if (!existsSync9(path)) {
19913
+ throw new Error(`Specialist not found in config/specialists/: ${name}`);
19914
+ }
19915
+ return path;
19916
+ }
19917
+ function parseValue(rawValue) {
19918
+ try {
19919
+ return $parse(rawValue);
19920
+ } catch {
19921
+ return rawValue;
19922
+ }
19923
+ }
19924
+ function formatValue(value) {
19925
+ if (value === undefined)
19926
+ return "<unset>";
19927
+ if (typeof value === "string")
19928
+ return value;
19929
+ return JSON.stringify(value);
19930
+ }
19931
+ async function getAcrossFiles(files, keyPath) {
19932
+ for (const file of files) {
19933
+ const content = await readFile3(file, "utf-8");
19934
+ const doc2 = $parseDocument(content);
19935
+ const value = doc2.getIn(keyPath);
19936
+ const name = getSpecialistNameFromPath(basename3(file));
19937
+ console.log(`${yellow7(name)}: ${formatValue(value)}`);
19938
+ }
19939
+ }
19940
+ async function setAcrossFiles(files, keyPath, rawValue) {
19941
+ const typedValue = parseValue(rawValue);
19942
+ for (const file of files) {
19943
+ const content = await readFile3(file, "utf-8");
19944
+ const doc2 = $parseDocument(content);
19945
+ doc2.setIn(keyPath, typedValue);
19946
+ await writeFile2(file, doc2.toString(), "utf-8");
19947
+ }
19948
+ console.log(`${green6("✓")} updated ${files.length} specialist${files.length === 1 ? "" : "s"}: ` + `${keyPath.join(".")} = ${formatValue(typedValue)}`);
19949
+ }
19950
+ async function run8() {
19951
+ let args;
19952
+ try {
19953
+ args = parseArgs5(process.argv.slice(3));
19954
+ } catch (error2) {
19955
+ if (error2 instanceof ArgParseError3) {
19956
+ console.error(error2.message);
19957
+ process.exit(1);
19958
+ }
19959
+ throw error2;
19960
+ }
19961
+ const keyPath = splitKeyPath(args.key);
19962
+ const projectDir = process.cwd();
19963
+ let files;
19964
+ try {
19965
+ files = args.name ? [await findNamedSpecialistFile(projectDir, args.name)] : await listSpecialistFiles(projectDir);
19966
+ } catch (error2) {
19967
+ const message = error2 instanceof Error ? error2.message : String(error2);
19968
+ console.error(message);
19969
+ process.exit(1);
19970
+ return;
19971
+ }
19972
+ if (files.length === 0) {
19973
+ console.error("No specialists found in config/specialists/");
19974
+ process.exit(1);
19975
+ return;
19976
+ }
19977
+ if (args.command === "get") {
19978
+ await getAcrossFiles(files, keyPath);
19979
+ return;
19980
+ }
19981
+ await setAcrossFiles(files, keyPath, args.value);
19982
+ }
19983
+ var green6 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError3;
19984
+ var init_config = __esm(() => {
19985
+ init_dist();
19986
+ ArgParseError3 = class ArgParseError3 extends Error {
19987
+ constructor(message) {
19988
+ super(message);
19989
+ this.name = "ArgParseError";
19990
+ }
19991
+ };
19992
+ });
19993
+
19994
+ // src/specialist/timeline-events.ts
19995
+ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
19996
+ const t = Date.now();
19997
+ switch (callbackEvent) {
19998
+ case "thinking":
19999
+ return { t, type: TIMELINE_EVENT_TYPES.THINKING };
20000
+ case "tool_execution_start":
20001
+ return {
20002
+ t,
20003
+ type: TIMELINE_EVENT_TYPES.TOOL,
20004
+ tool: context.tool ?? "unknown",
20005
+ phase: "start",
20006
+ tool_call_id: context.toolCallId,
20007
+ args: context.args,
20008
+ started_at: new Date(t).toISOString()
20009
+ };
20010
+ case "tool_execution_update":
20011
+ case "tool_execution":
20012
+ return {
20013
+ t,
20014
+ type: TIMELINE_EVENT_TYPES.TOOL,
20015
+ tool: context.tool ?? "unknown",
20016
+ phase: "update",
20017
+ tool_call_id: context.toolCallId
20018
+ };
20019
+ case "tool_execution_end":
20020
+ return {
20021
+ t,
20022
+ type: TIMELINE_EVENT_TYPES.TOOL,
20023
+ tool: context.tool ?? "unknown",
20024
+ phase: "end",
20025
+ tool_call_id: context.toolCallId,
20026
+ is_error: context.isError
20027
+ };
20028
+ case "message_start_assistant":
20029
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "assistant" };
20030
+ case "message_end_assistant":
20031
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "assistant" };
20032
+ case "message_start_tool_result":
20033
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "toolResult" };
20034
+ case "message_end_tool_result":
20035
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "toolResult" };
20036
+ case "turn_start":
20037
+ return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "start" };
20038
+ case "turn_end":
20039
+ return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "end" };
20040
+ case "text":
20041
+ return { t, type: TIMELINE_EVENT_TYPES.TEXT };
20042
+ case "agent_end":
20043
+ case "message_done":
20044
+ case "done":
20045
+ return null;
20046
+ default:
20047
+ return null;
20048
+ }
20049
+ }
20050
+ function createRunStartEvent(specialist, beadId) {
20051
+ return {
20052
+ t: Date.now(),
20053
+ type: TIMELINE_EVENT_TYPES.RUN_START,
20054
+ specialist,
20055
+ bead_id: beadId
20056
+ };
20057
+ }
20058
+ function createMetaEvent(model, backend) {
20059
+ return {
20060
+ t: Date.now(),
20061
+ type: TIMELINE_EVENT_TYPES.META,
20062
+ model,
20063
+ backend
20064
+ };
20065
+ }
20066
+ function createStaleWarningEvent(reason, options) {
20067
+ return {
20068
+ t: Date.now(),
20069
+ type: TIMELINE_EVENT_TYPES.STALE_WARNING,
20070
+ reason,
20071
+ silence_ms: options.silence_ms,
20072
+ threshold_ms: options.threshold_ms,
20073
+ ...options.tool !== undefined ? { tool: options.tool } : {}
20074
+ };
20075
+ }
20076
+ function createRunCompleteEvent(status, elapsed_s, options) {
20077
+ return {
20078
+ t: Date.now(),
20079
+ type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
20080
+ status,
20081
+ elapsed_s,
20082
+ ...options
20083
+ };
20084
+ }
20085
+ function parseTimelineEvent(line) {
20086
+ try {
20087
+ const parsed = JSON.parse(line);
20088
+ if (!parsed || typeof parsed !== "object")
20089
+ return null;
20090
+ if (typeof parsed.t !== "number")
20091
+ return null;
20092
+ if (typeof parsed.type !== "string")
20093
+ return null;
20094
+ if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
20095
+ return {
20096
+ t: parsed.t,
20097
+ type: TIMELINE_EVENT_TYPES.DONE,
20098
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
20099
+ };
20100
+ }
20101
+ if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
20102
+ return {
20103
+ t: parsed.t,
20104
+ type: TIMELINE_EVENT_TYPES.AGENT_END,
20105
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
20106
+ };
20107
+ }
20108
+ const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
20109
+ if (!knownTypes.includes(parsed.type))
20110
+ return null;
20111
+ return parsed;
20112
+ } catch {
20113
+ return null;
20114
+ }
20115
+ }
20116
+ function isRunCompleteEvent(event) {
20117
+ return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
20118
+ }
20119
+ function compareTimelineEvents(a, b) {
20120
+ return a.t - b.t;
20121
+ }
20122
+ var TIMELINE_EVENT_TYPES;
20123
+ var init_timeline_events = __esm(() => {
20124
+ TIMELINE_EVENT_TYPES = {
20125
+ RUN_START: "run_start",
20126
+ META: "meta",
20127
+ THINKING: "thinking",
20128
+ TOOL: "tool",
20129
+ TEXT: "text",
20130
+ MESSAGE: "message",
20131
+ TURN: "turn",
20132
+ RUN_COMPLETE: "run_complete",
20133
+ STALE_WARNING: "stale_warning",
20134
+ DONE: "done",
20135
+ AGENT_END: "agent_end"
20136
+ };
20137
+ });
20138
+
20139
+ // src/specialist/supervisor.ts
20140
+ import {
20141
+ appendFileSync,
20142
+ closeSync,
20143
+ existsSync as existsSync10,
20144
+ fsyncSync,
20145
+ mkdirSync as mkdirSync2,
20146
+ openSync,
20147
+ readdirSync as readdirSync4,
20148
+ readFileSync as readFileSync5,
20149
+ renameSync as renameSync2,
20150
+ rmSync,
20151
+ statSync,
20152
+ writeFileSync as writeFileSync3,
20153
+ writeSync
20154
+ } from "node:fs";
20155
+ import { join as join10 } from "node:path";
20156
+ import { createInterface } from "node:readline";
20157
+ import { createReadStream } from "node:fs";
20158
+ import { spawnSync as spawnSync6, execFileSync } from "node:child_process";
20159
+ function getCurrentGitSha() {
20160
+ const result = spawnSync6("git", ["rev-parse", "HEAD"], {
20161
+ encoding: "utf-8",
20162
+ stdio: ["ignore", "pipe", "ignore"]
20163
+ });
20164
+ if (result.status !== 0)
20165
+ return;
20166
+ const sha = result.stdout?.trim();
20167
+ return sha || undefined;
20168
+ }
20169
+ function formatBeadNotes(result) {
20170
+ const metadata = [
20171
+ `prompt_hash=${result.promptHash}`,
20172
+ `git_sha=${getCurrentGitSha() ?? "unknown"}`,
20173
+ `elapsed_ms=${Math.round(result.durationMs)}`,
20174
+ `model=${result.model}`,
20175
+ `backend=${result.backend}`
20176
+ ].join(`
20177
+ `);
20178
+ return `${result.output}
20179
+
20180
+ ---
20181
+ ${metadata}`;
20182
+ }
20183
+
20184
+ class Supervisor {
20185
+ opts;
20186
+ constructor(opts) {
20187
+ this.opts = opts;
20188
+ }
20189
+ jobDir(id) {
20190
+ return join10(this.opts.jobsDir, id);
20191
+ }
20192
+ statusPath(id) {
20193
+ return join10(this.jobDir(id), "status.json");
20194
+ }
20195
+ resultPath(id) {
20196
+ return join10(this.jobDir(id), "result.txt");
20197
+ }
20198
+ eventsPath(id) {
20199
+ return join10(this.jobDir(id), "events.jsonl");
20200
+ }
20201
+ readyDir() {
20202
+ return join10(this.opts.jobsDir, "..", "ready");
20203
+ }
20204
+ readStatus(id) {
20205
+ const path = this.statusPath(id);
20206
+ if (!existsSync10(path))
20207
+ return null;
20208
+ try {
20209
+ return JSON.parse(readFileSync5(path, "utf-8"));
20210
+ } catch {
20211
+ return null;
20212
+ }
20213
+ }
20214
+ listJobs() {
20215
+ if (!existsSync10(this.opts.jobsDir))
20216
+ return [];
20217
+ const jobs = [];
20218
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20219
+ const path = join10(this.opts.jobsDir, entry, "status.json");
20220
+ if (!existsSync10(path))
20221
+ continue;
20222
+ try {
20223
+ jobs.push(JSON.parse(readFileSync5(path, "utf-8")));
20224
+ } catch {}
20225
+ }
20226
+ return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
20227
+ }
20228
+ writeStatusFile(id, data) {
20229
+ mkdirSync2(this.jobDir(id), { recursive: true });
20230
+ const path = this.statusPath(id);
20231
+ const tmp = path + ".tmp";
20232
+ writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
20233
+ renameSync2(tmp, path);
20234
+ }
20235
+ updateStatus(id, updates) {
20236
+ const current = this.readStatus(id);
20237
+ if (!current)
20238
+ return;
20239
+ this.writeStatusFile(id, { ...current, ...updates });
20240
+ }
20241
+ gc() {
20242
+ if (!existsSync10(this.opts.jobsDir))
20243
+ return;
20244
+ const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
20245
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20246
+ const dir = join10(this.opts.jobsDir, entry);
20247
+ try {
20248
+ const stat2 = statSync(dir);
20249
+ if (!stat2.isDirectory())
20250
+ continue;
20251
+ if (stat2.mtimeMs < cutoff)
20252
+ rmSync(dir, { recursive: true, force: true });
20253
+ } catch {}
20254
+ }
20255
+ }
20256
+ crashRecovery() {
20257
+ if (!existsSync10(this.opts.jobsDir))
20258
+ return;
20259
+ const thresholds = {
20260
+ ...STALL_DETECTION_DEFAULTS,
20261
+ ...this.opts.stallDetection
20262
+ };
20263
+ const now = Date.now();
20264
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20265
+ const statusPath = join10(this.opts.jobsDir, entry, "status.json");
20266
+ if (!existsSync10(statusPath))
20267
+ continue;
20268
+ try {
20269
+ const s = JSON.parse(readFileSync5(statusPath, "utf-8"));
20270
+ if (s.status === "running" || s.status === "starting") {
20271
+ if (!s.pid)
20272
+ continue;
20273
+ let pidAlive = true;
20274
+ try {
20275
+ process.kill(s.pid, 0);
20276
+ } catch {
20277
+ pidAlive = false;
20278
+ }
20279
+ if (!pidAlive) {
20280
+ const tmp = statusPath + ".tmp";
20281
+ const updated = { ...s, status: "error", error: "Process crashed or was killed" };
20282
+ writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
20283
+ renameSync2(tmp, statusPath);
20284
+ } else if (s.status === "running") {
20285
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
20286
+ const silenceMs = now - lastEventAt;
20287
+ if (silenceMs > thresholds.running_silence_error_ms) {
20288
+ const tmp = statusPath + ".tmp";
20289
+ const updated = {
20290
+ ...s,
20291
+ status: "error",
20292
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
20293
+ };
20294
+ writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
20295
+ renameSync2(tmp, statusPath);
20296
+ }
20297
+ }
20298
+ } else if (s.status === "waiting") {
20299
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
20300
+ const silenceMs = now - lastEventAt;
20301
+ if (silenceMs > thresholds.waiting_stale_ms) {
20302
+ const eventsPath = join10(this.opts.jobsDir, entry, "events.jsonl");
20303
+ const event = createStaleWarningEvent("waiting_stale", {
20304
+ silence_ms: silenceMs,
20305
+ threshold_ms: thresholds.waiting_stale_ms
20306
+ });
20307
+ try {
20308
+ appendFileSync(eventsPath, JSON.stringify(event) + `
20309
+ `);
20310
+ } catch {}
20311
+ }
20312
+ }
20313
+ } catch {}
20314
+ }
20315
+ }
20316
+ async run() {
20317
+ const { runner, runOptions, jobsDir } = this.opts;
20318
+ this.gc();
20319
+ this.crashRecovery();
20320
+ const id = crypto.randomUUID().slice(0, 6);
20321
+ const dir = this.jobDir(id);
20322
+ const startedAtMs = Date.now();
20323
+ mkdirSync2(dir, { recursive: true });
20324
+ mkdirSync2(this.readyDir(), { recursive: true });
20325
+ const initialStatus = {
20326
+ id,
20327
+ specialist: runOptions.name,
20328
+ status: "starting",
20329
+ started_at_ms: startedAtMs,
20330
+ pid: process.pid,
20331
+ ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
20332
+ ...process.env.SPECIALISTS_TMUX_SESSION ? { tmux_session: process.env.SPECIALISTS_TMUX_SESSION } : {}
20333
+ };
20334
+ this.writeStatusFile(id, initialStatus);
20335
+ writeFileSync3(join10(this.opts.jobsDir, "latest"), `${id}
20336
+ `, "utf-8");
20337
+ this.opts.onJobStarted?.({ id });
20338
+ let statusSnapshot = initialStatus;
20339
+ const setStatus = (updates) => {
20340
+ statusSnapshot = { ...statusSnapshot, ...updates };
20341
+ this.writeStatusFile(id, statusSnapshot);
20342
+ };
20343
+ const eventsFd = openSync(this.eventsPath(id), "a");
20344
+ const appendTimelineEvent = (event) => {
20345
+ try {
20346
+ writeSync(eventsFd, JSON.stringify(event) + `
20347
+ `);
20348
+ } catch (err) {
20349
+ console.error(`[supervisor] Failed to write event: ${err?.message ?? err}`);
20350
+ }
20351
+ };
20352
+ appendTimelineEvent(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
20353
+ const fifoPath = join10(dir, "steer.pipe");
20354
+ try {
20355
+ execFileSync("mkfifo", [fifoPath]);
20356
+ setStatus({ fifo_path: fifoPath });
20357
+ } catch {}
20358
+ let textLogged = false;
20359
+ let currentTool = "";
20360
+ let currentToolCallId = "";
20361
+ let currentToolArgs;
20362
+ let currentToolIsError = false;
20363
+ const activeToolCalls = new Map;
20364
+ let killFn;
20365
+ let steerFn;
20366
+ let resumeFn;
20367
+ let closeFn;
20368
+ let fifoReadStream;
20369
+ let fifoReadline;
20370
+ const thresholds = {
20371
+ ...STALL_DETECTION_DEFAULTS,
20372
+ ...this.opts.stallDetection
20373
+ };
20374
+ let lastActivityMs = startedAtMs;
20375
+ let silenceWarnEmitted = false;
20376
+ let toolStartMs;
20377
+ let toolDurationWarnEmitted = false;
20378
+ let stuckIntervalId;
20379
+ stuckIntervalId = setInterval(() => {
20380
+ const now = Date.now();
20381
+ if (statusSnapshot.status === "running") {
20382
+ const silenceMs = now - lastActivityMs;
20383
+ if (!silenceWarnEmitted && silenceMs > thresholds.running_silence_warn_ms) {
20384
+ silenceWarnEmitted = true;
20385
+ appendTimelineEvent(createStaleWarningEvent("running_silence", {
20386
+ silence_ms: silenceMs,
20387
+ threshold_ms: thresholds.running_silence_warn_ms
20388
+ }));
20389
+ }
20390
+ if (silenceMs > thresholds.running_silence_error_ms) {
20391
+ appendTimelineEvent(createStaleWarningEvent("running_silence_error", {
20392
+ silence_ms: silenceMs,
20393
+ threshold_ms: thresholds.running_silence_error_ms
20394
+ }));
20395
+ setStatus({
20396
+ status: "error",
20397
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
20398
+ });
20399
+ killFn?.();
20400
+ clearInterval(stuckIntervalId);
20401
+ }
20402
+ }
20403
+ if (toolStartMs !== undefined && !toolDurationWarnEmitted) {
20404
+ const toolDurationMs = now - toolStartMs;
20405
+ if (toolDurationMs > thresholds.tool_duration_warn_ms) {
20406
+ toolDurationWarnEmitted = true;
20407
+ appendTimelineEvent(createStaleWarningEvent("tool_duration", {
20408
+ silence_ms: toolDurationMs,
20409
+ threshold_ms: thresholds.tool_duration_warn_ms,
20410
+ tool: currentTool
20411
+ }));
20412
+ }
20413
+ }
20414
+ }, 1e4);
20415
+ const sigtermHandler = () => killFn?.();
20416
+ process.once("SIGTERM", sigtermHandler);
20417
+ try {
20418
+ const result = await runner.run(runOptions, (delta) => {
20419
+ const toolMatch = delta.match(/⚙ (.+?)…/);
20420
+ if (toolMatch) {
20421
+ currentTool = toolMatch[1];
20422
+ setStatus({ current_tool: currentTool });
20423
+ }
20424
+ this.opts.onProgress?.(delta);
20425
+ }, (eventType) => {
20426
+ const now = Date.now();
20427
+ lastActivityMs = now;
20428
+ silenceWarnEmitted = false;
20429
+ setStatus({
20430
+ status: "running",
20431
+ current_event: eventType,
20432
+ last_event_at_ms: now,
20433
+ elapsed_s: Math.round((now - startedAtMs) / 1000)
20434
+ });
20435
+ const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
20436
+ tool: currentTool,
20437
+ toolCallId: currentToolCallId || undefined,
20438
+ args: currentToolArgs,
20439
+ isError: currentToolIsError
20440
+ });
20441
+ if (timelineEvent) {
20442
+ appendTimelineEvent(timelineEvent);
20443
+ } else if (eventType === "text" && !textLogged) {
20444
+ textLogged = true;
20445
+ appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
20446
+ }
20447
+ }, (meta) => {
20448
+ setStatus({ model: meta.model, backend: meta.backend });
20449
+ appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
20450
+ this.opts.onMeta?.(meta);
20451
+ }, (fn) => {
20452
+ killFn = fn;
20453
+ }, (beadId) => {
20454
+ setStatus({ bead_id: beadId });
20455
+ }, (fn) => {
20456
+ steerFn = fn;
20457
+ if (!existsSync10(fifoPath))
20458
+ return;
20459
+ fifoReadStream = createReadStream(fifoPath, { flags: "r+" });
20460
+ fifoReadline = createInterface({ input: fifoReadStream });
20461
+ fifoReadline.on("line", (line) => {
20462
+ try {
20463
+ const parsed = JSON.parse(line);
20464
+ if (parsed?.type === "steer" && typeof parsed.message === "string") {
20465
+ steerFn?.(parsed.message).catch(() => {});
20466
+ } else if (parsed?.type === "resume" && typeof parsed.task === "string") {
20467
+ if (resumeFn) {
20468
+ setStatus({ status: "running", current_event: "starting" });
20469
+ resumeFn(parsed.task).then((output) => {
20470
+ mkdirSync2(this.jobDir(id), { recursive: true });
20471
+ writeFileSync3(this.resultPath(id), output, "utf-8");
20472
+ setStatus({
20473
+ status: "waiting",
20474
+ current_event: "waiting",
20475
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
20476
+ last_event_at_ms: Date.now()
20477
+ });
20478
+ }).catch((err) => {
20479
+ setStatus({ status: "error", error: err?.message ?? String(err) });
20480
+ });
20481
+ }
20482
+ } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
20483
+ console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
20484
+ if (resumeFn) {
20485
+ setStatus({ status: "running", current_event: "starting" });
20486
+ resumeFn(parsed.message).then((output) => {
20487
+ mkdirSync2(this.jobDir(id), { recursive: true });
20488
+ writeFileSync3(this.resultPath(id), output, "utf-8");
20489
+ setStatus({
20490
+ status: "waiting",
20491
+ current_event: "waiting",
20492
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
20493
+ last_event_at_ms: Date.now()
20494
+ });
20495
+ }).catch((err) => {
20496
+ setStatus({ status: "error", error: err?.message ?? String(err) });
20497
+ });
20498
+ }
20499
+ } else if (parsed?.type === "close") {
20500
+ closeFn?.().catch(() => {});
20501
+ }
20502
+ } catch {}
20503
+ });
20504
+ fifoReadline.on("error", () => {});
20505
+ }, (rFn, cFn) => {
20506
+ resumeFn = rFn;
20507
+ closeFn = cFn;
20508
+ setStatus({ status: "waiting", current_event: "waiting" });
20509
+ }, (tool, args, toolCallId) => {
20510
+ currentTool = tool;
20511
+ currentToolArgs = args;
20512
+ currentToolCallId = toolCallId ?? "";
20513
+ currentToolIsError = false;
20514
+ toolStartMs = Date.now();
20515
+ toolDurationWarnEmitted = false;
20516
+ setStatus({ current_tool: tool });
20517
+ if (toolCallId) {
20518
+ activeToolCalls.set(toolCallId, { tool, args });
20519
+ }
20520
+ }, (tool, isError, toolCallId) => {
20521
+ if (toolCallId && activeToolCalls.has(toolCallId)) {
20522
+ const entry = activeToolCalls.get(toolCallId);
20523
+ currentTool = entry.tool;
20524
+ currentToolArgs = entry.args;
20525
+ currentToolCallId = toolCallId;
20526
+ activeToolCalls.delete(toolCallId);
20527
+ } else {
20528
+ currentTool = tool;
20529
+ }
20530
+ currentToolIsError = isError;
20531
+ toolStartMs = undefined;
20532
+ toolDurationWarnEmitted = false;
20533
+ });
20534
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
20535
+ mkdirSync2(this.jobDir(id), { recursive: true });
20536
+ writeFileSync3(this.resultPath(id), result.output, "utf-8");
20537
+ const inputBeadId = runOptions.inputBeadId;
20538
+ const ownsBead = Boolean(result.beadId && !inputBeadId);
20539
+ const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
20540
+ const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && result.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
20541
+ if (ownsBead && result.beadId) {
20542
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
20543
+ } else if (shouldWriteExternalBeadNotes) {
20544
+ if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
20545
+ this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(result));
20546
+ } else if (result.beadId) {
20547
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
20548
+ }
20549
+ }
20550
+ if (result.beadId) {
20551
+ if (!inputBeadId) {
20552
+ this.opts.beadsClient?.closeBead(result.beadId, "COMPLETE", result.durationMs, result.model);
20553
+ }
20554
+ }
20555
+ setStatus({
20556
+ status: "done",
20557
+ elapsed_s: elapsed,
20558
+ last_event_at_ms: Date.now(),
20559
+ model: result.model,
20560
+ backend: result.backend,
20561
+ bead_id: result.beadId
20562
+ });
20563
+ appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
20564
+ model: result.model,
20565
+ backend: result.backend,
20566
+ bead_id: result.beadId,
20567
+ output: result.output
20568
+ }));
20569
+ mkdirSync2(this.readyDir(), { recursive: true });
20570
+ writeFileSync3(join10(this.readyDir(), id), "", "utf-8");
20571
+ return id;
20572
+ } catch (err) {
20573
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
20574
+ const errorMsg = err?.message ?? String(err);
20575
+ setStatus({
20576
+ status: "error",
20577
+ elapsed_s: elapsed,
20578
+ error: errorMsg
20579
+ });
20580
+ appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
20581
+ error: errorMsg
20582
+ }));
20583
+ throw err;
20584
+ } finally {
20585
+ if (stuckIntervalId !== undefined)
20586
+ clearInterval(stuckIntervalId);
20587
+ process.removeListener("SIGTERM", sigtermHandler);
20588
+ try {
20589
+ fifoReadline?.close();
20590
+ } catch {}
20591
+ try {
20592
+ fifoReadStream?.destroy();
20593
+ } catch {}
20594
+ try {
20595
+ fsyncSync(eventsFd);
20596
+ } catch {}
20597
+ closeSync(eventsFd);
20598
+ try {
20599
+ if (existsSync10(fifoPath))
20600
+ rmSync(fifoPath);
20601
+ } catch {}
20602
+ if (statusSnapshot.tmux_session) {
20603
+ spawnSync6("tmux", ["kill-session", "-t", statusSnapshot.tmux_session], { stdio: "ignore" });
20604
+ }
20651
20605
  }
20652
- throw error2;
20653
- }
20654
- const keyPath = splitKeyPath(args.key);
20655
- const projectDir = process.cwd();
20656
- let files;
20657
- try {
20658
- files = args.name ? [await findNamedSpecialistFile(projectDir, args.name)] : await listSpecialistFiles(projectDir);
20659
- } catch (error2) {
20660
- const message = error2 instanceof Error ? error2.message : String(error2);
20661
- console.error(message);
20662
- process.exit(1);
20663
- return;
20664
- }
20665
- if (files.length === 0) {
20666
- console.error("No specialists found in config/specialists/");
20667
- process.exit(1);
20668
- return;
20669
- }
20670
- if (args.command === "get") {
20671
- await getAcrossFiles(files, keyPath);
20672
- return;
20673
20606
  }
20674
- await setAcrossFiles(files, keyPath, args.value);
20675
20607
  }
20676
- var green6 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError3;
20677
- var init_config = __esm(() => {
20678
- init_dist();
20679
- ArgParseError3 = class ArgParseError3 extends Error {
20680
- constructor(message) {
20681
- super(message);
20682
- this.name = "ArgParseError";
20683
- }
20608
+ var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS;
20609
+ var init_supervisor = __esm(() => {
20610
+ init_timeline_events();
20611
+ JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
20612
+ STALL_DETECTION_DEFAULTS = {
20613
+ running_silence_warn_ms: 60000,
20614
+ running_silence_error_ms: 300000,
20615
+ waiting_stale_ms: 3600000,
20616
+ tool_duration_warn_ms: 120000
20684
20617
  };
20685
20618
  });
20686
20619
 
@@ -20832,7 +20765,7 @@ var init_format_helpers = __esm(() => {
20832
20765
  });
20833
20766
 
20834
20767
  // src/cli/tmux-utils.ts
20835
- import { spawnSync as spawnSync8 } from "node:child_process";
20768
+ import { spawnSync as spawnSync7 } from "node:child_process";
20836
20769
  function escapeForSingleQuotedBash(script) {
20837
20770
  return script.replace(/'/g, "'\\''");
20838
20771
  }
@@ -20840,7 +20773,7 @@ function quoteShellValue(value) {
20840
20773
  return `'${escapeForSingleQuotedBash(value)}'`;
20841
20774
  }
20842
20775
  function isTmuxAvailable() {
20843
- return spawnSync8("which", ["tmux"], { encoding: "utf8", timeout: 2000 }).status === 0;
20776
+ return spawnSync7("which", ["tmux"], { encoding: "utf8", timeout: 2000 }).status === 0;
20844
20777
  }
20845
20778
  function buildSessionName(specialist, suffix) {
20846
20779
  return `${TMUX_SESSION_PREFIX}-${specialist}-${suffix}`;
@@ -20855,7 +20788,7 @@ function createTmuxSession(name, cwd, cmd, extraEnv = {}) {
20855
20788
  }
20856
20789
  const startupScript = `${exports.join("; ")}; exec ${cmd}`;
20857
20790
  const wrappedCommand = `/bin/bash -c '${escapeForSingleQuotedBash(startupScript)}'`;
20858
- const result = spawnSync8("tmux", ["new-session", "-d", "-s", name, "-c", cwd, wrappedCommand], { encoding: "utf8", stdio: "pipe" });
20791
+ const result = spawnSync7("tmux", ["new-session", "-d", "-s", name, "-c", cwd, wrappedCommand], { encoding: "utf8", stdio: "pipe" });
20859
20792
  if (result.status !== 0) {
20860
20793
  const errorOutput = (result.stderr ?? "").trim() || (result.error?.message ?? "unknown error");
20861
20794
  throw new Error(`Failed to create tmux session "${name}": ${errorOutput}`);
@@ -20869,8 +20802,8 @@ var exports_run = {};
20869
20802
  __export(exports_run, {
20870
20803
  run: () => run9
20871
20804
  });
20872
- import { join as join18 } from "node:path";
20873
- import { readFileSync as readFileSync8 } from "node:fs";
20805
+ import { join as join11 } from "node:path";
20806
+ import { readFileSync as readFileSync6 } from "node:fs";
20874
20807
  import { randomBytes } from "node:crypto";
20875
20808
  import { spawn as cpSpawn } from "node:child_process";
20876
20809
  async function parseArgs6(argv) {
@@ -20959,13 +20892,13 @@ async function parseArgs6(argv) {
20959
20892
  return { name, prompt, beadId, model, noBeads, noBeadNotes, keepAlive, noKeepAlive, background, contextDepth, outputMode };
20960
20893
  }
20961
20894
  function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
20962
- const eventsPath = join18(jobsDir, jobId, "events.jsonl");
20895
+ const eventsPath = join11(jobsDir, jobId, "events.jsonl");
20963
20896
  let linesRead = 0;
20964
20897
  let activeInlinePhase = null;
20965
20898
  const drain = () => {
20966
20899
  let content;
20967
20900
  try {
20968
- content = readFileSync8(eventsPath, "utf-8");
20901
+ content = readFileSync6(eventsPath, "utf-8");
20969
20902
  } catch {
20970
20903
  return;
20971
20904
  }
@@ -21027,10 +20960,10 @@ function shellQuote(value) {
21027
20960
  async function run9() {
21028
20961
  const args = await parseArgs6(process.argv.slice(3));
21029
20962
  if (args.background) {
21030
- const latestPath = join18(process.cwd(), ".specialists", "jobs", "latest");
20963
+ const latestPath = join11(process.cwd(), ".specialists", "jobs", "latest");
21031
20964
  const oldLatest = (() => {
21032
20965
  try {
21033
- return readFileSync8(latestPath, "utf-8").trim();
20966
+ return readFileSync6(latestPath, "utf-8").trim();
21034
20967
  } catch {
21035
20968
  return "";
21036
20969
  }
@@ -21058,7 +20991,7 @@ async function run9() {
21058
20991
  while (Date.now() < deadline) {
21059
20992
  await new Promise((r) => setTimeout(r, 100));
21060
20993
  try {
21061
- const current = readFileSync8(latestPath, "utf-8").trim();
20994
+ const current = readFileSync6(latestPath, "utf-8").trim();
21062
20995
  if (current && current !== oldLatest) {
21063
20996
  jobId2 = current;
21064
20997
  break;
@@ -21078,7 +21011,7 @@ async function run9() {
21078
21011
  }
21079
21012
  const loader = new SpecialistLoader;
21080
21013
  const circuitBreaker = new CircuitBreaker;
21081
- const hooks = new HookEmitter({ tracePath: join18(process.cwd(), ".specialists", "trace.jsonl") });
21014
+ const hooks = new HookEmitter({ tracePath: join11(process.cwd(), ".specialists", "trace.jsonl") });
21082
21015
  const beadsClient = args.noBeads ? undefined : new BeadsClient;
21083
21016
  const beadReader = beadsClient ?? new BeadsClient;
21084
21017
  let prompt = args.prompt;
@@ -21113,7 +21046,7 @@ async function run9() {
21113
21046
  beadsClient
21114
21047
  });
21115
21048
  const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
21116
- const jobsDir = join18(process.cwd(), ".specialists", "jobs");
21049
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
21117
21050
  let stopTailer;
21118
21051
  const supervisor = new Supervisor({
21119
21052
  runner,
@@ -21192,9 +21125,9 @@ var exports_status = {};
21192
21125
  __export(exports_status, {
21193
21126
  run: () => run10
21194
21127
  });
21195
- import { spawnSync as spawnSync9 } from "node:child_process";
21196
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
21197
- import { join as join19 } from "node:path";
21128
+ import { spawnSync as spawnSync8 } from "node:child_process";
21129
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
21130
+ import { join as join12 } from "node:path";
21198
21131
  function ok2(msg) {
21199
21132
  console.log(` ${green7("✓")} ${msg}`);
21200
21133
  }
@@ -21213,7 +21146,7 @@ function section(label) {
21213
21146
  ${bold7(`── ${label} ${line}`)}`);
21214
21147
  }
21215
21148
  function cmd(bin, args) {
21216
- const r = spawnSync9(bin, args, {
21149
+ const r = spawnSync8(bin, args, {
21217
21150
  encoding: "utf8",
21218
21151
  stdio: "pipe",
21219
21152
  timeout: 5000
@@ -21221,7 +21154,7 @@ function cmd(bin, args) {
21221
21154
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
21222
21155
  }
21223
21156
  function isInstalled(bin) {
21224
- return spawnSync9("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21157
+ return spawnSync8("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21225
21158
  }
21226
21159
  function formatElapsed2(s) {
21227
21160
  if (s.elapsed_s === undefined)
@@ -21273,10 +21206,10 @@ function parseStatusArgs(argv) {
21273
21206
  return { jsonMode, jobId };
21274
21207
  }
21275
21208
  function countJobEvents(jobsDir, jobId) {
21276
- const eventsFile = join19(jobsDir, jobId, "events.jsonl");
21277
- if (!existsSync14(eventsFile))
21209
+ const eventsFile = join12(jobsDir, jobId, "events.jsonl");
21210
+ if (!existsSync11(eventsFile))
21278
21211
  return 0;
21279
- const raw = readFileSync9(eventsFile, "utf-8").trim();
21212
+ const raw = readFileSync7(eventsFile, "utf-8").trim();
21280
21213
  if (!raw)
21281
21214
  return 0;
21282
21215
  return raw.split(`
@@ -21319,12 +21252,12 @@ async function run10() {
21319
21252
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
21320
21253
  const bdInstalled = isInstalled("bd");
21321
21254
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
21322
- const beadsPresent = existsSync14(join19(process.cwd(), ".beads"));
21255
+ const beadsPresent = existsSync11(join12(process.cwd(), ".beads"));
21323
21256
  const specialistsBin = cmd("which", ["specialists"]);
21324
- const jobsDir = join19(process.cwd(), ".specialists", "jobs");
21257
+ const jobsDir = join12(process.cwd(), ".specialists", "jobs");
21325
21258
  let jobs = [];
21326
21259
  let supervisor = null;
21327
- if (existsSync14(jobsDir)) {
21260
+ if (existsSync11(jobsDir)) {
21328
21261
  supervisor = new Supervisor({
21329
21262
  runner: null,
21330
21263
  runOptions: null,
@@ -21469,8 +21402,8 @@ var exports_result = {};
21469
21402
  __export(exports_result, {
21470
21403
  run: () => run11
21471
21404
  });
21472
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
21473
- import { join as join20 } from "node:path";
21405
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
21406
+ import { join as join13 } from "node:path";
21474
21407
  function parseArgs7(argv) {
21475
21408
  const jobId = argv[0];
21476
21409
  if (!jobId || jobId.startsWith("--")) {
@@ -21500,9 +21433,9 @@ function parseArgs7(argv) {
21500
21433
  async function run11() {
21501
21434
  const args = parseArgs7(process.argv.slice(3));
21502
21435
  const { jobId } = args;
21503
- const jobsDir = join20(process.cwd(), ".specialists", "jobs");
21436
+ const jobsDir = join13(process.cwd(), ".specialists", "jobs");
21504
21437
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21505
- const resultPath = join20(jobsDir, jobId, "result.txt");
21438
+ const resultPath = join13(jobsDir, jobId, "result.txt");
21506
21439
  if (args.wait) {
21507
21440
  const startMs = Date.now();
21508
21441
  while (true) {
@@ -21512,11 +21445,11 @@ async function run11() {
21512
21445
  process.exit(1);
21513
21446
  }
21514
21447
  if (status2.status === "done") {
21515
- if (!existsSync15(resultPath)) {
21448
+ if (!existsSync12(resultPath)) {
21516
21449
  console.error(`Result file not found for job ${jobId}`);
21517
21450
  process.exit(1);
21518
21451
  }
21519
- process.stdout.write(readFileSync10(resultPath, "utf-8"));
21452
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21520
21453
  return;
21521
21454
  }
21522
21455
  if (status2.status === "error") {
@@ -21540,40 +21473,163 @@ async function run11() {
21540
21473
  console.error(`No job found: ${jobId}`);
21541
21474
  process.exit(1);
21542
21475
  }
21543
- if (status.status === "running" || status.status === "starting") {
21544
- if (!existsSync15(resultPath)) {
21545
- process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
21546
- `);
21547
- process.exit(1);
21548
- }
21549
- process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
21550
- `);
21551
- process.stdout.write(readFileSync10(resultPath, "utf-8"));
21552
- return;
21476
+ if (status.status === "running" || status.status === "starting") {
21477
+ if (!existsSync12(resultPath)) {
21478
+ process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
21479
+ `);
21480
+ process.exit(1);
21481
+ }
21482
+ process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
21483
+ `);
21484
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21485
+ return;
21486
+ }
21487
+ if (status.status === "error") {
21488
+ process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
21489
+ `);
21490
+ process.exit(1);
21491
+ }
21492
+ if (!existsSync12(resultPath)) {
21493
+ console.error(`Result file not found for job ${jobId}`);
21494
+ process.exit(1);
21495
+ }
21496
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21497
+ }
21498
+ var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
21499
+ var init_result = __esm(() => {
21500
+ init_supervisor();
21501
+ });
21502
+
21503
+ // src/specialist/timeline-query.ts
21504
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "node:fs";
21505
+ import { join as join14 } from "node:path";
21506
+ function readJobEvents(jobDir) {
21507
+ const eventsPath = join14(jobDir, "events.jsonl");
21508
+ if (!existsSync13(eventsPath))
21509
+ return [];
21510
+ const content = readFileSync9(eventsPath, "utf-8");
21511
+ const lines = content.split(`
21512
+ `).filter(Boolean);
21513
+ const events = [];
21514
+ for (const line of lines) {
21515
+ const event = parseTimelineEvent(line);
21516
+ if (event)
21517
+ events.push(event);
21518
+ }
21519
+ events.sort(compareTimelineEvents);
21520
+ return events;
21521
+ }
21522
+ function readJobEventsById(jobsDir, jobId) {
21523
+ return readJobEvents(join14(jobsDir, jobId));
21524
+ }
21525
+ function readAllJobEvents(jobsDir) {
21526
+ if (!existsSync13(jobsDir))
21527
+ return [];
21528
+ const batches = [];
21529
+ const entries = readdirSync5(jobsDir);
21530
+ for (const entry of entries) {
21531
+ const jobDir = join14(jobsDir, entry);
21532
+ try {
21533
+ const stat2 = __require("node:fs").statSync(jobDir);
21534
+ if (!stat2.isDirectory())
21535
+ continue;
21536
+ } catch {
21537
+ continue;
21538
+ }
21539
+ const jobId = entry;
21540
+ const statusPath = join14(jobDir, "status.json");
21541
+ let specialist = "unknown";
21542
+ let beadId;
21543
+ if (existsSync13(statusPath)) {
21544
+ try {
21545
+ const status = JSON.parse(readFileSync9(statusPath, "utf-8"));
21546
+ specialist = status.specialist ?? "unknown";
21547
+ beadId = status.bead_id;
21548
+ } catch {}
21549
+ }
21550
+ const events = readJobEvents(jobDir);
21551
+ if (events.length > 0) {
21552
+ batches.push({ jobId, specialist, beadId, events });
21553
+ }
21554
+ }
21555
+ return batches;
21556
+ }
21557
+ function mergeTimelineEvents(batches) {
21558
+ const merged = [];
21559
+ for (const batch of batches) {
21560
+ for (const event of batch.events) {
21561
+ merged.push({
21562
+ jobId: batch.jobId,
21563
+ specialist: batch.specialist,
21564
+ beadId: batch.beadId,
21565
+ event
21566
+ });
21567
+ }
21568
+ }
21569
+ merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
21570
+ return merged;
21571
+ }
21572
+ function filterTimelineEvents(merged, filter) {
21573
+ let result = merged;
21574
+ if (filter.since !== undefined) {
21575
+ result = result.filter(({ event }) => event.t >= filter.since);
21576
+ }
21577
+ if (filter.jobId !== undefined) {
21578
+ result = result.filter(({ jobId }) => jobId === filter.jobId);
21579
+ }
21580
+ if (filter.specialist !== undefined) {
21581
+ result = result.filter(({ specialist }) => specialist === filter.specialist);
21582
+ }
21583
+ if (filter.limit !== undefined && filter.limit > 0) {
21584
+ result = result.slice(0, filter.limit);
21553
21585
  }
21554
- if (status.status === "error") {
21555
- process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
21556
- `);
21557
- process.exit(1);
21586
+ return result;
21587
+ }
21588
+ function queryTimeline(jobsDir, filter = {}) {
21589
+ let batches = readAllJobEvents(jobsDir);
21590
+ if (filter.jobId !== undefined) {
21591
+ batches = batches.filter((b) => b.jobId === filter.jobId);
21558
21592
  }
21559
- if (!existsSync15(resultPath)) {
21560
- console.error(`Result file not found for job ${jobId}`);
21561
- process.exit(1);
21593
+ if (filter.specialist !== undefined) {
21594
+ batches = batches.filter((b) => b.specialist === filter.specialist);
21562
21595
  }
21563
- process.stdout.write(readFileSync10(resultPath, "utf-8"));
21596
+ const merged = mergeTimelineEvents(batches);
21597
+ return filterTimelineEvents(merged, filter);
21564
21598
  }
21565
- var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
21566
- var init_result = __esm(() => {
21567
- init_supervisor();
21599
+ var init_timeline_query = __esm(() => {
21600
+ init_timeline_events();
21568
21601
  });
21569
21602
 
21603
+ // src/specialist/model-display.ts
21604
+ function extractModelId(model) {
21605
+ if (!model)
21606
+ return;
21607
+ const trimmed = model.trim();
21608
+ if (!trimmed)
21609
+ return;
21610
+ return trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
21611
+ }
21612
+ function toModelAlias(model) {
21613
+ const modelId = extractModelId(model);
21614
+ if (!modelId)
21615
+ return;
21616
+ if (modelId.startsWith("claude-")) {
21617
+ return modelId.slice("claude-".length);
21618
+ }
21619
+ return modelId;
21620
+ }
21621
+ function formatSpecialistModel(specialist, model) {
21622
+ const alias = toModelAlias(model);
21623
+ return alias ? `${specialist}/${alias}` : specialist;
21624
+ }
21625
+
21570
21626
  // src/cli/feed.ts
21571
21627
  var exports_feed = {};
21572
21628
  __export(exports_feed, {
21573
21629
  run: () => run12
21574
21630
  });
21575
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
21576
- import { join as join21 } from "node:path";
21631
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
21632
+ import { join as join15 } from "node:path";
21577
21633
  function getHumanEventKey(event) {
21578
21634
  switch (event.type) {
21579
21635
  case "meta":
@@ -21637,9 +21693,9 @@ function parseSince(value) {
21637
21693
  return;
21638
21694
  }
21639
21695
  function isTerminalJobStatus(jobsDir, jobId) {
21640
- const statusPath = join21(jobsDir, jobId, "status.json");
21696
+ const statusPath = join15(jobsDir, jobId, "status.json");
21641
21697
  try {
21642
- const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21698
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21643
21699
  return status.status === "done" || status.status === "error";
21644
21700
  } catch {
21645
21701
  return false;
@@ -21650,10 +21706,10 @@ function makeJobMetaReader(jobsDir) {
21650
21706
  return (jobId) => {
21651
21707
  if (cache.has(jobId))
21652
21708
  return cache.get(jobId);
21653
- const statusPath = join21(jobsDir, jobId, "status.json");
21709
+ const statusPath = join15(jobsDir, jobId, "status.json");
21654
21710
  let meta = { startedAtMs: Date.now() };
21655
21711
  try {
21656
- const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21712
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21657
21713
  meta = {
21658
21714
  model: status.model,
21659
21715
  backend: status.backend,
@@ -21845,8 +21901,8 @@ async function followMerged(jobsDir, options) {
21845
21901
  }
21846
21902
  async function run12() {
21847
21903
  const options = parseArgs8(process.argv.slice(3));
21848
- const jobsDir = join21(process.cwd(), ".specialists", "jobs");
21849
- if (!existsSync16(jobsDir)) {
21904
+ const jobsDir = join15(process.cwd(), ".specialists", "jobs");
21905
+ if (!existsSync14(jobsDir)) {
21850
21906
  console.log(dim7("No jobs directory found."));
21851
21907
  return;
21852
21908
  }
@@ -21873,8 +21929,8 @@ var exports_poll = {};
21873
21929
  __export(exports_poll, {
21874
21930
  run: () => run13
21875
21931
  });
21876
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
21877
- import { join as join22 } from "node:path";
21932
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
21933
+ import { join as join16 } from "node:path";
21878
21934
  function parseArgs9(argv) {
21879
21935
  let jobId;
21880
21936
  let cursor = 0;
@@ -21911,19 +21967,19 @@ function parseArgs9(argv) {
21911
21967
  return { jobId, cursor, outputCursor };
21912
21968
  }
21913
21969
  function readJobState(jobsDir, jobId, cursor, outputCursor) {
21914
- const jobDir = join22(jobsDir, jobId);
21915
- const statusPath = join22(jobDir, "status.json");
21970
+ const jobDir = join16(jobsDir, jobId);
21971
+ const statusPath = join16(jobDir, "status.json");
21916
21972
  let status = null;
21917
- if (existsSync17(statusPath)) {
21973
+ if (existsSync15(statusPath)) {
21918
21974
  try {
21919
- status = JSON.parse(readFileSync12(statusPath, "utf-8"));
21975
+ status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21920
21976
  } catch {}
21921
21977
  }
21922
- const resultPath = join22(jobDir, "result.txt");
21978
+ const resultPath = join16(jobDir, "result.txt");
21923
21979
  let fullOutput = "";
21924
- if (existsSync17(resultPath)) {
21980
+ if (existsSync15(resultPath)) {
21925
21981
  try {
21926
- fullOutput = readFileSync12(resultPath, "utf-8");
21982
+ fullOutput = readFileSync11(resultPath, "utf-8");
21927
21983
  } catch {}
21928
21984
  }
21929
21985
  const events = readJobEventsById(jobsDir, jobId);
@@ -21955,9 +22011,9 @@ function readJobState(jobsDir, jobId, cursor, outputCursor) {
21955
22011
  }
21956
22012
  async function run13() {
21957
22013
  const { jobId, cursor, outputCursor } = parseArgs9(process.argv.slice(3));
21958
- const jobsDir = join22(process.cwd(), ".specialists", "jobs");
21959
- const jobDir = join22(jobsDir, jobId);
21960
- if (!existsSync17(jobDir)) {
22014
+ const jobsDir = join16(process.cwd(), ".specialists", "jobs");
22015
+ const jobDir = join16(jobsDir, jobId);
22016
+ if (!existsSync15(jobDir)) {
21961
22017
  const result2 = {
21962
22018
  job_id: jobId,
21963
22019
  status: "error",
@@ -21988,8 +22044,8 @@ var exports_steer = {};
21988
22044
  __export(exports_steer, {
21989
22045
  run: () => run14
21990
22046
  });
21991
- import { join as join23 } from "node:path";
21992
- import { writeFileSync as writeFileSync6 } from "node:fs";
22047
+ import { join as join17 } from "node:path";
22048
+ import { writeFileSync as writeFileSync4 } from "node:fs";
21993
22049
  async function run14() {
21994
22050
  const jobId = process.argv[3];
21995
22051
  const message = process.argv[4];
@@ -21997,7 +22053,7 @@ async function run14() {
21997
22053
  console.error('Usage: specialists|sp steer <job-id> "<message>"');
21998
22054
  process.exit(1);
21999
22055
  }
22000
- const jobsDir = join23(process.cwd(), ".specialists", "jobs");
22056
+ const jobsDir = join17(process.cwd(), ".specialists", "jobs");
22001
22057
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22002
22058
  const status = supervisor.readStatus(jobId);
22003
22059
  if (!status) {
@@ -22019,7 +22075,7 @@ async function run14() {
22019
22075
  try {
22020
22076
  const payload = JSON.stringify({ type: "steer", message }) + `
22021
22077
  `;
22022
- writeFileSync6(status.fifo_path, payload, { flag: "a" });
22078
+ writeFileSync4(status.fifo_path, payload, { flag: "a" });
22023
22079
  process.stdout.write(`${green9("✓")} Steer message sent to job ${jobId}
22024
22080
  `);
22025
22081
  } catch (err) {
@@ -22038,8 +22094,8 @@ var exports_resume = {};
22038
22094
  __export(exports_resume, {
22039
22095
  run: () => run15
22040
22096
  });
22041
- import { join as join24 } from "node:path";
22042
- import { writeFileSync as writeFileSync7 } from "node:fs";
22097
+ import { join as join18 } from "node:path";
22098
+ import { writeFileSync as writeFileSync5 } from "node:fs";
22043
22099
  async function run15() {
22044
22100
  const jobId = process.argv[3];
22045
22101
  const task = process.argv[4];
@@ -22047,7 +22103,7 @@ async function run15() {
22047
22103
  console.error('Usage: specialists|sp resume <job-id> "<task>"');
22048
22104
  process.exit(1);
22049
22105
  }
22050
- const jobsDir = join24(process.cwd(), ".specialists", "jobs");
22106
+ const jobsDir = join18(process.cwd(), ".specialists", "jobs");
22051
22107
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22052
22108
  const status = supervisor.readStatus(jobId);
22053
22109
  if (!status) {
@@ -22069,7 +22125,7 @@ async function run15() {
22069
22125
  try {
22070
22126
  const payload = JSON.stringify({ type: "resume", task }) + `
22071
22127
  `;
22072
- writeFileSync7(status.fifo_path, payload, { flag: "a" });
22128
+ writeFileSync5(status.fifo_path, payload, { flag: "a" });
22073
22129
  process.stdout.write(`${green10("✓")} Resume sent to job ${jobId}
22074
22130
  `);
22075
22131
  process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
@@ -22102,13 +22158,13 @@ __export(exports_clean, {
22102
22158
  run: () => run17
22103
22159
  });
22104
22160
  import {
22105
- existsSync as existsSync18,
22161
+ existsSync as existsSync16,
22106
22162
  readdirSync as readdirSync6,
22107
- readFileSync as readFileSync13,
22163
+ readFileSync as readFileSync12,
22108
22164
  rmSync as rmSync2,
22109
22165
  statSync as statSync2
22110
22166
  } from "node:fs";
22111
- import { join as join25 } from "node:path";
22167
+ import { join as join19 } from "node:path";
22112
22168
  function parseTtlDaysFromEnvironment() {
22113
22169
  const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
22114
22170
  if (!rawValue)
@@ -22164,7 +22220,7 @@ function readDirectorySizeBytes(directoryPath) {
22164
22220
  let totalBytes = 0;
22165
22221
  const entries = readdirSync6(directoryPath, { withFileTypes: true });
22166
22222
  for (const entry of entries) {
22167
- const entryPath = join25(directoryPath, entry.name);
22223
+ const entryPath = join19(directoryPath, entry.name);
22168
22224
  const stats = statSync2(entryPath);
22169
22225
  if (stats.isDirectory()) {
22170
22226
  totalBytes += readDirectorySizeBytes(entryPath);
@@ -22177,13 +22233,13 @@ function readDirectorySizeBytes(directoryPath) {
22177
22233
  function readCompletedJobDirectory(baseDirectory, entry) {
22178
22234
  if (!entry.isDirectory())
22179
22235
  return null;
22180
- const directoryPath = join25(baseDirectory, entry.name);
22181
- const statusFilePath = join25(directoryPath, "status.json");
22182
- if (!existsSync18(statusFilePath))
22236
+ const directoryPath = join19(baseDirectory, entry.name);
22237
+ const statusFilePath = join19(directoryPath, "status.json");
22238
+ if (!existsSync16(statusFilePath))
22183
22239
  return null;
22184
22240
  let statusData;
22185
22241
  try {
22186
- statusData = JSON.parse(readFileSync13(statusFilePath, "utf-8"));
22242
+ statusData = JSON.parse(readFileSync12(statusFilePath, "utf-8"));
22187
22243
  } catch {
22188
22244
  return null;
22189
22245
  }
@@ -22264,8 +22320,8 @@ async function run17() {
22264
22320
  const message = error2 instanceof Error ? error2.message : String(error2);
22265
22321
  printUsageAndExit(message);
22266
22322
  }
22267
- const jobsDirectoryPath = join25(process.cwd(), ".specialists", "jobs");
22268
- if (!existsSync18(jobsDirectoryPath)) {
22323
+ const jobsDirectoryPath = join19(process.cwd(), ".specialists", "jobs");
22324
+ if (!existsSync16(jobsDirectoryPath)) {
22269
22325
  console.log("No jobs directory found.");
22270
22326
  return;
22271
22327
  }
@@ -22292,14 +22348,14 @@ var exports_stop = {};
22292
22348
  __export(exports_stop, {
22293
22349
  run: () => run18
22294
22350
  });
22295
- import { join as join26 } from "node:path";
22351
+ import { join as join20 } from "node:path";
22296
22352
  async function run18() {
22297
22353
  const jobId = process.argv[3];
22298
22354
  if (!jobId) {
22299
22355
  console.error("Usage: specialists|sp stop <job-id>");
22300
22356
  process.exit(1);
22301
22357
  }
22302
- const jobsDir = join26(process.cwd(), ".specialists", "jobs");
22358
+ const jobsDir = join20(process.cwd(), ".specialists", "jobs");
22303
22359
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22304
22360
  const status = supervisor.readStatus(jobId);
22305
22361
  if (!status) {
@@ -22341,16 +22397,16 @@ var exports_attach = {};
22341
22397
  __export(exports_attach, {
22342
22398
  run: () => run19
22343
22399
  });
22344
- import { execFileSync as execFileSync2, spawnSync as spawnSync10 } from "node:child_process";
22345
- import { readFileSync as readFileSync14 } from "node:fs";
22346
- import { join as join27 } from "node:path";
22400
+ import { execFileSync as execFileSync2, spawnSync as spawnSync9 } from "node:child_process";
22401
+ import { readFileSync as readFileSync13 } from "node:fs";
22402
+ import { join as join21 } from "node:path";
22347
22403
  function exitWithError(message) {
22348
22404
  console.error(message);
22349
22405
  process.exit(1);
22350
22406
  }
22351
22407
  function readStatus(statusPath, jobId) {
22352
22408
  try {
22353
- return JSON.parse(readFileSync14(statusPath, "utf-8"));
22409
+ return JSON.parse(readFileSync13(statusPath, "utf-8"));
22354
22410
  } catch (error2) {
22355
22411
  if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
22356
22412
  exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs.`);
@@ -22364,8 +22420,8 @@ async function run19() {
22364
22420
  if (!jobId) {
22365
22421
  exitWithError("Usage: specialists attach <job-id>");
22366
22422
  }
22367
- const jobsDir = join27(process.cwd(), ".specialists", "jobs");
22368
- const statusPath = join27(jobsDir, jobId, "status.json");
22423
+ const jobsDir = join21(process.cwd(), ".specialists", "jobs");
22424
+ const statusPath = join21(jobsDir, jobId, "status.json");
22369
22425
  const status = readStatus(statusPath, jobId);
22370
22426
  if (status.status === "done" || status.status === "error") {
22371
22427
  exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
@@ -22374,7 +22430,7 @@ async function run19() {
22374
22430
  if (!sessionName) {
22375
22431
  exitWithError("Job `" + jobId + "` has no tmux session. It may have been started without tmux or tmux was not installed.");
22376
22432
  }
22377
- const whichTmux = spawnSync10("which", ["tmux"], { stdio: "ignore" });
22433
+ const whichTmux = spawnSync9("which", ["tmux"], { stdio: "ignore" });
22378
22434
  if (whichTmux.status !== 0) {
22379
22435
  exitWithError("tmux is not installed. Install tmux to use `specialists attach`.");
22380
22436
  }
@@ -22621,9 +22677,9 @@ var exports_doctor = {};
22621
22677
  __export(exports_doctor, {
22622
22678
  run: () => run21
22623
22679
  });
22624
- import { spawnSync as spawnSync11 } from "node:child_process";
22625
- import { existsSync as existsSync19, mkdirSync as mkdirSync3, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "node:fs";
22626
- import { join as join28 } from "node:path";
22680
+ import { spawnSync as spawnSync10 } from "node:child_process";
22681
+ import { existsSync as existsSync17, mkdirSync as mkdirSync3, readFileSync as readFileSync14, readdirSync as readdirSync7 } from "node:fs";
22682
+ import { join as join22 } from "node:path";
22627
22683
  function ok3(msg) {
22628
22684
  console.log(` ${green13("✓")} ${msg}`);
22629
22685
  }
@@ -22645,17 +22701,17 @@ function section3(label) {
22645
22701
  ${bold10(`── ${label} ${line}`)}`);
22646
22702
  }
22647
22703
  function sp(bin, args) {
22648
- const r = spawnSync11(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22704
+ const r = spawnSync10(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22649
22705
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
22650
22706
  }
22651
22707
  function isInstalled2(bin) {
22652
- return spawnSync11("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22708
+ return spawnSync10("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22653
22709
  }
22654
22710
  function loadJson2(path) {
22655
- if (!existsSync19(path))
22711
+ if (!existsSync17(path))
22656
22712
  return null;
22657
22713
  try {
22658
- return JSON.parse(readFileSync15(path, "utf8"));
22714
+ return JSON.parse(readFileSync14(path, "utf8"));
22659
22715
  } catch {
22660
22716
  return null;
22661
22717
  }
@@ -22698,7 +22754,7 @@ function checkBd() {
22698
22754
  return false;
22699
22755
  }
22700
22756
  ok3(`bd installed ${dim12(sp("bd", ["--version"]).stdout || "")}`);
22701
- if (existsSync19(join28(CWD, ".beads")))
22757
+ if (existsSync17(join22(CWD, ".beads")))
22702
22758
  ok3(".beads/ present in project");
22703
22759
  else
22704
22760
  warn2(".beads/ not found in project");
@@ -22718,8 +22774,8 @@ function checkHooks() {
22718
22774
  section3("Claude Code hooks (2 expected)");
22719
22775
  let allPresent = true;
22720
22776
  for (const name of HOOK_NAMES) {
22721
- const dest = join28(HOOKS_DIR, name);
22722
- if (!existsSync19(dest)) {
22777
+ const dest = join22(HOOKS_DIR, name);
22778
+ if (!existsSync17(dest)) {
22723
22779
  fail2(`${name} ${red7("missing")}`);
22724
22780
  fix("specialists install");
22725
22781
  allPresent = false;
@@ -22763,18 +22819,18 @@ function checkMCP() {
22763
22819
  }
22764
22820
  function checkRuntimeDirs() {
22765
22821
  section3(".specialists/ runtime directories");
22766
- const rootDir = join28(CWD, ".specialists");
22767
- const jobsDir = join28(rootDir, "jobs");
22768
- const readyDir = join28(rootDir, "ready");
22822
+ const rootDir = join22(CWD, ".specialists");
22823
+ const jobsDir = join22(rootDir, "jobs");
22824
+ const readyDir = join22(rootDir, "ready");
22769
22825
  let allOk = true;
22770
- if (!existsSync19(rootDir)) {
22826
+ if (!existsSync17(rootDir)) {
22771
22827
  warn2(".specialists/ not found in current project");
22772
22828
  fix("specialists init");
22773
22829
  allOk = false;
22774
22830
  } else {
22775
22831
  ok3(".specialists/ present");
22776
22832
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
22777
- if (!existsSync19(subDir)) {
22833
+ if (!existsSync17(subDir)) {
22778
22834
  warn2(`.specialists/${label}/ missing — auto-creating`);
22779
22835
  mkdirSync3(subDir, { recursive: true });
22780
22836
  ok3(`.specialists/${label}/ created`);
@@ -22787,8 +22843,8 @@ function checkRuntimeDirs() {
22787
22843
  }
22788
22844
  function checkZombieJobs() {
22789
22845
  section3("Background jobs");
22790
- const jobsDir = join28(CWD, ".specialists", "jobs");
22791
- if (!existsSync19(jobsDir)) {
22846
+ const jobsDir = join22(CWD, ".specialists", "jobs");
22847
+ if (!existsSync17(jobsDir)) {
22792
22848
  hint("No .specialists/jobs/ — skipping");
22793
22849
  return true;
22794
22850
  }
@@ -22806,11 +22862,11 @@ function checkZombieJobs() {
22806
22862
  let total = 0;
22807
22863
  let running = 0;
22808
22864
  for (const jobId of entries) {
22809
- const statusPath = join28(jobsDir, jobId, "status.json");
22810
- if (!existsSync19(statusPath))
22865
+ const statusPath = join22(jobsDir, jobId, "status.json");
22866
+ if (!existsSync17(statusPath))
22811
22867
  continue;
22812
22868
  try {
22813
- const status = JSON.parse(readFileSync15(statusPath, "utf8"));
22869
+ const status = JSON.parse(readFileSync14(statusPath, "utf8"));
22814
22870
  total++;
22815
22871
  if (status.status === "running" || status.status === "starting") {
22816
22872
  const pid = status.pid;
@@ -22862,11 +22918,11 @@ ${bold10("specialists doctor")}
22862
22918
  var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green13 = (s) => `\x1B[32m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
22863
22919
  var init_doctor = __esm(() => {
22864
22920
  CWD = process.cwd();
22865
- CLAUDE_DIR = join28(CWD, ".claude");
22866
- SPECIALISTS_DIR = join28(CWD, ".specialists");
22867
- HOOKS_DIR = join28(SPECIALISTS_DIR, "default", "hooks");
22868
- SETTINGS_FILE = join28(CLAUDE_DIR, "settings.json");
22869
- MCP_FILE2 = join28(CWD, ".mcp.json");
22921
+ CLAUDE_DIR = join22(CWD, ".claude");
22922
+ SPECIALISTS_DIR = join22(CWD, ".specialists");
22923
+ HOOKS_DIR = join22(SPECIALISTS_DIR, "default", "hooks");
22924
+ SETTINGS_FILE = join22(CLAUDE_DIR, "settings.json");
22925
+ MCP_FILE2 = join22(CWD, ".mcp.json");
22870
22926
  HOOK_NAMES = [
22871
22927
  "specialists-complete.mjs",
22872
22928
  "specialists-session-start.mjs"
@@ -30234,7 +30290,7 @@ class StdioServerTransport {
30234
30290
  }
30235
30291
 
30236
30292
  // src/server.ts
30237
- import { join as join11 } from "node:path";
30293
+ import { join as join3 } from "node:path";
30238
30294
 
30239
30295
  // src/constants.ts
30240
30296
  var LOG_PREFIX = "[specialists]";
@@ -30304,24 +30360,6 @@ init_hooks();
30304
30360
  init_circuitBreaker();
30305
30361
  init_beads();
30306
30362
 
30307
- // src/tools/specialist/list_specialists.tool.ts
30308
- init_zod();
30309
- var listSpecialistsSchema = exports_external.object({
30310
- category: exports_external.string().optional().describe("Filter by category (e.g. analysis/code)"),
30311
- scope: exports_external.enum(["project", "user", "system", "all"]).optional().describe("Filter by scope")
30312
- });
30313
- function createListSpecialistsTool(loader) {
30314
- return {
30315
- name: "list_specialists",
30316
- description: "List available specialists. Returns lightweight catalog — no prompts or full config.",
30317
- inputSchema: listSpecialistsSchema,
30318
- async execute(input) {
30319
- const list = await loader.list(input.category);
30320
- return input.scope && input.scope !== "all" ? list.filter((s) => s.scope === input.scope) : list;
30321
- }
30322
- };
30323
- }
30324
-
30325
30363
  // src/tools/specialist/use_specialist.tool.ts
30326
30364
  init_zod();
30327
30365
  init_beads();
@@ -30371,633 +30409,9 @@ function createUseSpecialistTool(runner) {
30371
30409
  };
30372
30410
  }
30373
30411
 
30374
- // src/tools/specialist/run_parallel.tool.ts
30375
- init_zod();
30376
-
30377
- // src/specialist/pipeline.ts
30378
- async function runPipeline(steps, runner, onProgress) {
30379
- const results = [];
30380
- let previousResult = "";
30381
- for (const step of steps) {
30382
- const options = {
30383
- name: step.name,
30384
- prompt: step.prompt,
30385
- variables: { ...step.variables, previous_result: previousResult },
30386
- backendOverride: step.backend_override
30387
- };
30388
- try {
30389
- const result = await runner.run(options, onProgress);
30390
- previousResult = result.output;
30391
- results.push({
30392
- specialist: step.name,
30393
- status: "fulfilled",
30394
- output: result.output,
30395
- durationMs: result.durationMs,
30396
- error: null
30397
- });
30398
- } catch (err) {
30399
- results.push({
30400
- specialist: step.name,
30401
- status: "rejected",
30402
- output: null,
30403
- durationMs: null,
30404
- error: err.message ?? String(err)
30405
- });
30406
- break;
30407
- }
30408
- }
30409
- return {
30410
- steps: results,
30411
- final_output: results[results.length - 1]?.output ?? null
30412
- };
30413
- }
30414
-
30415
- // src/tools/specialist/run_parallel.tool.ts
30416
- var InvocationSchema = objectType({
30417
- name: stringType(),
30418
- prompt: stringType(),
30419
- variables: recordType(stringType()).optional(),
30420
- backend_override: stringType().optional()
30421
- });
30422
- var runParallelSchema = objectType({
30423
- specialists: arrayType(InvocationSchema).min(1),
30424
- merge_strategy: enumType(["collect", "synthesize", "vote", "pipeline"]).default("collect"),
30425
- timeout_ms: numberType().default(120000)
30426
- });
30427
- function createRunParallelTool(runner) {
30428
- return {
30429
- name: "run_parallel",
30430
- description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer start_specialist/feed_specialist for async orchestration.",
30431
- inputSchema: runParallelSchema,
30432
- async execute(input, onProgress) {
30433
- if (input.merge_strategy === "pipeline") {
30434
- return runPipeline(input.specialists.map((s) => ({
30435
- name: s.name,
30436
- prompt: s.prompt,
30437
- variables: s.variables,
30438
- backend_override: s.backend_override
30439
- })), runner, onProgress);
30440
- }
30441
- if (input.merge_strategy !== "collect") {
30442
- throw new Error(`Merge strategy '${input.merge_strategy}' not yet implemented (v2.1)`);
30443
- }
30444
- const results = await Promise.allSettled(input.specialists.map((s) => runner.run({
30445
- name: s.name,
30446
- prompt: s.prompt,
30447
- variables: s.variables,
30448
- backendOverride: s.backend_override
30449
- }, onProgress)));
30450
- return results.map((r, i) => ({
30451
- specialist: input.specialists[i].name,
30452
- status: r.status,
30453
- output: r.status === "fulfilled" ? r.value.output : null,
30454
- durationMs: r.status === "fulfilled" ? r.value.durationMs : null,
30455
- beadId: r.status === "fulfilled" ? r.value.beadId : undefined,
30456
- error: r.status === "rejected" ? String(r.reason?.message) : null
30457
- }));
30458
- }
30459
- };
30460
- }
30461
-
30462
- // src/tools/specialist/specialist_status.tool.ts
30463
- init_zod();
30464
- init_loader();
30465
- var BACKENDS2 = ["gemini", "qwen", "anthropic", "openai"];
30466
- function createSpecialistStatusTool(loader, circuitBreaker) {
30467
- return {
30468
- name: "specialist_status",
30469
- description: "System health: backend circuit breaker states, loaded specialists, staleness. Also shows active background jobs from .specialists/jobs/.",
30470
- inputSchema: exports_external.object({}),
30471
- async execute(_) {
30472
- const list = await loader.list();
30473
- const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
30474
- const { existsSync: existsSync4, readdirSync, readFileSync: readFileSync2 } = await import("node:fs");
30475
- const { join: join3 } = await import("node:path");
30476
- const jobsDir = join3(process.cwd(), ".specialists", "jobs");
30477
- const jobs = [];
30478
- if (existsSync4(jobsDir)) {
30479
- for (const entry of readdirSync(jobsDir)) {
30480
- const statusPath = join3(jobsDir, entry, "status.json");
30481
- if (!existsSync4(statusPath))
30482
- continue;
30483
- try {
30484
- jobs.push(JSON.parse(readFileSync2(statusPath, "utf-8")));
30485
- } catch {}
30486
- }
30487
- jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
30488
- }
30489
- return {
30490
- loaded_count: list.length,
30491
- backends_health: Object.fromEntries(BACKENDS2.map((b) => [b, circuitBreaker.getState(b)])),
30492
- specialists: list.map((s, i) => ({
30493
- name: s.name,
30494
- scope: s.scope,
30495
- category: s.category,
30496
- version: s.version,
30497
- staleness: stalenessResults[i]
30498
- })),
30499
- background_jobs: jobs.map((j) => ({
30500
- id: j.id,
30501
- specialist: j.specialist,
30502
- status: j.status,
30503
- elapsed_s: j.elapsed_s,
30504
- current_event: j.current_event,
30505
- bead_id: j.bead_id,
30506
- error: j.error
30507
- }))
30508
- };
30509
- }
30510
- };
30511
- }
30512
-
30513
- // src/specialist/jobRegistry.ts
30514
- class JobRegistry {
30515
- jobs = new Map;
30516
- register(id, meta) {
30517
- this.jobs.set(id, {
30518
- id,
30519
- status: "running",
30520
- outputBuffer: "",
30521
- currentEvent: "starting",
30522
- backend: meta.backend,
30523
- model: meta.model,
30524
- specialistVersion: meta.specialistVersion ?? "?",
30525
- startedAtMs: Date.now()
30526
- });
30527
- }
30528
- appendOutput(id, text) {
30529
- const job = this.jobs.get(id);
30530
- if (job && job.status === "running")
30531
- job.outputBuffer += text;
30532
- }
30533
- setCurrentEvent(id, eventType) {
30534
- const job = this.jobs.get(id);
30535
- if (job && job.status === "running")
30536
- job.currentEvent = eventType;
30537
- }
30538
- setMeta(id, meta) {
30539
- const job = this.jobs.get(id);
30540
- if (!job)
30541
- return;
30542
- if (meta.backend)
30543
- job.backend = meta.backend;
30544
- if (meta.model)
30545
- job.model = meta.model;
30546
- }
30547
- setBeadId(id, beadId) {
30548
- const job = this.jobs.get(id);
30549
- if (!job)
30550
- return;
30551
- job.beadId = beadId;
30552
- }
30553
- setKillFn(id, killFn) {
30554
- const job = this.jobs.get(id);
30555
- if (!job)
30556
- return;
30557
- if (job.status === "cancelled") {
30558
- killFn();
30559
- return;
30560
- }
30561
- job.killFn = killFn;
30562
- }
30563
- setSteerFn(id, steerFn) {
30564
- const job = this.jobs.get(id);
30565
- if (!job)
30566
- return;
30567
- job.steerFn = steerFn;
30568
- }
30569
- setResumeFn(id, resumeFn, closeFn) {
30570
- const job = this.jobs.get(id);
30571
- if (!job)
30572
- return;
30573
- job.resumeFn = resumeFn;
30574
- job.closeFn = closeFn;
30575
- job.status = "waiting";
30576
- job.currentEvent = "waiting";
30577
- }
30578
- async followUp(id, message) {
30579
- const job = this.jobs.get(id);
30580
- if (!job)
30581
- return { ok: false, error: `Job not found: ${id}` };
30582
- if (job.status !== "waiting")
30583
- return { ok: false, error: `Job is not waiting (status: ${job.status})` };
30584
- if (!job.resumeFn)
30585
- return { ok: false, error: "Job has no resume function" };
30586
- job.status = "running";
30587
- job.currentEvent = "starting";
30588
- try {
30589
- const output = await job.resumeFn(message);
30590
- job.outputBuffer = output;
30591
- job.status = "waiting";
30592
- job.currentEvent = "waiting";
30593
- return { ok: true, output };
30594
- } catch (err) {
30595
- job.status = "error";
30596
- job.error = err?.message ?? String(err);
30597
- return { ok: false, error: job.error };
30598
- }
30599
- }
30600
- async closeSession(id) {
30601
- const job = this.jobs.get(id);
30602
- if (!job)
30603
- return { ok: false, error: `Job not found: ${id}` };
30604
- if (job.status !== "waiting")
30605
- return { ok: false, error: `Job is not in waiting state` };
30606
- try {
30607
- await job.closeFn?.();
30608
- job.status = "done";
30609
- job.currentEvent = "done";
30610
- job.endedAtMs = Date.now();
30611
- return { ok: true };
30612
- } catch (err) {
30613
- return { ok: false, error: err?.message ?? String(err) };
30614
- }
30615
- }
30616
- async steer(id, message) {
30617
- const job = this.jobs.get(id);
30618
- if (!job)
30619
- return { ok: false, error: `Job not found: ${id}` };
30620
- if (job.status !== "running")
30621
- return { ok: false, error: `Job is not running (status: ${job.status})` };
30622
- if (!job.steerFn)
30623
- return { ok: false, error: "Job session not ready for steering yet" };
30624
- try {
30625
- await job.steerFn(message);
30626
- return { ok: true };
30627
- } catch (err) {
30628
- return { ok: false, error: err?.message ?? String(err) };
30629
- }
30630
- }
30631
- complete(id, result) {
30632
- const job = this.jobs.get(id);
30633
- if (!job || job.status !== "running")
30634
- return;
30635
- job.status = "done";
30636
- job.outputBuffer = result.output;
30637
- job.currentEvent = "done";
30638
- job.backend = result.backend;
30639
- job.model = result.model;
30640
- job.specialistVersion = result.specialistVersion;
30641
- job.endedAtMs = Date.now();
30642
- if (result.beadId)
30643
- job.beadId = result.beadId;
30644
- }
30645
- fail(id, err) {
30646
- const job = this.jobs.get(id);
30647
- if (!job || job.status !== "running")
30648
- return;
30649
- job.status = "error";
30650
- job.error = err.message;
30651
- job.currentEvent = "error";
30652
- job.endedAtMs = Date.now();
30653
- }
30654
- cancel(id) {
30655
- const job = this.jobs.get(id);
30656
- if (!job)
30657
- return;
30658
- job.killFn?.();
30659
- job.status = "cancelled";
30660
- job.currentEvent = "cancelled";
30661
- job.endedAtMs = Date.now();
30662
- return { status: "cancelled", duration_ms: job.endedAtMs - job.startedAtMs };
30663
- }
30664
- snapshot(id, cursor = 0) {
30665
- const job = this.jobs.get(id);
30666
- if (!job)
30667
- return;
30668
- const isDone = job.status === "done";
30669
- return {
30670
- job_id: job.id,
30671
- status: job.status,
30672
- output: isDone ? job.outputBuffer : "",
30673
- delta: job.outputBuffer.slice(cursor),
30674
- next_cursor: job.outputBuffer.length,
30675
- current_event: job.currentEvent,
30676
- backend: job.backend,
30677
- model: job.model,
30678
- specialist_version: job.specialistVersion,
30679
- duration_ms: (job.endedAtMs ?? Date.now()) - job.startedAtMs,
30680
- error: job.error,
30681
- beadId: job.beadId
30682
- };
30683
- }
30684
- delete(id) {
30685
- this.jobs.delete(id);
30686
- }
30687
- }
30688
-
30689
- // src/tools/specialist/start_specialist.tool.ts
30690
- init_zod();
30691
- init_supervisor();
30692
- import { join as join4 } from "node:path";
30693
- init_loader();
30694
- var startSpecialistSchema = objectType({
30695
- name: stringType().describe("Specialist identifier (e.g. codebase-explorer)"),
30696
- prompt: stringType().describe("The task or question for the specialist"),
30697
- variables: recordType(stringType()).optional().describe("Additional $variable substitutions"),
30698
- backend_override: stringType().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
30699
- bead_id: stringType().optional().describe("Existing bead ID to associate with this run (propagated into status.json and run_start event)"),
30700
- keep_alive: booleanType().optional().describe("Keep the specialist session open for resume_specialist (overrides execution.interactive)."),
30701
- no_keep_alive: booleanType().optional().describe("Force one-shot behavior even when execution.interactive is true.")
30702
- });
30703
- function createStartSpecialistTool(runner, beadsClient) {
30704
- return {
30705
- name: "start_specialist",
30706
- description: "Start a specialist asynchronously. Returns job_id immediately. " + "Use feed_specialist to stream events and track progress (pass job_id and --follow for live output). " + "Use specialist_status for circuit breaker health checks. " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
30707
- inputSchema: startSpecialistSchema,
30708
- async execute(input) {
30709
- const jobsDir = join4(process.cwd(), ".specialists", "jobs");
30710
- let keepAlive;
30711
- try {
30712
- const loader = new SpecialistLoader;
30713
- const specialist = await loader.get(input.name);
30714
- const interactiveDefault = specialist.specialist.execution.interactive ? true : undefined;
30715
- keepAlive = input.no_keep_alive ? false : input.keep_alive ?? interactiveDefault;
30716
- } catch {
30717
- keepAlive = input.no_keep_alive ? false : input.keep_alive;
30718
- }
30719
- const jobStarted = new Promise((resolve2, reject) => {
30720
- const supervisor = new Supervisor({
30721
- runner,
30722
- runOptions: {
30723
- name: input.name,
30724
- prompt: input.prompt,
30725
- variables: input.variables,
30726
- backendOverride: input.backend_override,
30727
- inputBeadId: input.bead_id,
30728
- keepAlive,
30729
- noKeepAlive: input.no_keep_alive ?? false
30730
- },
30731
- jobsDir,
30732
- beadsClient,
30733
- onJobStarted: ({ id }) => resolve2(id)
30734
- });
30735
- supervisor.run().catch((error2) => {
30736
- logger.error(`start_specialist job failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
30737
- reject(error2);
30738
- });
30739
- });
30740
- const jobId = await jobStarted;
30741
- return { job_id: jobId };
30742
- }
30743
- };
30744
- }
30745
-
30746
- // src/tools/specialist/stop_specialist.tool.ts
30747
- init_zod();
30748
- init_supervisor();
30749
- import { join as join5 } from "node:path";
30750
- var stopSpecialistSchema = objectType({
30751
- job_id: stringType().describe("Job ID returned by start_specialist")
30752
- });
30753
- function createStopSpecialistTool() {
30754
- return {
30755
- name: "stop_specialist",
30756
- description: "Cancel a running specialist job by sending SIGTERM to its recorded process. Works for jobs started via start_specialist and CLI background runs.",
30757
- inputSchema: stopSpecialistSchema,
30758
- async execute(input) {
30759
- const jobsDir = join5(process.cwd(), ".specialists", "jobs");
30760
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
30761
- const status = supervisor.readStatus(input.job_id);
30762
- if (!status) {
30763
- return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
30764
- }
30765
- if (status.status === "done" || status.status === "error") {
30766
- return {
30767
- status: "error",
30768
- error: `Job is already ${status.status}`,
30769
- job_id: input.job_id
30770
- };
30771
- }
30772
- if (!status.pid) {
30773
- return { status: "error", error: `No PID recorded for job ${input.job_id}`, job_id: input.job_id };
30774
- }
30775
- try {
30776
- process.kill(status.pid, "SIGTERM");
30777
- return { status: "cancelled", job_id: input.job_id, pid: status.pid };
30778
- } catch (err) {
30779
- if (err?.code === "ESRCH") {
30780
- return { status: "error", error: `Process ${status.pid} not found`, job_id: input.job_id };
30781
- }
30782
- return { status: "error", error: err?.message ?? String(err), job_id: input.job_id };
30783
- }
30784
- }
30785
- };
30786
- }
30787
-
30788
- // src/tools/specialist/steer_specialist.tool.ts
30789
- init_zod();
30790
- init_supervisor();
30791
- import { writeFileSync as writeFileSync2 } from "node:fs";
30792
- import { join as join6 } from "node:path";
30793
- var steerSpecialistSchema = exports_external.object({
30794
- job_id: exports_external.string().describe("Job ID returned by start_specialist or printed by specialists run"),
30795
- message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
30796
- });
30797
- function createSteerSpecialistTool(registry2) {
30798
- return {
30799
- name: "steer_specialist",
30800
- description: "Send a mid-run steering message to a running specialist job. The agent receives the message after its current tool calls finish, before the next LLM call. Works for both in-process jobs (start_specialist) and CLI-started jobs (specialists run).",
30801
- inputSchema: steerSpecialistSchema,
30802
- async execute(input) {
30803
- const snap = registry2.snapshot(input.job_id);
30804
- if (snap) {
30805
- const result = await registry2.steer(input.job_id, input.message);
30806
- if (result.ok) {
30807
- return { status: "steered", job_id: input.job_id, message: input.message };
30808
- }
30809
- return { status: "error", error: result.error, job_id: input.job_id };
30810
- }
30811
- const jobsDir = join6(process.cwd(), ".specialists", "jobs");
30812
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
30813
- const status = supervisor.readStatus(input.job_id);
30814
- if (!status) {
30815
- return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
30816
- }
30817
- if (status.status === "done" || status.status === "error") {
30818
- return { status: "error", error: `Job is already ${status.status}`, job_id: input.job_id };
30819
- }
30820
- if (!status.fifo_path) {
30821
- return { status: "error", error: "Job has no steer pipe (may have been started without FIFO support)", job_id: input.job_id };
30822
- }
30823
- try {
30824
- const payload = JSON.stringify({ type: "steer", message: input.message }) + `
30825
- `;
30826
- writeFileSync2(status.fifo_path, payload, { flag: "a" });
30827
- return { status: "steered", job_id: input.job_id, message: input.message };
30828
- } catch (err) {
30829
- return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
30830
- }
30831
- }
30832
- };
30833
- }
30834
-
30835
- // src/tools/specialist/follow_up_specialist.tool.ts
30836
- init_zod();
30837
-
30838
- // src/tools/specialist/resume_specialist.tool.ts
30839
- init_zod();
30840
- init_supervisor();
30841
- import { writeFileSync as writeFileSync3 } from "node:fs";
30842
- import { join as join7 } from "node:path";
30843
- var resumeSpecialistSchema = exports_external.object({
30844
- job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
30845
- task: exports_external.string().describe("Next task/prompt to send to the specialist (conversation history is retained)")
30846
- });
30847
- function createResumeSpecialistTool(registry2) {
30848
- return {
30849
- name: "resume_specialist",
30850
- description: "Resume a waiting keep-alive specialist session with a next-turn prompt. " + "The Pi session retains full conversation history between turns. " + "Only valid for jobs in waiting state (started with keepAlive=true, either explicit --keep-alive or execution.interactive default). " + "Use steer_specialist for mid-run steering of running jobs.",
30851
- inputSchema: resumeSpecialistSchema,
30852
- async execute(input) {
30853
- const snap = registry2.snapshot(input.job_id);
30854
- if (snap) {
30855
- if (snap.status !== "waiting") {
30856
- return { status: "error", error: `Job is not waiting (status: ${snap.status}). resume is only valid in waiting state.`, job_id: input.job_id };
30857
- }
30858
- const result = await registry2.followUp(input.job_id, input.task);
30859
- if (result.ok) {
30860
- return { status: "resumed", job_id: input.job_id, output: result.output };
30861
- }
30862
- return { status: "error", error: result.error, job_id: input.job_id };
30863
- }
30864
- const jobsDir = join7(process.cwd(), ".specialists", "jobs");
30865
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
30866
- const status = supervisor.readStatus(input.job_id);
30867
- if (!status) {
30868
- return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
30869
- }
30870
- if (status.status !== "waiting") {
30871
- return { status: "error", error: `Job is not waiting (status: ${status.status}). resume is only valid in waiting state.`, job_id: input.job_id };
30872
- }
30873
- if (!status.fifo_path) {
30874
- return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
30875
- }
30876
- try {
30877
- const payload = JSON.stringify({ type: "resume", task: input.task }) + `
30878
- `;
30879
- writeFileSync3(status.fifo_path, payload, { flag: "a" });
30880
- return { status: "sent", job_id: input.job_id, task: input.task };
30881
- } catch (err) {
30882
- return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
30883
- }
30884
- }
30885
- };
30886
- }
30887
-
30888
- // src/tools/specialist/follow_up_specialist.tool.ts
30889
- var followUpSpecialistSchema = exports_external.object({
30890
- job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
30891
- message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
30892
- });
30893
- function createFollowUpSpecialistTool(registry2) {
30894
- const resumeTool = createResumeSpecialistTool(registry2);
30895
- return {
30896
- name: "follow_up_specialist",
30897
- description: "[DEPRECATED] Use resume_specialist instead. " + "Delegates to resume_specialist with a deprecation warning.",
30898
- inputSchema: followUpSpecialistSchema,
30899
- async execute(input) {
30900
- console.error("[specialists] DEPRECATED: follow_up_specialist is deprecated. Use resume_specialist instead.");
30901
- return resumeTool.execute({ job_id: input.job_id, task: input.message });
30902
- }
30903
- };
30904
- }
30905
-
30906
- // src/tools/specialist/feed_specialist.tool.ts
30907
- init_zod();
30908
- init_timeline_query();
30909
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
30910
- import { join as join9 } from "node:path";
30911
- var feedSpecialistSchema = objectType({
30912
- job_id: stringType().describe("Job ID returned by start_specialist or printed by specialists run"),
30913
- cursor: numberType().int().min(0).optional().default(0).describe("Event index offset from previous call. Pass next_cursor from the last response to receive only new events. Omit (or pass 0) for the first call."),
30914
- limit: numberType().int().min(1).max(100).optional().default(50).describe("Maximum number of events to return per call.")
30915
- });
30916
- function createFeedSpecialistTool(jobsDir) {
30917
- return {
30918
- name: "feed_specialist",
30919
- description: "Read cursor-paginated timeline events from a specialist job's events.jsonl. " + "Returns structured event objects (run_start, meta, tool, text, run_complete, etc.) " + "with job metadata (status, specialist, model, bead_id). " + "Poll incrementally: pass next_cursor from each response as cursor on the next call. " + "When is_complete=true and has_more=false, the job is fully observed. " + "Use for structured event inspection; use specialists result <job-id> for final text output.",
30920
- inputSchema: feedSpecialistSchema,
30921
- async execute(input) {
30922
- const { job_id, cursor = 0, limit = 50 } = input;
30923
- const statusPath = join9(jobsDir, job_id, "status.json");
30924
- if (!existsSync6(statusPath)) {
30925
- return { error: `Job not found: ${job_id}`, job_id };
30926
- }
30927
- let status = "unknown";
30928
- let specialist = "unknown";
30929
- let model;
30930
- let bead_id;
30931
- try {
30932
- const s = JSON.parse(readFileSync4(statusPath, "utf-8"));
30933
- status = s.status ?? "unknown";
30934
- specialist = s.specialist ?? "unknown";
30935
- model = s.model;
30936
- bead_id = s.bead_id;
30937
- } catch {}
30938
- const allEvents = readJobEventsById(jobsDir, job_id);
30939
- const total = allEvents.length;
30940
- const sliced = allEvents.slice(cursor, cursor + limit);
30941
- const next_cursor = cursor + sliced.length;
30942
- const has_more = next_cursor < total;
30943
- const is_complete = isJobComplete(allEvents);
30944
- return {
30945
- job_id,
30946
- specialist,
30947
- specialist_model: formatSpecialistModel(specialist, model),
30948
- ...model !== undefined ? { model } : {},
30949
- status,
30950
- ...bead_id !== undefined ? { bead_id } : {},
30951
- events: sliced,
30952
- cursor,
30953
- next_cursor,
30954
- has_more,
30955
- is_complete
30956
- };
30957
- }
30958
- };
30959
- }
30960
-
30961
30412
  // src/server.ts
30962
30413
  init_zod();
30963
30414
 
30964
- // src/tools/specialist/specialist_init.tool.ts
30965
- init_zod();
30966
- import { spawnSync as spawnSync4 } from "node:child_process";
30967
- import { existsSync as existsSync7 } from "node:fs";
30968
- import { join as join10 } from "node:path";
30969
- var specialistInitSchema = objectType({});
30970
- function createSpecialistInitTool(loader, deps) {
30971
- const resolved = deps ?? {
30972
- bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
30973
- beadsExists: () => existsSync7(join10(process.cwd(), ".beads")),
30974
- bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
30975
- };
30976
- return {
30977
- name: "specialist_init",
30978
- description: "Call this first at session start. Returns available specialists and initializes beads " + "tracking (runs `bd init` if not already set up). " + "Response includes: specialists[] (use with use_specialist/start_specialist), " + "beads.available (bool), beads.initialized (bool). " + "If beads.available is true, specialists with permission LOW/MEDIUM/HIGH will auto-create " + "a beads issue when they run — no action needed from you.",
30979
- inputSchema: specialistInitSchema,
30980
- async execute(_input) {
30981
- const available = resolved.bdAvailable();
30982
- let initialized = false;
30983
- if (available) {
30984
- if (resolved.beadsExists()) {
30985
- initialized = true;
30986
- } else {
30987
- const result = resolved.bdInit();
30988
- initialized = result.status === 0;
30989
- }
30990
- }
30991
- const specialists = await loader.list();
30992
- return {
30993
- specialists,
30994
- beads: { available, initialized }
30995
- };
30996
- }
30997
- };
30998
- }
30999
-
31000
- // src/server.ts
31001
30415
  class SpecialistsServer {
31002
30416
  server;
31003
30417
  tools;
@@ -31005,42 +30419,18 @@ class SpecialistsServer {
31005
30419
  const circuitBreaker = new CircuitBreaker;
31006
30420
  const loader = new SpecialistLoader;
31007
30421
  const hooks = new HookEmitter({
31008
- tracePath: join11(process.cwd(), ".specialists", "trace.jsonl")
30422
+ tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
31009
30423
  });
31010
30424
  const beadsClient = new BeadsClient;
31011
30425
  const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
31012
- const registry2 = new JobRegistry;
31013
- const jobsDir = join11(process.cwd(), ".specialists", "jobs");
31014
- this.tools = [
31015
- createListSpecialistsTool(loader),
31016
- createUseSpecialistTool(runner),
31017
- createRunParallelTool(runner),
31018
- createSpecialistStatusTool(loader, circuitBreaker),
31019
- createStartSpecialistTool(runner, beadsClient),
31020
- createStopSpecialistTool(),
31021
- createSteerSpecialistTool(registry2),
31022
- createResumeSpecialistTool(registry2),
31023
- createFollowUpSpecialistTool(registry2),
31024
- createSpecialistInitTool(loader),
31025
- createFeedSpecialistTool(jobsDir)
31026
- ];
30426
+ this.tools = [createUseSpecialistTool(runner)];
31027
30427
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
31028
30428
  this.setupHandlers();
31029
30429
  }
31030
30430
  toolSchemas = {};
31031
30431
  setupHandlers() {
31032
30432
  const schemaMap = {
31033
- list_specialists: listSpecialistsSchema,
31034
- use_specialist: useSpecialistSchema,
31035
- run_parallel: runParallelSchema,
31036
- specialist_status: exports_external.object({}),
31037
- start_specialist: startSpecialistSchema,
31038
- stop_specialist: stopSpecialistSchema,
31039
- steer_specialist: steerSpecialistSchema,
31040
- resume_specialist: resumeSpecialistSchema,
31041
- follow_up_specialist: followUpSpecialistSchema,
31042
- specialist_init: specialistInitSchema,
31043
- feed_specialist: feedSpecialistSchema
30433
+ use_specialist: useSpecialistSchema
31044
30434
  };
31045
30435
  this.toolSchemas = schemaMap;
31046
30436
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {