@jaggerxtrm/specialists 2.1.21 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -122
- package/bin/install.js +33 -4
- package/dist/index.js +626 -30
- package/hooks/beads-close-memory-prompt.mjs +47 -0
- package/hooks/specialists-complete.mjs +60 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17435,6 +17435,7 @@ var init_schema = __esm(() => {
|
|
|
17435
17435
|
model: stringType(),
|
|
17436
17436
|
fallback_model: stringType().optional(),
|
|
17437
17437
|
timeout_ms: numberType().default(120000),
|
|
17438
|
+
stall_timeout_ms: numberType().optional(),
|
|
17438
17439
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
17439
17440
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
17440
17441
|
preferred_profile: stringType().optional(),
|
|
@@ -17455,7 +17456,8 @@ var init_schema = __esm(() => {
|
|
|
17455
17456
|
inject_output: booleanType().default(false)
|
|
17456
17457
|
})).optional(),
|
|
17457
17458
|
references: arrayType(unknownType()).optional(),
|
|
17458
|
-
tools: arrayType(stringType()).optional()
|
|
17459
|
+
tools: arrayType(stringType()).optional(),
|
|
17460
|
+
paths: arrayType(stringType()).optional()
|
|
17459
17461
|
}).optional();
|
|
17460
17462
|
CapabilitiesSchema = objectType({
|
|
17461
17463
|
file_scope: arrayType(stringType()).optional(),
|
|
@@ -17570,6 +17572,19 @@ class SpecialistLoader {
|
|
|
17570
17572
|
if (existsSync(filePath)) {
|
|
17571
17573
|
const content = await readFile(filePath, "utf-8");
|
|
17572
17574
|
const spec = await parseSpecialist(content);
|
|
17575
|
+
const rawPaths = spec.specialist.skills?.paths;
|
|
17576
|
+
if (rawPaths?.length) {
|
|
17577
|
+
const home = homedir();
|
|
17578
|
+
const fileDir = dir.path;
|
|
17579
|
+
const resolved = rawPaths.map((p) => {
|
|
17580
|
+
if (p.startsWith("~/"))
|
|
17581
|
+
return join(home, p.slice(2));
|
|
17582
|
+
if (p.startsWith("./"))
|
|
17583
|
+
return join(fileDir, p.slice(2));
|
|
17584
|
+
return p;
|
|
17585
|
+
});
|
|
17586
|
+
spec.specialist.skills.paths = resolved;
|
|
17587
|
+
}
|
|
17573
17588
|
this.cache.set(name, spec);
|
|
17574
17589
|
return spec;
|
|
17575
17590
|
}
|
|
@@ -17631,6 +17646,10 @@ function mapPermissionToTools(level) {
|
|
|
17631
17646
|
return "read,bash,grep,find,ls";
|
|
17632
17647
|
case "BASH_ONLY":
|
|
17633
17648
|
return "bash";
|
|
17649
|
+
case "LOW":
|
|
17650
|
+
case "MEDIUM":
|
|
17651
|
+
case "HIGH":
|
|
17652
|
+
return "read,bash,edit,write,grep,find,ls";
|
|
17634
17653
|
default:
|
|
17635
17654
|
return;
|
|
17636
17655
|
}
|
|
@@ -17645,6 +17664,7 @@ class PiAgentSession {
|
|
|
17645
17664
|
_agentEndReceived = false;
|
|
17646
17665
|
_killed = false;
|
|
17647
17666
|
_lineBuffer = "";
|
|
17667
|
+
_pendingCommand;
|
|
17648
17668
|
meta;
|
|
17649
17669
|
constructor(options, meta) {
|
|
17650
17670
|
this.options = options;
|
|
@@ -17719,6 +17739,12 @@ class PiAgentSession {
|
|
|
17719
17739
|
return;
|
|
17720
17740
|
}
|
|
17721
17741
|
const { type } = event;
|
|
17742
|
+
if (type === "response") {
|
|
17743
|
+
const handler = this._pendingCommand;
|
|
17744
|
+
this._pendingCommand = undefined;
|
|
17745
|
+
handler?.(event);
|
|
17746
|
+
return;
|
|
17747
|
+
}
|
|
17722
17748
|
if (type === "message_start" && event.message?.role === "assistant") {
|
|
17723
17749
|
const { provider, model } = event.message ?? {};
|
|
17724
17750
|
if (provider || model) {
|
|
@@ -17783,17 +17809,66 @@ class PiAgentSession {
|
|
|
17783
17809
|
}
|
|
17784
17810
|
}
|
|
17785
17811
|
}
|
|
17812
|
+
sendCommand(cmd) {
|
|
17813
|
+
return new Promise((resolve, reject) => {
|
|
17814
|
+
if (!this.proc?.stdin) {
|
|
17815
|
+
reject(new Error("No stdin available"));
|
|
17816
|
+
return;
|
|
17817
|
+
}
|
|
17818
|
+
this._pendingCommand = resolve;
|
|
17819
|
+
this.proc.stdin.write(JSON.stringify(cmd) + `
|
|
17820
|
+
`, (err) => {
|
|
17821
|
+
if (err) {
|
|
17822
|
+
this._pendingCommand = undefined;
|
|
17823
|
+
reject(err);
|
|
17824
|
+
}
|
|
17825
|
+
});
|
|
17826
|
+
});
|
|
17827
|
+
}
|
|
17786
17828
|
async prompt(task) {
|
|
17787
17829
|
const msg = JSON.stringify({ type: "prompt", message: task }) + `
|
|
17788
17830
|
`;
|
|
17789
17831
|
this.proc?.stdin?.write(msg);
|
|
17790
|
-
this.proc?.stdin?.end();
|
|
17791
17832
|
}
|
|
17792
|
-
async waitForDone() {
|
|
17793
|
-
|
|
17833
|
+
async waitForDone(timeout) {
|
|
17834
|
+
const donePromise = this._donePromise;
|
|
17835
|
+
if (!timeout)
|
|
17836
|
+
return donePromise;
|
|
17837
|
+
return Promise.race([
|
|
17838
|
+
donePromise,
|
|
17839
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Specialist timed out after ${timeout}ms`)), timeout))
|
|
17840
|
+
]);
|
|
17794
17841
|
}
|
|
17795
17842
|
async getLastOutput() {
|
|
17796
|
-
|
|
17843
|
+
if (!this.proc?.stdin || !this.proc.stdin.writable) {
|
|
17844
|
+
return this._lastOutput;
|
|
17845
|
+
}
|
|
17846
|
+
try {
|
|
17847
|
+
const response = await Promise.race([
|
|
17848
|
+
this.sendCommand({ type: "get_last_assistant_text" }),
|
|
17849
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
|
|
17850
|
+
]);
|
|
17851
|
+
return response?.data?.text ?? this._lastOutput;
|
|
17852
|
+
} catch {
|
|
17853
|
+
return this._lastOutput;
|
|
17854
|
+
}
|
|
17855
|
+
}
|
|
17856
|
+
async getState() {
|
|
17857
|
+
try {
|
|
17858
|
+
const response = await Promise.race([
|
|
17859
|
+
this.sendCommand({ type: "get_state" }),
|
|
17860
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
|
|
17861
|
+
]);
|
|
17862
|
+
return response?.data;
|
|
17863
|
+
} catch {
|
|
17864
|
+
return null;
|
|
17865
|
+
}
|
|
17866
|
+
}
|
|
17867
|
+
async close() {
|
|
17868
|
+
if (this._killed)
|
|
17869
|
+
return;
|
|
17870
|
+
this.proc?.stdin?.end();
|
|
17871
|
+
await this._donePromise.catch(() => {});
|
|
17797
17872
|
}
|
|
17798
17873
|
kill() {
|
|
17799
17874
|
if (this._killed)
|
|
@@ -17940,9 +18015,9 @@ class SpecialistRunner {
|
|
|
17940
18015
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
17941
18016
|
system_prompt_present: !!prompt.system
|
|
17942
18017
|
});
|
|
18018
|
+
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
17943
18019
|
let agentsMd = prompt.system ?? "";
|
|
17944
18020
|
if (prompt.skill_inherit) {
|
|
17945
|
-
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
17946
18021
|
const skillContent = await readFile2(prompt.skill_inherit, "utf-8").catch(() => "");
|
|
17947
18022
|
if (skillContent)
|
|
17948
18023
|
agentsMd += `
|
|
@@ -17950,6 +18025,17 @@ class SpecialistRunner {
|
|
|
17950
18025
|
---
|
|
17951
18026
|
# Service Knowledge
|
|
17952
18027
|
|
|
18028
|
+
${skillContent}`;
|
|
18029
|
+
}
|
|
18030
|
+
const skillPaths = spec.specialist.skills?.paths ?? [];
|
|
18031
|
+
for (const skillPath of skillPaths) {
|
|
18032
|
+
const skillContent = await readFile2(skillPath, "utf-8").catch(() => "");
|
|
18033
|
+
if (skillContent)
|
|
18034
|
+
agentsMd += `
|
|
18035
|
+
|
|
18036
|
+
---
|
|
18037
|
+
# Skill: ${skillPath}
|
|
18038
|
+
|
|
17953
18039
|
${skillContent}`;
|
|
17954
18040
|
}
|
|
17955
18041
|
if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
|
|
@@ -17998,10 +18084,11 @@ You have access via Bash:
|
|
|
17998
18084
|
await session.start();
|
|
17999
18085
|
onKillRegistered?.(session.kill.bind(session));
|
|
18000
18086
|
await session.prompt(renderedTask);
|
|
18001
|
-
await session.waitForDone();
|
|
18087
|
+
await session.waitForDone(execution.timeout_ms);
|
|
18002
18088
|
sessionBackend = session.meta.backend;
|
|
18003
18089
|
output = await session.getLastOutput();
|
|
18004
18090
|
sessionBackend = session.meta.backend;
|
|
18091
|
+
await session.close();
|
|
18005
18092
|
const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
|
|
18006
18093
|
for (const script of postScripts)
|
|
18007
18094
|
runScript(script.path);
|
|
@@ -18372,6 +18459,32 @@ ${bold3("specialists init")}
|
|
|
18372
18459
|
mkdirSync(specialistsDir, { recursive: true });
|
|
18373
18460
|
ok("created specialists/");
|
|
18374
18461
|
}
|
|
18462
|
+
const runtimeDir = join5(cwd, ".specialists");
|
|
18463
|
+
if (existsSync3(runtimeDir)) {
|
|
18464
|
+
skip(".specialists/ already exists");
|
|
18465
|
+
} else {
|
|
18466
|
+
mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
|
|
18467
|
+
mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
|
|
18468
|
+
ok("created .specialists/ (jobs/, ready/)");
|
|
18469
|
+
}
|
|
18470
|
+
const gitignorePath = join5(cwd, ".gitignore");
|
|
18471
|
+
if (existsSync3(gitignorePath)) {
|
|
18472
|
+
const existing = readFileSync(gitignorePath, "utf-8");
|
|
18473
|
+
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
18474
|
+
skip(".gitignore already has .specialists/ entry");
|
|
18475
|
+
} else {
|
|
18476
|
+
const separator = existing.endsWith(`
|
|
18477
|
+
`) ? "" : `
|
|
18478
|
+
`;
|
|
18479
|
+
writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
|
|
18480
|
+
`, "utf-8");
|
|
18481
|
+
ok("added .specialists/ to .gitignore");
|
|
18482
|
+
}
|
|
18483
|
+
} else {
|
|
18484
|
+
writeFileSync(gitignorePath, GITIGNORE_ENTRY + `
|
|
18485
|
+
`, "utf-8");
|
|
18486
|
+
ok("created .gitignore with .specialists/ entry");
|
|
18487
|
+
}
|
|
18375
18488
|
const agentsPath = join5(cwd, "AGENTS.md");
|
|
18376
18489
|
if (existsSync3(agentsPath)) {
|
|
18377
18490
|
const existing = readFileSync(agentsPath, "utf-8");
|
|
@@ -18396,7 +18509,7 @@ ${bold3("Done!")}
|
|
|
18396
18509
|
console.log(` 3. Restart Claude Code to pick up AGENTS.md changes
|
|
18397
18510
|
`);
|
|
18398
18511
|
}
|
|
18399
|
-
var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists";
|
|
18512
|
+
var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/";
|
|
18400
18513
|
var init_init = __esm(() => {
|
|
18401
18514
|
AGENTS_BLOCK = `
|
|
18402
18515
|
## Specialists
|
|
@@ -18538,21 +18651,220 @@ var init_edit = __esm(() => {
|
|
|
18538
18651
|
VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
|
|
18539
18652
|
});
|
|
18540
18653
|
|
|
18654
|
+
// src/specialist/supervisor.ts
|
|
18655
|
+
import {
|
|
18656
|
+
closeSync,
|
|
18657
|
+
existsSync as existsSync4,
|
|
18658
|
+
mkdirSync as mkdirSync2,
|
|
18659
|
+
openSync,
|
|
18660
|
+
readdirSync,
|
|
18661
|
+
readFileSync as readFileSync3,
|
|
18662
|
+
renameSync,
|
|
18663
|
+
rmSync,
|
|
18664
|
+
statSync,
|
|
18665
|
+
writeFileSync as writeFileSync3,
|
|
18666
|
+
writeSync
|
|
18667
|
+
} from "node:fs";
|
|
18668
|
+
import { join as join6 } from "node:path";
|
|
18669
|
+
|
|
18670
|
+
class Supervisor {
|
|
18671
|
+
opts;
|
|
18672
|
+
constructor(opts) {
|
|
18673
|
+
this.opts = opts;
|
|
18674
|
+
}
|
|
18675
|
+
jobDir(id) {
|
|
18676
|
+
return join6(this.opts.jobsDir, id);
|
|
18677
|
+
}
|
|
18678
|
+
statusPath(id) {
|
|
18679
|
+
return join6(this.jobDir(id), "status.json");
|
|
18680
|
+
}
|
|
18681
|
+
resultPath(id) {
|
|
18682
|
+
return join6(this.jobDir(id), "result.txt");
|
|
18683
|
+
}
|
|
18684
|
+
eventsPath(id) {
|
|
18685
|
+
return join6(this.jobDir(id), "events.jsonl");
|
|
18686
|
+
}
|
|
18687
|
+
readyDir() {
|
|
18688
|
+
return join6(this.opts.jobsDir, "..", "ready");
|
|
18689
|
+
}
|
|
18690
|
+
readStatus(id) {
|
|
18691
|
+
const path = this.statusPath(id);
|
|
18692
|
+
if (!existsSync4(path))
|
|
18693
|
+
return null;
|
|
18694
|
+
try {
|
|
18695
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
18696
|
+
} catch {
|
|
18697
|
+
return null;
|
|
18698
|
+
}
|
|
18699
|
+
}
|
|
18700
|
+
listJobs() {
|
|
18701
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18702
|
+
return [];
|
|
18703
|
+
const jobs = [];
|
|
18704
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18705
|
+
const path = join6(this.opts.jobsDir, entry, "status.json");
|
|
18706
|
+
if (!existsSync4(path))
|
|
18707
|
+
continue;
|
|
18708
|
+
try {
|
|
18709
|
+
jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
|
|
18710
|
+
} catch {}
|
|
18711
|
+
}
|
|
18712
|
+
return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
18713
|
+
}
|
|
18714
|
+
writeStatusFile(id, data) {
|
|
18715
|
+
const path = this.statusPath(id);
|
|
18716
|
+
const tmp = path + ".tmp";
|
|
18717
|
+
writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
18718
|
+
renameSync(tmp, path);
|
|
18719
|
+
}
|
|
18720
|
+
updateStatus(id, updates) {
|
|
18721
|
+
const current = this.readStatus(id);
|
|
18722
|
+
if (!current)
|
|
18723
|
+
return;
|
|
18724
|
+
this.writeStatusFile(id, { ...current, ...updates });
|
|
18725
|
+
}
|
|
18726
|
+
gc() {
|
|
18727
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18728
|
+
return;
|
|
18729
|
+
const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
|
|
18730
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18731
|
+
const dir = join6(this.opts.jobsDir, entry);
|
|
18732
|
+
try {
|
|
18733
|
+
const stat2 = statSync(dir);
|
|
18734
|
+
if (!stat2.isDirectory())
|
|
18735
|
+
continue;
|
|
18736
|
+
if (stat2.mtimeMs < cutoff)
|
|
18737
|
+
rmSync(dir, { recursive: true, force: true });
|
|
18738
|
+
} catch {}
|
|
18739
|
+
}
|
|
18740
|
+
}
|
|
18741
|
+
crashRecovery() {
|
|
18742
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18743
|
+
return;
|
|
18744
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18745
|
+
const statusPath = join6(this.opts.jobsDir, entry, "status.json");
|
|
18746
|
+
if (!existsSync4(statusPath))
|
|
18747
|
+
continue;
|
|
18748
|
+
try {
|
|
18749
|
+
const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
|
|
18750
|
+
if (s.status !== "running" && s.status !== "starting")
|
|
18751
|
+
continue;
|
|
18752
|
+
if (!s.pid)
|
|
18753
|
+
continue;
|
|
18754
|
+
try {
|
|
18755
|
+
process.kill(s.pid, 0);
|
|
18756
|
+
} catch {
|
|
18757
|
+
const tmp = statusPath + ".tmp";
|
|
18758
|
+
const updated = { ...s, status: "error", error: "Process crashed or was killed" };
|
|
18759
|
+
writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
18760
|
+
renameSync(tmp, statusPath);
|
|
18761
|
+
}
|
|
18762
|
+
} catch {}
|
|
18763
|
+
}
|
|
18764
|
+
}
|
|
18765
|
+
async run() {
|
|
18766
|
+
const { runner, runOptions, jobsDir } = this.opts;
|
|
18767
|
+
this.gc();
|
|
18768
|
+
this.crashRecovery();
|
|
18769
|
+
const id = crypto.randomUUID().slice(0, 6);
|
|
18770
|
+
const dir = this.jobDir(id);
|
|
18771
|
+
const startedAtMs = Date.now();
|
|
18772
|
+
mkdirSync2(dir, { recursive: true });
|
|
18773
|
+
mkdirSync2(this.readyDir(), { recursive: true });
|
|
18774
|
+
const initialStatus = {
|
|
18775
|
+
id,
|
|
18776
|
+
specialist: runOptions.name,
|
|
18777
|
+
status: "starting",
|
|
18778
|
+
started_at_ms: startedAtMs,
|
|
18779
|
+
pid: process.pid
|
|
18780
|
+
};
|
|
18781
|
+
this.writeStatusFile(id, initialStatus);
|
|
18782
|
+
const eventsFd = openSync(this.eventsPath(id), "a");
|
|
18783
|
+
const appendEvent = (obj) => {
|
|
18784
|
+
try {
|
|
18785
|
+
writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
|
|
18786
|
+
`);
|
|
18787
|
+
} catch {}
|
|
18788
|
+
};
|
|
18789
|
+
let textLogged = false;
|
|
18790
|
+
let currentTool = "";
|
|
18791
|
+
try {
|
|
18792
|
+
const result = await runner.run(runOptions, (delta) => {
|
|
18793
|
+
const toolMatch = delta.match(/⚙ (.+?)…/);
|
|
18794
|
+
if (toolMatch) {
|
|
18795
|
+
currentTool = toolMatch[1];
|
|
18796
|
+
this.updateStatus(id, { current_tool: currentTool });
|
|
18797
|
+
}
|
|
18798
|
+
}, (eventType) => {
|
|
18799
|
+
const now = Date.now();
|
|
18800
|
+
this.updateStatus(id, {
|
|
18801
|
+
status: "running",
|
|
18802
|
+
current_event: eventType,
|
|
18803
|
+
last_event_at_ms: now,
|
|
18804
|
+
elapsed_s: Math.round((now - startedAtMs) / 1000)
|
|
18805
|
+
});
|
|
18806
|
+
if (LOGGED_EVENTS.has(eventType)) {
|
|
18807
|
+
const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
|
|
18808
|
+
appendEvent({ type: eventType, ...tool ? { tool } : {} });
|
|
18809
|
+
} else if (eventType === "text" && !textLogged) {
|
|
18810
|
+
textLogged = true;
|
|
18811
|
+
appendEvent({ type: "text" });
|
|
18812
|
+
}
|
|
18813
|
+
}, (meta) => {
|
|
18814
|
+
this.updateStatus(id, { model: meta.model, backend: meta.backend });
|
|
18815
|
+
appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
|
|
18816
|
+
}, (_killFn) => {}, (beadId) => {
|
|
18817
|
+
this.updateStatus(id, { bead_id: beadId });
|
|
18818
|
+
});
|
|
18819
|
+
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18820
|
+
writeFileSync3(this.resultPath(id), result.output, "utf-8");
|
|
18821
|
+
this.updateStatus(id, {
|
|
18822
|
+
status: "done",
|
|
18823
|
+
elapsed_s: elapsed,
|
|
18824
|
+
last_event_at_ms: Date.now(),
|
|
18825
|
+
model: result.model,
|
|
18826
|
+
backend: result.backend,
|
|
18827
|
+
bead_id: result.beadId
|
|
18828
|
+
});
|
|
18829
|
+
appendEvent({ type: "agent_end", elapsed_s: elapsed });
|
|
18830
|
+
writeFileSync3(join6(this.readyDir(), id), "", "utf-8");
|
|
18831
|
+
return id;
|
|
18832
|
+
} catch (err) {
|
|
18833
|
+
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18834
|
+
this.updateStatus(id, {
|
|
18835
|
+
status: "error",
|
|
18836
|
+
elapsed_s: elapsed,
|
|
18837
|
+
error: err?.message ?? String(err)
|
|
18838
|
+
});
|
|
18839
|
+
appendEvent({ type: "error", message: err?.message ?? String(err) });
|
|
18840
|
+
throw err;
|
|
18841
|
+
} finally {
|
|
18842
|
+
closeSync(eventsFd);
|
|
18843
|
+
}
|
|
18844
|
+
}
|
|
18845
|
+
}
|
|
18846
|
+
var JOB_TTL_DAYS, LOGGED_EVENTS;
|
|
18847
|
+
var init_supervisor = __esm(() => {
|
|
18848
|
+
JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
|
|
18849
|
+
LOGGED_EVENTS = new Set(["thinking", "toolcall", "tool_execution_end", "done"]);
|
|
18850
|
+
});
|
|
18851
|
+
|
|
18541
18852
|
// src/cli/run.ts
|
|
18542
18853
|
var exports_run = {};
|
|
18543
18854
|
__export(exports_run, {
|
|
18544
18855
|
run: () => run7
|
|
18545
18856
|
});
|
|
18546
|
-
import { join as
|
|
18857
|
+
import { join as join7 } from "node:path";
|
|
18547
18858
|
async function parseArgs4(argv) {
|
|
18548
18859
|
const name = argv[0];
|
|
18549
18860
|
if (!name || name.startsWith("--")) {
|
|
18550
|
-
console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads]');
|
|
18861
|
+
console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
|
|
18551
18862
|
process.exit(1);
|
|
18552
18863
|
}
|
|
18553
18864
|
let prompt = "";
|
|
18554
18865
|
let model;
|
|
18555
18866
|
let noBeads = false;
|
|
18867
|
+
let background = false;
|
|
18556
18868
|
for (let i = 1;i < argv.length; i++) {
|
|
18557
18869
|
const token = argv[i];
|
|
18558
18870
|
if (token === "--prompt" && argv[i + 1]) {
|
|
@@ -18567,6 +18879,10 @@ async function parseArgs4(argv) {
|
|
|
18567
18879
|
noBeads = true;
|
|
18568
18880
|
continue;
|
|
18569
18881
|
}
|
|
18882
|
+
if (token === "--background") {
|
|
18883
|
+
background = true;
|
|
18884
|
+
continue;
|
|
18885
|
+
}
|
|
18570
18886
|
}
|
|
18571
18887
|
if (!prompt) {
|
|
18572
18888
|
if (process.stdin.isTTY) {
|
|
@@ -18581,13 +18897,13 @@ async function parseArgs4(argv) {
|
|
|
18581
18897
|
process.stdin.on("end", () => resolve(buf.trim()));
|
|
18582
18898
|
});
|
|
18583
18899
|
}
|
|
18584
|
-
return { name, prompt, model, noBeads };
|
|
18900
|
+
return { name, prompt, model, noBeads, background };
|
|
18585
18901
|
}
|
|
18586
18902
|
async function run7() {
|
|
18587
18903
|
const args = await parseArgs4(process.argv.slice(3));
|
|
18588
18904
|
const loader = new SpecialistLoader;
|
|
18589
18905
|
const circuitBreaker = new CircuitBreaker;
|
|
18590
|
-
const hooks = new HookEmitter({ tracePath:
|
|
18906
|
+
const hooks = new HookEmitter({ tracePath: join7(process.cwd(), ".specialists", "trace.jsonl") });
|
|
18591
18907
|
const beadsClient = args.noBeads ? null : new BeadsClient;
|
|
18592
18908
|
const runner = new SpecialistRunner({
|
|
18593
18909
|
loader,
|
|
@@ -18595,6 +18911,24 @@ async function run7() {
|
|
|
18595
18911
|
circuitBreaker,
|
|
18596
18912
|
beadsClient: beadsClient ?? undefined
|
|
18597
18913
|
});
|
|
18914
|
+
if (args.background) {
|
|
18915
|
+
const jobsDir = join7(process.cwd(), ".specialists", "jobs");
|
|
18916
|
+
const supervisor = new Supervisor({
|
|
18917
|
+
runner,
|
|
18918
|
+
runOptions: { name: args.name, prompt: args.prompt, backendOverride: args.model },
|
|
18919
|
+
jobsDir
|
|
18920
|
+
});
|
|
18921
|
+
try {
|
|
18922
|
+
const jobId = await supervisor.run();
|
|
18923
|
+
process.stdout.write(`Job started: ${jobId}
|
|
18924
|
+
`);
|
|
18925
|
+
} catch (err) {
|
|
18926
|
+
process.stderr.write(`Error: ${err?.message ?? err}
|
|
18927
|
+
`);
|
|
18928
|
+
process.exit(1);
|
|
18929
|
+
}
|
|
18930
|
+
return;
|
|
18931
|
+
}
|
|
18598
18932
|
process.stderr.write(`
|
|
18599
18933
|
${bold5(`Running ${cyan3(args.name)}`)}
|
|
18600
18934
|
|
|
@@ -18643,6 +18977,7 @@ var init_run = __esm(() => {
|
|
|
18643
18977
|
init_runner();
|
|
18644
18978
|
init_hooks();
|
|
18645
18979
|
init_beads();
|
|
18980
|
+
init_supervisor();
|
|
18646
18981
|
});
|
|
18647
18982
|
|
|
18648
18983
|
// src/cli/status.ts
|
|
@@ -18651,8 +18986,8 @@ __export(exports_status, {
|
|
|
18651
18986
|
run: () => run8
|
|
18652
18987
|
});
|
|
18653
18988
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
18654
|
-
import { existsSync as
|
|
18655
|
-
import { join as
|
|
18989
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
18990
|
+
import { join as join8 } from "node:path";
|
|
18656
18991
|
function ok2(msg) {
|
|
18657
18992
|
console.log(` ${green5("✓")} ${msg}`);
|
|
18658
18993
|
}
|
|
@@ -18681,6 +19016,27 @@ function cmd(bin, args) {
|
|
|
18681
19016
|
function isInstalled(bin) {
|
|
18682
19017
|
return spawnSync4("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
18683
19018
|
}
|
|
19019
|
+
function formatElapsed(s) {
|
|
19020
|
+
if (s.elapsed_s === undefined)
|
|
19021
|
+
return "...";
|
|
19022
|
+
const m = Math.floor(s.elapsed_s / 60);
|
|
19023
|
+
const sec = s.elapsed_s % 60;
|
|
19024
|
+
return m > 0 ? `${m}m${sec.toString().padStart(2, "0")}s` : `${sec}s`;
|
|
19025
|
+
}
|
|
19026
|
+
function statusColor(status) {
|
|
19027
|
+
switch (status) {
|
|
19028
|
+
case "running":
|
|
19029
|
+
return cyan4(status);
|
|
19030
|
+
case "done":
|
|
19031
|
+
return green5(status);
|
|
19032
|
+
case "error":
|
|
19033
|
+
return red(status);
|
|
19034
|
+
case "starting":
|
|
19035
|
+
return yellow5(status);
|
|
19036
|
+
default:
|
|
19037
|
+
return status;
|
|
19038
|
+
}
|
|
19039
|
+
}
|
|
18684
19040
|
async function run8() {
|
|
18685
19041
|
console.log(`
|
|
18686
19042
|
${bold6("specialists status")}
|
|
@@ -18724,7 +19080,7 @@ ${bold6("specialists status")}
|
|
|
18724
19080
|
} else {
|
|
18725
19081
|
const bdVersion = cmd("bd", ["--version"]);
|
|
18726
19082
|
ok2(`bd installed${bdVersion.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
|
|
18727
|
-
if (
|
|
19083
|
+
if (existsSync5(join8(process.cwd(), ".beads"))) {
|
|
18728
19084
|
ok2(".beads/ present in project");
|
|
18729
19085
|
} else {
|
|
18730
19086
|
warn(`.beads/ not found — run ${yellow5("bd init")} to enable issue tracking`);
|
|
@@ -18739,33 +19095,229 @@ ${bold6("specialists status")}
|
|
|
18739
19095
|
info(`verify registration: claude mcp get specialists`);
|
|
18740
19096
|
info(`re-register: specialists install`);
|
|
18741
19097
|
}
|
|
19098
|
+
const jobsDir = join8(process.cwd(), ".specialists", "jobs");
|
|
19099
|
+
if (existsSync5(jobsDir)) {
|
|
19100
|
+
const supervisor = new Supervisor({
|
|
19101
|
+
runner: null,
|
|
19102
|
+
runOptions: null,
|
|
19103
|
+
jobsDir
|
|
19104
|
+
});
|
|
19105
|
+
const jobs = supervisor.listJobs();
|
|
19106
|
+
if (jobs.length > 0) {
|
|
19107
|
+
section("Active Jobs");
|
|
19108
|
+
for (const job of jobs) {
|
|
19109
|
+
const elapsed = formatElapsed(job);
|
|
19110
|
+
const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
|
|
19111
|
+
console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
|
|
19112
|
+
}
|
|
19113
|
+
}
|
|
19114
|
+
}
|
|
18742
19115
|
console.log();
|
|
18743
19116
|
}
|
|
18744
|
-
var bold6 = (s) => `\x1B[1m${s}\x1B[0m`, dim6 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19117
|
+
var bold6 = (s) => `\x1B[1m${s}\x1B[0m`, dim6 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
18745
19118
|
var init_status = __esm(() => {
|
|
18746
19119
|
init_loader();
|
|
19120
|
+
init_supervisor();
|
|
19121
|
+
});
|
|
19122
|
+
|
|
19123
|
+
// src/cli/result.ts
|
|
19124
|
+
var exports_result = {};
|
|
19125
|
+
__export(exports_result, {
|
|
19126
|
+
run: () => run9
|
|
19127
|
+
});
|
|
19128
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
19129
|
+
import { join as join9 } from "node:path";
|
|
19130
|
+
async function run9() {
|
|
19131
|
+
const jobId = process.argv[3];
|
|
19132
|
+
if (!jobId) {
|
|
19133
|
+
console.error("Usage: specialists result <job-id>");
|
|
19134
|
+
process.exit(1);
|
|
19135
|
+
}
|
|
19136
|
+
const jobsDir = join9(process.cwd(), ".specialists", "jobs");
|
|
19137
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19138
|
+
const status = supervisor.readStatus(jobId);
|
|
19139
|
+
if (!status) {
|
|
19140
|
+
console.error(`No job found: ${jobId}`);
|
|
19141
|
+
process.exit(1);
|
|
19142
|
+
}
|
|
19143
|
+
if (status.status === "running" || status.status === "starting") {
|
|
19144
|
+
process.stderr.write(`${dim7(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
|
|
19145
|
+
`);
|
|
19146
|
+
process.exit(1);
|
|
19147
|
+
}
|
|
19148
|
+
if (status.status === "error") {
|
|
19149
|
+
process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
|
|
19150
|
+
`);
|
|
19151
|
+
process.exit(1);
|
|
19152
|
+
}
|
|
19153
|
+
const resultPath = join9(jobsDir, jobId, "result.txt");
|
|
19154
|
+
if (!existsSync6(resultPath)) {
|
|
19155
|
+
console.error(`Result file not found for job ${jobId}`);
|
|
19156
|
+
process.exit(1);
|
|
19157
|
+
}
|
|
19158
|
+
process.stdout.write(readFileSync4(resultPath, "utf-8"));
|
|
19159
|
+
}
|
|
19160
|
+
var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19161
|
+
var init_result = __esm(() => {
|
|
19162
|
+
init_supervisor();
|
|
19163
|
+
});
|
|
19164
|
+
|
|
19165
|
+
// src/cli/feed.ts
|
|
19166
|
+
var exports_feed = {};
|
|
19167
|
+
__export(exports_feed, {
|
|
19168
|
+
run: () => run10
|
|
19169
|
+
});
|
|
19170
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, watchFile } from "node:fs";
|
|
19171
|
+
import { join as join10 } from "node:path";
|
|
19172
|
+
function formatEvent(line) {
|
|
19173
|
+
try {
|
|
19174
|
+
const e = JSON.parse(line);
|
|
19175
|
+
const ts = new Date(e.t).toISOString().slice(11, 19);
|
|
19176
|
+
const type = e.type ?? "?";
|
|
19177
|
+
const extra = e.tool ? ` ${cyan5(e.tool)}` : e.model ? ` ${dim8(e.model)}` : e.message ? ` ${red3(e.message)}` : "";
|
|
19178
|
+
return `${dim8(ts)} ${type}${extra}`;
|
|
19179
|
+
} catch {
|
|
19180
|
+
return line;
|
|
19181
|
+
}
|
|
19182
|
+
}
|
|
19183
|
+
function printLines(content, from) {
|
|
19184
|
+
const lines = content.split(`
|
|
19185
|
+
`).filter(Boolean);
|
|
19186
|
+
for (let i = from;i < lines.length; i++) {
|
|
19187
|
+
console.log(formatEvent(lines[i]));
|
|
19188
|
+
}
|
|
19189
|
+
return lines.length;
|
|
19190
|
+
}
|
|
19191
|
+
async function run10() {
|
|
19192
|
+
const argv = process.argv.slice(3);
|
|
19193
|
+
let jobId;
|
|
19194
|
+
let follow = false;
|
|
19195
|
+
for (let i = 0;i < argv.length; i++) {
|
|
19196
|
+
if (argv[i] === "--job" && argv[i + 1]) {
|
|
19197
|
+
jobId = argv[++i];
|
|
19198
|
+
continue;
|
|
19199
|
+
}
|
|
19200
|
+
if (argv[i] === "--follow" || argv[i] === "-f") {
|
|
19201
|
+
follow = true;
|
|
19202
|
+
continue;
|
|
19203
|
+
}
|
|
19204
|
+
if (!jobId && !argv[i].startsWith("--"))
|
|
19205
|
+
jobId = argv[i];
|
|
19206
|
+
}
|
|
19207
|
+
if (!jobId) {
|
|
19208
|
+
console.error("Usage: specialists feed --job <job-id> [--follow]");
|
|
19209
|
+
process.exit(1);
|
|
19210
|
+
}
|
|
19211
|
+
const jobsDir = join10(process.cwd(), ".specialists", "jobs");
|
|
19212
|
+
const eventsPath = join10(jobsDir, jobId, "events.jsonl");
|
|
19213
|
+
if (!existsSync7(eventsPath)) {
|
|
19214
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19215
|
+
if (!supervisor.readStatus(jobId)) {
|
|
19216
|
+
console.error(`No job found: ${jobId}`);
|
|
19217
|
+
process.exit(1);
|
|
19218
|
+
}
|
|
19219
|
+
console.log(dim8("No events yet."));
|
|
19220
|
+
return;
|
|
19221
|
+
}
|
|
19222
|
+
const content = readFileSync5(eventsPath, "utf-8");
|
|
19223
|
+
let linesRead = printLines(content, 0);
|
|
19224
|
+
if (!follow)
|
|
19225
|
+
return;
|
|
19226
|
+
process.stderr.write(dim8(`Following ${jobId}... (Ctrl+C to stop)
|
|
19227
|
+
`));
|
|
19228
|
+
await new Promise((resolve) => {
|
|
19229
|
+
watchFile(eventsPath, { interval: 500 }, () => {
|
|
19230
|
+
try {
|
|
19231
|
+
const updated = readFileSync5(eventsPath, "utf-8");
|
|
19232
|
+
linesRead = printLines(updated, linesRead);
|
|
19233
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19234
|
+
const status = supervisor.readStatus(jobId);
|
|
19235
|
+
if (status && status.status !== "running" && status.status !== "starting") {
|
|
19236
|
+
const finalMsg = status.status === "done" ? `
|
|
19237
|
+
${yellow6("Job complete.")} Run: specialists result ${jobId}` : `
|
|
19238
|
+
${red3(`Job ${status.status}.`)} ${status.error ?? ""}`;
|
|
19239
|
+
process.stderr.write(finalMsg + `
|
|
19240
|
+
`);
|
|
19241
|
+
resolve();
|
|
19242
|
+
}
|
|
19243
|
+
} catch {}
|
|
19244
|
+
});
|
|
19245
|
+
});
|
|
19246
|
+
}
|
|
19247
|
+
var dim8 = (s) => `\x1B[2m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19248
|
+
var init_feed = __esm(() => {
|
|
19249
|
+
init_supervisor();
|
|
19250
|
+
});
|
|
19251
|
+
|
|
19252
|
+
// src/cli/stop.ts
|
|
19253
|
+
var exports_stop = {};
|
|
19254
|
+
__export(exports_stop, {
|
|
19255
|
+
run: () => run11
|
|
19256
|
+
});
|
|
19257
|
+
import { join as join11 } from "node:path";
|
|
19258
|
+
async function run11() {
|
|
19259
|
+
const jobId = process.argv[3];
|
|
19260
|
+
if (!jobId) {
|
|
19261
|
+
console.error("Usage: specialists stop <job-id>");
|
|
19262
|
+
process.exit(1);
|
|
19263
|
+
}
|
|
19264
|
+
const jobsDir = join11(process.cwd(), ".specialists", "jobs");
|
|
19265
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19266
|
+
const status = supervisor.readStatus(jobId);
|
|
19267
|
+
if (!status) {
|
|
19268
|
+
console.error(`No job found: ${jobId}`);
|
|
19269
|
+
process.exit(1);
|
|
19270
|
+
}
|
|
19271
|
+
if (status.status === "done" || status.status === "error") {
|
|
19272
|
+
process.stderr.write(`${dim9(`Job ${jobId} is already ${status.status}.`)}
|
|
19273
|
+
`);
|
|
19274
|
+
return;
|
|
19275
|
+
}
|
|
19276
|
+
if (!status.pid) {
|
|
19277
|
+
process.stderr.write(`${red4(`No PID recorded for job ${jobId}.`)}
|
|
19278
|
+
`);
|
|
19279
|
+
process.exit(1);
|
|
19280
|
+
}
|
|
19281
|
+
try {
|
|
19282
|
+
process.kill(status.pid, "SIGTERM");
|
|
19283
|
+
process.stdout.write(`${green6("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
19284
|
+
`);
|
|
19285
|
+
} catch (err) {
|
|
19286
|
+
if (err.code === "ESRCH") {
|
|
19287
|
+
process.stderr.write(`${red4(`Process ${status.pid} not found.`)} Job may have already completed.
|
|
19288
|
+
`);
|
|
19289
|
+
} else {
|
|
19290
|
+
process.stderr.write(`${red4("Error:")} ${err.message}
|
|
19291
|
+
`);
|
|
19292
|
+
process.exit(1);
|
|
19293
|
+
}
|
|
19294
|
+
}
|
|
19295
|
+
}
|
|
19296
|
+
var green6 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
19297
|
+
var init_stop = __esm(() => {
|
|
19298
|
+
init_supervisor();
|
|
18747
19299
|
});
|
|
18748
19300
|
|
|
18749
19301
|
// src/cli/help.ts
|
|
18750
19302
|
var exports_help = {};
|
|
18751
19303
|
__export(exports_help, {
|
|
18752
|
-
run: () =>
|
|
19304
|
+
run: () => run12
|
|
18753
19305
|
});
|
|
18754
|
-
async function
|
|
19306
|
+
async function run12() {
|
|
18755
19307
|
const lines = [
|
|
18756
19308
|
"",
|
|
18757
19309
|
bold7("specialists <command>"),
|
|
18758
19310
|
"",
|
|
18759
19311
|
"Commands:",
|
|
18760
|
-
...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${
|
|
19312
|
+
...COMMANDS.map(([cmd2, desc]) => ` ${cmd2.padEnd(COL_WIDTH)} ${dim10(desc)}`),
|
|
18761
19313
|
"",
|
|
18762
|
-
|
|
19314
|
+
dim10("Run 'specialists <command> --help' for command-specific options."),
|
|
18763
19315
|
""
|
|
18764
19316
|
];
|
|
18765
19317
|
console.log(lines.join(`
|
|
18766
19318
|
`));
|
|
18767
19319
|
}
|
|
18768
|
-
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
19320
|
+
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, COMMANDS, COL_WIDTH;
|
|
18769
19321
|
var init_help = __esm(() => {
|
|
18770
19322
|
COMMANDS = [
|
|
18771
19323
|
["install", "Full-stack installer: pi, beads, dolt, MCP registration, hooks"],
|
|
@@ -18774,8 +19326,11 @@ var init_help = __esm(() => {
|
|
|
18774
19326
|
["version", "Print installed version"],
|
|
18775
19327
|
["init", "Initialize specialists in the current project"],
|
|
18776
19328
|
["edit", "Edit a specialist field (e.g. --model, --description)"],
|
|
18777
|
-
["run", "Run a specialist with a prompt"],
|
|
18778
|
-
["
|
|
19329
|
+
["run", "Run a specialist with a prompt (--background for async)"],
|
|
19330
|
+
["result", "Print result of a background job"],
|
|
19331
|
+
["feed", "Tail events for a background job (--follow to stream)"],
|
|
19332
|
+
["stop", "Send SIGTERM to a running background job"],
|
|
19333
|
+
["status", "Show system health (pi, beads, MCP, jobs)"],
|
|
18779
19334
|
["help", "Show this help message"]
|
|
18780
19335
|
];
|
|
18781
19336
|
COL_WIDTH = Math.max(...COMMANDS.map(([cmd2]) => cmd2.length));
|
|
@@ -26152,7 +26707,7 @@ var runParallelSchema = objectType({
|
|
|
26152
26707
|
function createRunParallelTool(runner) {
|
|
26153
26708
|
return {
|
|
26154
26709
|
name: "run_parallel",
|
|
26155
|
-
description: "Execute multiple specialists concurrently. Returns aggregated results.",
|
|
26710
|
+
description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer CLI background jobs for async work.",
|
|
26156
26711
|
inputSchema: runParallelSchema,
|
|
26157
26712
|
async execute(input, onProgress) {
|
|
26158
26713
|
if (input.merge_strategy === "pipeline") {
|
|
@@ -26191,11 +26746,26 @@ var BACKENDS2 = ["gemini", "qwen", "anthropic", "openai"];
|
|
|
26191
26746
|
function createSpecialistStatusTool(loader, circuitBreaker) {
|
|
26192
26747
|
return {
|
|
26193
26748
|
name: "specialist_status",
|
|
26194
|
-
description: "System health: backend circuit breaker states, loaded specialists, staleness.",
|
|
26749
|
+
description: "System health: backend circuit breaker states, loaded specialists, staleness. Also shows active background jobs from .specialists/jobs/.",
|
|
26195
26750
|
inputSchema: exports_external.object({}),
|
|
26196
26751
|
async execute(_) {
|
|
26197
26752
|
const list = await loader.list();
|
|
26198
26753
|
const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
|
|
26754
|
+
const { existsSync: existsSync2, readdirSync, readFileSync } = await import("node:fs");
|
|
26755
|
+
const { join: join2 } = await import("node:path");
|
|
26756
|
+
const jobsDir = join2(process.cwd(), ".specialists", "jobs");
|
|
26757
|
+
const jobs = [];
|
|
26758
|
+
if (existsSync2(jobsDir)) {
|
|
26759
|
+
for (const entry of readdirSync(jobsDir)) {
|
|
26760
|
+
const statusPath = join2(jobsDir, entry, "status.json");
|
|
26761
|
+
if (!existsSync2(statusPath))
|
|
26762
|
+
continue;
|
|
26763
|
+
try {
|
|
26764
|
+
jobs.push(JSON.parse(readFileSync(statusPath, "utf-8")));
|
|
26765
|
+
} catch {}
|
|
26766
|
+
}
|
|
26767
|
+
jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
26768
|
+
}
|
|
26199
26769
|
return {
|
|
26200
26770
|
loaded_count: list.length,
|
|
26201
26771
|
backends_health: Object.fromEntries(BACKENDS2.map((b) => [b, circuitBreaker.getState(b)])),
|
|
@@ -26205,6 +26775,15 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
|
|
|
26205
26775
|
category: s.category,
|
|
26206
26776
|
version: s.version,
|
|
26207
26777
|
staleness: stalenessResults[i]
|
|
26778
|
+
})),
|
|
26779
|
+
background_jobs: jobs.map((j) => ({
|
|
26780
|
+
id: j.id,
|
|
26781
|
+
specialist: j.specialist,
|
|
26782
|
+
status: j.status,
|
|
26783
|
+
elapsed_s: j.elapsed_s,
|
|
26784
|
+
current_event: j.current_event,
|
|
26785
|
+
bead_id: j.bead_id,
|
|
26786
|
+
error: j.error
|
|
26208
26787
|
}))
|
|
26209
26788
|
};
|
|
26210
26789
|
}
|
|
@@ -26330,7 +26909,7 @@ var startSpecialistSchema = exports_external.object({
|
|
|
26330
26909
|
function createStartSpecialistTool(runner, registry2) {
|
|
26331
26910
|
return {
|
|
26332
26911
|
name: "start_specialist",
|
|
26333
|
-
description: "Start a specialist asynchronously. Returns job_id immediately. " + "Use poll_specialist to track progress, receive output delta, and retrieve beadId " + "(the beads issue auto-created for this run, if beads_integration policy applies). " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
|
|
26912
|
+
description: "[DEPRECATED v3] Start a specialist asynchronously. Returns job_id immediately. Prefer CLI: `specialists run <name> --background`. " + "Use poll_specialist to track progress, receive output delta, and retrieve beadId " + "(the beads issue auto-created for this run, if beads_integration policy applies). " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
|
|
26334
26913
|
inputSchema: startSpecialistSchema,
|
|
26335
26914
|
async execute(input) {
|
|
26336
26915
|
const jobId = await runner.startAsync({
|
|
@@ -26353,7 +26932,7 @@ var pollSpecialistSchema = exports_external.object({
|
|
|
26353
26932
|
function createPollSpecialistTool(registry2) {
|
|
26354
26933
|
return {
|
|
26355
26934
|
name: "poll_specialist",
|
|
26356
|
-
description: "Poll a running specialist job. Returns status (running|done|error|cancelled), " + "delta (new tokens since cursor), next_cursor, and full output when done. " + "Pass next_cursor back as cursor on each subsequent poll to receive only new content. " + "Response also includes beadId (string | undefined) once the specialist has started — " + "this is the beads issue tracking this run. If present after status=done, consider: " + '`bd update <beadId> --notes "<key finding>"` to attach results, or ' + '`bd remember "<insight>"` to persist discoveries across sessions.',
|
|
26935
|
+
description: "[DEPRECATED v3] Poll a running specialist job. Returns status (running|done|error|cancelled), " + "delta (new tokens since cursor), next_cursor, and full output when done. " + "Pass next_cursor back as cursor on each subsequent poll to receive only new content. " + "Response also includes beadId (string | undefined) once the specialist has started — " + "this is the beads issue tracking this run. If present after status=done, consider: " + '`bd update <beadId> --notes "<key finding>"` to attach results, or ' + '`bd remember "<insight>"` to persist discoveries across sessions.',
|
|
26357
26936
|
inputSchema: pollSpecialistSchema,
|
|
26358
26937
|
async execute(input) {
|
|
26359
26938
|
const snapshot = registry2.snapshot(input.job_id, input.cursor ?? 0);
|
|
@@ -26373,7 +26952,7 @@ var stopSpecialistSchema = exports_external.object({
|
|
|
26373
26952
|
function createStopSpecialistTool(registry2) {
|
|
26374
26953
|
return {
|
|
26375
26954
|
name: "stop_specialist",
|
|
26376
|
-
description: "Cancel a running specialist job. Kills the pi process immediately and sets status to cancelled. Subsequent poll_specialist calls return status: cancelled with output buffered up to that point.",
|
|
26955
|
+
description: "[DEPRECATED v3] Cancel a running specialist job. Prefer CLI: `specialists stop <id>`. Kills the pi process immediately and sets status to cancelled. Subsequent poll_specialist calls return status: cancelled with output buffered up to that point.",
|
|
26377
26956
|
inputSchema: stopSpecialistSchema,
|
|
26378
26957
|
async execute(input) {
|
|
26379
26958
|
const result = registry2.cancel(input.job_id);
|
|
@@ -26511,6 +27090,11 @@ class SpecialistsServer {
|
|
|
26511
27090
|
const transport = new StdioServerTransport;
|
|
26512
27091
|
await this.server.connect(transport);
|
|
26513
27092
|
logger.info(`Specialists MCP Server v2 started — ${this.tools.length} tools registered`);
|
|
27093
|
+
process.on("SIGTERM", async () => {
|
|
27094
|
+
logger.info("SIGTERM received — shutting down");
|
|
27095
|
+
await this.stop();
|
|
27096
|
+
process.exit(0);
|
|
27097
|
+
});
|
|
26514
27098
|
} catch (error2) {
|
|
26515
27099
|
logger.error("Failed to start server", error2);
|
|
26516
27100
|
process.exit(1);
|
|
@@ -26523,7 +27107,7 @@ class SpecialistsServer {
|
|
|
26523
27107
|
|
|
26524
27108
|
// src/index.ts
|
|
26525
27109
|
var sub = process.argv[2];
|
|
26526
|
-
async function
|
|
27110
|
+
async function run13() {
|
|
26527
27111
|
if (sub === "install") {
|
|
26528
27112
|
const { run: handler } = await Promise.resolve().then(() => (init_install(), exports_install));
|
|
26529
27113
|
return handler();
|
|
@@ -26556,6 +27140,18 @@ async function run10() {
|
|
|
26556
27140
|
const { run: handler } = await Promise.resolve().then(() => (init_status(), exports_status));
|
|
26557
27141
|
return handler();
|
|
26558
27142
|
}
|
|
27143
|
+
if (sub === "result") {
|
|
27144
|
+
const { run: handler } = await Promise.resolve().then(() => (init_result(), exports_result));
|
|
27145
|
+
return handler();
|
|
27146
|
+
}
|
|
27147
|
+
if (sub === "feed") {
|
|
27148
|
+
const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
|
|
27149
|
+
return handler();
|
|
27150
|
+
}
|
|
27151
|
+
if (sub === "stop") {
|
|
27152
|
+
const { run: handler } = await Promise.resolve().then(() => (init_stop(), exports_stop));
|
|
27153
|
+
return handler();
|
|
27154
|
+
}
|
|
26559
27155
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
26560
27156
|
const { run: handler } = await Promise.resolve().then(() => (init_help(), exports_help));
|
|
26561
27157
|
return handler();
|
|
@@ -26569,7 +27165,7 @@ Run 'specialists help' to see available commands.`);
|
|
|
26569
27165
|
const server = new SpecialistsServer;
|
|
26570
27166
|
await server.start();
|
|
26571
27167
|
}
|
|
26572
|
-
|
|
27168
|
+
run13().catch((error2) => {
|
|
26573
27169
|
logger.error(`Fatal error: ${error2}`);
|
|
26574
27170
|
process.exit(1);
|
|
26575
27171
|
});
|