@jaggerxtrm/specialists 3.4.2 → 3.4.4

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,1281 +18774,530 @@ 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");
19300
+ addHook("PostToolUse", "node .claude/hooks/specialists-complete.mjs");
20052
19301
  addHook("SessionStart", "node .claude/hooks/specialists-session-start.mjs");
20053
19302
  if (changed) {
20054
19303
  saveJson(settingsPath, settings);
@@ -20063,25 +19312,25 @@ function installProjectSkills(cwd) {
20063
19312
  skip("no canonical skills found in package");
20064
19313
  return;
20065
19314
  }
20066
- const skills = readdirSync4(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19315
+ const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
20067
19316
  if (skills.length === 0) {
20068
19317
  skip("no skill directories found in package");
20069
19318
  return;
20070
19319
  }
20071
19320
  const targetDirs = [
20072
- join14(cwd, ".claude", "skills"),
20073
- join14(cwd, ".pi", "skills")
19321
+ join6(cwd, ".claude", "skills"),
19322
+ join6(cwd, ".pi", "skills")
20074
19323
  ];
20075
19324
  let totalCopied = 0;
20076
19325
  let totalSkipped = 0;
20077
19326
  for (const targetDir of targetDirs) {
20078
- if (!existsSync10(targetDir)) {
20079
- mkdirSync2(targetDir, { recursive: true });
19327
+ if (!existsSync6(targetDir)) {
19328
+ mkdirSync(targetDir, { recursive: true });
20080
19329
  }
20081
19330
  for (const skill of skills) {
20082
- const src = join14(sourceDir, skill);
20083
- const dest = join14(targetDir, skill);
20084
- if (existsSync10(dest)) {
19331
+ const src = join6(sourceDir, skill);
19332
+ const dest = join6(targetDir, skill);
19333
+ if (existsSync6(dest)) {
20085
19334
  totalSkipped++;
20086
19335
  } else {
20087
19336
  cpSync(src, dest, { recursive: true });
@@ -20097,21 +19346,21 @@ function installProjectSkills(cwd) {
20097
19346
  }
20098
19347
  }
20099
19348
  function createUserDirs(cwd) {
20100
- const userDir = join14(cwd, ".specialists", "user");
20101
- if (!existsSync10(userDir)) {
20102
- mkdirSync2(userDir, { recursive: true });
19349
+ const userDir = join6(cwd, ".specialists", "user");
19350
+ if (!existsSync6(userDir)) {
19351
+ mkdirSync(userDir, { recursive: true });
20103
19352
  ok("created .specialists/user/ for custom specialists");
20104
19353
  }
20105
19354
  }
20106
19355
  function createRuntimeDirs(cwd) {
20107
19356
  const runtimeDirs = [
20108
- join14(cwd, ".specialists", "jobs"),
20109
- join14(cwd, ".specialists", "ready")
19357
+ join6(cwd, ".specialists", "jobs"),
19358
+ join6(cwd, ".specialists", "ready")
20110
19359
  ];
20111
19360
  let created = 0;
20112
19361
  for (const dir of runtimeDirs) {
20113
- if (!existsSync10(dir)) {
20114
- mkdirSync2(dir, { recursive: true });
19362
+ if (!existsSync6(dir)) {
19363
+ mkdirSync(dir, { recursive: true });
20115
19364
  created++;
20116
19365
  }
20117
19366
  }
@@ -20120,7 +19369,7 @@ function createRuntimeDirs(cwd) {
20120
19369
  }
20121
19370
  }
20122
19371
  function ensureProjectMcp(cwd) {
20123
- const mcpPath = join14(cwd, MCP_FILE);
19372
+ const mcpPath = join6(cwd, MCP_FILE);
20124
19373
  const mcp = loadJson(mcpPath, { mcpServers: {} });
20125
19374
  mcp.mcpServers ??= {};
20126
19375
  const existing = mcp.mcpServers[MCP_SERVER_NAME];
@@ -20133,8 +19382,8 @@ function ensureProjectMcp(cwd) {
20133
19382
  ok("registered specialists in project .mcp.json");
20134
19383
  }
20135
19384
  function ensureGitignore(cwd) {
20136
- const gitignorePath = join14(cwd, ".gitignore");
20137
- const existing = existsSync10(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
19385
+ const gitignorePath = join6(cwd, ".gitignore");
19386
+ const existing = existsSync6(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
20138
19387
  let added = 0;
20139
19388
  const lines = existing.split(`
20140
19389
  `);
@@ -20145,7 +19394,7 @@ function ensureGitignore(cwd) {
20145
19394
  }
20146
19395
  }
20147
19396
  if (added > 0) {
20148
- writeFileSync4(gitignorePath, lines.join(`
19397
+ writeFileSync(gitignorePath, lines.join(`
20149
19398
  `) + `
20150
19399
  `, "utf-8");
20151
19400
  ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
@@ -20154,30 +19403,89 @@ function ensureGitignore(cwd) {
20154
19403
  }
20155
19404
  }
20156
19405
  function ensureAgentsMd(cwd) {
20157
- const agentsPath = join14(cwd, "AGENTS.md");
20158
- if (existsSync10(agentsPath)) {
20159
- const existing = readFileSync6(agentsPath, "utf-8");
19406
+ const agentsPath = join6(cwd, "AGENTS.md");
19407
+ if (existsSync6(agentsPath)) {
19408
+ const existing = readFileSync3(agentsPath, "utf-8");
20160
19409
  if (existing.includes(AGENTS_MARKER)) {
20161
19410
  skip("AGENTS.md already has Specialists section");
20162
19411
  } else {
20163
- writeFileSync4(agentsPath, existing.trimEnd() + `
19412
+ writeFileSync(agentsPath, existing.trimEnd() + `
20164
19413
 
20165
19414
  ` + AGENTS_BLOCK, "utf-8");
20166
19415
  ok("appended Specialists section to AGENTS.md");
20167
19416
  }
20168
19417
  } else {
20169
- writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
19418
+ writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
20170
19419
  ok("created AGENTS.md with Specialists section");
20171
19420
  }
20172
19421
  }
19422
+ function hasPiSessionEnv() {
19423
+ return Boolean(process.env.PI_SESSION_ID || process.env.PI_RPC_SOCKET || process.env.PI_AGENT_SESSION || process.env.PI_CODING_AGENT);
19424
+ }
19425
+ function readLinuxProcFile(path) {
19426
+ try {
19427
+ return readFileSync3(path, "utf-8");
19428
+ } catch {
19429
+ return null;
19430
+ }
19431
+ }
19432
+ function getLinuxParentPid(pid) {
19433
+ const status = readLinuxProcFile(`/proc/${pid}/status`);
19434
+ if (!status)
19435
+ return null;
19436
+ const ppidLine = status.split(`
19437
+ `).find((line) => line.startsWith("PPid:"));
19438
+ if (!ppidLine)
19439
+ return null;
19440
+ const value = Number(ppidLine.replace("PPid:", "").trim());
19441
+ return Number.isFinite(value) && value > 0 ? value : null;
19442
+ }
19443
+ function hasPiAncestorProcess(maxDepth = 8) {
19444
+ let pid = process.ppid;
19445
+ let depth = 0;
19446
+ while (pid && depth < maxDepth) {
19447
+ const cmdline = readLinuxProcFile(`/proc/${pid}/cmdline`);
19448
+ if (!cmdline)
19449
+ break;
19450
+ const command = cmdline.replace(/\0/g, " ").trim();
19451
+ const executable = basename2(command.split(" ")[0] ?? "");
19452
+ const isPiExecutable = executable === "pi" || executable === "pi-coding-agent" || executable.startsWith("pi-");
19453
+ if (isPiExecutable || command.includes("@mariozechner/pi-coding-agent")) {
19454
+ return true;
19455
+ }
19456
+ pid = getLinuxParentPid(pid);
19457
+ depth++;
19458
+ }
19459
+ return false;
19460
+ }
19461
+ function hasExistingDefaultSpecialists(cwd) {
19462
+ const defaultDir = join6(cwd, ".specialists", "default");
19463
+ const legacyNestedDir = join6(defaultDir, "specialists");
19464
+ const hasFlat = existsSync6(defaultDir) && readdirSync2(defaultDir).some((file) => file.endsWith(".specialist.yaml"));
19465
+ if (hasFlat)
19466
+ return true;
19467
+ return existsSync6(legacyNestedDir) && readdirSync2(legacyNestedDir).some((file) => file.endsWith(".specialist.yaml"));
19468
+ }
19469
+ function shouldSkipDefaultSyncInPiSession(cwd) {
19470
+ if (process.env.SPECIALISTS_INIT_FORCE_DEFAULT_SYNC === "1")
19471
+ return false;
19472
+ if (!hasExistingDefaultSpecialists(cwd))
19473
+ return false;
19474
+ return hasPiSessionEnv() || hasPiAncestorProcess();
19475
+ }
20173
19476
  async function run5() {
20174
19477
  const cwd = process.cwd();
20175
19478
  console.log(`
20176
19479
  ${bold4("specialists init")}
20177
19480
  `);
20178
- migrateLegacySpecialists(cwd, "default");
19481
+ const skipDefaultSync = shouldSkipDefaultSyncInPiSession(cwd);
19482
+ if (skipDefaultSync) {
19483
+ skip("pi session detected with existing default specialists; skipped .specialists/default sync");
19484
+ } else {
19485
+ migrateLegacySpecialists(cwd, "default");
19486
+ copyCanonicalSpecialists(cwd);
19487
+ }
20179
19488
  migrateLegacySpecialists(cwd, "user");
20180
- copyCanonicalSpecialists(cwd);
20181
19489
  createUserDirs(cwd);
20182
19490
  createRuntimeDirs(cwd);
20183
19491
  ensureGitignore(cwd);
@@ -20249,8 +19557,8 @@ __export(exports_validate, {
20249
19557
  ArgParseError: () => ArgParseError2
20250
19558
  });
20251
19559
  import { readFile as readFile2 } from "node:fs/promises";
20252
- import { existsSync as existsSync11 } from "node:fs";
20253
- import { join as join15 } from "node:path";
19560
+ import { existsSync as existsSync7 } from "node:fs";
19561
+ import { join as join7 } from "node:path";
20254
19562
  function parseArgs3(argv) {
20255
19563
  const name = argv[0];
20256
19564
  if (!name || name.startsWith("--")) {
@@ -20261,15 +19569,15 @@ function parseArgs3(argv) {
20261
19569
  }
20262
19570
  function findSpecialistFile(name) {
20263
19571
  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")
19572
+ join7(process.cwd(), ".specialists", "user"),
19573
+ join7(process.cwd(), ".specialists", "user", "specialists"),
19574
+ join7(process.cwd(), ".specialists", "default"),
19575
+ join7(process.cwd(), ".specialists", "default", "specialists"),
19576
+ join7(process.cwd(), "specialists")
20269
19577
  ];
20270
19578
  for (const dir of scanDirs) {
20271
- const candidate = join15(dir, `${name}.specialist.yaml`);
20272
- if (existsSync11(candidate)) {
19579
+ const candidate = join7(dir, `${name}.specialist.yaml`);
19580
+ if (existsSync7(candidate)) {
20273
19581
  return candidate;
20274
19582
  }
20275
19583
  }
@@ -20361,9 +19669,9 @@ var exports_edit = {};
20361
19669
  __export(exports_edit, {
20362
19670
  run: () => run7
20363
19671
  });
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";
19672
+ import { spawnSync as spawnSync5 } from "node:child_process";
19673
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
19674
+ import { join as join8 } from "node:path";
20367
19675
  function parseArgs4(argv) {
20368
19676
  const name = argv[0];
20369
19677
  if (!name || name.startsWith("--")) {
@@ -20427,18 +19735,18 @@ function setIn(doc2, path, value) {
20427
19735
  }
20428
19736
  }
20429
19737
  function openAllConfigSpecialistsInEditor() {
20430
- const configDir = join16(process.cwd(), "config", "specialists");
20431
- if (!existsSync12(configDir)) {
19738
+ const configDir = join8(process.cwd(), "config", "specialists");
19739
+ if (!existsSync8(configDir)) {
20432
19740
  console.error(`Error: missing directory: ${configDir}`);
20433
19741
  process.exit(1);
20434
19742
  }
20435
- const files = readdirSync5(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join16(configDir, file));
19743
+ const files = readdirSync3(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join8(configDir, file));
20436
19744
  if (files.length === 0) {
20437
19745
  console.error("Error: no specialist YAML files found in config/specialists/");
20438
19746
  process.exit(1);
20439
19747
  }
20440
19748
  const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
20441
- const result = spawnSync7(editor, files, { stdio: "inherit", shell: true });
19749
+ const result = spawnSync5(editor, files, { stdio: "inherit", shell: true });
20442
19750
  if (result.status !== 0) {
20443
19751
  process.exit(result.status ?? 1);
20444
19752
  }
@@ -20460,7 +19768,7 @@ async function run7() {
20460
19768
  console.error(` Run ${yellow6("specialists list")} to see available specialists`);
20461
19769
  process.exit(1);
20462
19770
  }
20463
- const raw = readFileSync7(match.filePath, "utf-8");
19771
+ const raw = readFileSync4(match.filePath, "utf-8");
20464
19772
  const doc2 = $parseDocument(raw);
20465
19773
  const yamlPath = FIELD_MAP[field];
20466
19774
  let typedValue = value;
@@ -20491,7 +19799,7 @@ ${bold6(`[dry-run] ${match.filePath}`)}
20491
19799
  console.log();
20492
19800
  return;
20493
19801
  }
20494
- writeFileSync5(match.filePath, updated, "utf-8");
19802
+ writeFileSync2(match.filePath, updated, "utf-8");
20495
19803
  const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
20496
19804
  console.log(`${green5("✓")} ${bold6(name)}: ${yellow6(field)} = ${displayValue}` + dim6(` (${match.filePath})`));
20497
19805
  }
@@ -20515,9 +19823,9 @@ var exports_config = {};
20515
19823
  __export(exports_config, {
20516
19824
  run: () => run8
20517
19825
  });
20518
- import { existsSync as existsSync13 } from "node:fs";
19826
+ import { existsSync as existsSync9 } from "node:fs";
20519
19827
  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";
19828
+ import { basename as basename3, join as join9 } from "node:path";
20521
19829
  function usage() {
20522
19830
  return [
20523
19831
  "Usage:",
@@ -20566,121 +19874,751 @@ ${usage()}`);
20566
19874
  if (!next || next.startsWith("--")) {
20567
19875
  throw new ArgParseError3("--name requires a specialist name");
20568
19876
  }
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);
19877
+ name = next;
19878
+ continue;
19879
+ }
19880
+ throw new ArgParseError3(`Unknown option: ${token}`);
19881
+ }
19882
+ if (name && all) {
19883
+ throw new ArgParseError3("Use either --name or --all, not both");
19884
+ }
19885
+ if (!name) {
19886
+ all = true;
19887
+ }
19888
+ return { command, key, value, name, all };
19889
+ }
19890
+ function splitKeyPath(key) {
19891
+ const path = key.split(".").map((part) => part.trim()).filter(Boolean);
19892
+ if (path.length === 0) {
19893
+ throw new ArgParseError3(`Invalid key: ${key}`);
19894
+ }
19895
+ return path;
19896
+ }
19897
+ function getSpecialistDir(projectDir) {
19898
+ return join9(projectDir, "config", "specialists");
19899
+ }
19900
+ function getSpecialistNameFromPath(path) {
19901
+ return path.replace(/\.specialist\.yaml$/, "");
19902
+ }
19903
+ async function listSpecialistFiles(projectDir) {
19904
+ const specialistDir = getSpecialistDir(projectDir);
19905
+ if (!existsSync9(specialistDir)) {
19906
+ throw new Error(`Missing directory: ${specialistDir}`);
19907
+ }
19908
+ const entries = await readdir2(specialistDir);
19909
+ return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join9(specialistDir, entry));
19910
+ }
19911
+ async function findNamedSpecialistFile(projectDir, name) {
19912
+ const path = join9(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
19913
+ if (!existsSync9(path)) {
19914
+ throw new Error(`Specialist not found in config/specialists/: ${name}`);
19915
+ }
19916
+ return path;
19917
+ }
19918
+ function parseValue(rawValue) {
19919
+ try {
19920
+ return $parse(rawValue);
19921
+ } catch {
19922
+ return rawValue;
19923
+ }
19924
+ }
19925
+ function formatValue(value) {
19926
+ if (value === undefined)
19927
+ return "<unset>";
19928
+ if (typeof value === "string")
19929
+ return value;
19930
+ return JSON.stringify(value);
19931
+ }
19932
+ async function getAcrossFiles(files, keyPath) {
19933
+ for (const file of files) {
19934
+ const content = await readFile3(file, "utf-8");
19935
+ const doc2 = $parseDocument(content);
19936
+ const value = doc2.getIn(keyPath);
19937
+ const name = getSpecialistNameFromPath(basename3(file));
19938
+ console.log(`${yellow7(name)}: ${formatValue(value)}`);
19939
+ }
19940
+ }
19941
+ async function setAcrossFiles(files, keyPath, rawValue) {
19942
+ const typedValue = parseValue(rawValue);
19943
+ for (const file of files) {
19944
+ const content = await readFile3(file, "utf-8");
19945
+ const doc2 = $parseDocument(content);
19946
+ doc2.setIn(keyPath, typedValue);
19947
+ await writeFile2(file, doc2.toString(), "utf-8");
19948
+ }
19949
+ console.log(`${green6("✓")} updated ${files.length} specialist${files.length === 1 ? "" : "s"}: ` + `${keyPath.join(".")} = ${formatValue(typedValue)}`);
19950
+ }
19951
+ async function run8() {
19952
+ let args;
19953
+ try {
19954
+ args = parseArgs5(process.argv.slice(3));
19955
+ } catch (error2) {
19956
+ if (error2 instanceof ArgParseError3) {
19957
+ console.error(error2.message);
19958
+ process.exit(1);
19959
+ }
19960
+ throw error2;
19961
+ }
19962
+ const keyPath = splitKeyPath(args.key);
19963
+ const projectDir = process.cwd();
19964
+ let files;
19965
+ try {
19966
+ files = args.name ? [await findNamedSpecialistFile(projectDir, args.name)] : await listSpecialistFiles(projectDir);
19967
+ } catch (error2) {
19968
+ const message = error2 instanceof Error ? error2.message : String(error2);
19969
+ console.error(message);
19970
+ process.exit(1);
19971
+ return;
19972
+ }
19973
+ if (files.length === 0) {
19974
+ console.error("No specialists found in config/specialists/");
19975
+ process.exit(1);
19976
+ return;
19977
+ }
19978
+ if (args.command === "get") {
19979
+ await getAcrossFiles(files, keyPath);
19980
+ return;
19981
+ }
19982
+ await setAcrossFiles(files, keyPath, args.value);
19983
+ }
19984
+ var green6 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError3;
19985
+ var init_config = __esm(() => {
19986
+ init_dist();
19987
+ ArgParseError3 = class ArgParseError3 extends Error {
19988
+ constructor(message) {
19989
+ super(message);
19990
+ this.name = "ArgParseError";
19991
+ }
19992
+ };
19993
+ });
19994
+
19995
+ // src/specialist/timeline-events.ts
19996
+ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
19997
+ const t = Date.now();
19998
+ switch (callbackEvent) {
19999
+ case "thinking":
20000
+ return { t, type: TIMELINE_EVENT_TYPES.THINKING };
20001
+ case "tool_execution_start":
20002
+ return {
20003
+ t,
20004
+ type: TIMELINE_EVENT_TYPES.TOOL,
20005
+ tool: context.tool ?? "unknown",
20006
+ phase: "start",
20007
+ tool_call_id: context.toolCallId,
20008
+ args: context.args,
20009
+ started_at: new Date(t).toISOString()
20010
+ };
20011
+ case "tool_execution_update":
20012
+ case "tool_execution":
20013
+ return {
20014
+ t,
20015
+ type: TIMELINE_EVENT_TYPES.TOOL,
20016
+ tool: context.tool ?? "unknown",
20017
+ phase: "update",
20018
+ tool_call_id: context.toolCallId
20019
+ };
20020
+ case "tool_execution_end":
20021
+ return {
20022
+ t,
20023
+ type: TIMELINE_EVENT_TYPES.TOOL,
20024
+ tool: context.tool ?? "unknown",
20025
+ phase: "end",
20026
+ tool_call_id: context.toolCallId,
20027
+ is_error: context.isError
20028
+ };
20029
+ case "message_start_assistant":
20030
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "assistant" };
20031
+ case "message_end_assistant":
20032
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "assistant" };
20033
+ case "message_start_tool_result":
20034
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "start", role: "toolResult" };
20035
+ case "message_end_tool_result":
20036
+ return { t, type: TIMELINE_EVENT_TYPES.MESSAGE, phase: "end", role: "toolResult" };
20037
+ case "turn_start":
20038
+ return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "start" };
20039
+ case "turn_end":
20040
+ return { t, type: TIMELINE_EVENT_TYPES.TURN, phase: "end" };
20041
+ case "text":
20042
+ return { t, type: TIMELINE_EVENT_TYPES.TEXT };
20043
+ case "agent_end":
20044
+ case "message_done":
20045
+ case "done":
20046
+ return null;
20047
+ default:
20048
+ return null;
20049
+ }
20050
+ }
20051
+ function createRunStartEvent(specialist, beadId) {
20052
+ return {
20053
+ t: Date.now(),
20054
+ type: TIMELINE_EVENT_TYPES.RUN_START,
20055
+ specialist,
20056
+ bead_id: beadId
20057
+ };
20058
+ }
20059
+ function createMetaEvent(model, backend) {
20060
+ return {
20061
+ t: Date.now(),
20062
+ type: TIMELINE_EVENT_TYPES.META,
20063
+ model,
20064
+ backend
20065
+ };
20066
+ }
20067
+ function createStaleWarningEvent(reason, options) {
20068
+ return {
20069
+ t: Date.now(),
20070
+ type: TIMELINE_EVENT_TYPES.STALE_WARNING,
20071
+ reason,
20072
+ silence_ms: options.silence_ms,
20073
+ threshold_ms: options.threshold_ms,
20074
+ ...options.tool !== undefined ? { tool: options.tool } : {}
20075
+ };
20076
+ }
20077
+ function createRunCompleteEvent(status, elapsed_s, options) {
20078
+ return {
20079
+ t: Date.now(),
20080
+ type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
20081
+ status,
20082
+ elapsed_s,
20083
+ ...options
20084
+ };
20085
+ }
20086
+ function parseTimelineEvent(line) {
20087
+ try {
20088
+ const parsed = JSON.parse(line);
20089
+ if (!parsed || typeof parsed !== "object")
20090
+ return null;
20091
+ if (typeof parsed.t !== "number")
20092
+ return null;
20093
+ if (typeof parsed.type !== "string")
20094
+ return null;
20095
+ if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
20096
+ return {
20097
+ t: parsed.t,
20098
+ type: TIMELINE_EVENT_TYPES.DONE,
20099
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
20100
+ };
20101
+ }
20102
+ if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
20103
+ return {
20104
+ t: parsed.t,
20105
+ type: TIMELINE_EVENT_TYPES.AGENT_END,
20106
+ elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
20107
+ };
20108
+ }
20109
+ const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
20110
+ if (!knownTypes.includes(parsed.type))
20111
+ return null;
20112
+ return parsed;
20113
+ } catch {
20114
+ return null;
20115
+ }
20116
+ }
20117
+ function isRunCompleteEvent(event) {
20118
+ return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
20119
+ }
20120
+ function compareTimelineEvents(a, b) {
20121
+ return a.t - b.t;
20122
+ }
20123
+ var TIMELINE_EVENT_TYPES;
20124
+ var init_timeline_events = __esm(() => {
20125
+ TIMELINE_EVENT_TYPES = {
20126
+ RUN_START: "run_start",
20127
+ META: "meta",
20128
+ THINKING: "thinking",
20129
+ TOOL: "tool",
20130
+ TEXT: "text",
20131
+ MESSAGE: "message",
20132
+ TURN: "turn",
20133
+ RUN_COMPLETE: "run_complete",
20134
+ STALE_WARNING: "stale_warning",
20135
+ DONE: "done",
20136
+ AGENT_END: "agent_end"
20137
+ };
20138
+ });
20139
+
20140
+ // src/specialist/supervisor.ts
20141
+ import {
20142
+ appendFileSync,
20143
+ closeSync,
20144
+ existsSync as existsSync10,
20145
+ fsyncSync,
20146
+ mkdirSync as mkdirSync2,
20147
+ openSync,
20148
+ readdirSync as readdirSync4,
20149
+ readFileSync as readFileSync5,
20150
+ renameSync as renameSync2,
20151
+ rmSync,
20152
+ statSync,
20153
+ writeFileSync as writeFileSync3,
20154
+ writeSync
20155
+ } from "node:fs";
20156
+ import { join as join10 } from "node:path";
20157
+ import { createInterface } from "node:readline";
20158
+ import { createReadStream } from "node:fs";
20159
+ import { spawnSync as spawnSync6, execFileSync } from "node:child_process";
20160
+ function getCurrentGitSha() {
20161
+ const result = spawnSync6("git", ["rev-parse", "HEAD"], {
20162
+ encoding: "utf-8",
20163
+ stdio: ["ignore", "pipe", "ignore"]
20164
+ });
20165
+ if (result.status !== 0)
20166
+ return;
20167
+ const sha = result.stdout?.trim();
20168
+ return sha || undefined;
20169
+ }
20170
+ function formatBeadNotes(result) {
20171
+ const metadata = [
20172
+ `prompt_hash=${result.promptHash}`,
20173
+ `git_sha=${getCurrentGitSha() ?? "unknown"}`,
20174
+ `elapsed_ms=${Math.round(result.durationMs)}`,
20175
+ `model=${result.model}`,
20176
+ `backend=${result.backend}`
20177
+ ].join(`
20178
+ `);
20179
+ return `${result.output}
20180
+
20181
+ ---
20182
+ ${metadata}`;
20183
+ }
20184
+
20185
+ class Supervisor {
20186
+ opts;
20187
+ constructor(opts) {
20188
+ this.opts = opts;
20189
+ }
20190
+ jobDir(id) {
20191
+ return join10(this.opts.jobsDir, id);
20192
+ }
20193
+ statusPath(id) {
20194
+ return join10(this.jobDir(id), "status.json");
20195
+ }
20196
+ resultPath(id) {
20197
+ return join10(this.jobDir(id), "result.txt");
20198
+ }
20199
+ eventsPath(id) {
20200
+ return join10(this.jobDir(id), "events.jsonl");
20201
+ }
20202
+ readyDir() {
20203
+ return join10(this.opts.jobsDir, "..", "ready");
20204
+ }
20205
+ writeReadyMarker(id) {
20206
+ mkdirSync2(this.readyDir(), { recursive: true });
20207
+ writeFileSync3(join10(this.readyDir(), id), "", "utf-8");
20208
+ }
20209
+ readStatus(id) {
20210
+ const path = this.statusPath(id);
20211
+ if (!existsSync10(path))
20212
+ return null;
20213
+ try {
20214
+ return JSON.parse(readFileSync5(path, "utf-8"));
20215
+ } catch {
20216
+ return null;
20217
+ }
20218
+ }
20219
+ listJobs() {
20220
+ if (!existsSync10(this.opts.jobsDir))
20221
+ return [];
20222
+ const jobs = [];
20223
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20224
+ const path = join10(this.opts.jobsDir, entry, "status.json");
20225
+ if (!existsSync10(path))
20226
+ continue;
20227
+ try {
20228
+ jobs.push(JSON.parse(readFileSync5(path, "utf-8")));
20229
+ } catch {}
20230
+ }
20231
+ return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
20232
+ }
20233
+ writeStatusFile(id, data) {
20234
+ mkdirSync2(this.jobDir(id), { recursive: true });
20235
+ const path = this.statusPath(id);
20236
+ const tmp = path + ".tmp";
20237
+ writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
20238
+ renameSync2(tmp, path);
20239
+ }
20240
+ updateStatus(id, updates) {
20241
+ const current = this.readStatus(id);
20242
+ if (!current)
20243
+ return;
20244
+ this.writeStatusFile(id, { ...current, ...updates });
20245
+ }
20246
+ gc() {
20247
+ if (!existsSync10(this.opts.jobsDir))
20248
+ return;
20249
+ const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
20250
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20251
+ const dir = join10(this.opts.jobsDir, entry);
20252
+ try {
20253
+ const stat2 = statSync(dir);
20254
+ if (!stat2.isDirectory())
20255
+ continue;
20256
+ if (stat2.mtimeMs < cutoff)
20257
+ rmSync(dir, { recursive: true, force: true });
20258
+ } catch {}
20259
+ }
20260
+ }
20261
+ crashRecovery() {
20262
+ if (!existsSync10(this.opts.jobsDir))
20263
+ return;
20264
+ const thresholds = {
20265
+ ...STALL_DETECTION_DEFAULTS,
20266
+ ...this.opts.stallDetection
20267
+ };
20268
+ const now = Date.now();
20269
+ for (const entry of readdirSync4(this.opts.jobsDir)) {
20270
+ const statusPath = join10(this.opts.jobsDir, entry, "status.json");
20271
+ if (!existsSync10(statusPath))
20272
+ continue;
20273
+ try {
20274
+ const s = JSON.parse(readFileSync5(statusPath, "utf-8"));
20275
+ if (s.status === "running" || s.status === "starting") {
20276
+ if (!s.pid)
20277
+ continue;
20278
+ let pidAlive = true;
20279
+ try {
20280
+ process.kill(s.pid, 0);
20281
+ } catch {
20282
+ pidAlive = false;
20283
+ }
20284
+ if (!pidAlive) {
20285
+ const tmp = statusPath + ".tmp";
20286
+ const updated = { ...s, status: "error", error: "Process crashed or was killed" };
20287
+ writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
20288
+ renameSync2(tmp, statusPath);
20289
+ } else if (s.status === "running") {
20290
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
20291
+ const silenceMs = now - lastEventAt;
20292
+ if (silenceMs > thresholds.running_silence_error_ms) {
20293
+ const tmp = statusPath + ".tmp";
20294
+ const updated = {
20295
+ ...s,
20296
+ status: "error",
20297
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
20298
+ };
20299
+ writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
20300
+ renameSync2(tmp, statusPath);
20301
+ }
20302
+ }
20303
+ } else if (s.status === "waiting") {
20304
+ const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
20305
+ const silenceMs = now - lastEventAt;
20306
+ if (silenceMs > thresholds.waiting_stale_ms) {
20307
+ const eventsPath = join10(this.opts.jobsDir, entry, "events.jsonl");
20308
+ const event = createStaleWarningEvent("waiting_stale", {
20309
+ silence_ms: silenceMs,
20310
+ threshold_ms: thresholds.waiting_stale_ms
20311
+ });
20312
+ try {
20313
+ appendFileSync(eventsPath, JSON.stringify(event) + `
20314
+ `);
20315
+ } catch {}
20316
+ }
20317
+ }
20318
+ } catch {}
20319
+ }
20320
+ }
20321
+ async run() {
20322
+ const { runner, runOptions, jobsDir } = this.opts;
20323
+ this.gc();
20324
+ this.crashRecovery();
20325
+ const id = crypto.randomUUID().slice(0, 6);
20326
+ const dir = this.jobDir(id);
20327
+ const startedAtMs = Date.now();
20328
+ mkdirSync2(dir, { recursive: true });
20329
+ mkdirSync2(this.readyDir(), { recursive: true });
20330
+ const initialStatus = {
20331
+ id,
20332
+ specialist: runOptions.name,
20333
+ status: "starting",
20334
+ started_at_ms: startedAtMs,
20335
+ pid: process.pid,
20336
+ ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
20337
+ ...process.env.SPECIALISTS_TMUX_SESSION ? { tmux_session: process.env.SPECIALISTS_TMUX_SESSION } : {}
20338
+ };
20339
+ this.writeStatusFile(id, initialStatus);
20340
+ writeFileSync3(join10(this.opts.jobsDir, "latest"), `${id}
20341
+ `, "utf-8");
20342
+ this.opts.onJobStarted?.({ id });
20343
+ let statusSnapshot = initialStatus;
20344
+ const setStatus = (updates) => {
20345
+ statusSnapshot = { ...statusSnapshot, ...updates };
20346
+ this.writeStatusFile(id, statusSnapshot);
20347
+ };
20348
+ const eventsFd = openSync(this.eventsPath(id), "a");
20349
+ const appendTimelineEvent = (event) => {
20350
+ try {
20351
+ writeSync(eventsFd, JSON.stringify(event) + `
20352
+ `);
20353
+ } catch (err) {
20354
+ console.error(`[supervisor] Failed to write event: ${err?.message ?? err}`);
20355
+ }
20356
+ };
20357
+ appendTimelineEvent(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
20358
+ const fifoPath = join10(dir, "steer.pipe");
20359
+ try {
20360
+ execFileSync("mkfifo", [fifoPath]);
20361
+ setStatus({ fifo_path: fifoPath });
20362
+ } catch {}
20363
+ let textLogged = false;
20364
+ let currentTool = "";
20365
+ let currentToolCallId = "";
20366
+ let currentToolArgs;
20367
+ let currentToolIsError = false;
20368
+ const activeToolCalls = new Map;
20369
+ let killFn;
20370
+ let steerFn;
20371
+ let resumeFn;
20372
+ let closeFn;
20373
+ let fifoReadStream;
20374
+ let fifoReadline;
20375
+ const thresholds = {
20376
+ ...STALL_DETECTION_DEFAULTS,
20377
+ ...this.opts.stallDetection
20378
+ };
20379
+ let lastActivityMs = startedAtMs;
20380
+ let silenceWarnEmitted = false;
20381
+ let toolStartMs;
20382
+ let toolDurationWarnEmitted = false;
20383
+ let stuckIntervalId;
20384
+ stuckIntervalId = setInterval(() => {
20385
+ const now = Date.now();
20386
+ if (statusSnapshot.status === "running") {
20387
+ const silenceMs = now - lastActivityMs;
20388
+ if (!silenceWarnEmitted && silenceMs > thresholds.running_silence_warn_ms) {
20389
+ silenceWarnEmitted = true;
20390
+ appendTimelineEvent(createStaleWarningEvent("running_silence", {
20391
+ silence_ms: silenceMs,
20392
+ threshold_ms: thresholds.running_silence_warn_ms
20393
+ }));
20394
+ }
20395
+ if (silenceMs > thresholds.running_silence_error_ms) {
20396
+ appendTimelineEvent(createStaleWarningEvent("running_silence_error", {
20397
+ silence_ms: silenceMs,
20398
+ threshold_ms: thresholds.running_silence_error_ms
20399
+ }));
20400
+ setStatus({
20401
+ status: "error",
20402
+ error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
20403
+ });
20404
+ killFn?.();
20405
+ clearInterval(stuckIntervalId);
20406
+ }
20407
+ }
20408
+ if (toolStartMs !== undefined && !toolDurationWarnEmitted) {
20409
+ const toolDurationMs = now - toolStartMs;
20410
+ if (toolDurationMs > thresholds.tool_duration_warn_ms) {
20411
+ toolDurationWarnEmitted = true;
20412
+ appendTimelineEvent(createStaleWarningEvent("tool_duration", {
20413
+ silence_ms: toolDurationMs,
20414
+ threshold_ms: thresholds.tool_duration_warn_ms,
20415
+ tool: currentTool
20416
+ }));
20417
+ }
20418
+ }
20419
+ }, 1e4);
20420
+ const sigtermHandler = () => killFn?.();
20421
+ process.once("SIGTERM", sigtermHandler);
20422
+ try {
20423
+ const result = await runner.run(runOptions, (delta) => {
20424
+ const toolMatch = delta.match(/⚙ (.+?)…/);
20425
+ if (toolMatch) {
20426
+ currentTool = toolMatch[1];
20427
+ setStatus({ current_tool: currentTool });
20428
+ }
20429
+ this.opts.onProgress?.(delta);
20430
+ }, (eventType) => {
20431
+ const now = Date.now();
20432
+ lastActivityMs = now;
20433
+ silenceWarnEmitted = false;
20434
+ setStatus({
20435
+ status: "running",
20436
+ current_event: eventType,
20437
+ last_event_at_ms: now,
20438
+ elapsed_s: Math.round((now - startedAtMs) / 1000)
20439
+ });
20440
+ const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
20441
+ tool: currentTool,
20442
+ toolCallId: currentToolCallId || undefined,
20443
+ args: currentToolArgs,
20444
+ isError: currentToolIsError
20445
+ });
20446
+ if (timelineEvent) {
20447
+ appendTimelineEvent(timelineEvent);
20448
+ } else if (eventType === "text" && !textLogged) {
20449
+ textLogged = true;
20450
+ appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
20451
+ }
20452
+ }, (meta) => {
20453
+ setStatus({ model: meta.model, backend: meta.backend });
20454
+ appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
20455
+ this.opts.onMeta?.(meta);
20456
+ }, (fn) => {
20457
+ killFn = fn;
20458
+ }, (beadId) => {
20459
+ setStatus({ bead_id: beadId });
20460
+ }, (fn) => {
20461
+ steerFn = fn;
20462
+ if (!existsSync10(fifoPath))
20463
+ return;
20464
+ fifoReadStream = createReadStream(fifoPath, { flags: "r+" });
20465
+ fifoReadline = createInterface({ input: fifoReadStream });
20466
+ fifoReadline.on("line", (line) => {
20467
+ try {
20468
+ const parsed = JSON.parse(line);
20469
+ if (parsed?.type === "steer" && typeof parsed.message === "string") {
20470
+ steerFn?.(parsed.message).catch(() => {});
20471
+ } else if (parsed?.type === "resume" && typeof parsed.task === "string") {
20472
+ if (resumeFn) {
20473
+ setStatus({ status: "running", current_event: "starting" });
20474
+ resumeFn(parsed.task).then((output) => {
20475
+ mkdirSync2(this.jobDir(id), { recursive: true });
20476
+ writeFileSync3(this.resultPath(id), output, "utf-8");
20477
+ setStatus({
20478
+ status: "waiting",
20479
+ current_event: "waiting",
20480
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
20481
+ last_event_at_ms: Date.now()
20482
+ });
20483
+ }).catch((err) => {
20484
+ setStatus({ status: "error", error: err?.message ?? String(err) });
20485
+ });
20486
+ }
20487
+ } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
20488
+ console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
20489
+ if (resumeFn) {
20490
+ setStatus({ status: "running", current_event: "starting" });
20491
+ resumeFn(parsed.message).then((output) => {
20492
+ mkdirSync2(this.jobDir(id), { recursive: true });
20493
+ writeFileSync3(this.resultPath(id), output, "utf-8");
20494
+ setStatus({
20495
+ status: "waiting",
20496
+ current_event: "waiting",
20497
+ elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
20498
+ last_event_at_ms: Date.now()
20499
+ });
20500
+ }).catch((err) => {
20501
+ setStatus({ status: "error", error: err?.message ?? String(err) });
20502
+ });
20503
+ }
20504
+ } else if (parsed?.type === "close") {
20505
+ closeFn?.().catch(() => {});
20506
+ }
20507
+ } catch {}
20508
+ });
20509
+ fifoReadline.on("error", () => {});
20510
+ }, (rFn, cFn) => {
20511
+ resumeFn = rFn;
20512
+ closeFn = cFn;
20513
+ setStatus({ status: "waiting", current_event: "waiting" });
20514
+ }, (tool, args, toolCallId) => {
20515
+ currentTool = tool;
20516
+ currentToolArgs = args;
20517
+ currentToolCallId = toolCallId ?? "";
20518
+ currentToolIsError = false;
20519
+ toolStartMs = Date.now();
20520
+ toolDurationWarnEmitted = false;
20521
+ setStatus({ current_tool: tool });
20522
+ if (toolCallId) {
20523
+ activeToolCalls.set(toolCallId, { tool, args });
20524
+ }
20525
+ }, (tool, isError, toolCallId) => {
20526
+ if (toolCallId && activeToolCalls.has(toolCallId)) {
20527
+ const entry = activeToolCalls.get(toolCallId);
20528
+ currentTool = entry.tool;
20529
+ currentToolArgs = entry.args;
20530
+ currentToolCallId = toolCallId;
20531
+ activeToolCalls.delete(toolCallId);
20532
+ } else {
20533
+ currentTool = tool;
20534
+ }
20535
+ currentToolIsError = isError;
20536
+ toolStartMs = undefined;
20537
+ toolDurationWarnEmitted = false;
20538
+ });
20539
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
20540
+ mkdirSync2(this.jobDir(id), { recursive: true });
20541
+ writeFileSync3(this.resultPath(id), result.output, "utf-8");
20542
+ const inputBeadId = runOptions.inputBeadId;
20543
+ const ownsBead = Boolean(result.beadId && !inputBeadId);
20544
+ const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
20545
+ const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && result.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
20546
+ if (ownsBead && result.beadId) {
20547
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
20548
+ } else if (shouldWriteExternalBeadNotes) {
20549
+ if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
20550
+ this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(result));
20551
+ } else if (result.beadId) {
20552
+ this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
20553
+ }
20554
+ }
20555
+ if (result.beadId) {
20556
+ if (!inputBeadId) {
20557
+ this.opts.beadsClient?.closeBead(result.beadId, "COMPLETE", result.durationMs, result.model);
20558
+ }
20559
+ }
20560
+ setStatus({
20561
+ status: "done",
20562
+ elapsed_s: elapsed,
20563
+ last_event_at_ms: Date.now(),
20564
+ model: result.model,
20565
+ backend: result.backend,
20566
+ bead_id: result.beadId
20567
+ });
20568
+ appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
20569
+ model: result.model,
20570
+ backend: result.backend,
20571
+ bead_id: result.beadId,
20572
+ output: result.output
20573
+ }));
20574
+ this.writeReadyMarker(id);
20575
+ return id;
20576
+ } catch (err) {
20577
+ const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
20578
+ const errorMsg = err?.message ?? String(err);
20579
+ setStatus({
20580
+ status: "error",
20581
+ elapsed_s: elapsed,
20582
+ error: errorMsg
20583
+ });
20584
+ appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
20585
+ error: errorMsg
20586
+ }));
20587
+ this.writeReadyMarker(id);
20588
+ throw err;
20589
+ } finally {
20590
+ if (stuckIntervalId !== undefined)
20591
+ clearInterval(stuckIntervalId);
20592
+ process.removeListener("SIGTERM", sigtermHandler);
20593
+ try {
20594
+ fifoReadline?.close();
20595
+ } catch {}
20596
+ try {
20597
+ fifoReadStream?.destroy();
20598
+ } catch {}
20599
+ try {
20600
+ fsyncSync(eventsFd);
20601
+ } catch {}
20602
+ closeSync(eventsFd);
20603
+ try {
20604
+ if (existsSync10(fifoPath))
20605
+ rmSync(fifoPath);
20606
+ } catch {}
20607
+ if (statusSnapshot.tmux_session) {
20608
+ spawnSync6("tmux", ["kill-session", "-t", statusSnapshot.tmux_session], { stdio: "ignore" });
20609
+ }
20651
20610
  }
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
20611
  }
20674
- await setAcrossFiles(files, keyPath, args.value);
20675
20612
  }
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
- }
20613
+ var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS;
20614
+ var init_supervisor = __esm(() => {
20615
+ init_timeline_events();
20616
+ JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
20617
+ STALL_DETECTION_DEFAULTS = {
20618
+ running_silence_warn_ms: 60000,
20619
+ running_silence_error_ms: 300000,
20620
+ waiting_stale_ms: 3600000,
20621
+ tool_duration_warn_ms: 120000
20684
20622
  };
20685
20623
  });
20686
20624
 
@@ -20832,7 +20770,7 @@ var init_format_helpers = __esm(() => {
20832
20770
  });
20833
20771
 
20834
20772
  // src/cli/tmux-utils.ts
20835
- import { spawnSync as spawnSync8 } from "node:child_process";
20773
+ import { spawnSync as spawnSync7 } from "node:child_process";
20836
20774
  function escapeForSingleQuotedBash(script) {
20837
20775
  return script.replace(/'/g, "'\\''");
20838
20776
  }
@@ -20840,7 +20778,7 @@ function quoteShellValue(value) {
20840
20778
  return `'${escapeForSingleQuotedBash(value)}'`;
20841
20779
  }
20842
20780
  function isTmuxAvailable() {
20843
- return spawnSync8("which", ["tmux"], { encoding: "utf8", timeout: 2000 }).status === 0;
20781
+ return spawnSync7("which", ["tmux"], { encoding: "utf8", timeout: 2000 }).status === 0;
20844
20782
  }
20845
20783
  function buildSessionName(specialist, suffix) {
20846
20784
  return `${TMUX_SESSION_PREFIX}-${specialist}-${suffix}`;
@@ -20855,7 +20793,7 @@ function createTmuxSession(name, cwd, cmd, extraEnv = {}) {
20855
20793
  }
20856
20794
  const startupScript = `${exports.join("; ")}; exec ${cmd}`;
20857
20795
  const wrappedCommand = `/bin/bash -c '${escapeForSingleQuotedBash(startupScript)}'`;
20858
- const result = spawnSync8("tmux", ["new-session", "-d", "-s", name, "-c", cwd, wrappedCommand], { encoding: "utf8", stdio: "pipe" });
20796
+ const result = spawnSync7("tmux", ["new-session", "-d", "-s", name, "-c", cwd, wrappedCommand], { encoding: "utf8", stdio: "pipe" });
20859
20797
  if (result.status !== 0) {
20860
20798
  const errorOutput = (result.stderr ?? "").trim() || (result.error?.message ?? "unknown error");
20861
20799
  throw new Error(`Failed to create tmux session "${name}": ${errorOutput}`);
@@ -20869,8 +20807,8 @@ var exports_run = {};
20869
20807
  __export(exports_run, {
20870
20808
  run: () => run9
20871
20809
  });
20872
- import { join as join18 } from "node:path";
20873
- import { readFileSync as readFileSync8 } from "node:fs";
20810
+ import { join as join11 } from "node:path";
20811
+ import { readFileSync as readFileSync6 } from "node:fs";
20874
20812
  import { randomBytes } from "node:crypto";
20875
20813
  import { spawn as cpSpawn } from "node:child_process";
20876
20814
  async function parseArgs6(argv) {
@@ -20959,13 +20897,13 @@ async function parseArgs6(argv) {
20959
20897
  return { name, prompt, beadId, model, noBeads, noBeadNotes, keepAlive, noKeepAlive, background, contextDepth, outputMode };
20960
20898
  }
20961
20899
  function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
20962
- const eventsPath = join18(jobsDir, jobId, "events.jsonl");
20900
+ const eventsPath = join11(jobsDir, jobId, "events.jsonl");
20963
20901
  let linesRead = 0;
20964
20902
  let activeInlinePhase = null;
20965
20903
  const drain = () => {
20966
20904
  let content;
20967
20905
  try {
20968
- content = readFileSync8(eventsPath, "utf-8");
20906
+ content = readFileSync6(eventsPath, "utf-8");
20969
20907
  } catch {
20970
20908
  return;
20971
20909
  }
@@ -21027,10 +20965,10 @@ function shellQuote(value) {
21027
20965
  async function run9() {
21028
20966
  const args = await parseArgs6(process.argv.slice(3));
21029
20967
  if (args.background) {
21030
- const latestPath = join18(process.cwd(), ".specialists", "jobs", "latest");
20968
+ const latestPath = join11(process.cwd(), ".specialists", "jobs", "latest");
21031
20969
  const oldLatest = (() => {
21032
20970
  try {
21033
- return readFileSync8(latestPath, "utf-8").trim();
20971
+ return readFileSync6(latestPath, "utf-8").trim();
21034
20972
  } catch {
21035
20973
  return "";
21036
20974
  }
@@ -21058,7 +20996,7 @@ async function run9() {
21058
20996
  while (Date.now() < deadline) {
21059
20997
  await new Promise((r) => setTimeout(r, 100));
21060
20998
  try {
21061
- const current = readFileSync8(latestPath, "utf-8").trim();
20999
+ const current = readFileSync6(latestPath, "utf-8").trim();
21062
21000
  if (current && current !== oldLatest) {
21063
21001
  jobId2 = current;
21064
21002
  break;
@@ -21078,7 +21016,7 @@ async function run9() {
21078
21016
  }
21079
21017
  const loader = new SpecialistLoader;
21080
21018
  const circuitBreaker = new CircuitBreaker;
21081
- const hooks = new HookEmitter({ tracePath: join18(process.cwd(), ".specialists", "trace.jsonl") });
21019
+ const hooks = new HookEmitter({ tracePath: join11(process.cwd(), ".specialists", "trace.jsonl") });
21082
21020
  const beadsClient = args.noBeads ? undefined : new BeadsClient;
21083
21021
  const beadReader = beadsClient ?? new BeadsClient;
21084
21022
  let prompt = args.prompt;
@@ -21113,7 +21051,7 @@ async function run9() {
21113
21051
  beadsClient
21114
21052
  });
21115
21053
  const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
21116
- const jobsDir = join18(process.cwd(), ".specialists", "jobs");
21054
+ const jobsDir = join11(process.cwd(), ".specialists", "jobs");
21117
21055
  let stopTailer;
21118
21056
  const supervisor = new Supervisor({
21119
21057
  runner,
@@ -21192,9 +21130,9 @@ var exports_status = {};
21192
21130
  __export(exports_status, {
21193
21131
  run: () => run10
21194
21132
  });
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";
21133
+ import { spawnSync as spawnSync8 } from "node:child_process";
21134
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
21135
+ import { join as join12 } from "node:path";
21198
21136
  function ok2(msg) {
21199
21137
  console.log(` ${green7("✓")} ${msg}`);
21200
21138
  }
@@ -21213,7 +21151,7 @@ function section(label) {
21213
21151
  ${bold7(`── ${label} ${line}`)}`);
21214
21152
  }
21215
21153
  function cmd(bin, args) {
21216
- const r = spawnSync9(bin, args, {
21154
+ const r = spawnSync8(bin, args, {
21217
21155
  encoding: "utf8",
21218
21156
  stdio: "pipe",
21219
21157
  timeout: 5000
@@ -21221,7 +21159,7 @@ function cmd(bin, args) {
21221
21159
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
21222
21160
  }
21223
21161
  function isInstalled(bin) {
21224
- return spawnSync9("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21162
+ return spawnSync8("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21225
21163
  }
21226
21164
  function formatElapsed2(s) {
21227
21165
  if (s.elapsed_s === undefined)
@@ -21273,10 +21211,10 @@ function parseStatusArgs(argv) {
21273
21211
  return { jsonMode, jobId };
21274
21212
  }
21275
21213
  function countJobEvents(jobsDir, jobId) {
21276
- const eventsFile = join19(jobsDir, jobId, "events.jsonl");
21277
- if (!existsSync14(eventsFile))
21214
+ const eventsFile = join12(jobsDir, jobId, "events.jsonl");
21215
+ if (!existsSync11(eventsFile))
21278
21216
  return 0;
21279
- const raw = readFileSync9(eventsFile, "utf-8").trim();
21217
+ const raw = readFileSync7(eventsFile, "utf-8").trim();
21280
21218
  if (!raw)
21281
21219
  return 0;
21282
21220
  return raw.split(`
@@ -21319,12 +21257,12 @@ async function run10() {
21319
21257
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
21320
21258
  const bdInstalled = isInstalled("bd");
21321
21259
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
21322
- const beadsPresent = existsSync14(join19(process.cwd(), ".beads"));
21260
+ const beadsPresent = existsSync11(join12(process.cwd(), ".beads"));
21323
21261
  const specialistsBin = cmd("which", ["specialists"]);
21324
- const jobsDir = join19(process.cwd(), ".specialists", "jobs");
21262
+ const jobsDir = join12(process.cwd(), ".specialists", "jobs");
21325
21263
  let jobs = [];
21326
21264
  let supervisor = null;
21327
- if (existsSync14(jobsDir)) {
21265
+ if (existsSync11(jobsDir)) {
21328
21266
  supervisor = new Supervisor({
21329
21267
  runner: null,
21330
21268
  runOptions: null,
@@ -21469,8 +21407,8 @@ var exports_result = {};
21469
21407
  __export(exports_result, {
21470
21408
  run: () => run11
21471
21409
  });
21472
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
21473
- import { join as join20 } from "node:path";
21410
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
21411
+ import { join as join13 } from "node:path";
21474
21412
  function parseArgs7(argv) {
21475
21413
  const jobId = argv[0];
21476
21414
  if (!jobId || jobId.startsWith("--")) {
@@ -21500,9 +21438,9 @@ function parseArgs7(argv) {
21500
21438
  async function run11() {
21501
21439
  const args = parseArgs7(process.argv.slice(3));
21502
21440
  const { jobId } = args;
21503
- const jobsDir = join20(process.cwd(), ".specialists", "jobs");
21441
+ const jobsDir = join13(process.cwd(), ".specialists", "jobs");
21504
21442
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21505
- const resultPath = join20(jobsDir, jobId, "result.txt");
21443
+ const resultPath = join13(jobsDir, jobId, "result.txt");
21506
21444
  if (args.wait) {
21507
21445
  const startMs = Date.now();
21508
21446
  while (true) {
@@ -21512,11 +21450,11 @@ async function run11() {
21512
21450
  process.exit(1);
21513
21451
  }
21514
21452
  if (status2.status === "done") {
21515
- if (!existsSync15(resultPath)) {
21453
+ if (!existsSync12(resultPath)) {
21516
21454
  console.error(`Result file not found for job ${jobId}`);
21517
21455
  process.exit(1);
21518
21456
  }
21519
- process.stdout.write(readFileSync10(resultPath, "utf-8"));
21457
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21520
21458
  return;
21521
21459
  }
21522
21460
  if (status2.status === "error") {
@@ -21540,40 +21478,163 @@ async function run11() {
21540
21478
  console.error(`No job found: ${jobId}`);
21541
21479
  process.exit(1);
21542
21480
  }
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;
21481
+ if (status.status === "running" || status.status === "starting") {
21482
+ if (!existsSync12(resultPath)) {
21483
+ process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
21484
+ `);
21485
+ process.exit(1);
21486
+ }
21487
+ process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
21488
+ `);
21489
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21490
+ return;
21491
+ }
21492
+ if (status.status === "error") {
21493
+ process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
21494
+ `);
21495
+ process.exit(1);
21496
+ }
21497
+ if (!existsSync12(resultPath)) {
21498
+ console.error(`Result file not found for job ${jobId}`);
21499
+ process.exit(1);
21500
+ }
21501
+ process.stdout.write(readFileSync8(resultPath, "utf-8"));
21502
+ }
21503
+ var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
21504
+ var init_result = __esm(() => {
21505
+ init_supervisor();
21506
+ });
21507
+
21508
+ // src/specialist/timeline-query.ts
21509
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "node:fs";
21510
+ import { join as join14 } from "node:path";
21511
+ function readJobEvents(jobDir) {
21512
+ const eventsPath = join14(jobDir, "events.jsonl");
21513
+ if (!existsSync13(eventsPath))
21514
+ return [];
21515
+ const content = readFileSync9(eventsPath, "utf-8");
21516
+ const lines = content.split(`
21517
+ `).filter(Boolean);
21518
+ const events = [];
21519
+ for (const line of lines) {
21520
+ const event = parseTimelineEvent(line);
21521
+ if (event)
21522
+ events.push(event);
21523
+ }
21524
+ events.sort(compareTimelineEvents);
21525
+ return events;
21526
+ }
21527
+ function readJobEventsById(jobsDir, jobId) {
21528
+ return readJobEvents(join14(jobsDir, jobId));
21529
+ }
21530
+ function readAllJobEvents(jobsDir) {
21531
+ if (!existsSync13(jobsDir))
21532
+ return [];
21533
+ const batches = [];
21534
+ const entries = readdirSync5(jobsDir);
21535
+ for (const entry of entries) {
21536
+ const jobDir = join14(jobsDir, entry);
21537
+ try {
21538
+ const stat2 = __require("node:fs").statSync(jobDir);
21539
+ if (!stat2.isDirectory())
21540
+ continue;
21541
+ } catch {
21542
+ continue;
21543
+ }
21544
+ const jobId = entry;
21545
+ const statusPath = join14(jobDir, "status.json");
21546
+ let specialist = "unknown";
21547
+ let beadId;
21548
+ if (existsSync13(statusPath)) {
21549
+ try {
21550
+ const status = JSON.parse(readFileSync9(statusPath, "utf-8"));
21551
+ specialist = status.specialist ?? "unknown";
21552
+ beadId = status.bead_id;
21553
+ } catch {}
21554
+ }
21555
+ const events = readJobEvents(jobDir);
21556
+ if (events.length > 0) {
21557
+ batches.push({ jobId, specialist, beadId, events });
21558
+ }
21559
+ }
21560
+ return batches;
21561
+ }
21562
+ function mergeTimelineEvents(batches) {
21563
+ const merged = [];
21564
+ for (const batch of batches) {
21565
+ for (const event of batch.events) {
21566
+ merged.push({
21567
+ jobId: batch.jobId,
21568
+ specialist: batch.specialist,
21569
+ beadId: batch.beadId,
21570
+ event
21571
+ });
21572
+ }
21573
+ }
21574
+ merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
21575
+ return merged;
21576
+ }
21577
+ function filterTimelineEvents(merged, filter) {
21578
+ let result = merged;
21579
+ if (filter.since !== undefined) {
21580
+ result = result.filter(({ event }) => event.t >= filter.since);
21581
+ }
21582
+ if (filter.jobId !== undefined) {
21583
+ result = result.filter(({ jobId }) => jobId === filter.jobId);
21584
+ }
21585
+ if (filter.specialist !== undefined) {
21586
+ result = result.filter(({ specialist }) => specialist === filter.specialist);
21587
+ }
21588
+ if (filter.limit !== undefined && filter.limit > 0) {
21589
+ result = result.slice(0, filter.limit);
21553
21590
  }
21554
- if (status.status === "error") {
21555
- process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
21556
- `);
21557
- process.exit(1);
21591
+ return result;
21592
+ }
21593
+ function queryTimeline(jobsDir, filter = {}) {
21594
+ let batches = readAllJobEvents(jobsDir);
21595
+ if (filter.jobId !== undefined) {
21596
+ batches = batches.filter((b) => b.jobId === filter.jobId);
21558
21597
  }
21559
- if (!existsSync15(resultPath)) {
21560
- console.error(`Result file not found for job ${jobId}`);
21561
- process.exit(1);
21598
+ if (filter.specialist !== undefined) {
21599
+ batches = batches.filter((b) => b.specialist === filter.specialist);
21562
21600
  }
21563
- process.stdout.write(readFileSync10(resultPath, "utf-8"));
21601
+ const merged = mergeTimelineEvents(batches);
21602
+ return filterTimelineEvents(merged, filter);
21564
21603
  }
21565
- var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
21566
- var init_result = __esm(() => {
21567
- init_supervisor();
21604
+ var init_timeline_query = __esm(() => {
21605
+ init_timeline_events();
21568
21606
  });
21569
21607
 
21608
+ // src/specialist/model-display.ts
21609
+ function extractModelId(model) {
21610
+ if (!model)
21611
+ return;
21612
+ const trimmed = model.trim();
21613
+ if (!trimmed)
21614
+ return;
21615
+ return trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
21616
+ }
21617
+ function toModelAlias(model) {
21618
+ const modelId = extractModelId(model);
21619
+ if (!modelId)
21620
+ return;
21621
+ if (modelId.startsWith("claude-")) {
21622
+ return modelId.slice("claude-".length);
21623
+ }
21624
+ return modelId;
21625
+ }
21626
+ function formatSpecialistModel(specialist, model) {
21627
+ const alias = toModelAlias(model);
21628
+ return alias ? `${specialist}/${alias}` : specialist;
21629
+ }
21630
+
21570
21631
  // src/cli/feed.ts
21571
21632
  var exports_feed = {};
21572
21633
  __export(exports_feed, {
21573
21634
  run: () => run12
21574
21635
  });
21575
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
21576
- import { join as join21 } from "node:path";
21636
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
21637
+ import { join as join15 } from "node:path";
21577
21638
  function getHumanEventKey(event) {
21578
21639
  switch (event.type) {
21579
21640
  case "meta":
@@ -21637,9 +21698,9 @@ function parseSince(value) {
21637
21698
  return;
21638
21699
  }
21639
21700
  function isTerminalJobStatus(jobsDir, jobId) {
21640
- const statusPath = join21(jobsDir, jobId, "status.json");
21701
+ const statusPath = join15(jobsDir, jobId, "status.json");
21641
21702
  try {
21642
- const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21703
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21643
21704
  return status.status === "done" || status.status === "error";
21644
21705
  } catch {
21645
21706
  return false;
@@ -21650,10 +21711,10 @@ function makeJobMetaReader(jobsDir) {
21650
21711
  return (jobId) => {
21651
21712
  if (cache.has(jobId))
21652
21713
  return cache.get(jobId);
21653
- const statusPath = join21(jobsDir, jobId, "status.json");
21714
+ const statusPath = join15(jobsDir, jobId, "status.json");
21654
21715
  let meta = { startedAtMs: Date.now() };
21655
21716
  try {
21656
- const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21717
+ const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21657
21718
  meta = {
21658
21719
  model: status.model,
21659
21720
  backend: status.backend,
@@ -21845,8 +21906,8 @@ async function followMerged(jobsDir, options) {
21845
21906
  }
21846
21907
  async function run12() {
21847
21908
  const options = parseArgs8(process.argv.slice(3));
21848
- const jobsDir = join21(process.cwd(), ".specialists", "jobs");
21849
- if (!existsSync16(jobsDir)) {
21909
+ const jobsDir = join15(process.cwd(), ".specialists", "jobs");
21910
+ if (!existsSync14(jobsDir)) {
21850
21911
  console.log(dim7("No jobs directory found."));
21851
21912
  return;
21852
21913
  }
@@ -21873,8 +21934,8 @@ var exports_poll = {};
21873
21934
  __export(exports_poll, {
21874
21935
  run: () => run13
21875
21936
  });
21876
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
21877
- import { join as join22 } from "node:path";
21937
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
21938
+ import { join as join16 } from "node:path";
21878
21939
  function parseArgs9(argv) {
21879
21940
  let jobId;
21880
21941
  let cursor = 0;
@@ -21911,19 +21972,19 @@ function parseArgs9(argv) {
21911
21972
  return { jobId, cursor, outputCursor };
21912
21973
  }
21913
21974
  function readJobState(jobsDir, jobId, cursor, outputCursor) {
21914
- const jobDir = join22(jobsDir, jobId);
21915
- const statusPath = join22(jobDir, "status.json");
21975
+ const jobDir = join16(jobsDir, jobId);
21976
+ const statusPath = join16(jobDir, "status.json");
21916
21977
  let status = null;
21917
- if (existsSync17(statusPath)) {
21978
+ if (existsSync15(statusPath)) {
21918
21979
  try {
21919
- status = JSON.parse(readFileSync12(statusPath, "utf-8"));
21980
+ status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21920
21981
  } catch {}
21921
21982
  }
21922
- const resultPath = join22(jobDir, "result.txt");
21983
+ const resultPath = join16(jobDir, "result.txt");
21923
21984
  let fullOutput = "";
21924
- if (existsSync17(resultPath)) {
21985
+ if (existsSync15(resultPath)) {
21925
21986
  try {
21926
- fullOutput = readFileSync12(resultPath, "utf-8");
21987
+ fullOutput = readFileSync11(resultPath, "utf-8");
21927
21988
  } catch {}
21928
21989
  }
21929
21990
  const events = readJobEventsById(jobsDir, jobId);
@@ -21955,9 +22016,9 @@ function readJobState(jobsDir, jobId, cursor, outputCursor) {
21955
22016
  }
21956
22017
  async function run13() {
21957
22018
  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)) {
22019
+ const jobsDir = join16(process.cwd(), ".specialists", "jobs");
22020
+ const jobDir = join16(jobsDir, jobId);
22021
+ if (!existsSync15(jobDir)) {
21961
22022
  const result2 = {
21962
22023
  job_id: jobId,
21963
22024
  status: "error",
@@ -21988,8 +22049,8 @@ var exports_steer = {};
21988
22049
  __export(exports_steer, {
21989
22050
  run: () => run14
21990
22051
  });
21991
- import { join as join23 } from "node:path";
21992
- import { writeFileSync as writeFileSync6 } from "node:fs";
22052
+ import { join as join17 } from "node:path";
22053
+ import { writeFileSync as writeFileSync4 } from "node:fs";
21993
22054
  async function run14() {
21994
22055
  const jobId = process.argv[3];
21995
22056
  const message = process.argv[4];
@@ -21997,7 +22058,7 @@ async function run14() {
21997
22058
  console.error('Usage: specialists|sp steer <job-id> "<message>"');
21998
22059
  process.exit(1);
21999
22060
  }
22000
- const jobsDir = join23(process.cwd(), ".specialists", "jobs");
22061
+ const jobsDir = join17(process.cwd(), ".specialists", "jobs");
22001
22062
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22002
22063
  const status = supervisor.readStatus(jobId);
22003
22064
  if (!status) {
@@ -22019,7 +22080,7 @@ async function run14() {
22019
22080
  try {
22020
22081
  const payload = JSON.stringify({ type: "steer", message }) + `
22021
22082
  `;
22022
- writeFileSync6(status.fifo_path, payload, { flag: "a" });
22083
+ writeFileSync4(status.fifo_path, payload, { flag: "a" });
22023
22084
  process.stdout.write(`${green9("✓")} Steer message sent to job ${jobId}
22024
22085
  `);
22025
22086
  } catch (err) {
@@ -22038,8 +22099,8 @@ var exports_resume = {};
22038
22099
  __export(exports_resume, {
22039
22100
  run: () => run15
22040
22101
  });
22041
- import { join as join24 } from "node:path";
22042
- import { writeFileSync as writeFileSync7 } from "node:fs";
22102
+ import { join as join18 } from "node:path";
22103
+ import { writeFileSync as writeFileSync5 } from "node:fs";
22043
22104
  async function run15() {
22044
22105
  const jobId = process.argv[3];
22045
22106
  const task = process.argv[4];
@@ -22047,7 +22108,7 @@ async function run15() {
22047
22108
  console.error('Usage: specialists|sp resume <job-id> "<task>"');
22048
22109
  process.exit(1);
22049
22110
  }
22050
- const jobsDir = join24(process.cwd(), ".specialists", "jobs");
22111
+ const jobsDir = join18(process.cwd(), ".specialists", "jobs");
22051
22112
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22052
22113
  const status = supervisor.readStatus(jobId);
22053
22114
  if (!status) {
@@ -22069,7 +22130,7 @@ async function run15() {
22069
22130
  try {
22070
22131
  const payload = JSON.stringify({ type: "resume", task }) + `
22071
22132
  `;
22072
- writeFileSync7(status.fifo_path, payload, { flag: "a" });
22133
+ writeFileSync5(status.fifo_path, payload, { flag: "a" });
22073
22134
  process.stdout.write(`${green10("✓")} Resume sent to job ${jobId}
22074
22135
  `);
22075
22136
  process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
@@ -22102,13 +22163,13 @@ __export(exports_clean, {
22102
22163
  run: () => run17
22103
22164
  });
22104
22165
  import {
22105
- existsSync as existsSync18,
22166
+ existsSync as existsSync16,
22106
22167
  readdirSync as readdirSync6,
22107
- readFileSync as readFileSync13,
22168
+ readFileSync as readFileSync12,
22108
22169
  rmSync as rmSync2,
22109
22170
  statSync as statSync2
22110
22171
  } from "node:fs";
22111
- import { join as join25 } from "node:path";
22172
+ import { join as join19 } from "node:path";
22112
22173
  function parseTtlDaysFromEnvironment() {
22113
22174
  const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
22114
22175
  if (!rawValue)
@@ -22164,7 +22225,7 @@ function readDirectorySizeBytes(directoryPath) {
22164
22225
  let totalBytes = 0;
22165
22226
  const entries = readdirSync6(directoryPath, { withFileTypes: true });
22166
22227
  for (const entry of entries) {
22167
- const entryPath = join25(directoryPath, entry.name);
22228
+ const entryPath = join19(directoryPath, entry.name);
22168
22229
  const stats = statSync2(entryPath);
22169
22230
  if (stats.isDirectory()) {
22170
22231
  totalBytes += readDirectorySizeBytes(entryPath);
@@ -22177,13 +22238,13 @@ function readDirectorySizeBytes(directoryPath) {
22177
22238
  function readCompletedJobDirectory(baseDirectory, entry) {
22178
22239
  if (!entry.isDirectory())
22179
22240
  return null;
22180
- const directoryPath = join25(baseDirectory, entry.name);
22181
- const statusFilePath = join25(directoryPath, "status.json");
22182
- if (!existsSync18(statusFilePath))
22241
+ const directoryPath = join19(baseDirectory, entry.name);
22242
+ const statusFilePath = join19(directoryPath, "status.json");
22243
+ if (!existsSync16(statusFilePath))
22183
22244
  return null;
22184
22245
  let statusData;
22185
22246
  try {
22186
- statusData = JSON.parse(readFileSync13(statusFilePath, "utf-8"));
22247
+ statusData = JSON.parse(readFileSync12(statusFilePath, "utf-8"));
22187
22248
  } catch {
22188
22249
  return null;
22189
22250
  }
@@ -22264,8 +22325,8 @@ async function run17() {
22264
22325
  const message = error2 instanceof Error ? error2.message : String(error2);
22265
22326
  printUsageAndExit(message);
22266
22327
  }
22267
- const jobsDirectoryPath = join25(process.cwd(), ".specialists", "jobs");
22268
- if (!existsSync18(jobsDirectoryPath)) {
22328
+ const jobsDirectoryPath = join19(process.cwd(), ".specialists", "jobs");
22329
+ if (!existsSync16(jobsDirectoryPath)) {
22269
22330
  console.log("No jobs directory found.");
22270
22331
  return;
22271
22332
  }
@@ -22292,14 +22353,14 @@ var exports_stop = {};
22292
22353
  __export(exports_stop, {
22293
22354
  run: () => run18
22294
22355
  });
22295
- import { join as join26 } from "node:path";
22356
+ import { join as join20 } from "node:path";
22296
22357
  async function run18() {
22297
22358
  const jobId = process.argv[3];
22298
22359
  if (!jobId) {
22299
22360
  console.error("Usage: specialists|sp stop <job-id>");
22300
22361
  process.exit(1);
22301
22362
  }
22302
- const jobsDir = join26(process.cwd(), ".specialists", "jobs");
22363
+ const jobsDir = join20(process.cwd(), ".specialists", "jobs");
22303
22364
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22304
22365
  const status = supervisor.readStatus(jobId);
22305
22366
  if (!status) {
@@ -22341,16 +22402,16 @@ var exports_attach = {};
22341
22402
  __export(exports_attach, {
22342
22403
  run: () => run19
22343
22404
  });
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";
22405
+ import { execFileSync as execFileSync2, spawnSync as spawnSync9 } from "node:child_process";
22406
+ import { readFileSync as readFileSync13 } from "node:fs";
22407
+ import { join as join21 } from "node:path";
22347
22408
  function exitWithError(message) {
22348
22409
  console.error(message);
22349
22410
  process.exit(1);
22350
22411
  }
22351
22412
  function readStatus(statusPath, jobId) {
22352
22413
  try {
22353
- return JSON.parse(readFileSync14(statusPath, "utf-8"));
22414
+ return JSON.parse(readFileSync13(statusPath, "utf-8"));
22354
22415
  } catch (error2) {
22355
22416
  if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
22356
22417
  exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs.`);
@@ -22364,8 +22425,8 @@ async function run19() {
22364
22425
  if (!jobId) {
22365
22426
  exitWithError("Usage: specialists attach <job-id>");
22366
22427
  }
22367
- const jobsDir = join27(process.cwd(), ".specialists", "jobs");
22368
- const statusPath = join27(jobsDir, jobId, "status.json");
22428
+ const jobsDir = join21(process.cwd(), ".specialists", "jobs");
22429
+ const statusPath = join21(jobsDir, jobId, "status.json");
22369
22430
  const status = readStatus(statusPath, jobId);
22370
22431
  if (status.status === "done" || status.status === "error") {
22371
22432
  exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
@@ -22374,7 +22435,7 @@ async function run19() {
22374
22435
  if (!sessionName) {
22375
22436
  exitWithError("Job `" + jobId + "` has no tmux session. It may have been started without tmux or tmux was not installed.");
22376
22437
  }
22377
- const whichTmux = spawnSync10("which", ["tmux"], { stdio: "ignore" });
22438
+ const whichTmux = spawnSync9("which", ["tmux"], { stdio: "ignore" });
22378
22439
  if (whichTmux.status !== 0) {
22379
22440
  exitWithError("tmux is not installed. Install tmux to use `specialists attach`.");
22380
22441
  }
@@ -22621,9 +22682,9 @@ var exports_doctor = {};
22621
22682
  __export(exports_doctor, {
22622
22683
  run: () => run21
22623
22684
  });
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";
22685
+ import { spawnSync as spawnSync10 } from "node:child_process";
22686
+ import { existsSync as existsSync17, mkdirSync as mkdirSync3, readFileSync as readFileSync14, readdirSync as readdirSync7 } from "node:fs";
22687
+ import { join as join22 } from "node:path";
22627
22688
  function ok3(msg) {
22628
22689
  console.log(` ${green13("✓")} ${msg}`);
22629
22690
  }
@@ -22645,17 +22706,17 @@ function section3(label) {
22645
22706
  ${bold10(`── ${label} ${line}`)}`);
22646
22707
  }
22647
22708
  function sp(bin, args) {
22648
- const r = spawnSync11(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22709
+ const r = spawnSync10(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22649
22710
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
22650
22711
  }
22651
22712
  function isInstalled2(bin) {
22652
- return spawnSync11("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22713
+ return spawnSync10("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22653
22714
  }
22654
22715
  function loadJson2(path) {
22655
- if (!existsSync19(path))
22716
+ if (!existsSync17(path))
22656
22717
  return null;
22657
22718
  try {
22658
- return JSON.parse(readFileSync15(path, "utf8"));
22719
+ return JSON.parse(readFileSync14(path, "utf8"));
22659
22720
  } catch {
22660
22721
  return null;
22661
22722
  }
@@ -22698,7 +22759,7 @@ function checkBd() {
22698
22759
  return false;
22699
22760
  }
22700
22761
  ok3(`bd installed ${dim12(sp("bd", ["--version"]).stdout || "")}`);
22701
- if (existsSync19(join28(CWD, ".beads")))
22762
+ if (existsSync17(join22(CWD, ".beads")))
22702
22763
  ok3(".beads/ present in project");
22703
22764
  else
22704
22765
  warn2(".beads/ not found in project");
@@ -22718,8 +22779,8 @@ function checkHooks() {
22718
22779
  section3("Claude Code hooks (2 expected)");
22719
22780
  let allPresent = true;
22720
22781
  for (const name of HOOK_NAMES) {
22721
- const dest = join28(HOOKS_DIR, name);
22722
- if (!existsSync19(dest)) {
22782
+ const dest = join22(HOOKS_DIR, name);
22783
+ if (!existsSync17(dest)) {
22723
22784
  fail2(`${name} ${red7("missing")}`);
22724
22785
  fix("specialists install");
22725
22786
  allPresent = false;
@@ -22763,18 +22824,18 @@ function checkMCP() {
22763
22824
  }
22764
22825
  function checkRuntimeDirs() {
22765
22826
  section3(".specialists/ runtime directories");
22766
- const rootDir = join28(CWD, ".specialists");
22767
- const jobsDir = join28(rootDir, "jobs");
22768
- const readyDir = join28(rootDir, "ready");
22827
+ const rootDir = join22(CWD, ".specialists");
22828
+ const jobsDir = join22(rootDir, "jobs");
22829
+ const readyDir = join22(rootDir, "ready");
22769
22830
  let allOk = true;
22770
- if (!existsSync19(rootDir)) {
22831
+ if (!existsSync17(rootDir)) {
22771
22832
  warn2(".specialists/ not found in current project");
22772
22833
  fix("specialists init");
22773
22834
  allOk = false;
22774
22835
  } else {
22775
22836
  ok3(".specialists/ present");
22776
22837
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
22777
- if (!existsSync19(subDir)) {
22838
+ if (!existsSync17(subDir)) {
22778
22839
  warn2(`.specialists/${label}/ missing — auto-creating`);
22779
22840
  mkdirSync3(subDir, { recursive: true });
22780
22841
  ok3(`.specialists/${label}/ created`);
@@ -22787,8 +22848,8 @@ function checkRuntimeDirs() {
22787
22848
  }
22788
22849
  function checkZombieJobs() {
22789
22850
  section3("Background jobs");
22790
- const jobsDir = join28(CWD, ".specialists", "jobs");
22791
- if (!existsSync19(jobsDir)) {
22851
+ const jobsDir = join22(CWD, ".specialists", "jobs");
22852
+ if (!existsSync17(jobsDir)) {
22792
22853
  hint("No .specialists/jobs/ — skipping");
22793
22854
  return true;
22794
22855
  }
@@ -22806,11 +22867,11 @@ function checkZombieJobs() {
22806
22867
  let total = 0;
22807
22868
  let running = 0;
22808
22869
  for (const jobId of entries) {
22809
- const statusPath = join28(jobsDir, jobId, "status.json");
22810
- if (!existsSync19(statusPath))
22870
+ const statusPath = join22(jobsDir, jobId, "status.json");
22871
+ if (!existsSync17(statusPath))
22811
22872
  continue;
22812
22873
  try {
22813
- const status = JSON.parse(readFileSync15(statusPath, "utf8"));
22874
+ const status = JSON.parse(readFileSync14(statusPath, "utf8"));
22814
22875
  total++;
22815
22876
  if (status.status === "running" || status.status === "starting") {
22816
22877
  const pid = status.pid;
@@ -22862,11 +22923,11 @@ ${bold10("specialists doctor")}
22862
22923
  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
22924
  var init_doctor = __esm(() => {
22864
22925
  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");
22926
+ CLAUDE_DIR = join22(CWD, ".claude");
22927
+ SPECIALISTS_DIR = join22(CWD, ".specialists");
22928
+ HOOKS_DIR = join22(SPECIALISTS_DIR, "default", "hooks");
22929
+ SETTINGS_FILE = join22(CLAUDE_DIR, "settings.json");
22930
+ MCP_FILE2 = join22(CWD, ".mcp.json");
22870
22931
  HOOK_NAMES = [
22871
22932
  "specialists-complete.mjs",
22872
22933
  "specialists-session-start.mjs"
@@ -30234,7 +30295,7 @@ class StdioServerTransport {
30234
30295
  }
30235
30296
 
30236
30297
  // src/server.ts
30237
- import { join as join11 } from "node:path";
30298
+ import { join as join3 } from "node:path";
30238
30299
 
30239
30300
  // src/constants.ts
30240
30301
  var LOG_PREFIX = "[specialists]";
@@ -30304,24 +30365,6 @@ init_hooks();
30304
30365
  init_circuitBreaker();
30305
30366
  init_beads();
30306
30367
 
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
30368
  // src/tools/specialist/use_specialist.tool.ts
30326
30369
  init_zod();
30327
30370
  init_beads();
@@ -30371,633 +30414,9 @@ function createUseSpecialistTool(runner) {
30371
30414
  };
30372
30415
  }
30373
30416
 
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
30417
  // src/server.ts
30962
30418
  init_zod();
30963
30419
 
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
30420
  class SpecialistsServer {
31002
30421
  server;
31003
30422
  tools;
@@ -31005,42 +30424,18 @@ class SpecialistsServer {
31005
30424
  const circuitBreaker = new CircuitBreaker;
31006
30425
  const loader = new SpecialistLoader;
31007
30426
  const hooks = new HookEmitter({
31008
- tracePath: join11(process.cwd(), ".specialists", "trace.jsonl")
30427
+ tracePath: join3(process.cwd(), ".specialists", "trace.jsonl")
31009
30428
  });
31010
30429
  const beadsClient = new BeadsClient;
31011
30430
  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
- ];
30431
+ this.tools = [createUseSpecialistTool(runner)];
31027
30432
  this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
31028
30433
  this.setupHandlers();
31029
30434
  }
31030
30435
  toolSchemas = {};
31031
30436
  setupHandlers() {
31032
30437
  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
30438
+ use_specialist: useSpecialistSchema
31044
30439
  };
31045
30440
  this.toolSchemas = schemaMap;
31046
30441
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {