@jaggerxtrm/specialists 3.4.3 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// specialists-complete — Claude Code UserPromptSubmit hook
|
|
2
|
+
// specialists-complete — Claude Code UserPromptSubmit/PostToolUse hook
|
|
3
3
|
// Checks .specialists/ready/ for completed background job markers and injects
|
|
4
|
-
// completion banners into Claude's context.
|
|
4
|
+
// completion/failure banners into Claude's context.
|
|
5
5
|
//
|
|
6
6
|
// Installed by: specialists install
|
|
7
7
|
|
|
@@ -32,16 +32,26 @@ for (const jobId of markers) {
|
|
|
32
32
|
try {
|
|
33
33
|
let specialist = jobId;
|
|
34
34
|
let elapsed = '';
|
|
35
|
+
let completionStatus = 'done';
|
|
36
|
+
let errorMessage = '';
|
|
35
37
|
|
|
36
38
|
if (existsSync(statusPath)) {
|
|
37
39
|
const status = JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
38
40
|
specialist = status.specialist ?? jobId;
|
|
39
41
|
elapsed = status.elapsed_s !== undefined ? `, ${status.elapsed_s}s` : '';
|
|
42
|
+
completionStatus = status.status ?? 'done';
|
|
43
|
+
errorMessage = status.error ? ` — ${status.error}` : '';
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if (completionStatus === 'error') {
|
|
47
|
+
banners.push(
|
|
48
|
+
`[Specialist '${specialist}' failed (job ${jobId}${elapsed}${errorMessage}). Run: specialists feed ${jobId} --follow]`
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
banners.push(
|
|
52
|
+
`[Specialist '${specialist}' completed (job ${jobId}${elapsed}). Run: specialists result ${jobId}]`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
// Delete marker so it only fires once
|
|
47
57
|
unlinkSync(markerPath);
|
|
@@ -53,7 +63,7 @@ for (const jobId of markers) {
|
|
|
53
63
|
|
|
54
64
|
if (banners.length === 0) process.exit(0);
|
|
55
65
|
|
|
56
|
-
// UserPromptSubmit hooks inject content via JSON
|
|
66
|
+
// UserPromptSubmit/PostToolUse hooks inject content via JSON
|
|
57
67
|
process.stdout.write(JSON.stringify({
|
|
58
68
|
type: 'inject',
|
|
59
69
|
content: banners.join('\n'),
|
|
@@ -5,10 +5,9 @@ description: >
|
|
|
5
5
|
ask whether to delegate. Consult before any: code review, security audit, deep bug
|
|
6
6
|
investigation, test generation, multi-file refactor, or architecture analysis. Also
|
|
7
7
|
use for the mechanics of delegation: --bead workflow, --context-depth, background
|
|
8
|
-
jobs, MCP tool (`use_specialist`)
|
|
9
|
-
or specialists doctor. Don't wait for the user to say "use a specialist" — proactively
|
|
8
|
+
jobs, and MCP tool (`use_specialist`). Don't wait for the user to say "use a specialist" — proactively
|
|
10
9
|
evaluate whether delegation makes sense.
|
|
11
|
-
version: 3.
|
|
10
|
+
version: 3.7
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
# Specialists Usage
|
|
@@ -51,7 +50,6 @@ links results back to the tracker, and creates an audit trail.
|
|
|
51
50
|
### CLI commands
|
|
52
51
|
|
|
53
52
|
```bash
|
|
54
|
-
specialists init # first-time project setup
|
|
55
53
|
specialists list # discover available specialists
|
|
56
54
|
specialists run <name> --bead <id> # foreground run (streams output)
|
|
57
55
|
specialists run <name> --prompt "..." # ad-hoc (no bead tracking)
|
|
@@ -64,7 +62,6 @@ specialists stop <job-id> # cancel a job
|
|
|
64
62
|
specialists edit <name> # edit a specialist's YAML config
|
|
65
63
|
specialists status --job <job-id> # single-job detail view
|
|
66
64
|
specialists clean # purge old job directories
|
|
67
|
-
specialists doctor # health check
|
|
68
65
|
```
|
|
69
66
|
|
|
70
67
|
### Typical flow
|
|
@@ -265,7 +262,6 @@ git diff --stat # review what changed
|
|
|
265
262
|
If a specialist stalls or errors, surface it. Don't quietly do the work yourself.
|
|
266
263
|
```bash
|
|
267
264
|
specialists feed <job-id> # see what happened
|
|
268
|
-
specialists doctor # check for systemic issues
|
|
269
265
|
```
|
|
270
266
|
|
|
271
267
|
Options when a specialist fails:
|
|
@@ -288,14 +284,14 @@ python3 .agents/skills/sync-docs/scripts/drift_detector.py update-sync <file>
|
|
|
288
284
|
|
|
289
285
|
## MCP Tools (Claude Code)
|
|
290
286
|
|
|
291
|
-
Available after `specialists init` and session restart.
|
|
292
|
-
|
|
293
287
|
| Tool | Purpose |
|
|
294
288
|
|------|---------|
|
|
295
289
|
| `use_specialist` | Foreground run; pass `bead_id` for tracked work and get final output directly in conversation context |
|
|
296
290
|
|
|
297
291
|
MCP is intentionally minimal. Use CLI commands for orchestration, monitoring, steering,
|
|
298
292
|
resume, and cancellation.
|
|
293
|
+
If you encounter legacy `start_specialist`, treat it as deprecated and migrate to
|
|
294
|
+
`specialists run <name> --prompt "..." --background`.
|
|
299
295
|
|
|
300
296
|
---
|
|
301
297
|
|
|
@@ -311,17 +307,10 @@ resume, and cancellation.
|
|
|
311
307
|
|
|
312
308
|
---
|
|
313
309
|
|
|
314
|
-
##
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
specialists init # first-time setup: creates .specialists/, wires AGENTS.md/CLAUDE.md
|
|
318
|
-
specialists doctor # health check: hooks, MCP, zombie jobs
|
|
319
|
-
specialists edit <name> # edit a specialist's YAML config
|
|
320
|
-
```
|
|
310
|
+
## Troubleshooting
|
|
321
311
|
|
|
322
312
|
- **"specialist not found"** → `specialists list` (project-scope only)
|
|
323
313
|
- **Job hangs** → `specialists steer <id> "finish up"` or `specialists stop <id>`
|
|
324
|
-
- **MCP tools missing** → `specialists init` then restart Claude Code
|
|
325
314
|
- **YAML skipped** → stderr shows `[specialists] skipping <file>: <reason>`
|
|
326
315
|
- **Stall timeout** → specialist hit 120s inactivity. Check `specialists feed <id>`, then retry or switch specialist.
|
|
327
316
|
- **`--prompt` and `--bead` conflict** → use bead notes: `bd update <id> --notes "INSTRUCTION: ..."` then `--bead` only.
|
package/dist/index.js
CHANGED
|
@@ -18531,16 +18531,22 @@ class SpecialistRunner {
|
|
|
18531
18531
|
system_prompt_present: !!prompt.system
|
|
18532
18532
|
});
|
|
18533
18533
|
let agentsMd = prompt.system ?? "";
|
|
18534
|
-
|
|
18534
|
+
{
|
|
18535
|
+
const beadInstructions = options.inputBeadId ? `
|
|
18536
|
+
- Your task bead is: ${options.inputBeadId}
|
|
18537
|
+
- Claim it: \`bd update ${options.inputBeadId} --claim\`
|
|
18538
|
+
- Do NOT create new beads or sub-issues — this bead IS your task.
|
|
18539
|
+
- Do NOT run \`bd create\` — the orchestrator manages issue tracking.
|
|
18540
|
+
- Close when done: \`bd close ${options.inputBeadId} --reason="..."\`` : "";
|
|
18535
18541
|
agentsMd += `
|
|
18536
18542
|
|
|
18537
18543
|
---
|
|
18538
|
-
## Specialist Run Context
|
|
18539
|
-
You are
|
|
18540
|
-
-
|
|
18541
|
-
- Do NOT
|
|
18542
|
-
-
|
|
18543
|
-
-
|
|
18544
|
+
## Specialist Run Context (OVERRIDES CLAUDE.md / AGENTS.md)
|
|
18545
|
+
You are a specialist agent. The following rules OVERRIDE any conflicting instructions in CLAUDE.md or AGENTS.md:
|
|
18546
|
+
- Do NOT run \`specialists init\` or \`sp init\` — these are user-only bootstrap commands that will corrupt the project.
|
|
18547
|
+
- Do NOT run \`specialists setup\` or \`specialists install\`.
|
|
18548
|
+
- The edit-gate (bd create before editing) does NOT apply inside a specialist run.
|
|
18549
|
+
- GitNexus impact warnings (HIGH/CRITICAL risk) are INFORMATIONAL ONLY — do NOT stop or wait for confirmation. Proceed with the fix regardless of risk level. The orchestrator has already approved the work by dispatching you.${beadInstructions}
|
|
18544
18550
|
---
|
|
18545
18551
|
`;
|
|
18546
18552
|
}
|
|
@@ -19150,7 +19156,7 @@ __export(exports_init, {
|
|
|
19150
19156
|
run: () => run5
|
|
19151
19157
|
});
|
|
19152
19158
|
import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
19153
|
-
import {
|
|
19159
|
+
import { join as join6 } from "node:path";
|
|
19154
19160
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19155
19161
|
function ok(msg) {
|
|
19156
19162
|
console.log(` ${green3("✓")} ${msg}`);
|
|
@@ -19297,6 +19303,7 @@ function ensureProjectHookWiring(cwd) {
|
|
|
19297
19303
|
}
|
|
19298
19304
|
}
|
|
19299
19305
|
addHook("UserPromptSubmit", "node .claude/hooks/specialists-complete.mjs");
|
|
19306
|
+
addHook("PostToolUse", "node .claude/hooks/specialists-complete.mjs");
|
|
19300
19307
|
addHook("SessionStart", "node .claude/hooks/specialists-session-start.mjs");
|
|
19301
19308
|
if (changed) {
|
|
19302
19309
|
saveJson(settingsPath, settings);
|
|
@@ -19344,11 +19351,18 @@ function installProjectSkills(cwd) {
|
|
|
19344
19351
|
skip(`${totalSkipped} skill location${totalSkipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
19345
19352
|
}
|
|
19346
19353
|
}
|
|
19347
|
-
function
|
|
19354
|
+
function createSpecialistsDirs(cwd) {
|
|
19355
|
+
const defaultDir = join6(cwd, ".specialists", "default");
|
|
19348
19356
|
const userDir = join6(cwd, ".specialists", "user");
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19357
|
+
let created = 0;
|
|
19358
|
+
for (const dir of [defaultDir, userDir]) {
|
|
19359
|
+
if (!existsSync6(dir)) {
|
|
19360
|
+
mkdirSync(dir, { recursive: true });
|
|
19361
|
+
created++;
|
|
19362
|
+
}
|
|
19363
|
+
}
|
|
19364
|
+
if (created > 0) {
|
|
19365
|
+
ok("created .specialists/default/ and .specialists/user/");
|
|
19352
19366
|
}
|
|
19353
19367
|
}
|
|
19354
19368
|
function createRuntimeDirs(cwd) {
|
|
@@ -19418,74 +19432,26 @@ function ensureAgentsMd(cwd) {
|
|
|
19418
19432
|
ok("created AGENTS.md with Specialists section");
|
|
19419
19433
|
}
|
|
19420
19434
|
}
|
|
19421
|
-
function
|
|
19422
|
-
return Boolean(process.env.PI_SESSION_ID || process.env.PI_RPC_SOCKET || process.env.PI_AGENT_SESSION || process.env.PI_CODING_AGENT);
|
|
19423
|
-
}
|
|
19424
|
-
function readLinuxProcFile(path) {
|
|
19425
|
-
try {
|
|
19426
|
-
return readFileSync3(path, "utf-8");
|
|
19427
|
-
} catch {
|
|
19428
|
-
return null;
|
|
19429
|
-
}
|
|
19430
|
-
}
|
|
19431
|
-
function getLinuxParentPid(pid) {
|
|
19432
|
-
const status = readLinuxProcFile(`/proc/${pid}/status`);
|
|
19433
|
-
if (!status)
|
|
19434
|
-
return null;
|
|
19435
|
-
const ppidLine = status.split(`
|
|
19436
|
-
`).find((line) => line.startsWith("PPid:"));
|
|
19437
|
-
if (!ppidLine)
|
|
19438
|
-
return null;
|
|
19439
|
-
const value = Number(ppidLine.replace("PPid:", "").trim());
|
|
19440
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
19441
|
-
}
|
|
19442
|
-
function hasPiAncestorProcess(maxDepth = 8) {
|
|
19443
|
-
let pid = process.ppid;
|
|
19444
|
-
let depth = 0;
|
|
19445
|
-
while (pid && depth < maxDepth) {
|
|
19446
|
-
const cmdline = readLinuxProcFile(`/proc/${pid}/cmdline`);
|
|
19447
|
-
if (!cmdline)
|
|
19448
|
-
break;
|
|
19449
|
-
const command = cmdline.replace(/\0/g, " ").trim();
|
|
19450
|
-
const executable = basename2(command.split(" ")[0] ?? "");
|
|
19451
|
-
const isPiExecutable = executable === "pi" || executable === "pi-coding-agent" || executable.startsWith("pi-");
|
|
19452
|
-
if (isPiExecutable || command.includes("@mariozechner/pi-coding-agent")) {
|
|
19453
|
-
return true;
|
|
19454
|
-
}
|
|
19455
|
-
pid = getLinuxParentPid(pid);
|
|
19456
|
-
depth++;
|
|
19457
|
-
}
|
|
19458
|
-
return false;
|
|
19459
|
-
}
|
|
19460
|
-
function hasExistingDefaultSpecialists(cwd) {
|
|
19461
|
-
const defaultDir = join6(cwd, ".specialists", "default");
|
|
19462
|
-
const legacyNestedDir = join6(defaultDir, "specialists");
|
|
19463
|
-
const hasFlat = existsSync6(defaultDir) && readdirSync2(defaultDir).some((file) => file.endsWith(".specialist.yaml"));
|
|
19464
|
-
if (hasFlat)
|
|
19465
|
-
return true;
|
|
19466
|
-
return existsSync6(legacyNestedDir) && readdirSync2(legacyNestedDir).some((file) => file.endsWith(".specialist.yaml"));
|
|
19467
|
-
}
|
|
19468
|
-
function shouldSkipDefaultSyncInPiSession(cwd) {
|
|
19469
|
-
if (process.env.SPECIALISTS_INIT_FORCE_DEFAULT_SYNC === "1")
|
|
19470
|
-
return false;
|
|
19471
|
-
if (!hasExistingDefaultSpecialists(cwd))
|
|
19472
|
-
return false;
|
|
19473
|
-
return hasPiSessionEnv() || hasPiAncestorProcess();
|
|
19474
|
-
}
|
|
19475
|
-
async function run5() {
|
|
19435
|
+
async function run5(opts = {}) {
|
|
19476
19436
|
const cwd = process.cwd();
|
|
19437
|
+
const forceInit = process.env.SPECIALISTS_INIT_FORCE === "1";
|
|
19438
|
+
const inAgentSession = !forceInit && (!process.stdin.isTTY || !!process.env.SPECIALISTS_TMUX_SESSION || !!process.env.SPECIALISTS_JOB_ID || !!process.env.PI_SESSION_ID || !!process.env.PI_RPC_SOCKET);
|
|
19439
|
+
if (inAgentSession) {
|
|
19440
|
+
console.error("specialists init requires an interactive terminal. This is a user-only bootstrap command — do not invoke from scripts or agent sessions.");
|
|
19441
|
+
process.exit(1);
|
|
19442
|
+
}
|
|
19477
19443
|
console.log(`
|
|
19478
19444
|
${bold4("specialists init")}
|
|
19479
19445
|
`);
|
|
19480
|
-
const
|
|
19481
|
-
if (
|
|
19482
|
-
skip("pi session detected with existing default specialists; skipped .specialists/default sync");
|
|
19483
|
-
} else {
|
|
19446
|
+
const { syncDefaults = false } = opts;
|
|
19447
|
+
if (syncDefaults) {
|
|
19484
19448
|
migrateLegacySpecialists(cwd, "default");
|
|
19485
19449
|
copyCanonicalSpecialists(cwd);
|
|
19450
|
+
} else {
|
|
19451
|
+
skip(".specialists/default/ not synced (pass --sync-defaults to write canonical specialists)");
|
|
19486
19452
|
}
|
|
19487
19453
|
migrateLegacySpecialists(cwd, "user");
|
|
19488
|
-
|
|
19454
|
+
createSpecialistsDirs(cwd);
|
|
19489
19455
|
createRuntimeDirs(cwd);
|
|
19490
19456
|
ensureGitignore(cwd);
|
|
19491
19457
|
ensureAgentsMd(cwd);
|
|
@@ -19504,7 +19470,7 @@ ${bold4("Done!")}
|
|
|
19504
19470
|
console.log("");
|
|
19505
19471
|
console.log(` ${dim4(".specialists/ structure:")}`);
|
|
19506
19472
|
console.log(` .specialists/`);
|
|
19507
|
-
console.log(` ├── default/ ${dim4("# canonical specialists (from init)")}`);
|
|
19473
|
+
console.log(` ├── default/ ${dim4("# canonical specialists (from init --sync-defaults)")}`);
|
|
19508
19474
|
console.log(` ├── user/ ${dim4("# your custom specialists")}`);
|
|
19509
19475
|
console.log(` ├── jobs/ ${dim4("# runtime (gitignored)")}`);
|
|
19510
19476
|
console.log(` └── ready/ ${dim4("# runtime (gitignored)")}`);
|
|
@@ -19520,7 +19486,7 @@ var init_init = __esm(() => {
|
|
|
19520
19486
|
AGENTS_BLOCK = `
|
|
19521
19487
|
## Specialists
|
|
19522
19488
|
|
|
19523
|
-
|
|
19489
|
+
Use CLI commands via Bash to run and monitor specialists:
|
|
19524
19490
|
|
|
19525
19491
|
Core specialist commands (CLI-first in pi):
|
|
19526
19492
|
- \`specialists list\`
|
|
@@ -19824,7 +19790,7 @@ __export(exports_config, {
|
|
|
19824
19790
|
});
|
|
19825
19791
|
import { existsSync as existsSync9 } from "node:fs";
|
|
19826
19792
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
19827
|
-
import { basename as
|
|
19793
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
19828
19794
|
function usage() {
|
|
19829
19795
|
return [
|
|
19830
19796
|
"Usage:",
|
|
@@ -19933,7 +19899,7 @@ async function getAcrossFiles(files, keyPath) {
|
|
|
19933
19899
|
const content = await readFile3(file, "utf-8");
|
|
19934
19900
|
const doc2 = $parseDocument(content);
|
|
19935
19901
|
const value = doc2.getIn(keyPath);
|
|
19936
|
-
const name = getSpecialistNameFromPath(
|
|
19902
|
+
const name = getSpecialistNameFromPath(basename2(file));
|
|
19937
19903
|
console.log(`${yellow7(name)}: ${formatValue(value)}`);
|
|
19938
19904
|
}
|
|
19939
19905
|
}
|
|
@@ -20201,6 +20167,10 @@ class Supervisor {
|
|
|
20201
20167
|
readyDir() {
|
|
20202
20168
|
return join10(this.opts.jobsDir, "..", "ready");
|
|
20203
20169
|
}
|
|
20170
|
+
writeReadyMarker(id) {
|
|
20171
|
+
mkdirSync2(this.readyDir(), { recursive: true });
|
|
20172
|
+
writeFileSync3(join10(this.readyDir(), id), "", "utf-8");
|
|
20173
|
+
}
|
|
20204
20174
|
readStatus(id) {
|
|
20205
20175
|
const path = this.statusPath(id);
|
|
20206
20176
|
if (!existsSync10(path))
|
|
@@ -20367,6 +20337,58 @@ class Supervisor {
|
|
|
20367
20337
|
let closeFn;
|
|
20368
20338
|
let fifoReadStream;
|
|
20369
20339
|
let fifoReadline;
|
|
20340
|
+
let keepAliveSession = false;
|
|
20341
|
+
let latestOutput = "";
|
|
20342
|
+
let keepAliveExitResolved = false;
|
|
20343
|
+
let resolveKeepAliveExit;
|
|
20344
|
+
const keepAliveExitPromise = new Promise((resolve2) => {
|
|
20345
|
+
resolveKeepAliveExit = resolve2;
|
|
20346
|
+
});
|
|
20347
|
+
const finishKeepAlive = (exit) => {
|
|
20348
|
+
if (keepAliveExitResolved)
|
|
20349
|
+
return;
|
|
20350
|
+
keepAliveExitResolved = true;
|
|
20351
|
+
resolveKeepAliveExit?.(exit);
|
|
20352
|
+
};
|
|
20353
|
+
const handleResumeTurn = async (task) => {
|
|
20354
|
+
if (!resumeFn)
|
|
20355
|
+
return;
|
|
20356
|
+
const now = Date.now();
|
|
20357
|
+
setStatus({ status: "running", current_event: "starting", last_event_at_ms: now });
|
|
20358
|
+
lastActivityMs = now;
|
|
20359
|
+
silenceWarnEmitted = false;
|
|
20360
|
+
try {
|
|
20361
|
+
const output = await resumeFn(task);
|
|
20362
|
+
latestOutput = output;
|
|
20363
|
+
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20364
|
+
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
20365
|
+
const waitingAt = Date.now();
|
|
20366
|
+
setStatus({
|
|
20367
|
+
status: "waiting",
|
|
20368
|
+
current_event: "waiting",
|
|
20369
|
+
elapsed_s: Math.round((waitingAt - startedAtMs) / 1000),
|
|
20370
|
+
last_event_at_ms: waitingAt
|
|
20371
|
+
});
|
|
20372
|
+
} catch (err) {
|
|
20373
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
20374
|
+
setStatus({ status: "error", error: error2.message });
|
|
20375
|
+
finishKeepAlive({ kind: "fatal", error: error2 });
|
|
20376
|
+
}
|
|
20377
|
+
};
|
|
20378
|
+
const closeKeepAliveSession = async () => {
|
|
20379
|
+
if (!closeFn) {
|
|
20380
|
+
finishKeepAlive({ kind: "closed" });
|
|
20381
|
+
return;
|
|
20382
|
+
}
|
|
20383
|
+
try {
|
|
20384
|
+
await closeFn();
|
|
20385
|
+
finishKeepAlive({ kind: "closed" });
|
|
20386
|
+
} catch (err) {
|
|
20387
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
20388
|
+
setStatus({ status: "error", error: error2.message });
|
|
20389
|
+
finishKeepAlive({ kind: "fatal", error: error2 });
|
|
20390
|
+
}
|
|
20391
|
+
};
|
|
20370
20392
|
const thresholds = {
|
|
20371
20393
|
...STALL_DETECTION_DEFAULTS,
|
|
20372
20394
|
...this.opts.stallDetection
|
|
@@ -20412,7 +20434,13 @@ class Supervisor {
|
|
|
20412
20434
|
}
|
|
20413
20435
|
}
|
|
20414
20436
|
}, 1e4);
|
|
20415
|
-
const sigtermHandler = () =>
|
|
20437
|
+
const sigtermHandler = () => {
|
|
20438
|
+
if (keepAliveSession) {
|
|
20439
|
+
closeKeepAliveSession();
|
|
20440
|
+
return;
|
|
20441
|
+
}
|
|
20442
|
+
killFn?.();
|
|
20443
|
+
};
|
|
20416
20444
|
process.once("SIGTERM", sigtermHandler);
|
|
20417
20445
|
try {
|
|
20418
20446
|
const result = await runner.run(runOptions, (delta) => {
|
|
@@ -20464,48 +20492,21 @@ class Supervisor {
|
|
|
20464
20492
|
if (parsed?.type === "steer" && typeof parsed.message === "string") {
|
|
20465
20493
|
steerFn?.(parsed.message).catch(() => {});
|
|
20466
20494
|
} else if (parsed?.type === "resume" && typeof parsed.task === "string") {
|
|
20467
|
-
|
|
20468
|
-
setStatus({ status: "running", current_event: "starting" });
|
|
20469
|
-
resumeFn(parsed.task).then((output) => {
|
|
20470
|
-
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20471
|
-
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
20472
|
-
setStatus({
|
|
20473
|
-
status: "waiting",
|
|
20474
|
-
current_event: "waiting",
|
|
20475
|
-
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
20476
|
-
last_event_at_ms: Date.now()
|
|
20477
|
-
});
|
|
20478
|
-
}).catch((err) => {
|
|
20479
|
-
setStatus({ status: "error", error: err?.message ?? String(err) });
|
|
20480
|
-
});
|
|
20481
|
-
}
|
|
20495
|
+
handleResumeTurn(parsed.task);
|
|
20482
20496
|
} else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
|
|
20483
20497
|
console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
|
|
20484
|
-
|
|
20485
|
-
setStatus({ status: "running", current_event: "starting" });
|
|
20486
|
-
resumeFn(parsed.message).then((output) => {
|
|
20487
|
-
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20488
|
-
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
20489
|
-
setStatus({
|
|
20490
|
-
status: "waiting",
|
|
20491
|
-
current_event: "waiting",
|
|
20492
|
-
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
20493
|
-
last_event_at_ms: Date.now()
|
|
20494
|
-
});
|
|
20495
|
-
}).catch((err) => {
|
|
20496
|
-
setStatus({ status: "error", error: err?.message ?? String(err) });
|
|
20497
|
-
});
|
|
20498
|
-
}
|
|
20498
|
+
handleResumeTurn(parsed.message);
|
|
20499
20499
|
} else if (parsed?.type === "close") {
|
|
20500
|
-
|
|
20500
|
+
closeKeepAliveSession();
|
|
20501
20501
|
}
|
|
20502
20502
|
} catch {}
|
|
20503
20503
|
});
|
|
20504
20504
|
fifoReadline.on("error", () => {});
|
|
20505
20505
|
}, (rFn, cFn) => {
|
|
20506
|
+
keepAliveSession = true;
|
|
20506
20507
|
resumeFn = rFn;
|
|
20507
20508
|
closeFn = cFn;
|
|
20508
|
-
setStatus({ status: "waiting", current_event: "waiting" });
|
|
20509
|
+
setStatus({ status: "waiting", current_event: "waiting", last_event_at_ms: Date.now() });
|
|
20509
20510
|
}, (tool, args, toolCallId) => {
|
|
20510
20511
|
currentTool = tool;
|
|
20511
20512
|
currentToolArgs = args;
|
|
@@ -20531,43 +20532,62 @@ class Supervisor {
|
|
|
20531
20532
|
toolStartMs = undefined;
|
|
20532
20533
|
toolDurationWarnEmitted = false;
|
|
20533
20534
|
});
|
|
20534
|
-
|
|
20535
|
+
latestOutput = result.output;
|
|
20535
20536
|
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20536
|
-
writeFileSync3(this.resultPath(id),
|
|
20537
|
+
writeFileSync3(this.resultPath(id), latestOutput, "utf-8");
|
|
20538
|
+
if (keepAliveSession) {
|
|
20539
|
+
setStatus({
|
|
20540
|
+
status: "waiting",
|
|
20541
|
+
current_event: "waiting",
|
|
20542
|
+
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
20543
|
+
last_event_at_ms: Date.now(),
|
|
20544
|
+
model: result.model,
|
|
20545
|
+
backend: result.backend,
|
|
20546
|
+
bead_id: result.beadId
|
|
20547
|
+
});
|
|
20548
|
+
const keepAliveExit = await keepAliveExitPromise;
|
|
20549
|
+
if (keepAliveExit.kind === "fatal") {
|
|
20550
|
+
throw keepAliveExit.error;
|
|
20551
|
+
}
|
|
20552
|
+
}
|
|
20553
|
+
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
20554
|
+
const finalResult = {
|
|
20555
|
+
...result,
|
|
20556
|
+
output: latestOutput
|
|
20557
|
+
};
|
|
20537
20558
|
const inputBeadId = runOptions.inputBeadId;
|
|
20538
|
-
const ownsBead = Boolean(
|
|
20559
|
+
const ownsBead = Boolean(finalResult.beadId && !inputBeadId);
|
|
20539
20560
|
const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
|
|
20540
|
-
const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId &&
|
|
20541
|
-
if (ownsBead &&
|
|
20542
|
-
this.opts.beadsClient?.updateBeadNotes(
|
|
20561
|
+
const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId && finalResult.permissionRequired === "READ_ONLY" && this.opts.beadsClient);
|
|
20562
|
+
if (ownsBead && finalResult.beadId) {
|
|
20563
|
+
this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatBeadNotes(finalResult));
|
|
20543
20564
|
} else if (shouldWriteExternalBeadNotes) {
|
|
20544
20565
|
if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
|
|
20545
|
-
this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(
|
|
20546
|
-
} else if (
|
|
20547
|
-
this.opts.beadsClient?.updateBeadNotes(
|
|
20566
|
+
this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(finalResult));
|
|
20567
|
+
} else if (finalResult.beadId) {
|
|
20568
|
+
this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatBeadNotes(finalResult));
|
|
20548
20569
|
}
|
|
20549
20570
|
}
|
|
20550
|
-
if (
|
|
20571
|
+
if (finalResult.beadId) {
|
|
20551
20572
|
if (!inputBeadId) {
|
|
20552
|
-
this.opts.beadsClient?.closeBead(
|
|
20573
|
+
this.opts.beadsClient?.closeBead(finalResult.beadId, "COMPLETE", finalResult.durationMs, finalResult.model);
|
|
20553
20574
|
}
|
|
20554
20575
|
}
|
|
20555
20576
|
setStatus({
|
|
20556
20577
|
status: "done",
|
|
20557
20578
|
elapsed_s: elapsed,
|
|
20558
20579
|
last_event_at_ms: Date.now(),
|
|
20559
|
-
model:
|
|
20560
|
-
backend:
|
|
20561
|
-
bead_id:
|
|
20580
|
+
model: finalResult.model,
|
|
20581
|
+
backend: finalResult.backend,
|
|
20582
|
+
bead_id: finalResult.beadId
|
|
20562
20583
|
});
|
|
20563
20584
|
appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
|
|
20564
|
-
model:
|
|
20565
|
-
backend:
|
|
20566
|
-
bead_id:
|
|
20567
|
-
output:
|
|
20585
|
+
model: finalResult.model,
|
|
20586
|
+
backend: finalResult.backend,
|
|
20587
|
+
bead_id: finalResult.beadId,
|
|
20588
|
+
output: finalResult.output
|
|
20568
20589
|
}));
|
|
20569
|
-
|
|
20570
|
-
writeFileSync3(join10(this.readyDir(), id), "", "utf-8");
|
|
20590
|
+
this.writeReadyMarker(id);
|
|
20571
20591
|
return id;
|
|
20572
20592
|
} catch (err) {
|
|
20573
20593
|
const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
|
|
@@ -20580,6 +20600,7 @@ class Supervisor {
|
|
|
20580
20600
|
appendTimelineEvent(createRunCompleteEvent("ERROR", elapsed, {
|
|
20581
20601
|
error: errorMsg
|
|
20582
20602
|
}));
|
|
20603
|
+
this.writeReadyMarker(id);
|
|
20583
20604
|
throw err;
|
|
20584
20605
|
} finally {
|
|
20585
20606
|
if (stuckIntervalId !== undefined)
|
|
@@ -20794,6 +20815,9 @@ function createTmuxSession(name, cwd, cmd, extraEnv = {}) {
|
|
|
20794
20815
|
throw new Error(`Failed to create tmux session "${name}": ${errorOutput}`);
|
|
20795
20816
|
}
|
|
20796
20817
|
}
|
|
20818
|
+
function killTmuxSession(name) {
|
|
20819
|
+
spawnSync7("tmux", ["kill-session", "-t", name], { encoding: "utf8", stdio: "pipe" });
|
|
20820
|
+
}
|
|
20797
20821
|
var TMUX_SESSION_PREFIX = "sp";
|
|
20798
20822
|
var init_tmux_utils = () => {};
|
|
20799
20823
|
|
|
@@ -21628,7 +21652,14 @@ var exports_feed = {};
|
|
|
21628
21652
|
__export(exports_feed, {
|
|
21629
21653
|
run: () => run12
|
|
21630
21654
|
});
|
|
21631
|
-
import {
|
|
21655
|
+
import {
|
|
21656
|
+
closeSync as closeSync2,
|
|
21657
|
+
existsSync as existsSync14,
|
|
21658
|
+
openSync as openSync2,
|
|
21659
|
+
readFileSync as readFileSync10,
|
|
21660
|
+
readdirSync as readdirSync6,
|
|
21661
|
+
statSync as statSync2
|
|
21662
|
+
} from "node:fs";
|
|
21632
21663
|
import { join as join15 } from "node:path";
|
|
21633
21664
|
function getHumanEventKey(event) {
|
|
21634
21665
|
switch (event.type) {
|
|
@@ -21692,31 +21723,55 @@ function parseSince(value) {
|
|
|
21692
21723
|
}
|
|
21693
21724
|
return;
|
|
21694
21725
|
}
|
|
21695
|
-
function
|
|
21726
|
+
function readFileFresh(filePath) {
|
|
21727
|
+
try {
|
|
21728
|
+
const fd = openSync2(filePath, "r");
|
|
21729
|
+
try {
|
|
21730
|
+
return readFileSync10(fd, "utf-8");
|
|
21731
|
+
} finally {
|
|
21732
|
+
closeSync2(fd);
|
|
21733
|
+
}
|
|
21734
|
+
} catch {
|
|
21735
|
+
return null;
|
|
21736
|
+
}
|
|
21737
|
+
}
|
|
21738
|
+
function readStatusJson(jobsDir, jobId) {
|
|
21696
21739
|
const statusPath = join15(jobsDir, jobId, "status.json");
|
|
21740
|
+
const raw = readFileFresh(statusPath);
|
|
21741
|
+
if (!raw)
|
|
21742
|
+
return null;
|
|
21697
21743
|
try {
|
|
21698
|
-
|
|
21699
|
-
return status.status === "done" || status.status === "error";
|
|
21744
|
+
return JSON.parse(raw);
|
|
21700
21745
|
} catch {
|
|
21701
|
-
return
|
|
21746
|
+
return null;
|
|
21702
21747
|
}
|
|
21703
21748
|
}
|
|
21704
|
-
function
|
|
21749
|
+
function isTerminalJobStatus(jobsDir, jobId) {
|
|
21750
|
+
const status = readStatusJson(jobsDir, jobId);
|
|
21751
|
+
return status?.status === "done" || status?.status === "error";
|
|
21752
|
+
}
|
|
21753
|
+
function readJobMeta(jobsDir, jobId) {
|
|
21754
|
+
const status = readStatusJson(jobsDir, jobId);
|
|
21755
|
+
if (!status)
|
|
21756
|
+
return { startedAtMs: Date.now() };
|
|
21757
|
+
return {
|
|
21758
|
+
model: typeof status.model === "string" ? status.model : undefined,
|
|
21759
|
+
backend: typeof status.backend === "string" ? status.backend : undefined,
|
|
21760
|
+
beadId: typeof status.bead_id === "string" ? status.bead_id : undefined,
|
|
21761
|
+
startedAtMs: typeof status.started_at_ms === "number" ? status.started_at_ms : Date.now()
|
|
21762
|
+
};
|
|
21763
|
+
}
|
|
21764
|
+
function makeJobMetaReader(jobsDir, options = {}) {
|
|
21765
|
+
const useCache = options.useCache ?? true;
|
|
21766
|
+
if (!useCache) {
|
|
21767
|
+
return (jobId) => readJobMeta(jobsDir, jobId);
|
|
21768
|
+
}
|
|
21705
21769
|
const cache = new Map;
|
|
21706
21770
|
return (jobId) => {
|
|
21707
|
-
|
|
21708
|
-
|
|
21709
|
-
|
|
21710
|
-
|
|
21711
|
-
try {
|
|
21712
|
-
const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
|
|
21713
|
-
meta = {
|
|
21714
|
-
model: status.model,
|
|
21715
|
-
backend: status.backend,
|
|
21716
|
-
beadId: status.bead_id,
|
|
21717
|
-
startedAtMs: status.started_at_ms ?? Date.now()
|
|
21718
|
-
};
|
|
21719
|
-
} catch {}
|
|
21771
|
+
const cached2 = cache.get(jobId);
|
|
21772
|
+
if (cached2)
|
|
21773
|
+
return cached2;
|
|
21774
|
+
const meta = readJobMeta(jobsDir, jobId);
|
|
21720
21775
|
cache.set(jobId, meta);
|
|
21721
21776
|
return meta;
|
|
21722
21777
|
};
|
|
@@ -21725,6 +21780,7 @@ function parseArgs8(argv) {
|
|
|
21725
21780
|
let jobId;
|
|
21726
21781
|
let specialist;
|
|
21727
21782
|
let since;
|
|
21783
|
+
let from = 0;
|
|
21728
21784
|
let limit = 100;
|
|
21729
21785
|
let follow = false;
|
|
21730
21786
|
let forever = false;
|
|
@@ -21742,6 +21798,11 @@ function parseArgs8(argv) {
|
|
|
21742
21798
|
since = parseSince(argv[++i]);
|
|
21743
21799
|
continue;
|
|
21744
21800
|
}
|
|
21801
|
+
if (argv[i] === "--from" && argv[i + 1]) {
|
|
21802
|
+
const parsedFrom = parseInt(argv[++i], 10);
|
|
21803
|
+
from = Number.isFinite(parsedFrom) && parsedFrom >= 0 ? parsedFrom : 0;
|
|
21804
|
+
continue;
|
|
21805
|
+
}
|
|
21745
21806
|
if (argv[i] === "--limit" && argv[i + 1]) {
|
|
21746
21807
|
limit = parseInt(argv[++i], 10);
|
|
21747
21808
|
continue;
|
|
@@ -21761,7 +21822,7 @@ function parseArgs8(argv) {
|
|
|
21761
21822
|
if (!jobId && !argv[i].startsWith("--"))
|
|
21762
21823
|
jobId = argv[i];
|
|
21763
21824
|
}
|
|
21764
|
-
return { jobId, specialist, since, limit, follow, forever, json };
|
|
21825
|
+
return { jobId, specialist, since, from, limit, follow, forever, json };
|
|
21765
21826
|
}
|
|
21766
21827
|
function printSnapshot(merged, options, jobsDir) {
|
|
21767
21828
|
if (merged.length === 0) {
|
|
@@ -21806,36 +21867,106 @@ function printSnapshot(merged, options, jobsDir) {
|
|
|
21806
21867
|
function isCompletionEvent(event) {
|
|
21807
21868
|
return isRunCompleteEvent(event);
|
|
21808
21869
|
}
|
|
21870
|
+
function isEventAtOrAfterCursor(event, from) {
|
|
21871
|
+
if (from <= 0)
|
|
21872
|
+
return true;
|
|
21873
|
+
const seq = event.seq;
|
|
21874
|
+
if (typeof seq !== "number")
|
|
21875
|
+
return true;
|
|
21876
|
+
return seq >= from;
|
|
21877
|
+
}
|
|
21878
|
+
function filterMergedEventsByCursor(merged, from) {
|
|
21879
|
+
if (from <= 0)
|
|
21880
|
+
return merged;
|
|
21881
|
+
return merged.filter(({ event }) => isEventAtOrAfterCursor(event, from));
|
|
21882
|
+
}
|
|
21883
|
+
function listMatchingJobIds(jobsDir, options) {
|
|
21884
|
+
if (!existsSync14(jobsDir))
|
|
21885
|
+
return [];
|
|
21886
|
+
const jobIds = [];
|
|
21887
|
+
for (const entry of readdirSync6(jobsDir)) {
|
|
21888
|
+
const jobDir = join15(jobsDir, entry);
|
|
21889
|
+
try {
|
|
21890
|
+
if (!statSync2(jobDir).isDirectory())
|
|
21891
|
+
continue;
|
|
21892
|
+
} catch {
|
|
21893
|
+
continue;
|
|
21894
|
+
}
|
|
21895
|
+
if (options.jobId && entry !== options.jobId)
|
|
21896
|
+
continue;
|
|
21897
|
+
if (options.specialist) {
|
|
21898
|
+
const status = readStatusJson(jobsDir, entry);
|
|
21899
|
+
const specialist = typeof status?.specialist === "string" ? status.specialist : undefined;
|
|
21900
|
+
if (specialist !== options.specialist)
|
|
21901
|
+
continue;
|
|
21902
|
+
}
|
|
21903
|
+
jobIds.push(entry);
|
|
21904
|
+
}
|
|
21905
|
+
return jobIds;
|
|
21906
|
+
}
|
|
21907
|
+
function readJobEventsFresh(jobsDir, jobId) {
|
|
21908
|
+
const eventsPath = join15(jobsDir, jobId, "events.jsonl");
|
|
21909
|
+
const content = readFileFresh(eventsPath);
|
|
21910
|
+
if (!content)
|
|
21911
|
+
return [];
|
|
21912
|
+
const events = [];
|
|
21913
|
+
for (const line of content.split(`
|
|
21914
|
+
`)) {
|
|
21915
|
+
if (!line.trim())
|
|
21916
|
+
continue;
|
|
21917
|
+
const parsed = parseTimelineEvent(line);
|
|
21918
|
+
if (parsed)
|
|
21919
|
+
events.push(parsed);
|
|
21920
|
+
}
|
|
21921
|
+
events.sort((a, b) => a.t - b.t);
|
|
21922
|
+
return events;
|
|
21923
|
+
}
|
|
21924
|
+
function readFilteredBatchesFresh(jobsDir, options) {
|
|
21925
|
+
const batches = [];
|
|
21926
|
+
for (const jobId of listMatchingJobIds(jobsDir, options)) {
|
|
21927
|
+
const status = readStatusJson(jobsDir, jobId);
|
|
21928
|
+
const specialist = typeof status?.specialist === "string" ? status.specialist : "unknown";
|
|
21929
|
+
const beadId = typeof status?.bead_id === "string" ? status.bead_id : undefined;
|
|
21930
|
+
const events = readJobEventsFresh(jobsDir, jobId);
|
|
21931
|
+
if (events.length === 0)
|
|
21932
|
+
continue;
|
|
21933
|
+
batches.push({ jobId, specialist, beadId, events });
|
|
21934
|
+
}
|
|
21935
|
+
return batches;
|
|
21936
|
+
}
|
|
21809
21937
|
async function followMerged(jobsDir, options) {
|
|
21810
21938
|
const colorMap = new JobColorMap;
|
|
21811
|
-
const getJobMeta = makeJobMetaReader(jobsDir);
|
|
21939
|
+
const getJobMeta = makeJobMetaReader(jobsDir, { useCache: false });
|
|
21812
21940
|
const lastSeenT = new Map;
|
|
21941
|
+
const trackedJobs = new Set(listMatchingJobIds(jobsDir, options).filter((jobId) => !isTerminalJobStatus(jobsDir, jobId)));
|
|
21813
21942
|
const completedJobs = new Set;
|
|
21814
|
-
const filteredBatches = () =>
|
|
21815
|
-
const initial = queryTimeline(jobsDir, {
|
|
21943
|
+
const filteredBatches = () => readFilteredBatchesFresh(jobsDir, options);
|
|
21944
|
+
const initial = filterMergedEventsByCursor(queryTimeline(jobsDir, {
|
|
21816
21945
|
jobId: options.jobId,
|
|
21817
21946
|
specialist: options.specialist,
|
|
21818
21947
|
since: options.since,
|
|
21819
21948
|
limit: options.limit
|
|
21820
|
-
});
|
|
21949
|
+
}), options.from);
|
|
21821
21950
|
printSnapshot(initial, { ...options, json: options.json }, jobsDir);
|
|
21822
21951
|
for (const batch of filteredBatches()) {
|
|
21823
21952
|
if (batch.events.length > 0) {
|
|
21824
21953
|
const maxT = Math.max(...batch.events.map((event) => event.t));
|
|
21825
21954
|
lastSeenT.set(batch.jobId, maxT);
|
|
21826
21955
|
}
|
|
21827
|
-
if (batch.events.some(isCompletionEvent)
|
|
21956
|
+
if (trackedJobs.has(batch.jobId) && batch.events.some(isCompletionEvent)) {
|
|
21828
21957
|
completedJobs.add(batch.jobId);
|
|
21829
21958
|
}
|
|
21830
21959
|
}
|
|
21831
|
-
|
|
21832
|
-
if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
|
|
21960
|
+
if (!options.forever && trackedJobs.size === 0) {
|
|
21833
21961
|
if (!options.json) {
|
|
21834
21962
|
process.stderr.write(dim7(`All jobs complete.
|
|
21835
21963
|
`));
|
|
21836
21964
|
}
|
|
21837
21965
|
return;
|
|
21838
21966
|
}
|
|
21967
|
+
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
21968
|
+
return;
|
|
21969
|
+
}
|
|
21839
21970
|
if (!options.json) {
|
|
21840
21971
|
process.stderr.write(dim7(`Following... (Ctrl+C to stop)
|
|
21841
21972
|
`));
|
|
@@ -21845,11 +21976,21 @@ async function followMerged(jobsDir, options) {
|
|
|
21845
21976
|
await new Promise((resolve2) => {
|
|
21846
21977
|
const interval = setInterval(() => {
|
|
21847
21978
|
const batches = filteredBatches();
|
|
21979
|
+
for (const jobId of listMatchingJobIds(jobsDir, options)) {
|
|
21980
|
+
if (!isTerminalJobStatus(jobsDir, jobId)) {
|
|
21981
|
+
trackedJobs.add(jobId);
|
|
21982
|
+
}
|
|
21983
|
+
}
|
|
21984
|
+
for (const jobId of trackedJobs) {
|
|
21985
|
+
if (isTerminalJobStatus(jobsDir, jobId)) {
|
|
21986
|
+
completedJobs.add(jobId);
|
|
21987
|
+
}
|
|
21988
|
+
}
|
|
21848
21989
|
const newEvents = [];
|
|
21849
21990
|
for (const batch of batches) {
|
|
21850
21991
|
const lastT = lastSeenT.get(batch.jobId) ?? 0;
|
|
21851
21992
|
for (const event of batch.events) {
|
|
21852
|
-
if (event.t > lastT) {
|
|
21993
|
+
if (event.t > lastT && isEventAtOrAfterCursor(event, options.from)) {
|
|
21853
21994
|
newEvents.push({
|
|
21854
21995
|
jobId: batch.jobId,
|
|
21855
21996
|
specialist: batch.specialist,
|
|
@@ -21862,7 +22003,7 @@ async function followMerged(jobsDir, options) {
|
|
|
21862
22003
|
const maxT = Math.max(...batch.events.map((e) => e.t));
|
|
21863
22004
|
lastSeenT.set(batch.jobId, maxT);
|
|
21864
22005
|
}
|
|
21865
|
-
if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
|
|
22006
|
+
if (trackedJobs.has(batch.jobId) && (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId))) {
|
|
21866
22007
|
completedJobs.add(batch.jobId);
|
|
21867
22008
|
}
|
|
21868
22009
|
}
|
|
@@ -21892,7 +22033,7 @@ async function followMerged(jobsDir, options) {
|
|
|
21892
22033
|
console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
|
|
21893
22034
|
}
|
|
21894
22035
|
}
|
|
21895
|
-
if (!options.forever &&
|
|
22036
|
+
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
21896
22037
|
clearInterval(interval);
|
|
21897
22038
|
resolve2();
|
|
21898
22039
|
}
|
|
@@ -21906,16 +22047,19 @@ async function run12() {
|
|
|
21906
22047
|
console.log(dim7("No jobs directory found."));
|
|
21907
22048
|
return;
|
|
21908
22049
|
}
|
|
22050
|
+
if (options.from > 0 && !options.json) {
|
|
22051
|
+
console.log(dim7(`Showing events from seq ${options.from}`));
|
|
22052
|
+
}
|
|
21909
22053
|
if (options.follow) {
|
|
21910
22054
|
await followMerged(jobsDir, options);
|
|
21911
22055
|
return;
|
|
21912
22056
|
}
|
|
21913
|
-
const merged = queryTimeline(jobsDir, {
|
|
22057
|
+
const merged = filterMergedEventsByCursor(queryTimeline(jobsDir, {
|
|
21914
22058
|
jobId: options.jobId,
|
|
21915
22059
|
specialist: options.specialist,
|
|
21916
22060
|
since: options.since,
|
|
21917
22061
|
limit: options.limit
|
|
21918
|
-
});
|
|
22062
|
+
}), options.from);
|
|
21919
22063
|
printSnapshot(merged, options, jobsDir);
|
|
21920
22064
|
}
|
|
21921
22065
|
var init_feed = __esm(() => {
|
|
@@ -22159,10 +22303,10 @@ __export(exports_clean, {
|
|
|
22159
22303
|
});
|
|
22160
22304
|
import {
|
|
22161
22305
|
existsSync as existsSync16,
|
|
22162
|
-
readdirSync as
|
|
22306
|
+
readdirSync as readdirSync7,
|
|
22163
22307
|
readFileSync as readFileSync12,
|
|
22164
22308
|
rmSync as rmSync2,
|
|
22165
|
-
statSync as
|
|
22309
|
+
statSync as statSync3
|
|
22166
22310
|
} from "node:fs";
|
|
22167
22311
|
import { join as join19 } from "node:path";
|
|
22168
22312
|
function parseTtlDaysFromEnvironment() {
|
|
@@ -22218,10 +22362,10 @@ function parseOptions(argv) {
|
|
|
22218
22362
|
}
|
|
22219
22363
|
function readDirectorySizeBytes(directoryPath) {
|
|
22220
22364
|
let totalBytes = 0;
|
|
22221
|
-
const entries =
|
|
22365
|
+
const entries = readdirSync7(directoryPath, { withFileTypes: true });
|
|
22222
22366
|
for (const entry of entries) {
|
|
22223
22367
|
const entryPath = join19(directoryPath, entry.name);
|
|
22224
|
-
const stats =
|
|
22368
|
+
const stats = statSync3(entryPath);
|
|
22225
22369
|
if (stats.isDirectory()) {
|
|
22226
22370
|
totalBytes += readDirectorySizeBytes(entryPath);
|
|
22227
22371
|
continue;
|
|
@@ -22245,7 +22389,7 @@ function readCompletedJobDirectory(baseDirectory, entry) {
|
|
|
22245
22389
|
}
|
|
22246
22390
|
if (!COMPLETED_STATUSES.has(statusData.status))
|
|
22247
22391
|
return null;
|
|
22248
|
-
const directoryStats =
|
|
22392
|
+
const directoryStats = statSync3(directoryPath);
|
|
22249
22393
|
return {
|
|
22250
22394
|
id: entry.name,
|
|
22251
22395
|
directoryPath,
|
|
@@ -22255,7 +22399,7 @@ function readCompletedJobDirectory(baseDirectory, entry) {
|
|
|
22255
22399
|
};
|
|
22256
22400
|
}
|
|
22257
22401
|
function collectCompletedJobDirectories(jobsDirectoryPath) {
|
|
22258
|
-
const entries =
|
|
22402
|
+
const entries = readdirSync7(jobsDirectoryPath, { withFileTypes: true });
|
|
22259
22403
|
const completedJobs = [];
|
|
22260
22404
|
for (const entry of entries) {
|
|
22261
22405
|
const completedJob = readCompletedJobDirectory(jobsDirectoryPath, entry);
|
|
@@ -22372,14 +22516,25 @@ async function run18() {
|
|
|
22372
22516
|
`);
|
|
22373
22517
|
process.exit(1);
|
|
22374
22518
|
}
|
|
22519
|
+
const tmuxSession = status.tmux_session;
|
|
22375
22520
|
try {
|
|
22376
22521
|
process.kill(status.pid, "SIGTERM");
|
|
22377
22522
|
process.stdout.write(`${green11("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
22378
22523
|
`);
|
|
22524
|
+
if (tmuxSession) {
|
|
22525
|
+
killTmuxSession(tmuxSession);
|
|
22526
|
+
process.stdout.write(`${dim10(` tmux session ${tmuxSession} killed`)}
|
|
22527
|
+
`);
|
|
22528
|
+
}
|
|
22379
22529
|
} catch (err) {
|
|
22380
22530
|
if (err.code === "ESRCH") {
|
|
22381
22531
|
process.stderr.write(`${red6(`Process ${status.pid} not found.`)} Job may have already completed.
|
|
22382
22532
|
`);
|
|
22533
|
+
if (tmuxSession) {
|
|
22534
|
+
killTmuxSession(tmuxSession);
|
|
22535
|
+
process.stdout.write(`${dim10(` tmux session ${tmuxSession} killed`)}
|
|
22536
|
+
`);
|
|
22537
|
+
}
|
|
22383
22538
|
} else {
|
|
22384
22539
|
process.stderr.write(`${red6("Error:")} ${err.message}
|
|
22385
22540
|
`);
|
|
@@ -22390,6 +22545,7 @@ async function run18() {
|
|
|
22390
22545
|
var green11 = (s) => `\x1B[32m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
22391
22546
|
var init_stop = __esm(() => {
|
|
22392
22547
|
init_supervisor();
|
|
22548
|
+
init_tmux_utils();
|
|
22393
22549
|
});
|
|
22394
22550
|
|
|
22395
22551
|
// src/cli/attach.ts
|
|
@@ -22678,7 +22834,7 @@ __export(exports_doctor, {
|
|
|
22678
22834
|
run: () => run21
|
|
22679
22835
|
});
|
|
22680
22836
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
22681
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync3, readFileSync as readFileSync14, readdirSync as
|
|
22837
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync3, readFileSync as readFileSync14, readdirSync as readdirSync8 } from "node:fs";
|
|
22682
22838
|
import { join as join22 } from "node:path";
|
|
22683
22839
|
function ok3(msg) {
|
|
22684
22840
|
console.log(` ${green13("✓")} ${msg}`);
|
|
@@ -22850,7 +23006,7 @@ function checkZombieJobs() {
|
|
|
22850
23006
|
}
|
|
22851
23007
|
let entries;
|
|
22852
23008
|
try {
|
|
22853
|
-
entries =
|
|
23009
|
+
entries = readdirSync8(jobsDir);
|
|
22854
23010
|
} catch {
|
|
22855
23011
|
entries = [];
|
|
22856
23012
|
}
|
|
@@ -30577,34 +30733,39 @@ async function run24() {
|
|
|
30577
30733
|
if (wantsHelp()) {
|
|
30578
30734
|
console.log([
|
|
30579
30735
|
"",
|
|
30580
|
-
"Usage: specialists init [--
|
|
30736
|
+
"Usage: specialists init [--sync-defaults]",
|
|
30581
30737
|
"",
|
|
30582
30738
|
"Bootstrap a project for specialists. This is the sole onboarding command.",
|
|
30583
30739
|
"",
|
|
30584
|
-
"What it does:",
|
|
30585
|
-
" • creates specialists/ for
|
|
30586
|
-
" • creates .specialists/ runtime dirs
|
|
30587
|
-
" • adds
|
|
30588
|
-
" • injects the
|
|
30589
|
-
" • registers the Specialists MCP server at project scope",
|
|
30740
|
+
"What it does (always safe, idempotent):",
|
|
30741
|
+
" • creates .specialists/user/ for custom specialists",
|
|
30742
|
+
" • creates .specialists/jobs/ and .specialists/ready/ runtime dirs",
|
|
30743
|
+
" • adds runtime dirs to .gitignore",
|
|
30744
|
+
" • injects the Specialists section into AGENTS.md",
|
|
30745
|
+
" • registers the Specialists MCP server at project scope (.mcp.json)",
|
|
30746
|
+
" • installs hooks to .claude/hooks/ and wires .claude/settings.json",
|
|
30747
|
+
" • installs skills to .claude/skills/ and .pi/skills/",
|
|
30590
30748
|
"",
|
|
30591
30749
|
"Options:",
|
|
30592
|
-
" --
|
|
30750
|
+
" --sync-defaults Also copy canonical specialists to .specialists/default/.",
|
|
30751
|
+
" Human-only: rewrites default specialist YAML files.",
|
|
30593
30752
|
"",
|
|
30594
30753
|
"Examples:",
|
|
30595
|
-
" specialists init",
|
|
30596
|
-
" specialists init --
|
|
30754
|
+
" specialists init # safe for agents to call",
|
|
30755
|
+
" specialists init --sync-defaults # human-only: sync canonical specialists",
|
|
30597
30756
|
"",
|
|
30598
30757
|
"Notes:",
|
|
30599
30758
|
" setup and install are deprecated; use specialists init.",
|
|
30600
|
-
"
|
|
30759
|
+
" MCP missing → specialists init (safe for anyone to call).",
|
|
30760
|
+
" Specialists missing → specialists init --sync-defaults (human-only).",
|
|
30601
30761
|
""
|
|
30602
30762
|
].join(`
|
|
30603
30763
|
`));
|
|
30604
30764
|
return;
|
|
30605
30765
|
}
|
|
30766
|
+
const syncDefaults = process.argv.includes("--sync-defaults");
|
|
30606
30767
|
const { run: handler } = await Promise.resolve().then(() => (init_init(), exports_init));
|
|
30607
|
-
return handler();
|
|
30768
|
+
return handler({ syncDefaults });
|
|
30608
30769
|
}
|
|
30609
30770
|
if (sub === "validate") {
|
|
30610
30771
|
if (wantsHelp()) {
|
|
@@ -30812,11 +30973,13 @@ async function run24() {
|
|
|
30812
30973
|
" specialists feed -f Follow all jobs globally",
|
|
30813
30974
|
"",
|
|
30814
30975
|
"Options:",
|
|
30976
|
+
" --from <n> Show only events with seq >= <n>",
|
|
30815
30977
|
" -f, --follow Follow live updates",
|
|
30816
30978
|
" --forever Keep following in global mode even when all jobs complete",
|
|
30817
30979
|
"",
|
|
30818
30980
|
"Examples:",
|
|
30819
30981
|
" specialists feed 49adda",
|
|
30982
|
+
" specialists feed 49adda --from 15",
|
|
30820
30983
|
" specialists feed 49adda --follow",
|
|
30821
30984
|
" specialists feed -f",
|
|
30822
30985
|
" specialists feed -f --forever",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaggerxtrm/specialists",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "OmniSpecialist — 7-tool MCP orchestration layer powered by the Specialist System. Discover and execute .specialist.yaml files across project/user/system scopes via pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// specialists-session-start — Claude Code SessionStart hook
|
|
3
|
-
// Injects specialists context at the start of every session:
|
|
4
|
-
// • using-specialists skill (behavioral delegation guide)
|
|
5
|
-
// • Active background jobs (if any)
|
|
6
|
-
// • Available specialists list
|
|
7
|
-
// • Key CLI commands reminder
|
|
8
|
-
//
|
|
9
|
-
// Installed by: specialists init
|
|
10
|
-
// Hook type: SessionStart
|
|
11
|
-
|
|
12
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
17
|
-
const jobsDir = join(cwd, '.specialists', 'jobs');
|
|
18
|
-
const lines = [];
|
|
19
|
-
|
|
20
|
-
// ── 0. using-specialists skill ─────────────────────────────────────────────
|
|
21
|
-
// Inject the behavioral delegation guide so Claude knows when and how to
|
|
22
|
-
// use specialists without waiting for the user to ask.
|
|
23
|
-
const skillPath = join(cwd, '.specialists', 'default', 'skills', 'using-specialists', 'SKILL.md');
|
|
24
|
-
if (existsSync(skillPath)) {
|
|
25
|
-
const raw = readFileSync(skillPath, 'utf-8');
|
|
26
|
-
// Strip YAML frontmatter (--- ... ---) if present
|
|
27
|
-
const content = raw.startsWith('---')
|
|
28
|
-
? raw.replace(/^---[\s\S]*?---\n?/, '').trimStart()
|
|
29
|
-
: raw;
|
|
30
|
-
lines.push(content);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── 1. Active background jobs ──────────────────────────────────────────────
|
|
34
|
-
if (existsSync(jobsDir)) {
|
|
35
|
-
let entries = [];
|
|
36
|
-
try { entries = readdirSync(jobsDir); } catch { /* ignore */ }
|
|
37
|
-
|
|
38
|
-
const activeJobs = [];
|
|
39
|
-
for (const jobId of entries) {
|
|
40
|
-
const statusPath = join(jobsDir, jobId, 'status.json');
|
|
41
|
-
if (!existsSync(statusPath)) continue;
|
|
42
|
-
try {
|
|
43
|
-
const s = JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
44
|
-
if (s.status === 'running' || s.status === 'starting') {
|
|
45
|
-
const elapsed = s.elapsed_s !== undefined ? ` (${s.elapsed_s}s)` : '';
|
|
46
|
-
activeJobs.push(
|
|
47
|
-
` • ${s.specialist ?? jobId} [${s.status}]${elapsed} → specialists result ${jobId}`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
} catch { /* malformed status.json */ }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (activeJobs.length > 0) {
|
|
54
|
-
lines.push('## Specialists — Active Background Jobs');
|
|
55
|
-
lines.push('');
|
|
56
|
-
lines.push(...activeJobs);
|
|
57
|
-
lines.push('');
|
|
58
|
-
lines.push('Use `specialists feed <job-id> --follow` to stream events, or `specialists result <job-id>` when done.');
|
|
59
|
-
lines.push('');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── 2. Available specialists (read YAML dirs directly) ────────────────────
|
|
64
|
-
function readSpecialistNames(dir) {
|
|
65
|
-
if (!existsSync(dir)) return [];
|
|
66
|
-
try {
|
|
67
|
-
return readdirSync(dir)
|
|
68
|
-
.filter(f => f.endsWith('.specialist.yaml'))
|
|
69
|
-
.map(f => f.replace('.specialist.yaml', ''));
|
|
70
|
-
} catch {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const defaultNames = readSpecialistNames(join(cwd, '.specialists', 'default', 'specialists'));
|
|
76
|
-
const userNames = readSpecialistNames(join(cwd, '.specialists', 'user', 'specialists'));
|
|
77
|
-
|
|
78
|
-
// User takes precedence on name collision; merge and sort
|
|
79
|
-
const allNames = [...new Set([...userNames, ...defaultNames])].sort();
|
|
80
|
-
|
|
81
|
-
if (allNames.length > 0) {
|
|
82
|
-
lines.push('## Specialists — Available');
|
|
83
|
-
lines.push('');
|
|
84
|
-
if (defaultNames.length > 0) {
|
|
85
|
-
lines.push(`default (${defaultNames.length}): ${defaultNames.join(', ')}`);
|
|
86
|
-
}
|
|
87
|
-
if (userNames.length > 0) {
|
|
88
|
-
const extraUser = userNames.filter(n => !defaultNames.includes(n));
|
|
89
|
-
if (extraUser.length > 0) {
|
|
90
|
-
lines.push(`user (${extraUser.length}): ${extraUser.join(', ')}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
lines.push('');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── 3. Key commands reminder ───────────────────────────────────────────────
|
|
97
|
-
lines.push('## Specialists — Session Quick Reference');
|
|
98
|
-
lines.push('');
|
|
99
|
-
lines.push('```');
|
|
100
|
-
lines.push('specialists list # discover available specialists');
|
|
101
|
-
lines.push('specialists run <name> --prompt "..." # run foreground (streams output)');
|
|
102
|
-
lines.push('process start "specialists run <name> --prompt "..."" name="sp-<name>" # async via process extension');
|
|
103
|
-
lines.push('specialists run <name> --prompt "..." # foreground stream');
|
|
104
|
-
lines.push('specialists feed <job-id> --follow # tail live events');
|
|
105
|
-
lines.push('specialists result <job-id> # read final output');
|
|
106
|
-
lines.push('specialists status # system health');
|
|
107
|
-
lines.push('specialists doctor # troubleshoot issues');
|
|
108
|
-
lines.push('```');
|
|
109
|
-
lines.push('');
|
|
110
|
-
lines.push('MCP tools: specialist_init · use_specialist · start_specialist · feed_specialist · run_parallel');
|
|
111
|
-
|
|
112
|
-
// ── Output ─────────────────────────────────────────────────────────────────
|
|
113
|
-
if (lines.length === 0) process.exit(0);
|
|
114
|
-
|
|
115
|
-
process.stdout.write(JSON.stringify({
|
|
116
|
-
hookSpecificOutput: {
|
|
117
|
-
hookEventName: 'SessionStart',
|
|
118
|
-
additionalSystemPrompt: lines.join('\n'),
|
|
119
|
-
},
|
|
120
|
-
}) + '\n');
|