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