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