@jaggerxtrm/specialists 3.4.4 → 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.
|
@@ -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,8 +284,6 @@ 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 |
|
|
@@ -313,17 +307,10 @@ If you encounter legacy `start_specialist`, treat it as deprecated and migrate t
|
|
|
313
307
|
|
|
314
308
|
---
|
|
315
309
|
|
|
316
|
-
##
|
|
317
|
-
|
|
318
|
-
```bash
|
|
319
|
-
specialists init # first-time setup: creates .specialists/, wires AGENTS.md/CLAUDE.md
|
|
320
|
-
specialists doctor # health check: hooks, MCP, zombie jobs
|
|
321
|
-
specialists edit <name> # edit a specialist's YAML config
|
|
322
|
-
```
|
|
310
|
+
## Troubleshooting
|
|
323
311
|
|
|
324
312
|
- **"specialist not found"** → `specialists list` (project-scope only)
|
|
325
313
|
- **Job hangs** → `specialists steer <id> "finish up"` or `specialists stop <id>`
|
|
326
|
-
- **MCP tools missing** → `specialists init` then restart Claude Code
|
|
327
314
|
- **YAML skipped** → stderr shows `[specialists] skipping <file>: <reason>`
|
|
328
315
|
- **Stall timeout** → specialist hit 120s inactivity. Check `specialists feed <id>`, then retry or switch specialist.
|
|
329
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}`);
|
|
@@ -19345,11 +19351,18 @@ function installProjectSkills(cwd) {
|
|
|
19345
19351
|
skip(`${totalSkipped} skill location${totalSkipped === 1 ? "" : "s"} already exist (not overwritten)`);
|
|
19346
19352
|
}
|
|
19347
19353
|
}
|
|
19348
|
-
function
|
|
19354
|
+
function createSpecialistsDirs(cwd) {
|
|
19355
|
+
const defaultDir = join6(cwd, ".specialists", "default");
|
|
19349
19356
|
const userDir = join6(cwd, ".specialists", "user");
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
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/");
|
|
19353
19366
|
}
|
|
19354
19367
|
}
|
|
19355
19368
|
function createRuntimeDirs(cwd) {
|
|
@@ -19419,74 +19432,26 @@ function ensureAgentsMd(cwd) {
|
|
|
19419
19432
|
ok("created AGENTS.md with Specialists section");
|
|
19420
19433
|
}
|
|
19421
19434
|
}
|
|
19422
|
-
function
|
|
19423
|
-
return Boolean(process.env.PI_SESSION_ID || process.env.PI_RPC_SOCKET || process.env.PI_AGENT_SESSION || process.env.PI_CODING_AGENT);
|
|
19424
|
-
}
|
|
19425
|
-
function readLinuxProcFile(path) {
|
|
19426
|
-
try {
|
|
19427
|
-
return readFileSync3(path, "utf-8");
|
|
19428
|
-
} catch {
|
|
19429
|
-
return null;
|
|
19430
|
-
}
|
|
19431
|
-
}
|
|
19432
|
-
function getLinuxParentPid(pid) {
|
|
19433
|
-
const status = readLinuxProcFile(`/proc/${pid}/status`);
|
|
19434
|
-
if (!status)
|
|
19435
|
-
return null;
|
|
19436
|
-
const ppidLine = status.split(`
|
|
19437
|
-
`).find((line) => line.startsWith("PPid:"));
|
|
19438
|
-
if (!ppidLine)
|
|
19439
|
-
return null;
|
|
19440
|
-
const value = Number(ppidLine.replace("PPid:", "").trim());
|
|
19441
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
19442
|
-
}
|
|
19443
|
-
function hasPiAncestorProcess(maxDepth = 8) {
|
|
19444
|
-
let pid = process.ppid;
|
|
19445
|
-
let depth = 0;
|
|
19446
|
-
while (pid && depth < maxDepth) {
|
|
19447
|
-
const cmdline = readLinuxProcFile(`/proc/${pid}/cmdline`);
|
|
19448
|
-
if (!cmdline)
|
|
19449
|
-
break;
|
|
19450
|
-
const command = cmdline.replace(/\0/g, " ").trim();
|
|
19451
|
-
const executable = basename2(command.split(" ")[0] ?? "");
|
|
19452
|
-
const isPiExecutable = executable === "pi" || executable === "pi-coding-agent" || executable.startsWith("pi-");
|
|
19453
|
-
if (isPiExecutable || command.includes("@mariozechner/pi-coding-agent")) {
|
|
19454
|
-
return true;
|
|
19455
|
-
}
|
|
19456
|
-
pid = getLinuxParentPid(pid);
|
|
19457
|
-
depth++;
|
|
19458
|
-
}
|
|
19459
|
-
return false;
|
|
19460
|
-
}
|
|
19461
|
-
function hasExistingDefaultSpecialists(cwd) {
|
|
19462
|
-
const defaultDir = join6(cwd, ".specialists", "default");
|
|
19463
|
-
const legacyNestedDir = join6(defaultDir, "specialists");
|
|
19464
|
-
const hasFlat = existsSync6(defaultDir) && readdirSync2(defaultDir).some((file) => file.endsWith(".specialist.yaml"));
|
|
19465
|
-
if (hasFlat)
|
|
19466
|
-
return true;
|
|
19467
|
-
return existsSync6(legacyNestedDir) && readdirSync2(legacyNestedDir).some((file) => file.endsWith(".specialist.yaml"));
|
|
19468
|
-
}
|
|
19469
|
-
function shouldSkipDefaultSyncInPiSession(cwd) {
|
|
19470
|
-
if (process.env.SPECIALISTS_INIT_FORCE_DEFAULT_SYNC === "1")
|
|
19471
|
-
return false;
|
|
19472
|
-
if (!hasExistingDefaultSpecialists(cwd))
|
|
19473
|
-
return false;
|
|
19474
|
-
return hasPiSessionEnv() || hasPiAncestorProcess();
|
|
19475
|
-
}
|
|
19476
|
-
async function run5() {
|
|
19435
|
+
async function run5(opts = {}) {
|
|
19477
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
|
+
}
|
|
19478
19443
|
console.log(`
|
|
19479
19444
|
${bold4("specialists init")}
|
|
19480
19445
|
`);
|
|
19481
|
-
const
|
|
19482
|
-
if (
|
|
19483
|
-
skip("pi session detected with existing default specialists; skipped .specialists/default sync");
|
|
19484
|
-
} else {
|
|
19446
|
+
const { syncDefaults = false } = opts;
|
|
19447
|
+
if (syncDefaults) {
|
|
19485
19448
|
migrateLegacySpecialists(cwd, "default");
|
|
19486
19449
|
copyCanonicalSpecialists(cwd);
|
|
19450
|
+
} else {
|
|
19451
|
+
skip(".specialists/default/ not synced (pass --sync-defaults to write canonical specialists)");
|
|
19487
19452
|
}
|
|
19488
19453
|
migrateLegacySpecialists(cwd, "user");
|
|
19489
|
-
|
|
19454
|
+
createSpecialistsDirs(cwd);
|
|
19490
19455
|
createRuntimeDirs(cwd);
|
|
19491
19456
|
ensureGitignore(cwd);
|
|
19492
19457
|
ensureAgentsMd(cwd);
|
|
@@ -19505,7 +19470,7 @@ ${bold4("Done!")}
|
|
|
19505
19470
|
console.log("");
|
|
19506
19471
|
console.log(` ${dim4(".specialists/ structure:")}`);
|
|
19507
19472
|
console.log(` .specialists/`);
|
|
19508
|
-
console.log(` ├── default/ ${dim4("# canonical specialists (from init)")}`);
|
|
19473
|
+
console.log(` ├── default/ ${dim4("# canonical specialists (from init --sync-defaults)")}`);
|
|
19509
19474
|
console.log(` ├── user/ ${dim4("# your custom specialists")}`);
|
|
19510
19475
|
console.log(` ├── jobs/ ${dim4("# runtime (gitignored)")}`);
|
|
19511
19476
|
console.log(` └── ready/ ${dim4("# runtime (gitignored)")}`);
|
|
@@ -19521,7 +19486,7 @@ var init_init = __esm(() => {
|
|
|
19521
19486
|
AGENTS_BLOCK = `
|
|
19522
19487
|
## Specialists
|
|
19523
19488
|
|
|
19524
|
-
|
|
19489
|
+
Use CLI commands via Bash to run and monitor specialists:
|
|
19525
19490
|
|
|
19526
19491
|
Core specialist commands (CLI-first in pi):
|
|
19527
19492
|
- \`specialists list\`
|
|
@@ -19825,7 +19790,7 @@ __export(exports_config, {
|
|
|
19825
19790
|
});
|
|
19826
19791
|
import { existsSync as existsSync9 } from "node:fs";
|
|
19827
19792
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
19828
|
-
import { basename as
|
|
19793
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
19829
19794
|
function usage() {
|
|
19830
19795
|
return [
|
|
19831
19796
|
"Usage:",
|
|
@@ -19934,7 +19899,7 @@ async function getAcrossFiles(files, keyPath) {
|
|
|
19934
19899
|
const content = await readFile3(file, "utf-8");
|
|
19935
19900
|
const doc2 = $parseDocument(content);
|
|
19936
19901
|
const value = doc2.getIn(keyPath);
|
|
19937
|
-
const name = getSpecialistNameFromPath(
|
|
19902
|
+
const name = getSpecialistNameFromPath(basename2(file));
|
|
19938
19903
|
console.log(`${yellow7(name)}: ${formatValue(value)}`);
|
|
19939
19904
|
}
|
|
19940
19905
|
}
|
|
@@ -20372,6 +20337,58 @@ class Supervisor {
|
|
|
20372
20337
|
let closeFn;
|
|
20373
20338
|
let fifoReadStream;
|
|
20374
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
|
+
};
|
|
20375
20392
|
const thresholds = {
|
|
20376
20393
|
...STALL_DETECTION_DEFAULTS,
|
|
20377
20394
|
...this.opts.stallDetection
|
|
@@ -20417,7 +20434,13 @@ class Supervisor {
|
|
|
20417
20434
|
}
|
|
20418
20435
|
}
|
|
20419
20436
|
}, 1e4);
|
|
20420
|
-
const sigtermHandler = () =>
|
|
20437
|
+
const sigtermHandler = () => {
|
|
20438
|
+
if (keepAliveSession) {
|
|
20439
|
+
closeKeepAliveSession();
|
|
20440
|
+
return;
|
|
20441
|
+
}
|
|
20442
|
+
killFn?.();
|
|
20443
|
+
};
|
|
20421
20444
|
process.once("SIGTERM", sigtermHandler);
|
|
20422
20445
|
try {
|
|
20423
20446
|
const result = await runner.run(runOptions, (delta) => {
|
|
@@ -20469,48 +20492,21 @@ class Supervisor {
|
|
|
20469
20492
|
if (parsed?.type === "steer" && typeof parsed.message === "string") {
|
|
20470
20493
|
steerFn?.(parsed.message).catch(() => {});
|
|
20471
20494
|
} else if (parsed?.type === "resume" && typeof parsed.task === "string") {
|
|
20472
|
-
|
|
20473
|
-
setStatus({ status: "running", current_event: "starting" });
|
|
20474
|
-
resumeFn(parsed.task).then((output) => {
|
|
20475
|
-
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20476
|
-
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
20477
|
-
setStatus({
|
|
20478
|
-
status: "waiting",
|
|
20479
|
-
current_event: "waiting",
|
|
20480
|
-
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
20481
|
-
last_event_at_ms: Date.now()
|
|
20482
|
-
});
|
|
20483
|
-
}).catch((err) => {
|
|
20484
|
-
setStatus({ status: "error", error: err?.message ?? String(err) });
|
|
20485
|
-
});
|
|
20486
|
-
}
|
|
20495
|
+
handleResumeTurn(parsed.task);
|
|
20487
20496
|
} else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
|
|
20488
20497
|
console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
|
|
20489
|
-
|
|
20490
|
-
setStatus({ status: "running", current_event: "starting" });
|
|
20491
|
-
resumeFn(parsed.message).then((output) => {
|
|
20492
|
-
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20493
|
-
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
20494
|
-
setStatus({
|
|
20495
|
-
status: "waiting",
|
|
20496
|
-
current_event: "waiting",
|
|
20497
|
-
elapsed_s: Math.round((Date.now() - startedAtMs) / 1000),
|
|
20498
|
-
last_event_at_ms: Date.now()
|
|
20499
|
-
});
|
|
20500
|
-
}).catch((err) => {
|
|
20501
|
-
setStatus({ status: "error", error: err?.message ?? String(err) });
|
|
20502
|
-
});
|
|
20503
|
-
}
|
|
20498
|
+
handleResumeTurn(parsed.message);
|
|
20504
20499
|
} else if (parsed?.type === "close") {
|
|
20505
|
-
|
|
20500
|
+
closeKeepAliveSession();
|
|
20506
20501
|
}
|
|
20507
20502
|
} catch {}
|
|
20508
20503
|
});
|
|
20509
20504
|
fifoReadline.on("error", () => {});
|
|
20510
20505
|
}, (rFn, cFn) => {
|
|
20506
|
+
keepAliveSession = true;
|
|
20511
20507
|
resumeFn = rFn;
|
|
20512
20508
|
closeFn = cFn;
|
|
20513
|
-
setStatus({ status: "waiting", current_event: "waiting" });
|
|
20509
|
+
setStatus({ status: "waiting", current_event: "waiting", last_event_at_ms: Date.now() });
|
|
20514
20510
|
}, (tool, args, toolCallId) => {
|
|
20515
20511
|
currentTool = tool;
|
|
20516
20512
|
currentToolArgs = args;
|
|
@@ -20536,40 +20532,60 @@ class Supervisor {
|
|
|
20536
20532
|
toolStartMs = undefined;
|
|
20537
20533
|
toolDurationWarnEmitted = false;
|
|
20538
20534
|
});
|
|
20539
|
-
|
|
20535
|
+
latestOutput = result.output;
|
|
20540
20536
|
mkdirSync2(this.jobDir(id), { recursive: true });
|
|
20541
|
-
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
|
+
};
|
|
20542
20558
|
const inputBeadId = runOptions.inputBeadId;
|
|
20543
|
-
const ownsBead = Boolean(
|
|
20559
|
+
const ownsBead = Boolean(finalResult.beadId && !inputBeadId);
|
|
20544
20560
|
const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
|
|
20545
|
-
const shouldAppendReadOnlyResultToInputBead = Boolean(inputBeadId &&
|
|
20546
|
-
if (ownsBead &&
|
|
20547
|
-
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));
|
|
20548
20564
|
} else if (shouldWriteExternalBeadNotes) {
|
|
20549
20565
|
if (shouldAppendReadOnlyResultToInputBead && inputBeadId) {
|
|
20550
|
-
this.opts.beadsClient?.updateBeadNotes(inputBeadId, formatBeadNotes(
|
|
20551
|
-
} else if (
|
|
20552
|
-
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));
|
|
20553
20569
|
}
|
|
20554
20570
|
}
|
|
20555
|
-
if (
|
|
20571
|
+
if (finalResult.beadId) {
|
|
20556
20572
|
if (!inputBeadId) {
|
|
20557
|
-
this.opts.beadsClient?.closeBead(
|
|
20573
|
+
this.opts.beadsClient?.closeBead(finalResult.beadId, "COMPLETE", finalResult.durationMs, finalResult.model);
|
|
20558
20574
|
}
|
|
20559
20575
|
}
|
|
20560
20576
|
setStatus({
|
|
20561
20577
|
status: "done",
|
|
20562
20578
|
elapsed_s: elapsed,
|
|
20563
20579
|
last_event_at_ms: Date.now(),
|
|
20564
|
-
model:
|
|
20565
|
-
backend:
|
|
20566
|
-
bead_id:
|
|
20580
|
+
model: finalResult.model,
|
|
20581
|
+
backend: finalResult.backend,
|
|
20582
|
+
bead_id: finalResult.beadId
|
|
20567
20583
|
});
|
|
20568
20584
|
appendTimelineEvent(createRunCompleteEvent("COMPLETE", elapsed, {
|
|
20569
|
-
model:
|
|
20570
|
-
backend:
|
|
20571
|
-
bead_id:
|
|
20572
|
-
output:
|
|
20585
|
+
model: finalResult.model,
|
|
20586
|
+
backend: finalResult.backend,
|
|
20587
|
+
bead_id: finalResult.beadId,
|
|
20588
|
+
output: finalResult.output
|
|
20573
20589
|
}));
|
|
20574
20590
|
this.writeReadyMarker(id);
|
|
20575
20591
|
return id;
|
|
@@ -20799,6 +20815,9 @@ function createTmuxSession(name, cwd, cmd, extraEnv = {}) {
|
|
|
20799
20815
|
throw new Error(`Failed to create tmux session "${name}": ${errorOutput}`);
|
|
20800
20816
|
}
|
|
20801
20817
|
}
|
|
20818
|
+
function killTmuxSession(name) {
|
|
20819
|
+
spawnSync7("tmux", ["kill-session", "-t", name], { encoding: "utf8", stdio: "pipe" });
|
|
20820
|
+
}
|
|
20802
20821
|
var TMUX_SESSION_PREFIX = "sp";
|
|
20803
20822
|
var init_tmux_utils = () => {};
|
|
20804
20823
|
|
|
@@ -21633,7 +21652,14 @@ var exports_feed = {};
|
|
|
21633
21652
|
__export(exports_feed, {
|
|
21634
21653
|
run: () => run12
|
|
21635
21654
|
});
|
|
21636
|
-
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";
|
|
21637
21663
|
import { join as join15 } from "node:path";
|
|
21638
21664
|
function getHumanEventKey(event) {
|
|
21639
21665
|
switch (event.type) {
|
|
@@ -21697,31 +21723,55 @@ function parseSince(value) {
|
|
|
21697
21723
|
}
|
|
21698
21724
|
return;
|
|
21699
21725
|
}
|
|
21700
|
-
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) {
|
|
21701
21739
|
const statusPath = join15(jobsDir, jobId, "status.json");
|
|
21740
|
+
const raw = readFileFresh(statusPath);
|
|
21741
|
+
if (!raw)
|
|
21742
|
+
return null;
|
|
21702
21743
|
try {
|
|
21703
|
-
|
|
21704
|
-
return status.status === "done" || status.status === "error";
|
|
21744
|
+
return JSON.parse(raw);
|
|
21705
21745
|
} catch {
|
|
21706
|
-
return
|
|
21746
|
+
return null;
|
|
21707
21747
|
}
|
|
21708
21748
|
}
|
|
21709
|
-
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
|
+
}
|
|
21710
21769
|
const cache = new Map;
|
|
21711
21770
|
return (jobId) => {
|
|
21712
|
-
|
|
21713
|
-
|
|
21714
|
-
|
|
21715
|
-
|
|
21716
|
-
try {
|
|
21717
|
-
const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
|
|
21718
|
-
meta = {
|
|
21719
|
-
model: status.model,
|
|
21720
|
-
backend: status.backend,
|
|
21721
|
-
beadId: status.bead_id,
|
|
21722
|
-
startedAtMs: status.started_at_ms ?? Date.now()
|
|
21723
|
-
};
|
|
21724
|
-
} catch {}
|
|
21771
|
+
const cached2 = cache.get(jobId);
|
|
21772
|
+
if (cached2)
|
|
21773
|
+
return cached2;
|
|
21774
|
+
const meta = readJobMeta(jobsDir, jobId);
|
|
21725
21775
|
cache.set(jobId, meta);
|
|
21726
21776
|
return meta;
|
|
21727
21777
|
};
|
|
@@ -21730,6 +21780,7 @@ function parseArgs8(argv) {
|
|
|
21730
21780
|
let jobId;
|
|
21731
21781
|
let specialist;
|
|
21732
21782
|
let since;
|
|
21783
|
+
let from = 0;
|
|
21733
21784
|
let limit = 100;
|
|
21734
21785
|
let follow = false;
|
|
21735
21786
|
let forever = false;
|
|
@@ -21747,6 +21798,11 @@ function parseArgs8(argv) {
|
|
|
21747
21798
|
since = parseSince(argv[++i]);
|
|
21748
21799
|
continue;
|
|
21749
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
|
+
}
|
|
21750
21806
|
if (argv[i] === "--limit" && argv[i + 1]) {
|
|
21751
21807
|
limit = parseInt(argv[++i], 10);
|
|
21752
21808
|
continue;
|
|
@@ -21766,7 +21822,7 @@ function parseArgs8(argv) {
|
|
|
21766
21822
|
if (!jobId && !argv[i].startsWith("--"))
|
|
21767
21823
|
jobId = argv[i];
|
|
21768
21824
|
}
|
|
21769
|
-
return { jobId, specialist, since, limit, follow, forever, json };
|
|
21825
|
+
return { jobId, specialist, since, from, limit, follow, forever, json };
|
|
21770
21826
|
}
|
|
21771
21827
|
function printSnapshot(merged, options, jobsDir) {
|
|
21772
21828
|
if (merged.length === 0) {
|
|
@@ -21811,36 +21867,106 @@ function printSnapshot(merged, options, jobsDir) {
|
|
|
21811
21867
|
function isCompletionEvent(event) {
|
|
21812
21868
|
return isRunCompleteEvent(event);
|
|
21813
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
|
+
}
|
|
21814
21937
|
async function followMerged(jobsDir, options) {
|
|
21815
21938
|
const colorMap = new JobColorMap;
|
|
21816
|
-
const getJobMeta = makeJobMetaReader(jobsDir);
|
|
21939
|
+
const getJobMeta = makeJobMetaReader(jobsDir, { useCache: false });
|
|
21817
21940
|
const lastSeenT = new Map;
|
|
21941
|
+
const trackedJobs = new Set(listMatchingJobIds(jobsDir, options).filter((jobId) => !isTerminalJobStatus(jobsDir, jobId)));
|
|
21818
21942
|
const completedJobs = new Set;
|
|
21819
|
-
const filteredBatches = () =>
|
|
21820
|
-
const initial = queryTimeline(jobsDir, {
|
|
21943
|
+
const filteredBatches = () => readFilteredBatchesFresh(jobsDir, options);
|
|
21944
|
+
const initial = filterMergedEventsByCursor(queryTimeline(jobsDir, {
|
|
21821
21945
|
jobId: options.jobId,
|
|
21822
21946
|
specialist: options.specialist,
|
|
21823
21947
|
since: options.since,
|
|
21824
21948
|
limit: options.limit
|
|
21825
|
-
});
|
|
21949
|
+
}), options.from);
|
|
21826
21950
|
printSnapshot(initial, { ...options, json: options.json }, jobsDir);
|
|
21827
21951
|
for (const batch of filteredBatches()) {
|
|
21828
21952
|
if (batch.events.length > 0) {
|
|
21829
21953
|
const maxT = Math.max(...batch.events.map((event) => event.t));
|
|
21830
21954
|
lastSeenT.set(batch.jobId, maxT);
|
|
21831
21955
|
}
|
|
21832
|
-
if (batch.events.some(isCompletionEvent)
|
|
21956
|
+
if (trackedJobs.has(batch.jobId) && batch.events.some(isCompletionEvent)) {
|
|
21833
21957
|
completedJobs.add(batch.jobId);
|
|
21834
21958
|
}
|
|
21835
21959
|
}
|
|
21836
|
-
|
|
21837
|
-
if (!options.forever && initialBatchCount > 0 && completedJobs.size === initialBatchCount) {
|
|
21960
|
+
if (!options.forever && trackedJobs.size === 0) {
|
|
21838
21961
|
if (!options.json) {
|
|
21839
21962
|
process.stderr.write(dim7(`All jobs complete.
|
|
21840
21963
|
`));
|
|
21841
21964
|
}
|
|
21842
21965
|
return;
|
|
21843
21966
|
}
|
|
21967
|
+
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
21968
|
+
return;
|
|
21969
|
+
}
|
|
21844
21970
|
if (!options.json) {
|
|
21845
21971
|
process.stderr.write(dim7(`Following... (Ctrl+C to stop)
|
|
21846
21972
|
`));
|
|
@@ -21850,11 +21976,21 @@ async function followMerged(jobsDir, options) {
|
|
|
21850
21976
|
await new Promise((resolve2) => {
|
|
21851
21977
|
const interval = setInterval(() => {
|
|
21852
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
|
+
}
|
|
21853
21989
|
const newEvents = [];
|
|
21854
21990
|
for (const batch of batches) {
|
|
21855
21991
|
const lastT = lastSeenT.get(batch.jobId) ?? 0;
|
|
21856
21992
|
for (const event of batch.events) {
|
|
21857
|
-
if (event.t > lastT) {
|
|
21993
|
+
if (event.t > lastT && isEventAtOrAfterCursor(event, options.from)) {
|
|
21858
21994
|
newEvents.push({
|
|
21859
21995
|
jobId: batch.jobId,
|
|
21860
21996
|
specialist: batch.specialist,
|
|
@@ -21867,7 +22003,7 @@ async function followMerged(jobsDir, options) {
|
|
|
21867
22003
|
const maxT = Math.max(...batch.events.map((e) => e.t));
|
|
21868
22004
|
lastSeenT.set(batch.jobId, maxT);
|
|
21869
22005
|
}
|
|
21870
|
-
if (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId)) {
|
|
22006
|
+
if (trackedJobs.has(batch.jobId) && (batch.events.some(isCompletionEvent) || isTerminalJobStatus(jobsDir, batch.jobId))) {
|
|
21871
22007
|
completedJobs.add(batch.jobId);
|
|
21872
22008
|
}
|
|
21873
22009
|
}
|
|
@@ -21897,7 +22033,7 @@ async function followMerged(jobsDir, options) {
|
|
|
21897
22033
|
console.log(formatEventLine(event, { jobId, specialist: specialistDisplay, beadId, colorize }));
|
|
21898
22034
|
}
|
|
21899
22035
|
}
|
|
21900
|
-
if (!options.forever &&
|
|
22036
|
+
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
21901
22037
|
clearInterval(interval);
|
|
21902
22038
|
resolve2();
|
|
21903
22039
|
}
|
|
@@ -21911,16 +22047,19 @@ async function run12() {
|
|
|
21911
22047
|
console.log(dim7("No jobs directory found."));
|
|
21912
22048
|
return;
|
|
21913
22049
|
}
|
|
22050
|
+
if (options.from > 0 && !options.json) {
|
|
22051
|
+
console.log(dim7(`Showing events from seq ${options.from}`));
|
|
22052
|
+
}
|
|
21914
22053
|
if (options.follow) {
|
|
21915
22054
|
await followMerged(jobsDir, options);
|
|
21916
22055
|
return;
|
|
21917
22056
|
}
|
|
21918
|
-
const merged = queryTimeline(jobsDir, {
|
|
22057
|
+
const merged = filterMergedEventsByCursor(queryTimeline(jobsDir, {
|
|
21919
22058
|
jobId: options.jobId,
|
|
21920
22059
|
specialist: options.specialist,
|
|
21921
22060
|
since: options.since,
|
|
21922
22061
|
limit: options.limit
|
|
21923
|
-
});
|
|
22062
|
+
}), options.from);
|
|
21924
22063
|
printSnapshot(merged, options, jobsDir);
|
|
21925
22064
|
}
|
|
21926
22065
|
var init_feed = __esm(() => {
|
|
@@ -22164,10 +22303,10 @@ __export(exports_clean, {
|
|
|
22164
22303
|
});
|
|
22165
22304
|
import {
|
|
22166
22305
|
existsSync as existsSync16,
|
|
22167
|
-
readdirSync as
|
|
22306
|
+
readdirSync as readdirSync7,
|
|
22168
22307
|
readFileSync as readFileSync12,
|
|
22169
22308
|
rmSync as rmSync2,
|
|
22170
|
-
statSync as
|
|
22309
|
+
statSync as statSync3
|
|
22171
22310
|
} from "node:fs";
|
|
22172
22311
|
import { join as join19 } from "node:path";
|
|
22173
22312
|
function parseTtlDaysFromEnvironment() {
|
|
@@ -22223,10 +22362,10 @@ function parseOptions(argv) {
|
|
|
22223
22362
|
}
|
|
22224
22363
|
function readDirectorySizeBytes(directoryPath) {
|
|
22225
22364
|
let totalBytes = 0;
|
|
22226
|
-
const entries =
|
|
22365
|
+
const entries = readdirSync7(directoryPath, { withFileTypes: true });
|
|
22227
22366
|
for (const entry of entries) {
|
|
22228
22367
|
const entryPath = join19(directoryPath, entry.name);
|
|
22229
|
-
const stats =
|
|
22368
|
+
const stats = statSync3(entryPath);
|
|
22230
22369
|
if (stats.isDirectory()) {
|
|
22231
22370
|
totalBytes += readDirectorySizeBytes(entryPath);
|
|
22232
22371
|
continue;
|
|
@@ -22250,7 +22389,7 @@ function readCompletedJobDirectory(baseDirectory, entry) {
|
|
|
22250
22389
|
}
|
|
22251
22390
|
if (!COMPLETED_STATUSES.has(statusData.status))
|
|
22252
22391
|
return null;
|
|
22253
|
-
const directoryStats =
|
|
22392
|
+
const directoryStats = statSync3(directoryPath);
|
|
22254
22393
|
return {
|
|
22255
22394
|
id: entry.name,
|
|
22256
22395
|
directoryPath,
|
|
@@ -22260,7 +22399,7 @@ function readCompletedJobDirectory(baseDirectory, entry) {
|
|
|
22260
22399
|
};
|
|
22261
22400
|
}
|
|
22262
22401
|
function collectCompletedJobDirectories(jobsDirectoryPath) {
|
|
22263
|
-
const entries =
|
|
22402
|
+
const entries = readdirSync7(jobsDirectoryPath, { withFileTypes: true });
|
|
22264
22403
|
const completedJobs = [];
|
|
22265
22404
|
for (const entry of entries) {
|
|
22266
22405
|
const completedJob = readCompletedJobDirectory(jobsDirectoryPath, entry);
|
|
@@ -22377,14 +22516,25 @@ async function run18() {
|
|
|
22377
22516
|
`);
|
|
22378
22517
|
process.exit(1);
|
|
22379
22518
|
}
|
|
22519
|
+
const tmuxSession = status.tmux_session;
|
|
22380
22520
|
try {
|
|
22381
22521
|
process.kill(status.pid, "SIGTERM");
|
|
22382
22522
|
process.stdout.write(`${green11("✓")} Sent SIGTERM to PID ${status.pid} (job ${jobId})
|
|
22383
22523
|
`);
|
|
22524
|
+
if (tmuxSession) {
|
|
22525
|
+
killTmuxSession(tmuxSession);
|
|
22526
|
+
process.stdout.write(`${dim10(` tmux session ${tmuxSession} killed`)}
|
|
22527
|
+
`);
|
|
22528
|
+
}
|
|
22384
22529
|
} catch (err) {
|
|
22385
22530
|
if (err.code === "ESRCH") {
|
|
22386
22531
|
process.stderr.write(`${red6(`Process ${status.pid} not found.`)} Job may have already completed.
|
|
22387
22532
|
`);
|
|
22533
|
+
if (tmuxSession) {
|
|
22534
|
+
killTmuxSession(tmuxSession);
|
|
22535
|
+
process.stdout.write(`${dim10(` tmux session ${tmuxSession} killed`)}
|
|
22536
|
+
`);
|
|
22537
|
+
}
|
|
22388
22538
|
} else {
|
|
22389
22539
|
process.stderr.write(`${red6("Error:")} ${err.message}
|
|
22390
22540
|
`);
|
|
@@ -22395,6 +22545,7 @@ async function run18() {
|
|
|
22395
22545
|
var green11 = (s) => `\x1B[32m${s}\x1B[0m`, red6 = (s) => `\x1B[31m${s}\x1B[0m`, dim10 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
22396
22546
|
var init_stop = __esm(() => {
|
|
22397
22547
|
init_supervisor();
|
|
22548
|
+
init_tmux_utils();
|
|
22398
22549
|
});
|
|
22399
22550
|
|
|
22400
22551
|
// src/cli/attach.ts
|
|
@@ -22683,7 +22834,7 @@ __export(exports_doctor, {
|
|
|
22683
22834
|
run: () => run21
|
|
22684
22835
|
});
|
|
22685
22836
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
22686
|
-
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";
|
|
22687
22838
|
import { join as join22 } from "node:path";
|
|
22688
22839
|
function ok3(msg) {
|
|
22689
22840
|
console.log(` ${green13("✓")} ${msg}`);
|
|
@@ -22855,7 +23006,7 @@ function checkZombieJobs() {
|
|
|
22855
23006
|
}
|
|
22856
23007
|
let entries;
|
|
22857
23008
|
try {
|
|
22858
|
-
entries =
|
|
23009
|
+
entries = readdirSync8(jobsDir);
|
|
22859
23010
|
} catch {
|
|
22860
23011
|
entries = [];
|
|
22861
23012
|
}
|
|
@@ -30582,34 +30733,39 @@ async function run24() {
|
|
|
30582
30733
|
if (wantsHelp()) {
|
|
30583
30734
|
console.log([
|
|
30584
30735
|
"",
|
|
30585
|
-
"Usage: specialists init [--
|
|
30736
|
+
"Usage: specialists init [--sync-defaults]",
|
|
30586
30737
|
"",
|
|
30587
30738
|
"Bootstrap a project for specialists. This is the sole onboarding command.",
|
|
30588
30739
|
"",
|
|
30589
|
-
"What it does:",
|
|
30590
|
-
" • creates specialists/ for
|
|
30591
|
-
" • creates .specialists/ runtime dirs
|
|
30592
|
-
" • adds
|
|
30593
|
-
" • injects the
|
|
30594
|
-
" • 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/",
|
|
30595
30748
|
"",
|
|
30596
30749
|
"Options:",
|
|
30597
|
-
" --
|
|
30750
|
+
" --sync-defaults Also copy canonical specialists to .specialists/default/.",
|
|
30751
|
+
" Human-only: rewrites default specialist YAML files.",
|
|
30598
30752
|
"",
|
|
30599
30753
|
"Examples:",
|
|
30600
|
-
" specialists init",
|
|
30601
|
-
" specialists init --
|
|
30754
|
+
" specialists init # safe for agents to call",
|
|
30755
|
+
" specialists init --sync-defaults # human-only: sync canonical specialists",
|
|
30602
30756
|
"",
|
|
30603
30757
|
"Notes:",
|
|
30604
30758
|
" setup and install are deprecated; use specialists init.",
|
|
30605
|
-
"
|
|
30759
|
+
" MCP missing → specialists init (safe for anyone to call).",
|
|
30760
|
+
" Specialists missing → specialists init --sync-defaults (human-only).",
|
|
30606
30761
|
""
|
|
30607
30762
|
].join(`
|
|
30608
30763
|
`));
|
|
30609
30764
|
return;
|
|
30610
30765
|
}
|
|
30766
|
+
const syncDefaults = process.argv.includes("--sync-defaults");
|
|
30611
30767
|
const { run: handler } = await Promise.resolve().then(() => (init_init(), exports_init));
|
|
30612
|
-
return handler();
|
|
30768
|
+
return handler({ syncDefaults });
|
|
30613
30769
|
}
|
|
30614
30770
|
if (sub === "validate") {
|
|
30615
30771
|
if (wantsHelp()) {
|
|
@@ -30817,11 +30973,13 @@ async function run24() {
|
|
|
30817
30973
|
" specialists feed -f Follow all jobs globally",
|
|
30818
30974
|
"",
|
|
30819
30975
|
"Options:",
|
|
30976
|
+
" --from <n> Show only events with seq >= <n>",
|
|
30820
30977
|
" -f, --follow Follow live updates",
|
|
30821
30978
|
" --forever Keep following in global mode even when all jobs complete",
|
|
30822
30979
|
"",
|
|
30823
30980
|
"Examples:",
|
|
30824
30981
|
" specialists feed 49adda",
|
|
30982
|
+
" specialists feed 49adda --from 15",
|
|
30825
30983
|
" specialists feed 49adda --follow",
|
|
30826
30984
|
" specialists feed -f",
|
|
30827
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: use_specialist (foreground only)');
|
|
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');
|