@jaggerxtrm/specialists 3.2.1 → 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 +24 -4
- package/dist/index.js +1604 -852
- package/package.json +2 -1
- package/hooks/specialists-complete.mjs +0 -60
- 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/planner.specialist.yaml +0 -87
- package/specialists/report-generator.specialist.yaml +0 -59
- package/specialists/specialist-author.specialist.yaml +0 -56
- package/specialists/sync-docs.specialist.yaml +0 -53
- package/specialists/test-runner.specialist.yaml +0 -58
- package/specialists/xt-merge.specialist.yaml +0 -78
package/dist/index.js
CHANGED
|
@@ -17423,7 +17423,7 @@ async function parseSpecialist(yamlContent) {
|
|
|
17423
17423
|
const raw = $parse(yamlContent);
|
|
17424
17424
|
return SpecialistSchema.parseAsync(raw);
|
|
17425
17425
|
}
|
|
17426
|
-
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
|
|
17426
|
+
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
|
|
17427
17427
|
var init_schema = __esm(() => {
|
|
17428
17428
|
init_zod();
|
|
17429
17429
|
init_dist();
|
|
@@ -17447,6 +17447,7 @@ var init_schema = __esm(() => {
|
|
|
17447
17447
|
stall_timeout_ms: numberType().optional(),
|
|
17448
17448
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
17449
17449
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
17450
|
+
thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
|
17450
17451
|
preferred_profile: stringType().optional(),
|
|
17451
17452
|
approval_mode: stringType().optional()
|
|
17452
17453
|
});
|
|
@@ -17458,27 +17459,26 @@ var init_schema = __esm(() => {
|
|
|
17458
17459
|
examples: arrayType(unknownType()).optional(),
|
|
17459
17460
|
skill_inherit: stringType().optional()
|
|
17460
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
|
+
}));
|
|
17461
17472
|
SkillsSchema = objectType({
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
phase: enumType(["pre", "post"]),
|
|
17465
|
-
inject_output: booleanType().default(false)
|
|
17466
|
-
})).optional(),
|
|
17467
|
-
references: arrayType(unknownType()).optional(),
|
|
17468
|
-
tools: arrayType(stringType()).optional(),
|
|
17469
|
-
paths: arrayType(stringType()).optional()
|
|
17473
|
+
paths: arrayType(stringType()).optional(),
|
|
17474
|
+
scripts: arrayType(ScriptEntrySchema).optional()
|
|
17470
17475
|
}).optional();
|
|
17471
17476
|
CapabilitiesSchema = objectType({
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
can_spawn: booleanType().optional(),
|
|
17475
|
-
tools: arrayType(objectType({ name: stringType(), purpose: stringType() })).optional(),
|
|
17476
|
-
diagnostic_scripts: arrayType(stringType()).optional()
|
|
17477
|
+
required_tools: arrayType(stringType()).optional(),
|
|
17478
|
+
external_commands: arrayType(stringType()).optional()
|
|
17477
17479
|
}).optional();
|
|
17478
17480
|
CommunicationSchema = objectType({
|
|
17479
|
-
|
|
17480
|
-
subscribes: arrayType(stringType()).optional(),
|
|
17481
|
-
output_to: stringType().optional()
|
|
17481
|
+
next_specialists: unionType([stringType(), arrayType(stringType())]).optional()
|
|
17482
17482
|
}).optional();
|
|
17483
17483
|
ValidationSchema = objectType({
|
|
17484
17484
|
files_to_watch: arrayType(stringType()).optional(),
|
|
@@ -17494,6 +17494,7 @@ var init_schema = __esm(() => {
|
|
|
17494
17494
|
capabilities: CapabilitiesSchema,
|
|
17495
17495
|
communication: CommunicationSchema,
|
|
17496
17496
|
validation: ValidationSchema,
|
|
17497
|
+
output_file: stringType().optional(),
|
|
17497
17498
|
beads_integration: enumType(["auto", "always", "never"]).default("auto"),
|
|
17498
17499
|
heartbeat: unknownType().optional()
|
|
17499
17500
|
})
|
|
@@ -17503,7 +17504,6 @@ var init_schema = __esm(() => {
|
|
|
17503
17504
|
// src/specialist/loader.ts
|
|
17504
17505
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
17505
17506
|
import { join } from "node:path";
|
|
17506
|
-
import { homedir } from "node:os";
|
|
17507
17507
|
import { existsSync } from "node:fs";
|
|
17508
17508
|
async function checkStaleness(summary) {
|
|
17509
17509
|
if (!summary.filestoWatch?.length || !summary.updated)
|
|
@@ -17527,17 +17527,13 @@ async function checkStaleness(summary) {
|
|
|
17527
17527
|
class SpecialistLoader {
|
|
17528
17528
|
cache = new Map;
|
|
17529
17529
|
projectDir;
|
|
17530
|
-
userDir;
|
|
17531
17530
|
constructor(options = {}) {
|
|
17532
17531
|
this.projectDir = options.projectDir ?? process.cwd();
|
|
17533
|
-
this.userDir = options.userDir ?? join(homedir(), ".agents", "specialists");
|
|
17534
17532
|
}
|
|
17535
17533
|
getScanDirs() {
|
|
17536
17534
|
const dirs = [
|
|
17537
|
-
{ path: join(this.projectDir, "specialists"), scope: "
|
|
17538
|
-
{ path: join(this.projectDir, ".
|
|
17539
|
-
{ path: join(this.projectDir, ".agent-forge", "specialists"), scope: "project" },
|
|
17540
|
-
{ path: this.userDir, scope: "user" }
|
|
17535
|
+
{ path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
|
|
17536
|
+
{ path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" }
|
|
17541
17537
|
];
|
|
17542
17538
|
return dirs.filter((d) => existsSync(d.path));
|
|
17543
17539
|
}
|
|
@@ -17569,7 +17565,11 @@ class SpecialistLoader {
|
|
|
17569
17565
|
filestoWatch: spec.specialist.validation?.files_to_watch,
|
|
17570
17566
|
staleThresholdDays: spec.specialist.validation?.stale_threshold_days
|
|
17571
17567
|
});
|
|
17572
|
-
} catch {
|
|
17568
|
+
} catch (e) {
|
|
17569
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
17570
|
+
process.stderr.write(`[specialists] skipping ${filePath}: ${reason}
|
|
17571
|
+
`);
|
|
17572
|
+
}
|
|
17573
17573
|
}
|
|
17574
17574
|
}
|
|
17575
17575
|
return results;
|
|
@@ -17584,11 +17584,10 @@ class SpecialistLoader {
|
|
|
17584
17584
|
const spec = await parseSpecialist(content);
|
|
17585
17585
|
const rawPaths = spec.specialist.skills?.paths;
|
|
17586
17586
|
if (rawPaths?.length) {
|
|
17587
|
-
const home = homedir();
|
|
17588
17587
|
const fileDir = dir.path;
|
|
17589
17588
|
const resolved = rawPaths.map((p) => {
|
|
17590
17589
|
if (p.startsWith("~/"))
|
|
17591
|
-
return join(
|
|
17590
|
+
return join(process.env.HOME || "", p.slice(2));
|
|
17592
17591
|
if (p.startsWith("./"))
|
|
17593
17592
|
return join(fileDir, p.slice(2));
|
|
17594
17593
|
return p;
|
|
@@ -17651,16 +17650,16 @@ var init_backendMap = __esm(() => {
|
|
|
17651
17650
|
// src/pi/session.ts
|
|
17652
17651
|
import { spawn } from "node:child_process";
|
|
17653
17652
|
import { existsSync as existsSync2 } from "node:fs";
|
|
17654
|
-
import { homedir
|
|
17653
|
+
import { homedir } from "node:os";
|
|
17655
17654
|
import { join as join2 } from "node:path";
|
|
17656
17655
|
function mapPermissionToTools(level) {
|
|
17657
17656
|
switch (level?.toUpperCase()) {
|
|
17658
17657
|
case "READ_ONLY":
|
|
17659
|
-
return "read,
|
|
17660
|
-
case "BASH_ONLY":
|
|
17661
|
-
return "bash";
|
|
17658
|
+
return "read,grep,find,ls";
|
|
17662
17659
|
case "LOW":
|
|
17660
|
+
return "read,bash,grep,find,ls";
|
|
17663
17661
|
case "MEDIUM":
|
|
17662
|
+
return "read,bash,edit,grep,find,ls";
|
|
17664
17663
|
case "HIGH":
|
|
17665
17664
|
return "read,bash,edit,write,grep,find,ls";
|
|
17666
17665
|
default:
|
|
@@ -17708,7 +17707,13 @@ class PiAgentSession {
|
|
|
17708
17707
|
const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
|
|
17709
17708
|
if (toolsFlag)
|
|
17710
17709
|
args.push("--tools", toolsFlag);
|
|
17711
|
-
|
|
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");
|
|
17712
17717
|
const permLevel = (this.options.permissionLevel ?? "").toUpperCase();
|
|
17713
17718
|
if (permLevel !== "READ_ONLY") {
|
|
17714
17719
|
const qgPath = join2(piExtDir, "quality-gates");
|
|
@@ -17902,6 +17907,30 @@ class PiAgentSession {
|
|
|
17902
17907
|
this.proc = undefined;
|
|
17903
17908
|
this._doneReject?.(new SessionKilledError);
|
|
17904
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
|
+
}
|
|
17905
17934
|
}
|
|
17906
17935
|
var SessionKilledError;
|
|
17907
17936
|
var init_session = __esm(() => {
|
|
@@ -18055,14 +18084,16 @@ var init_beads = () => {};
|
|
|
18055
18084
|
// src/specialist/runner.ts
|
|
18056
18085
|
import { createHash } from "node:crypto";
|
|
18057
18086
|
import { writeFile } from "node:fs/promises";
|
|
18058
|
-
import { execSync } from "node:child_process";
|
|
18059
|
-
import {
|
|
18060
|
-
|
|
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) {
|
|
18061
18092
|
try {
|
|
18062
|
-
const output = execSync(
|
|
18063
|
-
return { name: basename(
|
|
18093
|
+
const output = execSync(run, { encoding: "utf8", timeout: 30000 });
|
|
18094
|
+
return { name: basename(run.split(" ")[0]), output, exitCode: 0 };
|
|
18064
18095
|
} catch (e) {
|
|
18065
|
-
return { name: basename(
|
|
18096
|
+
return { name: basename(run.split(" ")[0]), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
|
|
18066
18097
|
}
|
|
18067
18098
|
}
|
|
18068
18099
|
function formatScriptOutput(results) {
|
|
@@ -18080,6 +18111,77 @@ ${r.output.trim()}
|
|
|
18080
18111
|
${blocks}
|
|
18081
18112
|
</pre_flight_context>`;
|
|
18082
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
|
+
}
|
|
18083
18185
|
|
|
18084
18186
|
class SpecialistRunner {
|
|
18085
18187
|
deps;
|
|
@@ -18088,12 +18190,12 @@ class SpecialistRunner {
|
|
|
18088
18190
|
this.deps = deps;
|
|
18089
18191
|
this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
|
|
18090
18192
|
}
|
|
18091
|
-
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated) {
|
|
18193
|
+
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
|
|
18092
18194
|
const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
|
|
18093
18195
|
const invocationId = crypto.randomUUID();
|
|
18094
18196
|
const start = Date.now();
|
|
18095
18197
|
const spec = await loader.get(options.name);
|
|
18096
|
-
const { metadata, execution, prompt,
|
|
18198
|
+
const { metadata, execution, prompt, output_file } = spec.specialist;
|
|
18097
18199
|
const primaryModel = options.backendOverride ?? execution.model;
|
|
18098
18200
|
const model = circuitBreaker.isAvailable(primaryModel) ? primaryModel : execution.fallback_model ?? primaryModel;
|
|
18099
18201
|
const fallbackUsed = model !== primaryModel;
|
|
@@ -18104,8 +18206,9 @@ class SpecialistRunner {
|
|
|
18104
18206
|
circuit_breaker_state: circuitBreaker.getState(model),
|
|
18105
18207
|
scope: "project"
|
|
18106
18208
|
});
|
|
18209
|
+
validateBeforeRun(spec);
|
|
18107
18210
|
const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
|
|
18108
|
-
const preResults = preScripts.map((s) => runScript(s.
|
|
18211
|
+
const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
|
|
18109
18212
|
const preScriptOutput = formatScriptOutput(preResults);
|
|
18110
18213
|
const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
|
|
18111
18214
|
const variables = {
|
|
@@ -18123,40 +18226,32 @@ class SpecialistRunner {
|
|
|
18123
18226
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
18124
18227
|
system_prompt_present: !!prompt.system
|
|
18125
18228
|
});
|
|
18126
|
-
const
|
|
18127
|
-
|
|
18128
|
-
if (prompt.skill_inherit)
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
agentsMd += `
|
|
18143
|
-
|
|
18144
|
-
---
|
|
18145
|
-
# Skill: ${skillPath}
|
|
18146
|
-
|
|
18147
|
-
${skillContent}`;
|
|
18148
|
-
}
|
|
18149
|
-
if (spec.specialist.capabilities?.diagnostic_scripts?.length) {
|
|
18150
|
-
agentsMd += `
|
|
18151
|
-
|
|
18152
|
-
---
|
|
18153
|
-
# Diagnostic Scripts
|
|
18154
|
-
You have access via Bash:
|
|
18155
|
-
`;
|
|
18156
|
-
for (const s of spec.specialist.capabilities.diagnostic_scripts) {
|
|
18157
|
-
agentsMd += `- \`${s}\`
|
|
18158
|
-
`;
|
|
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
|
+
`);
|
|
18159
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
|
+
`);
|
|
18160
18255
|
}
|
|
18161
18256
|
const permissionLevel = options.autonomyLevel ?? execution.permission_required;
|
|
18162
18257
|
await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
|
|
@@ -18180,10 +18275,13 @@ You have access via Bash:
|
|
|
18180
18275
|
let output;
|
|
18181
18276
|
let sessionBackend = model;
|
|
18182
18277
|
let session;
|
|
18278
|
+
let keepAliveActive = false;
|
|
18183
18279
|
try {
|
|
18184
18280
|
session = await this.sessionFactory({
|
|
18185
18281
|
model,
|
|
18186
18282
|
systemPrompt: agentsMd || undefined,
|
|
18283
|
+
skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
|
|
18284
|
+
thinkingLevel: execution.thinking_level,
|
|
18187
18285
|
permissionLevel,
|
|
18188
18286
|
cwd: process.cwd(),
|
|
18189
18287
|
onToken: (delta) => onProgress?.(delta),
|
|
@@ -18197,15 +18295,29 @@ You have access via Bash:
|
|
|
18197
18295
|
});
|
|
18198
18296
|
await session.start();
|
|
18199
18297
|
onKillRegistered?.(session.kill.bind(session));
|
|
18298
|
+
onSteerRegistered?.((msg) => session.steer(msg));
|
|
18200
18299
|
await session.prompt(renderedTask);
|
|
18201
18300
|
await session.waitForDone(execution.timeout_ms);
|
|
18202
18301
|
sessionBackend = session.meta.backend;
|
|
18203
18302
|
output = await session.getLastOutput();
|
|
18204
18303
|
sessionBackend = session.meta.backend;
|
|
18205
|
-
|
|
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
|
+
}
|
|
18206
18318
|
const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
|
|
18207
18319
|
for (const script of postScripts)
|
|
18208
|
-
runScript(script.
|
|
18320
|
+
runScript(script.run);
|
|
18209
18321
|
circuitBreaker.recordSuccess(model);
|
|
18210
18322
|
} catch (err) {
|
|
18211
18323
|
const isCancelled = err instanceof SessionKilledError;
|
|
@@ -18226,11 +18338,13 @@ You have access via Bash:
|
|
|
18226
18338
|
});
|
|
18227
18339
|
throw err;
|
|
18228
18340
|
} finally {
|
|
18229
|
-
|
|
18341
|
+
if (!keepAliveActive) {
|
|
18342
|
+
session?.kill();
|
|
18343
|
+
}
|
|
18230
18344
|
}
|
|
18231
18345
|
const durationMs = Date.now() - start;
|
|
18232
|
-
if (
|
|
18233
|
-
await writeFile(
|
|
18346
|
+
if (output_file) {
|
|
18347
|
+
await writeFile(output_file, output, "utf-8").catch(() => {});
|
|
18234
18348
|
}
|
|
18235
18349
|
await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
|
|
18236
18350
|
status: "COMPLETE",
|
|
@@ -18264,7 +18378,7 @@ You have access via Bash:
|
|
|
18264
18378
|
model: "?",
|
|
18265
18379
|
specialistVersion
|
|
18266
18380
|
});
|
|
18267
|
-
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));
|
|
18268
18382
|
return jobId;
|
|
18269
18383
|
}
|
|
18270
18384
|
}
|
|
@@ -18344,656 +18458,198 @@ class CircuitBreaker {
|
|
|
18344
18458
|
}
|
|
18345
18459
|
}
|
|
18346
18460
|
|
|
18347
|
-
// src/
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
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
|
+
}
|
|
18358
18493
|
}
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
}
|
|
18366
|
-
import { createRequire as createRequire2 } from "node:module";
|
|
18367
|
-
async function run2() {
|
|
18368
|
-
const req = createRequire2(import.meta.url);
|
|
18369
|
-
const pkg = req("../package.json");
|
|
18370
|
-
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
|
+
};
|
|
18371
18501
|
}
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
ArgParseError: () => ArgParseError
|
|
18380
|
-
});
|
|
18381
|
-
function parseArgs(argv) {
|
|
18382
|
-
const result = {};
|
|
18383
|
-
for (let i = 0;i < argv.length; i++) {
|
|
18384
|
-
const token = argv[i];
|
|
18385
|
-
if (token === "--category") {
|
|
18386
|
-
const value = argv[++i];
|
|
18387
|
-
if (!value || value.startsWith("--")) {
|
|
18388
|
-
throw new ArgParseError("--category requires a value");
|
|
18389
|
-
}
|
|
18390
|
-
result.category = value;
|
|
18391
|
-
continue;
|
|
18392
|
-
}
|
|
18393
|
-
if (token === "--scope") {
|
|
18394
|
-
const value = argv[++i];
|
|
18395
|
-
if (value !== "project" && value !== "user") {
|
|
18396
|
-
throw new ArgParseError(`--scope must be "project" or "user", got: "${value ?? ""}"`);
|
|
18397
|
-
}
|
|
18398
|
-
result.scope = value;
|
|
18399
|
-
continue;
|
|
18400
|
-
}
|
|
18401
|
-
if (token === "--json") {
|
|
18402
|
-
result.json = true;
|
|
18403
|
-
continue;
|
|
18404
|
-
}
|
|
18405
|
-
}
|
|
18406
|
-
return result;
|
|
18502
|
+
function createMetaEvent(model, backend) {
|
|
18503
|
+
return {
|
|
18504
|
+
t: Date.now(),
|
|
18505
|
+
type: TIMELINE_EVENT_TYPES.META,
|
|
18506
|
+
model,
|
|
18507
|
+
backend
|
|
18508
|
+
};
|
|
18407
18509
|
}
|
|
18408
|
-
|
|
18409
|
-
|
|
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) {
|
|
18410
18520
|
try {
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
|
|
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
|
+
};
|
|
18416
18534
|
}
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
}
|
|
18424
|
-
if (args.json) {
|
|
18425
|
-
console.log(JSON.stringify(specialists, null, 2));
|
|
18426
|
-
return;
|
|
18427
|
-
}
|
|
18428
|
-
if (specialists.length === 0) {
|
|
18429
|
-
console.log("No specialists found.");
|
|
18430
|
-
return;
|
|
18431
|
-
}
|
|
18432
|
-
const nameWidth = Math.max(...specialists.map((s) => s.name.length), 4);
|
|
18433
|
-
console.log(`
|
|
18434
|
-
${bold(`Specialists (${specialists.length})`)}
|
|
18435
|
-
`);
|
|
18436
|
-
for (const s of specialists) {
|
|
18437
|
-
const name = cyan(s.name.padEnd(nameWidth));
|
|
18438
|
-
const scopeTag = yellow(`[${s.scope}]`);
|
|
18439
|
-
const model = dim(s.model);
|
|
18440
|
-
const desc = s.description.length > 80 ? s.description.slice(0, 79) + "…" : s.description;
|
|
18441
|
-
console.log(` ${name} ${scopeTag} ${model}`);
|
|
18442
|
-
console.log(` ${" ".repeat(nameWidth)} ${dim(desc)}`);
|
|
18443
|
-
console.log();
|
|
18444
|
-
}
|
|
18445
|
-
}
|
|
18446
|
-
var dim = (s) => `\x1B[2m${s}\x1B[0m`, bold = (s) => `\x1B[1m${s}\x1B[0m`, cyan = (s) => `\x1B[36m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError;
|
|
18447
|
-
var init_list = __esm(() => {
|
|
18448
|
-
init_loader();
|
|
18449
|
-
ArgParseError = class ArgParseError extends Error {
|
|
18450
|
-
constructor(message) {
|
|
18451
|
-
super(message);
|
|
18452
|
-
this.name = "ArgParseError";
|
|
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
|
+
};
|
|
18453
18541
|
}
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
|
|
18458
|
-
|
|
18459
|
-
__export(exports_models, {
|
|
18460
|
-
run: () => run4
|
|
18461
|
-
});
|
|
18462
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
18463
|
-
function parsePiModels() {
|
|
18464
|
-
const r = spawnSync3("pi", ["--list-models"], {
|
|
18465
|
-
encoding: "utf8",
|
|
18466
|
-
stdio: "pipe",
|
|
18467
|
-
timeout: 8000
|
|
18468
|
-
});
|
|
18469
|
-
if (r.status !== 0 || r.error)
|
|
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 {
|
|
18470
18547
|
return null;
|
|
18471
|
-
return r.stdout.split(`
|
|
18472
|
-
`).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
18473
|
-
const cols = line.split(/\s+/);
|
|
18474
|
-
return {
|
|
18475
|
-
provider: cols[0] ?? "",
|
|
18476
|
-
model: cols[1] ?? "",
|
|
18477
|
-
context: cols[2] ?? "",
|
|
18478
|
-
maxOut: cols[3] ?? "",
|
|
18479
|
-
thinking: cols[4] === "yes",
|
|
18480
|
-
images: cols[5] === "yes"
|
|
18481
|
-
};
|
|
18482
|
-
}).filter((m) => m.provider && m.model);
|
|
18483
|
-
}
|
|
18484
|
-
function parseArgs2(argv) {
|
|
18485
|
-
const out = {};
|
|
18486
|
-
for (let i = 0;i < argv.length; i++) {
|
|
18487
|
-
if (argv[i] === "--provider" && argv[i + 1]) {
|
|
18488
|
-
out.provider = argv[++i];
|
|
18489
|
-
continue;
|
|
18490
|
-
}
|
|
18491
|
-
if (argv[i] === "--used") {
|
|
18492
|
-
out.used = true;
|
|
18493
|
-
continue;
|
|
18494
|
-
}
|
|
18495
18548
|
}
|
|
18496
|
-
return out;
|
|
18497
18549
|
}
|
|
18498
|
-
|
|
18499
|
-
|
|
18500
|
-
const loader = new SpecialistLoader;
|
|
18501
|
-
const specialists = await loader.list();
|
|
18502
|
-
const usedBy = new Map;
|
|
18503
|
-
for (const s of specialists) {
|
|
18504
|
-
const key = s.model;
|
|
18505
|
-
if (!usedBy.has(key))
|
|
18506
|
-
usedBy.set(key, []);
|
|
18507
|
-
usedBy.get(key).push(s.name);
|
|
18508
|
-
}
|
|
18509
|
-
const allModels = parsePiModels();
|
|
18510
|
-
if (!allModels) {
|
|
18511
|
-
console.error("pi not found or failed — install and configure pi first");
|
|
18512
|
-
process.exit(1);
|
|
18513
|
-
}
|
|
18514
|
-
let models = allModels;
|
|
18515
|
-
if (args.provider) {
|
|
18516
|
-
models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
|
|
18517
|
-
}
|
|
18518
|
-
if (args.used) {
|
|
18519
|
-
models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
|
|
18520
|
-
}
|
|
18521
|
-
if (models.length === 0) {
|
|
18522
|
-
console.log("No models match.");
|
|
18523
|
-
return;
|
|
18524
|
-
}
|
|
18525
|
-
const byProvider = new Map;
|
|
18526
|
-
for (const m of models) {
|
|
18527
|
-
if (!byProvider.has(m.provider))
|
|
18528
|
-
byProvider.set(m.provider, []);
|
|
18529
|
-
byProvider.get(m.provider).push(m);
|
|
18530
|
-
}
|
|
18531
|
-
const total = models.length;
|
|
18532
|
-
console.log(`
|
|
18533
|
-
${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
|
|
18534
|
-
`);
|
|
18535
|
-
for (const [provider, pModels] of byProvider) {
|
|
18536
|
-
console.log(` ${cyan2(provider)} ${dim2(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
|
|
18537
|
-
const modelWidth = Math.max(...pModels.map((m) => m.model.length));
|
|
18538
|
-
for (const m of pModels) {
|
|
18539
|
-
const key = `${m.provider}/${m.model}`;
|
|
18540
|
-
const inUse = usedBy.get(key);
|
|
18541
|
-
const flags = [
|
|
18542
|
-
m.thinking ? green("thinking") : dim2("·"),
|
|
18543
|
-
m.images ? dim2("images") : ""
|
|
18544
|
-
].filter(Boolean).join(" ");
|
|
18545
|
-
const ctx = dim2(`ctx ${m.context}`);
|
|
18546
|
-
const usedLabel = inUse ? ` ${yellow2("←")} ${dim2(inUse.join(", "))}` : "";
|
|
18547
|
-
console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
|
|
18548
|
-
}
|
|
18549
|
-
console.log();
|
|
18550
|
-
}
|
|
18551
|
-
if (!args.used) {
|
|
18552
|
-
console.log(dim2(` --provider <name> filter by provider`));
|
|
18553
|
-
console.log(dim2(` --used only show models used by your specialists`));
|
|
18554
|
-
console.log();
|
|
18555
|
-
}
|
|
18550
|
+
function isRunCompleteEvent(event) {
|
|
18551
|
+
return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
|
|
18556
18552
|
}
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
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"
|
|
18567
|
+
};
|
|
18560
18568
|
});
|
|
18561
18569
|
|
|
18562
|
-
// src/
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
`, "utf-8");
|
|
18587
|
-
}
|
|
18588
|
-
function ensureProjectMcp(cwd) {
|
|
18589
|
-
const mcpPath = join6(cwd, MCP_FILE);
|
|
18590
|
-
const mcp = loadJson(mcpPath, { mcpServers: {} });
|
|
18591
|
-
mcp.mcpServers ??= {};
|
|
18592
|
-
const existing = mcp.mcpServers[MCP_SERVER_NAME];
|
|
18593
|
-
if (existing && existing.command === MCP_SERVER_CONFIG.command && Array.isArray(existing.args) && existing.args.length === MCP_SERVER_CONFIG.args.length) {
|
|
18594
|
-
skip(".mcp.json already registers specialists");
|
|
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)
|
|
18595
18594
|
return;
|
|
18596
|
-
|
|
18597
|
-
|
|
18598
|
-
saveJson(mcpPath, mcp);
|
|
18599
|
-
ok("registered specialists in project .mcp.json");
|
|
18595
|
+
const sha = result.stdout?.trim();
|
|
18596
|
+
return sha || undefined;
|
|
18600
18597
|
}
|
|
18601
|
-
|
|
18602
|
-
const
|
|
18603
|
-
|
|
18604
|
-
|
|
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(`
|
|
18605
18606
|
`);
|
|
18606
|
-
|
|
18607
|
-
if (existsSync4(specialistsDir)) {
|
|
18608
|
-
skip("specialists/ already exists");
|
|
18609
|
-
} else {
|
|
18610
|
-
mkdirSync(specialistsDir, { recursive: true });
|
|
18611
|
-
ok("created specialists/");
|
|
18612
|
-
}
|
|
18613
|
-
const runtimeDir = join6(cwd, ".specialists");
|
|
18614
|
-
if (existsSync4(runtimeDir)) {
|
|
18615
|
-
skip(".specialists/ already exists");
|
|
18616
|
-
} else {
|
|
18617
|
-
mkdirSync(join6(runtimeDir, "jobs"), { recursive: true });
|
|
18618
|
-
mkdirSync(join6(runtimeDir, "ready"), { recursive: true });
|
|
18619
|
-
ok("created .specialists/ (jobs/, ready/)");
|
|
18620
|
-
}
|
|
18621
|
-
const gitignorePath = join6(cwd, ".gitignore");
|
|
18622
|
-
if (existsSync4(gitignorePath)) {
|
|
18623
|
-
const existing = readFileSync(gitignorePath, "utf-8");
|
|
18624
|
-
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
18625
|
-
skip(".gitignore already has .specialists/ entry");
|
|
18626
|
-
} else {
|
|
18627
|
-
const separator = existing.endsWith(`
|
|
18628
|
-
`) ? "" : `
|
|
18629
|
-
`;
|
|
18630
|
-
writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + `
|
|
18631
|
-
`, "utf-8");
|
|
18632
|
-
ok("added .specialists/ to .gitignore");
|
|
18633
|
-
}
|
|
18634
|
-
} else {
|
|
18635
|
-
writeFileSync(gitignorePath, GITIGNORE_ENTRY + `
|
|
18636
|
-
`, "utf-8");
|
|
18637
|
-
ok("created .gitignore with .specialists/ entry");
|
|
18638
|
-
}
|
|
18639
|
-
const agentsPath = join6(cwd, "AGENTS.md");
|
|
18640
|
-
if (existsSync4(agentsPath)) {
|
|
18641
|
-
const existing = readFileSync(agentsPath, "utf-8");
|
|
18642
|
-
if (existing.includes(AGENTS_MARKER)) {
|
|
18643
|
-
skip("AGENTS.md already has Specialists section");
|
|
18644
|
-
} else {
|
|
18645
|
-
writeFileSync(agentsPath, existing.trimEnd() + `
|
|
18607
|
+
return `${result.output}
|
|
18646
18608
|
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
}
|
|
18650
|
-
} else {
|
|
18651
|
-
writeFileSync(agentsPath, AGENTS_BLOCK, "utf-8");
|
|
18652
|
-
ok("created AGENTS.md with Specialists section");
|
|
18653
|
-
}
|
|
18654
|
-
ensureProjectMcp(cwd);
|
|
18655
|
-
console.log(`
|
|
18656
|
-
${bold3("Done!")}
|
|
18657
|
-
`);
|
|
18658
|
-
console.log(` ${dim3("Next steps:")}`);
|
|
18659
|
-
console.log(` 1. Add your specialists to ${yellow3("specialists/")}`);
|
|
18660
|
-
console.log(` 2. Run ${yellow3("specialists list")} to verify they are discovered`);
|
|
18661
|
-
console.log(` 3. Restart Claude Code to pick up AGENTS.md / .mcp.json changes
|
|
18662
|
-
`);
|
|
18609
|
+
---
|
|
18610
|
+
${metadata}`;
|
|
18663
18611
|
}
|
|
18664
|
-
var bold3 = (s) => `\x1B[1m${s}\x1B[0m`, green2 = (s) => `\x1B[32m${s}\x1B[0m`, yellow3 = (s) => `\x1B[33m${s}\x1B[0m`, dim3 = (s) => `\x1B[2m${s}\x1B[0m`, AGENTS_BLOCK, AGENTS_MARKER = "## Specialists", GITIGNORE_ENTRY = ".specialists/", MCP_FILE = ".mcp.json", MCP_SERVER_NAME = "specialists", MCP_SERVER_CONFIG;
|
|
18665
|
-
var init_init = __esm(() => {
|
|
18666
|
-
AGENTS_BLOCK = `
|
|
18667
|
-
## Specialists
|
|
18668
18612
|
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
`.trimStart();
|
|
18674
|
-
MCP_SERVER_CONFIG = { command: "specialists", args: [] };
|
|
18675
|
-
});
|
|
18676
|
-
|
|
18677
|
-
// src/cli/edit.ts
|
|
18678
|
-
var exports_edit = {};
|
|
18679
|
-
__export(exports_edit, {
|
|
18680
|
-
run: () => run6
|
|
18681
|
-
});
|
|
18682
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18683
|
-
function parseArgs3(argv) {
|
|
18684
|
-
const name = argv[0];
|
|
18685
|
-
if (!name || name.startsWith("--")) {
|
|
18686
|
-
console.error("Usage: specialists edit <name> --<field> <value> [--dry-run]");
|
|
18687
|
-
console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
|
|
18688
|
-
process.exit(1);
|
|
18613
|
+
class Supervisor {
|
|
18614
|
+
opts;
|
|
18615
|
+
constructor(opts) {
|
|
18616
|
+
this.opts = opts;
|
|
18689
18617
|
}
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
let dryRun = false;
|
|
18693
|
-
let scope;
|
|
18694
|
-
for (let i = 1;i < argv.length; i++) {
|
|
18695
|
-
const token = argv[i];
|
|
18696
|
-
if (token === "--dry-run") {
|
|
18697
|
-
dryRun = true;
|
|
18698
|
-
continue;
|
|
18699
|
-
}
|
|
18700
|
-
if (token === "--scope") {
|
|
18701
|
-
const v = argv[++i];
|
|
18702
|
-
if (v !== "project" && v !== "user") {
|
|
18703
|
-
console.error(`Error: --scope must be "project" or "user", got: "${v ?? ""}"`);
|
|
18704
|
-
process.exit(1);
|
|
18705
|
-
}
|
|
18706
|
-
scope = v;
|
|
18707
|
-
continue;
|
|
18708
|
-
}
|
|
18709
|
-
if (token.startsWith("--") && !field) {
|
|
18710
|
-
field = token.slice(2);
|
|
18711
|
-
value = argv[++i];
|
|
18712
|
-
continue;
|
|
18713
|
-
}
|
|
18618
|
+
jobDir(id) {
|
|
18619
|
+
return join3(this.opts.jobsDir, id);
|
|
18714
18620
|
}
|
|
18715
|
-
|
|
18716
|
-
|
|
18717
|
-
process.exit(1);
|
|
18621
|
+
statusPath(id) {
|
|
18622
|
+
return join3(this.jobDir(id), "status.json");
|
|
18718
18623
|
}
|
|
18719
|
-
|
|
18720
|
-
|
|
18721
|
-
process.exit(1);
|
|
18624
|
+
resultPath(id) {
|
|
18625
|
+
return join3(this.jobDir(id), "result.txt");
|
|
18722
18626
|
}
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
process.exit(1);
|
|
18627
|
+
eventsPath(id) {
|
|
18628
|
+
return join3(this.jobDir(id), "events.jsonl");
|
|
18726
18629
|
}
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
process.exit(1);
|
|
18730
|
-
}
|
|
18731
|
-
return { name, field, value, dryRun, scope };
|
|
18732
|
-
}
|
|
18733
|
-
function setIn(doc2, path, value) {
|
|
18734
|
-
let node = doc2;
|
|
18735
|
-
for (let i = 0;i < path.length - 1; i++) {
|
|
18736
|
-
node = node.get(path[i], true);
|
|
18737
|
-
}
|
|
18738
|
-
const leaf = path[path.length - 1];
|
|
18739
|
-
if (Array.isArray(value)) {
|
|
18740
|
-
node.set(leaf, value);
|
|
18741
|
-
} else {
|
|
18742
|
-
node.set(leaf, value);
|
|
18743
|
-
}
|
|
18744
|
-
}
|
|
18745
|
-
async function run6() {
|
|
18746
|
-
const args = parseArgs3(process.argv.slice(3));
|
|
18747
|
-
const { name, field, value, dryRun, scope } = args;
|
|
18748
|
-
const loader = new SpecialistLoader;
|
|
18749
|
-
const all = await loader.list();
|
|
18750
|
-
const match = all.find((s) => s.name === name && (scope === undefined || s.scope === scope));
|
|
18751
|
-
if (!match) {
|
|
18752
|
-
const hint = scope ? ` (scope: ${scope})` : "";
|
|
18753
|
-
console.error(`Error: specialist "${name}" not found${hint}`);
|
|
18754
|
-
console.error(` Run ${yellow4("specialists list")} to see available specialists`);
|
|
18755
|
-
process.exit(1);
|
|
18756
|
-
}
|
|
18757
|
-
const raw = readFileSync2(match.filePath, "utf-8");
|
|
18758
|
-
const doc2 = $parseDocument(raw);
|
|
18759
|
-
const yamlPath = FIELD_MAP[field];
|
|
18760
|
-
let typedValue = value;
|
|
18761
|
-
if (field === "timeout") {
|
|
18762
|
-
typedValue = parseInt(value, 10);
|
|
18763
|
-
} else if (field === "tags") {
|
|
18764
|
-
typedValue = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
18765
|
-
}
|
|
18766
|
-
setIn(doc2, yamlPath, typedValue);
|
|
18767
|
-
const updated = doc2.toString();
|
|
18768
|
-
if (dryRun) {
|
|
18769
|
-
console.log(`
|
|
18770
|
-
${bold4(`[dry-run] ${match.filePath}`)}
|
|
18771
|
-
`);
|
|
18772
|
-
console.log(dim4("--- current"));
|
|
18773
|
-
console.log(dim4(`+++ updated`));
|
|
18774
|
-
const oldLines = raw.split(`
|
|
18775
|
-
`);
|
|
18776
|
-
const newLines = updated.split(`
|
|
18777
|
-
`);
|
|
18778
|
-
newLines.forEach((line, i) => {
|
|
18779
|
-
if (line !== oldLines[i]) {
|
|
18780
|
-
if (oldLines[i] !== undefined)
|
|
18781
|
-
console.log(dim4(`- ${oldLines[i]}`));
|
|
18782
|
-
console.log(green3(`+ ${line}`));
|
|
18783
|
-
}
|
|
18784
|
-
});
|
|
18785
|
-
console.log();
|
|
18786
|
-
return;
|
|
18787
|
-
}
|
|
18788
|
-
writeFileSync2(match.filePath, updated, "utf-8");
|
|
18789
|
-
const displayValue = field === "tags" ? `[${typedValue.join(", ")}]` : String(typedValue);
|
|
18790
|
-
console.log(`${green3("✓")} ${bold4(name)}: ${yellow4(field)} = ${displayValue}` + dim4(` (${match.filePath})`));
|
|
18791
|
-
}
|
|
18792
|
-
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;
|
|
18793
|
-
var init_edit = __esm(() => {
|
|
18794
|
-
init_dist();
|
|
18795
|
-
init_loader();
|
|
18796
|
-
FIELD_MAP = {
|
|
18797
|
-
model: ["specialist", "execution", "model"],
|
|
18798
|
-
"fallback-model": ["specialist", "execution", "fallback_model"],
|
|
18799
|
-
description: ["specialist", "metadata", "description"],
|
|
18800
|
-
permission: ["specialist", "execution", "permission_required"],
|
|
18801
|
-
timeout: ["specialist", "execution", "timeout_ms"],
|
|
18802
|
-
tags: ["specialist", "metadata", "tags"]
|
|
18803
|
-
};
|
|
18804
|
-
VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
|
|
18805
|
-
});
|
|
18806
|
-
|
|
18807
|
-
// src/specialist/timeline-events.ts
|
|
18808
|
-
function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
18809
|
-
const t = Date.now();
|
|
18810
|
-
switch (callbackEvent) {
|
|
18811
|
-
case "thinking":
|
|
18812
|
-
return { t, type: TIMELINE_EVENT_TYPES.THINKING };
|
|
18813
|
-
case "toolcall":
|
|
18814
|
-
return {
|
|
18815
|
-
t,
|
|
18816
|
-
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18817
|
-
tool: context.tool ?? "unknown",
|
|
18818
|
-
phase: "start",
|
|
18819
|
-
tool_call_id: context.toolCallId
|
|
18820
|
-
};
|
|
18821
|
-
case "tool_execution_end":
|
|
18822
|
-
return {
|
|
18823
|
-
t,
|
|
18824
|
-
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18825
|
-
tool: context.tool ?? "unknown",
|
|
18826
|
-
phase: "end",
|
|
18827
|
-
tool_call_id: context.toolCallId,
|
|
18828
|
-
is_error: context.isError
|
|
18829
|
-
};
|
|
18830
|
-
case "text":
|
|
18831
|
-
return { t, type: TIMELINE_EVENT_TYPES.TEXT };
|
|
18832
|
-
case "agent_end":
|
|
18833
|
-
case "message_done":
|
|
18834
|
-
case "done":
|
|
18835
|
-
return null;
|
|
18836
|
-
default:
|
|
18837
|
-
return null;
|
|
18838
|
-
}
|
|
18839
|
-
}
|
|
18840
|
-
function createRunStartEvent(specialist, beadId) {
|
|
18841
|
-
return {
|
|
18842
|
-
t: Date.now(),
|
|
18843
|
-
type: TIMELINE_EVENT_TYPES.RUN_START,
|
|
18844
|
-
specialist,
|
|
18845
|
-
bead_id: beadId
|
|
18846
|
-
};
|
|
18847
|
-
}
|
|
18848
|
-
function createMetaEvent(model, backend) {
|
|
18849
|
-
return {
|
|
18850
|
-
t: Date.now(),
|
|
18851
|
-
type: TIMELINE_EVENT_TYPES.META,
|
|
18852
|
-
model,
|
|
18853
|
-
backend
|
|
18854
|
-
};
|
|
18855
|
-
}
|
|
18856
|
-
function createRunCompleteEvent(status, elapsed_s, options) {
|
|
18857
|
-
return {
|
|
18858
|
-
t: Date.now(),
|
|
18859
|
-
type: TIMELINE_EVENT_TYPES.RUN_COMPLETE,
|
|
18860
|
-
status,
|
|
18861
|
-
elapsed_s,
|
|
18862
|
-
...options
|
|
18863
|
-
};
|
|
18864
|
-
}
|
|
18865
|
-
function parseTimelineEvent(line) {
|
|
18866
|
-
try {
|
|
18867
|
-
const parsed = JSON.parse(line);
|
|
18868
|
-
if (!parsed || typeof parsed !== "object")
|
|
18869
|
-
return null;
|
|
18870
|
-
if (typeof parsed.t !== "number")
|
|
18871
|
-
return null;
|
|
18872
|
-
if (typeof parsed.type !== "string")
|
|
18873
|
-
return null;
|
|
18874
|
-
if (parsed.type === TIMELINE_EVENT_TYPES.DONE) {
|
|
18875
|
-
return {
|
|
18876
|
-
t: parsed.t,
|
|
18877
|
-
type: TIMELINE_EVENT_TYPES.DONE,
|
|
18878
|
-
elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
|
|
18879
|
-
};
|
|
18880
|
-
}
|
|
18881
|
-
if (parsed.type === TIMELINE_EVENT_TYPES.AGENT_END) {
|
|
18882
|
-
return {
|
|
18883
|
-
t: parsed.t,
|
|
18884
|
-
type: TIMELINE_EVENT_TYPES.AGENT_END,
|
|
18885
|
-
elapsed_s: typeof parsed.elapsed_s === "number" ? parsed.elapsed_s : undefined
|
|
18886
|
-
};
|
|
18887
|
-
}
|
|
18888
|
-
const knownTypes = Object.values(TIMELINE_EVENT_TYPES).filter((type) => type !== TIMELINE_EVENT_TYPES.DONE && type !== TIMELINE_EVENT_TYPES.AGENT_END);
|
|
18889
|
-
if (!knownTypes.includes(parsed.type))
|
|
18890
|
-
return null;
|
|
18891
|
-
return parsed;
|
|
18892
|
-
} catch {
|
|
18893
|
-
return null;
|
|
18894
|
-
}
|
|
18895
|
-
}
|
|
18896
|
-
function isRunCompleteEvent(event) {
|
|
18897
|
-
return event.type === TIMELINE_EVENT_TYPES.RUN_COMPLETE;
|
|
18898
|
-
}
|
|
18899
|
-
function compareTimelineEvents(a, b) {
|
|
18900
|
-
return a.t - b.t;
|
|
18901
|
-
}
|
|
18902
|
-
var TIMELINE_EVENT_TYPES;
|
|
18903
|
-
var init_timeline_events = __esm(() => {
|
|
18904
|
-
TIMELINE_EVENT_TYPES = {
|
|
18905
|
-
RUN_START: "run_start",
|
|
18906
|
-
META: "meta",
|
|
18907
|
-
THINKING: "thinking",
|
|
18908
|
-
TOOL: "tool",
|
|
18909
|
-
TEXT: "text",
|
|
18910
|
-
RUN_COMPLETE: "run_complete",
|
|
18911
|
-
DONE: "done",
|
|
18912
|
-
AGENT_END: "agent_end"
|
|
18913
|
-
};
|
|
18914
|
-
});
|
|
18915
|
-
|
|
18916
|
-
// src/specialist/supervisor.ts
|
|
18917
|
-
import {
|
|
18918
|
-
closeSync,
|
|
18919
|
-
existsSync as existsSync5,
|
|
18920
|
-
mkdirSync as mkdirSync2,
|
|
18921
|
-
openSync,
|
|
18922
|
-
readdirSync,
|
|
18923
|
-
readFileSync as readFileSync3,
|
|
18924
|
-
renameSync,
|
|
18925
|
-
rmSync,
|
|
18926
|
-
statSync,
|
|
18927
|
-
writeFileSync as writeFileSync3,
|
|
18928
|
-
writeSync
|
|
18929
|
-
} from "node:fs";
|
|
18930
|
-
import { join as join7 } from "node:path";
|
|
18931
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
18932
|
-
function getCurrentGitSha() {
|
|
18933
|
-
const result = spawnSync4("git", ["rev-parse", "HEAD"], {
|
|
18934
|
-
encoding: "utf-8",
|
|
18935
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
18936
|
-
});
|
|
18937
|
-
if (result.status !== 0)
|
|
18938
|
-
return;
|
|
18939
|
-
const sha = result.stdout?.trim();
|
|
18940
|
-
return sha || undefined;
|
|
18941
|
-
}
|
|
18942
|
-
function formatBeadNotes(result) {
|
|
18943
|
-
const metadata = [
|
|
18944
|
-
`prompt_hash=${result.promptHash}`,
|
|
18945
|
-
`git_sha=${getCurrentGitSha() ?? "unknown"}`,
|
|
18946
|
-
`elapsed_ms=${Math.round(result.durationMs)}`,
|
|
18947
|
-
`model=${result.model}`,
|
|
18948
|
-
`backend=${result.backend}`
|
|
18949
|
-
].join(`
|
|
18950
|
-
`);
|
|
18951
|
-
return `${result.output}
|
|
18952
|
-
|
|
18953
|
-
---
|
|
18954
|
-
${metadata}`;
|
|
18955
|
-
}
|
|
18956
|
-
|
|
18957
|
-
class Supervisor {
|
|
18958
|
-
opts;
|
|
18959
|
-
constructor(opts) {
|
|
18960
|
-
this.opts = opts;
|
|
18961
|
-
}
|
|
18962
|
-
jobDir(id) {
|
|
18963
|
-
return join7(this.opts.jobsDir, id);
|
|
18964
|
-
}
|
|
18965
|
-
statusPath(id) {
|
|
18966
|
-
return join7(this.jobDir(id), "status.json");
|
|
18967
|
-
}
|
|
18968
|
-
resultPath(id) {
|
|
18969
|
-
return join7(this.jobDir(id), "result.txt");
|
|
18970
|
-
}
|
|
18971
|
-
eventsPath(id) {
|
|
18972
|
-
return join7(this.jobDir(id), "events.jsonl");
|
|
18973
|
-
}
|
|
18974
|
-
readyDir() {
|
|
18975
|
-
return join7(this.opts.jobsDir, "..", "ready");
|
|
18630
|
+
readyDir() {
|
|
18631
|
+
return join3(this.opts.jobsDir, "..", "ready");
|
|
18976
18632
|
}
|
|
18977
18633
|
readStatus(id) {
|
|
18978
18634
|
const path = this.statusPath(id);
|
|
18979
|
-
if (!
|
|
18635
|
+
if (!existsSync4(path))
|
|
18980
18636
|
return null;
|
|
18981
18637
|
try {
|
|
18982
|
-
return JSON.parse(
|
|
18638
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
18983
18639
|
} catch {
|
|
18984
18640
|
return null;
|
|
18985
18641
|
}
|
|
18986
18642
|
}
|
|
18987
18643
|
listJobs() {
|
|
18988
|
-
if (!
|
|
18644
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
18989
18645
|
return [];
|
|
18990
18646
|
const jobs = [];
|
|
18991
18647
|
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18992
|
-
const path =
|
|
18993
|
-
if (!
|
|
18648
|
+
const path = join3(this.opts.jobsDir, entry, "status.json");
|
|
18649
|
+
if (!existsSync4(path))
|
|
18994
18650
|
continue;
|
|
18995
18651
|
try {
|
|
18996
|
-
jobs.push(JSON.parse(
|
|
18652
|
+
jobs.push(JSON.parse(readFileSync2(path, "utf-8")));
|
|
18997
18653
|
} catch {}
|
|
18998
18654
|
}
|
|
18999
18655
|
return jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
@@ -19001,7 +18657,7 @@ class Supervisor {
|
|
|
19001
18657
|
writeStatusFile(id, data) {
|
|
19002
18658
|
const path = this.statusPath(id);
|
|
19003
18659
|
const tmp = path + ".tmp";
|
|
19004
|
-
|
|
18660
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
19005
18661
|
renameSync(tmp, path);
|
|
19006
18662
|
}
|
|
19007
18663
|
updateStatus(id, updates) {
|
|
@@ -19011,11 +18667,11 @@ class Supervisor {
|
|
|
19011
18667
|
this.writeStatusFile(id, { ...current, ...updates });
|
|
19012
18668
|
}
|
|
19013
18669
|
gc() {
|
|
19014
|
-
if (!
|
|
18670
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
19015
18671
|
return;
|
|
19016
18672
|
const cutoff = Date.now() - JOB_TTL_DAYS * 86400000;
|
|
19017
18673
|
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
19018
|
-
const dir =
|
|
18674
|
+
const dir = join3(this.opts.jobsDir, entry);
|
|
19019
18675
|
try {
|
|
19020
18676
|
const stat2 = statSync(dir);
|
|
19021
18677
|
if (!stat2.isDirectory())
|
|
@@ -19026,14 +18682,14 @@ class Supervisor {
|
|
|
19026
18682
|
}
|
|
19027
18683
|
}
|
|
19028
18684
|
crashRecovery() {
|
|
19029
|
-
if (!
|
|
18685
|
+
if (!existsSync4(this.opts.jobsDir))
|
|
19030
18686
|
return;
|
|
19031
18687
|
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
19032
|
-
const statusPath =
|
|
19033
|
-
if (!
|
|
18688
|
+
const statusPath = join3(this.opts.jobsDir, entry, "status.json");
|
|
18689
|
+
if (!existsSync4(statusPath))
|
|
19034
18690
|
continue;
|
|
19035
18691
|
try {
|
|
19036
|
-
const s = JSON.parse(
|
|
18692
|
+
const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
|
|
19037
18693
|
if (s.status !== "running" && s.status !== "starting")
|
|
19038
18694
|
continue;
|
|
19039
18695
|
if (!s.pid)
|
|
@@ -19043,7 +18699,7 @@ class Supervisor {
|
|
|
19043
18699
|
} catch {
|
|
19044
18700
|
const tmp = statusPath + ".tmp";
|
|
19045
18701
|
const updated = { ...s, status: "error", error: "Process crashed or was killed" };
|
|
19046
|
-
|
|
18702
|
+
writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
19047
18703
|
renameSync(tmp, statusPath);
|
|
19048
18704
|
}
|
|
19049
18705
|
} catch {}
|
|
@@ -19056,8 +18712,8 @@ class Supervisor {
|
|
|
19056
18712
|
const id = crypto.randomUUID().slice(0, 6);
|
|
19057
18713
|
const dir = this.jobDir(id);
|
|
19058
18714
|
const startedAtMs = Date.now();
|
|
19059
|
-
|
|
19060
|
-
|
|
18715
|
+
mkdirSync(dir, { recursive: true });
|
|
18716
|
+
mkdirSync(this.readyDir(), { recursive: true });
|
|
19061
18717
|
const initialStatus = {
|
|
19062
18718
|
id,
|
|
19063
18719
|
specialist: runOptions.name,
|
|
@@ -19074,10 +18730,18 @@ class Supervisor {
|
|
|
19074
18730
|
} catch {}
|
|
19075
18731
|
};
|
|
19076
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 {}
|
|
19077
18738
|
let textLogged = false;
|
|
19078
18739
|
let currentTool = "";
|
|
19079
18740
|
let currentToolCallId = "";
|
|
19080
18741
|
let killFn;
|
|
18742
|
+
let steerFn;
|
|
18743
|
+
let resumeFn;
|
|
18744
|
+
let closeFn;
|
|
19081
18745
|
const sigtermHandler = () => killFn?.();
|
|
19082
18746
|
process.once("SIGTERM", sigtermHandler);
|
|
19083
18747
|
try {
|
|
@@ -19112,9 +18776,44 @@ class Supervisor {
|
|
|
19112
18776
|
killFn = fn;
|
|
19113
18777
|
}, (beadId) => {
|
|
19114
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" });
|
|
19115
18814
|
});
|
|
19116
18815
|
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
19117
|
-
|
|
18816
|
+
writeFileSync(this.resultPath(id), result.output, "utf-8");
|
|
19118
18817
|
if (result.beadId) {
|
|
19119
18818
|
this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
|
|
19120
18819
|
}
|
|
@@ -19131,7 +18830,7 @@ class Supervisor {
|
|
|
19131
18830
|
backend: result.backend,
|
|
19132
18831
|
bead_id: result.beadId
|
|
19133
18832
|
}));
|
|
19134
|
-
|
|
18833
|
+
writeFileSync(join3(this.readyDir(), id), "", "utf-8");
|
|
19135
18834
|
return id;
|
|
19136
18835
|
} catch (err) {
|
|
19137
18836
|
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
@@ -19148,13 +18847,663 @@ class Supervisor {
|
|
|
19148
18847
|
} finally {
|
|
19149
18848
|
process.removeListener("SIGTERM", sigtermHandler);
|
|
19150
18849
|
closeSync(eventsFd);
|
|
18850
|
+
try {
|
|
18851
|
+
if (existsSync4(fifoPath))
|
|
18852
|
+
rmSync(fifoPath);
|
|
18853
|
+
} catch {}
|
|
19151
18854
|
}
|
|
19152
18855
|
}
|
|
19153
18856
|
}
|
|
19154
|
-
var JOB_TTL_DAYS;
|
|
19155
|
-
var init_supervisor = __esm(() => {
|
|
19156
|
-
init_timeline_events();
|
|
19157
|
-
JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
|
|
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)
|
|
18986
|
+
return null;
|
|
18987
|
+
return r.stdout.split(`
|
|
18988
|
+
`).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
18989
|
+
const cols = line.split(/\s+/);
|
|
18990
|
+
return {
|
|
18991
|
+
provider: cols[0] ?? "",
|
|
18992
|
+
model: cols[1] ?? "",
|
|
18993
|
+
context: cols[2] ?? "",
|
|
18994
|
+
maxOut: cols[3] ?? "",
|
|
18995
|
+
thinking: cols[4] === "yes",
|
|
18996
|
+
images: cols[5] === "yes"
|
|
18997
|
+
};
|
|
18998
|
+
}).filter((m) => m.provider && m.model);
|
|
18999
|
+
}
|
|
19000
|
+
function parseArgs2(argv) {
|
|
19001
|
+
const out = {};
|
|
19002
|
+
for (let i = 0;i < argv.length; i++) {
|
|
19003
|
+
if (argv[i] === "--provider" && argv[i + 1]) {
|
|
19004
|
+
out.provider = argv[++i];
|
|
19005
|
+
continue;
|
|
19006
|
+
}
|
|
19007
|
+
if (argv[i] === "--used") {
|
|
19008
|
+
out.used = true;
|
|
19009
|
+
continue;
|
|
19010
|
+
}
|
|
19011
|
+
}
|
|
19012
|
+
return out;
|
|
19013
|
+
}
|
|
19014
|
+
async function run4() {
|
|
19015
|
+
const args = parseArgs2(process.argv.slice(3));
|
|
19016
|
+
const loader = new SpecialistLoader;
|
|
19017
|
+
const specialists = await loader.list();
|
|
19018
|
+
const usedBy = new Map;
|
|
19019
|
+
for (const s of specialists) {
|
|
19020
|
+
const key = s.model;
|
|
19021
|
+
if (!usedBy.has(key))
|
|
19022
|
+
usedBy.set(key, []);
|
|
19023
|
+
usedBy.get(key).push(s.name);
|
|
19024
|
+
}
|
|
19025
|
+
const allModels = parsePiModels();
|
|
19026
|
+
if (!allModels) {
|
|
19027
|
+
console.error("pi not found or failed — install and configure pi first");
|
|
19028
|
+
process.exit(1);
|
|
19029
|
+
}
|
|
19030
|
+
let models = allModels;
|
|
19031
|
+
if (args.provider) {
|
|
19032
|
+
models = models.filter((m) => m.provider.toLowerCase().includes(args.provider.toLowerCase()));
|
|
19033
|
+
}
|
|
19034
|
+
if (args.used) {
|
|
19035
|
+
models = models.filter((m) => usedBy.has(`${m.provider}/${m.model}`));
|
|
19036
|
+
}
|
|
19037
|
+
if (models.length === 0) {
|
|
19038
|
+
console.log("No models match.");
|
|
19039
|
+
return;
|
|
19040
|
+
}
|
|
19041
|
+
const byProvider = new Map;
|
|
19042
|
+
for (const m of models) {
|
|
19043
|
+
if (!byProvider.has(m.provider))
|
|
19044
|
+
byProvider.set(m.provider, []);
|
|
19045
|
+
byProvider.get(m.provider).push(m);
|
|
19046
|
+
}
|
|
19047
|
+
const total = models.length;
|
|
19048
|
+
console.log(`
|
|
19049
|
+
${bold2(`Models on pi`)} ${dim2(`(${total} total)`)}
|
|
19050
|
+
`);
|
|
19051
|
+
for (const [provider, pModels] of byProvider) {
|
|
19052
|
+
console.log(` ${cyan2(provider)} ${dim2(`${pModels.length} model${pModels.length !== 1 ? "s" : ""}`)}`);
|
|
19053
|
+
const modelWidth = Math.max(...pModels.map((m) => m.model.length));
|
|
19054
|
+
for (const m of pModels) {
|
|
19055
|
+
const key = `${m.provider}/${m.model}`;
|
|
19056
|
+
const inUse = usedBy.get(key);
|
|
19057
|
+
const flags = [
|
|
19058
|
+
m.thinking ? green2("thinking") : dim2("·"),
|
|
19059
|
+
m.images ? dim2("images") : ""
|
|
19060
|
+
].filter(Boolean).join(" ");
|
|
19061
|
+
const ctx = dim2(`ctx ${m.context}`);
|
|
19062
|
+
const usedLabel = inUse ? ` ${yellow2("←")} ${dim2(inUse.join(", "))}` : "";
|
|
19063
|
+
console.log(` ${m.model.padEnd(modelWidth)} ${ctx.padEnd(18)} ${flags}${usedLabel}`);
|
|
19064
|
+
}
|
|
19065
|
+
console.log();
|
|
19066
|
+
}
|
|
19067
|
+
if (!args.used) {
|
|
19068
|
+
console.log(dim2(` --provider <name> filter by provider`));
|
|
19069
|
+
console.log(dim2(` --used only show models used by your specialists`));
|
|
19070
|
+
console.log();
|
|
19071
|
+
}
|
|
19072
|
+
}
|
|
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`;
|
|
19074
|
+
var init_models = __esm(() => {
|
|
19075
|
+
init_loader();
|
|
19076
|
+
});
|
|
19077
|
+
|
|
19078
|
+
// src/cli/init.ts
|
|
19079
|
+
var exports_init = {};
|
|
19080
|
+
__export(exports_init, {
|
|
19081
|
+
run: () => run5
|
|
19082
|
+
});
|
|
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";
|
|
19086
|
+
function ok(msg) {
|
|
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
|
+
}
|
|
19259
|
+
}
|
|
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
|
+
}
|
|
19275
|
+
}
|
|
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(`
|
|
19294
|
+
`);
|
|
19295
|
+
for (const entry of GITIGNORE_ENTRIES) {
|
|
19296
|
+
if (!lines.includes(entry)) {
|
|
19297
|
+
lines.push(entry);
|
|
19298
|
+
added++;
|
|
19299
|
+
}
|
|
19300
|
+
}
|
|
19301
|
+
if (added > 0) {
|
|
19302
|
+
writeFileSync4(gitignorePath, lines.join(`
|
|
19303
|
+
`) + `
|
|
19304
|
+
`, "utf-8");
|
|
19305
|
+
ok("added .specialists/jobs/ and .specialists/ready/ to .gitignore");
|
|
19306
|
+
} else {
|
|
19307
|
+
skip(".gitignore already has runtime entries");
|
|
19308
|
+
}
|
|
19309
|
+
}
|
|
19310
|
+
function ensureAgentsMd(cwd) {
|
|
19311
|
+
const agentsPath = join9(cwd, "AGENTS.md");
|
|
19312
|
+
if (existsSync6(agentsPath)) {
|
|
19313
|
+
const existing = readFileSync3(agentsPath, "utf-8");
|
|
19314
|
+
if (existing.includes(AGENTS_MARKER)) {
|
|
19315
|
+
skip("AGENTS.md already has Specialists section");
|
|
19316
|
+
} else {
|
|
19317
|
+
writeFileSync4(agentsPath, existing.trimEnd() + `
|
|
19318
|
+
|
|
19319
|
+
` + AGENTS_BLOCK, "utf-8");
|
|
19320
|
+
ok("appended Specialists section to AGENTS.md");
|
|
19321
|
+
}
|
|
19322
|
+
} else {
|
|
19323
|
+
writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
|
|
19324
|
+
ok("created AGENTS.md with Specialists section");
|
|
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);
|
|
19341
|
+
console.log(`
|
|
19342
|
+
${bold3("Done!")}
|
|
19343
|
+
`);
|
|
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
|
|
19361
|
+
`);
|
|
19362
|
+
}
|
|
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;
|
|
19364
|
+
var init_init = __esm(() => {
|
|
19365
|
+
AGENTS_BLOCK = `
|
|
19366
|
+
## Specialists
|
|
19367
|
+
|
|
19368
|
+
Call \`specialist_init\` at the start of every session to bootstrap context and
|
|
19369
|
+
see available specialists. Use \`use_specialist\` or \`start_specialist\` to
|
|
19370
|
+
delegate heavy tasks (code review, bug hunting, deep reasoning) to the right
|
|
19371
|
+
specialist without user intervention.
|
|
19372
|
+
|
|
19373
|
+
Add custom specialists to \`.specialists/user/specialists/\` to extend the defaults.
|
|
19374
|
+
`.trimStart();
|
|
19375
|
+
GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
|
|
19376
|
+
MCP_SERVER_CONFIG = { command: "specialists", args: [] };
|
|
19377
|
+
});
|
|
19378
|
+
|
|
19379
|
+
// src/cli/edit.ts
|
|
19380
|
+
var exports_edit = {};
|
|
19381
|
+
__export(exports_edit, {
|
|
19382
|
+
run: () => run6
|
|
19383
|
+
});
|
|
19384
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
19385
|
+
function parseArgs3(argv) {
|
|
19386
|
+
const name = argv[0];
|
|
19387
|
+
if (!name || name.startsWith("--")) {
|
|
19388
|
+
console.error("Usage: specialists|sp edit <name> --<field> <value> [--dry-run]");
|
|
19389
|
+
console.error(` Fields: ${Object.keys(FIELD_MAP).join(", ")}`);
|
|
19390
|
+
process.exit(1);
|
|
19391
|
+
}
|
|
19392
|
+
let field;
|
|
19393
|
+
let value;
|
|
19394
|
+
let dryRun = false;
|
|
19395
|
+
let scope;
|
|
19396
|
+
for (let i = 1;i < argv.length; i++) {
|
|
19397
|
+
const token = argv[i];
|
|
19398
|
+
if (token === "--dry-run") {
|
|
19399
|
+
dryRun = true;
|
|
19400
|
+
continue;
|
|
19401
|
+
}
|
|
19402
|
+
if (token === "--scope") {
|
|
19403
|
+
const v = argv[++i];
|
|
19404
|
+
if (v !== "project" && v !== "user") {
|
|
19405
|
+
console.error(`Error: --scope must be "project" or "user", got: "${v ?? ""}"`);
|
|
19406
|
+
process.exit(1);
|
|
19407
|
+
}
|
|
19408
|
+
scope = v;
|
|
19409
|
+
continue;
|
|
19410
|
+
}
|
|
19411
|
+
if (token.startsWith("--") && !field) {
|
|
19412
|
+
field = token.slice(2);
|
|
19413
|
+
value = argv[++i];
|
|
19414
|
+
continue;
|
|
19415
|
+
}
|
|
19416
|
+
}
|
|
19417
|
+
if (!field || !FIELD_MAP[field]) {
|
|
19418
|
+
console.error(`Error: unknown or missing field. Valid fields: ${Object.keys(FIELD_MAP).join(", ")}`);
|
|
19419
|
+
process.exit(1);
|
|
19420
|
+
}
|
|
19421
|
+
if (value === undefined || value === "") {
|
|
19422
|
+
console.error(`Error: --${field} requires a value`);
|
|
19423
|
+
process.exit(1);
|
|
19424
|
+
}
|
|
19425
|
+
if (field === "permission" && !VALID_PERMISSIONS.includes(value)) {
|
|
19426
|
+
console.error(`Error: --permission must be one of: ${VALID_PERMISSIONS.join(", ")}`);
|
|
19427
|
+
process.exit(1);
|
|
19428
|
+
}
|
|
19429
|
+
if (field === "timeout" && !/^\d+$/.test(value)) {
|
|
19430
|
+
console.error("Error: --timeout must be a number (milliseconds)");
|
|
19431
|
+
process.exit(1);
|
|
19432
|
+
}
|
|
19433
|
+
return { name, field, value, dryRun, scope };
|
|
19434
|
+
}
|
|
19435
|
+
function setIn(doc2, path, value) {
|
|
19436
|
+
let node = doc2;
|
|
19437
|
+
for (let i = 0;i < path.length - 1; i++) {
|
|
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);
|
|
19445
|
+
}
|
|
19446
|
+
}
|
|
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"];
|
|
19158
19507
|
});
|
|
19159
19508
|
|
|
19160
19509
|
// src/cli/run.ts
|
|
@@ -19162,11 +19511,12 @@ var exports_run = {};
|
|
|
19162
19511
|
__export(exports_run, {
|
|
19163
19512
|
run: () => run7
|
|
19164
19513
|
});
|
|
19165
|
-
import {
|
|
19514
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
19515
|
+
import { join as join10 } from "node:path";
|
|
19166
19516
|
async function parseArgs4(argv) {
|
|
19167
19517
|
const name = argv[0];
|
|
19168
19518
|
if (!name || name.startsWith("--")) {
|
|
19169
|
-
console.error('Usage: specialists run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--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]');
|
|
19170
19520
|
process.exit(1);
|
|
19171
19521
|
}
|
|
19172
19522
|
let prompt = "";
|
|
@@ -19174,6 +19524,8 @@ async function parseArgs4(argv) {
|
|
|
19174
19524
|
let model;
|
|
19175
19525
|
let noBeads = false;
|
|
19176
19526
|
let background = false;
|
|
19527
|
+
let follow = false;
|
|
19528
|
+
let keepAlive = false;
|
|
19177
19529
|
let contextDepth = 1;
|
|
19178
19530
|
for (let i = 1;i < argv.length; i++) {
|
|
19179
19531
|
const token = argv[i];
|
|
@@ -19201,32 +19553,40 @@ async function parseArgs4(argv) {
|
|
|
19201
19553
|
background = true;
|
|
19202
19554
|
continue;
|
|
19203
19555
|
}
|
|
19556
|
+
if (token === "--follow") {
|
|
19557
|
+
follow = true;
|
|
19558
|
+
continue;
|
|
19559
|
+
}
|
|
19560
|
+
if (token === "--keep-alive") {
|
|
19561
|
+
keepAlive = true;
|
|
19562
|
+
continue;
|
|
19563
|
+
}
|
|
19204
19564
|
}
|
|
19205
19565
|
if (prompt && beadId) {
|
|
19206
19566
|
console.error("Error: use either --prompt or --bead, not both.");
|
|
19207
19567
|
process.exit(1);
|
|
19208
19568
|
}
|
|
19209
19569
|
if (!prompt && !beadId && !process.stdin.isTTY) {
|
|
19210
|
-
prompt = await new Promise((
|
|
19570
|
+
prompt = await new Promise((resolve2) => {
|
|
19211
19571
|
let buf = "";
|
|
19212
19572
|
process.stdin.setEncoding("utf-8");
|
|
19213
19573
|
process.stdin.on("data", (chunk) => {
|
|
19214
19574
|
buf += chunk;
|
|
19215
19575
|
});
|
|
19216
|
-
process.stdin.on("end", () =>
|
|
19576
|
+
process.stdin.on("end", () => resolve2(buf.trim()));
|
|
19217
19577
|
});
|
|
19218
19578
|
}
|
|
19219
19579
|
if (!prompt && !beadId) {
|
|
19220
19580
|
console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
|
|
19221
19581
|
process.exit(1);
|
|
19222
19582
|
}
|
|
19223
|
-
return { name, prompt, beadId, model, noBeads, background, contextDepth };
|
|
19583
|
+
return { name, prompt, beadId, model, noBeads, background, follow, keepAlive, contextDepth };
|
|
19224
19584
|
}
|
|
19225
19585
|
async function run7() {
|
|
19226
19586
|
const args = await parseArgs4(process.argv.slice(3));
|
|
19227
19587
|
const loader = new SpecialistLoader;
|
|
19228
19588
|
const circuitBreaker = new CircuitBreaker;
|
|
19229
|
-
const hooks = new HookEmitter({ tracePath:
|
|
19589
|
+
const hooks = new HookEmitter({ tracePath: join10(process.cwd(), ".specialists", "trace.jsonl") });
|
|
19230
19590
|
const beadsClient = args.noBeads ? undefined : new BeadsClient;
|
|
19231
19591
|
const beadReader = beadsClient ?? new BeadsClient;
|
|
19232
19592
|
let prompt = args.prompt;
|
|
@@ -19255,8 +19615,8 @@ async function run7() {
|
|
|
19255
19615
|
circuitBreaker,
|
|
19256
19616
|
beadsClient
|
|
19257
19617
|
});
|
|
19258
|
-
if (args.background) {
|
|
19259
|
-
const jobsDir =
|
|
19618
|
+
if (args.background || args.follow) {
|
|
19619
|
+
const jobsDir = join10(process.cwd(), ".specialists", "jobs");
|
|
19260
19620
|
const supervisor = new Supervisor({
|
|
19261
19621
|
runner,
|
|
19262
19622
|
runOptions: {
|
|
@@ -19264,20 +19624,46 @@ async function run7() {
|
|
|
19264
19624
|
prompt,
|
|
19265
19625
|
variables,
|
|
19266
19626
|
backendOverride: args.model,
|
|
19267
|
-
inputBeadId: args.beadId
|
|
19627
|
+
inputBeadId: args.beadId,
|
|
19628
|
+
keepAlive: args.keepAlive
|
|
19268
19629
|
},
|
|
19269
19630
|
jobsDir,
|
|
19270
19631
|
beadsClient
|
|
19271
19632
|
});
|
|
19633
|
+
let jobId;
|
|
19272
19634
|
try {
|
|
19273
|
-
|
|
19274
|
-
|
|
19635
|
+
jobId = await supervisor.run();
|
|
19636
|
+
if (!args.follow) {
|
|
19637
|
+
process.stdout.write(`Job started: ${jobId}
|
|
19275
19638
|
`);
|
|
19639
|
+
}
|
|
19276
19640
|
} catch (err) {
|
|
19277
19641
|
process.stderr.write(`Error: ${err?.message ?? err}
|
|
19278
19642
|
`);
|
|
19279
19643
|
process.exit(1);
|
|
19280
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
|
+
}
|
|
19281
19667
|
return;
|
|
19282
19668
|
}
|
|
19283
19669
|
process.stderr.write(`
|
|
@@ -19321,11 +19707,11 @@ Interrupted.
|
|
|
19321
19707
|
dim5(result.model)
|
|
19322
19708
|
].filter(Boolean).join(" ");
|
|
19323
19709
|
process.stderr.write(`
|
|
19324
|
-
${
|
|
19710
|
+
${green5("✓")} ${footer}
|
|
19325
19711
|
|
|
19326
19712
|
`);
|
|
19327
19713
|
}
|
|
19328
|
-
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`;
|
|
19329
19715
|
var init_run = __esm(() => {
|
|
19330
19716
|
init_loader();
|
|
19331
19717
|
init_runner();
|
|
@@ -19407,9 +19793,9 @@ function formatEventLine(event, options) {
|
|
|
19407
19793
|
const detail = detailParts.length > 0 ? dim6(detailParts.join(" ")) : "";
|
|
19408
19794
|
return `${ts} ${prefix} ${label}${detail ? ` ${detail}` : ""}`.trimEnd();
|
|
19409
19795
|
}
|
|
19410
|
-
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`,
|
|
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;
|
|
19411
19797
|
var init_format_helpers = __esm(() => {
|
|
19412
|
-
JOB_COLORS = [cyan4, yellow5, magenta,
|
|
19798
|
+
JOB_COLORS = [cyan4, yellow5, magenta, green6, blue, red];
|
|
19413
19799
|
EVENT_LABELS = {
|
|
19414
19800
|
run_start: "START",
|
|
19415
19801
|
meta: "META",
|
|
@@ -19428,17 +19814,17 @@ var exports_status = {};
|
|
|
19428
19814
|
__export(exports_status, {
|
|
19429
19815
|
run: () => run8
|
|
19430
19816
|
});
|
|
19431
|
-
import { spawnSync as
|
|
19432
|
-
import { existsSync as
|
|
19433
|
-
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";
|
|
19434
19820
|
function ok2(msg) {
|
|
19435
|
-
console.log(` ${
|
|
19821
|
+
console.log(` ${green6("✓")} ${msg}`);
|
|
19436
19822
|
}
|
|
19437
19823
|
function warn(msg) {
|
|
19438
19824
|
console.log(` ${yellow5("○")} ${msg}`);
|
|
19439
19825
|
}
|
|
19440
19826
|
function fail(msg) {
|
|
19441
|
-
console.log(` ${
|
|
19827
|
+
console.log(` ${red("✗")} ${msg}`);
|
|
19442
19828
|
}
|
|
19443
19829
|
function info(msg) {
|
|
19444
19830
|
console.log(` ${dim6(msg)}`);
|
|
@@ -19449,7 +19835,7 @@ function section(label) {
|
|
|
19449
19835
|
${bold6(`── ${label} ${line}`)}`);
|
|
19450
19836
|
}
|
|
19451
19837
|
function cmd(bin, args) {
|
|
19452
|
-
const r =
|
|
19838
|
+
const r = spawnSync6(bin, args, {
|
|
19453
19839
|
encoding: "utf8",
|
|
19454
19840
|
stdio: "pipe",
|
|
19455
19841
|
timeout: 5000
|
|
@@ -19457,7 +19843,7 @@ function cmd(bin, args) {
|
|
|
19457
19843
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
19458
19844
|
}
|
|
19459
19845
|
function isInstalled(bin) {
|
|
19460
|
-
return
|
|
19846
|
+
return spawnSync6("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
19461
19847
|
}
|
|
19462
19848
|
function formatElapsed2(s) {
|
|
19463
19849
|
if (s.elapsed_s === undefined)
|
|
@@ -19469,11 +19855,11 @@ function formatElapsed2(s) {
|
|
|
19469
19855
|
function statusColor(status) {
|
|
19470
19856
|
switch (status) {
|
|
19471
19857
|
case "running":
|
|
19472
|
-
return
|
|
19858
|
+
return cyan4(status);
|
|
19473
19859
|
case "done":
|
|
19474
|
-
return
|
|
19860
|
+
return green6(status);
|
|
19475
19861
|
case "error":
|
|
19476
|
-
return
|
|
19862
|
+
return red(status);
|
|
19477
19863
|
case "starting":
|
|
19478
19864
|
return yellow5(status);
|
|
19479
19865
|
default:
|
|
@@ -19492,11 +19878,11 @@ async function run8() {
|
|
|
19492
19878
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
19493
19879
|
const bdInstalled = isInstalled("bd");
|
|
19494
19880
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
19495
|
-
const beadsPresent =
|
|
19881
|
+
const beadsPresent = existsSync7(join11(process.cwd(), ".beads"));
|
|
19496
19882
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
19497
|
-
const jobsDir =
|
|
19883
|
+
const jobsDir = join11(process.cwd(), ".specialists", "jobs");
|
|
19498
19884
|
let jobs = [];
|
|
19499
|
-
if (
|
|
19885
|
+
if (existsSync7(jobsDir)) {
|
|
19500
19886
|
const supervisor = new Supervisor({
|
|
19501
19887
|
runner: null,
|
|
19502
19888
|
runOptions: null,
|
|
@@ -19562,7 +19948,7 @@ ${bold6("specialists status")}
|
|
|
19562
19948
|
for (const s of allSpecialists) {
|
|
19563
19949
|
const staleness = stalenessMap[s.name];
|
|
19564
19950
|
if (staleness === "AGED") {
|
|
19565
|
-
warn(`${s.name} ${
|
|
19951
|
+
warn(`${s.name} ${red("AGED")} ${dim6(s.scope)}`);
|
|
19566
19952
|
} else if (staleness === "STALE") {
|
|
19567
19953
|
warn(`${s.name} ${yellow5("STALE")} ${dim6(s.scope)}`);
|
|
19568
19954
|
}
|
|
@@ -19599,13 +19985,12 @@ ${bold6("specialists status")}
|
|
|
19599
19985
|
section("Active Jobs");
|
|
19600
19986
|
for (const job of jobs) {
|
|
19601
19987
|
const elapsed = formatElapsed2(job);
|
|
19602
|
-
const detail = job.status === "error" ?
|
|
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 ?? "");
|
|
19603
19989
|
console.log(` ${dim6(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
|
|
19604
19990
|
}
|
|
19605
19991
|
}
|
|
19606
19992
|
console.log();
|
|
19607
19993
|
}
|
|
19608
|
-
var red2 = (s) => `\x1B[31m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
19609
19994
|
var init_status = __esm(() => {
|
|
19610
19995
|
init_loader();
|
|
19611
19996
|
init_supervisor();
|
|
@@ -19617,15 +20002,15 @@ var exports_result = {};
|
|
|
19617
20002
|
__export(exports_result, {
|
|
19618
20003
|
run: () => run9
|
|
19619
20004
|
});
|
|
19620
|
-
import { existsSync as
|
|
19621
|
-
import { join as
|
|
20005
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
20006
|
+
import { join as join12 } from "node:path";
|
|
19622
20007
|
async function run9() {
|
|
19623
20008
|
const jobId = process.argv[3];
|
|
19624
20009
|
if (!jobId) {
|
|
19625
|
-
console.error("Usage: specialists result <job-id>");
|
|
20010
|
+
console.error("Usage: specialists|sp result <job-id>");
|
|
19626
20011
|
process.exit(1);
|
|
19627
20012
|
}
|
|
19628
|
-
const jobsDir =
|
|
20013
|
+
const jobsDir = join12(process.cwd(), ".specialists", "jobs");
|
|
19629
20014
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19630
20015
|
const status = supervisor.readStatus(jobId);
|
|
19631
20016
|
if (!status) {
|
|
@@ -19638,30 +20023,30 @@ async function run9() {
|
|
|
19638
20023
|
process.exit(1);
|
|
19639
20024
|
}
|
|
19640
20025
|
if (status.status === "error") {
|
|
19641
|
-
process.stderr.write(`${
|
|
20026
|
+
process.stderr.write(`${red2(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
|
|
19642
20027
|
`);
|
|
19643
20028
|
process.exit(1);
|
|
19644
20029
|
}
|
|
19645
|
-
const resultPath =
|
|
19646
|
-
if (!
|
|
20030
|
+
const resultPath = join12(jobsDir, jobId, "result.txt");
|
|
20031
|
+
if (!existsSync8(resultPath)) {
|
|
19647
20032
|
console.error(`Result file not found for job ${jobId}`);
|
|
19648
20033
|
process.exit(1);
|
|
19649
20034
|
}
|
|
19650
|
-
process.stdout.write(
|
|
20035
|
+
process.stdout.write(readFileSync5(resultPath, "utf-8"));
|
|
19651
20036
|
}
|
|
19652
|
-
var dim7 = (s) => `\x1B[2m${s}\x1B[0m`,
|
|
20037
|
+
var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
19653
20038
|
var init_result = __esm(() => {
|
|
19654
20039
|
init_supervisor();
|
|
19655
20040
|
});
|
|
19656
20041
|
|
|
19657
20042
|
// src/specialist/timeline-query.ts
|
|
19658
|
-
import { existsSync as
|
|
19659
|
-
import { join as
|
|
20043
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
20044
|
+
import { join as join13 } from "node:path";
|
|
19660
20045
|
function readJobEvents(jobDir) {
|
|
19661
|
-
const eventsPath =
|
|
19662
|
-
if (!
|
|
20046
|
+
const eventsPath = join13(jobDir, "events.jsonl");
|
|
20047
|
+
if (!existsSync9(eventsPath))
|
|
19663
20048
|
return [];
|
|
19664
|
-
const content =
|
|
20049
|
+
const content = readFileSync6(eventsPath, "utf-8");
|
|
19665
20050
|
const lines = content.split(`
|
|
19666
20051
|
`).filter(Boolean);
|
|
19667
20052
|
const events = [];
|
|
@@ -19674,12 +20059,12 @@ function readJobEvents(jobDir) {
|
|
|
19674
20059
|
return events;
|
|
19675
20060
|
}
|
|
19676
20061
|
function readAllJobEvents(jobsDir) {
|
|
19677
|
-
if (!
|
|
20062
|
+
if (!existsSync9(jobsDir))
|
|
19678
20063
|
return [];
|
|
19679
20064
|
const batches = [];
|
|
19680
|
-
const entries =
|
|
20065
|
+
const entries = readdirSync3(jobsDir);
|
|
19681
20066
|
for (const entry of entries) {
|
|
19682
|
-
const jobDir =
|
|
20067
|
+
const jobDir = join13(jobsDir, entry);
|
|
19683
20068
|
try {
|
|
19684
20069
|
const stat2 = __require("node:fs").statSync(jobDir);
|
|
19685
20070
|
if (!stat2.isDirectory())
|
|
@@ -19688,12 +20073,12 @@ function readAllJobEvents(jobsDir) {
|
|
|
19688
20073
|
continue;
|
|
19689
20074
|
}
|
|
19690
20075
|
const jobId = entry;
|
|
19691
|
-
const statusPath =
|
|
20076
|
+
const statusPath = join13(jobDir, "status.json");
|
|
19692
20077
|
let specialist = "unknown";
|
|
19693
20078
|
let beadId;
|
|
19694
|
-
if (
|
|
20079
|
+
if (existsSync9(statusPath)) {
|
|
19695
20080
|
try {
|
|
19696
|
-
const status = JSON.parse(
|
|
20081
|
+
const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
|
|
19697
20082
|
specialist = status.specialist ?? "unknown";
|
|
19698
20083
|
beadId = status.bead_id;
|
|
19699
20084
|
} catch {}
|
|
@@ -19756,8 +20141,8 @@ var exports_feed = {};
|
|
|
19756
20141
|
__export(exports_feed, {
|
|
19757
20142
|
run: () => run10
|
|
19758
20143
|
});
|
|
19759
|
-
import { existsSync as
|
|
19760
|
-
import { join as
|
|
20144
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
20145
|
+
import { join as join14 } from "node:path";
|
|
19761
20146
|
function getHumanEventKey(event) {
|
|
19762
20147
|
switch (event.type) {
|
|
19763
20148
|
case "meta":
|
|
@@ -19907,7 +20292,7 @@ async function followMerged(jobsDir, options) {
|
|
|
19907
20292
|
}
|
|
19908
20293
|
const lastPrintedEventKey = new Map;
|
|
19909
20294
|
const seenMetaKey = new Map;
|
|
19910
|
-
await new Promise((
|
|
20295
|
+
await new Promise((resolve2) => {
|
|
19911
20296
|
const interval = setInterval(() => {
|
|
19912
20297
|
const batches = filteredBatches();
|
|
19913
20298
|
const newEvents = [];
|
|
@@ -19944,15 +20329,15 @@ async function followMerged(jobsDir, options) {
|
|
|
19944
20329
|
}
|
|
19945
20330
|
if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
|
|
19946
20331
|
clearInterval(interval);
|
|
19947
|
-
|
|
20332
|
+
resolve2();
|
|
19948
20333
|
}
|
|
19949
20334
|
}, 500);
|
|
19950
20335
|
});
|
|
19951
20336
|
}
|
|
19952
20337
|
async function run10() {
|
|
19953
20338
|
const options = parseArgs5(process.argv.slice(3));
|
|
19954
|
-
const jobsDir =
|
|
19955
|
-
if (!
|
|
20339
|
+
const jobsDir = join14(process.cwd(), ".specialists", "jobs");
|
|
20340
|
+
if (!existsSync10(jobsDir)) {
|
|
19956
20341
|
console.log(dim6("No jobs directory found."));
|
|
19957
20342
|
return;
|
|
19958
20343
|
}
|
|
@@ -19974,19 +20359,121 @@ var init_feed = __esm(() => {
|
|
|
19974
20359
|
init_format_helpers();
|
|
19975
20360
|
});
|
|
19976
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}
|
|
20450
|
+
`);
|
|
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
|
+
}
|
|
20458
|
+
}
|
|
20459
|
+
var green8 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
20460
|
+
var init_follow_up = __esm(() => {
|
|
20461
|
+
init_supervisor();
|
|
20462
|
+
});
|
|
20463
|
+
|
|
19977
20464
|
// src/cli/stop.ts
|
|
19978
20465
|
var exports_stop = {};
|
|
19979
20466
|
__export(exports_stop, {
|
|
19980
|
-
run: () =>
|
|
20467
|
+
run: () => run13
|
|
19981
20468
|
});
|
|
19982
|
-
import { join as
|
|
19983
|
-
async function
|
|
20469
|
+
import { join as join17 } from "node:path";
|
|
20470
|
+
async function run13() {
|
|
19984
20471
|
const jobId = process.argv[3];
|
|
19985
20472
|
if (!jobId) {
|
|
19986
|
-
console.error("Usage: specialists stop <job-id>");
|
|
20473
|
+
console.error("Usage: specialists|sp stop <job-id>");
|
|
19987
20474
|
process.exit(1);
|
|
19988
20475
|
}
|
|
19989
|
-
const jobsDir =
|
|
20476
|
+
const jobsDir = join17(process.cwd(), ".specialists", "jobs");
|
|
19990
20477
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
19991
20478
|
const status = supervisor.readStatus(jobId);
|
|
19992
20479
|
if (!status) {
|
|
@@ -20005,7 +20492,7 @@ async function run11() {
|
|
|
20005
20492
|
}
|
|
20006
20493
|
try {
|
|
20007
20494
|
process.kill(status.pid, "SIGTERM");
|
|
20008
|
-
process.stdout.write(`${
|
|
20495
|
+
process.stdout.write(`${green9("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
20009
20496
|
`);
|
|
20010
20497
|
} catch (err) {
|
|
20011
20498
|
if (err.code === "ESRCH") {
|
|
@@ -20018,7 +20505,7 @@ async function run11() {
|
|
|
20018
20505
|
}
|
|
20019
20506
|
}
|
|
20020
20507
|
}
|
|
20021
|
-
var
|
|
20508
|
+
var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
20022
20509
|
var init_stop = __esm(() => {
|
|
20023
20510
|
init_supervisor();
|
|
20024
20511
|
});
|
|
@@ -20026,25 +20513,26 @@ var init_stop = __esm(() => {
|
|
|
20026
20513
|
// src/cli/quickstart.ts
|
|
20027
20514
|
var exports_quickstart = {};
|
|
20028
20515
|
__export(exports_quickstart, {
|
|
20029
|
-
run: () =>
|
|
20516
|
+
run: () => run14
|
|
20030
20517
|
});
|
|
20031
20518
|
function section2(title) {
|
|
20032
20519
|
const bar = "─".repeat(60);
|
|
20033
20520
|
return `
|
|
20034
|
-
${bold7(
|
|
20521
|
+
${bold7(cyan5(title))}
|
|
20035
20522
|
${dim9(bar)}`;
|
|
20036
20523
|
}
|
|
20037
20524
|
function cmd2(s) {
|
|
20038
20525
|
return yellow6(s);
|
|
20039
20526
|
}
|
|
20040
20527
|
function flag(s) {
|
|
20041
|
-
return
|
|
20528
|
+
return green10(s);
|
|
20042
20529
|
}
|
|
20043
|
-
async function
|
|
20530
|
+
async function run14() {
|
|
20044
20531
|
const lines = [
|
|
20045
20532
|
"",
|
|
20046
20533
|
bold7("specialists · Quick Start Guide"),
|
|
20047
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."),
|
|
20048
20536
|
""
|
|
20049
20537
|
];
|
|
20050
20538
|
lines.push(section2("1. Installation"));
|
|
@@ -20106,6 +20594,17 @@ async function run12() {
|
|
|
20106
20594
|
lines.push(` ${bold7("Read results")} — print the final output:`);
|
|
20107
20595
|
lines.push(` ${cmd2("specialists result job_a1b2c3d4")} # exits 1 if still running`);
|
|
20108
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("");
|
|
20109
20608
|
lines.push(` ${bold7("Cancel a job")}:`);
|
|
20110
20609
|
lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
|
|
20111
20610
|
lines.push("");
|
|
@@ -20113,6 +20612,7 @@ async function run12() {
|
|
|
20113
20612
|
lines.push(` ${dim9("status.json")} — id, specialist, status, pid, started_at, elapsed_s, current_tool`);
|
|
20114
20613
|
lines.push(` ${dim9("events.jsonl")} — one JSON event per line (tool_use, text, agent_end, error …)`);
|
|
20115
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)`);
|
|
20116
20616
|
lines.push("");
|
|
20117
20617
|
lines.push(section2("6. Editing Specialists"));
|
|
20118
20618
|
lines.push("");
|
|
@@ -20200,7 +20700,9 @@ async function run12() {
|
|
|
20200
20700
|
lines.push(` ${bold7("run_parallel")} — concurrent or pipeline execution`);
|
|
20201
20701
|
lines.push(` ${bold7("start_specialist")} — async job start, returns job ID`);
|
|
20202
20702
|
lines.push(` ${bold7("poll_specialist")} — poll job status/output by ID`);
|
|
20203
|
-
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`);
|
|
20204
20706
|
lines.push(` ${bold7("specialist_status")} — circuit breaker health + staleness`);
|
|
20205
20707
|
lines.push("");
|
|
20206
20708
|
lines.push(section2("10. Common Workflows"));
|
|
@@ -20213,6 +20715,17 @@ async function run12() {
|
|
|
20213
20715
|
lines.push(` ${cmd2("specialists feed <job-id> --follow")}`);
|
|
20214
20716
|
lines.push(` ${cmd2("specialists result <job-id> > analysis.md")}`);
|
|
20215
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("");
|
|
20216
20729
|
lines.push(` ${bold7("Override model for a single run:")}`);
|
|
20217
20730
|
lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
|
|
20218
20731
|
lines.push("");
|
|
@@ -20223,18 +20736,18 @@ async function run12() {
|
|
|
20223
20736
|
console.log(lines.join(`
|
|
20224
20737
|
`));
|
|
20225
20738
|
}
|
|
20226
|
-
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${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`;
|
|
20227
20740
|
|
|
20228
20741
|
// src/cli/doctor.ts
|
|
20229
20742
|
var exports_doctor = {};
|
|
20230
20743
|
__export(exports_doctor, {
|
|
20231
|
-
run: () =>
|
|
20744
|
+
run: () => run15
|
|
20232
20745
|
});
|
|
20233
|
-
import { spawnSync as
|
|
20234
|
-
import { existsSync as
|
|
20235
|
-
import { join as
|
|
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";
|
|
20236
20749
|
function ok3(msg) {
|
|
20237
|
-
console.log(` ${
|
|
20750
|
+
console.log(` ${green11("✓")} ${msg}`);
|
|
20238
20751
|
}
|
|
20239
20752
|
function warn2(msg) {
|
|
20240
20753
|
console.log(` ${yellow7("○")} ${msg}`);
|
|
@@ -20254,17 +20767,17 @@ function section3(label) {
|
|
|
20254
20767
|
${bold8(`── ${label} ${line}`)}`);
|
|
20255
20768
|
}
|
|
20256
20769
|
function sp(bin, args) {
|
|
20257
|
-
const r =
|
|
20770
|
+
const r = spawnSync7(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
|
|
20258
20771
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
20259
20772
|
}
|
|
20260
20773
|
function isInstalled2(bin) {
|
|
20261
|
-
return
|
|
20774
|
+
return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
20262
20775
|
}
|
|
20263
20776
|
function loadJson2(path) {
|
|
20264
|
-
if (!
|
|
20777
|
+
if (!existsSync11(path))
|
|
20265
20778
|
return null;
|
|
20266
20779
|
try {
|
|
20267
|
-
return JSON.parse(
|
|
20780
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
20268
20781
|
} catch {
|
|
20269
20782
|
return null;
|
|
20270
20783
|
}
|
|
@@ -20297,7 +20810,7 @@ function checkBd() {
|
|
|
20297
20810
|
return false;
|
|
20298
20811
|
}
|
|
20299
20812
|
ok3(`bd installed ${dim10(sp("bd", ["--version"]).stdout || "")}`);
|
|
20300
|
-
if (
|
|
20813
|
+
if (existsSync11(join18(CWD, ".beads")))
|
|
20301
20814
|
ok3(".beads/ present in project");
|
|
20302
20815
|
else
|
|
20303
20816
|
warn2(".beads/ not found in project");
|
|
@@ -20317,8 +20830,8 @@ function checkHooks() {
|
|
|
20317
20830
|
section3("Claude Code hooks (2 expected)");
|
|
20318
20831
|
let allPresent = true;
|
|
20319
20832
|
for (const name of HOOK_NAMES) {
|
|
20320
|
-
const dest =
|
|
20321
|
-
if (!
|
|
20833
|
+
const dest = join18(HOOKS_DIR, name);
|
|
20834
|
+
if (!existsSync11(dest)) {
|
|
20322
20835
|
fail2(`${name} ${red6("missing")}`);
|
|
20323
20836
|
fix("specialists install");
|
|
20324
20837
|
allPresent = false;
|
|
@@ -20332,14 +20845,13 @@ function checkHooks() {
|
|
|
20332
20845
|
fix("specialists install");
|
|
20333
20846
|
return false;
|
|
20334
20847
|
}
|
|
20335
|
-
const hooks = settings.hooks ?? {};
|
|
20336
20848
|
const wiredCommands = new Set([
|
|
20337
|
-
...
|
|
20338
|
-
...
|
|
20849
|
+
...settings.UserPromptSubmit ?? [],
|
|
20850
|
+
...settings.SessionStart ?? []
|
|
20339
20851
|
].flatMap((entry) => (entry.hooks ?? []).map((h) => h.command ?? "")));
|
|
20340
20852
|
for (const name of HOOK_NAMES) {
|
|
20341
|
-
const
|
|
20342
|
-
if (!wiredCommands.has(
|
|
20853
|
+
const expectedRelative = `node .specialists/default/hooks/${name}`;
|
|
20854
|
+
if (!wiredCommands.has(expectedRelative)) {
|
|
20343
20855
|
warn2(`${name} not wired in settings.json`);
|
|
20344
20856
|
fix("specialists install");
|
|
20345
20857
|
allPresent = false;
|
|
@@ -20363,18 +20875,18 @@ function checkMCP() {
|
|
|
20363
20875
|
}
|
|
20364
20876
|
function checkRuntimeDirs() {
|
|
20365
20877
|
section3(".specialists/ runtime directories");
|
|
20366
|
-
const rootDir =
|
|
20367
|
-
const jobsDir =
|
|
20368
|
-
const readyDir =
|
|
20878
|
+
const rootDir = join18(CWD, ".specialists");
|
|
20879
|
+
const jobsDir = join18(rootDir, "jobs");
|
|
20880
|
+
const readyDir = join18(rootDir, "ready");
|
|
20369
20881
|
let allOk = true;
|
|
20370
|
-
if (!
|
|
20882
|
+
if (!existsSync11(rootDir)) {
|
|
20371
20883
|
warn2(".specialists/ not found in current project");
|
|
20372
20884
|
fix("specialists init");
|
|
20373
20885
|
allOk = false;
|
|
20374
20886
|
} else {
|
|
20375
20887
|
ok3(".specialists/ present");
|
|
20376
20888
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
20377
|
-
if (!
|
|
20889
|
+
if (!existsSync11(subDir)) {
|
|
20378
20890
|
warn2(`.specialists/${label}/ missing — auto-creating`);
|
|
20379
20891
|
mkdirSync3(subDir, { recursive: true });
|
|
20380
20892
|
ok3(`.specialists/${label}/ created`);
|
|
@@ -20387,14 +20899,14 @@ function checkRuntimeDirs() {
|
|
|
20387
20899
|
}
|
|
20388
20900
|
function checkZombieJobs() {
|
|
20389
20901
|
section3("Background jobs");
|
|
20390
|
-
const jobsDir =
|
|
20391
|
-
if (!
|
|
20902
|
+
const jobsDir = join18(CWD, ".specialists", "jobs");
|
|
20903
|
+
if (!existsSync11(jobsDir)) {
|
|
20392
20904
|
hint("No .specialists/jobs/ — skipping");
|
|
20393
20905
|
return true;
|
|
20394
20906
|
}
|
|
20395
20907
|
let entries;
|
|
20396
20908
|
try {
|
|
20397
|
-
entries =
|
|
20909
|
+
entries = readdirSync4(jobsDir);
|
|
20398
20910
|
} catch {
|
|
20399
20911
|
entries = [];
|
|
20400
20912
|
}
|
|
@@ -20406,11 +20918,11 @@ function checkZombieJobs() {
|
|
|
20406
20918
|
let total = 0;
|
|
20407
20919
|
let running = 0;
|
|
20408
20920
|
for (const jobId of entries) {
|
|
20409
|
-
const statusPath =
|
|
20410
|
-
if (!
|
|
20921
|
+
const statusPath = join18(jobsDir, jobId, "status.json");
|
|
20922
|
+
if (!existsSync11(statusPath))
|
|
20411
20923
|
continue;
|
|
20412
20924
|
try {
|
|
20413
|
-
const status = JSON.parse(
|
|
20925
|
+
const status = JSON.parse(readFileSync7(statusPath, "utf8"));
|
|
20414
20926
|
total++;
|
|
20415
20927
|
if (status.status === "running" || status.status === "starting") {
|
|
20416
20928
|
const pid = status.pid;
|
|
@@ -20437,7 +20949,7 @@ function checkZombieJobs() {
|
|
|
20437
20949
|
}
|
|
20438
20950
|
return zombies === 0;
|
|
20439
20951
|
}
|
|
20440
|
-
async function
|
|
20952
|
+
async function run15() {
|
|
20441
20953
|
console.log(`
|
|
20442
20954
|
${bold8("specialists doctor")}
|
|
20443
20955
|
`);
|
|
@@ -20451,20 +20963,21 @@ ${bold8("specialists doctor")}
|
|
|
20451
20963
|
const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
|
|
20452
20964
|
console.log("");
|
|
20453
20965
|
if (allOk) {
|
|
20454
|
-
console.log(` ${
|
|
20966
|
+
console.log(` ${green11("✓")} ${bold8("All checks passed")} — specialists is healthy`);
|
|
20455
20967
|
} else {
|
|
20456
20968
|
console.log(` ${yellow7("○")} ${bold8("Some checks failed")} — follow the fix hints above`);
|
|
20457
20969
|
console.log(` ${dim10("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
|
|
20458
20970
|
}
|
|
20459
20971
|
console.log("");
|
|
20460
20972
|
}
|
|
20461
|
-
var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${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;
|
|
20462
20974
|
var init_doctor = __esm(() => {
|
|
20463
20975
|
CWD = process.cwd();
|
|
20464
|
-
CLAUDE_DIR =
|
|
20465
|
-
|
|
20466
|
-
|
|
20467
|
-
|
|
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");
|
|
20468
20981
|
HOOK_NAMES = [
|
|
20469
20982
|
"specialists-complete.mjs",
|
|
20470
20983
|
"specialists-session-start.mjs"
|
|
@@ -20474,13 +20987,13 @@ var init_doctor = __esm(() => {
|
|
|
20474
20987
|
// src/cli/setup.ts
|
|
20475
20988
|
var exports_setup = {};
|
|
20476
20989
|
__export(exports_setup, {
|
|
20477
|
-
run: () =>
|
|
20990
|
+
run: () => run16
|
|
20478
20991
|
});
|
|
20479
|
-
import { existsSync as
|
|
20992
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20480
20993
|
import { homedir as homedir3 } from "node:os";
|
|
20481
|
-
import { join as
|
|
20994
|
+
import { join as join19, resolve as resolve2 } from "node:path";
|
|
20482
20995
|
function ok4(msg) {
|
|
20483
|
-
console.log(` ${
|
|
20996
|
+
console.log(` ${green12("✓")} ${msg}`);
|
|
20484
20997
|
}
|
|
20485
20998
|
function skip2(msg) {
|
|
20486
20999
|
console.log(` ${yellow8("○")} ${msg}`);
|
|
@@ -20488,12 +21001,12 @@ function skip2(msg) {
|
|
|
20488
21001
|
function resolveTarget(target) {
|
|
20489
21002
|
switch (target) {
|
|
20490
21003
|
case "global":
|
|
20491
|
-
return
|
|
21004
|
+
return join19(homedir3(), ".claude", "CLAUDE.md");
|
|
20492
21005
|
case "agents":
|
|
20493
|
-
return
|
|
21006
|
+
return join19(process.cwd(), "AGENTS.md");
|
|
20494
21007
|
case "project":
|
|
20495
21008
|
default:
|
|
20496
|
-
return
|
|
21009
|
+
return join19(process.cwd(), "CLAUDE.md");
|
|
20497
21010
|
}
|
|
20498
21011
|
}
|
|
20499
21012
|
function parseArgs6() {
|
|
@@ -20521,17 +21034,17 @@ function parseArgs6() {
|
|
|
20521
21034
|
}
|
|
20522
21035
|
return { target, dryRun };
|
|
20523
21036
|
}
|
|
20524
|
-
async function
|
|
21037
|
+
async function run16() {
|
|
20525
21038
|
const { target, dryRun } = parseArgs6();
|
|
20526
|
-
const filePath =
|
|
21039
|
+
const filePath = resolve2(resolveTarget(target));
|
|
20527
21040
|
const label = target === "global" ? "~/.claude/CLAUDE.md" : filePath.replace(process.cwd() + "/", "");
|
|
20528
21041
|
console.log(`
|
|
20529
21042
|
${bold9("specialists setup")}
|
|
20530
21043
|
`);
|
|
20531
21044
|
console.log(` Target: ${yellow8(label)}${dryRun ? dim11(" (dry-run)") : ""}
|
|
20532
21045
|
`);
|
|
20533
|
-
if (
|
|
20534
|
-
const existing =
|
|
21046
|
+
if (existsSync12(filePath)) {
|
|
21047
|
+
const existing = readFileSync8(filePath, "utf8");
|
|
20535
21048
|
if (existing.includes(MARKER)) {
|
|
20536
21049
|
skip2(`${label} already contains Specialists Workflow section`);
|
|
20537
21050
|
console.log(`
|
|
@@ -20552,7 +21065,7 @@ ${bold9("specialists setup")}
|
|
|
20552
21065
|
` : `
|
|
20553
21066
|
|
|
20554
21067
|
`;
|
|
20555
|
-
|
|
21068
|
+
writeFileSync8(filePath, existing.trimEnd() + separator + WORKFLOW_BLOCK, "utf8");
|
|
20556
21069
|
ok4(`Appended Specialists Workflow section to ${label}`);
|
|
20557
21070
|
} else {
|
|
20558
21071
|
if (dryRun) {
|
|
@@ -20563,7 +21076,7 @@ ${bold9("specialists setup")}
|
|
|
20563
21076
|
console.log(dim11("─".repeat(60)));
|
|
20564
21077
|
return;
|
|
20565
21078
|
}
|
|
20566
|
-
|
|
21079
|
+
writeFileSync8(filePath, WORKFLOW_BLOCK, "utf8");
|
|
20567
21080
|
ok4(`Created ${label} with Specialists Workflow section`);
|
|
20568
21081
|
}
|
|
20569
21082
|
console.log("");
|
|
@@ -20573,7 +21086,7 @@ ${bold9("specialists setup")}
|
|
|
20573
21086
|
console.log(` • Run ${yellow8("specialist_init")} in a new session to bootstrap context`);
|
|
20574
21087
|
console.log("");
|
|
20575
21088
|
}
|
|
20576
|
-
var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${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
|
|
20577
21090
|
|
|
20578
21091
|
> Injected by \`specialists setup\`. Keep this section — agents use it for context.
|
|
20579
21092
|
|
|
@@ -20641,26 +21154,28 @@ var init_setup = () => {};
|
|
|
20641
21154
|
// src/cli/help.ts
|
|
20642
21155
|
var exports_help = {};
|
|
20643
21156
|
__export(exports_help, {
|
|
20644
|
-
run: () =>
|
|
21157
|
+
run: () => run17
|
|
20645
21158
|
});
|
|
20646
21159
|
function formatCommands(entries) {
|
|
20647
21160
|
const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
|
|
20648
21161
|
return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
|
|
20649
21162
|
}
|
|
20650
|
-
async function
|
|
21163
|
+
async function run17() {
|
|
20651
21164
|
const lines = [
|
|
20652
21165
|
"",
|
|
20653
21166
|
"Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
|
|
20654
21167
|
"",
|
|
20655
21168
|
bold10("Usage:"),
|
|
20656
|
-
" specialists [command]",
|
|
20657
|
-
" specialists [command] --help",
|
|
21169
|
+
" specialists|sp [command]",
|
|
21170
|
+
" specialists|sp [command] --help",
|
|
21171
|
+
"",
|
|
21172
|
+
dim12(" sp is a shorter alias — sp run, sp list, sp feed etc. all work identically."),
|
|
20658
21173
|
"",
|
|
20659
21174
|
bold10("Common flows:"),
|
|
20660
21175
|
"",
|
|
20661
21176
|
" Tracked work (primary)",
|
|
20662
21177
|
' bd create "Task title" -t task -p 1 --json',
|
|
20663
|
-
" specialists run <name> --bead <id> [--context-depth N] [--background]",
|
|
21178
|
+
" specialists run <name> --bead <id> [--context-depth N] [--background] [--follow]",
|
|
20664
21179
|
" specialists feed -f",
|
|
20665
21180
|
' bd close <id> --reason "Done"',
|
|
20666
21181
|
"",
|
|
@@ -20672,6 +21187,7 @@ async function run15() {
|
|
|
20672
21187
|
" --prompt is for quick untracked work",
|
|
20673
21188
|
" --context-depth defaults to 1 with --bead",
|
|
20674
21189
|
" --no-beads does not disable bead reading",
|
|
21190
|
+
" --follow runs in background and streams output live",
|
|
20675
21191
|
"",
|
|
20676
21192
|
bold10("Core commands:"),
|
|
20677
21193
|
...formatCommands(CORE_COMMANDS),
|
|
@@ -20686,15 +21202,20 @@ async function run15() {
|
|
|
20686
21202
|
" specialists init",
|
|
20687
21203
|
" specialists list",
|
|
20688
21204
|
" specialists run bug-hunt --bead unitAI-123 --background",
|
|
21205
|
+
" specialists run sync-docs --follow # run + stream live output",
|
|
20689
21206
|
' specialists run codebase-explorer --prompt "Map the CLI architecture"',
|
|
20690
21207
|
" specialists feed -f",
|
|
21208
|
+
' specialists steer <job-id> "focus only on supervisor.ts"',
|
|
21209
|
+
' specialists follow-up <job-id> "now write the fix"',
|
|
20691
21210
|
" specialists result <job-id>",
|
|
20692
21211
|
"",
|
|
20693
21212
|
bold10("More help:"),
|
|
20694
|
-
" specialists quickstart
|
|
20695
|
-
" specialists run --help
|
|
20696
|
-
" specialists
|
|
20697
|
-
" specialists
|
|
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",
|
|
20698
21219
|
"",
|
|
20699
21220
|
dim12("Project model: specialists are project-only; user-scope discovery is deprecated."),
|
|
20700
21221
|
""
|
|
@@ -20710,6 +21231,8 @@ var init_help = __esm(() => {
|
|
|
20710
21231
|
["run", "Run a specialist with --bead for tracked work or --prompt for ad-hoc work"],
|
|
20711
21232
|
["feed", "Tail job events; use -f to follow all jobs"],
|
|
20712
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)"],
|
|
20713
21236
|
["stop", "Stop a running background job"],
|
|
20714
21237
|
["status", "Show health, MCP state, and active jobs"],
|
|
20715
21238
|
["doctor", "Diagnose installation/runtime problems"],
|
|
@@ -28002,7 +28525,7 @@ class StdioServerTransport {
|
|
|
28002
28525
|
}
|
|
28003
28526
|
|
|
28004
28527
|
// src/server.ts
|
|
28005
|
-
import { join as
|
|
28528
|
+
import { join as join7 } from "node:path";
|
|
28006
28529
|
|
|
28007
28530
|
// src/constants.ts
|
|
28008
28531
|
var LOG_PREFIX = "[specialists]";
|
|
@@ -28118,8 +28641,7 @@ function createUseSpecialistTool(runner) {
|
|
|
28118
28641
|
if (!bead) {
|
|
28119
28642
|
throw new Error(`Unable to read bead '${input.bead_id}' via bd show --json`);
|
|
28120
28643
|
}
|
|
28121
|
-
const
|
|
28122
|
-
const beadContext = buildBeadContext(bead, { blockers, depth: input.context_depth ?? 0 });
|
|
28644
|
+
const beadContext = buildBeadContext(bead);
|
|
28123
28645
|
prompt = beadContext;
|
|
28124
28646
|
variables = {
|
|
28125
28647
|
...input.variables ?? {},
|
|
@@ -28239,17 +28761,17 @@ function createSpecialistStatusTool(loader, circuitBreaker) {
|
|
|
28239
28761
|
async execute(_) {
|
|
28240
28762
|
const list = await loader.list();
|
|
28241
28763
|
const stalenessResults = await Promise.all(list.map((s) => checkStaleness(s)));
|
|
28242
|
-
const { existsSync:
|
|
28764
|
+
const { existsSync: existsSync4, readdirSync, readFileSync: readFileSync2 } = await import("node:fs");
|
|
28243
28765
|
const { join: join3 } = await import("node:path");
|
|
28244
28766
|
const jobsDir = join3(process.cwd(), ".specialists", "jobs");
|
|
28245
28767
|
const jobs = [];
|
|
28246
|
-
if (
|
|
28768
|
+
if (existsSync4(jobsDir)) {
|
|
28247
28769
|
for (const entry of readdirSync(jobsDir)) {
|
|
28248
28770
|
const statusPath = join3(jobsDir, entry, "status.json");
|
|
28249
|
-
if (!
|
|
28771
|
+
if (!existsSync4(statusPath))
|
|
28250
28772
|
continue;
|
|
28251
28773
|
try {
|
|
28252
|
-
jobs.push(JSON.parse(
|
|
28774
|
+
jobs.push(JSON.parse(readFileSync2(statusPath, "utf-8")));
|
|
28253
28775
|
} catch {}
|
|
28254
28776
|
}
|
|
28255
28777
|
jobs.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
@@ -28328,6 +28850,74 @@ class JobRegistry {
|
|
|
28328
28850
|
}
|
|
28329
28851
|
job.killFn = killFn;
|
|
28330
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
|
+
}
|
|
28331
28921
|
complete(id, result) {
|
|
28332
28922
|
const job = this.jobs.get(id);
|
|
28333
28923
|
if (!job || job.status !== "running")
|
|
@@ -28452,20 +29042,117 @@ function createStopSpecialistTool(registry2) {
|
|
|
28452
29042
|
};
|
|
28453
29043
|
}
|
|
28454
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
|
+
|
|
28455
29142
|
// src/server.ts
|
|
28456
29143
|
init_zod();
|
|
28457
29144
|
|
|
28458
29145
|
// src/tools/specialist/specialist_init.tool.ts
|
|
28459
29146
|
init_zod();
|
|
28460
|
-
import { spawnSync as
|
|
28461
|
-
import { existsSync as
|
|
28462
|
-
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";
|
|
28463
29150
|
var specialistInitSchema = objectType({});
|
|
28464
29151
|
function createSpecialistInitTool(loader, deps) {
|
|
28465
29152
|
const resolved = deps ?? {
|
|
28466
|
-
bdAvailable: () =>
|
|
28467
|
-
beadsExists: () =>
|
|
28468
|
-
bdInit: () =>
|
|
29153
|
+
bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
|
|
29154
|
+
beadsExists: () => existsSync5(join6(process.cwd(), ".beads")),
|
|
29155
|
+
bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
|
|
28469
29156
|
};
|
|
28470
29157
|
return {
|
|
28471
29158
|
name: "specialist_init",
|
|
@@ -28499,7 +29186,7 @@ class SpecialistsServer {
|
|
|
28499
29186
|
const circuitBreaker = new CircuitBreaker;
|
|
28500
29187
|
const loader = new SpecialistLoader;
|
|
28501
29188
|
const hooks = new HookEmitter({
|
|
28502
|
-
tracePath:
|
|
29189
|
+
tracePath: join7(process.cwd(), ".specialists", "trace.jsonl")
|
|
28503
29190
|
});
|
|
28504
29191
|
const beadsClient = new BeadsClient;
|
|
28505
29192
|
const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
|
|
@@ -28512,6 +29199,8 @@ class SpecialistsServer {
|
|
|
28512
29199
|
createStartSpecialistTool(runner, registry2),
|
|
28513
29200
|
createPollSpecialistTool(registry2),
|
|
28514
29201
|
createStopSpecialistTool(registry2),
|
|
29202
|
+
createSteerSpecialistTool(registry2),
|
|
29203
|
+
createFollowUpSpecialistTool(registry2),
|
|
28515
29204
|
createSpecialistInitTool(loader)
|
|
28516
29205
|
];
|
|
28517
29206
|
this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
|
|
@@ -28527,6 +29216,8 @@ class SpecialistsServer {
|
|
|
28527
29216
|
start_specialist: startSpecialistSchema,
|
|
28528
29217
|
poll_specialist: pollSpecialistSchema,
|
|
28529
29218
|
stop_specialist: stopSpecialistSchema,
|
|
29219
|
+
steer_specialist: steerSpecialistSchema,
|
|
29220
|
+
follow_up_specialist: followUpSpecialistSchema,
|
|
28530
29221
|
specialist_init: specialistInitSchema
|
|
28531
29222
|
};
|
|
28532
29223
|
this.toolSchemas = schemaMap;
|
|
@@ -28599,7 +29290,7 @@ var next = process.argv[3];
|
|
|
28599
29290
|
function wantsHelp() {
|
|
28600
29291
|
return next === "--help" || next === "-h";
|
|
28601
29292
|
}
|
|
28602
|
-
async function
|
|
29293
|
+
async function run18() {
|
|
28603
29294
|
if (sub === "install") {
|
|
28604
29295
|
if (wantsHelp()) {
|
|
28605
29296
|
console.log([
|
|
@@ -28754,11 +29445,13 @@ async function run16() {
|
|
|
28754
29445
|
" --context-depth <n> Dependency context depth when using --bead (default: 1)",
|
|
28755
29446
|
" --no-beads Do not create a new tracking bead (does not disable bead reading)",
|
|
28756
29447
|
" --background Start async and return a job id",
|
|
29448
|
+
" --follow Run in background and stream output live",
|
|
28757
29449
|
" --model <model> Override the configured model for this run",
|
|
28758
29450
|
"",
|
|
28759
29451
|
"Examples:",
|
|
28760
29452
|
" specialists run bug-hunt --bead unitAI-55d",
|
|
28761
29453
|
" specialists run bug-hunt --bead unitAI-55d --context-depth 2 --background",
|
|
29454
|
+
" specialists run sync-docs --follow",
|
|
28762
29455
|
' specialists run code-review --prompt "Audit src/api.ts"',
|
|
28763
29456
|
" cat brief.md | specialists run report-generator",
|
|
28764
29457
|
"",
|
|
@@ -28857,6 +29550,65 @@ async function run16() {
|
|
|
28857
29550
|
const { run: handler } = await Promise.resolve().then(() => (init_feed(), exports_feed));
|
|
28858
29551
|
return handler();
|
|
28859
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
|
+
}
|
|
28860
29612
|
if (sub === "stop") {
|
|
28861
29613
|
if (wantsHelp()) {
|
|
28862
29614
|
console.log([
|
|
@@ -28952,7 +29704,7 @@ Run 'specialists help' to see available commands.`);
|
|
|
28952
29704
|
const server = new SpecialistsServer;
|
|
28953
29705
|
await server.start();
|
|
28954
29706
|
}
|
|
28955
|
-
|
|
29707
|
+
run18().catch((error2) => {
|
|
28956
29708
|
logger.error(`Fatal error: ${error2}`);
|
|
28957
29709
|
process.exit(1);
|
|
28958
29710
|
});
|