@jaggerxtrm/specialists 3.2.0 → 3.3.0
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 +83 -254
- package/bin/install.js +152 -329
- package/dist/index.js +2531 -921
- package/package.json +3 -2
- package/hooks/beads-close-memory-prompt.mjs +0 -47
- package/hooks/beads-commit-gate.mjs +0 -58
- package/hooks/beads-edit-gate.mjs +0 -53
- package/hooks/beads-stop-gate.mjs +0 -52
- package/hooks/specialists-complete.mjs +0 -60
- package/hooks/specialists-main-guard.mjs +0 -90
- package/hooks/specialists-session-start.mjs +0 -105
- package/specialists/auto-remediation.specialist.yaml +0 -70
- package/specialists/bug-hunt.specialist.yaml +0 -94
- package/specialists/codebase-explorer.specialist.yaml +0 -79
- package/specialists/feature-design.specialist.yaml +0 -88
- package/specialists/init-session.specialist.yaml +0 -60
- package/specialists/overthinker.specialist.yaml +0 -63
- package/specialists/parallel-review.specialist.yaml +0 -61
- package/specialists/report-generator.specialist.yaml +0 -59
- package/specialists/test-runner.specialist.yaml +0 -58
package/dist/index.js
CHANGED
|
@@ -8531,6 +8531,7 @@ var require_limitLength = __commonJS((exports) => {
|
|
|
8531
8531
|
var require_pattern = __commonJS((exports) => {
|
|
8532
8532
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8533
8533
|
var code_1 = require_code2();
|
|
8534
|
+
var util_1 = require_util();
|
|
8534
8535
|
var codegen_1 = require_codegen();
|
|
8535
8536
|
var error2 = {
|
|
8536
8537
|
message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
|
|
@@ -8543,10 +8544,18 @@ var require_pattern = __commonJS((exports) => {
|
|
|
8543
8544
|
$data: true,
|
|
8544
8545
|
error: error2,
|
|
8545
8546
|
code(cxt) {
|
|
8546
|
-
const { data, $data, schema, schemaCode, it } = cxt;
|
|
8547
|
+
const { gen, data, $data, schema, schemaCode, it } = cxt;
|
|
8547
8548
|
const u = it.opts.unicodeRegExp ? "u" : "";
|
|
8548
|
-
|
|
8549
|
-
|
|
8549
|
+
if ($data) {
|
|
8550
|
+
const { regExp } = it.opts.code;
|
|
8551
|
+
const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
|
|
8552
|
+
const valid = gen.let("valid");
|
|
8553
|
+
gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
|
|
8554
|
+
cxt.fail$data((0, codegen_1._)`!${valid}`);
|
|
8555
|
+
} else {
|
|
8556
|
+
const regExp = (0, code_1.usePattern)(cxt, schema);
|
|
8557
|
+
cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
|
|
8558
|
+
}
|
|
8550
8559
|
}
|
|
8551
8560
|
};
|
|
8552
8561
|
exports.default = def;
|
|
@@ -17414,7 +17423,7 @@ async function parseSpecialist(yamlContent) {
|
|
|
17414
17423
|
const raw = $parse(yamlContent);
|
|
17415
17424
|
return SpecialistSchema.parseAsync(raw);
|
|
17416
17425
|
}
|
|
17417
|
-
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
|
|
17426
|
+
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
|
|
17418
17427
|
var init_schema = __esm(() => {
|
|
17419
17428
|
init_zod();
|
|
17420
17429
|
init_dist();
|
|
@@ -17438,6 +17447,7 @@ var init_schema = __esm(() => {
|
|
|
17438
17447
|
stall_timeout_ms: numberType().optional(),
|
|
17439
17448
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
17440
17449
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
17450
|
+
thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
|
17441
17451
|
preferred_profile: stringType().optional(),
|
|
17442
17452
|
approval_mode: stringType().optional()
|
|
17443
17453
|
});
|
|
@@ -17449,27 +17459,26 @@ var init_schema = __esm(() => {
|
|
|
17449
17459
|
examples: arrayType(unknownType()).optional(),
|
|
17450
17460
|
skill_inherit: stringType().optional()
|
|
17451
17461
|
});
|
|
17462
|
+
ScriptEntrySchema = objectType({
|
|
17463
|
+
run: stringType().optional(),
|
|
17464
|
+
path: stringType().optional(),
|
|
17465
|
+
phase: enumType(["pre", "post"]),
|
|
17466
|
+
inject_output: booleanType().default(false)
|
|
17467
|
+
}).transform((s) => ({
|
|
17468
|
+
run: s.run ?? s.path ?? "",
|
|
17469
|
+
phase: s.phase,
|
|
17470
|
+
inject_output: s.inject_output
|
|
17471
|
+
}));
|
|
17452
17472
|
SkillsSchema = objectType({
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
phase: enumType(["pre", "post"]),
|
|
17456
|
-
inject_output: booleanType().default(false)
|
|
17457
|
-
})).optional(),
|
|
17458
|
-
references: arrayType(unknownType()).optional(),
|
|
17459
|
-
tools: arrayType(stringType()).optional(),
|
|
17460
|
-
paths: arrayType(stringType()).optional()
|
|
17473
|
+
paths: arrayType(stringType()).optional(),
|
|
17474
|
+
scripts: arrayType(ScriptEntrySchema).optional()
|
|
17461
17475
|
}).optional();
|
|
17462
17476
|
CapabilitiesSchema = objectType({
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
can_spawn: booleanType().optional(),
|
|
17466
|
-
tools: arrayType(objectType({ name: stringType(), purpose: stringType() })).optional(),
|
|
17467
|
-
diagnostic_scripts: arrayType(stringType()).optional()
|
|
17477
|
+
required_tools: arrayType(stringType()).optional(),
|
|
17478
|
+
external_commands: arrayType(stringType()).optional()
|
|
17468
17479
|
}).optional();
|
|
17469
17480
|
CommunicationSchema = objectType({
|
|
17470
|
-
|
|
17471
|
-
subscribes: arrayType(stringType()).optional(),
|
|
17472
|
-
output_to: stringType().optional()
|
|
17481
|
+
next_specialists: unionType([stringType(), arrayType(stringType())]).optional()
|
|
17473
17482
|
}).optional();
|
|
17474
17483
|
ValidationSchema = objectType({
|
|
17475
17484
|
files_to_watch: arrayType(stringType()).optional(),
|
|
@@ -17485,6 +17494,7 @@ var init_schema = __esm(() => {
|
|
|
17485
17494
|
capabilities: CapabilitiesSchema,
|
|
17486
17495
|
communication: CommunicationSchema,
|
|
17487
17496
|
validation: ValidationSchema,
|
|
17497
|
+
output_file: stringType().optional(),
|
|
17488
17498
|
beads_integration: enumType(["auto", "always", "never"]).default("auto"),
|
|
17489
17499
|
heartbeat: unknownType().optional()
|
|
17490
17500
|
})
|
|
@@ -17494,7 +17504,6 @@ var init_schema = __esm(() => {
|
|
|
17494
17504
|
// src/specialist/loader.ts
|
|
17495
17505
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
17496
17506
|
import { join } from "node:path";
|
|
17497
|
-
import { homedir } from "node:os";
|
|
17498
17507
|
import { existsSync } from "node:fs";
|
|
17499
17508
|
async function checkStaleness(summary) {
|
|
17500
17509
|
if (!summary.filestoWatch?.length || !summary.updated)
|
|
@@ -17518,18 +17527,15 @@ async function checkStaleness(summary) {
|
|
|
17518
17527
|
class SpecialistLoader {
|
|
17519
17528
|
cache = new Map;
|
|
17520
17529
|
projectDir;
|
|
17521
|
-
userDir;
|
|
17522
17530
|
constructor(options = {}) {
|
|
17523
17531
|
this.projectDir = options.projectDir ?? process.cwd();
|
|
17524
|
-
this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
|
|
17525
17532
|
}
|
|
17526
17533
|
getScanDirs() {
|
|
17527
|
-
|
|
17528
|
-
{ path: join(this.projectDir, "specialists"), scope: "
|
|
17529
|
-
{ path: join(this.projectDir, ".
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
].filter((d) => existsSync(d.path));
|
|
17534
|
+
const dirs = [
|
|
17535
|
+
{ path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
|
|
17536
|
+
{ path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" }
|
|
17537
|
+
];
|
|
17538
|
+
return dirs.filter((d) => existsSync(d.path));
|
|
17533
17539
|
}
|
|
17534
17540
|
async list(category) {
|
|
17535
17541
|
const results = [];
|
|
@@ -17559,7 +17565,11 @@ class SpecialistLoader {
|
|
|
17559
17565
|
filestoWatch: spec.specialist.validation?.files_to_watch,
|
|
17560
17566
|
staleThresholdDays: spec.specialist.validation?.stale_threshold_days
|
|
17561
17567
|
});
|
|
17562
|
-
} catch {
|
|
17568
|
+
} catch (e) {
|
|
17569
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
17570
|
+
process.stderr.write(`[specialists] skipping ${filePath}: ${reason}
|
|
17571
|
+
`);
|
|
17572
|
+
}
|
|
17563
17573
|
}
|
|
17564
17574
|
}
|
|
17565
17575
|
return results;
|
|
@@ -17574,11 +17584,10 @@ class SpecialistLoader {
|
|
|
17574
17584
|
const spec = await parseSpecialist(content);
|
|
17575
17585
|
const rawPaths = spec.specialist.skills?.paths;
|
|
17576
17586
|
if (rawPaths?.length) {
|
|
17577
|
-
const home = homedir();
|
|
17578
17587
|
const fileDir = dir.path;
|
|
17579
17588
|
const resolved = rawPaths.map((p) => {
|
|
17580
17589
|
if (p.startsWith("~/"))
|
|
17581
|
-
return join(
|
|
17590
|
+
return join(process.env.HOME || "", p.slice(2));
|
|
17582
17591
|
if (p.startsWith("./"))
|
|
17583
17592
|
return join(fileDir, p.slice(2));
|
|
17584
17593
|
return p;
|
|
@@ -17640,14 +17649,17 @@ var init_backendMap = __esm(() => {
|
|
|
17640
17649
|
|
|
17641
17650
|
// src/pi/session.ts
|
|
17642
17651
|
import { spawn } from "node:child_process";
|
|
17652
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
17653
|
+
import { homedir } from "node:os";
|
|
17654
|
+
import { join as join2 } from "node:path";
|
|
17643
17655
|
function mapPermissionToTools(level) {
|
|
17644
17656
|
switch (level?.toUpperCase()) {
|
|
17645
17657
|
case "READ_ONLY":
|
|
17646
|
-
return "read,
|
|
17647
|
-
case "BASH_ONLY":
|
|
17648
|
-
return "bash";
|
|
17658
|
+
return "read,grep,find,ls";
|
|
17649
17659
|
case "LOW":
|
|
17660
|
+
return "read,bash,grep,find,ls";
|
|
17650
17661
|
case "MEDIUM":
|
|
17662
|
+
return "read,bash,edit,grep,find,ls";
|
|
17651
17663
|
case "HIGH":
|
|
17652
17664
|
return "read,bash,edit,write,grep,find,ls";
|
|
17653
17665
|
default:
|
|
@@ -17659,6 +17671,7 @@ class PiAgentSession {
|
|
|
17659
17671
|
options;
|
|
17660
17672
|
proc;
|
|
17661
17673
|
_lastOutput = "";
|
|
17674
|
+
_donePromise;
|
|
17662
17675
|
_doneResolve;
|
|
17663
17676
|
_doneReject;
|
|
17664
17677
|
_agentEndReceived = false;
|
|
@@ -17686,6 +17699,7 @@ class PiAgentSession {
|
|
|
17686
17699
|
const args = [
|
|
17687
17700
|
"--mode",
|
|
17688
17701
|
"rpc",
|
|
17702
|
+
"--no-extensions",
|
|
17689
17703
|
...providerArgs,
|
|
17690
17704
|
"--no-session",
|
|
17691
17705
|
...extraArgs
|
|
@@ -17693,11 +17707,28 @@ class PiAgentSession {
|
|
|
17693
17707
|
const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
|
|
17694
17708
|
if (toolsFlag)
|
|
17695
17709
|
args.push("--tools", toolsFlag);
|
|
17710
|
+
if (this.options.thinkingLevel) {
|
|
17711
|
+
args.push("--thinking", this.options.thinkingLevel);
|
|
17712
|
+
}
|
|
17713
|
+
for (const skillPath of this.options.skillPaths ?? []) {
|
|
17714
|
+
args.push("--skill", skillPath);
|
|
17715
|
+
}
|
|
17716
|
+
const piExtDir = join2(homedir(), ".pi", "agent", "extensions");
|
|
17717
|
+
const permLevel = (this.options.permissionLevel ?? "").toUpperCase();
|
|
17718
|
+
if (permLevel !== "READ_ONLY") {
|
|
17719
|
+
const qgPath = join2(piExtDir, "quality-gates");
|
|
17720
|
+
if (existsSync2(qgPath))
|
|
17721
|
+
args.push("-e", qgPath);
|
|
17722
|
+
}
|
|
17723
|
+
const ssPath = join2(piExtDir, "service-skills");
|
|
17724
|
+
if (existsSync2(ssPath))
|
|
17725
|
+
args.push("-e", ssPath);
|
|
17696
17726
|
if (this.options.systemPrompt) {
|
|
17697
17727
|
args.push("--append-system-prompt", this.options.systemPrompt);
|
|
17698
17728
|
}
|
|
17699
17729
|
this.proc = spawn("pi", args, {
|
|
17700
|
-
stdio: ["pipe", "pipe", "inherit"]
|
|
17730
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
17731
|
+
cwd: this.options.cwd
|
|
17701
17732
|
});
|
|
17702
17733
|
const donePromise = new Promise((resolve, reject) => {
|
|
17703
17734
|
this._doneResolve = resolve;
|
|
@@ -17759,34 +17790,11 @@ class PiAgentSession {
|
|
|
17759
17790
|
this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
17760
17791
|
}
|
|
17761
17792
|
this._agentEndReceived = true;
|
|
17762
|
-
this.options.onEvent?.("
|
|
17793
|
+
this.options.onEvent?.("agent_end");
|
|
17763
17794
|
this._doneResolve?.();
|
|
17764
17795
|
return;
|
|
17765
17796
|
}
|
|
17766
|
-
if (type === "thinking_start") {
|
|
17767
|
-
this.options.onEvent?.("thinking");
|
|
17768
|
-
return;
|
|
17769
|
-
}
|
|
17770
|
-
if (type === "thinking_delta") {
|
|
17771
|
-
if (event.delta)
|
|
17772
|
-
this.options.onThinking?.(event.delta);
|
|
17773
|
-
this.options.onEvent?.("thinking");
|
|
17774
|
-
return;
|
|
17775
|
-
}
|
|
17776
|
-
if (type === "thinking_end") {
|
|
17777
|
-
return;
|
|
17778
|
-
}
|
|
17779
|
-
if (type === "toolcall_start") {
|
|
17780
|
-
this.options.onToolStart?.(event.name ?? event.toolName ?? "tool");
|
|
17781
|
-
this.options.onEvent?.("toolcall");
|
|
17782
|
-
return;
|
|
17783
|
-
}
|
|
17784
|
-
if (type === "toolcall_end") {
|
|
17785
|
-
this.options.onEvent?.("toolcall");
|
|
17786
|
-
return;
|
|
17787
|
-
}
|
|
17788
17797
|
if (type === "tool_execution_start") {
|
|
17789
|
-
this.options.onToolStart?.(event.name ?? event.toolName ?? "tool");
|
|
17790
17798
|
this.options.onEvent?.("tool_execution");
|
|
17791
17799
|
return;
|
|
17792
17800
|
}
|
|
@@ -17795,7 +17803,7 @@ class PiAgentSession {
|
|
|
17795
17803
|
return;
|
|
17796
17804
|
}
|
|
17797
17805
|
if (type === "tool_execution_end") {
|
|
17798
|
-
this.options.onToolEnd?.(event.
|
|
17806
|
+
this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool");
|
|
17799
17807
|
this.options.onEvent?.("tool_execution_end");
|
|
17800
17808
|
return;
|
|
17801
17809
|
}
|
|
@@ -17803,9 +17811,30 @@ class PiAgentSession {
|
|
|
17803
17811
|
const ae = event.assistantMessageEvent;
|
|
17804
17812
|
if (!ae)
|
|
17805
17813
|
return;
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17814
|
+
switch (ae.type) {
|
|
17815
|
+
case "text_delta":
|
|
17816
|
+
if (ae.delta)
|
|
17817
|
+
this.options.onToken?.(ae.delta);
|
|
17818
|
+
this.options.onEvent?.("text");
|
|
17819
|
+
break;
|
|
17820
|
+
case "thinking_start":
|
|
17821
|
+
this.options.onEvent?.("thinking");
|
|
17822
|
+
break;
|
|
17823
|
+
case "thinking_delta":
|
|
17824
|
+
if (ae.delta)
|
|
17825
|
+
this.options.onThinking?.(ae.delta);
|
|
17826
|
+
this.options.onEvent?.("thinking");
|
|
17827
|
+
break;
|
|
17828
|
+
case "toolcall_start":
|
|
17829
|
+
this.options.onToolStart?.(ae.name ?? ae.toolName ?? "tool");
|
|
17830
|
+
this.options.onEvent?.("toolcall");
|
|
17831
|
+
break;
|
|
17832
|
+
case "toolcall_end":
|
|
17833
|
+
this.options.onEvent?.("toolcall");
|
|
17834
|
+
break;
|
|
17835
|
+
case "done":
|
|
17836
|
+
this.options.onEvent?.("message_done");
|
|
17837
|
+
break;
|
|
17809
17838
|
}
|
|
17810
17839
|
}
|
|
17811
17840
|
}
|
|
@@ -17831,7 +17860,7 @@ class PiAgentSession {
|
|
|
17831
17860
|
this.proc?.stdin?.write(msg);
|
|
17832
17861
|
}
|
|
17833
17862
|
async waitForDone(timeout) {
|
|
17834
|
-
const donePromise = this._donePromise;
|
|
17863
|
+
const donePromise = this._donePromise ?? Promise.resolve();
|
|
17835
17864
|
if (!timeout)
|
|
17836
17865
|
return donePromise;
|
|
17837
17866
|
return Promise.race([
|
|
@@ -17868,7 +17897,7 @@ class PiAgentSession {
|
|
|
17868
17897
|
if (this._killed)
|
|
17869
17898
|
return;
|
|
17870
17899
|
this.proc?.stdin?.end();
|
|
17871
|
-
await this._donePromise
|
|
17900
|
+
await this._donePromise?.catch(() => {});
|
|
17872
17901
|
}
|
|
17873
17902
|
kill() {
|
|
17874
17903
|
if (this._killed)
|
|
@@ -17878,6 +17907,30 @@ class PiAgentSession {
|
|
|
17878
17907
|
this.proc = undefined;
|
|
17879
17908
|
this._doneReject?.(new SessionKilledError);
|
|
17880
17909
|
}
|
|
17910
|
+
async steer(message) {
|
|
17911
|
+
if (this._killed || !this.proc?.stdin) {
|
|
17912
|
+
throw new Error("Session is not active");
|
|
17913
|
+
}
|
|
17914
|
+
const cmd = JSON.stringify({ type: "steer", message }) + `
|
|
17915
|
+
`;
|
|
17916
|
+
await new Promise((resolve, reject) => {
|
|
17917
|
+
this.proc.stdin.write(cmd, (err) => err ? reject(err) : resolve());
|
|
17918
|
+
});
|
|
17919
|
+
}
|
|
17920
|
+
async resume(task, timeout) {
|
|
17921
|
+
if (this._killed || !this.proc?.stdin) {
|
|
17922
|
+
throw new Error("Session is not active");
|
|
17923
|
+
}
|
|
17924
|
+
this._agentEndReceived = false;
|
|
17925
|
+
const donePromise = new Promise((resolve, reject) => {
|
|
17926
|
+
this._doneResolve = resolve;
|
|
17927
|
+
this._doneReject = reject;
|
|
17928
|
+
});
|
|
17929
|
+
donePromise.catch(() => {});
|
|
17930
|
+
this._donePromise = donePromise;
|
|
17931
|
+
await this.prompt(task);
|
|
17932
|
+
await this.waitForDone(timeout);
|
|
17933
|
+
}
|
|
17881
17934
|
}
|
|
17882
17935
|
var SessionKilledError;
|
|
17883
17936
|
var init_session = __esm(() => {
|
|
@@ -17892,6 +17945,29 @@ var init_session = __esm(() => {
|
|
|
17892
17945
|
|
|
17893
17946
|
// src/specialist/beads.ts
|
|
17894
17947
|
import { spawnSync } from "node:child_process";
|
|
17948
|
+
function buildBeadContext(bead, completedBlockers = []) {
|
|
17949
|
+
const lines = [`# Task: ${bead.title}`];
|
|
17950
|
+
if (bead.description?.trim()) {
|
|
17951
|
+
lines.push(bead.description.trim());
|
|
17952
|
+
}
|
|
17953
|
+
if (bead.notes?.trim()) {
|
|
17954
|
+
lines.push("", "## Notes", bead.notes.trim());
|
|
17955
|
+
}
|
|
17956
|
+
if (completedBlockers.length > 0) {
|
|
17957
|
+
lines.push("", "## Context from completed dependencies:");
|
|
17958
|
+
for (const blocker of completedBlockers) {
|
|
17959
|
+
lines.push("", `### ${blocker.title} (${blocker.id})`);
|
|
17960
|
+
if (blocker.description?.trim()) {
|
|
17961
|
+
lines.push(blocker.description.trim());
|
|
17962
|
+
}
|
|
17963
|
+
if (blocker.notes?.trim()) {
|
|
17964
|
+
lines.push("", blocker.notes.trim());
|
|
17965
|
+
}
|
|
17966
|
+
}
|
|
17967
|
+
}
|
|
17968
|
+
return lines.join(`
|
|
17969
|
+
`).trim();
|
|
17970
|
+
}
|
|
17895
17971
|
|
|
17896
17972
|
class BeadsClient {
|
|
17897
17973
|
available;
|
|
@@ -17917,12 +17993,66 @@ class BeadsClient {
|
|
|
17917
17993
|
const id = result.stdout?.trim();
|
|
17918
17994
|
return id || null;
|
|
17919
17995
|
}
|
|
17996
|
+
readBead(id) {
|
|
17997
|
+
if (!this.available || !id)
|
|
17998
|
+
return null;
|
|
17999
|
+
const result = spawnSync("bd", ["show", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
|
|
18000
|
+
if (result.error || result.status !== 0 || !result.stdout?.trim())
|
|
18001
|
+
return null;
|
|
18002
|
+
try {
|
|
18003
|
+
const parsed = JSON.parse(result.stdout);
|
|
18004
|
+
const bead = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
18005
|
+
if (!bead || typeof bead !== "object" || typeof bead.id !== "string" || typeof bead.title !== "string")
|
|
18006
|
+
return null;
|
|
18007
|
+
return bead;
|
|
18008
|
+
} catch (err) {
|
|
18009
|
+
console.warn(`[specialists] readBead: JSON parse failed for id=${id}: ${err}`);
|
|
18010
|
+
return null;
|
|
18011
|
+
}
|
|
18012
|
+
}
|
|
18013
|
+
getCompletedBlockers(id, depth = 1) {
|
|
18014
|
+
if (!this.available || !id || depth < 1)
|
|
18015
|
+
return [];
|
|
18016
|
+
const result = spawnSync("bd", ["dep", "list", id, "--json"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
|
|
18017
|
+
if (result.error || result.status !== 0 || !result.stdout?.trim())
|
|
18018
|
+
return [];
|
|
18019
|
+
let deps;
|
|
18020
|
+
try {
|
|
18021
|
+
deps = JSON.parse(result.stdout);
|
|
18022
|
+
if (!Array.isArray(deps))
|
|
18023
|
+
return [];
|
|
18024
|
+
} catch {
|
|
18025
|
+
return [];
|
|
18026
|
+
}
|
|
18027
|
+
const blockers = deps.filter((d) => d.dependency_type === "blocks" && d.status === "closed");
|
|
18028
|
+
const records = [];
|
|
18029
|
+
for (const dep of blockers) {
|
|
18030
|
+
const record3 = this.readBead(dep.id);
|
|
18031
|
+
if (record3) {
|
|
18032
|
+
records.push(record3);
|
|
18033
|
+
if (depth > 1) {
|
|
18034
|
+
records.push(...this.getCompletedBlockers(dep.id, depth - 1));
|
|
18035
|
+
}
|
|
18036
|
+
}
|
|
18037
|
+
}
|
|
18038
|
+
return records;
|
|
18039
|
+
}
|
|
18040
|
+
addDependency(trackingBeadId, inputBeadId) {
|
|
18041
|
+
if (!this.available || !trackingBeadId || !inputBeadId)
|
|
18042
|
+
return;
|
|
18043
|
+
spawnSync("bd", ["dep", "add", trackingBeadId, inputBeadId], { stdio: "ignore" });
|
|
18044
|
+
}
|
|
17920
18045
|
closeBead(id, status, durationMs, model) {
|
|
17921
18046
|
if (!this.available || !id)
|
|
17922
18047
|
return;
|
|
17923
18048
|
const reason = `${status}, ${Math.round(durationMs)}ms, ${model}`;
|
|
17924
18049
|
spawnSync("bd", ["close", id, "-r", reason], { stdio: "ignore" });
|
|
17925
18050
|
}
|
|
18051
|
+
updateBeadNotes(id, notes) {
|
|
18052
|
+
if (!this.available || !id || !notes)
|
|
18053
|
+
return;
|
|
18054
|
+
spawnSync("bd", ["update", id, "--notes", notes], { stdio: "ignore" });
|
|
18055
|
+
}
|
|
17926
18056
|
auditBead(id, toolName, model, exitCode) {
|
|
17927
18057
|
if (!this.available || !id)
|
|
17928
18058
|
return;
|
|
@@ -17954,14 +18084,16 @@ var init_beads = () => {};
|
|
|
17954
18084
|
// src/specialist/runner.ts
|
|
17955
18085
|
import { createHash } from "node:crypto";
|
|
17956
18086
|
import { writeFile } from "node:fs/promises";
|
|
17957
|
-
import { execSync } from "node:child_process";
|
|
17958
|
-
import {
|
|
17959
|
-
|
|
18087
|
+
import { execSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
18088
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
18089
|
+
import { basename, resolve } from "node:path";
|
|
18090
|
+
import { homedir as homedir2 } from "node:os";
|
|
18091
|
+
function runScript(run) {
|
|
17960
18092
|
try {
|
|
17961
|
-
const output = execSync(
|
|
17962
|
-
return { name: basename(
|
|
18093
|
+
const output = execSync(run, { encoding: "utf8", timeout: 30000 });
|
|
18094
|
+
return { name: basename(run.split(" ")[0]), output, exitCode: 0 };
|
|
17963
18095
|
} catch (e) {
|
|
17964
|
-
return { name: basename(
|
|
18096
|
+
return { name: basename(run.split(" ")[0]), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
|
|
17965
18097
|
}
|
|
17966
18098
|
}
|
|
17967
18099
|
function formatScriptOutput(results) {
|
|
@@ -17979,6 +18111,77 @@ ${r.output.trim()}
|
|
|
17979
18111
|
${blocks}
|
|
17980
18112
|
</pre_flight_context>`;
|
|
17981
18113
|
}
|
|
18114
|
+
function resolvePath(p) {
|
|
18115
|
+
return p.startsWith("~/") ? resolve(homedir2(), p.slice(2)) : resolve(p);
|
|
18116
|
+
}
|
|
18117
|
+
function commandExists(cmd) {
|
|
18118
|
+
const result = spawnSync2("which", [cmd], { stdio: "ignore" });
|
|
18119
|
+
return result.status === 0;
|
|
18120
|
+
}
|
|
18121
|
+
function validateShebang(filePath, errors5) {
|
|
18122
|
+
try {
|
|
18123
|
+
const head = readFileSync(filePath, "utf-8").slice(0, 120);
|
|
18124
|
+
if (!head.startsWith("#!"))
|
|
18125
|
+
return;
|
|
18126
|
+
const shebang = head.split(`
|
|
18127
|
+
`)[0].toLowerCase();
|
|
18128
|
+
const typos = [
|
|
18129
|
+
[/pytho[^n]|pyton|pyhon/, "python"],
|
|
18130
|
+
[/nod[^e]b/, "node"],
|
|
18131
|
+
[/bsh$|bas$/, "bash"],
|
|
18132
|
+
[/rub[^y]/, "ruby"]
|
|
18133
|
+
];
|
|
18134
|
+
for (const [pattern, correct] of typos) {
|
|
18135
|
+
if (pattern.test(shebang)) {
|
|
18136
|
+
errors5.push(` ✗ ${filePath}: shebang looks wrong — did you mean '${correct}'? (got: ${shebang})`);
|
|
18137
|
+
}
|
|
18138
|
+
}
|
|
18139
|
+
} catch {}
|
|
18140
|
+
}
|
|
18141
|
+
function validateBeforeRun(spec) {
|
|
18142
|
+
const errors5 = [];
|
|
18143
|
+
const warnings = [];
|
|
18144
|
+
for (const p of spec.specialist.skills?.paths ?? []) {
|
|
18145
|
+
const abs = resolvePath(p);
|
|
18146
|
+
if (!existsSync3(abs))
|
|
18147
|
+
warnings.push(` ⚠ skills.paths: file not found: ${p}`);
|
|
18148
|
+
}
|
|
18149
|
+
for (const script of spec.specialist.skills?.scripts ?? []) {
|
|
18150
|
+
const { run } = script;
|
|
18151
|
+
if (!run)
|
|
18152
|
+
continue;
|
|
18153
|
+
const isFilePath = run.startsWith("./") || run.startsWith("../") || run.startsWith("/") || run.startsWith("~/");
|
|
18154
|
+
if (isFilePath) {
|
|
18155
|
+
const abs = resolvePath(run);
|
|
18156
|
+
if (!existsSync3(abs)) {
|
|
18157
|
+
errors5.push(` ✗ skills.scripts: script not found: ${run}`);
|
|
18158
|
+
} else {
|
|
18159
|
+
validateShebang(abs, errors5);
|
|
18160
|
+
}
|
|
18161
|
+
} else {
|
|
18162
|
+
const binary = run.split(" ")[0];
|
|
18163
|
+
if (!commandExists(binary)) {
|
|
18164
|
+
errors5.push(` ✗ skills.scripts: command not found on PATH: ${binary}`);
|
|
18165
|
+
}
|
|
18166
|
+
}
|
|
18167
|
+
}
|
|
18168
|
+
for (const cmd of spec.specialist.capabilities?.external_commands ?? []) {
|
|
18169
|
+
if (!commandExists(cmd)) {
|
|
18170
|
+
errors5.push(` ✗ capabilities.external_commands: not found on PATH: ${cmd}`);
|
|
18171
|
+
}
|
|
18172
|
+
}
|
|
18173
|
+
if (warnings.length > 0) {
|
|
18174
|
+
process.stderr.write(`[specialists] pre-run warnings:
|
|
18175
|
+
${warnings.join(`
|
|
18176
|
+
`)}
|
|
18177
|
+
`);
|
|
18178
|
+
}
|
|
18179
|
+
if (errors5.length > 0) {
|
|
18180
|
+
throw new Error(`Specialist pre-run validation failed:
|
|
18181
|
+
${errors5.join(`
|
|
18182
|
+
`)}`);
|
|
18183
|
+
}
|
|
18184
|
+
}
|
|
17982
18185
|
|
|
17983
18186
|
class SpecialistRunner {
|
|
17984
18187
|
deps;
|
|
@@ -17987,12 +18190,12 @@ class SpecialistRunner {
|
|
|
17987
18190
|
this.deps = deps;
|
|
17988
18191
|
this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
|
|
17989
18192
|
}
|
|
17990
|
-
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated) {
|
|
18193
|
+
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
|
|
17991
18194
|
const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
|
|
17992
18195
|
const invocationId = crypto.randomUUID();
|
|
17993
18196
|
const start = Date.now();
|
|
17994
18197
|
const spec = await loader.get(options.name);
|
|
17995
|
-
const { metadata, execution, prompt,
|
|
18198
|
+
const { metadata, execution, prompt, output_file } = spec.specialist;
|
|
17996
18199
|
const primaryModel = options.backendOverride ?? execution.model;
|
|
17997
18200
|
const model = circuitBreaker.isAvailable(primaryModel) ? primaryModel : execution.fallback_model ?? primaryModel;
|
|
17998
18201
|
const fallbackUsed = model !== primaryModel;
|
|
@@ -18003,10 +18206,18 @@ class SpecialistRunner {
|
|
|
18003
18206
|
circuit_breaker_state: circuitBreaker.getState(model),
|
|
18004
18207
|
scope: "project"
|
|
18005
18208
|
});
|
|
18209
|
+
validateBeforeRun(spec);
|
|
18006
18210
|
const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
|
|
18007
|
-
const preResults = preScripts.map((s) => runScript(s.
|
|
18211
|
+
const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
|
|
18008
18212
|
const preScriptOutput = formatScriptOutput(preResults);
|
|
18009
|
-
const
|
|
18213
|
+
const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
|
|
18214
|
+
const variables = {
|
|
18215
|
+
prompt: options.prompt,
|
|
18216
|
+
cwd: process.cwd(),
|
|
18217
|
+
pre_script_output: preScriptOutput,
|
|
18218
|
+
...options.variables ?? {},
|
|
18219
|
+
...beadVariables
|
|
18220
|
+
};
|
|
18010
18221
|
const renderedTask = renderTemplate(prompt.task_template, variables);
|
|
18011
18222
|
const promptHash = createHash("sha256").update(renderedTask).digest("hex").slice(0, 16);
|
|
18012
18223
|
await hooks.emit("post_render", invocationId, metadata.name, metadata.version, {
|
|
@@ -18015,40 +18226,32 @@ class SpecialistRunner {
|
|
|
18015
18226
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
18016
18227
|
system_prompt_present: !!prompt.system
|
|
18017
18228
|
});
|
|
18018
|
-
const
|
|
18019
|
-
|
|
18020
|
-
if (prompt.skill_inherit)
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18025
|
-
|
|
18026
|
-
|
|
18027
|
-
|
|
18028
|
-
|
|
18029
|
-
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18033
|
-
|
|
18034
|
-
agentsMd += `
|
|
18035
|
-
|
|
18036
|
-
---
|
|
18037
|
-
# Skill: ${skillPath}
|
|
18038
|
-
|
|
18039
|
-
${skillContent}`;
|
|
18040
|
-
}
|
|
18041
|
-
if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
|
|
18042
|
-
agentsMd += `
|
|
18043
|
-
|
|
18044
|
-
---
|
|
18045
|
-
# Diagnostic Scripts
|
|
18046
|
-
You have access via Bash:
|
|
18047
|
-
`;
|
|
18048
|
-
for (const s of spec.specialist.capabilities.diagnostic_scripts) {
|
|
18049
|
-
agentsMd += `- \`${s}\`
|
|
18050
|
-
`;
|
|
18229
|
+
const agentsMd = prompt.system ?? "";
|
|
18230
|
+
const skillPaths = [];
|
|
18231
|
+
if (prompt.skill_inherit)
|
|
18232
|
+
skillPaths.push(prompt.skill_inherit);
|
|
18233
|
+
skillPaths.push(...spec.specialist.skills?.paths ?? []);
|
|
18234
|
+
if (skillPaths.length > 0 || preScripts.length > 0) {
|
|
18235
|
+
const line = "━".repeat(56);
|
|
18236
|
+
onProgress?.(`
|
|
18237
|
+
${line}
|
|
18238
|
+
◆ AUTO INJECTED
|
|
18239
|
+
`);
|
|
18240
|
+
if (skillPaths.length > 0) {
|
|
18241
|
+
onProgress?.(` skills (--skill):
|
|
18242
|
+
${skillPaths.map((p) => ` • ${p}`).join(`
|
|
18243
|
+
`)}
|
|
18244
|
+
`);
|
|
18051
18245
|
}
|
|
18246
|
+
if (preScripts.length > 0) {
|
|
18247
|
+
onProgress?.(` pre scripts/commands:
|
|
18248
|
+
${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
|
|
18249
|
+
`)}
|
|
18250
|
+
`);
|
|
18251
|
+
}
|
|
18252
|
+
onProgress?.(`${line}
|
|
18253
|
+
|
|
18254
|
+
`);
|
|
18052
18255
|
}
|
|
18053
18256
|
const permissionLevel = options.autonomyLevel ?? execution.permission_required;
|
|
18054
18257
|
await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
|
|
@@ -18059,19 +18262,28 @@ You have access via Bash:
|
|
|
18059
18262
|
});
|
|
18060
18263
|
const beadsIntegration = spec.specialist.beads_integration ?? "auto";
|
|
18061
18264
|
let beadId;
|
|
18062
|
-
|
|
18265
|
+
let ownsBead = false;
|
|
18266
|
+
if (options.inputBeadId) {
|
|
18267
|
+
beadId = options.inputBeadId;
|
|
18268
|
+
} else if (beadsClient && shouldCreateBead(beadsIntegration, execution.permission_required)) {
|
|
18063
18269
|
beadId = beadsClient.createBead(metadata.name) ?? undefined;
|
|
18064
|
-
if (beadId)
|
|
18270
|
+
if (beadId) {
|
|
18271
|
+
ownsBead = true;
|
|
18065
18272
|
onBeadCreated?.(beadId);
|
|
18273
|
+
}
|
|
18066
18274
|
}
|
|
18067
18275
|
let output;
|
|
18068
18276
|
let sessionBackend = model;
|
|
18069
18277
|
let session;
|
|
18278
|
+
let keepAliveActive = false;
|
|
18070
18279
|
try {
|
|
18071
18280
|
session = await this.sessionFactory({
|
|
18072
18281
|
model,
|
|
18073
18282
|
systemPrompt: agentsMd || undefined,
|
|
18283
|
+
skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
|
|
18284
|
+
thinkingLevel: execution.thinking_level,
|
|
18074
18285
|
permissionLevel,
|
|
18286
|
+
cwd: process.cwd(),
|
|
18075
18287
|
onToken: (delta) => onProgress?.(delta),
|
|
18076
18288
|
onThinking: (delta) => onProgress?.(`\uD83D\uDCAD ${delta}`),
|
|
18077
18289
|
onToolStart: (tool) => onProgress?.(`
|
|
@@ -18083,15 +18295,29 @@ You have access via Bash:
|
|
|
18083
18295
|
});
|
|
18084
18296
|
await session.start();
|
|
18085
18297
|
onKillRegistered?.(session.kill.bind(session));
|
|
18298
|
+
onSteerRegistered?.((msg) => session.steer(msg));
|
|
18086
18299
|
await session.prompt(renderedTask);
|
|
18087
18300
|
await session.waitForDone(execution.timeout_ms);
|
|
18088
18301
|
sessionBackend = session.meta.backend;
|
|
18089
18302
|
output = await session.getLastOutput();
|
|
18090
18303
|
sessionBackend = session.meta.backend;
|
|
18091
|
-
|
|
18304
|
+
if (options.keepAlive && onResumeReady) {
|
|
18305
|
+
keepAliveActive = true;
|
|
18306
|
+
const resumeFn = async (msg) => {
|
|
18307
|
+
await session.resume(msg, execution.timeout_ms);
|
|
18308
|
+
return session.getLastOutput();
|
|
18309
|
+
};
|
|
18310
|
+
const closeFn = async () => {
|
|
18311
|
+
keepAliveActive = false;
|
|
18312
|
+
await session.close();
|
|
18313
|
+
};
|
|
18314
|
+
onResumeReady(resumeFn, closeFn);
|
|
18315
|
+
} else {
|
|
18316
|
+
await session.close();
|
|
18317
|
+
}
|
|
18092
18318
|
const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
|
|
18093
18319
|
for (const script of postScripts)
|
|
18094
|
-
runScript(script.
|
|
18320
|
+
runScript(script.run);
|
|
18095
18321
|
circuitBreaker.recordSuccess(model);
|
|
18096
18322
|
} catch (err) {
|
|
18097
18323
|
const isCancelled = err instanceof SessionKilledError;
|
|
@@ -18100,7 +18326,8 @@ You have access via Bash:
|
|
|
18100
18326
|
}
|
|
18101
18327
|
const beadStatus = isCancelled ? "CANCELLED" : "ERROR";
|
|
18102
18328
|
if (beadId) {
|
|
18103
|
-
|
|
18329
|
+
if (ownsBead)
|
|
18330
|
+
beadsClient?.closeBead(beadId, beadStatus, Date.now() - start, model);
|
|
18104
18331
|
beadsClient?.auditBead(beadId, metadata.name, model, 1);
|
|
18105
18332
|
}
|
|
18106
18333
|
await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
|
|
@@ -18111,11 +18338,13 @@ You have access via Bash:
|
|
|
18111
18338
|
});
|
|
18112
18339
|
throw err;
|
|
18113
18340
|
} finally {
|
|
18114
|
-
|
|
18341
|
+
if (!keepAliveActive) {
|
|
18342
|
+
session?.kill();
|
|
18343
|
+
}
|
|
18115
18344
|
}
|
|
18116
18345
|
const durationMs = Date.now() - start;
|
|
18117
|
-
if (
|
|
18118
|
-
await writeFile(
|
|
18346
|
+
if (output_file) {
|
|
18347
|
+
await writeFile(output_file, output, "utf-8").catch(() => {});
|
|
18119
18348
|
}
|
|
18120
18349
|
await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
|
|
18121
18350
|
status: "COMPLETE",
|
|
@@ -18123,7 +18352,8 @@ You have access via Bash:
|
|
|
18123
18352
|
output_valid: true
|
|
18124
18353
|
});
|
|
18125
18354
|
if (beadId) {
|
|
18126
|
-
|
|
18355
|
+
if (ownsBead)
|
|
18356
|
+
beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
|
|
18127
18357
|
beadsClient?.auditBead(beadId, metadata.name, model, 0);
|
|
18128
18358
|
}
|
|
18129
18359
|
return {
|
|
@@ -18132,6 +18362,7 @@ You have access via Bash:
|
|
|
18132
18362
|
model,
|
|
18133
18363
|
durationMs,
|
|
18134
18364
|
specialistVersion: metadata.version,
|
|
18365
|
+
promptHash,
|
|
18135
18366
|
beadId
|
|
18136
18367
|
};
|
|
18137
18368
|
}
|
|
@@ -18147,7 +18378,7 @@ You have access via Bash:
|
|
|
18147
18378
|
model: "?",
|
|
18148
18379
|
specialistVersion
|
|
18149
18380
|
});
|
|
18150
|
-
this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
|
|
18381
|
+
this.run(options, (text) => registry2.appendOutput(jobId, text), (eventType) => registry2.setCurrentEvent(jobId, eventType), (meta) => registry2.setMeta(jobId, meta), (killFn) => registry2.setKillFn(jobId, killFn), (beadId) => registry2.setBeadId(jobId, beadId), (steerFn) => registry2.setSteerFn(jobId, steerFn), (resumeFn, closeFn) => registry2.setResumeFn(jobId, resumeFn, closeFn)).then((result) => registry2.complete(jobId, result)).catch((err) => registry2.fail(jobId, err));
|
|
18151
18382
|
return jobId;
|
|
18152
18383
|
}
|
|
18153
18384
|
}
|
|
@@ -18227,129 +18458,531 @@ class CircuitBreaker {
|
|
|
18227
18458
|
}
|
|
18228
18459
|
}
|
|
18229
18460
|
|
|
18230
|
-
// src/
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18461
|
+
// src/specialist/timeline-events.ts
|
|
18462
|
+
function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
18463
|
+
const t = Date.now();
|
|
18464
|
+
switch (callbackEvent) {
|
|
18465
|
+
case "thinking":
|
|
18466
|
+
return { t, type: TIMELINE_EVENT_TYPES.THINKING };
|
|
18467
|
+
case "toolcall":
|
|
18468
|
+
return {
|
|
18469
|
+
t,
|
|
18470
|
+
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18471
|
+
tool: context.tool ?? "unknown",
|
|
18472
|
+
phase: "start",
|
|
18473
|
+
tool_call_id: context.toolCallId
|
|
18474
|
+
};
|
|
18475
|
+
case "tool_execution_end":
|
|
18476
|
+
return {
|
|
18477
|
+
t,
|
|
18478
|
+
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18479
|
+
tool: context.tool ?? "unknown",
|
|
18480
|
+
phase: "end",
|
|
18481
|
+
tool_call_id: context.toolCallId,
|
|
18482
|
+
is_error: context.isError
|
|
18483
|
+
};
|
|
18484
|
+
case "text":
|
|
18485
|
+
return { t, type: TIMELINE_EVENT_TYPES.TEXT };
|
|
18486
|
+
case "agent_end":
|
|
18487
|
+
case "message_done":
|
|
18488
|
+
case "done":
|
|
18489
|
+
return null;
|
|
18490
|
+
default:
|
|
18491
|
+
return null;
|
|
18492
|
+
}
|
|
18241
18493
|
}
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
}
|
|
18249
|
-
import { createRequire as createRequire2 } from "node:module";
|
|
18250
|
-
async function run2() {
|
|
18251
|
-
const req = createRequire2(import.meta.url);
|
|
18252
|
-
const pkg = req("../package.json");
|
|
18253
|
-
console.log(`${pkg.name} v${pkg.version}`);
|
|
18494
|
+
function createRunStartEvent(specialist, beadId) {
|
|
18495
|
+
return {
|
|
18496
|
+
t: Date.now(),
|
|
18497
|
+
type: TIMELINE_EVENT_TYPES.RUN_START,
|
|
18498
|
+
specialist,
|
|
18499
|
+
bead_id: beadId
|
|
18500
|
+
};
|
|
18254
18501
|
}
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
ArgParseError: () => ArgParseError
|
|
18263
|
-
});
|
|
18264
|
-
function parseArgs(argv) {
|
|
18265
|
-
const result = {};
|
|
18266
|
-
for (let i = 0;i < argv.length; i++) {
|
|
18267
|
-
const token = argv[i];
|
|
18268
|
-
if (token === "--category") {
|
|
18269
|
-
const value = argv[++i];
|
|
18270
|
-
if (!value || value.startsWith("--")) {
|
|
18271
|
-
throw new ArgParseError("--category requires a value");
|
|
18272
|
-
}
|
|
18273
|
-
result.category = value;
|
|
18274
|
-
continue;
|
|
18275
|
-
}
|
|
18276
|
-
if (token === "--scope") {
|
|
18277
|
-
const value = argv[++i];
|
|
18278
|
-
if (value !== "project" && value !== "user") {
|
|
18279
|
-
throw new ArgParseError(`--scope must be "project" or "user", got: "${value ?? ""}"`);
|
|
18280
|
-
}
|
|
18281
|
-
result.scope = value;
|
|
18282
|
-
continue;
|
|
18283
|
-
}
|
|
18284
|
-
if (token === "--json") {
|
|
18285
|
-
result.json = true;
|
|
18286
|
-
continue;
|
|
18287
|
-
}
|
|
18288
|
-
}
|
|
18289
|
-
return result;
|
|
18502
|
+
function createMetaEvent(model, backend) {
|
|
18503
|
+
return {
|
|
18504
|
+
t: Date.now(),
|
|
18505
|
+
type: TIMELINE_EVENT_TYPES.META,
|
|
18506
|
+
model,
|
|
18507
|
+
backend
|
|
18508
|
+
};
|
|
18290
18509
|
}
|
|
18291
|
-
|
|
18292
|
-
|
|
18510
|
+
function createRunCompleteEvent(status, elapsed_s, options) {
|
|
18511
|
+
return {
|
|
18512
|
+
t: Date.now(),
|
|
18513
|
+
type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
|
|
18514
|
+
status,
|
|
18515
|
+
elapsed_s,
|
|
18516
|
+
...options
|
|
18517
|
+
};
|
|
18518
|
+
}
|
|
18519
|
+
function parseTimelineEvent(line) {
|
|
18293
18520
|
try {
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18521
|
+
const parsed = JSON.parse(line);
|
|
18522
|
+
if (!parsed || typeof parsed !== "object")
|
|
18523
|
+
return null;
|
|
18524
|
+
if (typeof parsed.t !== "number")
|
|
18525
|
+
return null;
|
|
18526
|
+
if (typeof parsed.type !== "string")
|
|
18527
|
+
return null;
|
|
18528
|
+
if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
|
|
18529
|
+
return {
|
|
18530
|
+
t: parsed.t,
|
|
18531
|
+
type: TIMELINE_EVENT_TYPES.DONE,
|
|
18532
|
+
elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
|
|
18533
|
+
};
|
|
18299
18534
|
}
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
return;
|
|
18314
|
-
}
|
|
18315
|
-
const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
|
|
18316
|
-
console.log(`
|
|
18317
|
-
${bold(`Specialists (${specialists.length})`)}
|
|
18318
|
-
`);
|
|
18319
|
-
for (const s of specialists) {
|
|
18320
|
-
const name = cyan(s.name.padEnd(nameWidth));
|
|
18321
|
-
const scopeTag = yellow(`[${s.scope}]`);
|
|
18322
|
-
const model = dim(s.model);
|
|
18323
|
-
const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
|
|
18324
|
-
console.log(` ${name} ${scopeTag} ${model}`);
|
|
18325
|
-
console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
|
|
18326
|
-
console.log();
|
|
18535
|
+
if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
|
|
18536
|
+
return {
|
|
18537
|
+
t: parsed.t,
|
|
18538
|
+
type: TIMELINE_EVENT_TYPES.AGENT_END,
|
|
18539
|
+
elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
|
|
18540
|
+
};
|
|
18541
|
+
}
|
|
18542
|
+
const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
|
|
18543
|
+
if (!knownTypes.includes(parsed.type))
|
|
18544
|
+
return null;
|
|
18545
|
+
return parsed;
|
|
18546
|
+
} catch {
|
|
18547
|
+
return null;
|
|
18327
18548
|
}
|
|
18328
18549
|
}
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18550
|
+
function isRunCompleteEvent(event) {
|
|
18551
|
+
return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
|
|
18552
|
+
}
|
|
18553
|
+
function compareTimelineEvents(a, b) {
|
|
18554
|
+
return a.t - b.t;
|
|
18555
|
+
}
|
|
18556
|
+
var TIMELINE_EVENT_TYPES;
|
|
18557
|
+
var init_timeline_events = __esm(() => {
|
|
18558
|
+
TIMELINE_EVENT_TYPES = {
|
|
18559
|
+
RUN_START: "run_start",
|
|
18560
|
+
META: "meta",
|
|
18561
|
+
THINKING: "thinking",
|
|
18562
|
+
TOOL: "tool",
|
|
18563
|
+
TEXT: "text",
|
|
18564
|
+
RUN_COMPLETE: "run_complete",
|
|
18565
|
+
DONE: "done",
|
|
18566
|
+
AGENT_END: "agent_end"
|
|
18337
18567
|
};
|
|
18338
18568
|
});
|
|
18339
18569
|
|
|
18340
|
-
// src/
|
|
18341
|
-
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18347
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18570
|
+
// src/specialist/supervisor.ts
|
|
18571
|
+
import {
|
|
18572
|
+
closeSync,
|
|
18573
|
+
existsSync as existsSync4,
|
|
18574
|
+
mkdirSync,
|
|
18575
|
+
openSync,
|
|
18576
|
+
readdirSync,
|
|
18577
|
+
readFileSync as readFileSync2,
|
|
18578
|
+
renameSync,
|
|
18579
|
+
rmSync,
|
|
18580
|
+
statSync,
|
|
18581
|
+
writeFileSync,
|
|
18582
|
+
writeSync
|
|
18583
|
+
} from "node:fs";
|
|
18584
|
+
import { join as join3 } from "node:path";
|
|
18585
|
+
import { createInterface } from "node:readline";
|
|
18586
|
+
import { createReadStream } from "node:fs";
|
|
18587
|
+
import { spawnSync as spawnSync3, execFileSync } from "node:child_process";
|
|
18588
|
+
function getCurrentGitSha() {
|
|
18589
|
+
const result = spawnSync3("git", ["rev-parse", "HEAD"], {
|
|
18590
|
+
encoding: "utf-8",
|
|
18591
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
18592
|
+
});
|
|
18593
|
+
if (result.status !== 0)
|
|
18594
|
+
return;
|
|
18595
|
+
const sha = result.stdout?.trim();
|
|
18596
|
+
return sha || undefined;
|
|
18597
|
+
}
|
|
18598
|
+
function formatBeadNotes(result) {
|
|
18599
|
+
const metadata = [
|
|
18600
|
+
`prompt_hash=${result.promptHash}`,
|
|
18601
|
+
`git_sha=${getCurrentGitSha() ?? "unknown"}`,
|
|
18602
|
+
`elapsed_ms=${Math.round(result.durationMs)}`,
|
|
18603
|
+
`model=${result.model}`,
|
|
18604
|
+
`backend=${result.backend}`
|
|
18605
|
+
].join(`
|
|
18606
|
+
`);
|
|
18607
|
+
return `${result.output}
|
|
18608
|
+
|
|
18609
|
+
---
|
|
18610
|
+
${metadata}`;
|
|
18611
|
+
}
|
|
18612
|
+
|
|
18613
|
+
class Supervisor {
|
|
18614
|
+
opts;
|
|
18615
|
+
constructor(opts) {
|
|
18616
|
+
this.opts = opts;
|
|
18617
|
+
}
|
|
18618
|
+
jobDir(id) {
|
|
18619
|
+
return join3(this.opts.jobsDir, id);
|
|
18620
|
+
}
|
|
18621
|
+
statusPath(id) {
|
|
18622
|
+
return join3(this.jobDir(id), "status.json");
|
|
18623
|
+
}
|
|
18624
|
+
resultPath(id) {
|
|
18625
|
+
return join3(this.jobDir(id), "result.txt");
|
|
18626
|
+
}
|
|
18627
|
+
eventsPath(id) {
|
|
18628
|
+
return join3(this.jobDir(id), "events.jsonl");
|
|
18629
|
+
}
|
|
18630
|
+
readyDir() {
|
|
18631
|
+
return join3(this.opts.jobsDir, "..", "ready");
|
|
18632
|
+
}
|
|
18633
|
+
readStatus(id) {
|
|
18634
|
+
const path = this.statusPath(id);
|
|
18635
|
+
if (!existsSync4(path))
|
|
18636
|
+
return null;
|
|
18637
|
+
try {
|
|
18638
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
18639
|
+
} catch {
|
|
18640
|
+
return null;
|
|
18641
|
+
}
|
|
18642
|
+
}
|
|
18643
|
+
listJobs() {
|
|
18644
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18645
|
+
return [];
|
|
18646
|
+
const jobs = [];
|
|
18647
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18648
|
+
const path = join3(this.opts.jobsDir, entry, "status.json");
|
|
18649
|
+
if (!existsSync4(path))
|
|
18650
|
+
continue;
|
|
18651
|
+
try {
|
|
18652
|
+
jobs.push(JSON.parse(readFileSync2(path, "utf-8")));
|
|
18653
|
+
} catch {}
|
|
18654
|
+
}
|
|
18655
|
+
return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
18656
|
+
}
|
|
18657
|
+
writeStatusFile(id, data) {
|
|
18658
|
+
const path = this.statusPath(id);
|
|
18659
|
+
const tmp = path + ".tmp";
|
|
18660
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
18661
|
+
renameSync(tmp, path);
|
|
18662
|
+
}
|
|
18663
|
+
updateStatus(id, updates) {
|
|
18664
|
+
const current = this.readStatus(id);
|
|
18665
|
+
if (!current)
|
|
18666
|
+
return;
|
|
18667
|
+
this.writeStatusFile(id, { ...current, ...updates });
|
|
18668
|
+
}
|
|
18669
|
+
gc() {
|
|
18670
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18671
|
+
return;
|
|
18672
|
+
const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
|
|
18673
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18674
|
+
const dir = join3(this.opts.jobsDir, entry);
|
|
18675
|
+
try {
|
|
18676
|
+
const stat2 = statSync(dir);
|
|
18677
|
+
if (!stat2.isDirectory())
|
|
18678
|
+
continue;
|
|
18679
|
+
if (stat2.mtimeMs < cutoff)
|
|
18680
|
+
rmSync(dir, { recursive: true, force: true });
|
|
18681
|
+
} catch {}
|
|
18682
|
+
}
|
|
18683
|
+
}
|
|
18684
|
+
crashRecovery() {
|
|
18685
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18686
|
+
return;
|
|
18687
|
+
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18688
|
+
const statusPath = join3(this.opts.jobsDir, entry, "status.json");
|
|
18689
|
+
if (!existsSync4(statusPath))
|
|
18690
|
+
continue;
|
|
18691
|
+
try {
|
|
18692
|
+
const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
|
|
18693
|
+
if (s.status !== "running" && s.status !== "starting")
|
|
18694
|
+
continue;
|
|
18695
|
+
if (!s.pid)
|
|
18696
|
+
continue;
|
|
18697
|
+
try {
|
|
18698
|
+
process.kill(s.pid, 0);
|
|
18699
|
+
} catch {
|
|
18700
|
+
const tmp = statusPath + ".tmp";
|
|
18701
|
+
const updated = { ...s, status: "error", error: "Process crashed or was killed" };
|
|
18702
|
+
writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
18703
|
+
renameSync(tmp, statusPath);
|
|
18704
|
+
}
|
|
18705
|
+
} catch {}
|
|
18706
|
+
}
|
|
18707
|
+
}
|
|
18708
|
+
async run() {
|
|
18709
|
+
const { runner, runOptions, jobsDir } = this.opts;
|
|
18710
|
+
this.gc();
|
|
18711
|
+
this.crashRecovery();
|
|
18712
|
+
const id = crypto.randomUUID().slice(0, 6);
|
|
18713
|
+
const dir = this.jobDir(id);
|
|
18714
|
+
const startedAtMs = Date.now();
|
|
18715
|
+
mkdirSync(dir, { recursive: true });
|
|
18716
|
+
mkdirSync(this.readyDir(), { recursive: true });
|
|
18717
|
+
const initialStatus = {
|
|
18718
|
+
id,
|
|
18719
|
+
specialist: runOptions.name,
|
|
18720
|
+
status: "starting",
|
|
18721
|
+
started_at_ms: startedAtMs,
|
|
18722
|
+
pid: process.pid
|
|
18723
|
+
};
|
|
18724
|
+
this.writeStatusFile(id, initialStatus);
|
|
18725
|
+
const eventsFd = openSync(this.eventsPath(id), "a");
|
|
18726
|
+
const appendTimelineEvent = (event) => {
|
|
18727
|
+
try {
|
|
18728
|
+
writeSync(eventsFd, JSON.stringify(event) + `
|
|
18729
|
+
`);
|
|
18730
|
+
} catch {}
|
|
18731
|
+
};
|
|
18732
|
+
appendTimelineEvent(createRunStartEvent(runOptions.name));
|
|
18733
|
+
const fifoPath = join3(dir, "steer.pipe");
|
|
18734
|
+
try {
|
|
18735
|
+
execFileSync("mkfifo", [fifoPath]);
|
|
18736
|
+
this.updateStatus(id, { fifo_path: fifoPath });
|
|
18737
|
+
} catch {}
|
|
18738
|
+
let textLogged = false;
|
|
18739
|
+
let currentTool = "";
|
|
18740
|
+
let currentToolCallId = "";
|
|
18741
|
+
let killFn;
|
|
18742
|
+
let steerFn;
|
|
18743
|
+
let resumeFn;
|
|
18744
|
+
let closeFn;
|
|
18745
|
+
const sigtermHandler = () => killFn?.();
|
|
18746
|
+
process.once("SIGTERM", sigtermHandler);
|
|
18747
|
+
try {
|
|
18748
|
+
const result = await runner.run(runOptions, (delta) => {
|
|
18749
|
+
const toolMatch = delta.match(/⚙ (.+?)…/);
|
|
18750
|
+
if (toolMatch) {
|
|
18751
|
+
currentTool = toolMatch[1];
|
|
18752
|
+
this.updateStatus(id, { current_tool: currentTool });
|
|
18753
|
+
}
|
|
18754
|
+
}, (eventType) => {
|
|
18755
|
+
const now = Date.now();
|
|
18756
|
+
this.updateStatus(id, {
|
|
18757
|
+
status: "running",
|
|
18758
|
+
current_event: eventType,
|
|
18759
|
+
last_event_at_ms: now,
|
|
18760
|
+
elapsed_s: Math.round((now - startedAtMs) / 1000)
|
|
18761
|
+
});
|
|
18762
|
+
const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
|
|
18763
|
+
tool: currentTool,
|
|
18764
|
+
toolCallId: currentToolCallId || undefined
|
|
18765
|
+
});
|
|
18766
|
+
if (timelineEvent) {
|
|
18767
|
+
appendTimelineEvent(timelineEvent);
|
|
18768
|
+
} else if (eventType === "text" && !textLogged) {
|
|
18769
|
+
textLogged = true;
|
|
18770
|
+
appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
|
|
18771
|
+
}
|
|
18772
|
+
}, (meta) => {
|
|
18773
|
+
this.updateStatus(id, { model: meta.model, backend: meta.backend });
|
|
18774
|
+
appendTimelineEvent(createMetaEvent(meta.model, meta.backend));
|
|
18775
|
+
}, (fn) => {
|
|
18776
|
+
killFn = fn;
|
|
18777
|
+
}, (beadId) => {
|
|
18778
|
+
this.updateStatus(id, { bead_id: beadId });
|
|
18779
|
+
}, (fn) => {
|
|
18780
|
+
steerFn = fn;
|
|
18781
|
+
if (existsSync4(fifoPath)) {
|
|
18782
|
+
const rl = createInterface({ input: createReadStream(fifoPath, { flags: "r+" }) });
|
|
18783
|
+
rl.on("line", (line) => {
|
|
18784
|
+
try {
|
|
18785
|
+
const parsed = JSON.parse(line);
|
|
18786
|
+
if (parsed?.type === "steer" && typeof parsed.message === "string") {
|
|
18787
|
+
steerFn?.(parsed.message).catch(() => {});
|
|
18788
|
+
} else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
|
|
18789
|
+
if (resumeFn) {
|
|
18790
|
+
this.updateStatus(id, { status: "running", current_event: "starting" });
|
|
18791
|
+
resumeFn(parsed.message).then((output) => {
|
|
18792
|
+
writeFileSync(this.resultPath(id), output, "utf-8");
|
|
18793
|
+
this.updateStatus(id, {
|
|
18794
|
+
status: "waiting",
|
|
18795
|
+
current_event: "waiting",
|
|
18796
|
+
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
18797
|
+
last_event_at_ms: Date.now()
|
|
18798
|
+
});
|
|
18799
|
+
}).catch((err) => {
|
|
18800
|
+
this.updateStatus(id, { status: "error", error: err?.message ?? String(err) });
|
|
18801
|
+
});
|
|
18802
|
+
}
|
|
18803
|
+
} else if (parsed?.type === "close") {
|
|
18804
|
+
closeFn?.().catch(() => {});
|
|
18805
|
+
}
|
|
18806
|
+
} catch {}
|
|
18807
|
+
});
|
|
18808
|
+
rl.on("error", () => {});
|
|
18809
|
+
}
|
|
18810
|
+
}, (rFn, cFn) => {
|
|
18811
|
+
resumeFn = rFn;
|
|
18812
|
+
closeFn = cFn;
|
|
18813
|
+
this.updateStatus(id, { status: "waiting", current_event: "waiting" });
|
|
18814
|
+
});
|
|
18815
|
+
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18816
|
+
writeFileSync(this.resultPath(id), result.output, "utf-8");
|
|
18817
|
+
if (result.beadId) {
|
|
18818
|
+
this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
|
|
18819
|
+
}
|
|
18820
|
+
this.updateStatus(id, {
|
|
18821
|
+
status: "done",
|
|
18822
|
+
elapsed_s: elapsed,
|
|
18823
|
+
last_event_at_ms: Date.now(),
|
|
18824
|
+
model: result.model,
|
|
18825
|
+
backend: result.backend,
|
|
18826
|
+
bead_id: result.beadId
|
|
18827
|
+
});
|
|
18828
|
+
appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
|
|
18829
|
+
model: result.model,
|
|
18830
|
+
backend: result.backend,
|
|
18831
|
+
bead_id: result.beadId
|
|
18832
|
+
}));
|
|
18833
|
+
writeFileSync(join3(this.readyDir(), id), "", "utf-8");
|
|
18834
|
+
return id;
|
|
18835
|
+
} catch (err) {
|
|
18836
|
+
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18837
|
+
const errorMsg = err?.message ?? String(err);
|
|
18838
|
+
this.updateStatus(id, {
|
|
18839
|
+
status: "error",
|
|
18840
|
+
elapsed_s: elapsed,
|
|
18841
|
+
error: errorMsg
|
|
18842
|
+
});
|
|
18843
|
+
appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
|
|
18844
|
+
error: errorMsg
|
|
18845
|
+
}));
|
|
18846
|
+
throw err;
|
|
18847
|
+
} finally {
|
|
18848
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
18849
|
+
closeSync(eventsFd);
|
|
18850
|
+
try {
|
|
18851
|
+
if (existsSync4(fifoPath))
|
|
18852
|
+
rmSync(fifoPath);
|
|
18853
|
+
} catch {}
|
|
18854
|
+
}
|
|
18855
|
+
}
|
|
18856
|
+
}
|
|
18857
|
+
var JOB_TTL_DAYS;
|
|
18858
|
+
var init_supervisor = __esm(() => {
|
|
18859
|
+
init_timeline_events();
|
|
18860
|
+
JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
|
|
18861
|
+
});
|
|
18862
|
+
|
|
18863
|
+
// src/cli/install.ts
|
|
18864
|
+
var exports_install = {};
|
|
18865
|
+
__export(exports_install, {
|
|
18866
|
+
run: () => run
|
|
18867
|
+
});
|
|
18868
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
18869
|
+
import { fileURLToPath } from "node:url";
|
|
18870
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
18871
|
+
async function run() {
|
|
18872
|
+
const installerPath = join8(dirname2(fileURLToPath(import.meta.url)), "..", "bin", "install.js");
|
|
18873
|
+
execFileSync2(process.execPath, [installerPath], { stdio: "inherit" });
|
|
18874
|
+
}
|
|
18875
|
+
var init_install = () => {};
|
|
18876
|
+
|
|
18877
|
+
// src/cli/version.ts
|
|
18878
|
+
var exports_version = {};
|
|
18879
|
+
__export(exports_version, {
|
|
18880
|
+
run: () => run2
|
|
18881
|
+
});
|
|
18882
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
18883
|
+
async function run2() {
|
|
18884
|
+
const req = createRequire2(import.meta.url);
|
|
18885
|
+
const pkg = req("../package.json");
|
|
18886
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
18887
|
+
}
|
|
18888
|
+
var init_version = () => {};
|
|
18889
|
+
|
|
18890
|
+
// src/cli/list.ts
|
|
18891
|
+
var exports_list = {};
|
|
18892
|
+
__export(exports_list, {
|
|
18893
|
+
run: () => run3,
|
|
18894
|
+
parseArgs: () => parseArgs,
|
|
18895
|
+
ArgParseError: () => ArgParseError
|
|
18896
|
+
});
|
|
18897
|
+
function parseArgs(argv) {
|
|
18898
|
+
const result = {};
|
|
18899
|
+
for (let i = 0;i < argv.length; i++) {
|
|
18900
|
+
const token = argv[i];
|
|
18901
|
+
if (token === "--category") {
|
|
18902
|
+
const value = argv[++i];
|
|
18903
|
+
if (!value || value.startsWith("--")) {
|
|
18904
|
+
throw new ArgParseError("--category requires a value");
|
|
18905
|
+
}
|
|
18906
|
+
result.category = value;
|
|
18907
|
+
continue;
|
|
18908
|
+
}
|
|
18909
|
+
if (token === "--scope") {
|
|
18910
|
+
const value = argv[++i];
|
|
18911
|
+
if (value !== "default" && value !== "user") {
|
|
18912
|
+
throw new ArgParseError(`--scope must be "default" or "user", got: "${value ?? ""}"`);
|
|
18913
|
+
}
|
|
18914
|
+
result.scope = value;
|
|
18915
|
+
continue;
|
|
18916
|
+
}
|
|
18917
|
+
if (token === "--json") {
|
|
18918
|
+
result.json = true;
|
|
18919
|
+
continue;
|
|
18920
|
+
}
|
|
18921
|
+
}
|
|
18922
|
+
return result;
|
|
18923
|
+
}
|
|
18924
|
+
async function run3() {
|
|
18925
|
+
let args;
|
|
18926
|
+
try {
|
|
18927
|
+
args = parseArgs(process.argv.slice(3));
|
|
18928
|
+
} catch (err) {
|
|
18929
|
+
if (err instanceof ArgParseError) {
|
|
18930
|
+
console.error(`Error: ${err.message}`);
|
|
18931
|
+
process.exit(1);
|
|
18932
|
+
}
|
|
18933
|
+
throw err;
|
|
18934
|
+
}
|
|
18935
|
+
const loader = new SpecialistLoader;
|
|
18936
|
+
let specialists = await loader.list(args.category);
|
|
18937
|
+
if (args.scope) {
|
|
18938
|
+
specialists = specialists.filter((s) => s.scope === args.scope);
|
|
18939
|
+
}
|
|
18940
|
+
if (args.json) {
|
|
18941
|
+
console.log(JSON.stringify(specialists, null, 2));
|
|
18942
|
+
return;
|
|
18943
|
+
}
|
|
18944
|
+
if (specialists.length === 0) {
|
|
18945
|
+
console.log("No specialists found.");
|
|
18946
|
+
return;
|
|
18947
|
+
}
|
|
18948
|
+
const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
|
|
18949
|
+
console.log(`
|
|
18950
|
+
${bold(`Specialists (${specialists.length})`)}
|
|
18951
|
+
`);
|
|
18952
|
+
for (const s of specialists) {
|
|
18953
|
+
const name = cyan(s.name.padEnd(nameWidth));
|
|
18954
|
+
const scopeTag = s.scope === "default" ? green("[default]") : yellow("[user]");
|
|
18955
|
+
const model = dim(s.model);
|
|
18956
|
+
const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
|
|
18957
|
+
console.log(` ${name} ${scopeTag} ${model}`);
|
|
18958
|
+
console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
|
|
18959
|
+
console.log();
|
|
18960
|
+
}
|
|
18961
|
+
}
|
|
18962
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, green = (s) => `\x1B[32m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
|
|
18963
|
+
var init_list = __esm(() => {
|
|
18964
|
+
init_loader();
|
|
18965
|
+
ArgParseError = class ArgParseError extends Error {
|
|
18966
|
+
constructor(message) {
|
|
18967
|
+
super(message);
|
|
18968
|
+
this.name = "ArgParseError";
|
|
18969
|
+
}
|
|
18970
|
+
};
|
|
18971
|
+
});
|
|
18972
|
+
|
|
18973
|
+
// src/cli/models.ts
|
|
18974
|
+
var exports_models = {};
|
|
18975
|
+
__export(exports_models, {
|
|
18976
|
+
run: () => run4
|
|
18977
|
+
});
|
|
18978
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
18979
|
+
function parsePiModels() {
|
|
18980
|
+
const r = spawnSync5("pi", ["--list-models"], {
|
|
18981
|
+
encoding: "utf8",
|
|
18982
|
+
stdio: "pipe",
|
|
18983
|
+
timeout: 8000
|
|
18984
|
+
});
|
|
18985
|
+
if (r.status !== 0 || r.error)
|
|
18353
18986
|
return null;
|
|
18354
18987
|
return r.stdout.split(`
|
|
18355
18988
|
`).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
@@ -18391,7 +19024,7 @@ async function run4() {
|
|
|
18391
19024
|
}
|
|
18392
19025
|
const allModels = parsePiModels();
|
|
18393
19026
|
if (!allModels) {
|
|
18394
|
-
console.error("pi not found or failed —
|
|
19027
|
+
console.error("pi not found or failed — install and configure pi first");
|
|
18395
19028
|
process.exit(1);
|
|
18396
19029
|
}
|
|
18397
19030
|
let models = allModels;
|
|
@@ -18422,7 +19055,7 @@ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
|
|
|
18422
19055
|
const key = `${m.provider}/${m.model}`;
|
|
18423
19056
|
const inUse = usedBy.get(key);
|
|
18424
19057
|
const flags = [
|
|
18425
|
-
m.thinking ?
|
|
19058
|
+
m.thinking ? green2("thinking") : dim2("·"),
|
|
18426
19059
|
m.images ? dim2("images") : ""
|
|
18427
19060
|
].filter(Boolean).join(" ");
|
|
18428
19061
|
const ctx = dim2(`ctx ${m.context}`);
|
|
@@ -18437,7 +19070,7 @@ ${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
|
|
|
18437
19070
|
console.log();
|
|
18438
19071
|
}
|
|
18439
19072
|
}
|
|
18440
|
-
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`,
|
|
19073
|
+
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`, dim2 = (s) => `\x1B[2m${s}\x1B[0m`, cyan2 = (s) => `\x1B[36m${s}\x1B[0m`, yellow2 = (s) => `\x1B[33m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
18441
19074
|
var init_models = __esm(() => {
|
|
18442
19075
|
init_loader();
|
|
18443
19076
|
});
|
|
@@ -18447,77 +19080,287 @@ var exports_init = {};
|
|
|
18447
19080
|
__export(exports_init, {
|
|
18448
19081
|
run: () => run5
|
|
18449
19082
|
});
|
|
18450
|
-
import { existsSync as
|
|
18451
|
-
import { join as
|
|
19083
|
+
import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
19084
|
+
import { join as join9 } from "node:path";
|
|
19085
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18452
19086
|
function ok(msg) {
|
|
18453
|
-
console.log(` ${
|
|
19087
|
+
console.log(` ${green3("✓")} ${msg}`);
|
|
19088
|
+
}
|
|
19089
|
+
function skip(msg) {
|
|
19090
|
+
console.log(` ${yellow3("○")} ${msg}`);
|
|
19091
|
+
}
|
|
19092
|
+
function loadJson(path, fallback) {
|
|
19093
|
+
if (!existsSync6(path))
|
|
19094
|
+
return structuredClone(fallback);
|
|
19095
|
+
try {
|
|
19096
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
19097
|
+
} catch {
|
|
19098
|
+
return structuredClone(fallback);
|
|
19099
|
+
}
|
|
19100
|
+
}
|
|
19101
|
+
function saveJson(path, value) {
|
|
19102
|
+
writeFileSync4(path, JSON.stringify(value, null, 2) + `
|
|
19103
|
+
`, "utf-8");
|
|
19104
|
+
}
|
|
19105
|
+
function resolvePackagePath(relativePath) {
|
|
19106
|
+
const configPath = `config/${relativePath}`;
|
|
19107
|
+
let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
|
|
19108
|
+
if (existsSync6(resolved))
|
|
19109
|
+
return resolved;
|
|
19110
|
+
resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
|
|
19111
|
+
if (existsSync6(resolved))
|
|
19112
|
+
return resolved;
|
|
19113
|
+
return null;
|
|
19114
|
+
}
|
|
19115
|
+
function copyCanonicalSpecialists(cwd) {
|
|
19116
|
+
const sourceDir = resolvePackagePath("specialists");
|
|
19117
|
+
if (!sourceDir) {
|
|
19118
|
+
skip("no canonical specialists found in package");
|
|
19119
|
+
return;
|
|
19120
|
+
}
|
|
19121
|
+
const targetDir = join9(cwd, ".specialists", "default", "specialists");
|
|
19122
|
+
const files = readdirSync2(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
|
|
19123
|
+
if (files.length === 0) {
|
|
19124
|
+
skip("no specialist files found in package");
|
|
19125
|
+
return;
|
|
19126
|
+
}
|
|
19127
|
+
if (!existsSync6(targetDir)) {
|
|
19128
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
19129
|
+
}
|
|
19130
|
+
let copied = 0;
|
|
19131
|
+
let skipped = 0;
|
|
19132
|
+
for (const file of files) {
|
|
19133
|
+
const src = join9(sourceDir, file);
|
|
19134
|
+
const dest = join9(targetDir, file);
|
|
19135
|
+
if (existsSync6(dest)) {
|
|
19136
|
+
skipped++;
|
|
19137
|
+
} else {
|
|
19138
|
+
copyFileSync(src, dest);
|
|
19139
|
+
copied++;
|
|
19140
|
+
}
|
|
19141
|
+
}
|
|
19142
|
+
if (copied > 0) {
|
|
19143
|
+
ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/specialists/`);
|
|
19144
|
+
}
|
|
19145
|
+
if (skipped > 0) {
|
|
19146
|
+
skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
19147
|
+
}
|
|
19148
|
+
}
|
|
19149
|
+
function copyCanonicalHooks(cwd) {
|
|
19150
|
+
const sourceDir = resolvePackagePath("hooks");
|
|
19151
|
+
if (!sourceDir) {
|
|
19152
|
+
skip("no canonical hooks found in package");
|
|
19153
|
+
return;
|
|
19154
|
+
}
|
|
19155
|
+
const targetDir = join9(cwd, ".specialists", "default", "hooks");
|
|
19156
|
+
const hooks = readdirSync2(sourceDir).filter((f) => f.endsWith(".mjs"));
|
|
19157
|
+
if (hooks.length === 0) {
|
|
19158
|
+
skip("no hook files found in package");
|
|
19159
|
+
return;
|
|
19160
|
+
}
|
|
19161
|
+
if (!existsSync6(targetDir)) {
|
|
19162
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
19163
|
+
}
|
|
19164
|
+
let copied = 0;
|
|
19165
|
+
let skipped = 0;
|
|
19166
|
+
for (const file of hooks) {
|
|
19167
|
+
const src = join9(sourceDir, file);
|
|
19168
|
+
const dest = join9(targetDir, file);
|
|
19169
|
+
if (existsSync6(dest)) {
|
|
19170
|
+
skipped++;
|
|
19171
|
+
} else {
|
|
19172
|
+
copyFileSync(src, dest);
|
|
19173
|
+
copied++;
|
|
19174
|
+
}
|
|
19175
|
+
}
|
|
19176
|
+
if (copied > 0) {
|
|
19177
|
+
ok(`copied ${copied} hook${copied === 1 ? "" : "s"} to .specialists/default/hooks/`);
|
|
19178
|
+
}
|
|
19179
|
+
if (skipped > 0) {
|
|
19180
|
+
skip(`${skipped} hook${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
19181
|
+
}
|
|
19182
|
+
}
|
|
19183
|
+
function ensureProjectHooks(cwd) {
|
|
19184
|
+
const settingsPath = join9(cwd, ".claude", "settings.json");
|
|
19185
|
+
const settingsDir = join9(cwd, ".claude");
|
|
19186
|
+
if (!existsSync6(settingsDir)) {
|
|
19187
|
+
mkdirSync2(settingsDir, { recursive: true });
|
|
19188
|
+
}
|
|
19189
|
+
const settings = loadJson(settingsPath, {});
|
|
19190
|
+
let changed = false;
|
|
19191
|
+
function addHook(event, command) {
|
|
19192
|
+
const eventList = settings[event] ?? [];
|
|
19193
|
+
settings[event] = eventList;
|
|
19194
|
+
const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
|
|
19195
|
+
if (!alreadyWired) {
|
|
19196
|
+
eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
|
|
19197
|
+
changed = true;
|
|
19198
|
+
}
|
|
19199
|
+
}
|
|
19200
|
+
addHook("UserPromptSubmit", "node .specialists/default/hooks/specialists-complete.mjs");
|
|
19201
|
+
addHook("SessionStart", "node .specialists/default/hooks/specialists-session-start.mjs");
|
|
19202
|
+
if (changed) {
|
|
19203
|
+
saveJson(settingsPath, settings);
|
|
19204
|
+
ok("wired specialists hooks in .claude/settings.json");
|
|
19205
|
+
} else {
|
|
19206
|
+
skip(".claude/settings.json already has specialists hooks");
|
|
19207
|
+
}
|
|
19208
|
+
}
|
|
19209
|
+
function copyCanonicalSkills(cwd) {
|
|
19210
|
+
const sourceDir = resolvePackagePath("skills");
|
|
19211
|
+
if (!sourceDir) {
|
|
19212
|
+
skip("no canonical skills found in package");
|
|
19213
|
+
return;
|
|
19214
|
+
}
|
|
19215
|
+
const skills = readdirSync2(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
19216
|
+
if (skills.length === 0) {
|
|
19217
|
+
skip("no skill directories found in package");
|
|
19218
|
+
return;
|
|
19219
|
+
}
|
|
19220
|
+
const targetDir = join9(cwd, ".specialists", "default", "skills");
|
|
19221
|
+
if (!existsSync6(targetDir)) {
|
|
19222
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
19223
|
+
}
|
|
19224
|
+
let copied = 0;
|
|
19225
|
+
let skipped = 0;
|
|
19226
|
+
for (const skill of skills) {
|
|
19227
|
+
const src = join9(sourceDir, skill);
|
|
19228
|
+
const dest = join9(targetDir, skill);
|
|
19229
|
+
if (existsSync6(dest)) {
|
|
19230
|
+
skipped++;
|
|
19231
|
+
} else {
|
|
19232
|
+
cpSync(src, dest, { recursive: true });
|
|
19233
|
+
copied++;
|
|
19234
|
+
}
|
|
19235
|
+
}
|
|
19236
|
+
if (copied > 0) {
|
|
19237
|
+
ok(`copied ${copied} skill${copied === 1 ? "" : "s"} to .specialists/default/skills/`);
|
|
19238
|
+
}
|
|
19239
|
+
if (skipped > 0) {
|
|
19240
|
+
skip(`${skipped} skill${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
19241
|
+
}
|
|
19242
|
+
}
|
|
19243
|
+
function createUserDirs(cwd) {
|
|
19244
|
+
const userDirs = [
|
|
19245
|
+
join9(cwd, ".specialists", "user", "specialists"),
|
|
19246
|
+
join9(cwd, ".specialists", "user", "hooks"),
|
|
19247
|
+
join9(cwd, ".specialists", "user", "skills")
|
|
19248
|
+
];
|
|
19249
|
+
let created = 0;
|
|
19250
|
+
for (const dir of userDirs) {
|
|
19251
|
+
if (!existsSync6(dir)) {
|
|
19252
|
+
mkdirSync2(dir, { recursive: true });
|
|
19253
|
+
created++;
|
|
19254
|
+
}
|
|
19255
|
+
}
|
|
19256
|
+
if (created > 0) {
|
|
19257
|
+
ok("created .specialists/user/ directories for custom assets");
|
|
19258
|
+
}
|
|
18454
19259
|
}
|
|
18455
|
-
function
|
|
18456
|
-
|
|
19260
|
+
function createRuntimeDirs(cwd) {
|
|
19261
|
+
const runtimeDirs = [
|
|
19262
|
+
join9(cwd, ".specialists", "jobs"),
|
|
19263
|
+
join9(cwd, ".specialists", "ready")
|
|
19264
|
+
];
|
|
19265
|
+
let created = 0;
|
|
19266
|
+
for (const dir of runtimeDirs) {
|
|
19267
|
+
if (!existsSync6(dir)) {
|
|
19268
|
+
mkdirSync2(dir, { recursive: true });
|
|
19269
|
+
created++;
|
|
19270
|
+
}
|
|
19271
|
+
}
|
|
19272
|
+
if (created > 0) {
|
|
19273
|
+
ok("created .specialists/jobs/ and .specialists/ready/");
|
|
19274
|
+
}
|
|
18457
19275
|
}
|
|
18458
|
-
|
|
18459
|
-
const
|
|
18460
|
-
|
|
18461
|
-
|
|
19276
|
+
function ensureProjectMcp(cwd) {
|
|
19277
|
+
const mcpPath = join9(cwd, MCP_FILE);
|
|
19278
|
+
const mcp = loadJson(mcpPath, { mcpServers: {} });
|
|
19279
|
+
mcp.mcpServers ??= {};
|
|
19280
|
+
const existing = mcp.mcpServers[MCP_SERVER_NAME];
|
|
19281
|
+
if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
|
|
19282
|
+
skip(".mcp.json already registers specialists");
|
|
19283
|
+
return;
|
|
19284
|
+
}
|
|
19285
|
+
mcp.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
|
|
19286
|
+
saveJson(mcpPath, mcp);
|
|
19287
|
+
ok("registered specialists in project .mcp.json");
|
|
19288
|
+
}
|
|
19289
|
+
function ensureGitignore(cwd) {
|
|
19290
|
+
const gitignorePath = join9(cwd, ".gitignore");
|
|
19291
|
+
const existing = existsSync6(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
|
|
19292
|
+
let added = 0;
|
|
19293
|
+
const lines = existing.split(`
|
|
18462
19294
|
`);
|
|
18463
|
-
const
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
ok("created specialists/");
|
|
19295
|
+
for (const entry of GITIGNORE_ENTRIES) {
|
|
19296
|
+
if (!lines.includes(entry)) {
|
|
19297
|
+
lines.push(entry);
|
|
19298
|
+
added++;
|
|
19299
|
+
}
|
|
18469
19300
|
}
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
} else {
|
|
18474
|
-
mkdirSync(join5(runtimeDir, "jobs"), { recursive: true });
|
|
18475
|
-
mkdirSync(join5(runtimeDir, "ready"), { recursive: true });
|
|
18476
|
-
ok("created .specialists/ (jobs/, ready/)");
|
|
18477
|
-
}
|
|
18478
|
-
const gitignorePath = join5(cwd, ".gitignore");
|
|
18479
|
-
if (existsSync3(gitignorePath)) {
|
|
18480
|
-
const existing = readFileSync(gitignorePath, "utf-8");
|
|
18481
|
-
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
18482
|
-
skip(".gitignore already has .specialists/ entry");
|
|
18483
|
-
} else {
|
|
18484
|
-
const separator = existing.endsWith(`
|
|
18485
|
-
`) ? "" : `
|
|
18486
|
-
`;
|
|
18487
|
-
writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
|
|
19301
|
+
if (added > 0) {
|
|
19302
|
+
writeFileSync4(gitignorePath, lines.join(`
|
|
19303
|
+
`) + `
|
|
18488
19304
|
`, "utf-8");
|
|
18489
|
-
|
|
18490
|
-
}
|
|
19305
|
+
ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
|
|
18491
19306
|
} else {
|
|
18492
|
-
|
|
18493
|
-
`, "utf-8");
|
|
18494
|
-
ok("created .gitignore with .specialists/ entry");
|
|
19307
|
+
skip(".gitignore already has runtime entries");
|
|
18495
19308
|
}
|
|
18496
|
-
|
|
18497
|
-
|
|
18498
|
-
|
|
19309
|
+
}
|
|
19310
|
+
function ensureAgentsMd(cwd) {
|
|
19311
|
+
const agentsPath = join9(cwd, "AGENTS.md");
|
|
19312
|
+
if (existsSync6(agentsPath)) {
|
|
19313
|
+
const existing = readFileSync3(agentsPath, "utf-8");
|
|
18499
19314
|
if (existing.includes(AGENTS_MARKER)) {
|
|
18500
19315
|
skip("AGENTS.md already has Specialists section");
|
|
18501
19316
|
} else {
|
|
18502
|
-
|
|
19317
|
+
writeFileSync4(agentsPath, existing.trimEnd() + `
|
|
18503
19318
|
|
|
18504
19319
|
` + AGENTS_BLOCK, "utf-8");
|
|
18505
19320
|
ok("appended Specialists section to AGENTS.md");
|
|
18506
19321
|
}
|
|
18507
19322
|
} else {
|
|
18508
|
-
|
|
19323
|
+
writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
|
|
18509
19324
|
ok("created AGENTS.md with Specialists section");
|
|
18510
19325
|
}
|
|
19326
|
+
}
|
|
19327
|
+
async function run5() {
|
|
19328
|
+
const cwd = process.cwd();
|
|
19329
|
+
console.log(`
|
|
19330
|
+
${bold3("specialists init")}
|
|
19331
|
+
`);
|
|
19332
|
+
copyCanonicalSpecialists(cwd);
|
|
19333
|
+
copyCanonicalHooks(cwd);
|
|
19334
|
+
copyCanonicalSkills(cwd);
|
|
19335
|
+
createUserDirs(cwd);
|
|
19336
|
+
createRuntimeDirs(cwd);
|
|
19337
|
+
ensureGitignore(cwd);
|
|
19338
|
+
ensureAgentsMd(cwd);
|
|
19339
|
+
ensureProjectMcp(cwd);
|
|
19340
|
+
ensureProjectHooks(cwd);
|
|
18511
19341
|
console.log(`
|
|
18512
19342
|
${bold3("Done!")}
|
|
18513
19343
|
`);
|
|
18514
|
-
console.log(` ${dim3("
|
|
18515
|
-
console.log(`
|
|
18516
|
-
console.log(`
|
|
18517
|
-
console.log(`
|
|
19344
|
+
console.log(` ${dim3("Directory structure:")}`);
|
|
19345
|
+
console.log(` .specialists/`);
|
|
19346
|
+
console.log(` ├── default/ ${dim3("# canonical assets (from init)")}`);
|
|
19347
|
+
console.log(` │ ├── specialists/`);
|
|
19348
|
+
console.log(` │ ├── hooks/`);
|
|
19349
|
+
console.log(` │ └── skills/`);
|
|
19350
|
+
console.log(` ├── user/ ${dim3("# your custom additions")}`);
|
|
19351
|
+
console.log(` │ ├── specialists/`);
|
|
19352
|
+
console.log(` │ ├── hooks/`);
|
|
19353
|
+
console.log(` │ └── skills/`);
|
|
19354
|
+
console.log(` ├── jobs/ ${dim3("# runtime (gitignored)")}`);
|
|
19355
|
+
console.log(` └── ready/ ${dim3("# runtime (gitignored)")}`);
|
|
19356
|
+
console.log(`
|
|
19357
|
+
${dim3("Next steps:")}`);
|
|
19358
|
+
console.log(` 1. Run ${yellow3("specialists list")} to see available specialists`);
|
|
19359
|
+
console.log(` 2. Add custom specialists to ${yellow3(".specialists/user/specialists/")}`);
|
|
19360
|
+
console.log(` 3. Restart Claude Code to pick up changes
|
|
18518
19361
|
`);
|
|
18519
19362
|
}
|
|
18520
|
-
var bold3 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
19363
|
+
var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (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_ENTRIES, MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
|
|
18521
19364
|
var init_init = __esm(() => {
|
|
18522
19365
|
AGENTS_BLOCK = `
|
|
18523
19366
|
## Specialists
|
|
@@ -18526,7 +19369,11 @@ Call \`specialist_init\` at the start of every session to bootstrap context and
|
|
|
18526
19369
|
see available specialists. Use \`use_specialist\` or \`start_specialist\` to
|
|
18527
19370
|
delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
|
|
18528
19371
|
specialist without user intervention.
|
|
19372
|
+
|
|
19373
|
+
Add custom specialists to \`.specialists/user/specialists/\` to extend the defaults.
|
|
18529
19374
|
`.trimStart();
|
|
19375
|
+
GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
|
|
19376
|
+
MCP_SERVER_CONFIG = { command: "specialists", args: [] };
|
|
18530
19377
|
});
|
|
18531
19378
|
|
|
18532
19379
|
// src/cli/edit.ts
|
|
@@ -18534,11 +19381,11 @@ var exports_edit = {};
|
|
|
18534
19381
|
__export(exports_edit, {
|
|
18535
19382
|
run: () => run6
|
|
18536
19383
|
});
|
|
18537
|
-
import { readFileSync as
|
|
19384
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
18538
19385
|
function parseArgs3(argv) {
|
|
18539
19386
|
const name = argv[0];
|
|
18540
19387
|
if (!name || name.startsWith("--")) {
|
|
18541
|
-
console.error("Usage: specialists edit <name> --<field> <value> [--dry-run]");
|
|
19388
|
+
console.error("Usage: specialists|sp edit <name> --<field> <value> [--dry-run]");
|
|
18542
19389
|
console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
|
|
18543
19390
|
process.exit(1);
|
|
18544
19391
|
}
|
|
@@ -18588,273 +19435,75 @@ function parseArgs3(argv) {
|
|
|
18588
19435
|
function setIn(doc2, path, value) {
|
|
18589
19436
|
let node = doc2;
|
|
18590
19437
|
for (let i = 0;i < path.length - 1; i++) {
|
|
18591
|
-
node = node.get(path[i], true);
|
|
18592
|
-
}
|
|
18593
|
-
const leaf = path[path.length - 1];
|
|
18594
|
-
if (Array.isArray(value)) {
|
|
18595
|
-
node.set(leaf, value);
|
|
18596
|
-
} else {
|
|
18597
|
-
node.set(leaf, value);
|
|
18598
|
-
}
|
|
18599
|
-
}
|
|
18600
|
-
async function run6() {
|
|
18601
|
-
const args = parseArgs3(process.argv.slice(3));
|
|
18602
|
-
const { name, field, value, dryRun, scope } = args;
|
|
18603
|
-
const loader = new SpecialistLoader;
|
|
18604
|
-
const all = await loader.list();
|
|
18605
|
-
const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
|
|
18606
|
-
if (!match) {
|
|
18607
|
-
const hint = scope ? ` (scope: ${scope})` : "";
|
|
18608
|
-
console.error(`Error: specialist "${name}" not found${hint}`);
|
|
18609
|
-
console.error(` Run ${yellow4("specialists list")} to see available specialists`);
|
|
18610
|
-
process.exit(1);
|
|
18611
|
-
}
|
|
18612
|
-
const raw = readFileSync2(match.filePath, "utf-8");
|
|
18613
|
-
const doc2 = $parseDocument(raw);
|
|
18614
|
-
const yamlPath = FIELD_MAP[field];
|
|
18615
|
-
let typedValue = value;
|
|
18616
|
-
if (field === "timeout") {
|
|
18617
|
-
typedValue = parseInt(value, 10);
|
|
18618
|
-
} else if (field === "tags") {
|
|
18619
|
-
typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
18620
|
-
}
|
|
18621
|
-
setIn(doc2, yamlPath, typedValue);
|
|
18622
|
-
const updated = doc2.toString();
|
|
18623
|
-
if (dryRun) {
|
|
18624
|
-
console.log(`
|
|
18625
|
-
${bold4(`[dry-run] ${match.filePath}`)}
|
|
18626
|
-
`);
|
|
18627
|
-
console.log(dim4("--- current"));
|
|
18628
|
-
console.log(dim4(`+++ updated`));
|
|
18629
|
-
const oldLines = raw.split(`
|
|
18630
|
-
`);
|
|
18631
|
-
const newLines = updated.split(`
|
|
18632
|
-
`);
|
|
18633
|
-
newLines.forEach((line, i) => {
|
|
18634
|
-
if (line !== oldLines[i]) {
|
|
18635
|
-
if (oldLines[i] !== undefined)
|
|
18636
|
-
console.log(dim4(`- ${oldLines[i]}`));
|
|
18637
|
-
console.log(green3(`+ ${line}`));
|
|
18638
|
-
}
|
|
18639
|
-
});
|
|
18640
|
-
console.log();
|
|
18641
|
-
return;
|
|
18642
|
-
}
|
|
18643
|
-
writeFileSync2(match.filePath, updated, "utf-8");
|
|
18644
|
-
const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
|
|
18645
|
-
console.log(`${green3("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
|
|
18646
|
-
}
|
|
18647
|
-
var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green3 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
|
|
18648
|
-
var init_edit = __esm(() => {
|
|
18649
|
-
init_dist();
|
|
18650
|
-
init_loader();
|
|
18651
|
-
FIELD_MAP = {
|
|
18652
|
-
model: ["specialist", "execution", "model"],
|
|
18653
|
-
"fallback-model": ["specialist", "execution", "fallback_model"],
|
|
18654
|
-
description: ["specialist", "metadata", "description"],
|
|
18655
|
-
permission: ["specialist", "execution", "permission_required"],
|
|
18656
|
-
timeout: ["specialist", "execution", "timeout_ms"],
|
|
18657
|
-
tags: ["specialist", "metadata", "tags"]
|
|
18658
|
-
};
|
|
18659
|
-
VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
|
|
18660
|
-
});
|
|
18661
|
-
|
|
18662
|
-
// src/specialist/supervisor.ts
|
|
18663
|
-
import {
|
|
18664
|
-
closeSync,
|
|
18665
|
-
existsSync as existsSync4,
|
|
18666
|
-
mkdirSync as mkdirSync2,
|
|
18667
|
-
openSync,
|
|
18668
|
-
readdirSync,
|
|
18669
|
-
readFileSync as readFileSync3,
|
|
18670
|
-
renameSync,
|
|
18671
|
-
rmSync,
|
|
18672
|
-
statSync,
|
|
18673
|
-
writeFileSync as writeFileSync3,
|
|
18674
|
-
writeSync
|
|
18675
|
-
} from "node:fs";
|
|
18676
|
-
import { join as join6 } from "node:path";
|
|
18677
|
-
|
|
18678
|
-
class Supervisor {
|
|
18679
|
-
opts;
|
|
18680
|
-
constructor(opts) {
|
|
18681
|
-
this.opts = opts;
|
|
18682
|
-
}
|
|
18683
|
-
jobDir(id) {
|
|
18684
|
-
return join6(this.opts.jobsDir, id);
|
|
18685
|
-
}
|
|
18686
|
-
statusPath(id) {
|
|
18687
|
-
return join6(this.jobDir(id), "status.json");
|
|
18688
|
-
}
|
|
18689
|
-
resultPath(id) {
|
|
18690
|
-
return join6(this.jobDir(id), "result.txt");
|
|
18691
|
-
}
|
|
18692
|
-
eventsPath(id) {
|
|
18693
|
-
return join6(this.jobDir(id), "events.jsonl");
|
|
18694
|
-
}
|
|
18695
|
-
readyDir() {
|
|
18696
|
-
return join6(this.opts.jobsDir, "..", "ready");
|
|
18697
|
-
}
|
|
18698
|
-
readStatus(id) {
|
|
18699
|
-
const path = this.statusPath(id);
|
|
18700
|
-
if (!existsSync4(path))
|
|
18701
|
-
return null;
|
|
18702
|
-
try {
|
|
18703
|
-
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
18704
|
-
} catch {
|
|
18705
|
-
return null;
|
|
18706
|
-
}
|
|
18707
|
-
}
|
|
18708
|
-
listJobs() {
|
|
18709
|
-
if (!existsSync4(this.opts.jobsDir))
|
|
18710
|
-
return [];
|
|
18711
|
-
const jobs = [];
|
|
18712
|
-
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18713
|
-
const path = join6(this.opts.jobsDir, entry, "status.json");
|
|
18714
|
-
if (!existsSync4(path))
|
|
18715
|
-
continue;
|
|
18716
|
-
try {
|
|
18717
|
-
jobs.push(JSON.parse(readFileSync3(path, "utf-8")));
|
|
18718
|
-
} catch {}
|
|
18719
|
-
}
|
|
18720
|
-
return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
18721
|
-
}
|
|
18722
|
-
writeStatusFile(id, data) {
|
|
18723
|
-
const path = this.statusPath(id);
|
|
18724
|
-
const tmp = path + ".tmp";
|
|
18725
|
-
writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
18726
|
-
renameSync(tmp, path);
|
|
18727
|
-
}
|
|
18728
|
-
updateStatus(id, updates) {
|
|
18729
|
-
const current = this.readStatus(id);
|
|
18730
|
-
if (!current)
|
|
18731
|
-
return;
|
|
18732
|
-
this.writeStatusFile(id, { ...current, ...updates });
|
|
18733
|
-
}
|
|
18734
|
-
gc() {
|
|
18735
|
-
if (!existsSync4(this.opts.jobsDir))
|
|
18736
|
-
return;
|
|
18737
|
-
const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
|
|
18738
|
-
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18739
|
-
const dir = join6(this.opts.jobsDir, entry);
|
|
18740
|
-
try {
|
|
18741
|
-
const stat2 = statSync(dir);
|
|
18742
|
-
if (!stat2.isDirectory())
|
|
18743
|
-
continue;
|
|
18744
|
-
if (stat2.mtimeMs < cutoff)
|
|
18745
|
-
rmSync(dir, { recursive: true, force: true });
|
|
18746
|
-
} catch {}
|
|
18747
|
-
}
|
|
18748
|
-
}
|
|
18749
|
-
crashRecovery() {
|
|
18750
|
-
if (!existsSync4(this.opts.jobsDir))
|
|
18751
|
-
return;
|
|
18752
|
-
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18753
|
-
const statusPath = join6(this.opts.jobsDir, entry, "status.json");
|
|
18754
|
-
if (!existsSync4(statusPath))
|
|
18755
|
-
continue;
|
|
18756
|
-
try {
|
|
18757
|
-
const s = JSON.parse(readFileSync3(statusPath, "utf-8"));
|
|
18758
|
-
if (s.status !== "running" && s.status !== "starting")
|
|
18759
|
-
continue;
|
|
18760
|
-
if (!s.pid)
|
|
18761
|
-
continue;
|
|
18762
|
-
try {
|
|
18763
|
-
process.kill(s.pid, 0);
|
|
18764
|
-
} catch {
|
|
18765
|
-
const tmp = statusPath + ".tmp";
|
|
18766
|
-
const updated = { ...s, status: "error", error: "Process crashed or was killed" };
|
|
18767
|
-
writeFileSync3(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
18768
|
-
renameSync(tmp, statusPath);
|
|
18769
|
-
}
|
|
18770
|
-
} catch {}
|
|
18771
|
-
}
|
|
18772
|
-
}
|
|
18773
|
-
async run() {
|
|
18774
|
-
const { runner, runOptions, jobsDir } = this.opts;
|
|
18775
|
-
this.gc();
|
|
18776
|
-
this.crashRecovery();
|
|
18777
|
-
const id = crypto.randomUUID().slice(0, 6);
|
|
18778
|
-
const dir = this.jobDir(id);
|
|
18779
|
-
const startedAtMs = Date.now();
|
|
18780
|
-
mkdirSync2(dir, { recursive: true });
|
|
18781
|
-
mkdirSync2(this.readyDir(), { recursive: true });
|
|
18782
|
-
const initialStatus = {
|
|
18783
|
-
id,
|
|
18784
|
-
specialist: runOptions.name,
|
|
18785
|
-
status: "starting",
|
|
18786
|
-
started_at_ms: startedAtMs,
|
|
18787
|
-
pid: process.pid
|
|
18788
|
-
};
|
|
18789
|
-
this.writeStatusFile(id, initialStatus);
|
|
18790
|
-
const eventsFd = openSync(this.eventsPath(id), "a");
|
|
18791
|
-
const appendEvent = (obj) => {
|
|
18792
|
-
try {
|
|
18793
|
-
writeSync(eventsFd, JSON.stringify({ t: Date.now(), ...obj }) + `
|
|
18794
|
-
`);
|
|
18795
|
-
} catch {}
|
|
18796
|
-
};
|
|
18797
|
-
let textLogged = false;
|
|
18798
|
-
let currentTool = "";
|
|
18799
|
-
try {
|
|
18800
|
-
const result = await runner.run(runOptions, (delta) => {
|
|
18801
|
-
const toolMatch = delta.match(/⚙ (.+?)…/);
|
|
18802
|
-
if (toolMatch) {
|
|
18803
|
-
currentTool = toolMatch[1];
|
|
18804
|
-
this.updateStatus(id, { current_tool: currentTool });
|
|
18805
|
-
}
|
|
18806
|
-
}, (eventType) => {
|
|
18807
|
-
const now = Date.now();
|
|
18808
|
-
this.updateStatus(id, {
|
|
18809
|
-
status: "running",
|
|
18810
|
-
current_event: eventType,
|
|
18811
|
-
last_event_at_ms: now,
|
|
18812
|
-
elapsed_s: Math.round((now - startedAtMs) / 1000)
|
|
18813
|
-
});
|
|
18814
|
-
if (LOGGED_EVENTS.has(eventType)) {
|
|
18815
|
-
const tool = eventType === "toolcall" || eventType === "tool_execution_end" ? currentTool : undefined;
|
|
18816
|
-
appendEvent({ type: eventType, ...tool ? { tool } : {} });
|
|
18817
|
-
} else if (eventType === "text" && !textLogged) {
|
|
18818
|
-
textLogged = true;
|
|
18819
|
-
appendEvent({ type: "text" });
|
|
18820
|
-
}
|
|
18821
|
-
}, (meta) => {
|
|
18822
|
-
this.updateStatus(id, { model: meta.model, backend: meta.backend });
|
|
18823
|
-
appendEvent({ type: "meta", model: meta.model, backend: meta.backend });
|
|
18824
|
-
}, (_killFn) => {}, (beadId) => {
|
|
18825
|
-
this.updateStatus(id, { bead_id: beadId });
|
|
18826
|
-
});
|
|
18827
|
-
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18828
|
-
writeFileSync3(this.resultPath(id), result.output, "utf-8");
|
|
18829
|
-
this.updateStatus(id, {
|
|
18830
|
-
status: "done",
|
|
18831
|
-
elapsed_s: elapsed,
|
|
18832
|
-
last_event_at_ms: Date.now(),
|
|
18833
|
-
model: result.model,
|
|
18834
|
-
backend: result.backend,
|
|
18835
|
-
bead_id: result.beadId
|
|
18836
|
-
});
|
|
18837
|
-
appendEvent({ type: "agent_end", elapsed_s: elapsed });
|
|
18838
|
-
writeFileSync3(join6(this.readyDir(), id), "", "utf-8");
|
|
18839
|
-
return id;
|
|
18840
|
-
} catch (err) {
|
|
18841
|
-
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18842
|
-
this.updateStatus(id, {
|
|
18843
|
-
status: "error",
|
|
18844
|
-
elapsed_s: elapsed,
|
|
18845
|
-
error: err?.message ?? String(err)
|
|
18846
|
-
});
|
|
18847
|
-
appendEvent({ type: "error", message: err?.message ?? String(err) });
|
|
18848
|
-
throw err;
|
|
18849
|
-
} finally {
|
|
18850
|
-
closeSync(eventsFd);
|
|
18851
|
-
}
|
|
19438
|
+
node = node.get(path[i], true);
|
|
19439
|
+
}
|
|
19440
|
+
const leaf = path[path.length - 1];
|
|
19441
|
+
if (Array.isArray(value)) {
|
|
19442
|
+
node.set(leaf, value);
|
|
19443
|
+
} else {
|
|
19444
|
+
node.set(leaf, value);
|
|
18852
19445
|
}
|
|
18853
19446
|
}
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
19447
|
+
async function run6() {
|
|
19448
|
+
const args = parseArgs3(process.argv.slice(3));
|
|
19449
|
+
const { name, field, value, dryRun, scope } = args;
|
|
19450
|
+
const loader = new SpecialistLoader;
|
|
19451
|
+
const all = await loader.list();
|
|
19452
|
+
const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
|
|
19453
|
+
if (!match) {
|
|
19454
|
+
const hint = scope ? ` (scope: ${scope})` : "";
|
|
19455
|
+
console.error(`Error: specialist "${name}" not found${hint}`);
|
|
19456
|
+
console.error(` Run ${yellow4("specialists list")} to see available specialists`);
|
|
19457
|
+
process.exit(1);
|
|
19458
|
+
}
|
|
19459
|
+
const raw = readFileSync4(match.filePath, "utf-8");
|
|
19460
|
+
const doc2 = $parseDocument(raw);
|
|
19461
|
+
const yamlPath = FIELD_MAP[field];
|
|
19462
|
+
let typedValue = value;
|
|
19463
|
+
if (field === "timeout") {
|
|
19464
|
+
typedValue = parseInt(value, 10);
|
|
19465
|
+
} else if (field === "tags") {
|
|
19466
|
+
typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
19467
|
+
}
|
|
19468
|
+
setIn(doc2, yamlPath, typedValue);
|
|
19469
|
+
const updated = doc2.toString();
|
|
19470
|
+
if (dryRun) {
|
|
19471
|
+
console.log(`
|
|
19472
|
+
${bold4(`[dry-run] ${match.filePath}`)}
|
|
19473
|
+
`);
|
|
19474
|
+
console.log(dim4("--- current"));
|
|
19475
|
+
console.log(dim4(`+++ updated`));
|
|
19476
|
+
const oldLines = raw.split(`
|
|
19477
|
+
`);
|
|
19478
|
+
const newLines = updated.split(`
|
|
19479
|
+
`);
|
|
19480
|
+
newLines.forEach((line, i) => {
|
|
19481
|
+
if (line !== oldLines[i]) {
|
|
19482
|
+
if (oldLines[i] !== undefined)
|
|
19483
|
+
console.log(dim4(`- ${oldLines[i]}`));
|
|
19484
|
+
console.log(green4(`+ ${line}`));
|
|
19485
|
+
}
|
|
19486
|
+
});
|
|
19487
|
+
console.log();
|
|
19488
|
+
return;
|
|
19489
|
+
}
|
|
19490
|
+
writeFileSync5(match.filePath, updated, "utf-8");
|
|
19491
|
+
const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
|
|
19492
|
+
console.log(`${green4("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
|
|
19493
|
+
}
|
|
19494
|
+
var bold4 = (s) => `\x1B[1m${s}\x1B[0m`, green4 = (s) => `\x1B[32m${s}\x1B[0m`, yellow4 = (s) => `\x1B[33m${s}\x1B[0m`, dim4 = (s) => `\x1B[2m${s}\x1B[0m`, FIELD_MAP, VALID_PERMISSIONS;
|
|
19495
|
+
var init_edit = __esm(() => {
|
|
19496
|
+
init_dist();
|
|
19497
|
+
init_loader();
|
|
19498
|
+
FIELD_MAP = {
|
|
19499
|
+
model: ["specialist", "execution", "model"],
|
|
19500
|
+
"fallback-model": ["specialist", "execution", "fallback_model"],
|
|
19501
|
+
description: ["specialist", "metadata", "description"],
|
|
19502
|
+
permission: ["specialist", "execution", "permission_required"],
|
|
19503
|
+
timeout: ["specialist", "execution", "timeout_ms"],
|
|
19504
|
+
tags: ["specialist", "metadata", "tags"]
|
|
19505
|
+
};
|
|
19506
|
+
VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
|
|
18858
19507
|
});
|
|
18859
19508
|
|
|
18860
19509
|
// src/cli/run.ts
|
|
@@ -18862,27 +19511,40 @@ var exports_run = {};
|
|
|
18862
19511
|
__export(exports_run, {
|
|
18863
19512
|
run: () => run7
|
|
18864
19513
|
});
|
|
18865
|
-
import {
|
|
19514
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
19515
|
+
import { join as join10 } from "node:path";
|
|
18866
19516
|
async function parseArgs4(argv) {
|
|
18867
19517
|
const name = argv[0];
|
|
18868
19518
|
if (!name || name.startsWith("--")) {
|
|
18869
|
-
console.error('Usage: specialists run <name> [--prompt "..."] [--model <model>] [--no-beads] [--background]');
|
|
19519
|
+
console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--background] [--follow]');
|
|
18870
19520
|
process.exit(1);
|
|
18871
19521
|
}
|
|
18872
19522
|
let prompt = "";
|
|
19523
|
+
let beadId;
|
|
18873
19524
|
let model;
|
|
18874
19525
|
let noBeads = false;
|
|
18875
19526
|
let background = false;
|
|
19527
|
+
let follow = false;
|
|
19528
|
+
let keepAlive = false;
|
|
19529
|
+
let contextDepth = 1;
|
|
18876
19530
|
for (let i = 1;i < argv.length; i++) {
|
|
18877
19531
|
const token = argv[i];
|
|
18878
19532
|
if (token === "--prompt" && argv[i + 1]) {
|
|
18879
19533
|
prompt = argv[++i];
|
|
18880
19534
|
continue;
|
|
18881
19535
|
}
|
|
19536
|
+
if (token === "--bead" && argv[i + 1]) {
|
|
19537
|
+
beadId = argv[++i];
|
|
19538
|
+
continue;
|
|
19539
|
+
}
|
|
18882
19540
|
if (token === "--model" && argv[i + 1]) {
|
|
18883
19541
|
model = argv[++i];
|
|
18884
19542
|
continue;
|
|
18885
19543
|
}
|
|
19544
|
+
if (token === "--context-depth" && argv[i + 1]) {
|
|
19545
|
+
contextDepth = parseInt(argv[++i], 10) || 0;
|
|
19546
|
+
continue;
|
|
19547
|
+
}
|
|
18886
19548
|
if (token === "--no-beads") {
|
|
18887
19549
|
noBeads = true;
|
|
18888
19550
|
continue;
|
|
@@ -18891,61 +19553,130 @@ async function parseArgs4(argv) {
|
|
|
18891
19553
|
background = true;
|
|
18892
19554
|
continue;
|
|
18893
19555
|
}
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
19556
|
+
if (token === "--follow") {
|
|
19557
|
+
follow = true;
|
|
19558
|
+
continue;
|
|
19559
|
+
}
|
|
19560
|
+
if (token === "--keep-alive") {
|
|
19561
|
+
keepAlive = true;
|
|
19562
|
+
continue;
|
|
18898
19563
|
}
|
|
18899
|
-
|
|
19564
|
+
}
|
|
19565
|
+
if (prompt && beadId) {
|
|
19566
|
+
console.error("Error: use either --prompt or --bead, not both.");
|
|
19567
|
+
process.exit(1);
|
|
19568
|
+
}
|
|
19569
|
+
if (!prompt && !beadId && !process.stdin.isTTY) {
|
|
19570
|
+
prompt = await new Promise((resolve2) => {
|
|
18900
19571
|
let buf = "";
|
|
18901
19572
|
process.stdin.setEncoding("utf-8");
|
|
18902
19573
|
process.stdin.on("data", (chunk) => {
|
|
18903
19574
|
buf += chunk;
|
|
18904
19575
|
});
|
|
18905
|
-
process.stdin.on("end", () =>
|
|
19576
|
+
process.stdin.on("end", () => resolve2(buf.trim()));
|
|
18906
19577
|
});
|
|
18907
19578
|
}
|
|
18908
|
-
|
|
19579
|
+
if (!prompt && !beadId) {
|
|
19580
|
+
console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
|
|
19581
|
+
process.exit(1);
|
|
19582
|
+
}
|
|
19583
|
+
return { name, prompt, beadId, model, noBeads, background, follow, keepAlive, contextDepth };
|
|
18909
19584
|
}
|
|
18910
19585
|
async function run7() {
|
|
18911
19586
|
const args = await parseArgs4(process.argv.slice(3));
|
|
18912
19587
|
const loader = new SpecialistLoader;
|
|
18913
19588
|
const circuitBreaker = new CircuitBreaker;
|
|
18914
|
-
const hooks = new HookEmitter({ tracePath:
|
|
18915
|
-
const beadsClient = args.noBeads ?
|
|
19589
|
+
const hooks = new HookEmitter({ tracePath: join10(process.cwd(), ".specialists", "trace.jsonl") });
|
|
19590
|
+
const beadsClient = args.noBeads ? undefined : new BeadsClient;
|
|
19591
|
+
const beadReader = beadsClient ?? new BeadsClient;
|
|
19592
|
+
let prompt = args.prompt;
|
|
19593
|
+
let variables;
|
|
19594
|
+
if (args.beadId) {
|
|
19595
|
+
const bead = beadReader.readBead(args.beadId);
|
|
19596
|
+
if (!bead) {
|
|
19597
|
+
throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
|
|
19598
|
+
}
|
|
19599
|
+
const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
|
|
19600
|
+
if (blockers.length > 0) {
|
|
19601
|
+
process.stderr.write(dim5(`
|
|
19602
|
+
[context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
|
|
19603
|
+
`));
|
|
19604
|
+
}
|
|
19605
|
+
const beadContext = buildBeadContext(bead, blockers);
|
|
19606
|
+
prompt = beadContext;
|
|
19607
|
+
variables = {
|
|
19608
|
+
bead_context: beadContext,
|
|
19609
|
+
bead_id: args.beadId
|
|
19610
|
+
};
|
|
19611
|
+
}
|
|
18916
19612
|
const runner = new SpecialistRunner({
|
|
18917
19613
|
loader,
|
|
18918
19614
|
hooks,
|
|
18919
19615
|
circuitBreaker,
|
|
18920
|
-
beadsClient
|
|
19616
|
+
beadsClient
|
|
18921
19617
|
});
|
|
18922
|
-
if (args.background) {
|
|
18923
|
-
const jobsDir =
|
|
19618
|
+
if (args.background || args.follow) {
|
|
19619
|
+
const jobsDir = join10(process.cwd(), ".specialists", "jobs");
|
|
18924
19620
|
const supervisor = new Supervisor({
|
|
18925
19621
|
runner,
|
|
18926
|
-
runOptions: {
|
|
18927
|
-
|
|
19622
|
+
runOptions: {
|
|
19623
|
+
name: args.name,
|
|
19624
|
+
prompt,
|
|
19625
|
+
variables,
|
|
19626
|
+
backendOverride: args.model,
|
|
19627
|
+
inputBeadId: args.beadId,
|
|
19628
|
+
keepAlive: args.keepAlive
|
|
19629
|
+
},
|
|
19630
|
+
jobsDir,
|
|
19631
|
+
beadsClient
|
|
18928
19632
|
});
|
|
19633
|
+
let jobId;
|
|
18929
19634
|
try {
|
|
18930
|
-
|
|
18931
|
-
|
|
19635
|
+
jobId = await supervisor.run();
|
|
19636
|
+
if (!args.follow) {
|
|
19637
|
+
process.stdout.write(`Job started: ${jobId}
|
|
18932
19638
|
`);
|
|
19639
|
+
}
|
|
18933
19640
|
} catch (err) {
|
|
18934
19641
|
process.stderr.write(`Error: ${err?.message ?? err}
|
|
18935
19642
|
`);
|
|
18936
19643
|
process.exit(1);
|
|
18937
19644
|
}
|
|
19645
|
+
if (args.follow) {
|
|
19646
|
+
await new Promise((resolve2, reject) => {
|
|
19647
|
+
const feed = spawn2("specialists", ["feed", "--job", jobId, "--follow"], {
|
|
19648
|
+
cwd: process.cwd(),
|
|
19649
|
+
stdio: "inherit"
|
|
19650
|
+
});
|
|
19651
|
+
feed.on("close", (code) => {
|
|
19652
|
+
if (code === 0) {
|
|
19653
|
+
resolve2();
|
|
19654
|
+
} else {
|
|
19655
|
+
reject(new Error(`Feed exited with code ${code}`));
|
|
19656
|
+
}
|
|
19657
|
+
});
|
|
19658
|
+
feed.on("error", (err) => {
|
|
19659
|
+
reject(err);
|
|
19660
|
+
});
|
|
19661
|
+
}).catch((err) => {
|
|
19662
|
+
process.stderr.write(`Error: ${err.message}
|
|
19663
|
+
`);
|
|
19664
|
+
process.exit(1);
|
|
19665
|
+
});
|
|
19666
|
+
}
|
|
18938
19667
|
return;
|
|
18939
19668
|
}
|
|
18940
19669
|
process.stderr.write(`
|
|
18941
19670
|
${bold5(`Running ${cyan3(args.name)}`)}
|
|
18942
19671
|
|
|
18943
19672
|
`);
|
|
18944
|
-
let
|
|
19673
|
+
let trackingBeadId;
|
|
18945
19674
|
const result = await runner.run({
|
|
18946
19675
|
name: args.name,
|
|
18947
|
-
prompt
|
|
18948
|
-
|
|
19676
|
+
prompt,
|
|
19677
|
+
variables,
|
|
19678
|
+
backendOverride: args.model,
|
|
19679
|
+
inputBeadId: args.beadId
|
|
18949
19680
|
}, (delta) => process.stdout.write(delta), undefined, (meta) => process.stderr.write(dim5(`
|
|
18950
19681
|
[${meta.backend} / ${meta.model}]
|
|
18951
19682
|
|
|
@@ -18958,10 +19689,10 @@ Interrupted.
|
|
|
18958
19689
|
killFn();
|
|
18959
19690
|
process.exit(130);
|
|
18960
19691
|
});
|
|
18961
|
-
}, (
|
|
18962
|
-
|
|
19692
|
+
}, (beadId) => {
|
|
19693
|
+
trackingBeadId = beadId;
|
|
18963
19694
|
process.stderr.write(dim5(`
|
|
18964
|
-
[bead: ${
|
|
19695
|
+
[bead: ${beadId}]
|
|
18965
19696
|
`));
|
|
18966
19697
|
});
|
|
18967
19698
|
if (result.output && !result.output.endsWith(`
|
|
@@ -18969,17 +19700,18 @@ Interrupted.
|
|
|
18969
19700
|
process.stdout.write(`
|
|
18970
19701
|
`);
|
|
18971
19702
|
const secs = (result.durationMs / 1000).toFixed(1);
|
|
19703
|
+
const effectiveBeadId = args.beadId ?? trackingBeadId;
|
|
18972
19704
|
const footer = [
|
|
18973
|
-
|
|
19705
|
+
effectiveBeadId ? `bead ${effectiveBeadId}` : "",
|
|
18974
19706
|
`${secs}s`,
|
|
18975
19707
|
dim5(result.model)
|
|
18976
19708
|
].filter(Boolean).join(" ");
|
|
18977
19709
|
process.stderr.write(`
|
|
18978
|
-
${
|
|
19710
|
+
${green5("✓")} ${footer}
|
|
18979
19711
|
|
|
18980
19712
|
`);
|
|
18981
19713
|
}
|
|
18982
|
-
var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`,
|
|
19714
|
+
var bold5 = (s) => `\x1B[1m${s}\x1B[0m`, dim5 = (s) => `\x1B[2m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, cyan3 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
18983
19715
|
var init_run = __esm(() => {
|
|
18984
19716
|
init_loader();
|
|
18985
19717
|
init_runner();
|
|
@@ -18988,16 +19720,105 @@ var init_run = __esm(() => {
|
|
|
18988
19720
|
init_supervisor();
|
|
18989
19721
|
});
|
|
18990
19722
|
|
|
19723
|
+
// src/cli/format-helpers.ts
|
|
19724
|
+
function formatTime(t) {
|
|
19725
|
+
return new Date(t).toISOString().slice(11, 19);
|
|
19726
|
+
}
|
|
19727
|
+
function formatElapsed(seconds) {
|
|
19728
|
+
if (seconds < 60)
|
|
19729
|
+
return `${seconds}s`;
|
|
19730
|
+
const m = Math.floor(seconds / 60);
|
|
19731
|
+
const s = seconds % 60;
|
|
19732
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
19733
|
+
}
|
|
19734
|
+
function getEventLabel(type) {
|
|
19735
|
+
return EVENT_LABELS[type] ?? type.slice(0, 5).toUpperCase();
|
|
19736
|
+
}
|
|
19737
|
+
|
|
19738
|
+
class JobColorMap {
|
|
19739
|
+
colors = new Map;
|
|
19740
|
+
nextIdx = 0;
|
|
19741
|
+
getColor(jobId) {
|
|
19742
|
+
let color = this.colors.get(jobId);
|
|
19743
|
+
if (!color) {
|
|
19744
|
+
color = JOB_COLORS[this.nextIdx % JOB_COLORS.length];
|
|
19745
|
+
this.colors.set(jobId, color);
|
|
19746
|
+
this.nextIdx++;
|
|
19747
|
+
}
|
|
19748
|
+
return color;
|
|
19749
|
+
}
|
|
19750
|
+
get(jobId) {
|
|
19751
|
+
return this.getColor(jobId);
|
|
19752
|
+
}
|
|
19753
|
+
has(jobId) {
|
|
19754
|
+
return this.colors.has(jobId);
|
|
19755
|
+
}
|
|
19756
|
+
get size() {
|
|
19757
|
+
return this.colors.size;
|
|
19758
|
+
}
|
|
19759
|
+
}
|
|
19760
|
+
function formatEventLine(event, options) {
|
|
19761
|
+
const ts = dim6(formatTime(event.t));
|
|
19762
|
+
const label = options.colorize(bold6(getEventLabel(event.type).padEnd(5)));
|
|
19763
|
+
const prefix = `${options.colorize(`[${options.jobId}]`)} ${options.specialist}${options.beadId ? ` ${dim6(`[${options.beadId}]`)}` : ""}`;
|
|
19764
|
+
const detailParts = [];
|
|
19765
|
+
if (event.type === "meta") {
|
|
19766
|
+
detailParts.push(`model=${event.model}`);
|
|
19767
|
+
detailParts.push(`backend=${event.backend}`);
|
|
19768
|
+
} else if (event.type === "tool") {
|
|
19769
|
+
detailParts.push(`tool=${event.tool}`);
|
|
19770
|
+
detailParts.push(`phase=${event.phase}`);
|
|
19771
|
+
if (event.phase === "end") {
|
|
19772
|
+
detailParts.push(`ok=${event.is_error ? "false" : "true"}`);
|
|
19773
|
+
}
|
|
19774
|
+
} else if (event.type === "run_complete") {
|
|
19775
|
+
detailParts.push(`status=${event.status}`);
|
|
19776
|
+
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
|
|
19777
|
+
if (event.error) {
|
|
19778
|
+
detailParts.push(`error=${event.error}`);
|
|
19779
|
+
}
|
|
19780
|
+
} else if (event.type === "done" || event.type === "agent_end") {
|
|
19781
|
+
detailParts.push("status=COMPLETE");
|
|
19782
|
+
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s ?? 0)}`);
|
|
19783
|
+
} else if (event.type === "run_start") {
|
|
19784
|
+
detailParts.push(`specialist=${event.specialist}`);
|
|
19785
|
+
if (event.bead_id) {
|
|
19786
|
+
detailParts.push(`bead=${event.bead_id}`);
|
|
19787
|
+
}
|
|
19788
|
+
} else if (event.type === "text") {
|
|
19789
|
+
detailParts.push("kind=assistant");
|
|
19790
|
+
} else if (event.type === "thinking") {
|
|
19791
|
+
detailParts.push("kind=model");
|
|
19792
|
+
}
|
|
19793
|
+
const detail = detailParts.length > 0 ? dim6(detailParts.join(" ")) : "";
|
|
19794
|
+
return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
|
|
19795
|
+
}
|
|
19796
|
+
var dim6 = (s) => `\x1B[2m${s}\x1B[0m`, bold6 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow5 = (s) => `\x1B[33m${s}\x1B[0m`, red = (s) => `\x1B[31m${s}\x1B[0m`, green6 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
|
|
19797
|
+
var init_format_helpers = __esm(() => {
|
|
19798
|
+
JOB_COLORS = [cyan4, yellow5, magenta, green6, blue, red];
|
|
19799
|
+
EVENT_LABELS = {
|
|
19800
|
+
run_start: "START",
|
|
19801
|
+
meta: "META",
|
|
19802
|
+
thinking: "THINK",
|
|
19803
|
+
tool: "TOOL",
|
|
19804
|
+
text: "TEXT",
|
|
19805
|
+
run_complete: "DONE",
|
|
19806
|
+
done: "DONE",
|
|
19807
|
+
agent_end: "DONE",
|
|
19808
|
+
error: "ERR"
|
|
19809
|
+
};
|
|
19810
|
+
});
|
|
19811
|
+
|
|
18991
19812
|
// src/cli/status.ts
|
|
18992
19813
|
var exports_status = {};
|
|
18993
19814
|
__export(exports_status, {
|
|
18994
19815
|
run: () => run8
|
|
18995
19816
|
});
|
|
18996
|
-
import { spawnSync as
|
|
18997
|
-
import { existsSync as
|
|
18998
|
-
import { join as
|
|
19817
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
19818
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
19819
|
+
import { join as join11 } from "node:path";
|
|
18999
19820
|
function ok2(msg) {
|
|
19000
|
-
console.log(` ${
|
|
19821
|
+
console.log(` ${green6("✓")} ${msg}`);
|
|
19001
19822
|
}
|
|
19002
19823
|
function warn(msg) {
|
|
19003
19824
|
console.log(` ${yellow5("○")} ${msg}`);
|
|
@@ -19014,7 +19835,7 @@ function section(label) {
|
|
|
19014
19835
|
${bold6(`── ${label} ${line}`)}`);
|
|
19015
19836
|
}
|
|
19016
19837
|
function cmd(bin, args) {
|
|
19017
|
-
const r =
|
|
19838
|
+
const r = spawnSync6(bin, args, {
|
|
19018
19839
|
encoding: "utf8",
|
|
19019
19840
|
stdio: "pipe",
|
|
19020
19841
|
timeout: 5000
|
|
@@ -19022,9 +19843,9 @@ function cmd(bin, args) {
|
|
|
19022
19843
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
19023
19844
|
}
|
|
19024
19845
|
function isInstalled(bin) {
|
|
19025
|
-
return
|
|
19846
|
+
return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
19026
19847
|
}
|
|
19027
|
-
function
|
|
19848
|
+
function formatElapsed2(s) {
|
|
19028
19849
|
if (s.elapsed_s === undefined)
|
|
19029
19850
|
return "...";
|
|
19030
19851
|
const m = Math.floor(s.elapsed_s / 60);
|
|
@@ -19036,7 +19857,7 @@ function statusColor(status) {
|
|
|
19036
19857
|
case "running":
|
|
19037
19858
|
return cyan4(status);
|
|
19038
19859
|
case "done":
|
|
19039
|
-
return
|
|
19860
|
+
return green6(status);
|
|
19040
19861
|
case "error":
|
|
19041
19862
|
return red(status);
|
|
19042
19863
|
case "starting":
|
|
@@ -19057,11 +19878,11 @@ async function run8() {
|
|
|
19057
19878
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
19058
19879
|
const bdInstalled = isInstalled("bd");
|
|
19059
19880
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
19060
|
-
const beadsPresent =
|
|
19881
|
+
const beadsPresent = existsSync7(join11(process.cwd(), ".beads"));
|
|
19061
19882
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
19062
|
-
const jobsDir =
|
|
19883
|
+
const jobsDir = join11(process.cwd(), ".specialists", "jobs");
|
|
19063
19884
|
let jobs = [];
|
|
19064
|
-
if (
|
|
19885
|
+
if (existsSync7(jobsDir)) {
|
|
19065
19886
|
const supervisor = new Supervisor({
|
|
19066
19887
|
runner: null,
|
|
19067
19888
|
runOptions: null,
|
|
@@ -19135,7 +19956,7 @@ ${bold6("specialists status")}
|
|
|
19135
19956
|
}
|
|
19136
19957
|
section("pi (coding agent runtime)");
|
|
19137
19958
|
if (!piInstalled) {
|
|
19138
|
-
fail(`pi not installed —
|
|
19959
|
+
fail(`pi not installed — install ${yellow5("pi")} first`);
|
|
19139
19960
|
} else {
|
|
19140
19961
|
const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
|
|
19141
19962
|
const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim6(`(${[...piProviders].join(", ")})`)} ` : yellow5("no providers configured — run pi config");
|
|
@@ -19143,7 +19964,7 @@ ${bold6("specialists status")}
|
|
|
19143
19964
|
}
|
|
19144
19965
|
section("beads (issue tracker)");
|
|
19145
19966
|
if (!bdInstalled) {
|
|
19146
|
-
fail(`bd not installed —
|
|
19967
|
+
fail(`bd not installed — install ${yellow5("bd")} first`);
|
|
19147
19968
|
} else {
|
|
19148
19969
|
ok2(`bd installed${bdVersion?.ok ? ` ${dim6(bdVersion.stdout)}` : ""}`);
|
|
19149
19970
|
if (beadsPresent) {
|
|
@@ -19163,17 +19984,17 @@ ${bold6("specialists status")}
|
|
|
19163
19984
|
if (jobs.length > 0) {
|
|
19164
19985
|
section("Active Jobs");
|
|
19165
19986
|
for (const job of jobs) {
|
|
19166
|
-
const elapsed =
|
|
19987
|
+
const elapsed = formatElapsed2(job);
|
|
19167
19988
|
const detail = job.status === "error" ? red(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim6(`tool: ${job.current_tool}`) : dim6(job.current_event ?? "");
|
|
19168
19989
|
console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
|
|
19169
19990
|
}
|
|
19170
19991
|
}
|
|
19171
19992
|
console.log();
|
|
19172
19993
|
}
|
|
19173
|
-
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`;
|
|
19174
19994
|
var init_status = __esm(() => {
|
|
19175
19995
|
init_loader();
|
|
19176
19996
|
init_supervisor();
|
|
19997
|
+
init_format_helpers();
|
|
19177
19998
|
});
|
|
19178
19999
|
|
|
19179
20000
|
// src/cli/result.ts
|
|
@@ -19181,15 +20002,15 @@ var exports_result = {};
|
|
|
19181
20002
|
__export(exports_result, {
|
|
19182
20003
|
run: () => run9
|
|
19183
20004
|
});
|
|
19184
|
-
import { existsSync as
|
|
19185
|
-
import { join as
|
|
20005
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
20006
|
+
import { join as join12 } from "node:path";
|
|
19186
20007
|
async function run9() {
|
|
19187
20008
|
const jobId = process.argv[3];
|
|
19188
20009
|
if (!jobId) {
|
|
19189
|
-
console.error("Usage: specialists result <job-id>");
|
|
20010
|
+
console.error("Usage: specialists|sp result <job-id>");
|
|
19190
20011
|
process.exit(1);
|
|
19191
20012
|
}
|
|
19192
|
-
const jobsDir =
|
|
20013
|
+
const jobsDir = join12(process.cwd(), ".specialists", "jobs");
|
|
19193
20014
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19194
20015
|
const status = supervisor.readStatus(jobId);
|
|
19195
20016
|
if (!status) {
|
|
@@ -19206,118 +20027,453 @@ async function run9() {
|
|
|
19206
20027
|
`);
|
|
19207
20028
|
process.exit(1);
|
|
19208
20029
|
}
|
|
19209
|
-
const resultPath =
|
|
19210
|
-
if (!
|
|
20030
|
+
const resultPath = join12(jobsDir, jobId, "result.txt");
|
|
20031
|
+
if (!existsSync8(resultPath)) {
|
|
19211
20032
|
console.error(`Result file not found for job ${jobId}`);
|
|
19212
20033
|
process.exit(1);
|
|
19213
20034
|
}
|
|
19214
|
-
process.stdout.write(
|
|
20035
|
+
process.stdout.write(readFileSync5(resultPath, "utf-8"));
|
|
19215
20036
|
}
|
|
19216
20037
|
var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19217
20038
|
var init_result = __esm(() => {
|
|
19218
20039
|
init_supervisor();
|
|
19219
20040
|
});
|
|
19220
20041
|
|
|
20042
|
+
// src/specialist/timeline-query.ts
|
|
20043
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
20044
|
+
import { join as join13 } from "node:path";
|
|
20045
|
+
function readJobEvents(jobDir) {
|
|
20046
|
+
const eventsPath = join13(jobDir, "events.jsonl");
|
|
20047
|
+
if (!existsSync9(eventsPath))
|
|
20048
|
+
return [];
|
|
20049
|
+
const content = readFileSync6(eventsPath, "utf-8");
|
|
20050
|
+
const lines = content.split(`
|
|
20051
|
+
`).filter(Boolean);
|
|
20052
|
+
const events = [];
|
|
20053
|
+
for (const line of lines) {
|
|
20054
|
+
const event = parseTimelineEvent(line);
|
|
20055
|
+
if (event)
|
|
20056
|
+
events.push(event);
|
|
20057
|
+
}
|
|
20058
|
+
events.sort(compareTimelineEvents);
|
|
20059
|
+
return events;
|
|
20060
|
+
}
|
|
20061
|
+
function readAllJobEvents(jobsDir) {
|
|
20062
|
+
if (!existsSync9(jobsDir))
|
|
20063
|
+
return [];
|
|
20064
|
+
const batches = [];
|
|
20065
|
+
const entries = readdirSync3(jobsDir);
|
|
20066
|
+
for (const entry of entries) {
|
|
20067
|
+
const jobDir = join13(jobsDir, entry);
|
|
20068
|
+
try {
|
|
20069
|
+
const stat2 = __require("node:fs").statSync(jobDir);
|
|
20070
|
+
if (!stat2.isDirectory())
|
|
20071
|
+
continue;
|
|
20072
|
+
} catch {
|
|
20073
|
+
continue;
|
|
20074
|
+
}
|
|
20075
|
+
const jobId = entry;
|
|
20076
|
+
const statusPath = join13(jobDir, "status.json");
|
|
20077
|
+
let specialist = "unknown";
|
|
20078
|
+
let beadId;
|
|
20079
|
+
if (existsSync9(statusPath)) {
|
|
20080
|
+
try {
|
|
20081
|
+
const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
|
|
20082
|
+
specialist = status.specialist ?? "unknown";
|
|
20083
|
+
beadId = status.bead_id;
|
|
20084
|
+
} catch {}
|
|
20085
|
+
}
|
|
20086
|
+
const events = readJobEvents(jobDir);
|
|
20087
|
+
if (events.length > 0) {
|
|
20088
|
+
batches.push({ jobId, specialist, beadId, events });
|
|
20089
|
+
}
|
|
20090
|
+
}
|
|
20091
|
+
return batches;
|
|
20092
|
+
}
|
|
20093
|
+
function mergeTimelineEvents(batches) {
|
|
20094
|
+
const merged = [];
|
|
20095
|
+
for (const batch of batches) {
|
|
20096
|
+
for (const event of batch.events) {
|
|
20097
|
+
merged.push({
|
|
20098
|
+
jobId: batch.jobId,
|
|
20099
|
+
specialist: batch.specialist,
|
|
20100
|
+
beadId: batch.beadId,
|
|
20101
|
+
event
|
|
20102
|
+
});
|
|
20103
|
+
}
|
|
20104
|
+
}
|
|
20105
|
+
merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
|
|
20106
|
+
return merged;
|
|
20107
|
+
}
|
|
20108
|
+
function filterTimelineEvents(merged, filter) {
|
|
20109
|
+
let result = merged;
|
|
20110
|
+
if (filter.since !== undefined) {
|
|
20111
|
+
result = result.filter(({ event }) => event.t >= filter.since);
|
|
20112
|
+
}
|
|
20113
|
+
if (filter.jobId !== undefined) {
|
|
20114
|
+
result = result.filter(({ jobId }) => jobId === filter.jobId);
|
|
20115
|
+
}
|
|
20116
|
+
if (filter.specialist !== undefined) {
|
|
20117
|
+
result = result.filter(({ specialist }) => specialist === filter.specialist);
|
|
20118
|
+
}
|
|
20119
|
+
if (filter.limit !== undefined && filter.limit > 0) {
|
|
20120
|
+
result = result.slice(0, filter.limit);
|
|
20121
|
+
}
|
|
20122
|
+
return result;
|
|
20123
|
+
}
|
|
20124
|
+
function queryTimeline(jobsDir, filter = {}) {
|
|
20125
|
+
let batches = readAllJobEvents(jobsDir);
|
|
20126
|
+
if (filter.jobId !== undefined) {
|
|
20127
|
+
batches = batches.filter((b) => b.jobId === filter.jobId);
|
|
20128
|
+
}
|
|
20129
|
+
if (filter.specialist !== undefined) {
|
|
20130
|
+
batches = batches.filter((b) => b.specialist === filter.specialist);
|
|
20131
|
+
}
|
|
20132
|
+
const merged = mergeTimelineEvents(batches);
|
|
20133
|
+
return filterTimelineEvents(merged, filter);
|
|
20134
|
+
}
|
|
20135
|
+
var init_timeline_query = __esm(() => {
|
|
20136
|
+
init_timeline_events();
|
|
20137
|
+
});
|
|
20138
|
+
|
|
19221
20139
|
// src/cli/feed.ts
|
|
19222
20140
|
var exports_feed = {};
|
|
19223
20141
|
__export(exports_feed, {
|
|
19224
20142
|
run: () => run10
|
|
19225
20143
|
});
|
|
19226
|
-
import { existsSync as
|
|
19227
|
-
import { join as
|
|
19228
|
-
function
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
20144
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
20145
|
+
import { join as join14 } from "node:path";
|
|
20146
|
+
function getHumanEventKey(event) {
|
|
20147
|
+
switch (event.type) {
|
|
20148
|
+
case "meta":
|
|
20149
|
+
return `meta:${event.backend}:${event.model}`;
|
|
20150
|
+
case "tool":
|
|
20151
|
+
return `tool:${event.tool}:${event.phase}:${event.is_error ? "error" : "ok"}`;
|
|
20152
|
+
case "text":
|
|
20153
|
+
return "text";
|
|
20154
|
+
case "thinking":
|
|
20155
|
+
return "thinking";
|
|
20156
|
+
case "run_start":
|
|
20157
|
+
return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
|
|
20158
|
+
case "run_complete":
|
|
20159
|
+
return `run_complete:${event.status}:${event.error ?? ""}`;
|
|
20160
|
+
case "done":
|
|
20161
|
+
case "agent_end":
|
|
20162
|
+
return `complete:${event.type}`;
|
|
20163
|
+
default:
|
|
20164
|
+
return event.type;
|
|
19237
20165
|
}
|
|
19238
20166
|
}
|
|
19239
|
-
function
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
20167
|
+
function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
|
|
20168
|
+
if (event.type === "meta") {
|
|
20169
|
+
const metaKey = `${event.backend}:${event.model}`;
|
|
20170
|
+
if (seenMetaKey.get(jobId) === metaKey)
|
|
20171
|
+
return true;
|
|
20172
|
+
seenMetaKey.set(jobId, metaKey);
|
|
20173
|
+
}
|
|
20174
|
+
const key = getHumanEventKey(event);
|
|
20175
|
+
if (lastPrintedEventKey.get(jobId) === key)
|
|
20176
|
+
return true;
|
|
20177
|
+
lastPrintedEventKey.set(jobId, key);
|
|
20178
|
+
return false;
|
|
20179
|
+
}
|
|
20180
|
+
function parseSince(value) {
|
|
20181
|
+
if (value.includes("T") || value.includes("-")) {
|
|
20182
|
+
return new Date(value).getTime();
|
|
20183
|
+
}
|
|
20184
|
+
const match = value.match(/^(\d+)([smhd])$/);
|
|
20185
|
+
if (match) {
|
|
20186
|
+
const num = parseInt(match[1], 10);
|
|
20187
|
+
const unit = match[2];
|
|
20188
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
20189
|
+
return Date.now() - num * multipliers[unit];
|
|
19244
20190
|
}
|
|
19245
|
-
return
|
|
20191
|
+
return;
|
|
19246
20192
|
}
|
|
19247
|
-
|
|
19248
|
-
const argv = process.argv.slice(3);
|
|
20193
|
+
function parseArgs5(argv) {
|
|
19249
20194
|
let jobId;
|
|
20195
|
+
let specialist;
|
|
20196
|
+
let since;
|
|
20197
|
+
let limit = 100;
|
|
19250
20198
|
let follow = false;
|
|
20199
|
+
let forever = false;
|
|
20200
|
+
let json = false;
|
|
19251
20201
|
for (let i = 0;i < argv.length; i++) {
|
|
19252
20202
|
if (argv[i] === "--job" && argv[i + 1]) {
|
|
19253
20203
|
jobId = argv[++i];
|
|
19254
20204
|
continue;
|
|
19255
20205
|
}
|
|
20206
|
+
if (argv[i] === "--specialist" && argv[i + 1]) {
|
|
20207
|
+
specialist = argv[++i];
|
|
20208
|
+
continue;
|
|
20209
|
+
}
|
|
20210
|
+
if (argv[i] === "--since" && argv[i + 1]) {
|
|
20211
|
+
since = parseSince(argv[++i]);
|
|
20212
|
+
continue;
|
|
20213
|
+
}
|
|
20214
|
+
if (argv[i] === "--limit" && argv[i + 1]) {
|
|
20215
|
+
limit = parseInt(argv[++i], 10);
|
|
20216
|
+
continue;
|
|
20217
|
+
}
|
|
19256
20218
|
if (argv[i] === "--follow" || argv[i] === "-f") {
|
|
19257
20219
|
follow = true;
|
|
19258
20220
|
continue;
|
|
19259
20221
|
}
|
|
20222
|
+
if (argv[i] === "--forever") {
|
|
20223
|
+
forever = true;
|
|
20224
|
+
continue;
|
|
20225
|
+
}
|
|
20226
|
+
if (argv[i] === "--json") {
|
|
20227
|
+
json = true;
|
|
20228
|
+
continue;
|
|
20229
|
+
}
|
|
19260
20230
|
if (!jobId && !argv[i].startsWith("--"))
|
|
19261
20231
|
jobId = argv[i];
|
|
19262
20232
|
}
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
20233
|
+
return { jobId, specialist, since, limit, follow, forever, json };
|
|
20234
|
+
}
|
|
20235
|
+
function printSnapshot(merged, options) {
|
|
20236
|
+
if (merged.length === 0) {
|
|
20237
|
+
if (!options.json)
|
|
20238
|
+
console.log(dim6("No events found."));
|
|
20239
|
+
return;
|
|
19266
20240
|
}
|
|
19267
|
-
const
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19272
|
-
|
|
19273
|
-
|
|
20241
|
+
const colorMap = new JobColorMap;
|
|
20242
|
+
if (options.json) {
|
|
20243
|
+
for (const { jobId, specialist, beadId, event } of merged) {
|
|
20244
|
+
console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
|
|
20245
|
+
}
|
|
20246
|
+
return;
|
|
20247
|
+
}
|
|
20248
|
+
const lastPrintedEventKey = new Map;
|
|
20249
|
+
const seenMetaKey = new Map;
|
|
20250
|
+
for (const { jobId, specialist, beadId, event } of merged) {
|
|
20251
|
+
if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
|
|
20252
|
+
continue;
|
|
20253
|
+
const colorize = colorMap.get(jobId);
|
|
20254
|
+
console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
|
|
20255
|
+
}
|
|
20256
|
+
}
|
|
20257
|
+
function isCompletionEvent(event) {
|
|
20258
|
+
return isRunCompleteEvent(event) || event.type === "done" || event.type === "agent_end";
|
|
20259
|
+
}
|
|
20260
|
+
async function followMerged(jobsDir, options) {
|
|
20261
|
+
const colorMap = new JobColorMap;
|
|
20262
|
+
const lastSeenT = new Map;
|
|
20263
|
+
const completedJobs = new Set;
|
|
20264
|
+
const filteredBatches = () => readAllJobEvents(jobsDir).filter((batch) => !options.jobId || batch.jobId === options.jobId).filter((batch) => !options.specialist || batch.specialist === options.specialist);
|
|
20265
|
+
const initial = queryTimeline(jobsDir, {
|
|
20266
|
+
jobId: options.jobId,
|
|
20267
|
+
specialist: options.specialist,
|
|
20268
|
+
since: options.since,
|
|
20269
|
+
limit: options.limit
|
|
20270
|
+
});
|
|
20271
|
+
printSnapshot(initial, { ...options, json: options.json });
|
|
20272
|
+
for (const batch of filteredBatches()) {
|
|
20273
|
+
if (batch.events.length > 0) {
|
|
20274
|
+
const maxT = Math.max(...batch.events.map((event) => event.t));
|
|
20275
|
+
lastSeenT.set(batch.jobId, maxT);
|
|
20276
|
+
}
|
|
20277
|
+
if (batch.events.some(isCompletionEvent)) {
|
|
20278
|
+
completedJobs.add(batch.jobId);
|
|
19274
20279
|
}
|
|
19275
|
-
|
|
20280
|
+
}
|
|
20281
|
+
const initialBatchCount = filteredBatches().length;
|
|
20282
|
+
if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
|
|
20283
|
+
if (!options.json) {
|
|
20284
|
+
process.stderr.write(dim6(`All jobs complete.
|
|
20285
|
+
`));
|
|
20286
|
+
}
|
|
20287
|
+
return;
|
|
20288
|
+
}
|
|
20289
|
+
if (!options.json) {
|
|
20290
|
+
process.stderr.write(dim6(`Following... (Ctrl+C to stop)
|
|
20291
|
+
`));
|
|
20292
|
+
}
|
|
20293
|
+
const lastPrintedEventKey = new Map;
|
|
20294
|
+
const seenMetaKey = new Map;
|
|
20295
|
+
await new Promise((resolve2) => {
|
|
20296
|
+
const interval = setInterval(() => {
|
|
20297
|
+
const batches = filteredBatches();
|
|
20298
|
+
const newEvents = [];
|
|
20299
|
+
for (const batch of batches) {
|
|
20300
|
+
const lastT = lastSeenT.get(batch.jobId) ?? 0;
|
|
20301
|
+
for (const event of batch.events) {
|
|
20302
|
+
if (event.t > lastT) {
|
|
20303
|
+
newEvents.push({
|
|
20304
|
+
jobId: batch.jobId,
|
|
20305
|
+
specialist: batch.specialist,
|
|
20306
|
+
beadId: batch.beadId,
|
|
20307
|
+
event
|
|
20308
|
+
});
|
|
20309
|
+
}
|
|
20310
|
+
}
|
|
20311
|
+
if (batch.events.length > 0) {
|
|
20312
|
+
const maxT = Math.max(...batch.events.map((e) => e.t));
|
|
20313
|
+
lastSeenT.set(batch.jobId, maxT);
|
|
20314
|
+
}
|
|
20315
|
+
if (batch.events.some(isCompletionEvent)) {
|
|
20316
|
+
completedJobs.add(batch.jobId);
|
|
20317
|
+
}
|
|
20318
|
+
}
|
|
20319
|
+
newEvents.sort((a, b) => a.event.t - b.event.t);
|
|
20320
|
+
for (const { jobId, specialist, beadId, event } of newEvents) {
|
|
20321
|
+
if (options.json) {
|
|
20322
|
+
console.log(JSON.stringify({ jobId, specialist, beadId, ...event }));
|
|
20323
|
+
} else {
|
|
20324
|
+
if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
|
|
20325
|
+
continue;
|
|
20326
|
+
const colorize = colorMap.get(jobId);
|
|
20327
|
+
console.log(formatEventLine(event, { jobId, specialist, beadId, colorize }));
|
|
20328
|
+
}
|
|
20329
|
+
}
|
|
20330
|
+
if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
|
|
20331
|
+
clearInterval(interval);
|
|
20332
|
+
resolve2();
|
|
20333
|
+
}
|
|
20334
|
+
}, 500);
|
|
20335
|
+
});
|
|
20336
|
+
}
|
|
20337
|
+
async function run10() {
|
|
20338
|
+
const options = parseArgs5(process.argv.slice(3));
|
|
20339
|
+
const jobsDir = join14(process.cwd(), ".specialists", "jobs");
|
|
20340
|
+
if (!existsSync10(jobsDir)) {
|
|
20341
|
+
console.log(dim6("No jobs directory found."));
|
|
20342
|
+
return;
|
|
20343
|
+
}
|
|
20344
|
+
if (options.follow) {
|
|
20345
|
+
await followMerged(jobsDir, options);
|
|
19276
20346
|
return;
|
|
19277
20347
|
}
|
|
19278
|
-
const
|
|
19279
|
-
|
|
19280
|
-
|
|
19281
|
-
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
20348
|
+
const merged = queryTimeline(jobsDir, {
|
|
20349
|
+
jobId: options.jobId,
|
|
20350
|
+
specialist: options.specialist,
|
|
20351
|
+
since: options.since,
|
|
20352
|
+
limit: options.limit
|
|
20353
|
+
});
|
|
20354
|
+
printSnapshot(merged, options);
|
|
20355
|
+
}
|
|
20356
|
+
var init_feed = __esm(() => {
|
|
20357
|
+
init_timeline_events();
|
|
20358
|
+
init_timeline_query();
|
|
20359
|
+
init_format_helpers();
|
|
20360
|
+
});
|
|
20361
|
+
|
|
20362
|
+
// src/cli/steer.ts
|
|
20363
|
+
var exports_steer = {};
|
|
20364
|
+
__export(exports_steer, {
|
|
20365
|
+
run: () => run11
|
|
20366
|
+
});
|
|
20367
|
+
import { join as join15 } from "node:path";
|
|
20368
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
20369
|
+
async function run11() {
|
|
20370
|
+
const jobId = process.argv[3];
|
|
20371
|
+
const message = process.argv[4];
|
|
20372
|
+
if (!jobId || !message) {
|
|
20373
|
+
console.error('Usage: specialists|sp steer <job-id> "<message>"');
|
|
20374
|
+
process.exit(1);
|
|
20375
|
+
}
|
|
20376
|
+
const jobsDir = join15(process.cwd(), ".specialists", "jobs");
|
|
20377
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
20378
|
+
const status = supervisor.readStatus(jobId);
|
|
20379
|
+
if (!status) {
|
|
20380
|
+
console.error(`No job found: ${jobId}`);
|
|
20381
|
+
process.exit(1);
|
|
20382
|
+
}
|
|
20383
|
+
if (status.status === "done" || status.status === "error") {
|
|
20384
|
+
process.stderr.write(`Job ${jobId} is already ${status.status}.
|
|
20385
|
+
`);
|
|
20386
|
+
process.exit(1);
|
|
20387
|
+
}
|
|
20388
|
+
if (!status.fifo_path) {
|
|
20389
|
+
process.stderr.write(`${red3("Error:")} Job ${jobId} has no steer pipe.
|
|
20390
|
+
`);
|
|
20391
|
+
process.stderr.write(`Only jobs started with --background support mid-run steering.
|
|
20392
|
+
`);
|
|
20393
|
+
process.exit(1);
|
|
20394
|
+
}
|
|
20395
|
+
try {
|
|
20396
|
+
const payload = JSON.stringify({ type: "steer", message }) + `
|
|
20397
|
+
`;
|
|
20398
|
+
writeFileSync6(status.fifo_path, payload, { flag: "a" });
|
|
20399
|
+
process.stdout.write(`${green7("✓")} Steer message sent to job ${jobId}
|
|
20400
|
+
`);
|
|
20401
|
+
} catch (err) {
|
|
20402
|
+
process.stderr.write(`${red3("Error:")} Failed to write to steer pipe: ${err?.message}
|
|
20403
|
+
`);
|
|
20404
|
+
process.exit(1);
|
|
20405
|
+
}
|
|
20406
|
+
}
|
|
20407
|
+
var green7 = (s) => `\x1B[32m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
20408
|
+
var init_steer = __esm(() => {
|
|
20409
|
+
init_supervisor();
|
|
20410
|
+
});
|
|
20411
|
+
|
|
20412
|
+
// src/cli/follow-up.ts
|
|
20413
|
+
var exports_follow_up = {};
|
|
20414
|
+
__export(exports_follow_up, {
|
|
20415
|
+
run: () => run12
|
|
20416
|
+
});
|
|
20417
|
+
import { join as join16 } from "node:path";
|
|
20418
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
20419
|
+
async function run12() {
|
|
20420
|
+
const jobId = process.argv[3];
|
|
20421
|
+
const message = process.argv[4];
|
|
20422
|
+
if (!jobId || !message) {
|
|
20423
|
+
console.error('Usage: specialists|sp follow-up <job-id> "<message>"');
|
|
20424
|
+
process.exit(1);
|
|
20425
|
+
}
|
|
20426
|
+
const jobsDir = join16(process.cwd(), ".specialists", "jobs");
|
|
20427
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
20428
|
+
const status = supervisor.readStatus(jobId);
|
|
20429
|
+
if (!status) {
|
|
20430
|
+
console.error(`No job found: ${jobId}`);
|
|
20431
|
+
process.exit(1);
|
|
20432
|
+
}
|
|
20433
|
+
if (status.status !== "waiting") {
|
|
20434
|
+
process.stderr.write(`${red4("Error:")} Job ${jobId} is not in waiting state (status: ${status.status}).
|
|
20435
|
+
`);
|
|
20436
|
+
process.stderr.write(`Only jobs started with --keep-alive and --background support follow-up prompts.
|
|
20437
|
+
`);
|
|
20438
|
+
process.exit(1);
|
|
20439
|
+
}
|
|
20440
|
+
if (!status.fifo_path) {
|
|
20441
|
+
process.stderr.write(`${red4("Error:")} Job ${jobId} has no steer pipe.
|
|
20442
|
+
`);
|
|
20443
|
+
process.exit(1);
|
|
20444
|
+
}
|
|
20445
|
+
try {
|
|
20446
|
+
const payload = JSON.stringify({ type: "prompt", message }) + `
|
|
20447
|
+
`;
|
|
20448
|
+
writeFileSync7(status.fifo_path, payload, { flag: "a" });
|
|
20449
|
+
process.stdout.write(`${green8("✓")} Follow-up sent to job ${jobId}
|
|
19296
20450
|
`);
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
}
|
|
19301
|
-
|
|
20451
|
+
process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
|
|
20452
|
+
`);
|
|
20453
|
+
} catch (err) {
|
|
20454
|
+
process.stderr.write(`${red4("Error:")} Failed to write to steer pipe: ${err?.message}
|
|
20455
|
+
`);
|
|
20456
|
+
process.exit(1);
|
|
20457
|
+
}
|
|
19302
20458
|
}
|
|
19303
|
-
var
|
|
19304
|
-
var
|
|
20459
|
+
var green8 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
20460
|
+
var init_follow_up = __esm(() => {
|
|
19305
20461
|
init_supervisor();
|
|
19306
20462
|
});
|
|
19307
20463
|
|
|
19308
20464
|
// src/cli/stop.ts
|
|
19309
20465
|
var exports_stop = {};
|
|
19310
20466
|
__export(exports_stop, {
|
|
19311
|
-
run: () =>
|
|
20467
|
+
run: () => run13
|
|
19312
20468
|
});
|
|
19313
|
-
import { join as
|
|
19314
|
-
async function
|
|
20469
|
+
import { join as join17 } from "node:path";
|
|
20470
|
+
async function run13() {
|
|
19315
20471
|
const jobId = process.argv[3];
|
|
19316
20472
|
if (!jobId) {
|
|
19317
|
-
console.error("Usage: specialists stop <job-id>");
|
|
20473
|
+
console.error("Usage: specialists|sp stop <job-id>");
|
|
19318
20474
|
process.exit(1);
|
|
19319
20475
|
}
|
|
19320
|
-
const jobsDir =
|
|
20476
|
+
const jobsDir = join17(process.cwd(), ".specialists", "jobs");
|
|
19321
20477
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19322
20478
|
const status = supervisor.readStatus(jobId);
|
|
19323
20479
|
if (!status) {
|
|
@@ -19325,31 +20481,31 @@ async function run11() {
|
|
|
19325
20481
|
process.exit(1);
|
|
19326
20482
|
}
|
|
19327
20483
|
if (status.status === "done" || status.status === "error") {
|
|
19328
|
-
process.stderr.write(`${
|
|
20484
|
+
process.stderr.write(`${dim8(`Job ${jobId} is already ${status.status}.`)}
|
|
19329
20485
|
`);
|
|
19330
20486
|
return;
|
|
19331
20487
|
}
|
|
19332
20488
|
if (!status.pid) {
|
|
19333
|
-
process.stderr.write(`${
|
|
20489
|
+
process.stderr.write(`${red5(`No PID recorded for job ${jobId}.`)}
|
|
19334
20490
|
`);
|
|
19335
20491
|
process.exit(1);
|
|
19336
20492
|
}
|
|
19337
20493
|
try {
|
|
19338
20494
|
process.kill(status.pid, "SIGTERM");
|
|
19339
|
-
process.stdout.write(`${
|
|
20495
|
+
process.stdout.write(`${green9("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
19340
20496
|
`);
|
|
19341
20497
|
} catch (err) {
|
|
19342
20498
|
if (err.code === "ESRCH") {
|
|
19343
|
-
process.stderr.write(`${
|
|
20499
|
+
process.stderr.write(`${red5(`Process ${status.pid} not found.`)} Job may have already completed.
|
|
19344
20500
|
`);
|
|
19345
20501
|
} else {
|
|
19346
|
-
process.stderr.write(`${
|
|
20502
|
+
process.stderr.write(`${red5("Error:")} ${err.message}
|
|
19347
20503
|
`);
|
|
19348
20504
|
process.exit(1);
|
|
19349
20505
|
}
|
|
19350
20506
|
}
|
|
19351
20507
|
}
|
|
19352
|
-
var
|
|
20508
|
+
var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
19353
20509
|
var init_stop = __esm(() => {
|
|
19354
20510
|
init_supervisor();
|
|
19355
20511
|
});
|
|
@@ -19357,32 +20513,33 @@ var init_stop = __esm(() => {
|
|
|
19357
20513
|
// src/cli/quickstart.ts
|
|
19358
20514
|
var exports_quickstart = {};
|
|
19359
20515
|
__export(exports_quickstart, {
|
|
19360
|
-
run: () =>
|
|
20516
|
+
run: () => run14
|
|
19361
20517
|
});
|
|
19362
20518
|
function section2(title) {
|
|
19363
20519
|
const bar = "─".repeat(60);
|
|
19364
20520
|
return `
|
|
19365
|
-
${bold7(
|
|
19366
|
-
${
|
|
20521
|
+
${bold7(cyan5(title))}
|
|
20522
|
+
${dim9(bar)}`;
|
|
19367
20523
|
}
|
|
19368
20524
|
function cmd2(s) {
|
|
19369
|
-
return
|
|
20525
|
+
return yellow6(s);
|
|
19370
20526
|
}
|
|
19371
20527
|
function flag(s) {
|
|
19372
|
-
return
|
|
20528
|
+
return green10(s);
|
|
19373
20529
|
}
|
|
19374
|
-
async function
|
|
20530
|
+
async function run14() {
|
|
19375
20531
|
const lines = [
|
|
19376
20532
|
"",
|
|
19377
20533
|
bold7("specialists · Quick Start Guide"),
|
|
19378
|
-
|
|
20534
|
+
dim9("One MCP server. Multiple AI backends. Intelligent orchestration."),
|
|
20535
|
+
dim9("Tip: sp is a shorter alias — sp run, sp list, sp feed etc. work identically."),
|
|
19379
20536
|
""
|
|
19380
20537
|
];
|
|
19381
20538
|
lines.push(section2("1. Installation"));
|
|
19382
20539
|
lines.push("");
|
|
19383
20540
|
lines.push(` ${cmd2("npm install -g @jaggerxtrm/specialists")} # install globally`);
|
|
19384
|
-
lines.push(` ${cmd2("specialists install")} #
|
|
19385
|
-
lines.push(` ${
|
|
20541
|
+
lines.push(` ${cmd2("specialists install")} # project setup:`);
|
|
20542
|
+
lines.push(` ${dim9(" # checks pi · bd · xt, then wires MCP + hooks")}`);
|
|
19386
20543
|
lines.push("");
|
|
19387
20544
|
lines.push(` Verify everything is healthy:`);
|
|
19388
20545
|
lines.push(` ${cmd2("specialists status")} # shows pi, beads, MCP, active jobs`);
|
|
@@ -19393,9 +20550,9 @@ async function run12() {
|
|
|
19393
20550
|
lines.push(` ${cmd2("specialists init")} # creates specialists/, .specialists/, AGENTS.md`);
|
|
19394
20551
|
lines.push("");
|
|
19395
20552
|
lines.push(` What this creates:`);
|
|
19396
|
-
lines.push(` ${
|
|
19397
|
-
lines.push(` ${
|
|
19398
|
-
lines.push(` ${
|
|
20553
|
+
lines.push(` ${dim9("specialists/")} — put your .specialist.yaml files here`);
|
|
20554
|
+
lines.push(` ${dim9(".specialists/")} — runtime data (jobs/, ready/) — gitignored`);
|
|
20555
|
+
lines.push(` ${dim9("AGENTS.md")} — context block injected into Claude sessions`);
|
|
19399
20556
|
lines.push("");
|
|
19400
20557
|
lines.push(section2("3. Discover Specialists"));
|
|
19401
20558
|
lines.push("");
|
|
@@ -19406,24 +20563,24 @@ async function run12() {
|
|
|
19406
20563
|
lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
|
|
19407
20564
|
lines.push("");
|
|
19408
20565
|
lines.push(` Scopes (searched in order):`);
|
|
19409
|
-
lines.push(` ${
|
|
19410
|
-
lines.push(` ${
|
|
19411
|
-
lines.push(` ${
|
|
20566
|
+
lines.push(` ${blue2("project")} ./specialists/*.specialist.yaml`);
|
|
20567
|
+
lines.push(` ${blue2("user")} ~/.specialists/*.specialist.yaml`);
|
|
20568
|
+
lines.push(` ${blue2("system")} bundled specialists (shipped with the package)`);
|
|
19412
20569
|
lines.push("");
|
|
19413
20570
|
lines.push(section2("4. Running a Specialist"));
|
|
19414
20571
|
lines.push("");
|
|
19415
20572
|
lines.push(` ${bold7("Foreground")} (streams output to stdout):`);
|
|
19416
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${
|
|
20573
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"Review src/api.ts for security issues"')}`);
|
|
19417
20574
|
lines.push("");
|
|
19418
20575
|
lines.push(` ${bold7("Background")} (returns a job ID immediately):`);
|
|
19419
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${
|
|
19420
|
-
lines.push(` ${
|
|
20576
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim9('"..."')} ${flag("--background")}`);
|
|
20577
|
+
lines.push(` ${dim9(" # → Job started: job_a1b2c3d4")}`);
|
|
19421
20578
|
lines.push("");
|
|
19422
20579
|
lines.push(` Override model for one run:`);
|
|
19423
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${
|
|
20580
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim9("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim9('"..."')}`);
|
|
19424
20581
|
lines.push("");
|
|
19425
20582
|
lines.push(` Run without beads issue tracking:`);
|
|
19426
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${
|
|
20583
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim9('"..."')}`);
|
|
19427
20584
|
lines.push("");
|
|
19428
20585
|
lines.push(` Pipe a prompt from stdin:`);
|
|
19429
20586
|
lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
|
|
@@ -19437,25 +20594,37 @@ async function run12() {
|
|
|
19437
20594
|
lines.push(` ${bold7("Read results")} — print the final output:`);
|
|
19438
20595
|
lines.push(` ${cmd2("specialists result job_a1b2c3d4")} # exits 1 if still running`);
|
|
19439
20596
|
lines.push("");
|
|
20597
|
+
lines.push(` ${bold7("Steer a running job")} — redirect the agent mid-run without cancelling:`);
|
|
20598
|
+
lines.push(` ${cmd2("specialists steer job_a1b2c3d4")} ${flag('"focus only on supervisor.ts"')}`);
|
|
20599
|
+
lines.push(` ${dim9(" # delivered after current tool calls finish, before the next LLM call")}`);
|
|
20600
|
+
lines.push("");
|
|
20601
|
+
lines.push(` ${bold7("Keep-alive multi-turn")} — start with ${flag("--keep-alive")}, then follow up:`);
|
|
20602
|
+
lines.push(` ${cmd2("specialists run bug-hunt")} ${flag("--bead unitAI-abc --keep-alive --background")}`);
|
|
20603
|
+
lines.push(` ${dim9(" # → Job started: a1b2c3 (status: waiting after first turn)")}`);
|
|
20604
|
+
lines.push(` ${cmd2("specialists result a1b2c3")} # read first turn`);
|
|
20605
|
+
lines.push(` ${cmd2("specialists follow-up a1b2c3")} ${flag('"now write the fix"')} # next turn, same Pi context`);
|
|
20606
|
+
lines.push(` ${cmd2("specialists feed a1b2c3")} ${flag("--follow")} # watch response`);
|
|
20607
|
+
lines.push("");
|
|
19440
20608
|
lines.push(` ${bold7("Cancel a job")}:`);
|
|
19441
20609
|
lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
|
|
19442
20610
|
lines.push("");
|
|
19443
|
-
lines.push(` ${bold7("Job files")} in ${
|
|
19444
|
-
lines.push(` ${
|
|
19445
|
-
lines.push(` ${
|
|
19446
|
-
lines.push(` ${
|
|
20611
|
+
lines.push(` ${bold7("Job files")} in ${dim9(".specialists/jobs/<job-id>/")}:`);
|
|
20612
|
+
lines.push(` ${dim9("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
|
|
20613
|
+
lines.push(` ${dim9("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
|
|
20614
|
+
lines.push(` ${dim9("result.txt")} — final output (written when status=done)`);
|
|
20615
|
+
lines.push(` ${dim9("steer.pipe")} — named FIFO for mid-run steering (removed on job completion)`);
|
|
19447
20616
|
lines.push("");
|
|
19448
20617
|
lines.push(section2("6. Editing Specialists"));
|
|
19449
20618
|
lines.push("");
|
|
19450
20619
|
lines.push(` Change a field without opening the YAML manually:`);
|
|
19451
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${
|
|
19452
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${
|
|
19453
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${
|
|
19454
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${
|
|
19455
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${
|
|
20620
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("anthropic/claude-sonnet-4-6")}`);
|
|
20621
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${dim9('"Updated description"')}`);
|
|
20622
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${dim9("120000")}`);
|
|
20623
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${dim9("HIGH")}`);
|
|
20624
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${dim9("analysis,security,review")}`);
|
|
19456
20625
|
lines.push("");
|
|
19457
20626
|
lines.push(` Preview without writing:`);
|
|
19458
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${
|
|
20627
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim9("...")} ${flag("--dry-run")}`);
|
|
19459
20628
|
lines.push("");
|
|
19460
20629
|
lines.push(section2("7. .specialist.yaml Schema"));
|
|
19461
20630
|
lines.push("");
|
|
@@ -19502,21 +20671,21 @@ async function run12() {
|
|
|
19502
20671
|
" priority: 2 # 0=critical … 4=backlog"
|
|
19503
20672
|
];
|
|
19504
20673
|
for (const l of schemaLines) {
|
|
19505
|
-
lines.push(` ${
|
|
20674
|
+
lines.push(` ${dim9(l)}`);
|
|
19506
20675
|
}
|
|
19507
20676
|
lines.push("");
|
|
19508
20677
|
lines.push(section2("8. Hook System"));
|
|
19509
20678
|
lines.push("");
|
|
19510
|
-
lines.push(` Specialists emits lifecycle events to ${
|
|
20679
|
+
lines.push(` Specialists emits lifecycle events to ${dim9(".specialists/trace.jsonl")}:`);
|
|
19511
20680
|
lines.push("");
|
|
19512
20681
|
lines.push(` ${bold7("Hook point")} ${bold7("When fired")}`);
|
|
19513
|
-
lines.push(` ${
|
|
19514
|
-
lines.push(` ${
|
|
19515
|
-
lines.push(` ${
|
|
19516
|
-
lines.push(` ${
|
|
20682
|
+
lines.push(` ${yellow6("specialist:start")} before the agent session begins`);
|
|
20683
|
+
lines.push(` ${yellow6("specialist:token")} on each streamed token (delta)`);
|
|
20684
|
+
lines.push(` ${yellow6("specialist:done")} after successful completion`);
|
|
20685
|
+
lines.push(` ${yellow6("specialist:error")} on failure or timeout`);
|
|
19517
20686
|
lines.push("");
|
|
19518
20687
|
lines.push(` Each event line in trace.jsonl:`);
|
|
19519
|
-
lines.push(` ${
|
|
20688
|
+
lines.push(` ${dim9('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
|
|
19520
20689
|
lines.push("");
|
|
19521
20690
|
lines.push(` Tail the trace file to observe all activity:`);
|
|
19522
20691
|
lines.push(` ${cmd2("tail -f .specialists/trace.jsonl | jq .")}`);
|
|
@@ -19531,7 +20700,9 @@ async function run12() {
|
|
|
19531
20700
|
lines.push(` ${bold7("run_parallel")} — concurrent or pipeline execution`);
|
|
19532
20701
|
lines.push(` ${bold7("start_specialist")} — async job start, returns job ID`);
|
|
19533
20702
|
lines.push(` ${bold7("poll_specialist")} — poll job status/output by ID`);
|
|
19534
|
-
lines.push(` ${bold7("
|
|
20703
|
+
lines.push(` ${bold7("steer_specialist")} — send a mid-run message to a running job`);
|
|
20704
|
+
lines.push(` ${bold7("follow_up_specialist")} — send a next-turn prompt to a keep-alive session`);
|
|
20705
|
+
lines.push(` ${bold7("stop_specialist")} — cancel a running job by ID`);
|
|
19535
20706
|
lines.push(` ${bold7("specialist_status")} — circuit breaker health + staleness`);
|
|
19536
20707
|
lines.push("");
|
|
19537
20708
|
lines.push(section2("10. Common Workflows"));
|
|
@@ -19544,41 +20715,51 @@ async function run12() {
|
|
|
19544
20715
|
lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
|
|
19545
20716
|
lines.push(` ${cmd2("specialists result <job-id> > analysis.md")}`);
|
|
19546
20717
|
lines.push("");
|
|
20718
|
+
lines.push(` ${bold7("Steer a job mid-run:")}`);
|
|
20719
|
+
lines.push(` ${cmd2('specialists run deep-analysis --prompt "..." --background')}`);
|
|
20720
|
+
lines.push(` ${cmd2('specialists steer <job-id> "focus only on the auth module"')}`);
|
|
20721
|
+
lines.push(` ${cmd2("specialists result <job-id>")}`);
|
|
20722
|
+
lines.push("");
|
|
20723
|
+
lines.push(` ${bold7("Multi-turn keep-alive (iterative work):")}`);
|
|
20724
|
+
lines.push(` ${cmd2("specialists run bug-hunt --bead unitAI-abc --keep-alive --background")}`);
|
|
20725
|
+
lines.push(` ${cmd2("specialists result <job-id>")}`);
|
|
20726
|
+
lines.push(` ${cmd2('specialists follow-up <job-id> "now write the fix for the root cause"')}`);
|
|
20727
|
+
lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
|
|
20728
|
+
lines.push("");
|
|
19547
20729
|
lines.push(` ${bold7("Override model for a single run:")}`);
|
|
19548
20730
|
lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
|
|
19549
20731
|
lines.push("");
|
|
19550
|
-
lines.push(
|
|
19551
|
-
lines.push(` ${
|
|
19552
|
-
lines.push(` ${
|
|
20732
|
+
lines.push(dim9("─".repeat(62)));
|
|
20733
|
+
lines.push(` ${dim9("specialists help")} command list ${dim9("specialists <cmd> --help")} per-command flags`);
|
|
20734
|
+
lines.push(` ${dim9("specialists status")} health check ${dim9("specialists models")} available models`);
|
|
19553
20735
|
lines.push("");
|
|
19554
20736
|
console.log(lines.join(`
|
|
19555
20737
|
`));
|
|
19556
20738
|
}
|
|
19557
|
-
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
20739
|
+
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green10 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
19558
20740
|
|
|
19559
20741
|
// src/cli/doctor.ts
|
|
19560
20742
|
var exports_doctor = {};
|
|
19561
20743
|
__export(exports_doctor, {
|
|
19562
|
-
run: () =>
|
|
20744
|
+
run: () => run15
|
|
19563
20745
|
});
|
|
19564
|
-
import { spawnSync as
|
|
19565
|
-
import { existsSync as
|
|
19566
|
-
import {
|
|
19567
|
-
import { join as join12 } from "node:path";
|
|
20746
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
20747
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
20748
|
+
import { join as join18 } from "node:path";
|
|
19568
20749
|
function ok3(msg) {
|
|
19569
|
-
console.log(` ${
|
|
20750
|
+
console.log(` ${green11("✓")} ${msg}`);
|
|
19570
20751
|
}
|
|
19571
20752
|
function warn2(msg) {
|
|
19572
|
-
console.log(` ${
|
|
20753
|
+
console.log(` ${yellow7("○")} ${msg}`);
|
|
19573
20754
|
}
|
|
19574
20755
|
function fail2(msg) {
|
|
19575
|
-
console.log(` ${
|
|
20756
|
+
console.log(` ${red6("✗")} ${msg}`);
|
|
19576
20757
|
}
|
|
19577
20758
|
function fix(msg) {
|
|
19578
|
-
console.log(` ${
|
|
20759
|
+
console.log(` ${dim10("→ fix:")} ${yellow7(msg)}`);
|
|
19579
20760
|
}
|
|
19580
20761
|
function hint(msg) {
|
|
19581
|
-
console.log(` ${
|
|
20762
|
+
console.log(` ${dim10(msg)}`);
|
|
19582
20763
|
}
|
|
19583
20764
|
function section3(label) {
|
|
19584
20765
|
const line = "─".repeat(Math.max(0, 38 - label.length));
|
|
@@ -19586,17 +20767,26 @@ function section3(label) {
|
|
|
19586
20767
|
${bold8(`── ${label} ${line}`)}`);
|
|
19587
20768
|
}
|
|
19588
20769
|
function sp(bin, args) {
|
|
19589
|
-
const r =
|
|
20770
|
+
const r = spawnSync7(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
|
|
19590
20771
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
19591
20772
|
}
|
|
19592
20773
|
function isInstalled2(bin) {
|
|
19593
|
-
return
|
|
20774
|
+
return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
20775
|
+
}
|
|
20776
|
+
function loadJson2(path) {
|
|
20777
|
+
if (!existsSync11(path))
|
|
20778
|
+
return null;
|
|
20779
|
+
try {
|
|
20780
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
20781
|
+
} catch {
|
|
20782
|
+
return null;
|
|
20783
|
+
}
|
|
19594
20784
|
}
|
|
19595
20785
|
function checkPi() {
|
|
19596
20786
|
section3("pi (coding agent runtime)");
|
|
19597
20787
|
if (!isInstalled2("pi")) {
|
|
19598
20788
|
fail2("pi not installed");
|
|
19599
|
-
fix("
|
|
20789
|
+
fix("install pi first");
|
|
19600
20790
|
return false;
|
|
19601
20791
|
}
|
|
19602
20792
|
const version2 = sp("pi", ["--version"]);
|
|
@@ -19609,75 +20799,94 @@ function checkPi() {
|
|
|
19609
20799
|
fix("pi config (add at least one API key)");
|
|
19610
20800
|
return false;
|
|
19611
20801
|
}
|
|
19612
|
-
ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${
|
|
20802
|
+
ok3(`pi ${vStr} — ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim10(`(${[...providers].join(", ")})`)}`);
|
|
20803
|
+
return true;
|
|
20804
|
+
}
|
|
20805
|
+
function checkBd() {
|
|
20806
|
+
section3("beads (issue tracker)");
|
|
20807
|
+
if (!isInstalled2("bd")) {
|
|
20808
|
+
fail2("bd not installed");
|
|
20809
|
+
fix("install beads (bd) first");
|
|
20810
|
+
return false;
|
|
20811
|
+
}
|
|
20812
|
+
ok3(`bd installed ${dim10(sp("bd", ["--version"]).stdout || "")}`);
|
|
20813
|
+
if (existsSync11(join18(CWD, ".beads")))
|
|
20814
|
+
ok3(".beads/ present in project");
|
|
20815
|
+
else
|
|
20816
|
+
warn2(".beads/ not found in project");
|
|
20817
|
+
return true;
|
|
20818
|
+
}
|
|
20819
|
+
function checkXt() {
|
|
20820
|
+
section3("xtrm-tools");
|
|
20821
|
+
if (!isInstalled2("xt")) {
|
|
20822
|
+
fail2("xt not installed");
|
|
20823
|
+
fix("install xtrm-tools first");
|
|
20824
|
+
return false;
|
|
20825
|
+
}
|
|
20826
|
+
ok3(`xt installed ${dim10(sp("xt", ["--version"]).stdout || "")}`);
|
|
19613
20827
|
return true;
|
|
19614
20828
|
}
|
|
19615
20829
|
function checkHooks() {
|
|
19616
|
-
section3("Claude Code hooks (
|
|
20830
|
+
section3("Claude Code hooks (2 expected)");
|
|
19617
20831
|
let allPresent = true;
|
|
19618
20832
|
for (const name of HOOK_NAMES) {
|
|
19619
|
-
const dest =
|
|
19620
|
-
if (!
|
|
19621
|
-
fail2(`${name} ${
|
|
19622
|
-
fix("specialists install
|
|
20833
|
+
const dest = join18(HOOKS_DIR, name);
|
|
20834
|
+
if (!existsSync11(dest)) {
|
|
20835
|
+
fail2(`${name} ${red6("missing")}`);
|
|
20836
|
+
fix("specialists install");
|
|
19623
20837
|
allPresent = false;
|
|
19624
20838
|
} else {
|
|
19625
20839
|
ok3(name);
|
|
19626
20840
|
}
|
|
19627
20841
|
}
|
|
19628
|
-
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
fix("specialists install (rewires hooks in settings.json)");
|
|
19644
|
-
allPresent = false;
|
|
19645
|
-
} else {
|
|
19646
|
-
hint(`Hooks wired in ${SETTINGS_FILE}`);
|
|
19647
|
-
}
|
|
19648
|
-
} catch {
|
|
19649
|
-
warn2(`Could not parse ${SETTINGS_FILE}`);
|
|
20842
|
+
const settings = loadJson2(SETTINGS_FILE);
|
|
20843
|
+
if (!settings) {
|
|
20844
|
+
warn2(`Could not read ${SETTINGS_FILE}`);
|
|
20845
|
+
fix("specialists install");
|
|
20846
|
+
return false;
|
|
20847
|
+
}
|
|
20848
|
+
const wiredCommands = new Set([
|
|
20849
|
+
...settings.UserPromptSubmit ?? [],
|
|
20850
|
+
...settings.SessionStart ?? []
|
|
20851
|
+
].flatMap((entry) => (entry.hooks ?? []).map((h) => h.command ?? "")));
|
|
20852
|
+
for (const name of HOOK_NAMES) {
|
|
20853
|
+
const expectedRelative = `node .specialists/default/hooks/${name}`;
|
|
20854
|
+
if (!wiredCommands.has(expectedRelative)) {
|
|
20855
|
+
warn2(`${name} not wired in settings.json`);
|
|
20856
|
+
fix("specialists install");
|
|
19650
20857
|
allPresent = false;
|
|
19651
20858
|
}
|
|
19652
20859
|
}
|
|
20860
|
+
if (allPresent)
|
|
20861
|
+
hint(`Hooks wired in ${SETTINGS_FILE}`);
|
|
19653
20862
|
return allPresent;
|
|
19654
20863
|
}
|
|
19655
20864
|
function checkMCP() {
|
|
19656
20865
|
section3("MCP registration");
|
|
19657
|
-
const
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
20866
|
+
const mcp = loadJson2(MCP_FILE2);
|
|
20867
|
+
const spec = mcp?.mcpServers?.specialists;
|
|
20868
|
+
if (!spec || spec.command !== "specialists") {
|
|
20869
|
+
fail2(`MCP server 'specialists' not registered in .mcp.json`);
|
|
20870
|
+
fix("specialists install");
|
|
19661
20871
|
return false;
|
|
19662
20872
|
}
|
|
19663
|
-
ok3(`MCP server '${
|
|
20873
|
+
ok3(`MCP server 'specialists' registered in ${MCP_FILE2}`);
|
|
19664
20874
|
return true;
|
|
19665
20875
|
}
|
|
19666
20876
|
function checkRuntimeDirs() {
|
|
19667
20877
|
section3(".specialists/ runtime directories");
|
|
19668
|
-
const
|
|
19669
|
-
const
|
|
19670
|
-
const
|
|
19671
|
-
const readyDir = join12(rootDir, "ready");
|
|
20878
|
+
const rootDir = join18(CWD, ".specialists");
|
|
20879
|
+
const jobsDir = join18(rootDir, "jobs");
|
|
20880
|
+
const readyDir = join18(rootDir, "ready");
|
|
19672
20881
|
let allOk = true;
|
|
19673
|
-
if (!
|
|
20882
|
+
if (!existsSync11(rootDir)) {
|
|
19674
20883
|
warn2(".specialists/ not found in current project");
|
|
19675
20884
|
fix("specialists init");
|
|
19676
20885
|
allOk = false;
|
|
19677
20886
|
} else {
|
|
19678
20887
|
ok3(".specialists/ present");
|
|
19679
20888
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
19680
|
-
if (!
|
|
20889
|
+
if (!existsSync11(subDir)) {
|
|
19681
20890
|
warn2(`.specialists/${label}/ missing — auto-creating`);
|
|
19682
20891
|
mkdirSync3(subDir, { recursive: true });
|
|
19683
20892
|
ok3(`.specialists/${label}/ created`);
|
|
@@ -19690,14 +20899,14 @@ function checkRuntimeDirs() {
|
|
|
19690
20899
|
}
|
|
19691
20900
|
function checkZombieJobs() {
|
|
19692
20901
|
section3("Background jobs");
|
|
19693
|
-
const jobsDir =
|
|
19694
|
-
if (!
|
|
20902
|
+
const jobsDir = join18(CWD, ".specialists", "jobs");
|
|
20903
|
+
if (!existsSync11(jobsDir)) {
|
|
19695
20904
|
hint("No .specialists/jobs/ — skipping");
|
|
19696
20905
|
return true;
|
|
19697
20906
|
}
|
|
19698
20907
|
let entries;
|
|
19699
20908
|
try {
|
|
19700
|
-
entries =
|
|
20909
|
+
entries = readdirSync4(jobsDir);
|
|
19701
20910
|
} catch {
|
|
19702
20911
|
entries = [];
|
|
19703
20912
|
}
|
|
@@ -19709,11 +20918,11 @@ function checkZombieJobs() {
|
|
|
19709
20918
|
let total = 0;
|
|
19710
20919
|
let running = 0;
|
|
19711
20920
|
for (const jobId of entries) {
|
|
19712
|
-
const statusPath =
|
|
19713
|
-
if (!
|
|
20921
|
+
const statusPath = join18(jobsDir, jobId, "status.json");
|
|
20922
|
+
if (!existsSync11(statusPath))
|
|
19714
20923
|
continue;
|
|
19715
20924
|
try {
|
|
19716
|
-
const status = JSON.parse(
|
|
20925
|
+
const status = JSON.parse(readFileSync7(statusPath, "utf8"));
|
|
19717
20926
|
total++;
|
|
19718
20927
|
if (status.status === "running" || status.status === "starting") {
|
|
19719
20928
|
const pid = status.pid;
|
|
@@ -19723,11 +20932,11 @@ function checkZombieJobs() {
|
|
|
19723
20932
|
process.kill(pid, 0);
|
|
19724
20933
|
alive = true;
|
|
19725
20934
|
} catch {}
|
|
19726
|
-
if (alive)
|
|
20935
|
+
if (alive)
|
|
19727
20936
|
running++;
|
|
19728
|
-
|
|
20937
|
+
else {
|
|
19729
20938
|
zombies++;
|
|
19730
|
-
warn2(`${jobId} ${
|
|
20939
|
+
warn2(`${jobId} ${yellow7("ZOMBIE")} ${dim10(`pid ${pid} not found, status=${status.status}`)}`);
|
|
19731
20940
|
fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
|
|
19732
20941
|
}
|
|
19733
20942
|
}
|
|
@@ -19740,37 +20949,36 @@ function checkZombieJobs() {
|
|
|
19740
20949
|
}
|
|
19741
20950
|
return zombies === 0;
|
|
19742
20951
|
}
|
|
19743
|
-
async function
|
|
20952
|
+
async function run15() {
|
|
19744
20953
|
console.log(`
|
|
19745
20954
|
${bold8("specialists doctor")}
|
|
19746
20955
|
`);
|
|
19747
20956
|
const piOk = checkPi();
|
|
20957
|
+
const bdOk = checkBd();
|
|
20958
|
+
const xtOk = checkXt();
|
|
19748
20959
|
const hooksOk = checkHooks();
|
|
19749
20960
|
const mcpOk = checkMCP();
|
|
19750
20961
|
const dirsOk = checkRuntimeDirs();
|
|
19751
20962
|
const jobsOk = checkZombieJobs();
|
|
19752
|
-
const allOk = piOk && hooksOk && mcpOk && dirsOk && jobsOk;
|
|
20963
|
+
const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
|
|
19753
20964
|
console.log("");
|
|
19754
20965
|
if (allOk) {
|
|
19755
|
-
console.log(` ${
|
|
20966
|
+
console.log(` ${green11("✓")} ${bold8("All checks passed")} — specialists is healthy`);
|
|
19756
20967
|
} else {
|
|
19757
|
-
console.log(` ${
|
|
19758
|
-
console.log(` ${
|
|
20968
|
+
console.log(` ${yellow7("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
|
|
20969
|
+
console.log(` ${dim10("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
|
|
19759
20970
|
}
|
|
19760
20971
|
console.log("");
|
|
19761
20972
|
}
|
|
19762
|
-
var bold8 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
20973
|
+
var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`, green11 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
|
|
19763
20974
|
var init_doctor = __esm(() => {
|
|
19764
|
-
|
|
19765
|
-
CLAUDE_DIR =
|
|
19766
|
-
|
|
19767
|
-
|
|
20975
|
+
CWD = process.cwd();
|
|
20976
|
+
CLAUDE_DIR = join18(CWD, ".claude");
|
|
20977
|
+
SPECIALISTS_DIR = join18(CWD, ".specialists");
|
|
20978
|
+
HOOKS_DIR = join18(SPECIALISTS_DIR, "default", "hooks");
|
|
20979
|
+
SETTINGS_FILE = join18(CLAUDE_DIR, "settings.json");
|
|
20980
|
+
MCP_FILE2 = join18(CWD, ".mcp.json");
|
|
19768
20981
|
HOOK_NAMES = [
|
|
19769
|
-
"specialists-main-guard.mjs",
|
|
19770
|
-
"beads-edit-gate.mjs",
|
|
19771
|
-
"beads-commit-gate.mjs",
|
|
19772
|
-
"beads-stop-gate.mjs",
|
|
19773
|
-
"beads-close-memory-prompt.mjs",
|
|
19774
20982
|
"specialists-complete.mjs",
|
|
19775
20983
|
"specialists-session-start.mjs"
|
|
19776
20984
|
];
|
|
@@ -19779,29 +20987,29 @@ var init_doctor = __esm(() => {
|
|
|
19779
20987
|
// src/cli/setup.ts
|
|
19780
20988
|
var exports_setup = {};
|
|
19781
20989
|
__export(exports_setup, {
|
|
19782
|
-
run: () =>
|
|
20990
|
+
run: () => run16
|
|
19783
20991
|
});
|
|
19784
|
-
import { existsSync as
|
|
20992
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19785
20993
|
import { homedir as homedir3 } from "node:os";
|
|
19786
|
-
import { join as
|
|
20994
|
+
import { join as join19, resolve as resolve2 } from "node:path";
|
|
19787
20995
|
function ok4(msg) {
|
|
19788
|
-
console.log(` ${
|
|
20996
|
+
console.log(` ${green12("✓")} ${msg}`);
|
|
19789
20997
|
}
|
|
19790
20998
|
function skip2(msg) {
|
|
19791
|
-
console.log(` ${
|
|
20999
|
+
console.log(` ${yellow8("○")} ${msg}`);
|
|
19792
21000
|
}
|
|
19793
21001
|
function resolveTarget(target) {
|
|
19794
21002
|
switch (target) {
|
|
19795
21003
|
case "global":
|
|
19796
|
-
return
|
|
21004
|
+
return join19(homedir3(), ".claude", "CLAUDE.md");
|
|
19797
21005
|
case "agents":
|
|
19798
|
-
return
|
|
21006
|
+
return join19(process.cwd(), "AGENTS.md");
|
|
19799
21007
|
case "project":
|
|
19800
21008
|
default:
|
|
19801
|
-
return
|
|
21009
|
+
return join19(process.cwd(), "CLAUDE.md");
|
|
19802
21010
|
}
|
|
19803
21011
|
}
|
|
19804
|
-
function
|
|
21012
|
+
function parseArgs6() {
|
|
19805
21013
|
const argv = process.argv.slice(3);
|
|
19806
21014
|
let target = "project";
|
|
19807
21015
|
let dryRun = false;
|
|
@@ -19826,30 +21034,30 @@ function parseArgs5() {
|
|
|
19826
21034
|
}
|
|
19827
21035
|
return { target, dryRun };
|
|
19828
21036
|
}
|
|
19829
|
-
async function
|
|
19830
|
-
const { target, dryRun } =
|
|
19831
|
-
const filePath =
|
|
21037
|
+
async function run16() {
|
|
21038
|
+
const { target, dryRun } = parseArgs6();
|
|
21039
|
+
const filePath = resolve2(resolveTarget(target));
|
|
19832
21040
|
const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
|
|
19833
21041
|
console.log(`
|
|
19834
21042
|
${bold9("specialists setup")}
|
|
19835
21043
|
`);
|
|
19836
|
-
console.log(` Target: ${
|
|
21044
|
+
console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
|
|
19837
21045
|
`);
|
|
19838
|
-
if (
|
|
19839
|
-
const existing =
|
|
21046
|
+
if (existsSync12(filePath)) {
|
|
21047
|
+
const existing = readFileSync8(filePath, "utf8");
|
|
19840
21048
|
if (existing.includes(MARKER)) {
|
|
19841
21049
|
skip2(`${label} already contains Specialists Workflow section`);
|
|
19842
21050
|
console.log(`
|
|
19843
|
-
${
|
|
21051
|
+
${dim11("To force-update, remove the ## Specialists Workflow section and re-run.")}
|
|
19844
21052
|
`);
|
|
19845
21053
|
return;
|
|
19846
21054
|
}
|
|
19847
21055
|
if (dryRun) {
|
|
19848
|
-
console.log(
|
|
19849
|
-
console.log(
|
|
21056
|
+
console.log(dim11("─".repeat(60)));
|
|
21057
|
+
console.log(dim11("Would append to existing file:"));
|
|
19850
21058
|
console.log("");
|
|
19851
21059
|
console.log(WORKFLOW_BLOCK);
|
|
19852
|
-
console.log(
|
|
21060
|
+
console.log(dim11("─".repeat(60)));
|
|
19853
21061
|
return;
|
|
19854
21062
|
}
|
|
19855
21063
|
const separator = existing.trimEnd().endsWith(`
|
|
@@ -19857,28 +21065,28 @@ ${bold9("specialists setup")}
|
|
|
19857
21065
|
` : `
|
|
19858
21066
|
|
|
19859
21067
|
`;
|
|
19860
|
-
|
|
21068
|
+
writeFileSync8(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
|
|
19861
21069
|
ok4(`Appended Specialists Workflow section to ${label}`);
|
|
19862
21070
|
} else {
|
|
19863
21071
|
if (dryRun) {
|
|
19864
|
-
console.log(
|
|
19865
|
-
console.log(
|
|
21072
|
+
console.log(dim11("─".repeat(60)));
|
|
21073
|
+
console.log(dim11(`Would create ${label}:`));
|
|
19866
21074
|
console.log("");
|
|
19867
21075
|
console.log(WORKFLOW_BLOCK);
|
|
19868
|
-
console.log(
|
|
21076
|
+
console.log(dim11("─".repeat(60)));
|
|
19869
21077
|
return;
|
|
19870
21078
|
}
|
|
19871
|
-
|
|
21079
|
+
writeFileSync8(filePath, WORKFLOW_BLOCK, "utf8");
|
|
19872
21080
|
ok4(`Created ${label} with Specialists Workflow section`);
|
|
19873
21081
|
}
|
|
19874
21082
|
console.log("");
|
|
19875
|
-
console.log(` ${
|
|
21083
|
+
console.log(` ${dim11("Next steps:")}`);
|
|
19876
21084
|
console.log(` • Restart Claude Code to pick up the new context`);
|
|
19877
|
-
console.log(` • Run ${
|
|
19878
|
-
console.log(` • Run ${
|
|
21085
|
+
console.log(` • Run ${yellow8("specialists list")} to see available specialists`);
|
|
21086
|
+
console.log(` • Run ${yellow8("specialist_init")} in a new session to bootstrap context`);
|
|
19879
21087
|
console.log("");
|
|
19880
21088
|
}
|
|
19881
|
-
var bold9 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
21089
|
+
var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, MARKER = "## Specialists Workflow", WORKFLOW_BLOCK = `## Specialists Workflow
|
|
19882
21090
|
|
|
19883
21091
|
> Injected by \`specialists setup\`. Keep this section — agents use it for context.
|
|
19884
21092
|
|
|
@@ -19946,61 +21154,104 @@ var init_setup = () => {};
|
|
|
19946
21154
|
// src/cli/help.ts
|
|
19947
21155
|
var exports_help = {};
|
|
19948
21156
|
__export(exports_help, {
|
|
19949
|
-
run: () =>
|
|
21157
|
+
run: () => run17
|
|
19950
21158
|
});
|
|
19951
|
-
function
|
|
19952
|
-
const
|
|
19953
|
-
return [
|
|
19954
|
-
"",
|
|
19955
|
-
bold10(cyan7(label)),
|
|
19956
|
-
...entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(colWidth)} ${dim13(desc)}`)
|
|
19957
|
-
];
|
|
21159
|
+
function formatCommands(entries) {
|
|
21160
|
+
const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
|
|
21161
|
+
return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
|
|
19958
21162
|
}
|
|
19959
|
-
async function
|
|
21163
|
+
async function run17() {
|
|
19960
21164
|
const lines = [
|
|
19961
21165
|
"",
|
|
19962
|
-
|
|
21166
|
+
"Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
|
|
21167
|
+
"",
|
|
21168
|
+
bold10("Usage:"),
|
|
21169
|
+
" specialists|sp [command]",
|
|
21170
|
+
" specialists|sp [command] --help",
|
|
19963
21171
|
"",
|
|
19964
|
-
|
|
19965
|
-
...formatGroup("Setup", SETUP),
|
|
19966
|
-
...formatGroup("Discovery", DISCOVERY),
|
|
19967
|
-
...formatGroup("Running", RUNNING),
|
|
19968
|
-
...formatGroup("Jobs", JOBS),
|
|
19969
|
-
...formatGroup("Other", OTHER),
|
|
21172
|
+
dim12(" sp is a shorter alias — sp run, sp list, sp feed etc. all work identically."),
|
|
19970
21173
|
"",
|
|
19971
|
-
|
|
19972
|
-
|
|
21174
|
+
bold10("Common flows:"),
|
|
21175
|
+
"",
|
|
21176
|
+
" Tracked work (primary)",
|
|
21177
|
+
' bd create "Task title" -t task -p 1 --json',
|
|
21178
|
+
" specialists run <name> --bead <id> [--context-depth N] [--background] [--follow]",
|
|
21179
|
+
" specialists feed -f",
|
|
21180
|
+
' bd close <id> --reason "Done"',
|
|
21181
|
+
"",
|
|
21182
|
+
" Ad-hoc work",
|
|
21183
|
+
' specialists run <name> --prompt "..."',
|
|
21184
|
+
"",
|
|
21185
|
+
" Rules",
|
|
21186
|
+
" --bead is for tracked work",
|
|
21187
|
+
" --prompt is for quick untracked work",
|
|
21188
|
+
" --context-depth defaults to 1 with --bead",
|
|
21189
|
+
" --no-beads does not disable bead reading",
|
|
21190
|
+
" --follow runs in background and streams output live",
|
|
21191
|
+
"",
|
|
21192
|
+
bold10("Core commands:"),
|
|
21193
|
+
...formatCommands(CORE_COMMANDS),
|
|
21194
|
+
"",
|
|
21195
|
+
bold10("Extended commands:"),
|
|
21196
|
+
...formatCommands(EXTENDED_COMMANDS),
|
|
21197
|
+
"",
|
|
21198
|
+
bold10("xtrm worktree commands:"),
|
|
21199
|
+
...formatCommands(WORKTREE_COMMANDS),
|
|
21200
|
+
"",
|
|
21201
|
+
bold10("Examples:"),
|
|
21202
|
+
" specialists init",
|
|
21203
|
+
" specialists list",
|
|
21204
|
+
" specialists run bug-hunt --bead unitAI-123 --background",
|
|
21205
|
+
" specialists run sync-docs --follow # run + stream live output",
|
|
21206
|
+
' specialists run codebase-explorer --prompt "Map the CLI architecture"',
|
|
21207
|
+
" specialists feed -f",
|
|
21208
|
+
' specialists steer <job-id> "focus only on supervisor.ts"',
|
|
21209
|
+
' specialists follow-up <job-id> "now write the fix"',
|
|
21210
|
+
" specialists result <job-id>",
|
|
21211
|
+
"",
|
|
21212
|
+
bold10("More help:"),
|
|
21213
|
+
" specialists quickstart Full guide and workflow reference",
|
|
21214
|
+
" specialists run --help Run command details and flags",
|
|
21215
|
+
" specialists steer --help Mid-run steering details",
|
|
21216
|
+
" specialists follow-up --help Multi-turn keep-alive details",
|
|
21217
|
+
" specialists init --help Bootstrap behavior and workflow injection",
|
|
21218
|
+
" specialists feed --help Background job monitoring details",
|
|
21219
|
+
"",
|
|
21220
|
+
dim12("Project model: specialists are project-only; user-scope discovery is deprecated."),
|
|
19973
21221
|
""
|
|
19974
21222
|
];
|
|
19975
21223
|
console.log(lines.join(`
|
|
19976
21224
|
`));
|
|
19977
21225
|
}
|
|
19978
|
-
var bold10 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
21226
|
+
var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, CORE_COMMANDS, EXTENDED_COMMANDS, WORKTREE_COMMANDS;
|
|
19979
21227
|
var init_help = __esm(() => {
|
|
19980
|
-
|
|
19981
|
-
["
|
|
19982
|
-
["
|
|
19983
|
-
["
|
|
19984
|
-
["
|
|
19985
|
-
["
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
["
|
|
19989
|
-
["
|
|
19990
|
-
["
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
["run", "Run a specialist with a prompt (--background for async)"],
|
|
19994
|
-
["edit", "Edit a specialist field (e.g. --model, --description)"]
|
|
19995
|
-
];
|
|
19996
|
-
JOBS = [
|
|
19997
|
-
["feed", "Tail events for a background job (--follow to stream)"],
|
|
19998
|
-
["result", "Print result of a background job"],
|
|
19999
|
-
["stop", "Send SIGTERM to a running background job"]
|
|
21228
|
+
CORE_COMMANDS = [
|
|
21229
|
+
["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
|
|
21230
|
+
["list", "List specialists in this project"],
|
|
21231
|
+
["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
|
|
21232
|
+
["feed", "Tail job events; use -f to follow all jobs"],
|
|
21233
|
+
["result", "Print final output of a completed background job"],
|
|
21234
|
+
["steer", "Send a mid-run message to a running background job"],
|
|
21235
|
+
["follow-up", "Send a next-turn prompt to a keep-alive session (retains full context)"],
|
|
21236
|
+
["stop", "Stop a running background job"],
|
|
21237
|
+
["status", "Show health, MCP state, and active jobs"],
|
|
21238
|
+
["doctor", "Diagnose installation/runtime problems"],
|
|
21239
|
+
["quickstart", "Full getting-started guide"],
|
|
21240
|
+
["help", "Show this help"]
|
|
20000
21241
|
];
|
|
20001
|
-
|
|
21242
|
+
EXTENDED_COMMANDS = [
|
|
21243
|
+
["edit", "Edit a specialist field such as model or description"],
|
|
21244
|
+
["models", "List models available on pi"],
|
|
20002
21245
|
["version", "Print installed version"],
|
|
20003
|
-
["
|
|
21246
|
+
["setup", "[deprecated] Use specialists init instead"],
|
|
21247
|
+
["install", "[deprecated] Use specialists init instead"]
|
|
21248
|
+
];
|
|
21249
|
+
WORKTREE_COMMANDS = [
|
|
21250
|
+
["xt pi [name]", "Start a Pi session in a sandboxed xt worktree"],
|
|
21251
|
+
["xt claude [name]", "Start a Claude session in a sandboxed xt worktree"],
|
|
21252
|
+
["xt attach [slug]", "Resume an existing xt worktree session"],
|
|
21253
|
+
["xt worktree list", "List worktrees with runtime and activity"],
|
|
21254
|
+
["xt end", "Close session, push, PR, cleanup"]
|
|
20004
21255
|
];
|
|
20005
21256
|
});
|
|
20006
21257
|
|
|
@@ -26030,6 +27281,9 @@ class Protocol {
|
|
|
26030
27281
|
}
|
|
26031
27282
|
}
|
|
26032
27283
|
async connect(transport) {
|
|
27284
|
+
if (this._transport) {
|
|
27285
|
+
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
27286
|
+
}
|
|
26033
27287
|
this._transport = transport;
|
|
26034
27288
|
const _onclose = this.transport?.onclose;
|
|
26035
27289
|
this._transport.onclose = () => {
|
|
@@ -26062,6 +27316,10 @@ class Protocol {
|
|
|
26062
27316
|
this._progressHandlers.clear();
|
|
26063
27317
|
this._taskProgressTokens.clear();
|
|
26064
27318
|
this._pendingDebouncedNotifications.clear();
|
|
27319
|
+
for (const controller of this._requestHandlerAbortControllers.values()) {
|
|
27320
|
+
controller.abort();
|
|
27321
|
+
}
|
|
27322
|
+
this._requestHandlerAbortControllers.clear();
|
|
26065
27323
|
const error2 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed");
|
|
26066
27324
|
this._transport = undefined;
|
|
26067
27325
|
this.onclose?.();
|
|
@@ -26112,6 +27370,8 @@ class Protocol {
|
|
|
26112
27370
|
sessionId: capturedTransport?.sessionId,
|
|
26113
27371
|
_meta: request.params?._meta,
|
|
26114
27372
|
sendNotification: async (notification) => {
|
|
27373
|
+
if (abortController.signal.aborted)
|
|
27374
|
+
return;
|
|
26115
27375
|
const notificationOptions = { relatedRequestId: request.id };
|
|
26116
27376
|
if (relatedTaskId) {
|
|
26117
27377
|
notificationOptions.relatedTask = { taskId: relatedTaskId };
|
|
@@ -26119,6 +27379,9 @@ class Protocol {
|
|
|
26119
27379
|
await this.notification(notification, notificationOptions);
|
|
26120
27380
|
},
|
|
26121
27381
|
sendRequest: async (r, resultSchema, options) => {
|
|
27382
|
+
if (abortController.signal.aborted) {
|
|
27383
|
+
throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
|
|
27384
|
+
}
|
|
26122
27385
|
const requestOptions = { ...options, relatedRequestId: request.id };
|
|
26123
27386
|
if (relatedTaskId && !requestOptions.relatedTask) {
|
|
26124
27387
|
requestOptions.relatedTask = { taskId: relatedTaskId };
|
|
@@ -26732,6 +27995,62 @@ class ExperimentalServerTasks {
|
|
|
26732
27995
|
requestStream(request, resultSchema, options) {
|
|
26733
27996
|
return this._server.requestStream(request, resultSchema, options);
|
|
26734
27997
|
}
|
|
27998
|
+
createMessageStream(params, options) {
|
|
27999
|
+
const clientCapabilities = this._server.getClientCapabilities();
|
|
28000
|
+
if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
|
|
28001
|
+
throw new Error("Client does not support sampling tools capability.");
|
|
28002
|
+
}
|
|
28003
|
+
if (params.messages.length > 0) {
|
|
28004
|
+
const lastMessage = params.messages[params.messages.length - 1];
|
|
28005
|
+
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
|
28006
|
+
const hasToolResults = lastContent.some((c) => c.type === "tool_result");
|
|
28007
|
+
const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
|
|
28008
|
+
const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
|
|
28009
|
+
const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
|
|
28010
|
+
if (hasToolResults) {
|
|
28011
|
+
if (lastContent.some((c) => c.type !== "tool_result")) {
|
|
28012
|
+
throw new Error("The last message must contain only tool_result content if any is present");
|
|
28013
|
+
}
|
|
28014
|
+
if (!hasPreviousToolUse) {
|
|
28015
|
+
throw new Error("tool_result blocks are not matching any tool_use from the previous message");
|
|
28016
|
+
}
|
|
28017
|
+
}
|
|
28018
|
+
if (hasPreviousToolUse) {
|
|
28019
|
+
const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
|
|
28020
|
+
const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
|
|
28021
|
+
if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) {
|
|
28022
|
+
throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
|
|
28023
|
+
}
|
|
28024
|
+
}
|
|
28025
|
+
}
|
|
28026
|
+
return this.requestStream({
|
|
28027
|
+
method: "sampling/createMessage",
|
|
28028
|
+
params
|
|
28029
|
+
}, CreateMessageResultSchema, options);
|
|
28030
|
+
}
|
|
28031
|
+
elicitInputStream(params, options) {
|
|
28032
|
+
const clientCapabilities = this._server.getClientCapabilities();
|
|
28033
|
+
const mode = params.mode ?? "form";
|
|
28034
|
+
switch (mode) {
|
|
28035
|
+
case "url": {
|
|
28036
|
+
if (!clientCapabilities?.elicitation?.url) {
|
|
28037
|
+
throw new Error("Client does not support url elicitation.");
|
|
28038
|
+
}
|
|
28039
|
+
break;
|
|
28040
|
+
}
|
|
28041
|
+
case "form": {
|
|
28042
|
+
if (!clientCapabilities?.elicitation?.form) {
|
|
28043
|
+
throw new Error("Client does not support form elicitation.");
|
|
28044
|
+
}
|
|
28045
|
+
break;
|
|
28046
|
+
}
|
|
28047
|
+
}
|
|
28048
|
+
const normalizedParams = mode === "form" && params.mode === undefined ? { ...params, mode: "form" } : params;
|
|
28049
|
+
return this.requestStream({
|
|
28050
|
+
method: "elicitation/create",
|
|
28051
|
+
params: normalizedParams
|
|
28052
|
+
}, ElicitResultSchema, options);
|
|
28053
|
+
}
|
|
26735
28054
|
async getTask(taskId, options) {
|
|
26736
28055
|
return this._server.getTask({ taskId }, options);
|
|
26737
28056
|
}
|
|
@@ -27206,7 +28525,7 @@ class StdioServerTransport {
|
|
|
27206
28525
|
}
|
|
27207
28526
|
|
|
27208
28527
|
// src/server.ts
|
|
27209
|
-
import { join as
|
|
28528
|
+
import { join as join7 } from "node:path";
|
|
27210
28529
|
|
|
27211
28530
|
// src/constants.ts
|
|
27212
28531
|
var LOG_PREFIX = "[specialists]";
|
|
@@ -27295,25 +28614,48 @@ function createListSpecialistsTool(loader) {
|
|
|
27295
28614
|
|
|
27296
28615
|
// src/tools/specialist/use_specialist.tool.ts
|
|
27297
28616
|
init_zod();
|
|
28617
|
+
init_beads();
|
|
27298
28618
|
var useSpecialistSchema = exports_external.object({
|
|
27299
28619
|
name: exports_external.string().describe("Specialist identifier (e.g. codebase-explorer)"),
|
|
27300
|
-
prompt: exports_external.string().describe("The task or question for the specialist"),
|
|
28620
|
+
prompt: exports_external.string().optional().describe("The task or question for the specialist"),
|
|
28621
|
+
bead_id: exports_external.string().optional().describe("Use an existing bead as the specialist prompt"),
|
|
27301
28622
|
variables: exports_external.record(exports_external.string()).optional().describe("Additional $variable substitutions"),
|
|
27302
28623
|
backend_override: exports_external.string().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
|
|
27303
|
-
autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation")
|
|
28624
|
+
autonomy_level: exports_external.string().optional().describe("Override permission level for this invocation"),
|
|
28625
|
+
context_depth: exports_external.number().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
|
|
28626
|
+
}).refine((input) => Boolean(input.prompt?.trim() || input.bead_id), {
|
|
28627
|
+
message: "Either prompt or bead_id is required",
|
|
28628
|
+
path: ["prompt"]
|
|
27304
28629
|
});
|
|
27305
28630
|
function createUseSpecialistTool(runner) {
|
|
27306
28631
|
return {
|
|
27307
28632
|
name: "use_specialist",
|
|
27308
|
-
description: "Run a specialist synchronously and wait for the result. " + "Full lifecycle: load → agents.md → pi session → output. " + "Response includes output, model, durationMs, and beadId (string | undefined). " + "beadId is set when the specialist's beads_integration policy triggered bead creation " + "(default: auto — creates for LOW/MEDIUM/HIGH permission, skips for READ_ONLY). " + "If beadId is present, use `bd update <beadId> --notes` to attach findings or " + "`bd remember` to persist key discoveries for future sessions.",
|
|
28633
|
+
description: "Run a specialist synchronously and wait for the result. " + "Full lifecycle: load → agents.md → pi session → output. " + "Response includes output, model, durationMs, and beadId (string | undefined). " + "beadId is set when the specialist's beads_integration policy triggered bead creation " + "(default: auto — creates for LOW/MEDIUM/HIGH permission, skips for READ_ONLY). " + "If beadId is present, use `bd update <beadId> --notes` to attach findings or " + "`bd remember` to persist key discoveries for future sessions. " + "When bead_id is provided, the source bead becomes the specialist prompt and the tracking bead links back to it. " + "Use context_depth to inject outputs from completed blocking dependencies (depth 1 = immediate blockers, 2 = include their blockers too).",
|
|
27309
28634
|
inputSchema: useSpecialistSchema,
|
|
27310
28635
|
async execute(input, onProgress) {
|
|
28636
|
+
let prompt = input.prompt?.trim() ?? "";
|
|
28637
|
+
let variables = input.variables;
|
|
28638
|
+
if (input.bead_id) {
|
|
28639
|
+
const beadsClient = new BeadsClient;
|
|
28640
|
+
const bead = beadsClient.readBead(input.bead_id);
|
|
28641
|
+
if (!bead) {
|
|
28642
|
+
throw new Error(`Unable to read bead '${input.bead_id}' via bd show --json`);
|
|
28643
|
+
}
|
|
28644
|
+
const beadContext = buildBeadContext(bead);
|
|
28645
|
+
prompt = beadContext;
|
|
28646
|
+
variables = {
|
|
28647
|
+
...input.variables ?? {},
|
|
28648
|
+
bead_context: beadContext,
|
|
28649
|
+
bead_id: input.bead_id
|
|
28650
|
+
};
|
|
28651
|
+
}
|
|
27311
28652
|
return runner.run({
|
|
27312
28653
|
name: input.name,
|
|
27313
|
-
prompt
|
|
27314
|
-
variables
|
|
28654
|
+
prompt,
|
|
28655
|
+
variables,
|
|
27315
28656
|
backendOverride: input.backend_override,
|
|
27316
|
-
autonomyLevel: input.autonomy_level
|
|
28657
|
+
autonomyLevel: input.autonomy_level,
|
|
28658
|
+
inputBeadId: input.bead_id
|
|
27317
28659
|
}, onProgress);
|
|
27318
28660
|
}
|
|
27319
28661
|
};
|
|
@@ -27419,17 +28761,17 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
|
|
|
27419
28761
|
async execute(_) {
|
|
27420
28762
|
const list = await loader.list();
|
|
27421
28763
|
const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
|
|
27422
|
-
const { existsSync:
|
|
27423
|
-
const { join:
|
|
27424
|
-
const jobsDir =
|
|
28764
|
+
const { existsSync: existsSync4, readdirSync, readFileSync: readFileSync2 } = await import("node:fs");
|
|
28765
|
+
const { join: join3 } = await import("node:path");
|
|
28766
|
+
const jobsDir = join3(process.cwd(), ".specialists", "jobs");
|
|
27425
28767
|
const jobs = [];
|
|
27426
|
-
if (
|
|
28768
|
+
if (existsSync4(jobsDir)) {
|
|
27427
28769
|
for (const entry of readdirSync(jobsDir)) {
|
|
27428
|
-
const statusPath =
|
|
27429
|
-
if (!
|
|
28770
|
+
const statusPath = join3(jobsDir, entry, "status.json");
|
|
28771
|
+
if (!existsSync4(statusPath))
|
|
27430
28772
|
continue;
|
|
27431
28773
|
try {
|
|
27432
|
-
jobs.push(JSON.parse(
|
|
28774
|
+
jobs.push(JSON.parse(readFileSync2(statusPath, "utf-8")));
|
|
27433
28775
|
} catch {}
|
|
27434
28776
|
}
|
|
27435
28777
|
jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
@@ -27508,6 +28850,74 @@ class JobRegistry {
|
|
|
27508
28850
|
}
|
|
27509
28851
|
job.killFn = killFn;
|
|
27510
28852
|
}
|
|
28853
|
+
setSteerFn(id, steerFn) {
|
|
28854
|
+
const job = this.jobs.get(id);
|
|
28855
|
+
if (!job)
|
|
28856
|
+
return;
|
|
28857
|
+
job.steerFn = steerFn;
|
|
28858
|
+
}
|
|
28859
|
+
setResumeFn(id, resumeFn, closeFn) {
|
|
28860
|
+
const job = this.jobs.get(id);
|
|
28861
|
+
if (!job)
|
|
28862
|
+
return;
|
|
28863
|
+
job.resumeFn = resumeFn;
|
|
28864
|
+
job.closeFn = closeFn;
|
|
28865
|
+
job.status = "waiting";
|
|
28866
|
+
job.currentEvent = "waiting";
|
|
28867
|
+
}
|
|
28868
|
+
async followUp(id, message) {
|
|
28869
|
+
const job = this.jobs.get(id);
|
|
28870
|
+
if (!job)
|
|
28871
|
+
return { ok: false, error: `Job not found: ${id}` };
|
|
28872
|
+
if (job.status !== "waiting")
|
|
28873
|
+
return { ok: false, error: `Job is not waiting (status: ${job.status})` };
|
|
28874
|
+
if (!job.resumeFn)
|
|
28875
|
+
return { ok: false, error: "Job has no resume function" };
|
|
28876
|
+
job.status = "running";
|
|
28877
|
+
job.currentEvent = "starting";
|
|
28878
|
+
try {
|
|
28879
|
+
const output = await job.resumeFn(message);
|
|
28880
|
+
job.outputBuffer = output;
|
|
28881
|
+
job.status = "waiting";
|
|
28882
|
+
job.currentEvent = "waiting";
|
|
28883
|
+
return { ok: true, output };
|
|
28884
|
+
} catch (err) {
|
|
28885
|
+
job.status = "error";
|
|
28886
|
+
job.error = err?.message ?? String(err);
|
|
28887
|
+
return { ok: false, error: job.error };
|
|
28888
|
+
}
|
|
28889
|
+
}
|
|
28890
|
+
async closeSession(id) {
|
|
28891
|
+
const job = this.jobs.get(id);
|
|
28892
|
+
if (!job)
|
|
28893
|
+
return { ok: false, error: `Job not found: ${id}` };
|
|
28894
|
+
if (job.status !== "waiting")
|
|
28895
|
+
return { ok: false, error: `Job is not in waiting state` };
|
|
28896
|
+
try {
|
|
28897
|
+
await job.closeFn?.();
|
|
28898
|
+
job.status = "done";
|
|
28899
|
+
job.currentEvent = "done";
|
|
28900
|
+
job.endedAtMs = Date.now();
|
|
28901
|
+
return { ok: true };
|
|
28902
|
+
} catch (err) {
|
|
28903
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
28904
|
+
}
|
|
28905
|
+
}
|
|
28906
|
+
async steer(id, message) {
|
|
28907
|
+
const job = this.jobs.get(id);
|
|
28908
|
+
if (!job)
|
|
28909
|
+
return { ok: false, error: `Job not found: ${id}` };
|
|
28910
|
+
if (job.status !== "running")
|
|
28911
|
+
return { ok: false, error: `Job is not running (status: ${job.status})` };
|
|
28912
|
+
if (!job.steerFn)
|
|
28913
|
+
return { ok: false, error: "Job session not ready for steering yet" };
|
|
28914
|
+
try {
|
|
28915
|
+
await job.steerFn(message);
|
|
28916
|
+
return { ok: true };
|
|
28917
|
+
} catch (err) {
|
|
28918
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
28919
|
+
}
|
|
28920
|
+
}
|
|
27511
28921
|
complete(id, result) {
|
|
27512
28922
|
const job = this.jobs.get(id);
|
|
27513
28923
|
if (!job || job.status !== "running")
|
|
@@ -27632,20 +29042,117 @@ function createStopSpecialistTool(registry2) {
|
|
|
27632
29042
|
};
|
|
27633
29043
|
}
|
|
27634
29044
|
|
|
29045
|
+
// src/tools/specialist/steer_specialist.tool.ts
|
|
29046
|
+
init_zod();
|
|
29047
|
+
init_supervisor();
|
|
29048
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
29049
|
+
import { join as join4 } from "node:path";
|
|
29050
|
+
var steerSpecialistSchema = exports_external.object({
|
|
29051
|
+
job_id: exports_external.string().describe("Job ID returned by start_specialist or specialists run --background"),
|
|
29052
|
+
message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
|
|
29053
|
+
});
|
|
29054
|
+
function createSteerSpecialistTool(registry2) {
|
|
29055
|
+
return {
|
|
29056
|
+
name: "steer_specialist",
|
|
29057
|
+
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 background CLI jobs (specialists run --background).",
|
|
29058
|
+
inputSchema: steerSpecialistSchema,
|
|
29059
|
+
async execute(input) {
|
|
29060
|
+
const snap = registry2.snapshot(input.job_id);
|
|
29061
|
+
if (snap) {
|
|
29062
|
+
const result = await registry2.steer(input.job_id, input.message);
|
|
29063
|
+
if (result.ok) {
|
|
29064
|
+
return { status: "steered", job_id: input.job_id, message: input.message };
|
|
29065
|
+
}
|
|
29066
|
+
return { status: "error", error: result.error, job_id: input.job_id };
|
|
29067
|
+
}
|
|
29068
|
+
const jobsDir = join4(process.cwd(), ".specialists", "jobs");
|
|
29069
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
29070
|
+
const status = supervisor.readStatus(input.job_id);
|
|
29071
|
+
if (!status) {
|
|
29072
|
+
return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
|
|
29073
|
+
}
|
|
29074
|
+
if (status.status === "done" || status.status === "error") {
|
|
29075
|
+
return { status: "error", error: `Job is already ${status.status}`, job_id: input.job_id };
|
|
29076
|
+
}
|
|
29077
|
+
if (!status.fifo_path) {
|
|
29078
|
+
return { status: "error", error: "Job has no steer pipe (may have been started without FIFO support)", job_id: input.job_id };
|
|
29079
|
+
}
|
|
29080
|
+
try {
|
|
29081
|
+
const payload = JSON.stringify({ type: "steer", message: input.message }) + `
|
|
29082
|
+
`;
|
|
29083
|
+
writeFileSync2(status.fifo_path, payload, { flag: "a" });
|
|
29084
|
+
return { status: "steered", job_id: input.job_id, message: input.message };
|
|
29085
|
+
} catch (err) {
|
|
29086
|
+
return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
|
|
29087
|
+
}
|
|
29088
|
+
}
|
|
29089
|
+
};
|
|
29090
|
+
}
|
|
29091
|
+
|
|
29092
|
+
// src/tools/specialist/follow_up_specialist.tool.ts
|
|
29093
|
+
init_zod();
|
|
29094
|
+
init_supervisor();
|
|
29095
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
29096
|
+
import { join as join5 } from "node:path";
|
|
29097
|
+
var followUpSpecialistSchema = exports_external.object({
|
|
29098
|
+
job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
|
|
29099
|
+
message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
|
|
29100
|
+
});
|
|
29101
|
+
function createFollowUpSpecialistTool(registry2) {
|
|
29102
|
+
return {
|
|
29103
|
+
name: "follow_up_specialist",
|
|
29104
|
+
description: "Send a follow-up prompt to a waiting keep-alive specialist session. The Pi session retains full conversation history between turns. Only works for jobs started with keepAlive=true (CLI: --keep-alive --background).",
|
|
29105
|
+
inputSchema: followUpSpecialistSchema,
|
|
29106
|
+
async execute(input) {
|
|
29107
|
+
const snap = registry2.snapshot(input.job_id);
|
|
29108
|
+
if (snap) {
|
|
29109
|
+
if (snap.status !== "waiting") {
|
|
29110
|
+
return { status: "error", error: `Job is not waiting (status: ${snap.status})`, job_id: input.job_id };
|
|
29111
|
+
}
|
|
29112
|
+
const result = await registry2.followUp(input.job_id, input.message);
|
|
29113
|
+
if (result.ok) {
|
|
29114
|
+
return { status: "resumed", job_id: input.job_id, output: result.output };
|
|
29115
|
+
}
|
|
29116
|
+
return { status: "error", error: result.error, job_id: input.job_id };
|
|
29117
|
+
}
|
|
29118
|
+
const jobsDir = join5(process.cwd(), ".specialists", "jobs");
|
|
29119
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
29120
|
+
const status = supervisor.readStatus(input.job_id);
|
|
29121
|
+
if (!status) {
|
|
29122
|
+
return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
|
|
29123
|
+
}
|
|
29124
|
+
if (status.status !== "waiting") {
|
|
29125
|
+
return { status: "error", error: `Job is not waiting (status: ${status.status})`, job_id: input.job_id };
|
|
29126
|
+
}
|
|
29127
|
+
if (!status.fifo_path) {
|
|
29128
|
+
return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
|
|
29129
|
+
}
|
|
29130
|
+
try {
|
|
29131
|
+
const payload = JSON.stringify({ type: "prompt", message: input.message }) + `
|
|
29132
|
+
`;
|
|
29133
|
+
writeFileSync3(status.fifo_path, payload, { flag: "a" });
|
|
29134
|
+
return { status: "sent", job_id: input.job_id, message: input.message };
|
|
29135
|
+
} catch (err) {
|
|
29136
|
+
return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
|
|
29137
|
+
}
|
|
29138
|
+
}
|
|
29139
|
+
};
|
|
29140
|
+
}
|
|
29141
|
+
|
|
27635
29142
|
// src/server.ts
|
|
27636
29143
|
init_zod();
|
|
27637
29144
|
|
|
27638
29145
|
// src/tools/specialist/specialist_init.tool.ts
|
|
27639
29146
|
init_zod();
|
|
27640
|
-
import { spawnSync as
|
|
27641
|
-
import { existsSync as
|
|
27642
|
-
import { join as
|
|
29147
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
29148
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
29149
|
+
import { join as join6 } from "node:path";
|
|
27643
29150
|
var specialistInitSchema = objectType({});
|
|
27644
29151
|
function createSpecialistInitTool(loader, deps) {
|
|
27645
29152
|
const resolved = deps ?? {
|
|
27646
|
-
bdAvailable: () =>
|
|
27647
|
-
beadsExists: () =>
|
|
27648
|
-
bdInit: () =>
|
|
29153
|
+
bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
|
|
29154
|
+
beadsExists: () => existsSync5(join6(process.cwd(), ".beads")),
|
|
29155
|
+
bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
|
|
27649
29156
|
};
|
|
27650
29157
|
return {
|
|
27651
29158
|
name: "specialist_init",
|
|
@@ -27679,7 +29186,7 @@ class SpecialistsServer {
|
|
|
27679
29186
|
const circuitBreaker = new CircuitBreaker;
|
|
27680
29187
|
const loader = new SpecialistLoader;
|
|
27681
29188
|
const hooks = new HookEmitter({
|
|
27682
|
-
tracePath:
|
|
29189
|
+
tracePath: join7(process.cwd(), ".specialists", "trace.jsonl")
|
|
27683
29190
|
});
|
|
27684
29191
|
const beadsClient = new BeadsClient;
|
|
27685
29192
|
const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
|
|
@@ -27692,6 +29199,8 @@ class SpecialistsServer {
|
|
|
27692
29199
|
createStartSpecialistTool(runner, registry2),
|
|
27693
29200
|
createPollSpecialistTool(registry2),
|
|
27694
29201
|
createStopSpecialistTool(registry2),
|
|
29202
|
+
createSteerSpecialistTool(registry2),
|
|
29203
|
+
createFollowUpSpecialistTool(registry2),
|
|
27695
29204
|
createSpecialistInitTool(loader)
|
|
27696
29205
|
];
|
|
27697
29206
|
this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
|
|
@@ -27707,6 +29216,8 @@ class SpecialistsServer {
|
|
|
27707
29216
|
start_specialist: startSpecialistSchema,
|
|
27708
29217
|
poll_specialist: pollSpecialistSchema,
|
|
27709
29218
|
stop_specialist: stopSpecialistSchema,
|
|
29219
|
+
steer_specialist: steerSpecialistSchema,
|
|
29220
|
+
follow_up_specialist: followUpSpecialistSchema,
|
|
27710
29221
|
specialist_init: specialistInitSchema
|
|
27711
29222
|
};
|
|
27712
29223
|
this.toolSchemas = schemaMap;
|
|
@@ -27779,15 +29290,15 @@ var next = process.argv[3];
|
|
|
27779
29290
|
function wantsHelp() {
|
|
27780
29291
|
return next === "--help" || next === "-h";
|
|
27781
29292
|
}
|
|
27782
|
-
async function
|
|
29293
|
+
async function run18() {
|
|
27783
29294
|
if (sub === "install") {
|
|
27784
29295
|
if (wantsHelp()) {
|
|
27785
29296
|
console.log([
|
|
27786
29297
|
"",
|
|
27787
29298
|
"Usage: specialists install",
|
|
27788
29299
|
"",
|
|
27789
|
-
"
|
|
27790
|
-
"and installs
|
|
29300
|
+
"Project setup: checks pi/bd/xt prerequisites, registers the MCP server,",
|
|
29301
|
+
"and installs specialists-specific project hooks.",
|
|
27791
29302
|
"",
|
|
27792
29303
|
"No flags — just run it.",
|
|
27793
29304
|
""
|
|
@@ -27808,18 +29319,24 @@ async function run16() {
|
|
|
27808
29319
|
"",
|
|
27809
29320
|
"Usage: specialists list [options]",
|
|
27810
29321
|
"",
|
|
27811
|
-
"List
|
|
29322
|
+
"List specialists in the current project.",
|
|
29323
|
+
"",
|
|
29324
|
+
"What it shows:",
|
|
29325
|
+
" - specialist name",
|
|
29326
|
+
" - model",
|
|
29327
|
+
" - short description",
|
|
27812
29328
|
"",
|
|
27813
29329
|
"Options:",
|
|
27814
|
-
" --
|
|
27815
|
-
" --
|
|
27816
|
-
" --json Output as JSON array",
|
|
29330
|
+
" --category <name> Filter by category tag",
|
|
29331
|
+
" --json Output as JSON array",
|
|
27817
29332
|
"",
|
|
27818
29333
|
"Examples:",
|
|
27819
29334
|
" specialists list",
|
|
27820
|
-
" specialists list --scope project",
|
|
27821
29335
|
" specialists list --category analysis",
|
|
27822
29336
|
" specialists list --json",
|
|
29337
|
+
"",
|
|
29338
|
+
"Project model:",
|
|
29339
|
+
" Specialists are project-only. User-scope discovery is deprecated.",
|
|
27823
29340
|
""
|
|
27824
29341
|
].join(`
|
|
27825
29342
|
`));
|
|
@@ -27849,15 +29366,27 @@ async function run16() {
|
|
|
27849
29366
|
if (wantsHelp()) {
|
|
27850
29367
|
console.log([
|
|
27851
29368
|
"",
|
|
27852
|
-
"Usage: specialists init",
|
|
29369
|
+
"Usage: specialists init [--force-workflow]",
|
|
29370
|
+
"",
|
|
29371
|
+
"Bootstrap a project for specialists. This is the sole onboarding command.",
|
|
29372
|
+
"",
|
|
29373
|
+
"What it does:",
|
|
29374
|
+
" • creates specialists/ for project .specialist.yaml files",
|
|
29375
|
+
" • creates .specialists/ runtime dirs (jobs/, ready/)",
|
|
29376
|
+
" • adds .specialists/ to .gitignore",
|
|
29377
|
+
" • injects the managed workflow block into AGENTS.md and CLAUDE.md",
|
|
29378
|
+
" • registers the Specialists MCP server at project scope",
|
|
27853
29379
|
"",
|
|
27854
|
-
"
|
|
27855
|
-
"
|
|
27856
|
-
"
|
|
27857
|
-
"
|
|
27858
|
-
"
|
|
29380
|
+
"Options:",
|
|
29381
|
+
" --force-workflow Overwrite existing managed workflow blocks",
|
|
29382
|
+
"",
|
|
29383
|
+
"Examples:",
|
|
29384
|
+
" specialists init",
|
|
29385
|
+
" specialists init --force-workflow",
|
|
27859
29386
|
"",
|
|
27860
|
-
"
|
|
29387
|
+
"Notes:",
|
|
29388
|
+
" setup and install are deprecated; use specialists init.",
|
|
29389
|
+
" Safe to run again; existing project state is preserved where possible.",
|
|
27861
29390
|
""
|
|
27862
29391
|
].join(`
|
|
27863
29392
|
`));
|
|
@@ -27904,24 +29433,31 @@ async function run16() {
|
|
|
27904
29433
|
"",
|
|
27905
29434
|
"Usage: specialists run <name> [options]",
|
|
27906
29435
|
"",
|
|
27907
|
-
"Run a specialist
|
|
27908
|
-
"
|
|
29436
|
+
"Run a specialist in foreground or background.",
|
|
29437
|
+
"",
|
|
29438
|
+
"Primary modes:",
|
|
29439
|
+
" tracked: specialists run <name> --bead <id>",
|
|
29440
|
+
' ad-hoc: specialists run <name> --prompt "..."',
|
|
27909
29441
|
"",
|
|
27910
29442
|
"Options:",
|
|
27911
|
-
" --
|
|
27912
|
-
" --
|
|
27913
|
-
" --
|
|
27914
|
-
" --no-beads
|
|
29443
|
+
" --bead <id> Use an existing bead as the prompt source",
|
|
29444
|
+
" --prompt <text> Ad-hoc prompt for untracked work",
|
|
29445
|
+
" --context-depth <n> Dependency context depth when using --bead (default: 1)",
|
|
29446
|
+
" --no-beads Do not create a new tracking bead (does not disable bead reading)",
|
|
29447
|
+
" --background Start async and return a job id",
|
|
29448
|
+
" --follow Run in background and stream output live",
|
|
29449
|
+
" --model <model> Override the configured model for this run",
|
|
27915
29450
|
"",
|
|
27916
29451
|
"Examples:",
|
|
29452
|
+
" specialists run bug-hunt --bead unitAI-55d",
|
|
29453
|
+
" specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
|
|
29454
|
+
" specialists run sync-docs --follow",
|
|
27917
29455
|
' specialists run code-review --prompt "Audit src/api.ts"',
|
|
27918
|
-
|
|
27919
|
-
" cat brief.md | specialists run deep-analysis",
|
|
27920
|
-
' specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."',
|
|
29456
|
+
" cat brief.md | specialists run report-generator",
|
|
27921
29457
|
"",
|
|
27922
|
-
"
|
|
27923
|
-
"
|
|
27924
|
-
"
|
|
29458
|
+
"Rules:",
|
|
29459
|
+
" Use --bead for tracked work.",
|
|
29460
|
+
" Use --prompt for quick ad-hoc work.",
|
|
27925
29461
|
""
|
|
27926
29462
|
].join(`
|
|
27927
29463
|
`));
|
|
@@ -27936,11 +29472,17 @@ async function run16() {
|
|
|
27936
29472
|
"",
|
|
27937
29473
|
"Usage: specialists status [options]",
|
|
27938
29474
|
"",
|
|
27939
|
-
"Show
|
|
27940
|
-
"
|
|
29475
|
+
"Show current runtime state.",
|
|
29476
|
+
"",
|
|
29477
|
+
"Sections include:",
|
|
29478
|
+
" - discovered specialists",
|
|
29479
|
+
" - pi provider/runtime health",
|
|
29480
|
+
" - beads availability",
|
|
29481
|
+
" - MCP registration hints",
|
|
29482
|
+
" - active background jobs",
|
|
27941
29483
|
"",
|
|
27942
29484
|
"Options:",
|
|
27943
|
-
" --json
|
|
29485
|
+
" --json Output machine-readable JSON",
|
|
27944
29486
|
"",
|
|
27945
29487
|
"Examples:",
|
|
27946
29488
|
" specialists status",
|
|
@@ -27982,20 +29524,24 @@ async function run16() {
|
|
|
27982
29524
|
console.log([
|
|
27983
29525
|
"",
|
|
27984
29526
|
"Usage: specialists feed <job-id> [options]",
|
|
27985
|
-
" specialists feed
|
|
29527
|
+
" specialists feed -f [--forever]",
|
|
27986
29528
|
"",
|
|
27987
|
-
"
|
|
29529
|
+
"Read background job events.",
|
|
29530
|
+
"",
|
|
29531
|
+
"Modes:",
|
|
29532
|
+
" specialists feed <job-id> Replay events for one job",
|
|
29533
|
+
" specialists feed <job-id> -f Follow one job until completion",
|
|
29534
|
+
" specialists feed -f Follow all jobs globally",
|
|
27988
29535
|
"",
|
|
27989
29536
|
"Options:",
|
|
27990
|
-
"
|
|
27991
|
-
"
|
|
29537
|
+
" -f, --follow Follow live updates",
|
|
29538
|
+
" --forever Keep following in global mode even when all jobs complete",
|
|
27992
29539
|
"",
|
|
27993
29540
|
"Examples:",
|
|
27994
|
-
" specialists feed
|
|
27995
|
-
" specialists feed
|
|
27996
|
-
" specialists feed
|
|
27997
|
-
"",
|
|
27998
|
-
"Event types: tool_use · tool_result · text · agent_end · error",
|
|
29541
|
+
" specialists feed 49adda",
|
|
29542
|
+
" specialists feed 49adda --follow",
|
|
29543
|
+
" specialists feed -f",
|
|
29544
|
+
" specialists feed -f --forever",
|
|
27999
29545
|
""
|
|
28000
29546
|
].join(`
|
|
28001
29547
|
`));
|
|
@@ -28004,6 +29550,65 @@ async function run16() {
|
|
|
28004
29550
|
const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
|
|
28005
29551
|
return handler();
|
|
28006
29552
|
}
|
|
29553
|
+
if (sub === "steer") {
|
|
29554
|
+
if (wantsHelp()) {
|
|
29555
|
+
console.log([
|
|
29556
|
+
"",
|
|
29557
|
+
'Usage: specialists steer <job-id> "<message>"',
|
|
29558
|
+
"",
|
|
29559
|
+
"Send a mid-run steering message to a running background specialist job.",
|
|
29560
|
+
"The agent receives the message after its current tool calls finish,",
|
|
29561
|
+
"before the next LLM call.",
|
|
29562
|
+
"",
|
|
29563
|
+
'Pi RPC steer command: {"type":"steer","message":"..."}',
|
|
29564
|
+
'Response: {"type":"response","command":"steer","success":true}',
|
|
29565
|
+
"",
|
|
29566
|
+
"Examples:",
|
|
29567
|
+
' specialists steer a1b2c3 "focus only on supervisor.ts"',
|
|
29568
|
+
' specialists steer a1b2c3 "skip tests, just fix the bug"',
|
|
29569
|
+
"",
|
|
29570
|
+
"Notes:",
|
|
29571
|
+
" - Only works for jobs started with --background.",
|
|
29572
|
+
" - Delivery is best-effort: the agent processes it on its next turn.",
|
|
29573
|
+
""
|
|
29574
|
+
].join(`
|
|
29575
|
+
`));
|
|
29576
|
+
return;
|
|
29577
|
+
}
|
|
29578
|
+
const { run: handler } = await Promise.resolve().then(() => (init_steer(), exports_steer));
|
|
29579
|
+
return handler();
|
|
29580
|
+
}
|
|
29581
|
+
if (sub === "follow-up") {
|
|
29582
|
+
if (wantsHelp()) {
|
|
29583
|
+
console.log([
|
|
29584
|
+
"",
|
|
29585
|
+
'Usage: specialists follow-up <job-id> "<message>"',
|
|
29586
|
+
"",
|
|
29587
|
+
"Send a follow-up prompt to a waiting keep-alive specialist session.",
|
|
29588
|
+
"The Pi session retains full conversation history between turns.",
|
|
29589
|
+
"",
|
|
29590
|
+
"Requires: job started with --keep-alive --background.",
|
|
29591
|
+
"",
|
|
29592
|
+
"Examples:",
|
|
29593
|
+
' specialists follow-up a1b2c3 "Now write the fix for the bug you found"',
|
|
29594
|
+
' specialists follow-up a1b2c3 "Focus only on the auth module"',
|
|
29595
|
+
"",
|
|
29596
|
+
"Workflow:",
|
|
29597
|
+
" specialists run bug-hunt --bead <id> --keep-alive --background",
|
|
29598
|
+
" # → Job started: a1b2c3 (status: waiting after first turn)",
|
|
29599
|
+
" specialists result a1b2c3 # read first turn output",
|
|
29600
|
+
' specialists follow-up a1b2c3 "..." # send next prompt',
|
|
29601
|
+
" specialists feed a1b2c3 --follow # watch response",
|
|
29602
|
+
"",
|
|
29603
|
+
"See also: specialists steer (mid-run redirect)",
|
|
29604
|
+
""
|
|
29605
|
+
].join(`
|
|
29606
|
+
`));
|
|
29607
|
+
return;
|
|
29608
|
+
}
|
|
29609
|
+
const { run: handler } = await Promise.resolve().then(() => (init_follow_up(), exports_follow_up));
|
|
29610
|
+
return handler();
|
|
29611
|
+
}
|
|
28007
29612
|
if (sub === "stop") {
|
|
28008
29613
|
if (wantsHelp()) {
|
|
28009
29614
|
console.log([
|
|
@@ -28033,15 +29638,20 @@ async function run16() {
|
|
|
28033
29638
|
"",
|
|
28034
29639
|
"Usage: specialists doctor",
|
|
28035
29640
|
"",
|
|
28036
|
-
"
|
|
28037
|
-
"
|
|
28038
|
-
"
|
|
28039
|
-
"
|
|
28040
|
-
"
|
|
28041
|
-
"
|
|
29641
|
+
"Diagnose bootstrap and runtime problems.",
|
|
29642
|
+
"",
|
|
29643
|
+
"Checks:",
|
|
29644
|
+
" 1. pi installed and has active providers",
|
|
29645
|
+
" 2. beads installed and .beads/ present",
|
|
29646
|
+
" 3. xtrm-tools availability",
|
|
29647
|
+
" 4. Specialists MCP registration in .mcp.json",
|
|
29648
|
+
" 5. .specialists/ runtime directories",
|
|
29649
|
+
" 6. hook wiring expectations",
|
|
29650
|
+
" 7. zombie job detection",
|
|
28042
29651
|
"",
|
|
28043
|
-
"
|
|
28044
|
-
"
|
|
29652
|
+
"Behavior:",
|
|
29653
|
+
" - prints fix hints for failing checks",
|
|
29654
|
+
" - auto-creates missing runtime directories when possible",
|
|
28045
29655
|
"",
|
|
28046
29656
|
"Examples:",
|
|
28047
29657
|
" specialists doctor",
|
|
@@ -28094,7 +29704,7 @@ Run 'specialists help' to see available commands.`);
|
|
|
28094
29704
|
const server = new SpecialistsServer;
|
|
28095
29705
|
await server.start();
|
|
28096
29706
|
}
|
|
28097
|
-
|
|
29707
|
+
run18().catch((error2) => {
|
|
28098
29708
|
logger.error(`Fatal error: ${error2}`);
|
|
28099
29709
|
process.exit(1);
|
|
28100
29710
|
});
|