@jaggerxtrm/specialists 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/hooks/specialists-session-start.mjs +3 -3
- package/config/skills/specialists-creator/SKILL.md +22 -1
- package/config/skills/using-specialists/SKILL.md +261 -50
- package/config/specialists/debugger.specialist.yaml +13 -3
- package/config/specialists/executor.specialist.yaml +257 -0
- package/config/specialists/explorer.specialist.yaml +12 -6
- package/config/specialists/memory-processor.specialist.yaml +15 -1
- package/config/specialists/overthinker.specialist.yaml +16 -3
- package/config/specialists/{parallel-runner.specialist.yaml → parallel-review.specialist.yaml} +15 -1
- package/config/specialists/planner.specialist.yaml +31 -24
- package/config/specialists/reviewer.specialist.yaml +142 -0
- package/config/specialists/specialists-creator.specialist.yaml +10 -2
- package/config/specialists/sync-docs.specialist.yaml +20 -5
- package/config/specialists/test-runner.specialist.yaml +8 -1
- package/config/specialists/xt-merge.specialist.yaml +97 -16
- package/dist/index.js +2278 -848
- package/package.json +1 -1
- package/config/specialists/auto-remediation.specialist.yaml +0 -70
package/dist/index.js
CHANGED
|
@@ -17442,9 +17442,6 @@ function getFriendlyMessage(issue2) {
|
|
|
17442
17442
|
if (issue2.code === "invalid_literal") {
|
|
17443
17443
|
return `Invalid value at "${path}": expected "${issue2.expected}"`;
|
|
17444
17444
|
}
|
|
17445
|
-
if (issue2.code === "missing_key") {
|
|
17446
|
-
return `Missing required field: "${formatPath(issue2.path)}"`;
|
|
17447
|
-
}
|
|
17448
17445
|
return issue2.message;
|
|
17449
17446
|
}
|
|
17450
17447
|
async function validateSpecialist(yamlContent) {
|
|
@@ -17505,7 +17502,7 @@ ${result.warnings.map((w) => ` ⚠ ${w}`).join(`
|
|
|
17505
17502
|
const raw = $parse(yamlContent);
|
|
17506
17503
|
return SpecialistSchema.parseAsync(raw);
|
|
17507
17504
|
}
|
|
17508
|
-
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, SpecialistSchema;
|
|
17505
|
+
var KebabCase, Semver, MetadataSchema, ExecutionSchema, PromptSchema2, ScriptEntrySchema, SkillsSchema, CapabilitiesSchema, CommunicationSchema, ValidationSchema, StallDetectionSchema, SpecialistSchema;
|
|
17509
17506
|
var init_schema = __esm(() => {
|
|
17510
17507
|
init_zod();
|
|
17511
17508
|
init_dist();
|
|
@@ -17527,6 +17524,8 @@ var init_schema = __esm(() => {
|
|
|
17527
17524
|
fallback_model: stringType().optional(),
|
|
17528
17525
|
timeout_ms: numberType().default(120000),
|
|
17529
17526
|
stall_timeout_ms: numberType().optional(),
|
|
17527
|
+
max_retries: numberType().int().min(0).default(0),
|
|
17528
|
+
interactive: booleanType().default(false),
|
|
17530
17529
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
17531
17530
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
17532
17531
|
thinking_level: enumType(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
|
@@ -17564,9 +17563,14 @@ var init_schema = __esm(() => {
|
|
|
17564
17563
|
}).optional();
|
|
17565
17564
|
ValidationSchema = objectType({
|
|
17566
17565
|
files_to_watch: arrayType(stringType()).optional(),
|
|
17567
|
-
references: arrayType(unknownType()).optional(),
|
|
17568
17566
|
stale_threshold_days: numberType().optional()
|
|
17569
17567
|
}).optional();
|
|
17568
|
+
StallDetectionSchema = objectType({
|
|
17569
|
+
running_silence_warn_ms: numberType().optional(),
|
|
17570
|
+
running_silence_error_ms: numberType().optional(),
|
|
17571
|
+
waiting_stale_ms: numberType().optional(),
|
|
17572
|
+
tool_duration_warn_ms: numberType().optional()
|
|
17573
|
+
}).optional();
|
|
17570
17574
|
SpecialistSchema = objectType({
|
|
17571
17575
|
specialist: objectType({
|
|
17572
17576
|
metadata: MetadataSchema,
|
|
@@ -17576,8 +17580,10 @@ var init_schema = __esm(() => {
|
|
|
17576
17580
|
capabilities: CapabilitiesSchema,
|
|
17577
17581
|
communication: CommunicationSchema,
|
|
17578
17582
|
validation: ValidationSchema,
|
|
17583
|
+
stall_detection: StallDetectionSchema,
|
|
17579
17584
|
output_file: stringType().optional(),
|
|
17580
17585
|
beads_integration: enumType(["auto", "always", "never"]).default("auto"),
|
|
17586
|
+
beads_write_notes: booleanType().default(true),
|
|
17581
17587
|
heartbeat: unknownType().optional()
|
|
17582
17588
|
})
|
|
17583
17589
|
});
|
|
@@ -17614,8 +17620,13 @@ class SpecialistLoader {
|
|
|
17614
17620
|
}
|
|
17615
17621
|
getScanDirs() {
|
|
17616
17622
|
const dirs = [
|
|
17623
|
+
{ path: join(this.projectDir, ".specialists", "user"), scope: "user" },
|
|
17617
17624
|
{ path: join(this.projectDir, ".specialists", "user", "specialists"), scope: "user" },
|
|
17618
|
-
{ path: join(this.projectDir, ".specialists", "default"
|
|
17625
|
+
{ path: join(this.projectDir, ".specialists", "default"), scope: "default" },
|
|
17626
|
+
{ path: join(this.projectDir, ".specialists", "default", "specialists"), scope: "default" },
|
|
17627
|
+
{ path: join(this.projectDir, "specialists"), scope: "default" },
|
|
17628
|
+
{ path: join(this.projectDir, ".claude", "specialists"), scope: "default" },
|
|
17629
|
+
{ path: join(this.projectDir, ".agent-forge", "specialists"), scope: "default" }
|
|
17619
17630
|
];
|
|
17620
17631
|
return dirs.filter((d) => existsSync(d.path));
|
|
17621
17632
|
}
|
|
@@ -17645,7 +17656,8 @@ class SpecialistLoader {
|
|
|
17645
17656
|
filePath,
|
|
17646
17657
|
updated,
|
|
17647
17658
|
filestoWatch: spec.specialist.validation?.files_to_watch,
|
|
17648
|
-
staleThresholdDays: spec.specialist.validation?.stale_threshold_days
|
|
17659
|
+
staleThresholdDays: spec.specialist.validation?.stale_threshold_days,
|
|
17660
|
+
stallDetection: spec.specialist.stall_detection ?? undefined
|
|
17649
17661
|
});
|
|
17650
17662
|
} catch (e) {
|
|
17651
17663
|
const reason = e instanceof Error ? e.message : String(e);
|
|
@@ -17759,7 +17771,11 @@ class PiAgentSession {
|
|
|
17759
17771
|
_agentEndReceived = false;
|
|
17760
17772
|
_killed = false;
|
|
17761
17773
|
_lineBuffer = "";
|
|
17762
|
-
|
|
17774
|
+
_pendingRequests = new Map;
|
|
17775
|
+
_nextRequestId = 1;
|
|
17776
|
+
_stderrBuffer = "";
|
|
17777
|
+
_stallTimer;
|
|
17778
|
+
_stallError;
|
|
17763
17779
|
meta;
|
|
17764
17780
|
constructor(options, meta) {
|
|
17765
17781
|
this.options = options;
|
|
@@ -17809,7 +17825,7 @@ class PiAgentSession {
|
|
|
17809
17825
|
args.push("--append-system-prompt", this.options.systemPrompt);
|
|
17810
17826
|
}
|
|
17811
17827
|
this.proc = spawn("pi", args, {
|
|
17812
|
-
stdio: ["pipe", "pipe", "
|
|
17828
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17813
17829
|
cwd: this.options.cwd
|
|
17814
17830
|
});
|
|
17815
17831
|
const donePromise = new Promise((resolve, reject) => {
|
|
@@ -17818,6 +17834,9 @@ class PiAgentSession {
|
|
|
17818
17834
|
});
|
|
17819
17835
|
donePromise.catch(() => {});
|
|
17820
17836
|
this._donePromise = donePromise;
|
|
17837
|
+
this.proc.stderr?.on("data", (chunk) => {
|
|
17838
|
+
this._stderrBuffer += chunk.toString();
|
|
17839
|
+
});
|
|
17821
17840
|
this.proc.stdout?.on("data", (chunk) => {
|
|
17822
17841
|
this._lineBuffer += chunk.toString();
|
|
17823
17842
|
const lines = this._lineBuffer.split(`
|
|
@@ -17835,6 +17854,7 @@ class PiAgentSession {
|
|
|
17835
17854
|
}
|
|
17836
17855
|
});
|
|
17837
17856
|
this.proc.on("close", (code) => {
|
|
17857
|
+
this._clearStallTimer();
|
|
17838
17858
|
if (this._agentEndReceived || this._killed) {
|
|
17839
17859
|
this._doneResolve?.();
|
|
17840
17860
|
} else if (code === 0 || code === null) {
|
|
@@ -17844,6 +17864,25 @@ class PiAgentSession {
|
|
|
17844
17864
|
}
|
|
17845
17865
|
});
|
|
17846
17866
|
}
|
|
17867
|
+
_clearStallTimer() {
|
|
17868
|
+
if (this._stallTimer) {
|
|
17869
|
+
clearTimeout(this._stallTimer);
|
|
17870
|
+
this._stallTimer = undefined;
|
|
17871
|
+
}
|
|
17872
|
+
}
|
|
17873
|
+
_markActivity() {
|
|
17874
|
+
const timeoutMs = this.options.stallTimeoutMs;
|
|
17875
|
+
if (!timeoutMs || timeoutMs <= 0 || this._killed || this._agentEndReceived)
|
|
17876
|
+
return;
|
|
17877
|
+
this._clearStallTimer();
|
|
17878
|
+
this._stallTimer = setTimeout(() => {
|
|
17879
|
+
if (this._killed || this._agentEndReceived)
|
|
17880
|
+
return;
|
|
17881
|
+
const err = new StallTimeoutError(timeoutMs);
|
|
17882
|
+
this._stallError = err;
|
|
17883
|
+
this.kill(err);
|
|
17884
|
+
}, timeoutMs);
|
|
17885
|
+
}
|
|
17847
17886
|
_handleEvent(line) {
|
|
17848
17887
|
let event;
|
|
17849
17888
|
try {
|
|
@@ -17851,11 +17890,18 @@ class PiAgentSession {
|
|
|
17851
17890
|
} catch {
|
|
17852
17891
|
return;
|
|
17853
17892
|
}
|
|
17893
|
+
this._markActivity();
|
|
17854
17894
|
const { type } = event;
|
|
17855
17895
|
if (type === "response") {
|
|
17856
|
-
const
|
|
17857
|
-
|
|
17858
|
-
|
|
17896
|
+
const id = event.id;
|
|
17897
|
+
if (id !== undefined) {
|
|
17898
|
+
const entry = this._pendingRequests.get(id);
|
|
17899
|
+
if (entry) {
|
|
17900
|
+
clearTimeout(entry.timer);
|
|
17901
|
+
this._pendingRequests.delete(id);
|
|
17902
|
+
entry.resolve(event);
|
|
17903
|
+
}
|
|
17904
|
+
}
|
|
17859
17905
|
return;
|
|
17860
17906
|
}
|
|
17861
17907
|
if (type === "message_start") {
|
|
@@ -17895,11 +17941,13 @@ class PiAgentSession {
|
|
|
17895
17941
|
this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
17896
17942
|
}
|
|
17897
17943
|
this._agentEndReceived = true;
|
|
17944
|
+
this._clearStallTimer();
|
|
17898
17945
|
this.options.onEvent?.("agent_end");
|
|
17899
17946
|
this._doneResolve?.();
|
|
17900
17947
|
return;
|
|
17901
17948
|
}
|
|
17902
17949
|
if (type === "tool_execution_start") {
|
|
17950
|
+
this.options.onToolStart?.(event.toolName ?? event.name ?? "tool", event.args, event.toolCallId);
|
|
17903
17951
|
this.options.onEvent?.("tool_execution_start");
|
|
17904
17952
|
return;
|
|
17905
17953
|
}
|
|
@@ -17908,10 +17956,18 @@ class PiAgentSession {
|
|
|
17908
17956
|
return;
|
|
17909
17957
|
}
|
|
17910
17958
|
if (type === "tool_execution_end") {
|
|
17911
|
-
this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool");
|
|
17959
|
+
this.options.onToolEnd?.(event.toolName ?? event.name ?? "tool", event.isError ?? false, event.toolCallId);
|
|
17912
17960
|
this.options.onEvent?.("tool_execution_end");
|
|
17913
17961
|
return;
|
|
17914
17962
|
}
|
|
17963
|
+
if (type === "auto_compaction_start" || type === "auto_compaction_end") {
|
|
17964
|
+
this.options.onEvent?.("auto_compaction");
|
|
17965
|
+
return;
|
|
17966
|
+
}
|
|
17967
|
+
if (type === "auto_retry_start" || type === "auto_retry_end") {
|
|
17968
|
+
this.options.onEvent?.("auto_retry");
|
|
17969
|
+
return;
|
|
17970
|
+
}
|
|
17915
17971
|
if (type === "message_update") {
|
|
17916
17972
|
const ae = event.assistantMessageEvent;
|
|
17917
17973
|
if (!ae)
|
|
@@ -17943,26 +17999,38 @@ class PiAgentSession {
|
|
|
17943
17999
|
}
|
|
17944
18000
|
}
|
|
17945
18001
|
}
|
|
17946
|
-
sendCommand(cmd) {
|
|
18002
|
+
sendCommand(cmd, timeoutMs = 1e4) {
|
|
17947
18003
|
return new Promise((resolve, reject) => {
|
|
17948
18004
|
if (!this.proc?.stdin) {
|
|
17949
18005
|
reject(new Error("No stdin available"));
|
|
17950
18006
|
return;
|
|
17951
18007
|
}
|
|
17952
|
-
|
|
17953
|
-
|
|
18008
|
+
const id = this._nextRequestId++;
|
|
18009
|
+
const timer = setTimeout(() => {
|
|
18010
|
+
this._pendingRequests.delete(id);
|
|
18011
|
+
reject(new Error(`RPC timeout: no response for command id=${id} after ${timeoutMs}ms`));
|
|
18012
|
+
}, timeoutMs);
|
|
18013
|
+
this._pendingRequests.set(id, { resolve, reject, timer });
|
|
18014
|
+
this.proc.stdin.write(JSON.stringify({ ...cmd, id }) + `
|
|
17954
18015
|
`, (err) => {
|
|
17955
18016
|
if (err) {
|
|
17956
|
-
|
|
18017
|
+
const entry = this._pendingRequests.get(id);
|
|
18018
|
+
if (entry) {
|
|
18019
|
+
clearTimeout(entry.timer);
|
|
18020
|
+
this._pendingRequests.delete(id);
|
|
18021
|
+
}
|
|
17957
18022
|
reject(err);
|
|
17958
18023
|
}
|
|
17959
18024
|
});
|
|
17960
18025
|
});
|
|
17961
18026
|
}
|
|
17962
18027
|
async prompt(task) {
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
this.
|
|
18028
|
+
this._stallError = undefined;
|
|
18029
|
+
this._markActivity();
|
|
18030
|
+
const response = await this.sendCommand({ type: "prompt", message: task });
|
|
18031
|
+
if (response?.success === false) {
|
|
18032
|
+
throw new Error(`Prompt rejected by pi: ${response.error ?? "already streaming"}`);
|
|
18033
|
+
}
|
|
17966
18034
|
}
|
|
17967
18035
|
async waitForDone(timeout) {
|
|
17968
18036
|
const donePromise = this._donePromise ?? Promise.resolve();
|
|
@@ -18001,6 +18069,7 @@ class PiAgentSession {
|
|
|
18001
18069
|
async close() {
|
|
18002
18070
|
if (this._killed)
|
|
18003
18071
|
return;
|
|
18072
|
+
this._clearStallTimer();
|
|
18004
18073
|
this.proc?.stdin?.end();
|
|
18005
18074
|
if (this.proc) {
|
|
18006
18075
|
await new Promise((resolve) => {
|
|
@@ -18014,23 +18083,41 @@ class PiAgentSession {
|
|
|
18014
18083
|
});
|
|
18015
18084
|
}
|
|
18016
18085
|
}
|
|
18017
|
-
kill() {
|
|
18086
|
+
kill(reason) {
|
|
18018
18087
|
if (this._killed)
|
|
18019
18088
|
return;
|
|
18020
18089
|
this._killed = true;
|
|
18090
|
+
this._clearStallTimer();
|
|
18091
|
+
if (this.proc?.stdin?.writable) {
|
|
18092
|
+
try {
|
|
18093
|
+
this.proc.stdin.write(JSON.stringify({ type: "abort" }) + `
|
|
18094
|
+
`);
|
|
18095
|
+
} catch {}
|
|
18096
|
+
}
|
|
18097
|
+
const killError = reason ?? this._stallError ?? new SessionKilledError;
|
|
18098
|
+
for (const [, entry] of this._pendingRequests) {
|
|
18099
|
+
clearTimeout(entry.timer);
|
|
18100
|
+
entry.reject(killError);
|
|
18101
|
+
}
|
|
18102
|
+
this._pendingRequests.clear();
|
|
18021
18103
|
this.proc?.kill();
|
|
18022
18104
|
this.proc = undefined;
|
|
18023
|
-
this._doneReject?.(
|
|
18105
|
+
this._doneReject?.(killError);
|
|
18106
|
+
}
|
|
18107
|
+
getStderr() {
|
|
18108
|
+
return this._stderrBuffer;
|
|
18024
18109
|
}
|
|
18025
18110
|
async steer(message) {
|
|
18026
18111
|
if (this._killed || !this.proc?.stdin) {
|
|
18027
18112
|
throw new Error("Session is not active");
|
|
18028
18113
|
}
|
|
18029
|
-
const
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18033
|
-
|
|
18114
|
+
const response = await this.sendCommand({ type: "steer", message });
|
|
18115
|
+
if (response?.success === false) {
|
|
18116
|
+
throw new Error(`Steer rejected by pi: ${response.error ?? "steer failed"}`);
|
|
18117
|
+
}
|
|
18118
|
+
}
|
|
18119
|
+
followUp(_task) {
|
|
18120
|
+
throw new Error("followUp() is not yet implemented. Use resume() to send a next-turn prompt to a waiting session.");
|
|
18034
18121
|
}
|
|
18035
18122
|
async resume(task, timeout) {
|
|
18036
18123
|
if (this._killed || !this.proc?.stdin) {
|
|
@@ -18047,7 +18134,7 @@ class PiAgentSession {
|
|
|
18047
18134
|
await this.waitForDone(timeout);
|
|
18048
18135
|
}
|
|
18049
18136
|
}
|
|
18050
|
-
var SessionKilledError;
|
|
18137
|
+
var SessionKilledError, StallTimeoutError;
|
|
18051
18138
|
var init_session = __esm(() => {
|
|
18052
18139
|
init_backendMap();
|
|
18053
18140
|
SessionKilledError = class SessionKilledError extends Error {
|
|
@@ -18056,6 +18143,74 @@ var init_session = __esm(() => {
|
|
|
18056
18143
|
this.name = "SessionKilledError";
|
|
18057
18144
|
}
|
|
18058
18145
|
};
|
|
18146
|
+
StallTimeoutError = class StallTimeoutError extends Error {
|
|
18147
|
+
constructor(timeoutMs) {
|
|
18148
|
+
super(`Session stalled: no activity for ${timeoutMs}ms`);
|
|
18149
|
+
this.name = "StallTimeoutError";
|
|
18150
|
+
}
|
|
18151
|
+
};
|
|
18152
|
+
});
|
|
18153
|
+
|
|
18154
|
+
// src/utils/circuitBreaker.ts
|
|
18155
|
+
function isTransientError(error2) {
|
|
18156
|
+
if (!error2)
|
|
18157
|
+
return false;
|
|
18158
|
+
const status = error2.status ?? error2.statusCode;
|
|
18159
|
+
if (typeof status === "number" && status >= 500 && status < 600) {
|
|
18160
|
+
return true;
|
|
18161
|
+
}
|
|
18162
|
+
const message = error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : JSON.stringify(error2);
|
|
18163
|
+
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
18164
|
+
}
|
|
18165
|
+
|
|
18166
|
+
class CircuitBreaker {
|
|
18167
|
+
states = new Map;
|
|
18168
|
+
threshold;
|
|
18169
|
+
cooldownMs;
|
|
18170
|
+
constructor(options = {}) {
|
|
18171
|
+
this.threshold = options.failureThreshold ?? 3;
|
|
18172
|
+
this.cooldownMs = options.cooldownMs ?? 60000;
|
|
18173
|
+
}
|
|
18174
|
+
getState(backend) {
|
|
18175
|
+
const entry = this.states.get(backend);
|
|
18176
|
+
if (!entry)
|
|
18177
|
+
return "CLOSED";
|
|
18178
|
+
if (entry.state === "OPEN" && Date.now() - entry.openedAt > this.cooldownMs) {
|
|
18179
|
+
entry.state = "HALF_OPEN";
|
|
18180
|
+
}
|
|
18181
|
+
return entry.state;
|
|
18182
|
+
}
|
|
18183
|
+
isAvailable(backend) {
|
|
18184
|
+
return this.getState(backend) !== "OPEN";
|
|
18185
|
+
}
|
|
18186
|
+
recordSuccess(backend) {
|
|
18187
|
+
this.states.set(backend, { state: "CLOSED", failures: 0 });
|
|
18188
|
+
}
|
|
18189
|
+
recordFailure(backend) {
|
|
18190
|
+
const entry = this.states.get(backend) ?? { state: "CLOSED", failures: 0 };
|
|
18191
|
+
entry.failures++;
|
|
18192
|
+
if (entry.failures >= this.threshold) {
|
|
18193
|
+
entry.state = "OPEN";
|
|
18194
|
+
entry.openedAt = Date.now();
|
|
18195
|
+
}
|
|
18196
|
+
this.states.set(backend, entry);
|
|
18197
|
+
}
|
|
18198
|
+
}
|
|
18199
|
+
var TRANSIENT_ERROR_PATTERNS;
|
|
18200
|
+
var init_circuitBreaker = __esm(() => {
|
|
18201
|
+
TRANSIENT_ERROR_PATTERNS = [
|
|
18202
|
+
/\b5\d{2}\b/,
|
|
18203
|
+
/timeout/i,
|
|
18204
|
+
/timed out/i,
|
|
18205
|
+
/econnreset/i,
|
|
18206
|
+
/econnrefused/i,
|
|
18207
|
+
/eai_again/i,
|
|
18208
|
+
/etimedout/i,
|
|
18209
|
+
/network error/i,
|
|
18210
|
+
/service unavailable/i,
|
|
18211
|
+
/bad gateway/i,
|
|
18212
|
+
/gateway timeout/i
|
|
18213
|
+
];
|
|
18059
18214
|
});
|
|
18060
18215
|
|
|
18061
18216
|
// src/specialist/beads.ts
|
|
@@ -18065,6 +18220,9 @@ function buildBeadContext(bead, completedBlockers = []) {
|
|
|
18065
18220
|
if (bead.description?.trim()) {
|
|
18066
18221
|
lines.push(bead.description.trim());
|
|
18067
18222
|
}
|
|
18223
|
+
if (bead.parent?.trim()) {
|
|
18224
|
+
lines.push("", "## Parent epic", bead.parent.trim());
|
|
18225
|
+
}
|
|
18068
18226
|
if (bead.notes?.trim()) {
|
|
18069
18227
|
lines.push("", "## Notes", bead.notes.trim());
|
|
18070
18228
|
}
|
|
@@ -18203,12 +18361,17 @@ import { execSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
|
18203
18361
|
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
18204
18362
|
import { basename, resolve } from "node:path";
|
|
18205
18363
|
import { homedir as homedir2 } from "node:os";
|
|
18206
|
-
function runScript(
|
|
18364
|
+
function runScript(command) {
|
|
18365
|
+
const run = (command ?? "").trim();
|
|
18366
|
+
if (!run) {
|
|
18367
|
+
return { name: "unknown", output: "Missing script command (expected `run` or legacy `path`).", exitCode: 1 };
|
|
18368
|
+
}
|
|
18369
|
+
const scriptName = basename(run.split(" ")[0]);
|
|
18207
18370
|
try {
|
|
18208
18371
|
const output = execSync(run, { encoding: "utf8", timeout: 30000 });
|
|
18209
|
-
return { name:
|
|
18372
|
+
return { name: scriptName, output, exitCode: 0 };
|
|
18210
18373
|
} catch (e) {
|
|
18211
|
-
return { name:
|
|
18374
|
+
return { name: scriptName, output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
|
|
18212
18375
|
}
|
|
18213
18376
|
}
|
|
18214
18377
|
function formatScriptOutput(results) {
|
|
@@ -18253,7 +18416,14 @@ function validateShebang(filePath, errors5) {
|
|
|
18253
18416
|
}
|
|
18254
18417
|
} catch {}
|
|
18255
18418
|
}
|
|
18256
|
-
function
|
|
18419
|
+
function isToolAvailable(tool, permissionLevel) {
|
|
18420
|
+
const normalized = permissionLevel.toUpperCase();
|
|
18421
|
+
const gatedLevels = PERMISSION_GATED_TOOLS[tool.toLowerCase()];
|
|
18422
|
+
if (!gatedLevels)
|
|
18423
|
+
return true;
|
|
18424
|
+
return gatedLevels.includes(normalized);
|
|
18425
|
+
}
|
|
18426
|
+
function validateBeforeRun(spec, permissionLevel) {
|
|
18257
18427
|
const errors5 = [];
|
|
18258
18428
|
const warnings = [];
|
|
18259
18429
|
for (const p of spec.specialist.skills?.paths ?? []) {
|
|
@@ -18262,7 +18432,7 @@ function validateBeforeRun(spec) {
|
|
|
18262
18432
|
warnings.push(` ⚠ skills.paths: file not found: ${p}`);
|
|
18263
18433
|
}
|
|
18264
18434
|
for (const script of spec.specialist.skills?.scripts ?? []) {
|
|
18265
|
-
const
|
|
18435
|
+
const run = script.run ?? script.path;
|
|
18266
18436
|
if (!run)
|
|
18267
18437
|
continue;
|
|
18268
18438
|
const isFilePath = run.startsWith("./") || run.startsWith("../") || run.startsWith("/") || run.startsWith("~/");
|
|
@@ -18285,6 +18455,11 @@ function validateBeforeRun(spec) {
|
|
|
18285
18455
|
errors5.push(` ✗ capabilities.external_commands: not found on PATH: ${cmd}`);
|
|
18286
18456
|
}
|
|
18287
18457
|
}
|
|
18458
|
+
for (const tool of spec.specialist.capabilities?.required_tools ?? []) {
|
|
18459
|
+
if (!isToolAvailable(tool, permissionLevel)) {
|
|
18460
|
+
errors5.push(` ✗ capabilities.required_tools: tool "${tool}" requires higher permission than "${permissionLevel}"`);
|
|
18461
|
+
}
|
|
18462
|
+
}
|
|
18288
18463
|
if (warnings.length > 0) {
|
|
18289
18464
|
process.stderr.write(`[specialists] pre-run warnings:
|
|
18290
18465
|
${warnings.join(`
|
|
@@ -18297,6 +18472,18 @@ ${errors5.join(`
|
|
|
18297
18472
|
`)}`);
|
|
18298
18473
|
}
|
|
18299
18474
|
}
|
|
18475
|
+
function sleep(ms) {
|
|
18476
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
18477
|
+
}
|
|
18478
|
+
function getRetryDelayMs(attemptNumber) {
|
|
18479
|
+
const baseDelay = RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attemptNumber - 1);
|
|
18480
|
+
const jitterMultiplier = 1 + (Math.random() * 2 - 1) * RETRY_MAX_JITTER;
|
|
18481
|
+
return Math.max(0, Math.round(baseDelay * jitterMultiplier));
|
|
18482
|
+
}
|
|
18483
|
+
function isAuthError(error2) {
|
|
18484
|
+
const message = error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : JSON.stringify(error2);
|
|
18485
|
+
return /\b(401|403|unauthorized|forbidden|authentication|auth|invalid api key|api key)\b/i.test(message);
|
|
18486
|
+
}
|
|
18300
18487
|
|
|
18301
18488
|
class SpecialistRunner {
|
|
18302
18489
|
deps;
|
|
@@ -18305,7 +18492,7 @@ class SpecialistRunner {
|
|
|
18305
18492
|
this.deps = deps;
|
|
18306
18493
|
this.sessionFactory = deps.sessionFactory ?? PiAgentSession.create.bind(PiAgentSession);
|
|
18307
18494
|
}
|
|
18308
|
-
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady) {
|
|
18495
|
+
async run(options, onProgress, onEvent, onMeta, onKillRegistered, onBeadCreated, onSteerRegistered, onResumeReady, onToolStartCallback, onToolEndCallback) {
|
|
18309
18496
|
const { loader, hooks, circuitBreaker, beadsClient } = this.deps;
|
|
18310
18497
|
const invocationId = crypto.randomUUID();
|
|
18311
18498
|
const start = Date.now();
|
|
@@ -18321,9 +18508,11 @@ class SpecialistRunner {
|
|
|
18321
18508
|
circuit_breaker_state: circuitBreaker.getState(model),
|
|
18322
18509
|
scope: "project"
|
|
18323
18510
|
});
|
|
18324
|
-
|
|
18511
|
+
const permissionLevel = options.autonomyLevel ?? execution.permission_required;
|
|
18512
|
+
const effectiveKeepAlive = options.noKeepAlive ? false : options.keepAlive ?? execution.interactive ?? false;
|
|
18513
|
+
validateBeforeRun(spec, permissionLevel);
|
|
18325
18514
|
const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
|
|
18326
|
-
const preResults = preScripts.map((s) => runScript(s.run)).filter((_, i) => preScripts[i].inject_output);
|
|
18515
|
+
const preResults = preScripts.map((s) => runScript(s.run ?? s.path)).filter((_, i) => preScripts[i].inject_output);
|
|
18327
18516
|
const preScriptOutput = formatScriptOutput(preResults);
|
|
18328
18517
|
const beadVariables = options.inputBeadId ? { bead_context: options.prompt, bead_id: options.inputBeadId } : {};
|
|
18329
18518
|
const variables = {
|
|
@@ -18341,7 +18530,20 @@ class SpecialistRunner {
|
|
|
18341
18530
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
18342
18531
|
system_prompt_present: !!prompt.system
|
|
18343
18532
|
});
|
|
18344
|
-
|
|
18533
|
+
let agentsMd = prompt.system ?? "";
|
|
18534
|
+
if (options.inputBeadId) {
|
|
18535
|
+
agentsMd += `
|
|
18536
|
+
|
|
18537
|
+
---
|
|
18538
|
+
## Specialist Run Context
|
|
18539
|
+
You are running as a specialist with bead ${options.inputBeadId} as your task.
|
|
18540
|
+
- Claim this bead directly: \`bd update ${options.inputBeadId} --claim\`
|
|
18541
|
+
- Do NOT create new beads or sub-issues — this bead IS your task.
|
|
18542
|
+
- Do NOT run \`bd create\` — the orchestrator manages issue tracking.
|
|
18543
|
+
- Close the bead when done: \`bd close ${options.inputBeadId} --reason="..."\`
|
|
18544
|
+
---
|
|
18545
|
+
`;
|
|
18546
|
+
}
|
|
18345
18547
|
const skillPaths = [];
|
|
18346
18548
|
if (prompt.skill_inherit)
|
|
18347
18549
|
skillPaths.push(prompt.skill_inherit);
|
|
@@ -18360,7 +18562,7 @@ ${skillPaths.map((p) => ` • ${p}`).join(`
|
|
|
18360
18562
|
}
|
|
18361
18563
|
if (preScripts.length > 0) {
|
|
18362
18564
|
onProgress?.(` pre scripts/commands:
|
|
18363
|
-
${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
|
|
18565
|
+
${preScripts.map((s) => ` • ${s.run ?? s.path ?? "<missing>"}${s.inject_output ? " → $pre_script_output" : ""}`).join(`
|
|
18364
18566
|
`)}
|
|
18365
18567
|
`);
|
|
18366
18568
|
}
|
|
@@ -18368,13 +18570,6 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18368
18570
|
|
|
18369
18571
|
`);
|
|
18370
18572
|
}
|
|
18371
|
-
const permissionLevel = options.autonomyLevel ?? execution.permission_required;
|
|
18372
|
-
await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
|
|
18373
|
-
backend: model,
|
|
18374
|
-
model,
|
|
18375
|
-
timeout_ms: execution.timeout_ms,
|
|
18376
|
-
permission_level: permissionLevel
|
|
18377
|
-
});
|
|
18378
18573
|
const beadsIntegration = spec.specialist.beads_integration ?? "auto";
|
|
18379
18574
|
let beadId;
|
|
18380
18575
|
let ownsBead = false;
|
|
@@ -18387,11 +18582,19 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18387
18582
|
onBeadCreated?.(beadId);
|
|
18388
18583
|
}
|
|
18389
18584
|
}
|
|
18585
|
+
await hooks.emit("pre_execute", invocationId, metadata.name, metadata.version, {
|
|
18586
|
+
backend: model,
|
|
18587
|
+
model,
|
|
18588
|
+
timeout_ms: execution.timeout_ms,
|
|
18589
|
+
permission_level: permissionLevel
|
|
18590
|
+
});
|
|
18390
18591
|
let output;
|
|
18391
18592
|
let sessionBackend = model;
|
|
18392
18593
|
let session;
|
|
18393
18594
|
let keepAliveActive = false;
|
|
18394
18595
|
let sessionClosed = false;
|
|
18596
|
+
const maxRetries = Math.max(0, Math.trunc(options.maxRetries ?? execution.max_retries ?? 0));
|
|
18597
|
+
const maxAttempts = maxRetries + 1;
|
|
18395
18598
|
try {
|
|
18396
18599
|
session = await this.sessionFactory({
|
|
18397
18600
|
model,
|
|
@@ -18399,25 +18602,50 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18399
18602
|
skillPaths: skillPaths.length > 0 ? skillPaths : undefined,
|
|
18400
18603
|
thinkingLevel: execution.thinking_level,
|
|
18401
18604
|
permissionLevel,
|
|
18605
|
+
stallTimeoutMs: execution.stall_timeout_ms,
|
|
18402
18606
|
cwd: process.cwd(),
|
|
18403
18607
|
onToken: (delta) => onProgress?.(delta),
|
|
18404
18608
|
onThinking: (delta) => onProgress?.(`\uD83D\uDCAD ${delta}`),
|
|
18405
|
-
onToolStart: (tool) =>
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18609
|
+
onToolStart: (tool, args, toolCallId) => {
|
|
18610
|
+
onProgress?.(`
|
|
18611
|
+
⚙ ${tool}…`);
|
|
18612
|
+
onToolStartCallback?.(tool, args, toolCallId);
|
|
18613
|
+
},
|
|
18614
|
+
onToolEnd: (tool, isError, toolCallId) => {
|
|
18615
|
+
onProgress?.(`✓
|
|
18616
|
+
`);
|
|
18617
|
+
onToolEndCallback?.(tool, isError, toolCallId);
|
|
18618
|
+
},
|
|
18409
18619
|
onEvent: (type) => onEvent?.(type),
|
|
18410
18620
|
onMeta: (meta) => onMeta?.(meta)
|
|
18411
18621
|
});
|
|
18412
18622
|
await session.start();
|
|
18413
18623
|
onKillRegistered?.(session.kill.bind(session));
|
|
18414
18624
|
onSteerRegistered?.((msg) => session.steer(msg));
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18625
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
18626
|
+
try {
|
|
18627
|
+
await session.prompt(renderedTask);
|
|
18628
|
+
await session.waitForDone(execution.timeout_ms);
|
|
18629
|
+
output = await session.getLastOutput();
|
|
18630
|
+
sessionBackend = session.meta.backend;
|
|
18631
|
+
break;
|
|
18632
|
+
} catch (err) {
|
|
18633
|
+
const shouldRetry = attempt < maxAttempts && !(err instanceof SessionKilledError) && !isAuthError(err) && isTransientError(err);
|
|
18634
|
+
if (!shouldRetry) {
|
|
18635
|
+
throw err;
|
|
18636
|
+
}
|
|
18637
|
+
const delayMs = getRetryDelayMs(attempt);
|
|
18638
|
+
onEvent?.("auto_retry");
|
|
18639
|
+
onProgress?.(`
|
|
18640
|
+
↻ transient backend error on attempt ${attempt}/${maxAttempts}; retrying in ${delayMs}ms
|
|
18641
|
+
`);
|
|
18642
|
+
await sleep(delayMs);
|
|
18643
|
+
}
|
|
18644
|
+
}
|
|
18645
|
+
if (output === undefined) {
|
|
18646
|
+
throw new Error("Specialist run finished without output");
|
|
18647
|
+
}
|
|
18648
|
+
if (effectiveKeepAlive && onResumeReady) {
|
|
18421
18649
|
keepAliveActive = true;
|
|
18422
18650
|
const resumeFn = async (msg) => {
|
|
18423
18651
|
await session.resume(msg, execution.timeout_ms);
|
|
@@ -18434,7 +18662,7 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18434
18662
|
}
|
|
18435
18663
|
const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
|
|
18436
18664
|
for (const script of postScripts)
|
|
18437
|
-
runScript(script.run);
|
|
18665
|
+
runScript(script.run ?? script.path);
|
|
18438
18666
|
circuitBreaker.recordSuccess(model);
|
|
18439
18667
|
} catch (err) {
|
|
18440
18668
|
const isCancelled = err instanceof SessionKilledError;
|
|
@@ -18469,8 +18697,6 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18469
18697
|
output_valid: true
|
|
18470
18698
|
});
|
|
18471
18699
|
if (beadId) {
|
|
18472
|
-
if (ownsBead)
|
|
18473
|
-
beadsClient?.closeBead(beadId, "COMPLETE", durationMs, model);
|
|
18474
18700
|
beadsClient?.auditBead(beadId, metadata.name, model, 0);
|
|
18475
18701
|
}
|
|
18476
18702
|
return {
|
|
@@ -18480,7 +18706,8 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18480
18706
|
durationMs,
|
|
18481
18707
|
specialistVersion: metadata.version,
|
|
18482
18708
|
promptHash,
|
|
18483
|
-
beadId
|
|
18709
|
+
beadId,
|
|
18710
|
+
permissionRequired: execution.permission_required
|
|
18484
18711
|
};
|
|
18485
18712
|
}
|
|
18486
18713
|
async startAsync(options, registry2) {
|
|
@@ -18499,9 +18726,16 @@ ${preScripts.map((s) => ` • ${s.run}${s.inject_output ? " → $pre_script_o
|
|
|
18499
18726
|
return jobId;
|
|
18500
18727
|
}
|
|
18501
18728
|
}
|
|
18729
|
+
var PERMISSION_GATED_TOOLS, RETRY_BASE_DELAY_MS = 1000, RETRY_MAX_JITTER = 0.2;
|
|
18502
18730
|
var init_runner = __esm(() => {
|
|
18503
18731
|
init_session();
|
|
18732
|
+
init_circuitBreaker();
|
|
18504
18733
|
init_beads();
|
|
18734
|
+
PERMISSION_GATED_TOOLS = {
|
|
18735
|
+
bash: ["LOW", "MEDIUM", "HIGH"],
|
|
18736
|
+
edit: ["MEDIUM", "HIGH"],
|
|
18737
|
+
write: ["HIGH"]
|
|
18738
|
+
};
|
|
18505
18739
|
});
|
|
18506
18740
|
|
|
18507
18741
|
// src/specialist/hooks.ts
|
|
@@ -18540,62 +18774,21 @@ class HookEmitter {
|
|
|
18540
18774
|
}
|
|
18541
18775
|
var init_hooks = () => {};
|
|
18542
18776
|
|
|
18543
|
-
// src/utils/circuitBreaker.ts
|
|
18544
|
-
class CircuitBreaker {
|
|
18545
|
-
states = new Map;
|
|
18546
|
-
threshold;
|
|
18547
|
-
cooldownMs;
|
|
18548
|
-
constructor(options = {}) {
|
|
18549
|
-
this.threshold = options.failureThreshold ?? 3;
|
|
18550
|
-
this.cooldownMs = options.cooldownMs ?? 60000;
|
|
18551
|
-
}
|
|
18552
|
-
getState(backend) {
|
|
18553
|
-
const entry = this.states.get(backend);
|
|
18554
|
-
if (!entry)
|
|
18555
|
-
return "CLOSED";
|
|
18556
|
-
if (entry.state === "OPEN" && Date.now() - entry.openedAt > this.cooldownMs) {
|
|
18557
|
-
entry.state = "HALF_OPEN";
|
|
18558
|
-
}
|
|
18559
|
-
return entry.state;
|
|
18560
|
-
}
|
|
18561
|
-
isAvailable(backend) {
|
|
18562
|
-
return this.getState(backend) !== "OPEN";
|
|
18563
|
-
}
|
|
18564
|
-
recordSuccess(backend) {
|
|
18565
|
-
this.states.set(backend, { state: "CLOSED", failures: 0 });
|
|
18566
|
-
}
|
|
18567
|
-
recordFailure(backend) {
|
|
18568
|
-
const entry = this.states.get(backend) ?? { state: "CLOSED", failures: 0 };
|
|
18569
|
-
entry.failures++;
|
|
18570
|
-
if (entry.failures >= this.threshold) {
|
|
18571
|
-
entry.state = "OPEN";
|
|
18572
|
-
entry.openedAt = Date.now();
|
|
18573
|
-
}
|
|
18574
|
-
this.states.set(backend, entry);
|
|
18575
|
-
}
|
|
18576
|
-
}
|
|
18577
|
-
|
|
18578
18777
|
// src/specialist/timeline-events.ts
|
|
18579
18778
|
function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
18580
18779
|
const t = Date.now();
|
|
18581
18780
|
switch (callbackEvent) {
|
|
18582
18781
|
case "thinking":
|
|
18583
18782
|
return { t, type: TIMELINE_EVENT_TYPES.THINKING };
|
|
18584
|
-
case "toolcall":
|
|
18585
|
-
return {
|
|
18586
|
-
t,
|
|
18587
|
-
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18588
|
-
tool: context.tool ?? "unknown",
|
|
18589
|
-
phase: "start",
|
|
18590
|
-
tool_call_id: context.toolCallId
|
|
18591
|
-
};
|
|
18592
18783
|
case "tool_execution_start":
|
|
18593
18784
|
return {
|
|
18594
18785
|
t,
|
|
18595
18786
|
type: TIMELINE_EVENT_TYPES.TOOL,
|
|
18596
18787
|
tool: context.tool ?? "unknown",
|
|
18597
18788
|
phase: "start",
|
|
18598
|
-
tool_call_id: context.toolCallId
|
|
18789
|
+
tool_call_id: context.toolCallId,
|
|
18790
|
+
args: context.args,
|
|
18791
|
+
started_at: new Date(t).toISOString()
|
|
18599
18792
|
};
|
|
18600
18793
|
case "tool_execution_update":
|
|
18601
18794
|
case "tool_execution":
|
|
@@ -18653,6 +18846,16 @@ function createMetaEvent(model, backend) {
|
|
|
18653
18846
|
backend
|
|
18654
18847
|
};
|
|
18655
18848
|
}
|
|
18849
|
+
function createStaleWarningEvent(reason, options) {
|
|
18850
|
+
return {
|
|
18851
|
+
t: Date.now(),
|
|
18852
|
+
type: TIMELINE_EVENT_TYPES.STALE_WARNING,
|
|
18853
|
+
reason,
|
|
18854
|
+
silence_ms: options.silence_ms,
|
|
18855
|
+
threshold_ms: options.threshold_ms,
|
|
18856
|
+
...options.tool !== undefined ? { tool: options.tool } : {}
|
|
18857
|
+
};
|
|
18858
|
+
}
|
|
18656
18859
|
function createRunCompleteEvent(status, elapsed_s, options) {
|
|
18657
18860
|
return {
|
|
18658
18861
|
t: Date.now(),
|
|
@@ -18710,6 +18913,7 @@ var init_timeline_events = __esm(() => {
|
|
|
18710
18913
|
MESSAGE: "message",
|
|
18711
18914
|
TURN: "turn",
|
|
18712
18915
|
RUN_COMPLETE: "run_complete",
|
|
18916
|
+
STALE_WARNING: "stale_warning",
|
|
18713
18917
|
DONE: "done",
|
|
18714
18918
|
AGENT_END: "agent_end"
|
|
18715
18919
|
};
|
|
@@ -18717,6 +18921,7 @@ var init_timeline_events = __esm(() => {
|
|
|
18717
18921
|
|
|
18718
18922
|
// src/specialist/supervisor.ts
|
|
18719
18923
|
import {
|
|
18924
|
+
appendFileSync,
|
|
18720
18925
|
closeSync,
|
|
18721
18926
|
existsSync as existsSync4,
|
|
18722
18927
|
fsyncSync,
|
|
@@ -18834,23 +19039,59 @@ class Supervisor {
|
|
|
18834
19039
|
crashRecovery() {
|
|
18835
19040
|
if (!existsSync4(this.opts.jobsDir))
|
|
18836
19041
|
return;
|
|
19042
|
+
const thresholds = {
|
|
19043
|
+
...STALL_DETECTION_DEFAULTS,
|
|
19044
|
+
...this.opts.stallDetection
|
|
19045
|
+
};
|
|
19046
|
+
const now = Date.now();
|
|
18837
19047
|
for (const entry of readdirSync(this.opts.jobsDir)) {
|
|
18838
19048
|
const statusPath = join3(this.opts.jobsDir, entry, "status.json");
|
|
18839
19049
|
if (!existsSync4(statusPath))
|
|
18840
19050
|
continue;
|
|
18841
19051
|
try {
|
|
18842
19052
|
const s = JSON.parse(readFileSync2(statusPath, "utf-8"));
|
|
18843
|
-
if (s.status
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
19053
|
+
if (s.status === "running" || s.status === "starting") {
|
|
19054
|
+
if (!s.pid)
|
|
19055
|
+
continue;
|
|
19056
|
+
let pidAlive = true;
|
|
19057
|
+
try {
|
|
19058
|
+
process.kill(s.pid, 0);
|
|
19059
|
+
} catch {
|
|
19060
|
+
pidAlive = false;
|
|
19061
|
+
}
|
|
19062
|
+
if (!pidAlive) {
|
|
19063
|
+
const tmp = statusPath + ".tmp";
|
|
19064
|
+
const updated = { ...s, status: "error", error: "Process crashed or was killed" };
|
|
19065
|
+
writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
19066
|
+
renameSync(tmp, statusPath);
|
|
19067
|
+
} else if (s.status === "running") {
|
|
19068
|
+
const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
|
|
19069
|
+
const silenceMs = now - lastEventAt;
|
|
19070
|
+
if (silenceMs > thresholds.running_silence_error_ms) {
|
|
19071
|
+
const tmp = statusPath + ".tmp";
|
|
19072
|
+
const updated = {
|
|
19073
|
+
...s,
|
|
19074
|
+
status: "error",
|
|
19075
|
+
error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
|
|
19076
|
+
};
|
|
19077
|
+
writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
19078
|
+
renameSync(tmp, statusPath);
|
|
19079
|
+
}
|
|
19080
|
+
}
|
|
19081
|
+
} else if (s.status === "waiting") {
|
|
19082
|
+
const lastEventAt = s.last_event_at_ms ?? s.started_at_ms;
|
|
19083
|
+
const silenceMs = now - lastEventAt;
|
|
19084
|
+
if (silenceMs > thresholds.waiting_stale_ms) {
|
|
19085
|
+
const eventsPath = join3(this.opts.jobsDir, entry, "events.jsonl");
|
|
19086
|
+
const event = createStaleWarningEvent("waiting_stale", {
|
|
19087
|
+
silence_ms: silenceMs,
|
|
19088
|
+
threshold_ms: thresholds.waiting_stale_ms
|
|
19089
|
+
});
|
|
19090
|
+
try {
|
|
19091
|
+
appendFileSync(eventsPath, JSON.stringify(event) + `
|
|
19092
|
+
`);
|
|
19093
|
+
} catch {}
|
|
19094
|
+
}
|
|
18854
19095
|
}
|
|
18855
19096
|
} catch {}
|
|
18856
19097
|
}
|
|
@@ -18869,7 +19110,8 @@ class Supervisor {
|
|
|
18869
19110
|
specialist: runOptions.name,
|
|
18870
19111
|
status: "starting",
|
|
18871
19112
|
started_at_ms: startedAtMs,
|
|
18872
|
-
pid: process.pid
|
|
19113
|
+
pid: process.pid,
|
|
19114
|
+
...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {}
|
|
18873
19115
|
};
|
|
18874
19116
|
this.writeStatusFile(id, initialStatus);
|
|
18875
19117
|
writeFileSync(join3(this.opts.jobsDir, "latest"), `${id}
|
|
@@ -18889,24 +19131,69 @@ class Supervisor {
|
|
|
18889
19131
|
console.error(`[supervisor] Failed to write event: ${err?.message ?? err}`);
|
|
18890
19132
|
}
|
|
18891
19133
|
};
|
|
18892
|
-
appendTimelineEvent(createRunStartEvent(runOptions.name));
|
|
19134
|
+
appendTimelineEvent(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
|
|
18893
19135
|
const fifoPath = join3(dir, "steer.pipe");
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
setStatus({ fifo_path: fifoPath });
|
|
18899
|
-
} catch {}
|
|
18900
|
-
}
|
|
19136
|
+
try {
|
|
19137
|
+
execFileSync("mkfifo", [fifoPath]);
|
|
19138
|
+
setStatus({ fifo_path: fifoPath });
|
|
19139
|
+
} catch {}
|
|
18901
19140
|
let textLogged = false;
|
|
18902
19141
|
let currentTool = "";
|
|
18903
19142
|
let currentToolCallId = "";
|
|
19143
|
+
let currentToolArgs;
|
|
19144
|
+
let currentToolIsError = false;
|
|
19145
|
+
const activeToolCalls = new Map;
|
|
18904
19146
|
let killFn;
|
|
18905
19147
|
let steerFn;
|
|
18906
19148
|
let resumeFn;
|
|
18907
19149
|
let closeFn;
|
|
18908
19150
|
let fifoReadStream;
|
|
18909
19151
|
let fifoReadline;
|
|
19152
|
+
const thresholds = {
|
|
19153
|
+
...STALL_DETECTION_DEFAULTS,
|
|
19154
|
+
...this.opts.stallDetection
|
|
19155
|
+
};
|
|
19156
|
+
let lastActivityMs = startedAtMs;
|
|
19157
|
+
let silenceWarnEmitted = false;
|
|
19158
|
+
let toolStartMs;
|
|
19159
|
+
let toolDurationWarnEmitted = false;
|
|
19160
|
+
let stuckIntervalId;
|
|
19161
|
+
stuckIntervalId = setInterval(() => {
|
|
19162
|
+
const now = Date.now();
|
|
19163
|
+
if (statusSnapshot.status === "running") {
|
|
19164
|
+
const silenceMs = now - lastActivityMs;
|
|
19165
|
+
if (!silenceWarnEmitted && silenceMs > thresholds.running_silence_warn_ms) {
|
|
19166
|
+
silenceWarnEmitted = true;
|
|
19167
|
+
appendTimelineEvent(createStaleWarningEvent("running_silence", {
|
|
19168
|
+
silence_ms: silenceMs,
|
|
19169
|
+
threshold_ms: thresholds.running_silence_warn_ms
|
|
19170
|
+
}));
|
|
19171
|
+
}
|
|
19172
|
+
if (silenceMs > thresholds.running_silence_error_ms) {
|
|
19173
|
+
appendTimelineEvent(createStaleWarningEvent("running_silence_error", {
|
|
19174
|
+
silence_ms: silenceMs,
|
|
19175
|
+
threshold_ms: thresholds.running_silence_error_ms
|
|
19176
|
+
}));
|
|
19177
|
+
setStatus({
|
|
19178
|
+
status: "error",
|
|
19179
|
+
error: `No activity for ${Math.round(silenceMs / 1000)}s (threshold: ${thresholds.running_silence_error_ms / 1000}s)`
|
|
19180
|
+
});
|
|
19181
|
+
killFn?.();
|
|
19182
|
+
clearInterval(stuckIntervalId);
|
|
19183
|
+
}
|
|
19184
|
+
}
|
|
19185
|
+
if (toolStartMs !== undefined && !toolDurationWarnEmitted) {
|
|
19186
|
+
const toolDurationMs = now - toolStartMs;
|
|
19187
|
+
if (toolDurationMs > thresholds.tool_duration_warn_ms) {
|
|
19188
|
+
toolDurationWarnEmitted = true;
|
|
19189
|
+
appendTimelineEvent(createStaleWarningEvent("tool_duration", {
|
|
19190
|
+
silence_ms: toolDurationMs,
|
|
19191
|
+
threshold_ms: thresholds.tool_duration_warn_ms,
|
|
19192
|
+
tool: currentTool
|
|
19193
|
+
}));
|
|
19194
|
+
}
|
|
19195
|
+
}
|
|
19196
|
+
}, 1e4);
|
|
18910
19197
|
const sigtermHandler = () => killFn?.();
|
|
18911
19198
|
process.once("SIGTERM", sigtermHandler);
|
|
18912
19199
|
try {
|
|
@@ -18919,6 +19206,8 @@ class Supervisor {
|
|
|
18919
19206
|
this.opts.onProgress?.(delta);
|
|
18920
19207
|
}, (eventType) => {
|
|
18921
19208
|
const now = Date.now();
|
|
19209
|
+
lastActivityMs = now;
|
|
19210
|
+
silenceWarnEmitted = false;
|
|
18922
19211
|
setStatus({
|
|
18923
19212
|
status: "running",
|
|
18924
19213
|
current_event: eventType,
|
|
@@ -18927,7 +19216,9 @@ class Supervisor {
|
|
|
18927
19216
|
});
|
|
18928
19217
|
const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
|
|
18929
19218
|
tool: currentTool,
|
|
18930
|
-
toolCallId: currentToolCallId || undefined
|
|
19219
|
+
toolCallId: currentToolCallId || undefined,
|
|
19220
|
+
args: currentToolArgs,
|
|
19221
|
+
isError: currentToolIsError
|
|
18931
19222
|
});
|
|
18932
19223
|
if (timelineEvent) {
|
|
18933
19224
|
appendTimelineEvent(timelineEvent);
|
|
@@ -18945,7 +19236,7 @@ class Supervisor {
|
|
|
18945
19236
|
setStatus({ bead_id: beadId });
|
|
18946
19237
|
}, (fn) => {
|
|
18947
19238
|
steerFn = fn;
|
|
18948
|
-
if (!
|
|
19239
|
+
if (!existsSync4(fifoPath))
|
|
18949
19240
|
return;
|
|
18950
19241
|
fifoReadStream = createReadStream(fifoPath, { flags: "r+" });
|
|
18951
19242
|
fifoReadline = createInterface({ input: fifoReadStream });
|
|
@@ -18954,7 +19245,24 @@ class Supervisor {
|
|
|
18954
19245
|
const parsed = JSON.parse(line);
|
|
18955
19246
|
if (parsed?.type === "steer" && typeof parsed.message === "string") {
|
|
18956
19247
|
steerFn?.(parsed.message).catch(() => {});
|
|
19248
|
+
} else if (parsed?.type === "resume" && typeof parsed.task === "string") {
|
|
19249
|
+
if (resumeFn) {
|
|
19250
|
+
setStatus({ status: "running", current_event: "starting" });
|
|
19251
|
+
resumeFn(parsed.task).then((output) => {
|
|
19252
|
+
mkdirSync(this.jobDir(id), { recursive: true });
|
|
19253
|
+
writeFileSync(this.resultPath(id), output, "utf-8");
|
|
19254
|
+
setStatus({
|
|
19255
|
+
status: "waiting",
|
|
19256
|
+
current_event: "waiting",
|
|
19257
|
+
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
19258
|
+
last_event_at_ms: Date.now()
|
|
19259
|
+
});
|
|
19260
|
+
}).catch((err) => {
|
|
19261
|
+
setStatus({ status: "error", error: err?.message ?? String(err) });
|
|
19262
|
+
});
|
|
19263
|
+
}
|
|
18957
19264
|
} else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
|
|
19265
|
+
console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
|
|
18958
19266
|
if (resumeFn) {
|
|
18959
19267
|
setStatus({ status: "running", current_event: "starting" });
|
|
18960
19268
|
resumeFn(parsed.message).then((output) => {
|
|
@@ -18980,12 +19288,51 @@ class Supervisor {
|
|
|
18980
19288
|
resumeFn = rFn;
|
|
18981
19289
|
closeFn = cFn;
|
|
18982
19290
|
setStatus({ status: "waiting", current_event: "waiting" });
|
|
19291
|
+
}, (tool, args, toolCallId) => {
|
|
19292
|
+
currentTool = tool;
|
|
19293
|
+
currentToolArgs = args;
|
|
19294
|
+
currentToolCallId = toolCallId ?? "";
|
|
19295
|
+
currentToolIsError = false;
|
|
19296
|
+
toolStartMs = Date.now();
|
|
19297
|
+
toolDurationWarnEmitted = false;
|
|
19298
|
+
setStatus({ current_tool: tool });
|
|
19299
|
+
if (toolCallId) {
|
|
19300
|
+
activeToolCalls.set(toolCallId, { tool, args });
|
|
19301
|
+
}
|
|
19302
|
+
}, (tool, isError, toolCallId) => {
|
|
19303
|
+
if (toolCallId && activeToolCalls.has(toolCallId)) {
|
|
19304
|
+
const entry = activeToolCalls.get(toolCallId);
|
|
19305
|
+
currentTool = entry.tool;
|
|
19306
|
+
currentToolArgs = entry.args;
|
|
19307
|
+
currentToolCallId = toolCallId;
|
|
19308
|
+
activeToolCalls.delete(toolCallId);
|
|
19309
|
+
} else {
|
|
19310
|
+
currentTool = tool;
|
|
19311
|
+
}
|
|
19312
|
+
currentToolIsError = isError;
|
|
19313
|
+
toolStartMs = undefined;
|
|
19314
|
+
toolDurationWarnEmitted = false;
|
|
18983
19315
|
});
|
|
18984
19316
|
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
18985
19317
|
mkdirSync(this.jobDir(id), { recursive: true });
|
|
18986
19318
|
writeFileSync(this.resultPath(id), result.output, "utf-8");
|
|
18987
|
-
|
|
19319
|
+
const inputBeadId = runOptions.inputBeadId;
|
|
19320
|
+
const ownsBead = Boolean(result.beadId && !inputBeadId);
|
|
19321
|
+
const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
|
|
19322
|
+
const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && result.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
|
|
19323
|
+
if (ownsBead && result.beadId) {
|
|
18988
19324
|
this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
|
|
19325
|
+
} else if (shouldWriteExternalBeadNotes) {
|
|
19326
|
+
if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
|
|
19327
|
+
this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(result));
|
|
19328
|
+
} else if (result.beadId) {
|
|
19329
|
+
this.opts.beadsClient?.updateBeadNotes(result.beadId, formatBeadNotes(result));
|
|
19330
|
+
}
|
|
19331
|
+
}
|
|
19332
|
+
if (result.beadId) {
|
|
19333
|
+
if (!inputBeadId) {
|
|
19334
|
+
this.opts.beadsClient?.closeBead(result.beadId, "COMPLETE", result.durationMs, result.model);
|
|
19335
|
+
}
|
|
18989
19336
|
}
|
|
18990
19337
|
setStatus({
|
|
18991
19338
|
status: "done",
|
|
@@ -18998,7 +19345,8 @@ class Supervisor {
|
|
|
18998
19345
|
appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
|
|
18999
19346
|
model: result.model,
|
|
19000
19347
|
backend: result.backend,
|
|
19001
|
-
bead_id: result.beadId
|
|
19348
|
+
bead_id: result.beadId,
|
|
19349
|
+
output: result.output
|
|
19002
19350
|
}));
|
|
19003
19351
|
mkdirSync(this.readyDir(), { recursive: true });
|
|
19004
19352
|
writeFileSync(join3(this.readyDir(), id), "", "utf-8");
|
|
@@ -19016,6 +19364,8 @@ class Supervisor {
|
|
|
19016
19364
|
}));
|
|
19017
19365
|
throw err;
|
|
19018
19366
|
} finally {
|
|
19367
|
+
if (stuckIntervalId !== undefined)
|
|
19368
|
+
clearInterval(stuckIntervalId);
|
|
19019
19369
|
process.removeListener("SIGTERM", sigtermHandler);
|
|
19020
19370
|
try {
|
|
19021
19371
|
fifoReadline?.close();
|
|
@@ -19034,52 +19384,184 @@ class Supervisor {
|
|
|
19034
19384
|
}
|
|
19035
19385
|
}
|
|
19036
19386
|
}
|
|
19037
|
-
var JOB_TTL_DAYS;
|
|
19387
|
+
var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS;
|
|
19038
19388
|
var init_supervisor = __esm(() => {
|
|
19039
19389
|
init_timeline_events();
|
|
19040
19390
|
JOB_TTL_DAYS = Number(process.env.SPECIALISTS_JOB_TTL_DAYS ?? 7);
|
|
19391
|
+
STALL_DETECTION_DEFAULTS = {
|
|
19392
|
+
running_silence_warn_ms: 60000,
|
|
19393
|
+
running_silence_error_ms: 300000,
|
|
19394
|
+
waiting_stale_ms: 3600000,
|
|
19395
|
+
tool_duration_warn_ms: 120000
|
|
19396
|
+
};
|
|
19041
19397
|
});
|
|
19042
19398
|
|
|
19043
|
-
// src/
|
|
19044
|
-
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19399
|
+
// src/specialist/timeline-query.ts
|
|
19400
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
19401
|
+
import { join as join8 } from "node:path";
|
|
19402
|
+
function readJobEvents(jobDir) {
|
|
19403
|
+
const eventsPath = join8(jobDir, "events.jsonl");
|
|
19404
|
+
if (!existsSync5(eventsPath))
|
|
19405
|
+
return [];
|
|
19406
|
+
const content = readFileSync3(eventsPath, "utf-8");
|
|
19407
|
+
const lines = content.split(`
|
|
19408
|
+
`).filter(Boolean);
|
|
19409
|
+
const events = [];
|
|
19410
|
+
for (const line of lines) {
|
|
19411
|
+
const event = parseTimelineEvent(line);
|
|
19412
|
+
if (event)
|
|
19413
|
+
events.push(event);
|
|
19414
|
+
}
|
|
19415
|
+
events.sort(compareTimelineEvents);
|
|
19416
|
+
return events;
|
|
19061
19417
|
}
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19075
|
-
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19418
|
+
function readJobEventsById(jobsDir, jobId) {
|
|
19419
|
+
return readJobEvents(join8(jobsDir, jobId));
|
|
19420
|
+
}
|
|
19421
|
+
function readAllJobEvents(jobsDir) {
|
|
19422
|
+
if (!existsSync5(jobsDir))
|
|
19423
|
+
return [];
|
|
19424
|
+
const batches = [];
|
|
19425
|
+
const entries = readdirSync2(jobsDir);
|
|
19426
|
+
for (const entry of entries) {
|
|
19427
|
+
const jobDir = join8(jobsDir, entry);
|
|
19428
|
+
try {
|
|
19429
|
+
const stat2 = __require("node:fs").statSync(jobDir);
|
|
19430
|
+
if (!stat2.isDirectory())
|
|
19431
|
+
continue;
|
|
19432
|
+
} catch {
|
|
19433
|
+
continue;
|
|
19434
|
+
}
|
|
19435
|
+
const jobId = entry;
|
|
19436
|
+
const statusPath = join8(jobDir, "status.json");
|
|
19437
|
+
let specialist = "unknown";
|
|
19438
|
+
let beadId;
|
|
19439
|
+
if (existsSync5(statusPath)) {
|
|
19440
|
+
try {
|
|
19441
|
+
const status = JSON.parse(readFileSync3(statusPath, "utf-8"));
|
|
19442
|
+
specialist = status.specialist ?? "unknown";
|
|
19443
|
+
beadId = status.bead_id;
|
|
19444
|
+
} catch {}
|
|
19445
|
+
}
|
|
19446
|
+
const events = readJobEvents(jobDir);
|
|
19447
|
+
if (events.length > 0) {
|
|
19448
|
+
batches.push({ jobId, specialist, beadId, events });
|
|
19449
|
+
}
|
|
19450
|
+
}
|
|
19451
|
+
return batches;
|
|
19452
|
+
}
|
|
19453
|
+
function mergeTimelineEvents(batches) {
|
|
19454
|
+
const merged = [];
|
|
19455
|
+
for (const batch of batches) {
|
|
19456
|
+
for (const event of batch.events) {
|
|
19457
|
+
merged.push({
|
|
19458
|
+
jobId: batch.jobId,
|
|
19459
|
+
specialist: batch.specialist,
|
|
19460
|
+
beadId: batch.beadId,
|
|
19461
|
+
event
|
|
19462
|
+
});
|
|
19463
|
+
}
|
|
19464
|
+
}
|
|
19465
|
+
merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
|
|
19466
|
+
return merged;
|
|
19467
|
+
}
|
|
19468
|
+
function filterTimelineEvents(merged, filter) {
|
|
19469
|
+
let result = merged;
|
|
19470
|
+
if (filter.since !== undefined) {
|
|
19471
|
+
result = result.filter(({ event }) => event.t >= filter.since);
|
|
19472
|
+
}
|
|
19473
|
+
if (filter.jobId !== undefined) {
|
|
19474
|
+
result = result.filter(({ jobId }) => jobId === filter.jobId);
|
|
19475
|
+
}
|
|
19476
|
+
if (filter.specialist !== undefined) {
|
|
19477
|
+
result = result.filter(({ specialist }) => specialist === filter.specialist);
|
|
19478
|
+
}
|
|
19479
|
+
if (filter.limit !== undefined && filter.limit > 0) {
|
|
19480
|
+
result = result.slice(0, filter.limit);
|
|
19481
|
+
}
|
|
19482
|
+
return result;
|
|
19483
|
+
}
|
|
19484
|
+
function queryTimeline(jobsDir, filter = {}) {
|
|
19485
|
+
let batches = readAllJobEvents(jobsDir);
|
|
19486
|
+
if (filter.jobId !== undefined) {
|
|
19487
|
+
batches = batches.filter((b) => b.jobId === filter.jobId);
|
|
19488
|
+
}
|
|
19489
|
+
if (filter.specialist !== undefined) {
|
|
19490
|
+
batches = batches.filter((b) => b.specialist === filter.specialist);
|
|
19491
|
+
}
|
|
19492
|
+
const merged = mergeTimelineEvents(batches);
|
|
19493
|
+
return filterTimelineEvents(merged, filter);
|
|
19494
|
+
}
|
|
19495
|
+
function isJobComplete(events) {
|
|
19496
|
+
return events.some((e) => e.type === "run_complete");
|
|
19497
|
+
}
|
|
19498
|
+
var init_timeline_query = __esm(() => {
|
|
19499
|
+
init_timeline_events();
|
|
19500
|
+
});
|
|
19501
|
+
|
|
19502
|
+
// src/specialist/model-display.ts
|
|
19503
|
+
function extractModelId(model) {
|
|
19504
|
+
if (!model)
|
|
19505
|
+
return;
|
|
19506
|
+
const trimmed = model.trim();
|
|
19507
|
+
if (!trimmed)
|
|
19508
|
+
return;
|
|
19509
|
+
return trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
|
|
19510
|
+
}
|
|
19511
|
+
function toModelAlias(model) {
|
|
19512
|
+
const modelId = extractModelId(model);
|
|
19513
|
+
if (!modelId)
|
|
19514
|
+
return;
|
|
19515
|
+
if (modelId.startsWith("claude-")) {
|
|
19516
|
+
return modelId.slice("claude-".length);
|
|
19517
|
+
}
|
|
19518
|
+
return modelId;
|
|
19519
|
+
}
|
|
19520
|
+
function formatSpecialistModel(specialist, model) {
|
|
19521
|
+
const alias = toModelAlias(model);
|
|
19522
|
+
return alias ? `${specialist}/${alias}` : specialist;
|
|
19523
|
+
}
|
|
19524
|
+
|
|
19525
|
+
// src/cli/install.ts
|
|
19526
|
+
var exports_install = {};
|
|
19527
|
+
__export(exports_install, {
|
|
19528
|
+
run: () => run
|
|
19529
|
+
});
|
|
19530
|
+
async function run() {
|
|
19531
|
+
console.log("");
|
|
19532
|
+
console.log(yellow("⚠ DEPRECATED: `specialists install` is deprecated"));
|
|
19533
|
+
console.log("");
|
|
19534
|
+
console.log(` Use ${bold("specialists init")} instead.`);
|
|
19535
|
+
console.log("");
|
|
19536
|
+
console.log(" The init command:");
|
|
19537
|
+
console.log(" • creates specialists/ and .specialists/ directories");
|
|
19538
|
+
console.log(" • registers the MCP server in .mcp.json");
|
|
19539
|
+
console.log(" • injects workflow context into AGENTS.md/CLAUDE.md");
|
|
19540
|
+
console.log("");
|
|
19541
|
+
console.log(` ${dim("Run: specialists init --help for full details")}`);
|
|
19542
|
+
console.log("");
|
|
19543
|
+
}
|
|
19544
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`, yellow = (s) => `\x1B[33m${s}\x1B[0m`, dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
19545
|
+
|
|
19546
|
+
// src/cli/version.ts
|
|
19547
|
+
var exports_version = {};
|
|
19548
|
+
__export(exports_version, {
|
|
19549
|
+
run: () => run2
|
|
19550
|
+
});
|
|
19551
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
19552
|
+
import { fileURLToPath } from "node:url";
|
|
19553
|
+
import { dirname as dirname2, join as join12 } from "node:path";
|
|
19554
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
19555
|
+
async function run2() {
|
|
19556
|
+
const req = createRequire2(import.meta.url);
|
|
19557
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
19558
|
+
const bundlePkgPath = join12(here, "..", "package.json");
|
|
19559
|
+
const sourcePkgPath = join12(here, "..", "..", "package.json");
|
|
19560
|
+
let pkg;
|
|
19561
|
+
if (existsSync8(bundlePkgPath)) {
|
|
19562
|
+
pkg = req("../package.json");
|
|
19563
|
+
} else if (existsSync8(sourcePkgPath)) {
|
|
19564
|
+
pkg = req("../../package.json");
|
|
19083
19565
|
} else {
|
|
19084
19566
|
console.error("Cannot find package.json");
|
|
19085
19567
|
process.exit(1);
|
|
@@ -19281,8 +19763,8 @@ var exports_init = {};
|
|
|
19281
19763
|
__export(exports_init, {
|
|
19282
19764
|
run: () => run5
|
|
19283
19765
|
});
|
|
19284
|
-
import { copyFileSync, cpSync, existsSync as
|
|
19285
|
-
import { join as
|
|
19766
|
+
import { copyFileSync, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
19767
|
+
import { join as join13 } from "node:path";
|
|
19286
19768
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19287
19769
|
function ok(msg) {
|
|
19288
19770
|
console.log(` ${green3("✓")} ${msg}`);
|
|
@@ -19291,10 +19773,10 @@ function skip(msg) {
|
|
|
19291
19773
|
console.log(` ${yellow4("○")} ${msg}`);
|
|
19292
19774
|
}
|
|
19293
19775
|
function loadJson(path, fallback) {
|
|
19294
|
-
if (!
|
|
19776
|
+
if (!existsSync9(path))
|
|
19295
19777
|
return structuredClone(fallback);
|
|
19296
19778
|
try {
|
|
19297
|
-
return JSON.parse(
|
|
19779
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
19298
19780
|
} catch {
|
|
19299
19781
|
return structuredClone(fallback);
|
|
19300
19782
|
}
|
|
@@ -19306,34 +19788,64 @@ function saveJson(path, value) {
|
|
|
19306
19788
|
function resolvePackagePath(relativePath) {
|
|
19307
19789
|
const configPath = `config/${relativePath}`;
|
|
19308
19790
|
let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
|
|
19309
|
-
if (
|
|
19791
|
+
if (existsSync9(resolved))
|
|
19310
19792
|
return resolved;
|
|
19311
19793
|
resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
|
|
19312
|
-
if (
|
|
19794
|
+
if (existsSync9(resolved))
|
|
19313
19795
|
return resolved;
|
|
19314
19796
|
return null;
|
|
19315
19797
|
}
|
|
19798
|
+
function migrateLegacySpecialists(cwd, scope) {
|
|
19799
|
+
const sourceDir = join13(cwd, ".specialists", scope, "specialists");
|
|
19800
|
+
if (!existsSync9(sourceDir))
|
|
19801
|
+
return;
|
|
19802
|
+
const targetDir = join13(cwd, ".specialists", scope);
|
|
19803
|
+
if (!existsSync9(targetDir)) {
|
|
19804
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
19805
|
+
}
|
|
19806
|
+
const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
|
|
19807
|
+
if (files.length === 0)
|
|
19808
|
+
return;
|
|
19809
|
+
let moved = 0;
|
|
19810
|
+
let skipped = 0;
|
|
19811
|
+
for (const file of files) {
|
|
19812
|
+
const src = join13(sourceDir, file);
|
|
19813
|
+
const dest = join13(targetDir, file);
|
|
19814
|
+
if (existsSync9(dest)) {
|
|
19815
|
+
skipped++;
|
|
19816
|
+
continue;
|
|
19817
|
+
}
|
|
19818
|
+
renameSync2(src, dest);
|
|
19819
|
+
moved++;
|
|
19820
|
+
}
|
|
19821
|
+
if (moved > 0) {
|
|
19822
|
+
ok(`migrated ${moved} specialist${moved === 1 ? "" : "s"} from .specialists/${scope}/specialists/ to .specialists/${scope}/`);
|
|
19823
|
+
}
|
|
19824
|
+
if (skipped > 0) {
|
|
19825
|
+
skip(`${skipped} legacy specialist${skipped === 1 ? "" : "s"} already exist in .specialists/${scope}/ (not moved)`);
|
|
19826
|
+
}
|
|
19827
|
+
}
|
|
19316
19828
|
function copyCanonicalSpecialists(cwd) {
|
|
19317
19829
|
const sourceDir = resolvePackagePath("specialists");
|
|
19318
19830
|
if (!sourceDir) {
|
|
19319
19831
|
skip("no canonical specialists found in package");
|
|
19320
19832
|
return;
|
|
19321
19833
|
}
|
|
19322
|
-
const targetDir =
|
|
19323
|
-
const files =
|
|
19834
|
+
const targetDir = join13(cwd, ".specialists", "default");
|
|
19835
|
+
const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
|
|
19324
19836
|
if (files.length === 0) {
|
|
19325
19837
|
skip("no specialist files found in package");
|
|
19326
19838
|
return;
|
|
19327
19839
|
}
|
|
19328
|
-
if (!
|
|
19840
|
+
if (!existsSync9(targetDir)) {
|
|
19329
19841
|
mkdirSync2(targetDir, { recursive: true });
|
|
19330
19842
|
}
|
|
19331
19843
|
let copied = 0;
|
|
19332
19844
|
let skipped = 0;
|
|
19333
19845
|
for (const file of files) {
|
|
19334
|
-
const src =
|
|
19335
|
-
const dest =
|
|
19336
|
-
if (
|
|
19846
|
+
const src = join13(sourceDir, file);
|
|
19847
|
+
const dest = join13(targetDir, file);
|
|
19848
|
+
if (existsSync9(dest)) {
|
|
19337
19849
|
skipped++;
|
|
19338
19850
|
} else {
|
|
19339
19851
|
copyFileSync(src, dest);
|
|
@@ -19341,7 +19853,7 @@ function copyCanonicalSpecialists(cwd) {
|
|
|
19341
19853
|
}
|
|
19342
19854
|
}
|
|
19343
19855
|
if (copied > 0) {
|
|
19344
|
-
ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default
|
|
19856
|
+
ok(`copied ${copied} canonical specialist${copied === 1 ? "" : "s"} to .specialists/default/`);
|
|
19345
19857
|
}
|
|
19346
19858
|
if (skipped > 0) {
|
|
19347
19859
|
skip(`${skipped} specialist${skipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
@@ -19353,21 +19865,21 @@ function installProjectHooks(cwd) {
|
|
|
19353
19865
|
skip("no canonical hooks found in package");
|
|
19354
19866
|
return;
|
|
19355
19867
|
}
|
|
19356
|
-
const targetDir =
|
|
19357
|
-
const hooks =
|
|
19868
|
+
const targetDir = join13(cwd, ".claude", "hooks");
|
|
19869
|
+
const hooks = readdirSync3(sourceDir).filter((f) => f.endsWith(".mjs"));
|
|
19358
19870
|
if (hooks.length === 0) {
|
|
19359
19871
|
skip("no hook files found in package");
|
|
19360
19872
|
return;
|
|
19361
19873
|
}
|
|
19362
|
-
if (!
|
|
19874
|
+
if (!existsSync9(targetDir)) {
|
|
19363
19875
|
mkdirSync2(targetDir, { recursive: true });
|
|
19364
19876
|
}
|
|
19365
19877
|
let copied = 0;
|
|
19366
19878
|
let skipped = 0;
|
|
19367
19879
|
for (const file of hooks) {
|
|
19368
|
-
const src =
|
|
19369
|
-
const dest =
|
|
19370
|
-
if (
|
|
19880
|
+
const src = join13(sourceDir, file);
|
|
19881
|
+
const dest = join13(targetDir, file);
|
|
19882
|
+
if (existsSync9(dest)) {
|
|
19371
19883
|
skipped++;
|
|
19372
19884
|
} else {
|
|
19373
19885
|
copyFileSync(src, dest);
|
|
@@ -19382,9 +19894,9 @@ function installProjectHooks(cwd) {
|
|
|
19382
19894
|
}
|
|
19383
19895
|
}
|
|
19384
19896
|
function ensureProjectHookWiring(cwd) {
|
|
19385
|
-
const settingsPath =
|
|
19386
|
-
const settingsDir =
|
|
19387
|
-
if (!
|
|
19897
|
+
const settingsPath = join13(cwd, ".claude", "settings.json");
|
|
19898
|
+
const settingsDir = join13(cwd, ".claude");
|
|
19899
|
+
if (!existsSync9(settingsDir)) {
|
|
19388
19900
|
mkdirSync2(settingsDir, { recursive: true });
|
|
19389
19901
|
}
|
|
19390
19902
|
const settings = loadJson(settingsPath, {});
|
|
@@ -19413,25 +19925,25 @@ function installProjectSkills(cwd) {
|
|
|
19413
19925
|
skip("no canonical skills found in package");
|
|
19414
19926
|
return;
|
|
19415
19927
|
}
|
|
19416
|
-
const skills =
|
|
19928
|
+
const skills = readdirSync3(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
19417
19929
|
if (skills.length === 0) {
|
|
19418
19930
|
skip("no skill directories found in package");
|
|
19419
19931
|
return;
|
|
19420
19932
|
}
|
|
19421
19933
|
const targetDirs = [
|
|
19422
|
-
|
|
19423
|
-
|
|
19934
|
+
join13(cwd, ".claude", "skills"),
|
|
19935
|
+
join13(cwd, ".pi", "skills")
|
|
19424
19936
|
];
|
|
19425
19937
|
let totalCopied = 0;
|
|
19426
19938
|
let totalSkipped = 0;
|
|
19427
19939
|
for (const targetDir of targetDirs) {
|
|
19428
|
-
if (!
|
|
19940
|
+
if (!existsSync9(targetDir)) {
|
|
19429
19941
|
mkdirSync2(targetDir, { recursive: true });
|
|
19430
19942
|
}
|
|
19431
19943
|
for (const skill of skills) {
|
|
19432
|
-
const src =
|
|
19433
|
-
const dest =
|
|
19434
|
-
if (
|
|
19944
|
+
const src = join13(sourceDir, skill);
|
|
19945
|
+
const dest = join13(targetDir, skill);
|
|
19946
|
+
if (existsSync9(dest)) {
|
|
19435
19947
|
totalSkipped++;
|
|
19436
19948
|
} else {
|
|
19437
19949
|
cpSync(src, dest, { recursive: true });
|
|
@@ -19447,20 +19959,20 @@ function installProjectSkills(cwd) {
|
|
|
19447
19959
|
}
|
|
19448
19960
|
}
|
|
19449
19961
|
function createUserDirs(cwd) {
|
|
19450
|
-
const userDir =
|
|
19451
|
-
if (!
|
|
19962
|
+
const userDir = join13(cwd, ".specialists", "user");
|
|
19963
|
+
if (!existsSync9(userDir)) {
|
|
19452
19964
|
mkdirSync2(userDir, { recursive: true });
|
|
19453
|
-
ok("created .specialists/user/
|
|
19965
|
+
ok("created .specialists/user/ for custom specialists");
|
|
19454
19966
|
}
|
|
19455
19967
|
}
|
|
19456
19968
|
function createRuntimeDirs(cwd) {
|
|
19457
19969
|
const runtimeDirs = [
|
|
19458
|
-
|
|
19459
|
-
|
|
19970
|
+
join13(cwd, ".specialists", "jobs"),
|
|
19971
|
+
join13(cwd, ".specialists", "ready")
|
|
19460
19972
|
];
|
|
19461
19973
|
let created = 0;
|
|
19462
19974
|
for (const dir of runtimeDirs) {
|
|
19463
|
-
if (!
|
|
19975
|
+
if (!existsSync9(dir)) {
|
|
19464
19976
|
mkdirSync2(dir, { recursive: true });
|
|
19465
19977
|
created++;
|
|
19466
19978
|
}
|
|
@@ -19470,7 +19982,7 @@ function createRuntimeDirs(cwd) {
|
|
|
19470
19982
|
}
|
|
19471
19983
|
}
|
|
19472
19984
|
function ensureProjectMcp(cwd) {
|
|
19473
|
-
const mcpPath =
|
|
19985
|
+
const mcpPath = join13(cwd, MCP_FILE);
|
|
19474
19986
|
const mcp = loadJson(mcpPath, { mcpServers: {} });
|
|
19475
19987
|
mcp.mcpServers ??= {};
|
|
19476
19988
|
const existing = mcp.mcpServers[MCP_SERVER_NAME];
|
|
@@ -19483,8 +19995,8 @@ function ensureProjectMcp(cwd) {
|
|
|
19483
19995
|
ok("registered specialists in project .mcp.json");
|
|
19484
19996
|
}
|
|
19485
19997
|
function ensureGitignore(cwd) {
|
|
19486
|
-
const gitignorePath =
|
|
19487
|
-
const existing =
|
|
19998
|
+
const gitignorePath = join13(cwd, ".gitignore");
|
|
19999
|
+
const existing = existsSync9(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
|
|
19488
20000
|
let added = 0;
|
|
19489
20001
|
const lines = existing.split(`
|
|
19490
20002
|
`);
|
|
@@ -19504,9 +20016,9 @@ function ensureGitignore(cwd) {
|
|
|
19504
20016
|
}
|
|
19505
20017
|
}
|
|
19506
20018
|
function ensureAgentsMd(cwd) {
|
|
19507
|
-
const agentsPath =
|
|
19508
|
-
if (
|
|
19509
|
-
const existing =
|
|
20019
|
+
const agentsPath = join13(cwd, "AGENTS.md");
|
|
20020
|
+
if (existsSync9(agentsPath)) {
|
|
20021
|
+
const existing = readFileSync5(agentsPath, "utf-8");
|
|
19510
20022
|
if (existing.includes(AGENTS_MARKER)) {
|
|
19511
20023
|
skip("AGENTS.md already has Specialists section");
|
|
19512
20024
|
} else {
|
|
@@ -19525,6 +20037,8 @@ async function run5() {
|
|
|
19525
20037
|
console.log(`
|
|
19526
20038
|
${bold4("specialists init")}
|
|
19527
20039
|
`);
|
|
20040
|
+
migrateLegacySpecialists(cwd, "default");
|
|
20041
|
+
migrateLegacySpecialists(cwd, "user");
|
|
19528
20042
|
copyCanonicalSpecialists(cwd);
|
|
19529
20043
|
createUserDirs(cwd);
|
|
19530
20044
|
createRuntimeDirs(cwd);
|
|
@@ -19546,15 +20060,13 @@ ${bold4("Done!")}
|
|
|
19546
20060
|
console.log(` ${dim4(".specialists/ structure:")}`);
|
|
19547
20061
|
console.log(` .specialists/`);
|
|
19548
20062
|
console.log(` ├── default/ ${dim4("# canonical specialists (from init)")}`);
|
|
19549
|
-
console.log(` │ └── specialists/`);
|
|
19550
20063
|
console.log(` ├── user/ ${dim4("# your custom specialists")}`);
|
|
19551
|
-
console.log(` │ └── specialists/`);
|
|
19552
20064
|
console.log(` ├── jobs/ ${dim4("# runtime (gitignored)")}`);
|
|
19553
20065
|
console.log(` └── ready/ ${dim4("# runtime (gitignored)")}`);
|
|
19554
20066
|
console.log(`
|
|
19555
20067
|
${dim4("Next steps:")}`);
|
|
19556
20068
|
console.log(` 1. Run ${yellow4("specialists list")} to see available specialists`);
|
|
19557
|
-
console.log(` 2. Add custom specialists to ${yellow4(".specialists/user/
|
|
20069
|
+
console.log(` 2. Add custom specialists to ${yellow4(".specialists/user/")}`);
|
|
19558
20070
|
console.log(` 3. Restart Claude Code or pi to pick up changes
|
|
19559
20071
|
`);
|
|
19560
20072
|
}
|
|
@@ -19563,12 +20075,29 @@ var init_init = __esm(() => {
|
|
|
19563
20075
|
AGENTS_BLOCK = `
|
|
19564
20076
|
## Specialists
|
|
19565
20077
|
|
|
19566
|
-
Call \`
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
20078
|
+
Call \`specialists init\` once per project, then use CLI commands via Bash.
|
|
20079
|
+
|
|
20080
|
+
Core specialist commands (CLI-first in pi):
|
|
20081
|
+
- \`specialists list\`
|
|
20082
|
+
- \`specialists run <name> --bead <id>\`
|
|
20083
|
+
- \`specialists run <name> --prompt "..."\`
|
|
20084
|
+
- \`specialists feed -f\` / \`specialists feed <job-id>\`
|
|
20085
|
+
- \`specialists result <job-id>\`
|
|
20086
|
+
- \`specialists resume <job-id> "next task"\` (for keep-alive jobs in waiting)
|
|
20087
|
+
- \`specialists stop <job-id>\`
|
|
20088
|
+
|
|
20089
|
+
For background specialists in pi, prefer the process extension:
|
|
20090
|
+
- \`process start\`, \`process list\`, \`process output\`, \`process logs\`, \`process kill\`, \`process clear\`
|
|
20091
|
+
- TUI: \`/ps\`, \`/ps:pin\`, \`/ps:logs\`, \`/ps:kill\`, \`/ps:clear\`, \`/ps:dock\`, \`/ps:settings\`
|
|
20092
|
+
|
|
20093
|
+
Canonical tracked flow:
|
|
20094
|
+
1. Create/claim bead issue
|
|
20095
|
+
2. Run specialist with \`--bead <id>\` (for long work, launch via \`process start\`)
|
|
20096
|
+
3. Observe progress (\`process output\` / \`process logs\` or \`specialists feed\`)
|
|
20097
|
+
4. Read final output (\`specialists result <job-id>\`)
|
|
20098
|
+
5. Close/update bead with outcome
|
|
20099
|
+
|
|
20100
|
+
Add custom specialists to \`.specialists/user/\` to extend defaults.
|
|
19572
20101
|
`.trimStart();
|
|
19573
20102
|
GITIGNORE_ENTRIES = [".specialists/jobs/", ".specialists/ready/"];
|
|
19574
20103
|
MCP_SERVER_CONFIG = { command: "specialists", args: [] };
|
|
@@ -19582,8 +20111,8 @@ __export(exports_validate, {
|
|
|
19582
20111
|
ArgParseError: () => ArgParseError2
|
|
19583
20112
|
});
|
|
19584
20113
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
19585
|
-
import { existsSync as
|
|
19586
|
-
import { join as
|
|
20114
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
20115
|
+
import { join as join14 } from "node:path";
|
|
19587
20116
|
function parseArgs3(argv) {
|
|
19588
20117
|
const name = argv[0];
|
|
19589
20118
|
if (!name || name.startsWith("--")) {
|
|
@@ -19594,13 +20123,15 @@ function parseArgs3(argv) {
|
|
|
19594
20123
|
}
|
|
19595
20124
|
function findSpecialistFile(name) {
|
|
19596
20125
|
const scanDirs = [
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
20126
|
+
join14(process.cwd(), ".specialists", "user"),
|
|
20127
|
+
join14(process.cwd(), ".specialists", "user", "specialists"),
|
|
20128
|
+
join14(process.cwd(), ".specialists", "default"),
|
|
20129
|
+
join14(process.cwd(), ".specialists", "default", "specialists"),
|
|
20130
|
+
join14(process.cwd(), "specialists")
|
|
19600
20131
|
];
|
|
19601
20132
|
for (const dir of scanDirs) {
|
|
19602
|
-
const candidate =
|
|
19603
|
-
if (
|
|
20133
|
+
const candidate = join14(dir, `${name}.specialist.yaml`);
|
|
20134
|
+
if (existsSync10(candidate)) {
|
|
19604
20135
|
return candidate;
|
|
19605
20136
|
}
|
|
19606
20137
|
}
|
|
@@ -19692,7 +20223,9 @@ var exports_edit = {};
|
|
|
19692
20223
|
__export(exports_edit, {
|
|
19693
20224
|
run: () => run7
|
|
19694
20225
|
});
|
|
19695
|
-
import {
|
|
20226
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
20227
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
20228
|
+
import { join as join15 } from "node:path";
|
|
19696
20229
|
function parseArgs4(argv) {
|
|
19697
20230
|
const name = argv[0];
|
|
19698
20231
|
if (!name || name.startsWith("--")) {
|
|
@@ -19755,8 +20288,30 @@ function setIn(doc2, path, value) {
|
|
|
19755
20288
|
node.set(leaf, value);
|
|
19756
20289
|
}
|
|
19757
20290
|
}
|
|
20291
|
+
function openAllConfigSpecialistsInEditor() {
|
|
20292
|
+
const configDir = join15(process.cwd(), "config", "specialists");
|
|
20293
|
+
if (!existsSync11(configDir)) {
|
|
20294
|
+
console.error(`Error: missing directory: ${configDir}`);
|
|
20295
|
+
process.exit(1);
|
|
20296
|
+
}
|
|
20297
|
+
const files = readdirSync4(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join15(configDir, file));
|
|
20298
|
+
if (files.length === 0) {
|
|
20299
|
+
console.error("Error: no specialist YAML files found in config/specialists/");
|
|
20300
|
+
process.exit(1);
|
|
20301
|
+
}
|
|
20302
|
+
const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
|
|
20303
|
+
const result = spawnSync6(editor, files, { stdio: "inherit", shell: true });
|
|
20304
|
+
if (result.status !== 0) {
|
|
20305
|
+
process.exit(result.status ?? 1);
|
|
20306
|
+
}
|
|
20307
|
+
}
|
|
19758
20308
|
async function run7() {
|
|
19759
|
-
const
|
|
20309
|
+
const rawArgs = process.argv.slice(3);
|
|
20310
|
+
if (rawArgs.length === 1 && rawArgs[0] === "--all") {
|
|
20311
|
+
openAllConfigSpecialistsInEditor();
|
|
20312
|
+
return;
|
|
20313
|
+
}
|
|
20314
|
+
const args = parseArgs4(rawArgs);
|
|
19760
20315
|
const { name, field, value, dryRun, scope } = args;
|
|
19761
20316
|
const loader = new SpecialistLoader;
|
|
19762
20317
|
const all = await loader.list();
|
|
@@ -19767,7 +20322,7 @@ async function run7() {
|
|
|
19767
20322
|
console.error(` Run ${yellow6("specialists list")} to see available specialists`);
|
|
19768
20323
|
process.exit(1);
|
|
19769
20324
|
}
|
|
19770
|
-
const raw =
|
|
20325
|
+
const raw = readFileSync6(match.filePath, "utf-8");
|
|
19771
20326
|
const doc2 = $parseDocument(raw);
|
|
19772
20327
|
const yamlPath = FIELD_MAP[field];
|
|
19773
20328
|
let typedValue = value;
|
|
@@ -19817,168 +20372,178 @@ var init_edit = __esm(() => {
|
|
|
19817
20372
|
VALID_PERMISSIONS = ["READ_ONLY", "LOW", "MEDIUM", "HIGH"];
|
|
19818
20373
|
});
|
|
19819
20374
|
|
|
19820
|
-
// src/cli/
|
|
19821
|
-
var
|
|
19822
|
-
__export(
|
|
20375
|
+
// src/cli/config.ts
|
|
20376
|
+
var exports_config = {};
|
|
20377
|
+
__export(exports_config, {
|
|
19823
20378
|
run: () => run8
|
|
19824
20379
|
});
|
|
19825
|
-
import {
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
20380
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
20381
|
+
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
20382
|
+
import { basename as basename2, join as join16 } from "node:path";
|
|
20383
|
+
function usage() {
|
|
20384
|
+
return [
|
|
20385
|
+
"Usage:",
|
|
20386
|
+
" specialists config get <key> [--all] [--name <specialist>]",
|
|
20387
|
+
" specialists config set <key> <value> [--all] [--name <specialist>]",
|
|
20388
|
+
"",
|
|
20389
|
+
"Examples:",
|
|
20390
|
+
" specialists config get specialist.execution.stall_timeout_ms",
|
|
20391
|
+
" specialists config set specialist.execution.stall_timeout_ms 180000",
|
|
20392
|
+
" specialists config set specialist.execution.stall_timeout_ms 120000 --name executor"
|
|
20393
|
+
].join(`
|
|
20394
|
+
`);
|
|
20395
|
+
}
|
|
20396
|
+
function parseArgs5(argv) {
|
|
20397
|
+
const command = argv[0];
|
|
20398
|
+
if (command !== "get" && command !== "set") {
|
|
20399
|
+
throw new ArgParseError3(usage());
|
|
19831
20400
|
}
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
continue;
|
|
19847
|
-
}
|
|
19848
|
-
if (token === "--model" && argv[i + 1]) {
|
|
19849
|
-
model = argv[++i];
|
|
19850
|
-
continue;
|
|
19851
|
-
}
|
|
19852
|
-
if (token === "--context-depth" && argv[i + 1]) {
|
|
19853
|
-
contextDepth = parseInt(argv[++i], 10) || 0;
|
|
19854
|
-
continue;
|
|
20401
|
+
const key = argv[1];
|
|
20402
|
+
if (!key || key.startsWith("--")) {
|
|
20403
|
+
throw new ArgParseError3(`Missing key
|
|
20404
|
+
|
|
20405
|
+
${usage()}`);
|
|
20406
|
+
}
|
|
20407
|
+
let value;
|
|
20408
|
+
let index = 2;
|
|
20409
|
+
if (command === "set") {
|
|
20410
|
+
value = argv[2];
|
|
20411
|
+
if (value === undefined || value.startsWith("--")) {
|
|
20412
|
+
throw new ArgParseError3(`Missing value for set
|
|
20413
|
+
|
|
20414
|
+
${usage()}`);
|
|
19855
20415
|
}
|
|
19856
|
-
|
|
19857
|
-
|
|
20416
|
+
index = 3;
|
|
20417
|
+
}
|
|
20418
|
+
let name;
|
|
20419
|
+
let all = false;
|
|
20420
|
+
for (let i = index;i < argv.length; i++) {
|
|
20421
|
+
const token = argv[i];
|
|
20422
|
+
if (token === "--all") {
|
|
20423
|
+
all = true;
|
|
19858
20424
|
continue;
|
|
19859
20425
|
}
|
|
19860
|
-
if (token === "--
|
|
19861
|
-
|
|
20426
|
+
if (token === "--name") {
|
|
20427
|
+
const next = argv[++i];
|
|
20428
|
+
if (!next || next.startsWith("--")) {
|
|
20429
|
+
throw new ArgParseError3("--name requires a specialist name");
|
|
20430
|
+
}
|
|
20431
|
+
name = next;
|
|
19862
20432
|
continue;
|
|
19863
20433
|
}
|
|
20434
|
+
throw new ArgParseError3(`Unknown option: ${token}`);
|
|
19864
20435
|
}
|
|
19865
|
-
if (
|
|
19866
|
-
|
|
19867
|
-
process.exit(1);
|
|
20436
|
+
if (name && all) {
|
|
20437
|
+
throw new ArgParseError3("Use either --name or --all, not both");
|
|
19868
20438
|
}
|
|
19869
|
-
if (!
|
|
19870
|
-
|
|
19871
|
-
let buf = "";
|
|
19872
|
-
process.stdin.setEncoding("utf-8");
|
|
19873
|
-
process.stdin.on("data", (chunk) => {
|
|
19874
|
-
buf += chunk;
|
|
19875
|
-
});
|
|
19876
|
-
process.stdin.on("end", () => resolve2(buf.trim()));
|
|
19877
|
-
});
|
|
20439
|
+
if (!name) {
|
|
20440
|
+
all = true;
|
|
19878
20441
|
}
|
|
19879
|
-
|
|
19880
|
-
|
|
19881
|
-
|
|
20442
|
+
return { command, key, value, name, all };
|
|
20443
|
+
}
|
|
20444
|
+
function splitKeyPath(key) {
|
|
20445
|
+
const path = key.split(".").map((part) => part.trim()).filter(Boolean);
|
|
20446
|
+
if (path.length === 0) {
|
|
20447
|
+
throw new ArgParseError3(`Invalid key: ${key}`);
|
|
20448
|
+
}
|
|
20449
|
+
return path;
|
|
20450
|
+
}
|
|
20451
|
+
function getSpecialistDir(projectDir) {
|
|
20452
|
+
return join16(projectDir, "config", "specialists");
|
|
20453
|
+
}
|
|
20454
|
+
function getSpecialistNameFromPath(path) {
|
|
20455
|
+
return path.replace(/\.specialist\.yaml$/, "");
|
|
20456
|
+
}
|
|
20457
|
+
async function listSpecialistFiles(projectDir) {
|
|
20458
|
+
const specialistDir = getSpecialistDir(projectDir);
|
|
20459
|
+
if (!existsSync12(specialistDir)) {
|
|
20460
|
+
throw new Error(`Missing directory: ${specialistDir}`);
|
|
20461
|
+
}
|
|
20462
|
+
const entries = await readdir2(specialistDir);
|
|
20463
|
+
return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join16(specialistDir, entry));
|
|
20464
|
+
}
|
|
20465
|
+
async function findNamedSpecialistFile(projectDir, name) {
|
|
20466
|
+
const path = join16(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
|
|
20467
|
+
if (!existsSync12(path)) {
|
|
20468
|
+
throw new Error(`Specialist not found in config/specialists/: ${name}`);
|
|
20469
|
+
}
|
|
20470
|
+
return path;
|
|
20471
|
+
}
|
|
20472
|
+
function parseValue(rawValue) {
|
|
20473
|
+
try {
|
|
20474
|
+
return $parse(rawValue);
|
|
20475
|
+
} catch {
|
|
20476
|
+
return rawValue;
|
|
20477
|
+
}
|
|
20478
|
+
}
|
|
20479
|
+
function formatValue(value) {
|
|
20480
|
+
if (value === undefined)
|
|
20481
|
+
return "<unset>";
|
|
20482
|
+
if (typeof value === "string")
|
|
20483
|
+
return value;
|
|
20484
|
+
return JSON.stringify(value);
|
|
20485
|
+
}
|
|
20486
|
+
async function getAcrossFiles(files, keyPath) {
|
|
20487
|
+
for (const file of files) {
|
|
20488
|
+
const content = await readFile3(file, "utf-8");
|
|
20489
|
+
const doc2 = $parseDocument(content);
|
|
20490
|
+
const value = doc2.getIn(keyPath);
|
|
20491
|
+
const name = getSpecialistNameFromPath(basename2(file));
|
|
20492
|
+
console.log(`${yellow7(name)}: ${formatValue(value)}`);
|
|
20493
|
+
}
|
|
20494
|
+
}
|
|
20495
|
+
async function setAcrossFiles(files, keyPath, rawValue) {
|
|
20496
|
+
const typedValue = parseValue(rawValue);
|
|
20497
|
+
for (const file of files) {
|
|
20498
|
+
const content = await readFile3(file, "utf-8");
|
|
20499
|
+
const doc2 = $parseDocument(content);
|
|
20500
|
+
doc2.setIn(keyPath, typedValue);
|
|
20501
|
+
await writeFile2(file, doc2.toString(), "utf-8");
|
|
19882
20502
|
}
|
|
19883
|
-
|
|
20503
|
+
console.log(`${green6("✓")} updated ${files.length} specialist${files.length === 1 ? "" : "s"}: ` + `${keyPath.join(".")} = ${formatValue(typedValue)}`);
|
|
19884
20504
|
}
|
|
19885
20505
|
async function run8() {
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19889
|
-
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
let variables;
|
|
19894
|
-
if (args.beadId) {
|
|
19895
|
-
const bead = beadReader.readBead(args.beadId);
|
|
19896
|
-
if (!bead) {
|
|
19897
|
-
throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
|
|
19898
|
-
}
|
|
19899
|
-
const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
|
|
19900
|
-
if (blockers.length > 0) {
|
|
19901
|
-
process.stderr.write(dim7(`
|
|
19902
|
-
[context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
|
|
19903
|
-
`));
|
|
20506
|
+
let args;
|
|
20507
|
+
try {
|
|
20508
|
+
args = parseArgs5(process.argv.slice(3));
|
|
20509
|
+
} catch (error2) {
|
|
20510
|
+
if (error2 instanceof ArgParseError3) {
|
|
20511
|
+
console.error(error2.message);
|
|
20512
|
+
process.exit(1);
|
|
19904
20513
|
}
|
|
19905
|
-
|
|
19906
|
-
prompt = beadContext;
|
|
19907
|
-
variables = {
|
|
19908
|
-
bead_context: beadContext,
|
|
19909
|
-
bead_id: args.beadId
|
|
19910
|
-
};
|
|
20514
|
+
throw error2;
|
|
19911
20515
|
}
|
|
19912
|
-
const
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
circuitBreaker,
|
|
19916
|
-
beadsClient
|
|
19917
|
-
});
|
|
19918
|
-
const jobsDir = join11(process.cwd(), ".specialists", "jobs");
|
|
19919
|
-
const supervisor = new Supervisor({
|
|
19920
|
-
runner,
|
|
19921
|
-
runOptions: {
|
|
19922
|
-
name: args.name,
|
|
19923
|
-
prompt,
|
|
19924
|
-
variables,
|
|
19925
|
-
backendOverride: args.model,
|
|
19926
|
-
inputBeadId: args.beadId,
|
|
19927
|
-
keepAlive: args.keepAlive
|
|
19928
|
-
},
|
|
19929
|
-
jobsDir,
|
|
19930
|
-
beadsClient,
|
|
19931
|
-
onProgress: (delta) => process.stdout.write(delta),
|
|
19932
|
-
onMeta: (meta) => process.stderr.write(dim7(`
|
|
19933
|
-
[${meta.backend} / ${meta.model}]
|
|
19934
|
-
|
|
19935
|
-
`)),
|
|
19936
|
-
onJobStarted: ({ id }) => process.stderr.write(dim7(`[job started: ${id}]
|
|
19937
|
-
`))
|
|
19938
|
-
});
|
|
20516
|
+
const keyPath = splitKeyPath(args.key);
|
|
20517
|
+
const projectDir = process.cwd();
|
|
20518
|
+
let files;
|
|
19939
20519
|
try {
|
|
19940
|
-
await
|
|
19941
|
-
} catch (
|
|
19942
|
-
|
|
19943
|
-
|
|
20520
|
+
files = args.name ? [await findNamedSpecialistFile(projectDir, args.name)] : await listSpecialistFiles(projectDir);
|
|
20521
|
+
} catch (error2) {
|
|
20522
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
20523
|
+
console.error(message);
|
|
19944
20524
|
process.exit(1);
|
|
20525
|
+
return;
|
|
19945
20526
|
}
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
`);
|
|
19950
|
-
let jobId;
|
|
19951
|
-
try {
|
|
19952
|
-
jobId = await supervisor.run();
|
|
19953
|
-
} catch (err) {
|
|
19954
|
-
process.stderr.write(`Error: ${err?.message ?? err}
|
|
19955
|
-
`);
|
|
20527
|
+
if (files.length === 0) {
|
|
20528
|
+
console.error("No specialists found in config/specialists/");
|
|
19956
20529
|
process.exit(1);
|
|
20530
|
+
return;
|
|
19957
20531
|
}
|
|
19958
|
-
|
|
19959
|
-
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
`${secs.toFixed(1)}s`,
|
|
19964
|
-
status?.model ? dim7(`${status.backend}/${status.model}`) : ""
|
|
19965
|
-
].filter(Boolean).join(" ");
|
|
19966
|
-
process.stderr.write(`
|
|
19967
|
-
${green6("✓")} ${footer}
|
|
19968
|
-
|
|
19969
|
-
`);
|
|
19970
|
-
process.stderr.write(dim7(`Poll: specialists poll ${jobId} --json
|
|
19971
|
-
|
|
19972
|
-
`));
|
|
19973
|
-
process.exit(0);
|
|
20532
|
+
if (args.command === "get") {
|
|
20533
|
+
await getAcrossFiles(files, keyPath);
|
|
20534
|
+
return;
|
|
20535
|
+
}
|
|
20536
|
+
await setAcrossFiles(files, keyPath, args.value);
|
|
19974
20537
|
}
|
|
19975
|
-
var
|
|
19976
|
-
var
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
20538
|
+
var green6 = (s) => `\x1B[32m${s}\x1B[0m`, yellow7 = (s) => `\x1B[33m${s}\x1B[0m`, ArgParseError3;
|
|
20539
|
+
var init_config = __esm(() => {
|
|
20540
|
+
init_dist();
|
|
20541
|
+
ArgParseError3 = class ArgParseError3 extends Error {
|
|
20542
|
+
constructor(message) {
|
|
20543
|
+
super(message);
|
|
20544
|
+
this.name = "ArgParseError";
|
|
20545
|
+
}
|
|
20546
|
+
};
|
|
19982
20547
|
});
|
|
19983
20548
|
|
|
19984
20549
|
// src/cli/format-helpers.ts
|
|
@@ -20018,29 +20583,46 @@ class JobColorMap {
|
|
|
20018
20583
|
return this.colors.size;
|
|
20019
20584
|
}
|
|
20020
20585
|
}
|
|
20586
|
+
function formatToolArgValue(value, maxLen = 240) {
|
|
20587
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
20588
|
+
const flat = raw.replace(/\s+/g, " ").trim();
|
|
20589
|
+
return flat.length > maxLen ? `${flat.slice(0, maxLen - 3)}...` : flat;
|
|
20590
|
+
}
|
|
20591
|
+
function formatToolDetail(event) {
|
|
20592
|
+
const toolName = cyan4(event.tool);
|
|
20593
|
+
if (event.phase === "start") {
|
|
20594
|
+
if (typeof event.args?.command === "string") {
|
|
20595
|
+
return `${toolName}: ${yellow8(formatToolArgValue(event.args.command))}`;
|
|
20596
|
+
}
|
|
20597
|
+
if (event.args && Object.keys(event.args).length > 0) {
|
|
20598
|
+
const argStr = Object.entries(event.args).map(([k, v]) => `${k}=${formatToolArgValue(v)}`).join(" ");
|
|
20599
|
+
return `${toolName}: ${dim7(argStr)}`;
|
|
20600
|
+
}
|
|
20601
|
+
return `${toolName}: ${dim7("start")}`;
|
|
20602
|
+
}
|
|
20603
|
+
if (event.phase === "end" && event.is_error) {
|
|
20604
|
+
return `${toolName}: ${red2("error")}`;
|
|
20605
|
+
}
|
|
20606
|
+
return `${toolName}: ${dim7(event.phase)}`;
|
|
20607
|
+
}
|
|
20021
20608
|
function formatEventLine(event, options) {
|
|
20022
|
-
const ts =
|
|
20023
|
-
const
|
|
20024
|
-
const
|
|
20609
|
+
const ts = dim7(formatTime(event.t));
|
|
20610
|
+
const job = options.colorize(`[${options.jobId}]`);
|
|
20611
|
+
const bead = dim7(`[${options.beadId ?? "-"}]`);
|
|
20612
|
+
const label = options.colorize(bold7(getEventLabel(event.type).padEnd(5)));
|
|
20025
20613
|
const detailParts = [];
|
|
20614
|
+
let detail = "";
|
|
20026
20615
|
if (event.type === "meta") {
|
|
20027
20616
|
detailParts.push(`model=${event.model}`);
|
|
20028
20617
|
detailParts.push(`backend=${event.backend}`);
|
|
20029
20618
|
} else if (event.type === "tool") {
|
|
20030
|
-
|
|
20031
|
-
detailParts.push(`phase=${event.phase}`);
|
|
20032
|
-
if (event.phase === "end") {
|
|
20033
|
-
detailParts.push(`ok=${event.is_error ? "false" : "true"}`);
|
|
20034
|
-
}
|
|
20619
|
+
detail = formatToolDetail(event);
|
|
20035
20620
|
} else if (event.type === "run_complete") {
|
|
20036
20621
|
detailParts.push(`status=${event.status}`);
|
|
20037
20622
|
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
|
|
20038
20623
|
if (event.error) {
|
|
20039
20624
|
detailParts.push(`error=${event.error}`);
|
|
20040
20625
|
}
|
|
20041
|
-
} else if (event.type === "done" || event.type === "agent_end") {
|
|
20042
|
-
detailParts.push("status=COMPLETE");
|
|
20043
|
-
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s ?? 0)}`);
|
|
20044
20626
|
} else if (event.type === "run_start") {
|
|
20045
20627
|
detailParts.push(`specialist=${event.specialist}`);
|
|
20046
20628
|
if (event.bead_id) {
|
|
@@ -20056,54 +20638,396 @@ function formatEventLine(event, options) {
|
|
|
20056
20638
|
} else if (event.type === "turn") {
|
|
20057
20639
|
detailParts.push(`phase=${event.phase}`);
|
|
20058
20640
|
}
|
|
20059
|
-
|
|
20060
|
-
|
|
20641
|
+
if (!detail && detailParts.length > 0) {
|
|
20642
|
+
detail = dim7(detailParts.join(" "));
|
|
20643
|
+
}
|
|
20644
|
+
return `${ts} ${job} ${bead} ${label} ${options.specialist}${detail ? ` ${detail}` : ""}`.trimEnd();
|
|
20645
|
+
}
|
|
20646
|
+
function formatEventInline(event) {
|
|
20647
|
+
switch (event.type) {
|
|
20648
|
+
case "meta":
|
|
20649
|
+
return dim7(`[model] ${event.backend}/${event.model}`);
|
|
20650
|
+
case "thinking":
|
|
20651
|
+
return dim7("[thinking...]");
|
|
20652
|
+
case "text":
|
|
20653
|
+
return dim7("[response]");
|
|
20654
|
+
case "tool": {
|
|
20655
|
+
if (event.phase !== "start")
|
|
20656
|
+
return null;
|
|
20657
|
+
const firstArgVal = event.args ? Object.values(event.args)[0] : undefined;
|
|
20658
|
+
const argStr = firstArgVal !== undefined ? ": " + (typeof firstArgVal === "string" ? firstArgVal.split(`
|
|
20659
|
+
`)[0].slice(0, 80) : JSON.stringify(firstArgVal).slice(0, 80)) : "";
|
|
20660
|
+
return `${dim7("[tool]")} ${cyan4(event.tool)}${dim7(argStr)}`;
|
|
20661
|
+
}
|
|
20662
|
+
case "stale_warning":
|
|
20663
|
+
return yellow8(`[warning] ${event.reason}: ${Math.round(event.silence_ms / 1000)}s silent`);
|
|
20664
|
+
default:
|
|
20665
|
+
return null;
|
|
20666
|
+
}
|
|
20667
|
+
}
|
|
20668
|
+
function formatEventInlineDebounced(event, activePhase) {
|
|
20669
|
+
if (event.type === "thinking" || event.type === "text") {
|
|
20670
|
+
if (activePhase === event.type) {
|
|
20671
|
+
return { line: null, nextPhase: activePhase };
|
|
20672
|
+
}
|
|
20673
|
+
return { line: formatEventInline(event), nextPhase: event.type };
|
|
20674
|
+
}
|
|
20675
|
+
return {
|
|
20676
|
+
line: formatEventInline(event),
|
|
20677
|
+
nextPhase: null
|
|
20678
|
+
};
|
|
20679
|
+
}
|
|
20680
|
+
var dim7 = (s) => `\x1B[2m${s}\x1B[0m`, bold7 = (s) => `\x1B[1m${s}\x1B[0m`, cyan4 = (s) => `\x1B[36m${s}\x1B[0m`, yellow8 = (s) => `\x1B[33m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`, green7 = (s) => `\x1B[32m${s}\x1B[0m`, blue = (s) => `\x1B[34m${s}\x1B[0m`, magenta = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
|
|
20681
|
+
var init_format_helpers = __esm(() => {
|
|
20682
|
+
JOB_COLORS = [cyan4, yellow8, magenta, green7, blue, red2];
|
|
20683
|
+
EVENT_LABELS = {
|
|
20684
|
+
run_start: "START",
|
|
20685
|
+
meta: "META",
|
|
20686
|
+
thinking: "THINK",
|
|
20687
|
+
tool: "TOOL",
|
|
20688
|
+
text: "TEXT",
|
|
20689
|
+
message: "MSG",
|
|
20690
|
+
turn: "TURN",
|
|
20691
|
+
run_complete: "DONE",
|
|
20692
|
+
error: "ERR"
|
|
20693
|
+
};
|
|
20694
|
+
});
|
|
20695
|
+
|
|
20696
|
+
// src/cli/run.ts
|
|
20697
|
+
var exports_run = {};
|
|
20698
|
+
__export(exports_run, {
|
|
20699
|
+
run: () => run9
|
|
20700
|
+
});
|
|
20701
|
+
import { join as join17 } from "node:path";
|
|
20702
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
20703
|
+
import { spawn as cpSpawn } from "node:child_process";
|
|
20704
|
+
async function parseArgs6(argv) {
|
|
20705
|
+
const name = argv[0];
|
|
20706
|
+
if (!name || name.startsWith("--")) {
|
|
20707
|
+
console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] [--context-depth <n>] [--model <model>] [--no-beads] [--no-bead-notes] [--keep-alive|--no-keep-alive] [--json|--raw]');
|
|
20708
|
+
process.exit(1);
|
|
20709
|
+
}
|
|
20710
|
+
let prompt = "";
|
|
20711
|
+
let beadId;
|
|
20712
|
+
let model;
|
|
20713
|
+
let noBeads = false;
|
|
20714
|
+
let noBeadNotes = false;
|
|
20715
|
+
let keepAlive;
|
|
20716
|
+
let noKeepAlive = false;
|
|
20717
|
+
let background = false;
|
|
20718
|
+
let outputMode = "human";
|
|
20719
|
+
let contextDepth = 1;
|
|
20720
|
+
for (let i = 1;i < argv.length; i++) {
|
|
20721
|
+
const token = argv[i];
|
|
20722
|
+
if (token === "--prompt" && argv[i + 1]) {
|
|
20723
|
+
prompt = argv[++i];
|
|
20724
|
+
continue;
|
|
20725
|
+
}
|
|
20726
|
+
if (token === "--bead" && argv[i + 1]) {
|
|
20727
|
+
beadId = argv[++i];
|
|
20728
|
+
continue;
|
|
20729
|
+
}
|
|
20730
|
+
if (token === "--model" && argv[i + 1]) {
|
|
20731
|
+
model = argv[++i];
|
|
20732
|
+
continue;
|
|
20733
|
+
}
|
|
20734
|
+
if (token === "--context-depth" && argv[i + 1]) {
|
|
20735
|
+
contextDepth = parseInt(argv[++i], 10) || 0;
|
|
20736
|
+
continue;
|
|
20737
|
+
}
|
|
20738
|
+
if (token === "--no-beads") {
|
|
20739
|
+
noBeads = true;
|
|
20740
|
+
continue;
|
|
20741
|
+
}
|
|
20742
|
+
if (token === "--no-bead-notes") {
|
|
20743
|
+
noBeadNotes = true;
|
|
20744
|
+
continue;
|
|
20745
|
+
}
|
|
20746
|
+
if (token === "--keep-alive") {
|
|
20747
|
+
keepAlive = true;
|
|
20748
|
+
noKeepAlive = false;
|
|
20749
|
+
continue;
|
|
20750
|
+
}
|
|
20751
|
+
if (token === "--no-keep-alive") {
|
|
20752
|
+
keepAlive = undefined;
|
|
20753
|
+
noKeepAlive = true;
|
|
20754
|
+
continue;
|
|
20755
|
+
}
|
|
20756
|
+
if (token === "--background") {
|
|
20757
|
+
background = true;
|
|
20758
|
+
continue;
|
|
20759
|
+
}
|
|
20760
|
+
if (token === "--json") {
|
|
20761
|
+
outputMode = "json";
|
|
20762
|
+
continue;
|
|
20763
|
+
}
|
|
20764
|
+
if (token === "--raw") {
|
|
20765
|
+
outputMode = "raw";
|
|
20766
|
+
continue;
|
|
20767
|
+
}
|
|
20768
|
+
}
|
|
20769
|
+
if (prompt && beadId) {
|
|
20770
|
+
console.error("Error: use either --prompt or --bead, not both.");
|
|
20771
|
+
process.exit(1);
|
|
20772
|
+
}
|
|
20773
|
+
if (!prompt && !beadId && !process.stdin.isTTY) {
|
|
20774
|
+
prompt = await new Promise((resolve2) => {
|
|
20775
|
+
let buf = "";
|
|
20776
|
+
process.stdin.setEncoding("utf-8");
|
|
20777
|
+
process.stdin.on("data", (chunk) => {
|
|
20778
|
+
buf += chunk;
|
|
20779
|
+
});
|
|
20780
|
+
process.stdin.on("end", () => resolve2(buf.trim()));
|
|
20781
|
+
});
|
|
20782
|
+
}
|
|
20783
|
+
if (!prompt && !beadId) {
|
|
20784
|
+
console.error("Error: provide --prompt, pipe stdin, or use --bead <id>.");
|
|
20785
|
+
process.exit(1);
|
|
20786
|
+
}
|
|
20787
|
+
return { name, prompt, beadId, model, noBeads, noBeadNotes, keepAlive, noKeepAlive, background, contextDepth, outputMode };
|
|
20788
|
+
}
|
|
20789
|
+
function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
|
|
20790
|
+
const eventsPath = join17(jobsDir, jobId, "events.jsonl");
|
|
20791
|
+
let linesRead = 0;
|
|
20792
|
+
let activeInlinePhase = null;
|
|
20793
|
+
const drain = () => {
|
|
20794
|
+
let content;
|
|
20795
|
+
try {
|
|
20796
|
+
content = readFileSync7(eventsPath, "utf-8");
|
|
20797
|
+
} catch {
|
|
20798
|
+
return;
|
|
20799
|
+
}
|
|
20800
|
+
if (!content)
|
|
20801
|
+
return;
|
|
20802
|
+
const lastNl = content.lastIndexOf(`
|
|
20803
|
+
`);
|
|
20804
|
+
if (lastNl < 0)
|
|
20805
|
+
return;
|
|
20806
|
+
const complete = content.slice(0, lastNl);
|
|
20807
|
+
const lines = complete.split(`
|
|
20808
|
+
`);
|
|
20809
|
+
for (let i = linesRead;i < lines.length; i++) {
|
|
20810
|
+
linesRead++;
|
|
20811
|
+
const line = lines[i].trim();
|
|
20812
|
+
if (!line)
|
|
20813
|
+
continue;
|
|
20814
|
+
let event;
|
|
20815
|
+
try {
|
|
20816
|
+
event = JSON.parse(line);
|
|
20817
|
+
} catch {
|
|
20818
|
+
continue;
|
|
20819
|
+
}
|
|
20820
|
+
if (mode === "json") {
|
|
20821
|
+
process.stdout.write(JSON.stringify({ jobId, specialist, beadId, ...event }) + `
|
|
20822
|
+
`);
|
|
20823
|
+
} else {
|
|
20824
|
+
if (event.type === "run_complete" && event.output) {
|
|
20825
|
+
activeInlinePhase = null;
|
|
20826
|
+
process.stdout.write(`
|
|
20827
|
+
` + event.output + `
|
|
20828
|
+
`);
|
|
20829
|
+
} else {
|
|
20830
|
+
const { line: line2, nextPhase } = formatEventInlineDebounced(event, activeInlinePhase);
|
|
20831
|
+
activeInlinePhase = nextPhase;
|
|
20832
|
+
if (line2)
|
|
20833
|
+
process.stdout.write(line2 + `
|
|
20834
|
+
`);
|
|
20835
|
+
}
|
|
20836
|
+
}
|
|
20837
|
+
}
|
|
20838
|
+
};
|
|
20839
|
+
const intervalId = setInterval(drain, 100);
|
|
20840
|
+
return () => {
|
|
20841
|
+
clearInterval(intervalId);
|
|
20842
|
+
drain();
|
|
20843
|
+
};
|
|
20844
|
+
}
|
|
20845
|
+
function formatFooterModel(backend, model) {
|
|
20846
|
+
if (!model)
|
|
20847
|
+
return "";
|
|
20848
|
+
if (!backend)
|
|
20849
|
+
return model;
|
|
20850
|
+
return model.startsWith(`${backend}/`) ? model : `${backend}/${model}`;
|
|
20851
|
+
}
|
|
20852
|
+
async function run9() {
|
|
20853
|
+
const args = await parseArgs6(process.argv.slice(3));
|
|
20854
|
+
if (args.background) {
|
|
20855
|
+
const latestPath = join17(process.cwd(), ".specialists", "jobs", "latest");
|
|
20856
|
+
const oldLatest = (() => {
|
|
20857
|
+
try {
|
|
20858
|
+
return readFileSync7(latestPath, "utf-8").trim();
|
|
20859
|
+
} catch {
|
|
20860
|
+
return "";
|
|
20861
|
+
}
|
|
20862
|
+
})();
|
|
20863
|
+
const childArgs = process.argv.slice(2).filter((a) => a !== "--background");
|
|
20864
|
+
const child = cpSpawn(process.execPath, [process.argv[1], ...childArgs], {
|
|
20865
|
+
detached: true,
|
|
20866
|
+
stdio: "ignore",
|
|
20867
|
+
cwd: process.cwd(),
|
|
20868
|
+
env: process.env
|
|
20869
|
+
});
|
|
20870
|
+
child.unref();
|
|
20871
|
+
const deadline = Date.now() + 5000;
|
|
20872
|
+
let jobId2 = "";
|
|
20873
|
+
while (Date.now() < deadline) {
|
|
20874
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
20875
|
+
try {
|
|
20876
|
+
const current = readFileSync7(latestPath, "utf-8").trim();
|
|
20877
|
+
if (current && current !== oldLatest) {
|
|
20878
|
+
jobId2 = current;
|
|
20879
|
+
break;
|
|
20880
|
+
}
|
|
20881
|
+
} catch {}
|
|
20882
|
+
}
|
|
20883
|
+
if (jobId2) {
|
|
20884
|
+
process.stdout.write(`${jobId2}
|
|
20885
|
+
`);
|
|
20886
|
+
} else {
|
|
20887
|
+
process.stderr.write(`Warning: job started but ID not yet available. Check specialists status.
|
|
20888
|
+
`);
|
|
20889
|
+
process.stdout.write(`${child.pid}
|
|
20890
|
+
`);
|
|
20891
|
+
}
|
|
20892
|
+
process.exit(0);
|
|
20893
|
+
}
|
|
20894
|
+
const loader = new SpecialistLoader;
|
|
20895
|
+
const circuitBreaker = new CircuitBreaker;
|
|
20896
|
+
const hooks = new HookEmitter({ tracePath: join17(process.cwd(), ".specialists", "trace.jsonl") });
|
|
20897
|
+
const beadsClient = args.noBeads ? undefined : new BeadsClient;
|
|
20898
|
+
const beadReader = beadsClient ?? new BeadsClient;
|
|
20899
|
+
let prompt = args.prompt;
|
|
20900
|
+
let variables;
|
|
20901
|
+
if (args.beadId) {
|
|
20902
|
+
const bead = beadReader.readBead(args.beadId);
|
|
20903
|
+
if (!bead) {
|
|
20904
|
+
throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
|
|
20905
|
+
}
|
|
20906
|
+
const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(args.beadId, args.contextDepth) : [];
|
|
20907
|
+
if (blockers.length > 0) {
|
|
20908
|
+
process.stderr.write(dim8(`
|
|
20909
|
+
[context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
|
|
20910
|
+
`));
|
|
20911
|
+
}
|
|
20912
|
+
const beadContext = buildBeadContext(bead, blockers);
|
|
20913
|
+
prompt = beadContext;
|
|
20914
|
+
variables = {
|
|
20915
|
+
bead_context: beadContext,
|
|
20916
|
+
bead_id: args.beadId
|
|
20917
|
+
};
|
|
20918
|
+
}
|
|
20919
|
+
const specialist = await loader.get(args.name).catch((err) => {
|
|
20920
|
+
process.stderr.write(`Error: ${err?.message ?? err}
|
|
20921
|
+
`);
|
|
20922
|
+
process.exit(1);
|
|
20923
|
+
});
|
|
20924
|
+
const runner = new SpecialistRunner({
|
|
20925
|
+
loader,
|
|
20926
|
+
hooks,
|
|
20927
|
+
circuitBreaker,
|
|
20928
|
+
beadsClient
|
|
20929
|
+
});
|
|
20930
|
+
const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
|
|
20931
|
+
const jobsDir = join17(process.cwd(), ".specialists", "jobs");
|
|
20932
|
+
let stopTailer;
|
|
20933
|
+
const supervisor = new Supervisor({
|
|
20934
|
+
runner,
|
|
20935
|
+
runOptions: {
|
|
20936
|
+
name: args.name,
|
|
20937
|
+
prompt,
|
|
20938
|
+
variables,
|
|
20939
|
+
backendOverride: args.model,
|
|
20940
|
+
inputBeadId: args.beadId,
|
|
20941
|
+
keepAlive: args.keepAlive,
|
|
20942
|
+
noKeepAlive: args.noKeepAlive,
|
|
20943
|
+
beadsWriteNotes
|
|
20944
|
+
},
|
|
20945
|
+
jobsDir,
|
|
20946
|
+
beadsClient,
|
|
20947
|
+
stallDetection: specialist.specialist.stall_detection,
|
|
20948
|
+
onProgress: args.outputMode === "raw" ? (delta) => process.stdout.write(delta) : undefined,
|
|
20949
|
+
onMeta: args.outputMode !== "human" ? (meta) => process.stderr.write(dim8(`
|
|
20950
|
+
[${meta.backend} / ${meta.model}]
|
|
20951
|
+
|
|
20952
|
+
`)) : undefined,
|
|
20953
|
+
onJobStarted: ({ id }) => {
|
|
20954
|
+
process.stderr.write(dim8(`[job started: ${id}]
|
|
20955
|
+
`));
|
|
20956
|
+
if (args.outputMode !== "raw") {
|
|
20957
|
+
stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name, args.beadId);
|
|
20958
|
+
}
|
|
20959
|
+
}
|
|
20960
|
+
});
|
|
20961
|
+
process.stderr.write(`
|
|
20962
|
+
${bold8(`Running ${cyan5(args.name)}`)}
|
|
20963
|
+
|
|
20964
|
+
`);
|
|
20965
|
+
let jobId;
|
|
20966
|
+
try {
|
|
20967
|
+
jobId = await supervisor.run();
|
|
20968
|
+
} catch (err) {
|
|
20969
|
+
stopTailer?.();
|
|
20970
|
+
process.stderr.write(`Error: ${err?.message ?? err}
|
|
20971
|
+
`);
|
|
20972
|
+
process.exit(1);
|
|
20973
|
+
}
|
|
20974
|
+
stopTailer?.();
|
|
20975
|
+
const status = supervisor.readStatus(jobId);
|
|
20976
|
+
const secs = ((status?.last_event_at_ms ?? Date.now()) - (status?.started_at_ms ?? Date.now())) / 1000;
|
|
20977
|
+
const modelLabel = formatFooterModel(status?.backend, status?.model);
|
|
20978
|
+
const footer = [
|
|
20979
|
+
`job ${jobId}`,
|
|
20980
|
+
status?.bead_id ? `bead ${status.bead_id}` : "",
|
|
20981
|
+
`${secs.toFixed(1)}s`,
|
|
20982
|
+
modelLabel ? dim8(modelLabel) : ""
|
|
20983
|
+
].filter(Boolean).join(" ");
|
|
20984
|
+
process.stderr.write(`
|
|
20985
|
+
${green8("✓")} ${footer}
|
|
20986
|
+
|
|
20987
|
+
`);
|
|
20988
|
+
process.stderr.write(dim8(`Poll: specialists poll ${jobId} --json
|
|
20989
|
+
|
|
20990
|
+
`));
|
|
20991
|
+
process.exit(0);
|
|
20061
20992
|
}
|
|
20062
|
-
var
|
|
20063
|
-
var
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
message: "MSG",
|
|
20072
|
-
turn: "TURN",
|
|
20073
|
-
run_complete: "DONE",
|
|
20074
|
-
done: "DONE",
|
|
20075
|
-
agent_end: "DONE",
|
|
20076
|
-
error: "ERR"
|
|
20077
|
-
};
|
|
20993
|
+
var bold8 = (s) => `\x1B[1m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`, green8 = (s) => `\x1B[32m${s}\x1B[0m`, cyan5 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
20994
|
+
var init_run = __esm(() => {
|
|
20995
|
+
init_loader();
|
|
20996
|
+
init_runner();
|
|
20997
|
+
init_circuitBreaker();
|
|
20998
|
+
init_hooks();
|
|
20999
|
+
init_beads();
|
|
21000
|
+
init_supervisor();
|
|
21001
|
+
init_format_helpers();
|
|
20078
21002
|
});
|
|
20079
21003
|
|
|
20080
21004
|
// src/cli/status.ts
|
|
20081
21005
|
var exports_status = {};
|
|
20082
21006
|
__export(exports_status, {
|
|
20083
|
-
run: () =>
|
|
21007
|
+
run: () => run10
|
|
20084
21008
|
});
|
|
20085
|
-
import { spawnSync as
|
|
20086
|
-
import { existsSync as
|
|
20087
|
-
import { join as
|
|
21009
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
21010
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
|
|
21011
|
+
import { join as join18 } from "node:path";
|
|
20088
21012
|
function ok2(msg) {
|
|
20089
21013
|
console.log(` ${green7("✓")} ${msg}`);
|
|
20090
21014
|
}
|
|
20091
21015
|
function warn(msg) {
|
|
20092
|
-
console.log(` ${
|
|
21016
|
+
console.log(` ${yellow8("○")} ${msg}`);
|
|
20093
21017
|
}
|
|
20094
21018
|
function fail(msg) {
|
|
20095
21019
|
console.log(` ${red2("✗")} ${msg}`);
|
|
20096
21020
|
}
|
|
20097
21021
|
function info(msg) {
|
|
20098
|
-
console.log(` ${
|
|
21022
|
+
console.log(` ${dim7(msg)}`);
|
|
20099
21023
|
}
|
|
20100
21024
|
function section(label) {
|
|
20101
21025
|
const line = "─".repeat(Math.max(0, 38 - label.length));
|
|
20102
21026
|
console.log(`
|
|
20103
|
-
${
|
|
21027
|
+
${bold7(`── ${label} ${line}`)}`);
|
|
20104
21028
|
}
|
|
20105
21029
|
function cmd(bin, args) {
|
|
20106
|
-
const r =
|
|
21030
|
+
const r = spawnSync7(bin, args, {
|
|
20107
21031
|
encoding: "utf8",
|
|
20108
21032
|
stdio: "pipe",
|
|
20109
21033
|
timeout: 5000
|
|
@@ -20111,7 +21035,7 @@ function cmd(bin, args) {
|
|
|
20111
21035
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
20112
21036
|
}
|
|
20113
21037
|
function isInstalled(bin) {
|
|
20114
|
-
return
|
|
21038
|
+
return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
20115
21039
|
}
|
|
20116
21040
|
function formatElapsed2(s) {
|
|
20117
21041
|
if (s.elapsed_s === undefined)
|
|
@@ -20123,20 +21047,83 @@ function formatElapsed2(s) {
|
|
|
20123
21047
|
function statusColor(status) {
|
|
20124
21048
|
switch (status) {
|
|
20125
21049
|
case "running":
|
|
20126
|
-
return
|
|
21050
|
+
return cyan4(status);
|
|
20127
21051
|
case "done":
|
|
20128
21052
|
return green7(status);
|
|
20129
21053
|
case "error":
|
|
20130
21054
|
return red2(status);
|
|
20131
21055
|
case "starting":
|
|
20132
|
-
return
|
|
21056
|
+
return yellow8(status);
|
|
20133
21057
|
default:
|
|
20134
21058
|
return status;
|
|
20135
21059
|
}
|
|
20136
21060
|
}
|
|
20137
|
-
|
|
21061
|
+
function parseStatusArgs(argv) {
|
|
21062
|
+
let jsonMode = false;
|
|
21063
|
+
let jobId;
|
|
21064
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
21065
|
+
const arg = argv[i];
|
|
21066
|
+
if (arg === "--json") {
|
|
21067
|
+
jsonMode = true;
|
|
21068
|
+
continue;
|
|
21069
|
+
}
|
|
21070
|
+
if (arg === "--job") {
|
|
21071
|
+
const candidate = argv[i + 1];
|
|
21072
|
+
if (!candidate || candidate.startsWith("--")) {
|
|
21073
|
+
throw new Error("--job requires a value");
|
|
21074
|
+
}
|
|
21075
|
+
jobId = candidate;
|
|
21076
|
+
i += 1;
|
|
21077
|
+
continue;
|
|
21078
|
+
}
|
|
21079
|
+
if (arg.startsWith("--job=")) {
|
|
21080
|
+
const candidate = arg.slice("--job=".length).trim();
|
|
21081
|
+
if (!candidate) {
|
|
21082
|
+
throw new Error("--job requires a value");
|
|
21083
|
+
}
|
|
21084
|
+
jobId = candidate;
|
|
21085
|
+
}
|
|
21086
|
+
}
|
|
21087
|
+
return { jsonMode, jobId };
|
|
21088
|
+
}
|
|
21089
|
+
function countJobEvents(jobsDir, jobId) {
|
|
21090
|
+
const eventsFile = join18(jobsDir, jobId, "events.jsonl");
|
|
21091
|
+
if (!existsSync13(eventsFile))
|
|
21092
|
+
return 0;
|
|
21093
|
+
const raw = readFileSync8(eventsFile, "utf-8").trim();
|
|
21094
|
+
if (!raw)
|
|
21095
|
+
return 0;
|
|
21096
|
+
return raw.split(`
|
|
21097
|
+
`).filter((line) => line.trim().length > 0).length;
|
|
21098
|
+
}
|
|
21099
|
+
function renderJobDetail(job, eventCount) {
|
|
21100
|
+
console.log(`
|
|
21101
|
+
${bold7("specialists status")}
|
|
21102
|
+
`);
|
|
21103
|
+
section(`Job ${job.id}`);
|
|
21104
|
+
console.log(` specialist ${job.specialist}`);
|
|
21105
|
+
console.log(` status ${statusColor(job.status)}`);
|
|
21106
|
+
console.log(` model ${job.model ?? "n/a"}`);
|
|
21107
|
+
console.log(` backend ${job.backend ?? "n/a"}`);
|
|
21108
|
+
console.log(` elapsed ${formatElapsed2(job)}`);
|
|
21109
|
+
console.log(` bead_id ${job.bead_id ?? "n/a"}`);
|
|
21110
|
+
console.log(` events ${eventCount}`);
|
|
21111
|
+
if (job.session_file)
|
|
21112
|
+
console.log(` session_file ${job.session_file}`);
|
|
21113
|
+
if (job.error)
|
|
21114
|
+
console.log(` error ${red2(job.error)}`);
|
|
21115
|
+
console.log();
|
|
21116
|
+
}
|
|
21117
|
+
async function run10() {
|
|
20138
21118
|
const argv = process.argv.slice(3);
|
|
20139
|
-
|
|
21119
|
+
let parsedArgs;
|
|
21120
|
+
try {
|
|
21121
|
+
parsedArgs = parseStatusArgs(argv);
|
|
21122
|
+
} catch (error2) {
|
|
21123
|
+
console.error(red2(error2.message));
|
|
21124
|
+
process.exit(1);
|
|
21125
|
+
}
|
|
21126
|
+
const { jsonMode, jobId } = parsedArgs;
|
|
20140
21127
|
const loader = new SpecialistLoader;
|
|
20141
21128
|
const allSpecialists = await loader.list();
|
|
20142
21129
|
const piInstalled = isInstalled("pi");
|
|
@@ -20146,18 +21133,42 @@ async function run9() {
|
|
|
20146
21133
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
20147
21134
|
const bdInstalled = isInstalled("bd");
|
|
20148
21135
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
20149
|
-
const beadsPresent =
|
|
21136
|
+
const beadsPresent = existsSync13(join18(process.cwd(), ".beads"));
|
|
20150
21137
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
20151
|
-
const jobsDir =
|
|
21138
|
+
const jobsDir = join18(process.cwd(), ".specialists", "jobs");
|
|
20152
21139
|
let jobs = [];
|
|
20153
|
-
|
|
20154
|
-
|
|
21140
|
+
let supervisor = null;
|
|
21141
|
+
if (existsSync13(jobsDir)) {
|
|
21142
|
+
supervisor = new Supervisor({
|
|
20155
21143
|
runner: null,
|
|
20156
21144
|
runOptions: null,
|
|
20157
21145
|
jobsDir
|
|
20158
21146
|
});
|
|
20159
21147
|
jobs = supervisor.listJobs();
|
|
20160
21148
|
}
|
|
21149
|
+
if (jobId) {
|
|
21150
|
+
const selectedJob = supervisor?.readStatus(jobId) ?? null;
|
|
21151
|
+
if (!selectedJob) {
|
|
21152
|
+
if (jsonMode) {
|
|
21153
|
+
console.log(JSON.stringify({ error: `Job not found: ${jobId}` }, null, 2));
|
|
21154
|
+
} else {
|
|
21155
|
+
fail(`job not found: ${jobId}`);
|
|
21156
|
+
}
|
|
21157
|
+
process.exit(1);
|
|
21158
|
+
}
|
|
21159
|
+
const eventCount = countJobEvents(jobsDir, jobId);
|
|
21160
|
+
if (jsonMode) {
|
|
21161
|
+
console.log(JSON.stringify({
|
|
21162
|
+
job: {
|
|
21163
|
+
...selectedJob,
|
|
21164
|
+
event_count: eventCount
|
|
21165
|
+
}
|
|
21166
|
+
}, null, 2));
|
|
21167
|
+
return;
|
|
21168
|
+
}
|
|
21169
|
+
renderJobDetail(selectedJob, eventCount);
|
|
21170
|
+
return;
|
|
21171
|
+
}
|
|
20161
21172
|
const stalenessMap = {};
|
|
20162
21173
|
for (const s of allSpecialists) {
|
|
20163
21174
|
stalenessMap[s.name] = await checkStaleness(s);
|
|
@@ -20201,51 +21212,51 @@ async function run9() {
|
|
|
20201
21212
|
return;
|
|
20202
21213
|
}
|
|
20203
21214
|
console.log(`
|
|
20204
|
-
${
|
|
21215
|
+
${bold7("specialists status")}
|
|
20205
21216
|
`);
|
|
20206
21217
|
section("Specialists");
|
|
20207
21218
|
if (allSpecialists.length === 0) {
|
|
20208
|
-
warn(`no specialists found — run ${
|
|
21219
|
+
warn(`no specialists found — run ${yellow8("specialists init")} to scaffold`);
|
|
20209
21220
|
} else {
|
|
20210
21221
|
const byScope = allSpecialists.reduce((acc, s) => {
|
|
20211
21222
|
acc[s.scope] = (acc[s.scope] ?? 0) + 1;
|
|
20212
21223
|
return acc;
|
|
20213
21224
|
}, {});
|
|
20214
21225
|
const scopeSummary = Object.entries(byScope).map(([scope, n]) => `${n} ${scope}`).join(", ");
|
|
20215
|
-
ok2(`${allSpecialists.length} found ${
|
|
21226
|
+
ok2(`${allSpecialists.length} found ${dim7(`(${scopeSummary})`)}`);
|
|
20216
21227
|
for (const s of allSpecialists) {
|
|
20217
21228
|
const staleness = stalenessMap[s.name];
|
|
20218
21229
|
if (staleness === "AGED") {
|
|
20219
|
-
warn(`${s.name} ${red2("AGED")} ${
|
|
21230
|
+
warn(`${s.name} ${red2("AGED")} ${dim7(s.scope)}`);
|
|
20220
21231
|
} else if (staleness === "STALE") {
|
|
20221
|
-
warn(`${s.name} ${
|
|
21232
|
+
warn(`${s.name} ${yellow8("STALE")} ${dim7(s.scope)}`);
|
|
20222
21233
|
}
|
|
20223
21234
|
}
|
|
20224
21235
|
}
|
|
20225
21236
|
section("pi (coding agent runtime)");
|
|
20226
21237
|
if (!piInstalled) {
|
|
20227
|
-
fail(`pi not installed — install ${
|
|
21238
|
+
fail(`pi not installed — install ${yellow8("pi")} first`);
|
|
20228
21239
|
} else {
|
|
20229
21240
|
const vStr = piVersion?.ok ? `v${piVersion.stdout}` : "unknown version";
|
|
20230
|
-
const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${
|
|
21241
|
+
const pStr = piProviders.size > 0 ? `${piProviders.size} provider${piProviders.size > 1 ? "s" : ""} active ${dim7(`(${[...piProviders].join(", ")})`)} ` : yellow8("no providers configured — run pi config");
|
|
20231
21242
|
ok2(`${vStr} — ${pStr}`);
|
|
20232
21243
|
}
|
|
20233
21244
|
section("beads (issue tracker)");
|
|
20234
21245
|
if (!bdInstalled) {
|
|
20235
|
-
fail(`bd not installed — install ${
|
|
21246
|
+
fail(`bd not installed — install ${yellow8("bd")} first`);
|
|
20236
21247
|
} else {
|
|
20237
|
-
ok2(`bd installed${bdVersion?.ok ? ` ${
|
|
21248
|
+
ok2(`bd installed${bdVersion?.ok ? ` ${dim7(bdVersion.stdout)}` : ""}`);
|
|
20238
21249
|
if (beadsPresent) {
|
|
20239
21250
|
ok2(".beads/ present in project");
|
|
20240
21251
|
} else {
|
|
20241
|
-
warn(`.beads/ not found — run ${
|
|
21252
|
+
warn(`.beads/ not found — run ${yellow8("bd init")} to enable issue tracking`);
|
|
20242
21253
|
}
|
|
20243
21254
|
}
|
|
20244
21255
|
section("MCP");
|
|
20245
21256
|
if (!specialistsBin.ok) {
|
|
20246
|
-
fail(`specialists not installed globally — run ${
|
|
21257
|
+
fail(`specialists not installed globally — run ${yellow8("npm install -g @jaggerxtrm/specialists")}`);
|
|
20247
21258
|
} else {
|
|
20248
|
-
ok2(`specialists binary installed ${
|
|
21259
|
+
ok2(`specialists binary installed ${dim7(specialistsBin.stdout)}`);
|
|
20249
21260
|
info(`verify registration: claude mcp get specialists`);
|
|
20250
21261
|
info(`re-register: specialists install`);
|
|
20251
21262
|
}
|
|
@@ -20255,8 +21266,8 @@ ${bold8("specialists status")}
|
|
|
20255
21266
|
} else {
|
|
20256
21267
|
for (const job of jobs) {
|
|
20257
21268
|
const elapsed = formatElapsed2(job);
|
|
20258
|
-
const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ?
|
|
20259
|
-
console.log(` ${
|
|
21269
|
+
const detail = job.status === "error" ? red2(job.error?.slice(0, 40) ?? "error") : job.current_tool ? dim7(`tool: ${job.current_tool}`) : dim7(job.current_event ?? "");
|
|
21270
|
+
console.log(` ${dim7(job.id)} ${job.specialist.padEnd(20)} ${statusColor(job.status).padEnd(7)} ${elapsed.padStart(6)} ${detail}`);
|
|
20260
21271
|
}
|
|
20261
21272
|
}
|
|
20262
21273
|
console.log();
|
|
@@ -20270,33 +21281,88 @@ var init_status = __esm(() => {
|
|
|
20270
21281
|
// src/cli/result.ts
|
|
20271
21282
|
var exports_result = {};
|
|
20272
21283
|
__export(exports_result, {
|
|
20273
|
-
run: () =>
|
|
21284
|
+
run: () => run11
|
|
20274
21285
|
});
|
|
20275
|
-
import { existsSync as
|
|
20276
|
-
import { join as
|
|
20277
|
-
|
|
20278
|
-
const jobId =
|
|
20279
|
-
if (!jobId) {
|
|
20280
|
-
console.error("Usage: specialists|sp result <job-id>");
|
|
21286
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
|
|
21287
|
+
import { join as join19 } from "node:path";
|
|
21288
|
+
function parseArgs7(argv) {
|
|
21289
|
+
const jobId = argv[0];
|
|
21290
|
+
if (!jobId || jobId.startsWith("--")) {
|
|
21291
|
+
console.error("Usage: specialists|sp result <job-id> [--wait] [--timeout <seconds>]");
|
|
20281
21292
|
process.exit(1);
|
|
20282
21293
|
}
|
|
20283
|
-
|
|
21294
|
+
let wait = false;
|
|
21295
|
+
let timeout;
|
|
21296
|
+
for (let i = 1;i < argv.length; i++) {
|
|
21297
|
+
const token = argv[i];
|
|
21298
|
+
if (token === "--wait") {
|
|
21299
|
+
wait = true;
|
|
21300
|
+
continue;
|
|
21301
|
+
}
|
|
21302
|
+
if (token === "--timeout" && argv[i + 1]) {
|
|
21303
|
+
const parsed = parseInt(argv[++i], 10);
|
|
21304
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
21305
|
+
console.error("Error: --timeout must be a positive integer (seconds)");
|
|
21306
|
+
process.exit(1);
|
|
21307
|
+
}
|
|
21308
|
+
timeout = parsed;
|
|
21309
|
+
continue;
|
|
21310
|
+
}
|
|
21311
|
+
}
|
|
21312
|
+
return { jobId, wait, timeout };
|
|
21313
|
+
}
|
|
21314
|
+
async function run11() {
|
|
21315
|
+
const args = parseArgs7(process.argv.slice(3));
|
|
21316
|
+
const { jobId } = args;
|
|
21317
|
+
const jobsDir = join19(process.cwd(), ".specialists", "jobs");
|
|
20284
21318
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
21319
|
+
const resultPath = join19(jobsDir, jobId, "result.txt");
|
|
21320
|
+
if (args.wait) {
|
|
21321
|
+
const startMs = Date.now();
|
|
21322
|
+
while (true) {
|
|
21323
|
+
const status2 = supervisor.readStatus(jobId);
|
|
21324
|
+
if (!status2) {
|
|
21325
|
+
console.error(`No job found: ${jobId}`);
|
|
21326
|
+
process.exit(1);
|
|
21327
|
+
}
|
|
21328
|
+
if (status2.status === "done") {
|
|
21329
|
+
if (!existsSync14(resultPath)) {
|
|
21330
|
+
console.error(`Result file not found for job ${jobId}`);
|
|
21331
|
+
process.exit(1);
|
|
21332
|
+
}
|
|
21333
|
+
process.stdout.write(readFileSync9(resultPath, "utf-8"));
|
|
21334
|
+
return;
|
|
21335
|
+
}
|
|
21336
|
+
if (status2.status === "error") {
|
|
21337
|
+
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status2.error ?? "unknown error"}
|
|
21338
|
+
`);
|
|
21339
|
+
process.exit(1);
|
|
21340
|
+
}
|
|
21341
|
+
if (args.timeout !== undefined) {
|
|
21342
|
+
const elapsedSecs = (Date.now() - startMs) / 1000;
|
|
21343
|
+
if (elapsedSecs >= args.timeout) {
|
|
21344
|
+
process.stderr.write(`Timeout: job ${jobId} did not complete within ${args.timeout}s
|
|
21345
|
+
`);
|
|
21346
|
+
process.exit(1);
|
|
21347
|
+
}
|
|
21348
|
+
}
|
|
21349
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
21350
|
+
}
|
|
21351
|
+
}
|
|
20285
21352
|
const status = supervisor.readStatus(jobId);
|
|
20286
21353
|
if (!status) {
|
|
20287
21354
|
console.error(`No job found: ${jobId}`);
|
|
20288
21355
|
process.exit(1);
|
|
20289
21356
|
}
|
|
20290
|
-
const resultPath = join13(jobsDir, jobId, "result.txt");
|
|
20291
21357
|
if (status.status === "running" || status.status === "starting") {
|
|
20292
|
-
if (!
|
|
21358
|
+
if (!existsSync14(resultPath)) {
|
|
20293
21359
|
process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
|
|
20294
21360
|
`);
|
|
20295
21361
|
process.exit(1);
|
|
20296
21362
|
}
|
|
20297
21363
|
process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
|
|
20298
21364
|
`);
|
|
20299
|
-
process.stdout.write(
|
|
21365
|
+
process.stdout.write(readFileSync9(resultPath, "utf-8"));
|
|
20300
21366
|
return;
|
|
20301
21367
|
}
|
|
20302
21368
|
if (status.status === "error") {
|
|
@@ -20304,130 +21370,30 @@ async function run10() {
|
|
|
20304
21370
|
`);
|
|
20305
21371
|
process.exit(1);
|
|
20306
21372
|
}
|
|
20307
|
-
if (!
|
|
21373
|
+
if (!existsSync14(resultPath)) {
|
|
20308
21374
|
console.error(`Result file not found for job ${jobId}`);
|
|
20309
21375
|
process.exit(1);
|
|
20310
21376
|
}
|
|
20311
|
-
process.stdout.write(
|
|
21377
|
+
process.stdout.write(readFileSync9(resultPath, "utf-8"));
|
|
20312
21378
|
}
|
|
20313
21379
|
var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
20314
21380
|
var init_result = __esm(() => {
|
|
20315
21381
|
init_supervisor();
|
|
20316
21382
|
});
|
|
20317
21383
|
|
|
20318
|
-
// src/specialist/timeline-query.ts
|
|
20319
|
-
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
20320
|
-
import { join as join14 } from "node:path";
|
|
20321
|
-
function readJobEvents(jobDir) {
|
|
20322
|
-
const eventsPath = join14(jobDir, "events.jsonl");
|
|
20323
|
-
if (!existsSync11(eventsPath))
|
|
20324
|
-
return [];
|
|
20325
|
-
const content = readFileSync6(eventsPath, "utf-8");
|
|
20326
|
-
const lines = content.split(`
|
|
20327
|
-
`).filter(Boolean);
|
|
20328
|
-
const events = [];
|
|
20329
|
-
for (const line of lines) {
|
|
20330
|
-
const event = parseTimelineEvent(line);
|
|
20331
|
-
if (event)
|
|
20332
|
-
events.push(event);
|
|
20333
|
-
}
|
|
20334
|
-
events.sort(compareTimelineEvents);
|
|
20335
|
-
return events;
|
|
20336
|
-
}
|
|
20337
|
-
function readJobEventsById(jobsDir, jobId) {
|
|
20338
|
-
return readJobEvents(join14(jobsDir, jobId));
|
|
20339
|
-
}
|
|
20340
|
-
function readAllJobEvents(jobsDir) {
|
|
20341
|
-
if (!existsSync11(jobsDir))
|
|
20342
|
-
return [];
|
|
20343
|
-
const batches = [];
|
|
20344
|
-
const entries = readdirSync3(jobsDir);
|
|
20345
|
-
for (const entry of entries) {
|
|
20346
|
-
const jobDir = join14(jobsDir, entry);
|
|
20347
|
-
try {
|
|
20348
|
-
const stat2 = __require("node:fs").statSync(jobDir);
|
|
20349
|
-
if (!stat2.isDirectory())
|
|
20350
|
-
continue;
|
|
20351
|
-
} catch {
|
|
20352
|
-
continue;
|
|
20353
|
-
}
|
|
20354
|
-
const jobId = entry;
|
|
20355
|
-
const statusPath = join14(jobDir, "status.json");
|
|
20356
|
-
let specialist = "unknown";
|
|
20357
|
-
let beadId;
|
|
20358
|
-
if (existsSync11(statusPath)) {
|
|
20359
|
-
try {
|
|
20360
|
-
const status = JSON.parse(readFileSync6(statusPath, "utf-8"));
|
|
20361
|
-
specialist = status.specialist ?? "unknown";
|
|
20362
|
-
beadId = status.bead_id;
|
|
20363
|
-
} catch {}
|
|
20364
|
-
}
|
|
20365
|
-
const events = readJobEvents(jobDir);
|
|
20366
|
-
if (events.length > 0) {
|
|
20367
|
-
batches.push({ jobId, specialist, beadId, events });
|
|
20368
|
-
}
|
|
20369
|
-
}
|
|
20370
|
-
return batches;
|
|
20371
|
-
}
|
|
20372
|
-
function mergeTimelineEvents(batches) {
|
|
20373
|
-
const merged = [];
|
|
20374
|
-
for (const batch of batches) {
|
|
20375
|
-
for (const event of batch.events) {
|
|
20376
|
-
merged.push({
|
|
20377
|
-
jobId: batch.jobId,
|
|
20378
|
-
specialist: batch.specialist,
|
|
20379
|
-
beadId: batch.beadId,
|
|
20380
|
-
event
|
|
20381
|
-
});
|
|
20382
|
-
}
|
|
20383
|
-
}
|
|
20384
|
-
merged.sort((a, b) => compareTimelineEvents(a.event, b.event));
|
|
20385
|
-
return merged;
|
|
20386
|
-
}
|
|
20387
|
-
function filterTimelineEvents(merged, filter) {
|
|
20388
|
-
let result = merged;
|
|
20389
|
-
if (filter.since !== undefined) {
|
|
20390
|
-
result = result.filter(({ event }) => event.t >= filter.since);
|
|
20391
|
-
}
|
|
20392
|
-
if (filter.jobId !== undefined) {
|
|
20393
|
-
result = result.filter(({ jobId }) => jobId === filter.jobId);
|
|
20394
|
-
}
|
|
20395
|
-
if (filter.specialist !== undefined) {
|
|
20396
|
-
result = result.filter(({ specialist }) => specialist === filter.specialist);
|
|
20397
|
-
}
|
|
20398
|
-
if (filter.limit !== undefined && filter.limit > 0) {
|
|
20399
|
-
result = result.slice(0, filter.limit);
|
|
20400
|
-
}
|
|
20401
|
-
return result;
|
|
20402
|
-
}
|
|
20403
|
-
function queryTimeline(jobsDir, filter = {}) {
|
|
20404
|
-
let batches = readAllJobEvents(jobsDir);
|
|
20405
|
-
if (filter.jobId !== undefined) {
|
|
20406
|
-
batches = batches.filter((b) => b.jobId === filter.jobId);
|
|
20407
|
-
}
|
|
20408
|
-
if (filter.specialist !== undefined) {
|
|
20409
|
-
batches = batches.filter((b) => b.specialist === filter.specialist);
|
|
20410
|
-
}
|
|
20411
|
-
const merged = mergeTimelineEvents(batches);
|
|
20412
|
-
return filterTimelineEvents(merged, filter);
|
|
20413
|
-
}
|
|
20414
|
-
var init_timeline_query = __esm(() => {
|
|
20415
|
-
init_timeline_events();
|
|
20416
|
-
});
|
|
20417
|
-
|
|
20418
21384
|
// src/cli/feed.ts
|
|
20419
21385
|
var exports_feed = {};
|
|
20420
21386
|
__export(exports_feed, {
|
|
20421
|
-
run: () =>
|
|
21387
|
+
run: () => run12
|
|
20422
21388
|
});
|
|
20423
|
-
import { existsSync as
|
|
20424
|
-
import { join as
|
|
21389
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
|
|
21390
|
+
import { join as join20 } from "node:path";
|
|
20425
21391
|
function getHumanEventKey(event) {
|
|
20426
21392
|
switch (event.type) {
|
|
20427
21393
|
case "meta":
|
|
20428
21394
|
return `meta:${event.backend}:${event.model}`;
|
|
20429
21395
|
case "tool":
|
|
20430
|
-
return `tool:${event.tool}:${event.phase}:${event.
|
|
21396
|
+
return `tool:${event.tool}:${event.phase}:${event.tool_call_id ?? event.t}`;
|
|
20431
21397
|
case "text":
|
|
20432
21398
|
return "text";
|
|
20433
21399
|
case "thinking":
|
|
@@ -20440,13 +21406,21 @@ function getHumanEventKey(event) {
|
|
|
20440
21406
|
return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
|
|
20441
21407
|
case "run_complete":
|
|
20442
21408
|
return `run_complete:${event.status}:${event.error ?? ""}`;
|
|
20443
|
-
case "done":
|
|
20444
|
-
case "agent_end":
|
|
20445
|
-
return `complete:${event.type}`;
|
|
20446
21409
|
default:
|
|
20447
21410
|
return event.type;
|
|
20448
21411
|
}
|
|
20449
21412
|
}
|
|
21413
|
+
function shouldRenderHumanEvent(event) {
|
|
21414
|
+
if (event.type === "message" || event.type === "turn")
|
|
21415
|
+
return false;
|
|
21416
|
+
if (event.type === "tool") {
|
|
21417
|
+
if (event.phase === "update")
|
|
21418
|
+
return false;
|
|
21419
|
+
if (event.phase === "end" && !event.is_error)
|
|
21420
|
+
return false;
|
|
21421
|
+
}
|
|
21422
|
+
return true;
|
|
21423
|
+
}
|
|
20450
21424
|
function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
|
|
20451
21425
|
if (event.type === "meta") {
|
|
20452
21426
|
const metaKey = `${event.backend}:${event.model}`;
|
|
@@ -20454,6 +21428,9 @@ function shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey) {
|
|
|
20454
21428
|
return true;
|
|
20455
21429
|
seenMetaKey.set(jobId, metaKey);
|
|
20456
21430
|
}
|
|
21431
|
+
if (event.type === "tool") {
|
|
21432
|
+
return false;
|
|
21433
|
+
}
|
|
20457
21434
|
const key = getHumanEventKey(event);
|
|
20458
21435
|
if (lastPrintedEventKey.get(jobId) === key)
|
|
20459
21436
|
return true;
|
|
@@ -20473,7 +21450,36 @@ function parseSince(value) {
|
|
|
20473
21450
|
}
|
|
20474
21451
|
return;
|
|
20475
21452
|
}
|
|
20476
|
-
function
|
|
21453
|
+
function isTerminalJobStatus(jobsDir, jobId) {
|
|
21454
|
+
const statusPath = join20(jobsDir, jobId, "status.json");
|
|
21455
|
+
try {
|
|
21456
|
+
const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
|
|
21457
|
+
return status.status === "done" || status.status === "error";
|
|
21458
|
+
} catch {
|
|
21459
|
+
return false;
|
|
21460
|
+
}
|
|
21461
|
+
}
|
|
21462
|
+
function makeJobMetaReader(jobsDir) {
|
|
21463
|
+
const cache = new Map;
|
|
21464
|
+
return (jobId) => {
|
|
21465
|
+
if (cache.has(jobId))
|
|
21466
|
+
return cache.get(jobId);
|
|
21467
|
+
const statusPath = join20(jobsDir, jobId, "status.json");
|
|
21468
|
+
let meta = { startedAtMs: Date.now() };
|
|
21469
|
+
try {
|
|
21470
|
+
const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
|
|
21471
|
+
meta = {
|
|
21472
|
+
model: status.model,
|
|
21473
|
+
backend: status.backend,
|
|
21474
|
+
beadId: status.bead_id,
|
|
21475
|
+
startedAtMs: status.started_at_ms ?? Date.now()
|
|
21476
|
+
};
|
|
21477
|
+
} catch {}
|
|
21478
|
+
cache.set(jobId, meta);
|
|
21479
|
+
return meta;
|
|
21480
|
+
};
|
|
21481
|
+
}
|
|
21482
|
+
function parseArgs8(argv) {
|
|
20477
21483
|
let jobId;
|
|
20478
21484
|
let specialist;
|
|
20479
21485
|
let since;
|
|
@@ -20515,33 +21521,52 @@ function parseArgs6(argv) {
|
|
|
20515
21521
|
}
|
|
20516
21522
|
return { jobId, specialist, since, limit, follow, forever, json };
|
|
20517
21523
|
}
|
|
20518
|
-
function printSnapshot(merged, options) {
|
|
21524
|
+
function printSnapshot(merged, options, jobsDir) {
|
|
20519
21525
|
if (merged.length === 0) {
|
|
20520
21526
|
if (!options.json)
|
|
20521
|
-
console.log(
|
|
21527
|
+
console.log(dim7("No events found."));
|
|
20522
21528
|
return;
|
|
20523
21529
|
}
|
|
20524
21530
|
const colorMap = new JobColorMap;
|
|
20525
21531
|
if (options.json) {
|
|
21532
|
+
const getJobMeta2 = jobsDir ? makeJobMetaReader(jobsDir) : () => ({ startedAtMs: Date.now() });
|
|
20526
21533
|
for (const { jobId, specialist, beadId, event } of merged) {
|
|
20527
|
-
|
|
21534
|
+
const meta = getJobMeta2(jobId);
|
|
21535
|
+
const model = meta.model ?? (event.type === "meta" ? event.model : undefined);
|
|
21536
|
+
const backend = meta.backend ?? (event.type === "meta" ? event.backend : undefined);
|
|
21537
|
+
console.log(JSON.stringify({
|
|
21538
|
+
jobId,
|
|
21539
|
+
specialist,
|
|
21540
|
+
specialist_model: formatSpecialistModel(specialist, model),
|
|
21541
|
+
model,
|
|
21542
|
+
backend,
|
|
21543
|
+
beadId: meta.beadId ?? beadId,
|
|
21544
|
+
elapsed_ms: Date.now() - meta.startedAtMs,
|
|
21545
|
+
...event
|
|
21546
|
+
}));
|
|
20528
21547
|
}
|
|
20529
21548
|
return;
|
|
20530
21549
|
}
|
|
20531
21550
|
const lastPrintedEventKey = new Map;
|
|
20532
21551
|
const seenMetaKey = new Map;
|
|
21552
|
+
const getJobMeta = jobsDir ? makeJobMetaReader(jobsDir) : () => ({ startedAtMs: Date.now() });
|
|
20533
21553
|
for (const { jobId, specialist, beadId, event } of merged) {
|
|
21554
|
+
if (!shouldRenderHumanEvent(event))
|
|
21555
|
+
continue;
|
|
20534
21556
|
if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
|
|
20535
21557
|
continue;
|
|
20536
21558
|
const colorize = colorMap.get(jobId);
|
|
20537
|
-
|
|
21559
|
+
const meta = getJobMeta(jobId);
|
|
21560
|
+
const specialistDisplay = formatSpecialistModel(specialist, meta.model ?? (event.type === "meta" ? event.model : undefined));
|
|
21561
|
+
console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
|
|
20538
21562
|
}
|
|
20539
21563
|
}
|
|
20540
21564
|
function isCompletionEvent(event) {
|
|
20541
|
-
return isRunCompleteEvent(event)
|
|
21565
|
+
return isRunCompleteEvent(event);
|
|
20542
21566
|
}
|
|
20543
21567
|
async function followMerged(jobsDir, options) {
|
|
20544
21568
|
const colorMap = new JobColorMap;
|
|
21569
|
+
const getJobMeta = makeJobMetaReader(jobsDir);
|
|
20545
21570
|
const lastSeenT = new Map;
|
|
20546
21571
|
const completedJobs = new Set;
|
|
20547
21572
|
const filteredBatches = () => readAllJobEvents(jobsDir).filter((batch) => !options.jobId || batch.jobId === options.jobId).filter((batch) => !options.specialist || batch.specialist === options.specialist);
|
|
@@ -20551,26 +21576,26 @@ async function followMerged(jobsDir, options) {
|
|
|
20551
21576
|
since: options.since,
|
|
20552
21577
|
limit: options.limit
|
|
20553
21578
|
});
|
|
20554
|
-
printSnapshot(initial, { ...options, json: options.json });
|
|
21579
|
+
printSnapshot(initial, { ...options, json: options.json }, jobsDir);
|
|
20555
21580
|
for (const batch of filteredBatches()) {
|
|
20556
21581
|
if (batch.events.length > 0) {
|
|
20557
21582
|
const maxT = Math.max(...batch.events.map((event) => event.t));
|
|
20558
21583
|
lastSeenT.set(batch.jobId, maxT);
|
|
20559
21584
|
}
|
|
20560
|
-
if (batch.events.some(isCompletionEvent)) {
|
|
21585
|
+
if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
|
|
20561
21586
|
completedJobs.add(batch.jobId);
|
|
20562
21587
|
}
|
|
20563
21588
|
}
|
|
20564
21589
|
const initialBatchCount = filteredBatches().length;
|
|
20565
21590
|
if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
|
|
20566
21591
|
if (!options.json) {
|
|
20567
|
-
process.stderr.write(
|
|
21592
|
+
process.stderr.write(dim7(`All jobs complete.
|
|
20568
21593
|
`));
|
|
20569
21594
|
}
|
|
20570
21595
|
return;
|
|
20571
21596
|
}
|
|
20572
21597
|
if (!options.json) {
|
|
20573
|
-
process.stderr.write(
|
|
21598
|
+
process.stderr.write(dim7(`Following... (Ctrl+C to stop)
|
|
20574
21599
|
`));
|
|
20575
21600
|
}
|
|
20576
21601
|
const lastPrintedEventKey = new Map;
|
|
@@ -20595,19 +21620,34 @@ async function followMerged(jobsDir, options) {
|
|
|
20595
21620
|
const maxT = Math.max(...batch.events.map((e) => e.t));
|
|
20596
21621
|
lastSeenT.set(batch.jobId, maxT);
|
|
20597
21622
|
}
|
|
20598
|
-
if (batch.events.some(isCompletionEvent)) {
|
|
21623
|
+
if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
|
|
20599
21624
|
completedJobs.add(batch.jobId);
|
|
20600
21625
|
}
|
|
20601
21626
|
}
|
|
20602
21627
|
newEvents.sort((a, b) => a.event.t - b.event.t);
|
|
20603
21628
|
for (const { jobId, specialist, beadId, event } of newEvents) {
|
|
21629
|
+
const meta = getJobMeta(jobId);
|
|
21630
|
+
const model = meta.model ?? (event.type === "meta" ? event.model : undefined);
|
|
21631
|
+
const backend = meta.backend ?? (event.type === "meta" ? event.backend : undefined);
|
|
20604
21632
|
if (options.json) {
|
|
20605
|
-
console.log(JSON.stringify({
|
|
21633
|
+
console.log(JSON.stringify({
|
|
21634
|
+
jobId,
|
|
21635
|
+
specialist,
|
|
21636
|
+
specialist_model: formatSpecialistModel(specialist, model),
|
|
21637
|
+
model,
|
|
21638
|
+
backend,
|
|
21639
|
+
beadId: meta.beadId ?? beadId,
|
|
21640
|
+
elapsed_ms: Date.now() - meta.startedAtMs,
|
|
21641
|
+
...event
|
|
21642
|
+
}));
|
|
20606
21643
|
} else {
|
|
21644
|
+
if (!shouldRenderHumanEvent(event))
|
|
21645
|
+
continue;
|
|
20607
21646
|
if (shouldSkipHumanEvent(event, jobId, lastPrintedEventKey, seenMetaKey))
|
|
20608
21647
|
continue;
|
|
20609
21648
|
const colorize = colorMap.get(jobId);
|
|
20610
|
-
|
|
21649
|
+
const specialistDisplay = formatSpecialistModel(specialist, model);
|
|
21650
|
+
console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
|
|
20611
21651
|
}
|
|
20612
21652
|
}
|
|
20613
21653
|
if (!options.forever && batches.length > 0 && completedJobs.size === batches.length) {
|
|
@@ -20617,37 +21657,11 @@ async function followMerged(jobsDir, options) {
|
|
|
20617
21657
|
}, 500);
|
|
20618
21658
|
});
|
|
20619
21659
|
}
|
|
20620
|
-
function
|
|
20621
|
-
|
|
20622
|
-
|
|
20623
|
-
|
|
20624
|
-
|
|
20625
|
-
|
|
20626
|
-
Modes:
|
|
20627
|
-
specialists feed <job-id> Replay events for one job
|
|
20628
|
-
specialists feed <job-id> -f Follow one job until completion
|
|
20629
|
-
specialists feed -f Follow all jobs globally
|
|
20630
|
-
|
|
20631
|
-
Options:
|
|
20632
|
-
-f, --follow Follow live updates
|
|
20633
|
-
--forever Keep following in global mode even when all jobs complete
|
|
20634
|
-
|
|
20635
|
-
Examples:
|
|
20636
|
-
specialists feed 49adda
|
|
20637
|
-
specialists feed 49adda --follow
|
|
20638
|
-
specialists feed -f
|
|
20639
|
-
specialists feed -f --forever
|
|
20640
|
-
`);
|
|
20641
|
-
}
|
|
20642
|
-
async function run11() {
|
|
20643
|
-
const options = parseArgs6(process.argv.slice(3));
|
|
20644
|
-
if (!options.jobId && !options.follow) {
|
|
20645
|
-
showUsage();
|
|
20646
|
-
process.exit(1);
|
|
20647
|
-
}
|
|
20648
|
-
const jobsDir = join15(process.cwd(), ".specialists", "jobs");
|
|
20649
|
-
if (!existsSync12(jobsDir)) {
|
|
20650
|
-
console.log(dim8("No jobs directory found."));
|
|
21660
|
+
async function run12() {
|
|
21661
|
+
const options = parseArgs8(process.argv.slice(3));
|
|
21662
|
+
const jobsDir = join20(process.cwd(), ".specialists", "jobs");
|
|
21663
|
+
if (!existsSync15(jobsDir)) {
|
|
21664
|
+
console.log(dim7("No jobs directory found."));
|
|
20651
21665
|
return;
|
|
20652
21666
|
}
|
|
20653
21667
|
if (options.follow) {
|
|
@@ -20660,7 +21674,7 @@ async function run11() {
|
|
|
20660
21674
|
since: options.since,
|
|
20661
21675
|
limit: options.limit
|
|
20662
21676
|
});
|
|
20663
|
-
printSnapshot(merged, options);
|
|
21677
|
+
printSnapshot(merged, options, jobsDir);
|
|
20664
21678
|
}
|
|
20665
21679
|
var init_feed = __esm(() => {
|
|
20666
21680
|
init_timeline_events();
|
|
@@ -20671,14 +21685,14 @@ var init_feed = __esm(() => {
|
|
|
20671
21685
|
// src/cli/poll.ts
|
|
20672
21686
|
var exports_poll = {};
|
|
20673
21687
|
__export(exports_poll, {
|
|
20674
|
-
run: () =>
|
|
21688
|
+
run: () => run13
|
|
20675
21689
|
});
|
|
20676
|
-
import { existsSync as
|
|
20677
|
-
import { join as
|
|
20678
|
-
function
|
|
21690
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
|
|
21691
|
+
import { join as join21 } from "node:path";
|
|
21692
|
+
function parseArgs9(argv) {
|
|
20679
21693
|
let jobId;
|
|
20680
21694
|
let cursor = 0;
|
|
20681
|
-
let
|
|
21695
|
+
let outputCursor = 0;
|
|
20682
21696
|
for (let i = 0;i < argv.length; i++) {
|
|
20683
21697
|
if (argv[i] === "--cursor" && argv[i + 1]) {
|
|
20684
21698
|
cursor = parseInt(argv[++i], 10);
|
|
@@ -20686,50 +21700,44 @@ function parseArgs7(argv) {
|
|
|
20686
21700
|
cursor = 0;
|
|
20687
21701
|
continue;
|
|
20688
21702
|
}
|
|
21703
|
+
if (argv[i] === "--output-cursor" && argv[i + 1]) {
|
|
21704
|
+
outputCursor = parseInt(argv[++i], 10);
|
|
21705
|
+
if (isNaN(outputCursor) || outputCursor < 0)
|
|
21706
|
+
outputCursor = 0;
|
|
21707
|
+
continue;
|
|
21708
|
+
}
|
|
20689
21709
|
if (argv[i] === "--json") {
|
|
20690
|
-
json = true;
|
|
20691
21710
|
continue;
|
|
20692
21711
|
}
|
|
20693
|
-
if (
|
|
20694
|
-
|
|
21712
|
+
if (argv[i] === "--follow" || argv[i] === "-f") {
|
|
21713
|
+
process.stderr.write(`--follow removed from poll. Use 'specialists feed --follow' for live human-readable output.
|
|
21714
|
+
`);
|
|
21715
|
+
process.exit(1);
|
|
20695
21716
|
}
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
|
|
20699
|
-
|
|
20700
|
-
|
|
20701
|
-
|
|
20702
|
-
}
|
|
20703
|
-
async function run12() {
|
|
20704
|
-
const { jobId, cursor, json } = parseArgs7(process.argv.slice(3));
|
|
20705
|
-
const jobsDir = join16(process.cwd(), ".specialists", "jobs");
|
|
20706
|
-
const jobDir = join16(jobsDir, jobId);
|
|
20707
|
-
if (!existsSync13(jobDir)) {
|
|
20708
|
-
const result2 = {
|
|
20709
|
-
job_id: jobId,
|
|
20710
|
-
status: "error",
|
|
20711
|
-
elapsed_ms: 0,
|
|
20712
|
-
cursor: 0,
|
|
20713
|
-
output: "",
|
|
20714
|
-
output_delta: "",
|
|
20715
|
-
events: [],
|
|
20716
|
-
error: `Job not found: ${jobId}`
|
|
20717
|
-
};
|
|
20718
|
-
console.log(JSON.stringify(result2));
|
|
21717
|
+
if (!argv[i].startsWith("-")) {
|
|
21718
|
+
jobId = argv[i];
|
|
21719
|
+
}
|
|
21720
|
+
}
|
|
21721
|
+
if (!jobId) {
|
|
21722
|
+
console.error("Usage: specialists poll <job-id> [--cursor N] [--output-cursor N]");
|
|
20719
21723
|
process.exit(1);
|
|
20720
21724
|
}
|
|
20721
|
-
|
|
21725
|
+
return { jobId, cursor, outputCursor };
|
|
21726
|
+
}
|
|
21727
|
+
function readJobState(jobsDir, jobId, cursor, outputCursor) {
|
|
21728
|
+
const jobDir = join21(jobsDir, jobId);
|
|
21729
|
+
const statusPath = join21(jobDir, "status.json");
|
|
20722
21730
|
let status = null;
|
|
20723
|
-
if (
|
|
21731
|
+
if (existsSync16(statusPath)) {
|
|
20724
21732
|
try {
|
|
20725
|
-
status = JSON.parse(
|
|
21733
|
+
status = JSON.parse(readFileSync11(statusPath, "utf-8"));
|
|
20726
21734
|
} catch {}
|
|
20727
21735
|
}
|
|
20728
|
-
const resultPath =
|
|
20729
|
-
let
|
|
20730
|
-
if (
|
|
21736
|
+
const resultPath = join21(jobDir, "result.txt");
|
|
21737
|
+
let fullOutput = "";
|
|
21738
|
+
if (existsSync16(resultPath)) {
|
|
20731
21739
|
try {
|
|
20732
|
-
|
|
21740
|
+
fullOutput = readFileSync11(resultPath, "utf-8");
|
|
20733
21741
|
} catch {}
|
|
20734
21742
|
}
|
|
20735
21743
|
const events = readJobEventsById(jobsDir, jobId);
|
|
@@ -20738,13 +21746,18 @@ async function run12() {
|
|
|
20738
21746
|
const startedAt = status?.started_at_ms ?? Date.now();
|
|
20739
21747
|
const lastEvent = status?.last_event_at_ms ?? Date.now();
|
|
20740
21748
|
const elapsedMs = status?.status === "done" || status?.status === "error" ? lastEvent - startedAt : Date.now() - startedAt;
|
|
20741
|
-
const
|
|
21749
|
+
const isDone = status?.status === "done";
|
|
21750
|
+
const output = isDone ? fullOutput : "";
|
|
21751
|
+
const outputDelta = fullOutput.length > outputCursor ? fullOutput.slice(outputCursor) : "";
|
|
21752
|
+
const nextOutputCursor = fullOutput.length;
|
|
21753
|
+
return {
|
|
20742
21754
|
job_id: jobId,
|
|
20743
21755
|
status: status?.status ?? "starting",
|
|
20744
21756
|
elapsed_ms: elapsedMs,
|
|
20745
21757
|
cursor: nextCursor,
|
|
20746
|
-
|
|
20747
|
-
|
|
21758
|
+
output_cursor: nextOutputCursor,
|
|
21759
|
+
output,
|
|
21760
|
+
output_delta: outputDelta,
|
|
20748
21761
|
events: newEvents,
|
|
20749
21762
|
current_event: status?.current_event,
|
|
20750
21763
|
current_tool: status?.current_tool,
|
|
@@ -20753,27 +21766,32 @@ async function run12() {
|
|
|
20753
21766
|
bead_id: status?.bead_id,
|
|
20754
21767
|
error: status?.error
|
|
20755
21768
|
};
|
|
20756
|
-
|
|
20757
|
-
|
|
20758
|
-
}
|
|
20759
|
-
|
|
20760
|
-
|
|
20761
|
-
|
|
20762
|
-
|
|
20763
|
-
|
|
20764
|
-
|
|
20765
|
-
|
|
20766
|
-
|
|
20767
|
-
|
|
20768
|
-
|
|
20769
|
-
|
|
20770
|
-
|
|
20771
|
-
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
|
-
|
|
20775
|
-
}
|
|
21769
|
+
}
|
|
21770
|
+
async function run13() {
|
|
21771
|
+
const { jobId, cursor, outputCursor } = parseArgs9(process.argv.slice(3));
|
|
21772
|
+
const jobsDir = join21(process.cwd(), ".specialists", "jobs");
|
|
21773
|
+
const jobDir = join21(jobsDir, jobId);
|
|
21774
|
+
if (!existsSync16(jobDir)) {
|
|
21775
|
+
const result2 = {
|
|
21776
|
+
job_id: jobId,
|
|
21777
|
+
status: "error",
|
|
21778
|
+
elapsed_ms: 0,
|
|
21779
|
+
cursor: 0,
|
|
21780
|
+
output_cursor: 0,
|
|
21781
|
+
output: "",
|
|
21782
|
+
output_delta: "",
|
|
21783
|
+
events: [],
|
|
21784
|
+
error: `Job not found: ${jobId}`
|
|
21785
|
+
};
|
|
21786
|
+
console.log(JSON.stringify(result2));
|
|
21787
|
+
process.exit(1);
|
|
20776
21788
|
}
|
|
21789
|
+
const result = readJobState(jobsDir, jobId, cursor, outputCursor);
|
|
21790
|
+
if (result.status !== "done" && result.status !== "error" && !result.output_delta) {
|
|
21791
|
+
process.stderr.write(`Tip: use 'specialists feed --follow' for live human-readable output.
|
|
21792
|
+
`);
|
|
21793
|
+
}
|
|
21794
|
+
console.log(JSON.stringify(result));
|
|
20777
21795
|
}
|
|
20778
21796
|
var init_poll = __esm(() => {
|
|
20779
21797
|
init_timeline_query();
|
|
@@ -20782,18 +21800,18 @@ var init_poll = __esm(() => {
|
|
|
20782
21800
|
// src/cli/steer.ts
|
|
20783
21801
|
var exports_steer = {};
|
|
20784
21802
|
__export(exports_steer, {
|
|
20785
|
-
run: () =>
|
|
21803
|
+
run: () => run14
|
|
20786
21804
|
});
|
|
20787
|
-
import { join as
|
|
21805
|
+
import { join as join22 } from "node:path";
|
|
20788
21806
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
20789
|
-
async function
|
|
21807
|
+
async function run14() {
|
|
20790
21808
|
const jobId = process.argv[3];
|
|
20791
21809
|
const message = process.argv[4];
|
|
20792
21810
|
if (!jobId || !message) {
|
|
20793
21811
|
console.error('Usage: specialists|sp steer <job-id> "<message>"');
|
|
20794
21812
|
process.exit(1);
|
|
20795
21813
|
}
|
|
20796
|
-
const jobsDir =
|
|
21814
|
+
const jobsDir = join22(process.cwd(), ".specialists", "jobs");
|
|
20797
21815
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
20798
21816
|
const status = supervisor.readStatus(jobId);
|
|
20799
21817
|
if (!status) {
|
|
@@ -20808,7 +21826,7 @@ async function run13() {
|
|
|
20808
21826
|
if (!status.fifo_path) {
|
|
20809
21827
|
process.stderr.write(`${red4("Error:")} Job ${jobId} has no steer pipe.
|
|
20810
21828
|
`);
|
|
20811
|
-
process.stderr.write(`
|
|
21829
|
+
process.stderr.write(`FIFO support may not be available on this system (mkfifo failed at job start).
|
|
20812
21830
|
`);
|
|
20813
21831
|
process.exit(1);
|
|
20814
21832
|
}
|
|
@@ -20816,7 +21834,7 @@ async function run13() {
|
|
|
20816
21834
|
const payload = JSON.stringify({ type: "steer", message }) + `
|
|
20817
21835
|
`;
|
|
20818
21836
|
writeFileSync6(status.fifo_path, payload, { flag: "a" });
|
|
20819
|
-
process.stdout.write(`${
|
|
21837
|
+
process.stdout.write(`${green9("✓")} Steer message sent to job ${jobId}
|
|
20820
21838
|
`);
|
|
20821
21839
|
} catch (err) {
|
|
20822
21840
|
process.stderr.write(`${red4("Error:")} Failed to write to steer pipe: ${err?.message}
|
|
@@ -20824,26 +21842,26 @@ async function run13() {
|
|
|
20824
21842
|
process.exit(1);
|
|
20825
21843
|
}
|
|
20826
21844
|
}
|
|
20827
|
-
var
|
|
21845
|
+
var green9 = (s) => `\x1B[32m${s}\x1B[0m`, red4 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
20828
21846
|
var init_steer = __esm(() => {
|
|
20829
21847
|
init_supervisor();
|
|
20830
21848
|
});
|
|
20831
21849
|
|
|
20832
|
-
// src/cli/
|
|
20833
|
-
var
|
|
20834
|
-
__export(
|
|
20835
|
-
run: () =>
|
|
21850
|
+
// src/cli/resume.ts
|
|
21851
|
+
var exports_resume = {};
|
|
21852
|
+
__export(exports_resume, {
|
|
21853
|
+
run: () => run15
|
|
20836
21854
|
});
|
|
20837
|
-
import { join as
|
|
21855
|
+
import { join as join23 } from "node:path";
|
|
20838
21856
|
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
20839
|
-
async function
|
|
21857
|
+
async function run15() {
|
|
20840
21858
|
const jobId = process.argv[3];
|
|
20841
|
-
const
|
|
20842
|
-
if (!jobId || !
|
|
20843
|
-
console.error('Usage: specialists|sp
|
|
21859
|
+
const task = process.argv[4];
|
|
21860
|
+
if (!jobId || !task) {
|
|
21861
|
+
console.error('Usage: specialists|sp resume <job-id> "<task>"');
|
|
20844
21862
|
process.exit(1);
|
|
20845
21863
|
}
|
|
20846
|
-
const jobsDir =
|
|
21864
|
+
const jobsDir = join23(process.cwd(), ".specialists", "jobs");
|
|
20847
21865
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
20848
21866
|
const status = supervisor.readStatus(jobId);
|
|
20849
21867
|
if (!status) {
|
|
@@ -20853,7 +21871,7 @@ async function run14() {
|
|
|
20853
21871
|
if (status.status !== "waiting") {
|
|
20854
21872
|
process.stderr.write(`${red5("Error:")} Job ${jobId} is not in waiting state (status: ${status.status}).
|
|
20855
21873
|
`);
|
|
20856
|
-
process.stderr.write(`
|
|
21874
|
+
process.stderr.write(`resume is only valid for keep-alive jobs in waiting state. Use steer for running jobs.
|
|
20857
21875
|
`);
|
|
20858
21876
|
process.exit(1);
|
|
20859
21877
|
}
|
|
@@ -20863,10 +21881,10 @@ async function run14() {
|
|
|
20863
21881
|
process.exit(1);
|
|
20864
21882
|
}
|
|
20865
21883
|
try {
|
|
20866
|
-
const payload = JSON.stringify({ type: "
|
|
21884
|
+
const payload = JSON.stringify({ type: "resume", task }) + `
|
|
20867
21885
|
`;
|
|
20868
21886
|
writeFileSync7(status.fifo_path, payload, { flag: "a" });
|
|
20869
|
-
process.stdout.write(`${
|
|
21887
|
+
process.stdout.write(`${green10("✓")} Resume sent to job ${jobId}
|
|
20870
21888
|
`);
|
|
20871
21889
|
process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
|
|
20872
21890
|
`);
|
|
@@ -20876,24 +21894,226 @@ async function run14() {
|
|
|
20876
21894
|
process.exit(1);
|
|
20877
21895
|
}
|
|
20878
21896
|
}
|
|
20879
|
-
var
|
|
20880
|
-
var
|
|
21897
|
+
var green10 = (s) => `\x1B[32m${s}\x1B[0m`, red5 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
21898
|
+
var init_resume = __esm(() => {
|
|
20881
21899
|
init_supervisor();
|
|
20882
21900
|
});
|
|
20883
21901
|
|
|
21902
|
+
// src/cli/follow-up.ts
|
|
21903
|
+
var exports_follow_up = {};
|
|
21904
|
+
__export(exports_follow_up, {
|
|
21905
|
+
run: () => run16
|
|
21906
|
+
});
|
|
21907
|
+
async function run16() {
|
|
21908
|
+
process.stderr.write("\x1B[33m⚠ DEPRECATED:\x1B[0m `specialists follow-up` is deprecated. Use `specialists resume` instead.\n\n");
|
|
21909
|
+
const { run: resumeRun } = await Promise.resolve().then(() => (init_resume(), exports_resume));
|
|
21910
|
+
return resumeRun();
|
|
21911
|
+
}
|
|
21912
|
+
|
|
21913
|
+
// src/cli/clean.ts
|
|
21914
|
+
var exports_clean = {};
|
|
21915
|
+
__export(exports_clean, {
|
|
21916
|
+
run: () => run17
|
|
21917
|
+
});
|
|
21918
|
+
import {
|
|
21919
|
+
existsSync as existsSync17,
|
|
21920
|
+
readdirSync as readdirSync5,
|
|
21921
|
+
readFileSync as readFileSync12,
|
|
21922
|
+
rmSync as rmSync2,
|
|
21923
|
+
statSync as statSync2
|
|
21924
|
+
} from "node:fs";
|
|
21925
|
+
import { join as join24 } from "node:path";
|
|
21926
|
+
function parseTtlDaysFromEnvironment() {
|
|
21927
|
+
const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
|
|
21928
|
+
if (!rawValue)
|
|
21929
|
+
return DEFAULT_TTL_DAYS;
|
|
21930
|
+
const parsedValue = Number(rawValue);
|
|
21931
|
+
if (!Number.isFinite(parsedValue) || parsedValue < 0)
|
|
21932
|
+
return DEFAULT_TTL_DAYS;
|
|
21933
|
+
return parsedValue;
|
|
21934
|
+
}
|
|
21935
|
+
function parseOptions(argv) {
|
|
21936
|
+
let removeAllCompleted = false;
|
|
21937
|
+
let dryRun = false;
|
|
21938
|
+
let keepRecentCount = null;
|
|
21939
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
21940
|
+
const argument = argv[index];
|
|
21941
|
+
if (argument === "--all") {
|
|
21942
|
+
removeAllCompleted = true;
|
|
21943
|
+
continue;
|
|
21944
|
+
}
|
|
21945
|
+
if (argument === "--dry-run") {
|
|
21946
|
+
dryRun = true;
|
|
21947
|
+
continue;
|
|
21948
|
+
}
|
|
21949
|
+
if (argument === "--keep") {
|
|
21950
|
+
const value = argv[index + 1];
|
|
21951
|
+
if (!value) {
|
|
21952
|
+
throw new Error("Missing value for --keep");
|
|
21953
|
+
}
|
|
21954
|
+
const parsedValue = Number(value);
|
|
21955
|
+
const isInteger = Number.isInteger(parsedValue);
|
|
21956
|
+
if (!isInteger || parsedValue < 0) {
|
|
21957
|
+
throw new Error("--keep must be a non-negative integer");
|
|
21958
|
+
}
|
|
21959
|
+
keepRecentCount = parsedValue;
|
|
21960
|
+
index += 1;
|
|
21961
|
+
continue;
|
|
21962
|
+
}
|
|
21963
|
+
if (argument.startsWith("--keep=")) {
|
|
21964
|
+
const value = argument.slice("--keep=".length);
|
|
21965
|
+
const parsedValue = Number(value);
|
|
21966
|
+
const isInteger = Number.isInteger(parsedValue);
|
|
21967
|
+
if (!isInteger || parsedValue < 0) {
|
|
21968
|
+
throw new Error("--keep must be a non-negative integer");
|
|
21969
|
+
}
|
|
21970
|
+
keepRecentCount = parsedValue;
|
|
21971
|
+
continue;
|
|
21972
|
+
}
|
|
21973
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
21974
|
+
}
|
|
21975
|
+
return { removeAllCompleted, dryRun, keepRecentCount };
|
|
21976
|
+
}
|
|
21977
|
+
function readDirectorySizeBytes(directoryPath) {
|
|
21978
|
+
let totalBytes = 0;
|
|
21979
|
+
const entries = readdirSync5(directoryPath, { withFileTypes: true });
|
|
21980
|
+
for (const entry of entries) {
|
|
21981
|
+
const entryPath = join24(directoryPath, entry.name);
|
|
21982
|
+
const stats = statSync2(entryPath);
|
|
21983
|
+
if (stats.isDirectory()) {
|
|
21984
|
+
totalBytes += readDirectorySizeBytes(entryPath);
|
|
21985
|
+
continue;
|
|
21986
|
+
}
|
|
21987
|
+
totalBytes += stats.size;
|
|
21988
|
+
}
|
|
21989
|
+
return totalBytes;
|
|
21990
|
+
}
|
|
21991
|
+
function readCompletedJobDirectory(baseDirectory, entry) {
|
|
21992
|
+
if (!entry.isDirectory())
|
|
21993
|
+
return null;
|
|
21994
|
+
const directoryPath = join24(baseDirectory, entry.name);
|
|
21995
|
+
const statusFilePath = join24(directoryPath, "status.json");
|
|
21996
|
+
if (!existsSync17(statusFilePath))
|
|
21997
|
+
return null;
|
|
21998
|
+
let statusData;
|
|
21999
|
+
try {
|
|
22000
|
+
statusData = JSON.parse(readFileSync12(statusFilePath, "utf-8"));
|
|
22001
|
+
} catch {
|
|
22002
|
+
return null;
|
|
22003
|
+
}
|
|
22004
|
+
if (!COMPLETED_STATUSES.has(statusData.status))
|
|
22005
|
+
return null;
|
|
22006
|
+
const directoryStats = statSync2(directoryPath);
|
|
22007
|
+
return {
|
|
22008
|
+
id: entry.name,
|
|
22009
|
+
directoryPath,
|
|
22010
|
+
modifiedAtMs: directoryStats.mtimeMs,
|
|
22011
|
+
startedAtMs: statusData.started_at_ms,
|
|
22012
|
+
sizeBytes: readDirectorySizeBytes(directoryPath)
|
|
22013
|
+
};
|
|
22014
|
+
}
|
|
22015
|
+
function collectCompletedJobDirectories(jobsDirectoryPath) {
|
|
22016
|
+
const entries = readdirSync5(jobsDirectoryPath, { withFileTypes: true });
|
|
22017
|
+
const completedJobs = [];
|
|
22018
|
+
for (const entry of entries) {
|
|
22019
|
+
const completedJob = readCompletedJobDirectory(jobsDirectoryPath, entry);
|
|
22020
|
+
if (completedJob) {
|
|
22021
|
+
completedJobs.push(completedJob);
|
|
22022
|
+
}
|
|
22023
|
+
}
|
|
22024
|
+
return completedJobs;
|
|
22025
|
+
}
|
|
22026
|
+
function selectJobsToRemove(completedJobs, options) {
|
|
22027
|
+
const jobsByNewest = [...completedJobs].sort((left, right) => {
|
|
22028
|
+
if (right.startedAtMs !== left.startedAtMs) {
|
|
22029
|
+
return right.startedAtMs - left.startedAtMs;
|
|
22030
|
+
}
|
|
22031
|
+
return right.modifiedAtMs - left.modifiedAtMs;
|
|
22032
|
+
});
|
|
22033
|
+
if (options.keepRecentCount !== null) {
|
|
22034
|
+
return jobsByNewest.slice(options.keepRecentCount);
|
|
22035
|
+
}
|
|
22036
|
+
if (options.removeAllCompleted) {
|
|
22037
|
+
return jobsByNewest;
|
|
22038
|
+
}
|
|
22039
|
+
const ttlDays = parseTtlDaysFromEnvironment();
|
|
22040
|
+
const cutoffMs = Date.now() - ttlDays * MS_PER_DAY;
|
|
22041
|
+
return jobsByNewest.filter((job) => job.modifiedAtMs < cutoffMs);
|
|
22042
|
+
}
|
|
22043
|
+
function formatBytes(bytes) {
|
|
22044
|
+
if (bytes < 1024)
|
|
22045
|
+
return `${bytes} B`;
|
|
22046
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
22047
|
+
let value = bytes / 1024;
|
|
22048
|
+
let unitIndex = 0;
|
|
22049
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
22050
|
+
value /= 1024;
|
|
22051
|
+
unitIndex += 1;
|
|
22052
|
+
}
|
|
22053
|
+
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
22054
|
+
}
|
|
22055
|
+
function renderSummary(removedCount, freedBytes, dryRun) {
|
|
22056
|
+
const action = dryRun ? "Would remove" : "Removed";
|
|
22057
|
+
const noun = removedCount === 1 ? "directory" : "directories";
|
|
22058
|
+
return `${action} ${removedCount} job ${noun} (${formatBytes(freedBytes)} freed)`;
|
|
22059
|
+
}
|
|
22060
|
+
function printDryRunPlan(jobs) {
|
|
22061
|
+
if (jobs.length === 0)
|
|
22062
|
+
return;
|
|
22063
|
+
console.log("Would remove:");
|
|
22064
|
+
for (const job of jobs) {
|
|
22065
|
+
console.log(` - ${job.id}`);
|
|
22066
|
+
}
|
|
22067
|
+
}
|
|
22068
|
+
function printUsageAndExit(message) {
|
|
22069
|
+
console.error(message);
|
|
22070
|
+
console.error("Usage: specialists|sp clean [--all] [--keep <n>] [--dry-run]");
|
|
22071
|
+
process.exit(1);
|
|
22072
|
+
}
|
|
22073
|
+
async function run17() {
|
|
22074
|
+
let options;
|
|
22075
|
+
try {
|
|
22076
|
+
options = parseOptions(process.argv.slice(3));
|
|
22077
|
+
} catch (error2) {
|
|
22078
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
22079
|
+
printUsageAndExit(message);
|
|
22080
|
+
}
|
|
22081
|
+
const jobsDirectoryPath = join24(process.cwd(), ".specialists", "jobs");
|
|
22082
|
+
if (!existsSync17(jobsDirectoryPath)) {
|
|
22083
|
+
console.log("No jobs directory found.");
|
|
22084
|
+
return;
|
|
22085
|
+
}
|
|
22086
|
+
const completedJobs = collectCompletedJobDirectories(jobsDirectoryPath);
|
|
22087
|
+
const jobsToRemove = selectJobsToRemove(completedJobs, options);
|
|
22088
|
+
const freedBytes = jobsToRemove.reduce((total, job) => total + job.sizeBytes, 0);
|
|
22089
|
+
if (options.dryRun) {
|
|
22090
|
+
printDryRunPlan(jobsToRemove);
|
|
22091
|
+
console.log(renderSummary(jobsToRemove.length, freedBytes, true));
|
|
22092
|
+
return;
|
|
22093
|
+
}
|
|
22094
|
+
for (const job of jobsToRemove) {
|
|
22095
|
+
rmSync2(job.directoryPath, { recursive: true, force: true });
|
|
22096
|
+
}
|
|
22097
|
+
console.log(renderSummary(jobsToRemove.length, freedBytes, false));
|
|
22098
|
+
}
|
|
22099
|
+
var MS_PER_DAY = 86400000, DEFAULT_TTL_DAYS = 7, COMPLETED_STATUSES;
|
|
22100
|
+
var init_clean = __esm(() => {
|
|
22101
|
+
COMPLETED_STATUSES = new Set(["done", "error"]);
|
|
22102
|
+
});
|
|
22103
|
+
|
|
20884
22104
|
// src/cli/stop.ts
|
|
20885
22105
|
var exports_stop = {};
|
|
20886
22106
|
__export(exports_stop, {
|
|
20887
|
-
run: () =>
|
|
22107
|
+
run: () => run18
|
|
20888
22108
|
});
|
|
20889
|
-
import { join as
|
|
20890
|
-
async function
|
|
22109
|
+
import { join as join25 } from "node:path";
|
|
22110
|
+
async function run18() {
|
|
20891
22111
|
const jobId = process.argv[3];
|
|
20892
22112
|
if (!jobId) {
|
|
20893
22113
|
console.error("Usage: specialists|sp stop <job-id>");
|
|
20894
22114
|
process.exit(1);
|
|
20895
22115
|
}
|
|
20896
|
-
const jobsDir =
|
|
22116
|
+
const jobsDir = join25(process.cwd(), ".specialists", "jobs");
|
|
20897
22117
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
20898
22118
|
const status = supervisor.readStatus(jobId);
|
|
20899
22119
|
if (!status) {
|
|
@@ -20912,7 +22132,7 @@ async function run15() {
|
|
|
20912
22132
|
}
|
|
20913
22133
|
try {
|
|
20914
22134
|
process.kill(status.pid, "SIGTERM");
|
|
20915
|
-
process.stdout.write(`${
|
|
22135
|
+
process.stdout.write(`${green11("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
20916
22136
|
`);
|
|
20917
22137
|
} catch (err) {
|
|
20918
22138
|
if (err.code === "ESRCH") {
|
|
@@ -20925,7 +22145,7 @@ async function run15() {
|
|
|
20925
22145
|
}
|
|
20926
22146
|
}
|
|
20927
22147
|
}
|
|
20928
|
-
var
|
|
22148
|
+
var green11 = (s) => `\x1B[32m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
20929
22149
|
var init_stop = __esm(() => {
|
|
20930
22150
|
init_supervisor();
|
|
20931
22151
|
});
|
|
@@ -20933,7 +22153,7 @@ var init_stop = __esm(() => {
|
|
|
20933
22153
|
// src/cli/quickstart.ts
|
|
20934
22154
|
var exports_quickstart = {};
|
|
20935
22155
|
__export(exports_quickstart, {
|
|
20936
|
-
run: () =>
|
|
22156
|
+
run: () => run19
|
|
20937
22157
|
});
|
|
20938
22158
|
function section2(title) {
|
|
20939
22159
|
const bar = "─".repeat(60);
|
|
@@ -20942,12 +22162,12 @@ ${bold9(cyan6(title))}
|
|
|
20942
22162
|
${dim11(bar)}`;
|
|
20943
22163
|
}
|
|
20944
22164
|
function cmd2(s) {
|
|
20945
|
-
return
|
|
22165
|
+
return yellow9(s);
|
|
20946
22166
|
}
|
|
20947
22167
|
function flag(s) {
|
|
20948
|
-
return
|
|
22168
|
+
return green12(s);
|
|
20949
22169
|
}
|
|
20950
|
-
async function
|
|
22170
|
+
async function run19() {
|
|
20951
22171
|
const lines = [
|
|
20952
22172
|
"",
|
|
20953
22173
|
bold9("specialists · Quick Start Guide"),
|
|
@@ -20967,11 +22187,12 @@ async function run16() {
|
|
|
20967
22187
|
lines.push(section2("2. Initialize a Project"));
|
|
20968
22188
|
lines.push("");
|
|
20969
22189
|
lines.push(` Run once per project root:`);
|
|
20970
|
-
lines.push(` ${cmd2("specialists init")} # creates
|
|
22190
|
+
lines.push(` ${cmd2("specialists init")} # creates .specialists/, wires MCP + AGENTS.md`);
|
|
20971
22191
|
lines.push("");
|
|
20972
22192
|
lines.push(` What this creates:`);
|
|
20973
|
-
lines.push(` ${dim11("specialists/")}
|
|
20974
|
-
lines.push(` ${dim11(".specialists/")}
|
|
22193
|
+
lines.push(` ${dim11(".specialists/default/")} — canonical specialists (from init)`);
|
|
22194
|
+
lines.push(` ${dim11(".specialists/user/")} — custom .specialist.yaml files`);
|
|
22195
|
+
lines.push(` ${dim11(".specialists/jobs|ready")} — runtime data — gitignored`);
|
|
20975
22196
|
lines.push(` ${dim11("AGENTS.md")} — context block injected into Claude sessions`);
|
|
20976
22197
|
lines.push("");
|
|
20977
22198
|
lines.push(section2("3. Discover Specialists"));
|
|
@@ -20983,8 +22204,8 @@ async function run16() {
|
|
|
20983
22204
|
lines.push(` ${cmd2("specialists list")} ${flag("--json")} # machine-readable JSON`);
|
|
20984
22205
|
lines.push("");
|
|
20985
22206
|
lines.push(` Scopes (searched in order, user wins on name collision):`);
|
|
20986
|
-
lines.push(` ${blue2("user")} .specialists/user
|
|
20987
|
-
lines.push(` ${blue2("default")} .specialists/default
|
|
22207
|
+
lines.push(` ${blue2("user")} .specialists/user/*.specialist.yaml`);
|
|
22208
|
+
lines.push(` ${blue2("default")} .specialists/default/*.specialist.yaml`);
|
|
20988
22209
|
lines.push("");
|
|
20989
22210
|
lines.push(section2("4. Running a Specialist"));
|
|
20990
22211
|
lines.push("");
|
|
@@ -21004,9 +22225,11 @@ async function run16() {
|
|
|
21004
22225
|
lines.push(` Pipe a prompt from stdin:`);
|
|
21005
22226
|
lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
|
|
21006
22227
|
lines.push("");
|
|
21007
|
-
lines.push(section2("5.
|
|
22228
|
+
lines.push(section2("5. Async Job Lifecycle"));
|
|
21008
22229
|
lines.push("");
|
|
21009
|
-
lines.push(`
|
|
22230
|
+
lines.push(` ${bold9("MCP pattern")}: ${cmd2("start_specialist")} → ${cmd2("feed_specialist")} (returns job_id directly)`);
|
|
22231
|
+
lines.push(` ${bold9("CLI pattern")}: ${cmd2('specialists run <name> --prompt "..."')} prints ${dim11("[job started: <id>]")} to stderr`);
|
|
22232
|
+
lines.push(` ${bold9("Shell pattern")}: ${cmd2('specialists run <name> --prompt "..." &')} for native backgrounding`);
|
|
21010
22233
|
lines.push("");
|
|
21011
22234
|
lines.push(` ${bold9("Watch progress")} — stream events as they arrive:`);
|
|
21012
22235
|
lines.push(` ${cmd2("specialists feed job_a1b2c3d4")} # print events so far`);
|
|
@@ -21100,10 +22323,10 @@ async function run16() {
|
|
|
21100
22323
|
lines.push(` Specialists emits lifecycle events to ${dim11(".specialists/trace.jsonl")}:`);
|
|
21101
22324
|
lines.push("");
|
|
21102
22325
|
lines.push(` ${bold9("Hook point")} ${bold9("When fired")}`);
|
|
21103
|
-
lines.push(` ${
|
|
21104
|
-
lines.push(` ${
|
|
21105
|
-
lines.push(` ${
|
|
21106
|
-
lines.push(` ${
|
|
22326
|
+
lines.push(` ${yellow9("specialist:start")} before the agent session begins`);
|
|
22327
|
+
lines.push(` ${yellow9("specialist:token")} on each streamed token (delta)`);
|
|
22328
|
+
lines.push(` ${yellow9("specialist:done")} after successful completion`);
|
|
22329
|
+
lines.push(` ${yellow9("specialist:error")} on failure or timeout`);
|
|
21107
22330
|
lines.push("");
|
|
21108
22331
|
lines.push(` Each event line in trace.jsonl:`);
|
|
21109
22332
|
lines.push(` ${dim11('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
|
|
@@ -21120,9 +22343,9 @@ async function run16() {
|
|
|
21120
22343
|
lines.push(` ${bold9("use_specialist")} — full lifecycle: load → agents.md → run → output`);
|
|
21121
22344
|
lines.push(` ${bold9("run_parallel")} — concurrent or pipeline execution`);
|
|
21122
22345
|
lines.push(` ${bold9("start_specialist")} — async job start, returns job ID`);
|
|
21123
|
-
lines.push(` ${bold9("
|
|
22346
|
+
lines.push(` ${bold9("feed_specialist")} — stream events/output by job ID`);
|
|
21124
22347
|
lines.push(` ${bold9("steer_specialist")} — send a mid-run message to a running job`);
|
|
21125
|
-
lines.push(` ${bold9("
|
|
22348
|
+
lines.push(` ${bold9("resume_specialist")} — resume a waiting keep-alive session with a next-turn prompt`);
|
|
21126
22349
|
lines.push(` ${bold9("stop_specialist")} — cancel a running job by ID`);
|
|
21127
22350
|
lines.push(` ${bold9("specialist_status")} — circuit breaker health + staleness`);
|
|
21128
22351
|
lines.push("");
|
|
@@ -21155,27 +22378,27 @@ async function run16() {
|
|
|
21155
22378
|
console.log(lines.join(`
|
|
21156
22379
|
`));
|
|
21157
22380
|
}
|
|
21158
|
-
var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`,
|
|
22381
|
+
var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, yellow9 = (s) => `\x1B[33m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, blue2 = (s) => `\x1B[34m${s}\x1B[0m`, green12 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
21159
22382
|
|
|
21160
22383
|
// src/cli/doctor.ts
|
|
21161
22384
|
var exports_doctor = {};
|
|
21162
22385
|
__export(exports_doctor, {
|
|
21163
|
-
run: () =>
|
|
22386
|
+
run: () => run20
|
|
21164
22387
|
});
|
|
21165
|
-
import { spawnSync as
|
|
21166
|
-
import { existsSync as
|
|
21167
|
-
import { join as
|
|
22388
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
22389
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync3, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "node:fs";
|
|
22390
|
+
import { join as join26 } from "node:path";
|
|
21168
22391
|
function ok3(msg) {
|
|
21169
|
-
console.log(` ${
|
|
22392
|
+
console.log(` ${green13("✓")} ${msg}`);
|
|
21170
22393
|
}
|
|
21171
22394
|
function warn2(msg) {
|
|
21172
|
-
console.log(` ${
|
|
22395
|
+
console.log(` ${yellow10("○")} ${msg}`);
|
|
21173
22396
|
}
|
|
21174
22397
|
function fail2(msg) {
|
|
21175
22398
|
console.log(` ${red7("✗")} ${msg}`);
|
|
21176
22399
|
}
|
|
21177
22400
|
function fix(msg) {
|
|
21178
|
-
console.log(` ${dim12("→ fix:")} ${
|
|
22401
|
+
console.log(` ${dim12("→ fix:")} ${yellow10(msg)}`);
|
|
21179
22402
|
}
|
|
21180
22403
|
function hint(msg) {
|
|
21181
22404
|
console.log(` ${dim12(msg)}`);
|
|
@@ -21186,17 +22409,17 @@ function section3(label) {
|
|
|
21186
22409
|
${bold10(`── ${label} ${line}`)}`);
|
|
21187
22410
|
}
|
|
21188
22411
|
function sp(bin, args) {
|
|
21189
|
-
const r =
|
|
22412
|
+
const r = spawnSync8(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
|
|
21190
22413
|
return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
|
|
21191
22414
|
}
|
|
21192
22415
|
function isInstalled2(bin) {
|
|
21193
|
-
return
|
|
22416
|
+
return spawnSync8("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
21194
22417
|
}
|
|
21195
22418
|
function loadJson2(path) {
|
|
21196
|
-
if (!
|
|
22419
|
+
if (!existsSync18(path))
|
|
21197
22420
|
return null;
|
|
21198
22421
|
try {
|
|
21199
|
-
return JSON.parse(
|
|
22422
|
+
return JSON.parse(readFileSync13(path, "utf8"));
|
|
21200
22423
|
} catch {
|
|
21201
22424
|
return null;
|
|
21202
22425
|
}
|
|
@@ -21239,7 +22462,7 @@ function checkBd() {
|
|
|
21239
22462
|
return false;
|
|
21240
22463
|
}
|
|
21241
22464
|
ok3(`bd installed ${dim12(sp("bd", ["--version"]).stdout || "")}`);
|
|
21242
|
-
if (
|
|
22465
|
+
if (existsSync18(join26(CWD, ".beads")))
|
|
21243
22466
|
ok3(".beads/ present in project");
|
|
21244
22467
|
else
|
|
21245
22468
|
warn2(".beads/ not found in project");
|
|
@@ -21259,8 +22482,8 @@ function checkHooks() {
|
|
|
21259
22482
|
section3("Claude Code hooks (2 expected)");
|
|
21260
22483
|
let allPresent = true;
|
|
21261
22484
|
for (const name of HOOK_NAMES) {
|
|
21262
|
-
const dest =
|
|
21263
|
-
if (!
|
|
22485
|
+
const dest = join26(HOOKS_DIR, name);
|
|
22486
|
+
if (!existsSync18(dest)) {
|
|
21264
22487
|
fail2(`${name} ${red7("missing")}`);
|
|
21265
22488
|
fix("specialists install");
|
|
21266
22489
|
allPresent = false;
|
|
@@ -21304,18 +22527,18 @@ function checkMCP() {
|
|
|
21304
22527
|
}
|
|
21305
22528
|
function checkRuntimeDirs() {
|
|
21306
22529
|
section3(".specialists/ runtime directories");
|
|
21307
|
-
const rootDir =
|
|
21308
|
-
const jobsDir =
|
|
21309
|
-
const readyDir =
|
|
22530
|
+
const rootDir = join26(CWD, ".specialists");
|
|
22531
|
+
const jobsDir = join26(rootDir, "jobs");
|
|
22532
|
+
const readyDir = join26(rootDir, "ready");
|
|
21310
22533
|
let allOk = true;
|
|
21311
|
-
if (!
|
|
22534
|
+
if (!existsSync18(rootDir)) {
|
|
21312
22535
|
warn2(".specialists/ not found in current project");
|
|
21313
22536
|
fix("specialists init");
|
|
21314
22537
|
allOk = false;
|
|
21315
22538
|
} else {
|
|
21316
22539
|
ok3(".specialists/ present");
|
|
21317
22540
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
21318
|
-
if (!
|
|
22541
|
+
if (!existsSync18(subDir)) {
|
|
21319
22542
|
warn2(`.specialists/${label}/ missing — auto-creating`);
|
|
21320
22543
|
mkdirSync3(subDir, { recursive: true });
|
|
21321
22544
|
ok3(`.specialists/${label}/ created`);
|
|
@@ -21328,14 +22551,14 @@ function checkRuntimeDirs() {
|
|
|
21328
22551
|
}
|
|
21329
22552
|
function checkZombieJobs() {
|
|
21330
22553
|
section3("Background jobs");
|
|
21331
|
-
const jobsDir =
|
|
21332
|
-
if (!
|
|
22554
|
+
const jobsDir = join26(CWD, ".specialists", "jobs");
|
|
22555
|
+
if (!existsSync18(jobsDir)) {
|
|
21333
22556
|
hint("No .specialists/jobs/ — skipping");
|
|
21334
22557
|
return true;
|
|
21335
22558
|
}
|
|
21336
22559
|
let entries;
|
|
21337
22560
|
try {
|
|
21338
|
-
entries =
|
|
22561
|
+
entries = readdirSync6(jobsDir);
|
|
21339
22562
|
} catch {
|
|
21340
22563
|
entries = [];
|
|
21341
22564
|
}
|
|
@@ -21347,11 +22570,11 @@ function checkZombieJobs() {
|
|
|
21347
22570
|
let total = 0;
|
|
21348
22571
|
let running = 0;
|
|
21349
22572
|
for (const jobId of entries) {
|
|
21350
|
-
const statusPath =
|
|
21351
|
-
if (!
|
|
22573
|
+
const statusPath = join26(jobsDir, jobId, "status.json");
|
|
22574
|
+
if (!existsSync18(statusPath))
|
|
21352
22575
|
continue;
|
|
21353
22576
|
try {
|
|
21354
|
-
const status = JSON.parse(
|
|
22577
|
+
const status = JSON.parse(readFileSync13(statusPath, "utf8"));
|
|
21355
22578
|
total++;
|
|
21356
22579
|
if (status.status === "running" || status.status === "starting") {
|
|
21357
22580
|
const pid = status.pid;
|
|
@@ -21365,7 +22588,7 @@ function checkZombieJobs() {
|
|
|
21365
22588
|
running++;
|
|
21366
22589
|
else {
|
|
21367
22590
|
zombies++;
|
|
21368
|
-
warn2(`${jobId} ${
|
|
22591
|
+
warn2(`${jobId} ${yellow10("ZOMBIE")} ${dim12(`pid ${pid} not found, status=${status.status}`)}`);
|
|
21369
22592
|
fix(`Edit .specialists/jobs/${jobId}/status.json → set "status": "error"`);
|
|
21370
22593
|
}
|
|
21371
22594
|
}
|
|
@@ -21378,7 +22601,7 @@ function checkZombieJobs() {
|
|
|
21378
22601
|
}
|
|
21379
22602
|
return zombies === 0;
|
|
21380
22603
|
}
|
|
21381
|
-
async function
|
|
22604
|
+
async function run20() {
|
|
21382
22605
|
console.log(`
|
|
21383
22606
|
${bold10("specialists doctor")}
|
|
21384
22607
|
`);
|
|
@@ -21393,21 +22616,21 @@ ${bold10("specialists doctor")}
|
|
|
21393
22616
|
const allOk = piOk && bdOk && xtOk && hooksOk && mcpOk && dirsOk && jobsOk;
|
|
21394
22617
|
console.log("");
|
|
21395
22618
|
if (allOk) {
|
|
21396
|
-
console.log(` ${
|
|
22619
|
+
console.log(` ${green13("✓")} ${bold10("All checks passed")} — specialists is healthy`);
|
|
21397
22620
|
} else {
|
|
21398
|
-
console.log(` ${
|
|
22621
|
+
console.log(` ${yellow10("○")} ${bold10("Some checks failed")} — follow the fix hints above`);
|
|
21399
22622
|
console.log(` ${dim12("specialists install fixes hook + MCP registration; pi, bd, and xt must be installed separately.")}`);
|
|
21400
22623
|
}
|
|
21401
22624
|
console.log("");
|
|
21402
22625
|
}
|
|
21403
|
-
var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`,
|
|
22626
|
+
var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green13 = (s) => `\x1B[32m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
|
|
21404
22627
|
var init_doctor = __esm(() => {
|
|
21405
22628
|
CWD = process.cwd();
|
|
21406
|
-
CLAUDE_DIR =
|
|
21407
|
-
SPECIALISTS_DIR =
|
|
21408
|
-
HOOKS_DIR =
|
|
21409
|
-
SETTINGS_FILE =
|
|
21410
|
-
MCP_FILE2 =
|
|
22629
|
+
CLAUDE_DIR = join26(CWD, ".claude");
|
|
22630
|
+
SPECIALISTS_DIR = join26(CWD, ".specialists");
|
|
22631
|
+
HOOKS_DIR = join26(SPECIALISTS_DIR, "default", "hooks");
|
|
22632
|
+
SETTINGS_FILE = join26(CLAUDE_DIR, "settings.json");
|
|
22633
|
+
MCP_FILE2 = join26(CWD, ".mcp.json");
|
|
21411
22634
|
HOOK_NAMES = [
|
|
21412
22635
|
"specialists-complete.mjs",
|
|
21413
22636
|
"specialists-session-start.mjs"
|
|
@@ -21417,11 +22640,11 @@ var init_doctor = __esm(() => {
|
|
|
21417
22640
|
// src/cli/setup.ts
|
|
21418
22641
|
var exports_setup = {};
|
|
21419
22642
|
__export(exports_setup, {
|
|
21420
|
-
run: () =>
|
|
22643
|
+
run: () => run21
|
|
21421
22644
|
});
|
|
21422
|
-
async function
|
|
22645
|
+
async function run21() {
|
|
21423
22646
|
console.log("");
|
|
21424
|
-
console.log(
|
|
22647
|
+
console.log(yellow11("⚠ DEPRECATED: `specialists setup` is deprecated"));
|
|
21425
22648
|
console.log("");
|
|
21426
22649
|
console.log(` Use ${bold11("specialists init")} instead.`);
|
|
21427
22650
|
console.log("");
|
|
@@ -21437,18 +22660,18 @@ async function run18() {
|
|
|
21437
22660
|
console.log(` ${dim13("Run: specialists init --help for full details")}`);
|
|
21438
22661
|
console.log("");
|
|
21439
22662
|
}
|
|
21440
|
-
var bold11 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
22663
|
+
var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, yellow11 = (s) => `\x1B[33m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
21441
22664
|
|
|
21442
22665
|
// src/cli/help.ts
|
|
21443
22666
|
var exports_help = {};
|
|
21444
22667
|
__export(exports_help, {
|
|
21445
|
-
run: () =>
|
|
22668
|
+
run: () => run22
|
|
21446
22669
|
});
|
|
21447
22670
|
function formatCommands(entries) {
|
|
21448
22671
|
const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
|
|
21449
22672
|
return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
|
|
21450
22673
|
}
|
|
21451
|
-
async function
|
|
22674
|
+
async function run22() {
|
|
21452
22675
|
const lines = [
|
|
21453
22676
|
"",
|
|
21454
22677
|
"Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
|
|
@@ -21476,10 +22699,16 @@ async function run19() {
|
|
|
21476
22699
|
" --context-depth defaults to 1 with --bead",
|
|
21477
22700
|
" --no-beads does not disable bead reading",
|
|
21478
22701
|
"",
|
|
21479
|
-
"
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
22702
|
+
" Output modes",
|
|
22703
|
+
' specialists run <name> --prompt "..." # human (default): formatted event summary',
|
|
22704
|
+
' specialists run <name> --prompt "..." --json # NDJSON event stream to stdout',
|
|
22705
|
+
' specialists run <name> --prompt "..." --raw # legacy: raw LLM text deltas',
|
|
22706
|
+
"",
|
|
22707
|
+
" Async patterns",
|
|
22708
|
+
" MCP: start_specialist + feed_specialist",
|
|
22709
|
+
' CLI: specialists run <name> --prompt "..." # job ID prints on stderr',
|
|
22710
|
+
" specialists feed|poll|result <job-id> # observe/progress/final output",
|
|
22711
|
+
' Shell: specialists run <name> --prompt "..." & # native shell backgrounding',
|
|
21483
22712
|
"",
|
|
21484
22713
|
bold12("Core commands:"),
|
|
21485
22714
|
...formatCommands(CORE_COMMANDS),
|
|
@@ -21493,20 +22722,24 @@ async function run19() {
|
|
|
21493
22722
|
bold12("Examples:"),
|
|
21494
22723
|
" specialists init",
|
|
21495
22724
|
" specialists list",
|
|
22725
|
+
" specialists config get specialist.execution.stall_timeout_ms",
|
|
21496
22726
|
" specialists run debugger --bead unitAI-123",
|
|
21497
22727
|
' specialists run codebase-explorer --prompt "Map the CLI architecture"',
|
|
21498
22728
|
" specialists poll abc123 --json # check job status",
|
|
21499
22729
|
" specialists feed -f # stream all job events",
|
|
21500
22730
|
' specialists steer <job-id> "focus only on supervisor.ts"',
|
|
21501
|
-
' specialists
|
|
21502
|
-
|
|
22731
|
+
' specialists resume <job-id> "now write the fix"',
|
|
22732
|
+
' specialists run debugger --prompt "why does auth fail"',
|
|
22733
|
+
" specialists report list",
|
|
22734
|
+
" specialists report show --specialists",
|
|
22735
|
+
" specialists result <job-id> --wait",
|
|
21503
22736
|
"",
|
|
21504
22737
|
bold12("More help:"),
|
|
21505
22738
|
" specialists quickstart Full guide and workflow reference",
|
|
21506
22739
|
" specialists run --help Run command details and flags",
|
|
21507
22740
|
" specialists poll --help Job status polling details",
|
|
21508
22741
|
" specialists steer --help Mid-run steering details",
|
|
21509
|
-
" specialists
|
|
22742
|
+
" specialists resume --help Multi-turn keep-alive details",
|
|
21510
22743
|
" specialists init --help Bootstrap behavior and workflow injection",
|
|
21511
22744
|
" specialists feed --help Job event streaming details",
|
|
21512
22745
|
"",
|
|
@@ -21522,13 +22755,17 @@ var init_help = __esm(() => {
|
|
|
21522
22755
|
["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
|
|
21523
22756
|
["list", "List specialists in this project"],
|
|
21524
22757
|
["validate", "Validate a specialist YAML against the schema"],
|
|
21525
|
-
["
|
|
22758
|
+
["config", "Batch get/set specialist YAML keys in config/specialists/"],
|
|
22759
|
+
["run", "Run a specialist; --json for NDJSON event stream, --raw for legacy text"],
|
|
21526
22760
|
["feed", "Tail job events; use -f to follow all jobs"],
|
|
21527
22761
|
["poll", "Machine-readable job status polling (for scripts/Claude Code)"],
|
|
21528
|
-
["result", "Print final output of a completed job"],
|
|
22762
|
+
["result", "Print final output of a completed job; --wait polls until done, --timeout <ms> sets a limit"],
|
|
22763
|
+
["clean", "Purge completed job directories (TTL, --all, --keep, --dry-run)"],
|
|
21529
22764
|
["steer", "Send a mid-run message to a running job"],
|
|
21530
|
-
["
|
|
22765
|
+
["resume", "Resume a waiting keep-alive session with a next-turn prompt (retains full context)"],
|
|
22766
|
+
["follow-up", "[deprecated] Use resume instead"],
|
|
21531
22767
|
["stop", "Stop a running job"],
|
|
22768
|
+
["report", "Generate/show/list/diff session reports in .xtrm/reports/"],
|
|
21532
22769
|
["status", "Show health, MCP state, and active jobs"],
|
|
21533
22770
|
["doctor", "Diagnose installation/runtime problems"],
|
|
21534
22771
|
["quickstart", "Full getting-started guide"],
|
|
@@ -21546,7 +22783,8 @@ var init_help = __esm(() => {
|
|
|
21546
22783
|
["xt claude [name]", "Start a Claude session in a sandboxed xt worktree"],
|
|
21547
22784
|
["xt attach [slug]", "Resume an existing xt worktree session"],
|
|
21548
22785
|
["xt worktree list", "List worktrees with runtime and activity"],
|
|
21549
|
-
["xt end", "Close session, push, PR, cleanup"]
|
|
22786
|
+
["xt end", "Close session, push, PR, cleanup"],
|
|
22787
|
+
["xt report show|list|diff", "Session report surfaces (same .xtrm/reports files)"]
|
|
21550
22788
|
];
|
|
21551
22789
|
});
|
|
21552
22790
|
|
|
@@ -28752,7 +29990,7 @@ class StdioServerTransport {
|
|
|
28752
29990
|
}
|
|
28753
29991
|
|
|
28754
29992
|
// src/server.ts
|
|
28755
|
-
import { join as
|
|
29993
|
+
import { join as join11 } from "node:path";
|
|
28756
29994
|
|
|
28757
29995
|
// src/constants.ts
|
|
28758
29996
|
var LOG_PREFIX = "[specialists]";
|
|
@@ -28819,6 +30057,7 @@ var logger = {
|
|
|
28819
30057
|
init_loader();
|
|
28820
30058
|
init_runner();
|
|
28821
30059
|
init_hooks();
|
|
30060
|
+
init_circuitBreaker();
|
|
28822
30061
|
init_beads();
|
|
28823
30062
|
|
|
28824
30063
|
// src/tools/specialist/list_specialists.tool.ts
|
|
@@ -28842,14 +30081,14 @@ function createListSpecialistsTool(loader) {
|
|
|
28842
30081
|
// src/tools/specialist/use_specialist.tool.ts
|
|
28843
30082
|
init_zod();
|
|
28844
30083
|
init_beads();
|
|
28845
|
-
var useSpecialistSchema =
|
|
28846
|
-
name:
|
|
28847
|
-
prompt:
|
|
28848
|
-
bead_id:
|
|
28849
|
-
variables:
|
|
28850
|
-
backend_override:
|
|
28851
|
-
autonomy_level:
|
|
28852
|
-
context_depth:
|
|
30084
|
+
var useSpecialistSchema = objectType({
|
|
30085
|
+
name: stringType().describe("Specialist identifier (e.g. codebase-explorer)"),
|
|
30086
|
+
prompt: stringType().optional().describe("The task or question for the specialist"),
|
|
30087
|
+
bead_id: stringType().optional().describe("Use an existing bead as the specialist prompt"),
|
|
30088
|
+
variables: recordType(stringType()).optional().describe("Additional $variable substitutions"),
|
|
30089
|
+
backend_override: stringType().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
|
|
30090
|
+
autonomy_level: stringType().optional().describe("Override permission level for this invocation"),
|
|
30091
|
+
context_depth: numberType().optional().describe("Depth of blocker context injection (0 = none, 1 = immediate blockers, etc.)")
|
|
28853
30092
|
}).refine((input) => Boolean(input.prompt?.trim() || input.bead_id), {
|
|
28854
30093
|
message: "Either prompt or bead_id is required",
|
|
28855
30094
|
path: ["prompt"]
|
|
@@ -28944,7 +30183,7 @@ var runParallelSchema = objectType({
|
|
|
28944
30183
|
function createRunParallelTool(runner) {
|
|
28945
30184
|
return {
|
|
28946
30185
|
name: "run_parallel",
|
|
28947
|
-
description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer
|
|
30186
|
+
description: "[DEPRECATED v3] Execute multiple specialists concurrently. Returns aggregated results. Prefer start_specialist/feed_specialist for async orchestration.",
|
|
28948
30187
|
inputSchema: runParallelSchema,
|
|
28949
30188
|
async execute(input, onProgress) {
|
|
28950
30189
|
if (input.merge_strategy === "pipeline") {
|
|
@@ -29205,66 +30444,99 @@ class JobRegistry {
|
|
|
29205
30444
|
|
|
29206
30445
|
// src/tools/specialist/start_specialist.tool.ts
|
|
29207
30446
|
init_zod();
|
|
29208
|
-
|
|
29209
|
-
|
|
29210
|
-
|
|
29211
|
-
|
|
29212
|
-
|
|
29213
|
-
|
|
29214
|
-
|
|
30447
|
+
init_supervisor();
|
|
30448
|
+
import { join as join4 } from "node:path";
|
|
30449
|
+
init_loader();
|
|
30450
|
+
var startSpecialistSchema = objectType({
|
|
30451
|
+
name: stringType().describe("Specialist identifier (e.g. codebase-explorer)"),
|
|
30452
|
+
prompt: stringType().describe("The task or question for the specialist"),
|
|
30453
|
+
variables: recordType(stringType()).optional().describe("Additional $variable substitutions"),
|
|
30454
|
+
backend_override: stringType().optional().describe("Force a specific backend (gemini, qwen, anthropic)"),
|
|
30455
|
+
bead_id: stringType().optional().describe("Existing bead ID to associate with this run (propagated into status.json and run_start event)"),
|
|
30456
|
+
keep_alive: booleanType().optional().describe("Keep the specialist session open for resume_specialist (overrides execution.interactive)."),
|
|
30457
|
+
no_keep_alive: booleanType().optional().describe("Force one-shot behavior even when execution.interactive is true.")
|
|
30458
|
+
});
|
|
30459
|
+
function createStartSpecialistTool(runner, beadsClient) {
|
|
29215
30460
|
return {
|
|
29216
30461
|
name: "start_specialist",
|
|
29217
|
-
description: "
|
|
30462
|
+
description: "Start a specialist asynchronously. Returns job_id immediately. " + "Use feed_specialist to stream events and track progress (pass job_id and --follow for live output). " + "Use specialist_status for circuit breaker health checks. " + "Use stop_specialist to cancel. Enables true parallel execution of multiple specialists.",
|
|
29218
30463
|
inputSchema: startSpecialistSchema,
|
|
29219
30464
|
async execute(input) {
|
|
29220
|
-
const
|
|
29221
|
-
|
|
29222
|
-
|
|
29223
|
-
|
|
29224
|
-
|
|
29225
|
-
|
|
30465
|
+
const jobsDir = join4(process.cwd(), ".specialists", "jobs");
|
|
30466
|
+
let keepAlive;
|
|
30467
|
+
try {
|
|
30468
|
+
const loader = new SpecialistLoader;
|
|
30469
|
+
const specialist = await loader.get(input.name);
|
|
30470
|
+
const interactiveDefault = specialist.specialist.execution.interactive ? true : undefined;
|
|
30471
|
+
keepAlive = input.no_keep_alive ? false : input.keep_alive ?? interactiveDefault;
|
|
30472
|
+
} catch {
|
|
30473
|
+
keepAlive = input.no_keep_alive ? false : input.keep_alive;
|
|
30474
|
+
}
|
|
30475
|
+
const jobStarted = new Promise((resolve2, reject) => {
|
|
30476
|
+
const supervisor = new Supervisor({
|
|
30477
|
+
runner,
|
|
30478
|
+
runOptions: {
|
|
30479
|
+
name: input.name,
|
|
30480
|
+
prompt: input.prompt,
|
|
30481
|
+
variables: input.variables,
|
|
30482
|
+
backendOverride: input.backend_override,
|
|
30483
|
+
inputBeadId: input.bead_id,
|
|
30484
|
+
keepAlive,
|
|
30485
|
+
noKeepAlive: input.no_keep_alive ?? false
|
|
30486
|
+
},
|
|
30487
|
+
jobsDir,
|
|
30488
|
+
beadsClient,
|
|
30489
|
+
onJobStarted: ({ id }) => resolve2(id)
|
|
30490
|
+
});
|
|
30491
|
+
supervisor.run().catch((error2) => {
|
|
30492
|
+
logger.error(`start_specialist job failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
30493
|
+
reject(error2);
|
|
30494
|
+
});
|
|
30495
|
+
});
|
|
30496
|
+
const jobId = await jobStarted;
|
|
29226
30497
|
return { job_id: jobId };
|
|
29227
30498
|
}
|
|
29228
30499
|
};
|
|
29229
30500
|
}
|
|
29230
30501
|
|
|
29231
|
-
// src/tools/specialist/poll_specialist.tool.ts
|
|
29232
|
-
init_zod();
|
|
29233
|
-
var pollSpecialistSchema = exports_external.object({
|
|
29234
|
-
job_id: exports_external.string().describe("Job ID returned by start_specialist"),
|
|
29235
|
-
cursor: exports_external.number().int().min(0).optional().default(0).describe("Character offset from previous poll. Pass next_cursor from the last response to receive only new content. Omit (or pass 0) for the first poll.")
|
|
29236
|
-
});
|
|
29237
|
-
function createPollSpecialistTool(registry2) {
|
|
29238
|
-
return {
|
|
29239
|
-
name: "poll_specialist",
|
|
29240
|
-
description: "[DEPRECATED v3] Poll a running specialist job. Returns status (running|done|error|cancelled), " + "delta (new tokens since cursor), next_cursor, and full output when done. " + "Pass next_cursor back as cursor on each subsequent poll to receive only new content. " + "Response also includes beadId (string | undefined) once the specialist has started — " + "this is the beads issue tracking this run. If present after status=done, consider: " + '`bd update <beadId> --notes "<key finding>"` to attach results, or ' + '`bd remember "<insight>"` to persist discoveries across sessions.',
|
|
29241
|
-
inputSchema: pollSpecialistSchema,
|
|
29242
|
-
async execute(input) {
|
|
29243
|
-
const snapshot = registry2.snapshot(input.job_id, input.cursor ?? 0);
|
|
29244
|
-
if (!snapshot) {
|
|
29245
|
-
return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
|
|
29246
|
-
}
|
|
29247
|
-
return snapshot;
|
|
29248
|
-
}
|
|
29249
|
-
};
|
|
29250
|
-
}
|
|
29251
|
-
|
|
29252
30502
|
// src/tools/specialist/stop_specialist.tool.ts
|
|
29253
30503
|
init_zod();
|
|
29254
|
-
|
|
29255
|
-
|
|
30504
|
+
init_supervisor();
|
|
30505
|
+
import { join as join5 } from "node:path";
|
|
30506
|
+
var stopSpecialistSchema = objectType({
|
|
30507
|
+
job_id: stringType().describe("Job ID returned by start_specialist")
|
|
29256
30508
|
});
|
|
29257
|
-
function createStopSpecialistTool(
|
|
30509
|
+
function createStopSpecialistTool() {
|
|
29258
30510
|
return {
|
|
29259
30511
|
name: "stop_specialist",
|
|
29260
|
-
description: "
|
|
30512
|
+
description: "Cancel a running specialist job by sending SIGTERM to its recorded process. Works for jobs started via start_specialist and CLI background runs.",
|
|
29261
30513
|
inputSchema: stopSpecialistSchema,
|
|
29262
30514
|
async execute(input) {
|
|
29263
|
-
const
|
|
29264
|
-
|
|
30515
|
+
const jobsDir = join5(process.cwd(), ".specialists", "jobs");
|
|
30516
|
+
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
30517
|
+
const status = supervisor.readStatus(input.job_id);
|
|
30518
|
+
if (!status) {
|
|
29265
30519
|
return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
|
|
29266
30520
|
}
|
|
29267
|
-
|
|
30521
|
+
if (status.status === "done" || status.status === "error") {
|
|
30522
|
+
return {
|
|
30523
|
+
status: "error",
|
|
30524
|
+
error: `Job is already ${status.status}`,
|
|
30525
|
+
job_id: input.job_id
|
|
30526
|
+
};
|
|
30527
|
+
}
|
|
30528
|
+
if (!status.pid) {
|
|
30529
|
+
return { status: "error", error: `No PID recorded for job ${input.job_id}`, job_id: input.job_id };
|
|
30530
|
+
}
|
|
30531
|
+
try {
|
|
30532
|
+
process.kill(status.pid, "SIGTERM");
|
|
30533
|
+
return { status: "cancelled", job_id: input.job_id, pid: status.pid };
|
|
30534
|
+
} catch (err) {
|
|
30535
|
+
if (err?.code === "ESRCH") {
|
|
30536
|
+
return { status: "error", error: `Process ${status.pid} not found`, job_id: input.job_id };
|
|
30537
|
+
}
|
|
30538
|
+
return { status: "error", error: err?.message ?? String(err), job_id: input.job_id };
|
|
30539
|
+
}
|
|
29268
30540
|
}
|
|
29269
30541
|
};
|
|
29270
30542
|
}
|
|
@@ -29273,15 +30545,15 @@ function createStopSpecialistTool(registry2) {
|
|
|
29273
30545
|
init_zod();
|
|
29274
30546
|
init_supervisor();
|
|
29275
30547
|
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
29276
|
-
import { join as
|
|
30548
|
+
import { join as join6 } from "node:path";
|
|
29277
30549
|
var steerSpecialistSchema = exports_external.object({
|
|
29278
|
-
job_id: exports_external.string().describe("Job ID returned by start_specialist or specialists run
|
|
30550
|
+
job_id: exports_external.string().describe("Job ID returned by start_specialist or printed by specialists run"),
|
|
29279
30551
|
message: exports_external.string().describe('Steering instruction to send to the running agent (e.g. "focus only on supervisor.ts")')
|
|
29280
30552
|
});
|
|
29281
30553
|
function createSteerSpecialistTool(registry2) {
|
|
29282
30554
|
return {
|
|
29283
30555
|
name: "steer_specialist",
|
|
29284
|
-
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
|
|
30556
|
+
description: "Send a mid-run steering message to a running specialist job. The agent receives the message after its current tool calls finish, before the next LLM call. Works for both in-process jobs (start_specialist) and CLI-started jobs (specialists run).",
|
|
29285
30557
|
inputSchema: steerSpecialistSchema,
|
|
29286
30558
|
async execute(input) {
|
|
29287
30559
|
const snap = registry2.snapshot(input.job_id);
|
|
@@ -29292,7 +30564,7 @@ function createSteerSpecialistTool(registry2) {
|
|
|
29292
30564
|
}
|
|
29293
30565
|
return { status: "error", error: result.error, job_id: input.job_id };
|
|
29294
30566
|
}
|
|
29295
|
-
const jobsDir =
|
|
30567
|
+
const jobsDir = join6(process.cwd(), ".specialists", "jobs");
|
|
29296
30568
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
29297
30569
|
const status = supervisor.readStatus(input.job_id);
|
|
29298
30570
|
if (!status) {
|
|
@@ -29318,47 +30590,50 @@ function createSteerSpecialistTool(registry2) {
|
|
|
29318
30590
|
|
|
29319
30591
|
// src/tools/specialist/follow_up_specialist.tool.ts
|
|
29320
30592
|
init_zod();
|
|
30593
|
+
|
|
30594
|
+
// src/tools/specialist/resume_specialist.tool.ts
|
|
30595
|
+
init_zod();
|
|
29321
30596
|
init_supervisor();
|
|
29322
30597
|
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
29323
|
-
import { join as
|
|
29324
|
-
var
|
|
30598
|
+
import { join as join7 } from "node:path";
|
|
30599
|
+
var resumeSpecialistSchema = exports_external.object({
|
|
29325
30600
|
job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
|
|
29326
|
-
|
|
30601
|
+
task: exports_external.string().describe("Next task/prompt to send to the specialist (conversation history is retained)")
|
|
29327
30602
|
});
|
|
29328
|
-
function
|
|
30603
|
+
function createResumeSpecialistTool(registry2) {
|
|
29329
30604
|
return {
|
|
29330
|
-
name: "
|
|
29331
|
-
description: "
|
|
29332
|
-
inputSchema:
|
|
30605
|
+
name: "resume_specialist",
|
|
30606
|
+
description: "Resume a waiting keep-alive specialist session with a next-turn prompt. " + "The Pi session retains full conversation history between turns. " + "Only valid for jobs in waiting state (started with keepAlive=true, either explicit --keep-alive or execution.interactive default). " + "Use steer_specialist for mid-run steering of running jobs.",
|
|
30607
|
+
inputSchema: resumeSpecialistSchema,
|
|
29333
30608
|
async execute(input) {
|
|
29334
30609
|
const snap = registry2.snapshot(input.job_id);
|
|
29335
30610
|
if (snap) {
|
|
29336
30611
|
if (snap.status !== "waiting") {
|
|
29337
|
-
return { status: "error", error: `Job is not waiting (status: ${snap.status})
|
|
30612
|
+
return { status: "error", error: `Job is not waiting (status: ${snap.status}). resume is only valid in waiting state.`, job_id: input.job_id };
|
|
29338
30613
|
}
|
|
29339
|
-
const result = await registry2.followUp(input.job_id, input.
|
|
30614
|
+
const result = await registry2.followUp(input.job_id, input.task);
|
|
29340
30615
|
if (result.ok) {
|
|
29341
30616
|
return { status: "resumed", job_id: input.job_id, output: result.output };
|
|
29342
30617
|
}
|
|
29343
30618
|
return { status: "error", error: result.error, job_id: input.job_id };
|
|
29344
30619
|
}
|
|
29345
|
-
const jobsDir =
|
|
30620
|
+
const jobsDir = join7(process.cwd(), ".specialists", "jobs");
|
|
29346
30621
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
29347
30622
|
const status = supervisor.readStatus(input.job_id);
|
|
29348
30623
|
if (!status) {
|
|
29349
30624
|
return { status: "error", error: `Job not found: ${input.job_id}`, job_id: input.job_id };
|
|
29350
30625
|
}
|
|
29351
30626
|
if (status.status !== "waiting") {
|
|
29352
|
-
return { status: "error", error: `Job is not waiting (status: ${status.status})
|
|
30627
|
+
return { status: "error", error: `Job is not waiting (status: ${status.status}). resume is only valid in waiting state.`, job_id: input.job_id };
|
|
29353
30628
|
}
|
|
29354
30629
|
if (!status.fifo_path) {
|
|
29355
30630
|
return { status: "error", error: "Job has no steer pipe", job_id: input.job_id };
|
|
29356
30631
|
}
|
|
29357
30632
|
try {
|
|
29358
|
-
const payload = JSON.stringify({ type: "
|
|
30633
|
+
const payload = JSON.stringify({ type: "resume", task: input.task }) + `
|
|
29359
30634
|
`;
|
|
29360
30635
|
writeFileSync3(status.fifo_path, payload, { flag: "a" });
|
|
29361
|
-
return { status: "sent", job_id: input.job_id,
|
|
30636
|
+
return { status: "sent", job_id: input.job_id, task: input.task };
|
|
29362
30637
|
} catch (err) {
|
|
29363
30638
|
return { status: "error", error: `Failed to write to steer pipe: ${err?.message}`, job_id: input.job_id };
|
|
29364
30639
|
}
|
|
@@ -29366,19 +30641,92 @@ function createFollowUpSpecialistTool(registry2) {
|
|
|
29366
30641
|
};
|
|
29367
30642
|
}
|
|
29368
30643
|
|
|
30644
|
+
// src/tools/specialist/follow_up_specialist.tool.ts
|
|
30645
|
+
var followUpSpecialistSchema = exports_external.object({
|
|
30646
|
+
job_id: exports_external.string().describe("Job ID of a waiting keep-alive specialist session"),
|
|
30647
|
+
message: exports_external.string().describe("Next prompt to send to the specialist (conversation history is retained)")
|
|
30648
|
+
});
|
|
30649
|
+
function createFollowUpSpecialistTool(registry2) {
|
|
30650
|
+
const resumeTool = createResumeSpecialistTool(registry2);
|
|
30651
|
+
return {
|
|
30652
|
+
name: "follow_up_specialist",
|
|
30653
|
+
description: "[DEPRECATED] Use resume_specialist instead. " + "Delegates to resume_specialist with a deprecation warning.",
|
|
30654
|
+
inputSchema: followUpSpecialistSchema,
|
|
30655
|
+
async execute(input) {
|
|
30656
|
+
console.error("[specialists] DEPRECATED: follow_up_specialist is deprecated. Use resume_specialist instead.");
|
|
30657
|
+
return resumeTool.execute({ job_id: input.job_id, task: input.message });
|
|
30658
|
+
}
|
|
30659
|
+
};
|
|
30660
|
+
}
|
|
30661
|
+
|
|
30662
|
+
// src/tools/specialist/feed_specialist.tool.ts
|
|
30663
|
+
init_zod();
|
|
30664
|
+
init_timeline_query();
|
|
30665
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
30666
|
+
import { join as join9 } from "node:path";
|
|
30667
|
+
var feedSpecialistSchema = objectType({
|
|
30668
|
+
job_id: stringType().describe("Job ID returned by start_specialist or printed by specialists run"),
|
|
30669
|
+
cursor: numberType().int().min(0).optional().default(0).describe("Event index offset from previous call. Pass next_cursor from the last response to receive only new events. Omit (or pass 0) for the first call."),
|
|
30670
|
+
limit: numberType().int().min(1).max(100).optional().default(50).describe("Maximum number of events to return per call.")
|
|
30671
|
+
});
|
|
30672
|
+
function createFeedSpecialistTool(jobsDir) {
|
|
30673
|
+
return {
|
|
30674
|
+
name: "feed_specialist",
|
|
30675
|
+
description: "Read cursor-paginated timeline events from a specialist job's events.jsonl. " + "Returns structured event objects (run_start, meta, tool, text, run_complete, etc.) " + "with job metadata (status, specialist, model, bead_id). " + "Poll incrementally: pass next_cursor from each response as cursor on the next call. " + "When is_complete=true and has_more=false, the job is fully observed. " + "Use for structured event inspection; use specialists result <job-id> for final text output.",
|
|
30676
|
+
inputSchema: feedSpecialistSchema,
|
|
30677
|
+
async execute(input) {
|
|
30678
|
+
const { job_id, cursor = 0, limit = 50 } = input;
|
|
30679
|
+
const statusPath = join9(jobsDir, job_id, "status.json");
|
|
30680
|
+
if (!existsSync6(statusPath)) {
|
|
30681
|
+
return { error: `Job not found: ${job_id}`, job_id };
|
|
30682
|
+
}
|
|
30683
|
+
let status = "unknown";
|
|
30684
|
+
let specialist = "unknown";
|
|
30685
|
+
let model;
|
|
30686
|
+
let bead_id;
|
|
30687
|
+
try {
|
|
30688
|
+
const s = JSON.parse(readFileSync4(statusPath, "utf-8"));
|
|
30689
|
+
status = s.status ?? "unknown";
|
|
30690
|
+
specialist = s.specialist ?? "unknown";
|
|
30691
|
+
model = s.model;
|
|
30692
|
+
bead_id = s.bead_id;
|
|
30693
|
+
} catch {}
|
|
30694
|
+
const allEvents = readJobEventsById(jobsDir, job_id);
|
|
30695
|
+
const total = allEvents.length;
|
|
30696
|
+
const sliced = allEvents.slice(cursor, cursor + limit);
|
|
30697
|
+
const next_cursor = cursor + sliced.length;
|
|
30698
|
+
const has_more = next_cursor < total;
|
|
30699
|
+
const is_complete = isJobComplete(allEvents);
|
|
30700
|
+
return {
|
|
30701
|
+
job_id,
|
|
30702
|
+
specialist,
|
|
30703
|
+
specialist_model: formatSpecialistModel(specialist, model),
|
|
30704
|
+
...model !== undefined ? { model } : {},
|
|
30705
|
+
status,
|
|
30706
|
+
...bead_id !== undefined ? { bead_id } : {},
|
|
30707
|
+
events: sliced,
|
|
30708
|
+
cursor,
|
|
30709
|
+
next_cursor,
|
|
30710
|
+
has_more,
|
|
30711
|
+
is_complete
|
|
30712
|
+
};
|
|
30713
|
+
}
|
|
30714
|
+
};
|
|
30715
|
+
}
|
|
30716
|
+
|
|
29369
30717
|
// src/server.ts
|
|
29370
30718
|
init_zod();
|
|
29371
30719
|
|
|
29372
30720
|
// src/tools/specialist/specialist_init.tool.ts
|
|
29373
30721
|
init_zod();
|
|
29374
30722
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
29375
|
-
import { existsSync as
|
|
29376
|
-
import { join as
|
|
30723
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
30724
|
+
import { join as join10 } from "node:path";
|
|
29377
30725
|
var specialistInitSchema = objectType({});
|
|
29378
30726
|
function createSpecialistInitTool(loader, deps) {
|
|
29379
30727
|
const resolved = deps ?? {
|
|
29380
30728
|
bdAvailable: () => spawnSync4("bd", ["--version"], { stdio: "ignore" }).status === 0,
|
|
29381
|
-
beadsExists: () =>
|
|
30729
|
+
beadsExists: () => existsSync7(join10(process.cwd(), ".beads")),
|
|
29382
30730
|
bdInit: () => spawnSync4("bd", ["init"], { stdio: "ignore" })
|
|
29383
30731
|
};
|
|
29384
30732
|
return {
|
|
@@ -29413,22 +30761,24 @@ class SpecialistsServer {
|
|
|
29413
30761
|
const circuitBreaker = new CircuitBreaker;
|
|
29414
30762
|
const loader = new SpecialistLoader;
|
|
29415
30763
|
const hooks = new HookEmitter({
|
|
29416
|
-
tracePath:
|
|
30764
|
+
tracePath: join11(process.cwd(), ".specialists", "trace.jsonl")
|
|
29417
30765
|
});
|
|
29418
30766
|
const beadsClient = new BeadsClient;
|
|
29419
30767
|
const runner = new SpecialistRunner({ loader, hooks, circuitBreaker, beadsClient });
|
|
29420
30768
|
const registry2 = new JobRegistry;
|
|
30769
|
+
const jobsDir = join11(process.cwd(), ".specialists", "jobs");
|
|
29421
30770
|
this.tools = [
|
|
29422
30771
|
createListSpecialistsTool(loader),
|
|
29423
30772
|
createUseSpecialistTool(runner),
|
|
29424
30773
|
createRunParallelTool(runner),
|
|
29425
30774
|
createSpecialistStatusTool(loader, circuitBreaker),
|
|
29426
|
-
createStartSpecialistTool(runner,
|
|
29427
|
-
|
|
29428
|
-
createStopSpecialistTool(registry2),
|
|
30775
|
+
createStartSpecialistTool(runner, beadsClient),
|
|
30776
|
+
createStopSpecialistTool(),
|
|
29429
30777
|
createSteerSpecialistTool(registry2),
|
|
30778
|
+
createResumeSpecialistTool(registry2),
|
|
29430
30779
|
createFollowUpSpecialistTool(registry2),
|
|
29431
|
-
createSpecialistInitTool(loader)
|
|
30780
|
+
createSpecialistInitTool(loader),
|
|
30781
|
+
createFeedSpecialistTool(jobsDir)
|
|
29432
30782
|
];
|
|
29433
30783
|
this.server = new Server({ name: MCP_CONFIG.SERVER_NAME, version: MCP_CONFIG.VERSION }, { capabilities: MCP_CONFIG.CAPABILITIES });
|
|
29434
30784
|
this.setupHandlers();
|
|
@@ -29441,11 +30791,12 @@ class SpecialistsServer {
|
|
|
29441
30791
|
run_parallel: runParallelSchema,
|
|
29442
30792
|
specialist_status: exports_external.object({}),
|
|
29443
30793
|
start_specialist: startSpecialistSchema,
|
|
29444
|
-
poll_specialist: pollSpecialistSchema,
|
|
29445
30794
|
stop_specialist: stopSpecialistSchema,
|
|
29446
30795
|
steer_specialist: steerSpecialistSchema,
|
|
30796
|
+
resume_specialist: resumeSpecialistSchema,
|
|
29447
30797
|
follow_up_specialist: followUpSpecialistSchema,
|
|
29448
|
-
specialist_init: specialistInitSchema
|
|
30798
|
+
specialist_init: specialistInitSchema,
|
|
30799
|
+
feed_specialist: feedSpecialistSchema
|
|
29449
30800
|
};
|
|
29450
30801
|
this.toolSchemas = schemaMap;
|
|
29451
30802
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -29517,7 +30868,7 @@ var next = process.argv[3];
|
|
|
29517
30868
|
function wantsHelp() {
|
|
29518
30869
|
return next === "--help" || next === "-h";
|
|
29519
30870
|
}
|
|
29520
|
-
async function
|
|
30871
|
+
async function run23() {
|
|
29521
30872
|
if (sub === "install") {
|
|
29522
30873
|
if (wantsHelp()) {
|
|
29523
30874
|
console.log([
|
|
@@ -29656,6 +31007,7 @@ async function run20() {
|
|
|
29656
31007
|
console.log([
|
|
29657
31008
|
"",
|
|
29658
31009
|
"Usage: specialists edit <name> --<field> <value> [options]",
|
|
31010
|
+
" specialists edit --all",
|
|
29659
31011
|
"",
|
|
29660
31012
|
"Edit a field in a .specialist.yaml without opening the file.",
|
|
29661
31013
|
"",
|
|
@@ -29670,11 +31022,13 @@ async function run20() {
|
|
|
29670
31022
|
"Options:",
|
|
29671
31023
|
" --dry-run Preview the change without writing",
|
|
29672
31024
|
" --scope <default|user> Disambiguate if same name exists in multiple scopes",
|
|
31025
|
+
" --all Open all YAML files in config/specialists/ in $EDITOR",
|
|
29673
31026
|
"",
|
|
29674
31027
|
"Examples:",
|
|
29675
31028
|
" specialists edit code-review --model anthropic/claude-opus-4-6",
|
|
29676
31029
|
" specialists edit code-review --permission HIGH --dry-run",
|
|
29677
31030
|
" specialists edit code-review --tags analysis,security",
|
|
31031
|
+
" specialists edit --all",
|
|
29678
31032
|
""
|
|
29679
31033
|
].join(`
|
|
29680
31034
|
`));
|
|
@@ -29683,6 +31037,34 @@ async function run20() {
|
|
|
29683
31037
|
const { run: handler } = await Promise.resolve().then(() => (init_edit(), exports_edit));
|
|
29684
31038
|
return handler();
|
|
29685
31039
|
}
|
|
31040
|
+
if (sub === "config") {
|
|
31041
|
+
if (wantsHelp()) {
|
|
31042
|
+
console.log([
|
|
31043
|
+
"",
|
|
31044
|
+
"Usage: specialists config <get|set> <key> [value] [options]",
|
|
31045
|
+
"",
|
|
31046
|
+
"Batch-read or batch-update specialist YAML config in config/specialists/.",
|
|
31047
|
+
"",
|
|
31048
|
+
"Commands:",
|
|
31049
|
+
" get <key> Show a key across all specialists",
|
|
31050
|
+
" set <key> <value> Set a key across all specialists",
|
|
31051
|
+
"",
|
|
31052
|
+
"Options:",
|
|
31053
|
+
" --all Apply to all specialists (default when --name omitted)",
|
|
31054
|
+
" --name <specialist> Target one specialist",
|
|
31055
|
+
"",
|
|
31056
|
+
"Examples:",
|
|
31057
|
+
" specialists config get specialist.execution.stall_timeout_ms",
|
|
31058
|
+
" specialists config set specialist.execution.stall_timeout_ms 180000",
|
|
31059
|
+
" specialists config set specialist.execution.stall_timeout_ms 120000 --name executor",
|
|
31060
|
+
""
|
|
31061
|
+
].join(`
|
|
31062
|
+
`));
|
|
31063
|
+
return;
|
|
31064
|
+
}
|
|
31065
|
+
const { run: handler } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
31066
|
+
return handler();
|
|
31067
|
+
}
|
|
29686
31068
|
if (sub === "run") {
|
|
29687
31069
|
if (wantsHelp()) {
|
|
29688
31070
|
console.log([
|
|
@@ -29700,6 +31082,7 @@ async function run20() {
|
|
|
29700
31082
|
" --prompt <text> Ad-hoc prompt for untracked work",
|
|
29701
31083
|
" --context-depth <n> Dependency context depth when using --bead (default: 1)",
|
|
29702
31084
|
" --no-beads Do not create a new tracking bead (does not disable bead reading)",
|
|
31085
|
+
" --no-bead-notes Do not append completion notes to an external --bead",
|
|
29703
31086
|
" --model <model> Override the configured model for this run",
|
|
29704
31087
|
" --keep-alive Keep session alive for follow-up prompts",
|
|
29705
31088
|
"",
|
|
@@ -29713,10 +31096,10 @@ async function run20() {
|
|
|
29713
31096
|
" Use --bead for tracked work.",
|
|
29714
31097
|
" Use --prompt for quick ad-hoc work.",
|
|
29715
31098
|
"",
|
|
29716
|
-
"
|
|
29717
|
-
"
|
|
29718
|
-
"
|
|
29719
|
-
|
|
31099
|
+
"Async execution patterns:",
|
|
31100
|
+
" MCP: start_specialist + feed_specialist",
|
|
31101
|
+
" CLI: run prints [job started: <id>] on stderr, then use feed/poll/result",
|
|
31102
|
+
' Shell: specialists run <name> --prompt "..." &',
|
|
29720
31103
|
""
|
|
29721
31104
|
].join(`
|
|
29722
31105
|
`));
|
|
@@ -29760,7 +31143,7 @@ async function run20() {
|
|
|
29760
31143
|
"",
|
|
29761
31144
|
"Usage: specialists result <job-id>",
|
|
29762
31145
|
"",
|
|
29763
|
-
"Print the final output of a completed
|
|
31146
|
+
"Print the final output of a completed job.",
|
|
29764
31147
|
"Exits with code 1 if the job is still running or failed.",
|
|
29765
31148
|
"",
|
|
29766
31149
|
"Examples:",
|
|
@@ -29785,7 +31168,7 @@ async function run20() {
|
|
|
29785
31168
|
"Usage: specialists feed <job-id> [options]",
|
|
29786
31169
|
" specialists feed -f [--forever]",
|
|
29787
31170
|
"",
|
|
29788
|
-
"Read
|
|
31171
|
+
"Read job events.",
|
|
29789
31172
|
"",
|
|
29790
31173
|
"Modes:",
|
|
29791
31174
|
" specialists feed <job-id> Replay events for one job",
|
|
@@ -29880,35 +31263,82 @@ async function run20() {
|
|
|
29880
31263
|
const { run: handler } = await Promise.resolve().then(() => (init_steer(), exports_steer));
|
|
29881
31264
|
return handler();
|
|
29882
31265
|
}
|
|
29883
|
-
if (sub === "
|
|
31266
|
+
if (sub === "resume") {
|
|
29884
31267
|
if (wantsHelp()) {
|
|
29885
31268
|
console.log([
|
|
29886
31269
|
"",
|
|
29887
|
-
'Usage: specialists
|
|
31270
|
+
'Usage: specialists resume <job-id> "<task>"',
|
|
29888
31271
|
"",
|
|
29889
|
-
"
|
|
31272
|
+
"Resume a waiting keep-alive specialist session with a next-turn prompt.",
|
|
29890
31273
|
"The Pi session retains full conversation history between turns.",
|
|
29891
31274
|
"",
|
|
29892
31275
|
"Requires: job started with --keep-alive.",
|
|
29893
31276
|
"",
|
|
29894
31277
|
"Examples:",
|
|
29895
|
-
' specialists
|
|
29896
|
-
' specialists
|
|
31278
|
+
' specialists resume a1b2c3 "Now write the fix for the bug you found"',
|
|
31279
|
+
' specialists resume a1b2c3 "Focus only on the auth module"',
|
|
29897
31280
|
"",
|
|
29898
31281
|
"Workflow:",
|
|
29899
31282
|
" specialists run debugger --bead <id> --keep-alive",
|
|
29900
31283
|
" # → Job started: a1b2c3 (status: waiting after first turn)",
|
|
29901
|
-
" specialists result a1b2c3
|
|
29902
|
-
' specialists
|
|
29903
|
-
" specialists feed a1b2c3 --follow
|
|
31284
|
+
" specialists result a1b2c3 # read first turn output",
|
|
31285
|
+
' specialists resume a1b2c3 "..." # send next task',
|
|
31286
|
+
" specialists feed a1b2c3 --follow # watch response",
|
|
31287
|
+
"",
|
|
31288
|
+
"See also: specialists steer (mid-run redirect for running jobs)",
|
|
31289
|
+
""
|
|
31290
|
+
].join(`
|
|
31291
|
+
`));
|
|
31292
|
+
return;
|
|
31293
|
+
}
|
|
31294
|
+
const { run: handler } = await Promise.resolve().then(() => (init_resume(), exports_resume));
|
|
31295
|
+
return handler();
|
|
31296
|
+
}
|
|
31297
|
+
if (sub === "follow-up") {
|
|
31298
|
+
if (wantsHelp()) {
|
|
31299
|
+
console.log([
|
|
31300
|
+
"",
|
|
31301
|
+
"⚠ DEPRECATED: Use `specialists resume` instead.",
|
|
31302
|
+
"",
|
|
31303
|
+
'Usage: specialists follow-up <job-id> "<task>"',
|
|
29904
31304
|
"",
|
|
29905
|
-
"
|
|
31305
|
+
"Delegates to `specialists resume`. This alias will be removed in a future release.",
|
|
31306
|
+
""
|
|
31307
|
+
].join(`
|
|
31308
|
+
`));
|
|
31309
|
+
return;
|
|
31310
|
+
}
|
|
31311
|
+
const { run: handler } = await Promise.resolve().then(() => exports_follow_up);
|
|
31312
|
+
return handler();
|
|
31313
|
+
}
|
|
31314
|
+
if (sub === "clean") {
|
|
31315
|
+
if (wantsHelp()) {
|
|
31316
|
+
console.log([
|
|
31317
|
+
"",
|
|
31318
|
+
"Usage: specialists clean [--all] [--keep <n>] [--dry-run]",
|
|
31319
|
+
"",
|
|
31320
|
+
"Purge completed job directories from .specialists/jobs/.",
|
|
31321
|
+
"",
|
|
31322
|
+
"Default behavior:",
|
|
31323
|
+
" - removes done/error jobs older than SPECIALISTS_JOB_TTL_DAYS",
|
|
31324
|
+
" - TTL defaults to 7 days if env is unset",
|
|
31325
|
+
"",
|
|
31326
|
+
"Options:",
|
|
31327
|
+
" --all Remove all done/error jobs regardless of age",
|
|
31328
|
+
" --keep <n> Keep only the N most recent done/error jobs",
|
|
31329
|
+
" --dry-run Show what would be removed without deleting",
|
|
31330
|
+
"",
|
|
31331
|
+
"Examples:",
|
|
31332
|
+
" specialists clean",
|
|
31333
|
+
" specialists clean --all",
|
|
31334
|
+
" specialists clean --keep 20",
|
|
31335
|
+
" specialists clean --dry-run",
|
|
29906
31336
|
""
|
|
29907
31337
|
].join(`
|
|
29908
31338
|
`));
|
|
29909
31339
|
return;
|
|
29910
31340
|
}
|
|
29911
|
-
const { run: handler } = await Promise.resolve().then(() => (
|
|
31341
|
+
const { run: handler } = await Promise.resolve().then(() => (init_clean(), exports_clean));
|
|
29912
31342
|
return handler();
|
|
29913
31343
|
}
|
|
29914
31344
|
if (sub === "stop") {
|
|
@@ -29993,7 +31423,7 @@ Run 'specialists help' to see available commands.`);
|
|
|
29993
31423
|
const server = new SpecialistsServer;
|
|
29994
31424
|
await server.start();
|
|
29995
31425
|
}
|
|
29996
|
-
|
|
31426
|
+
run23().catch((error2) => {
|
|
29997
31427
|
logger.error(`Fatal error: ${error2}`);
|
|
29998
31428
|
process.exit(1);
|
|
29999
31429
|
});
|