@jaggerxtrm/specialists 3.6.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/skills/using-specialists/SKILL.md +58 -29
- package/dist/index.js +118 -80
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ description: >
|
|
|
9
9
|
workflow, --context-depth, background jobs, MCP tool (`use_specialist`),
|
|
10
10
|
or specialists doctor. Don't wait for the user to say
|
|
11
11
|
"use a specialist" — proactively evaluate whether delegation makes sense.
|
|
12
|
-
version: 4.
|
|
12
|
+
version: 4.6
|
|
13
13
|
synced_at: zz22-docs
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -40,6 +40,7 @@ Specialists are autonomous AI agents that run independently — fresh context, d
|
|
|
40
40
|
7. **Merge through epics, not manual git.** Use `sp epic merge <epic-id>` for wave-bound chains or `sp merge <chain-root-bead>` for standalone chains. Never use manual `git merge` for specialist work.
|
|
41
41
|
8. **No destructive operations by specialists.** No `rm -rf`, no force pushes, no database drops, no credential rotation, no mass deletes, no history rewrites. Surface destructive requirements to the user.
|
|
42
42
|
9. **Executor does not run tests.** Executor runs lint + tsc only. Tests are the reviewer's and test-runner's responsibility in the chained pipeline.
|
|
43
|
+
10. **Keep specialists alive through the review cycle.** Never `sp stop` an executor or debugger before the reviewer delivers its verdict. The specialist stays in `waiting` so you can `resume` it — to commit changes, apply fixes from reviewer feedback, or continue work. Only stop after final reviewer PASS and confirmed commit.
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -426,21 +427,27 @@ The review → fix loop is the mechanism for iterative quality improvement withi
|
|
|
426
427
|
### Standard loop
|
|
427
428
|
|
|
428
429
|
```
|
|
429
|
-
1. Executor
|
|
430
|
-
-> Job: exec-job
|
|
430
|
+
1. Executor provisions --worktree, implements, enters waiting.
|
|
431
|
+
-> Job: exec-job (KEEP ALIVE — do not stop)
|
|
431
432
|
|
|
432
433
|
2. Reviewer enters same worktree via --job exec-job.
|
|
433
|
-
->
|
|
434
|
+
-> sp ps shows the chain:
|
|
435
|
+
feature/unitAI-impl-executor · unitAI-impl
|
|
436
|
+
◐ exec-job executor waiting
|
|
437
|
+
└ ◐ rev-job reviewer starting
|
|
434
438
|
-> Auto-appends verdict (PASS/PARTIAL/FAIL) to bead notes.
|
|
435
439
|
|
|
436
|
-
3a. PASS:
|
|
440
|
+
3a. PASS:
|
|
441
|
+
-> Resume executor: "Reviewer PASS. Commit your changes."
|
|
442
|
+
-> Verify commit landed on branch (git log)
|
|
443
|
+
-> Stop reviewer, then stop executor
|
|
444
|
+
-> Merge via sp merge
|
|
437
445
|
|
|
438
446
|
3b. PARTIAL/FAIL:
|
|
439
|
-
->
|
|
440
|
-
->
|
|
441
|
-
->
|
|
442
|
-
->
|
|
443
|
-
-> Return to step 2 (reviewer on same job).
|
|
447
|
+
-> Resume the SAME executor: "Reviewer PARTIAL. Fix: <specific findings>"
|
|
448
|
+
-> Executor retains full conversation context — no re-dispatch needed
|
|
449
|
+
-> Executor applies fixes, enters waiting again
|
|
450
|
+
-> Return to step 2 (new reviewer on same --job)
|
|
444
451
|
|
|
445
452
|
4. Repeat until PASS.
|
|
446
453
|
```
|
|
@@ -448,33 +455,51 @@ The review → fix loop is the mechanism for iterative quality improvement withi
|
|
|
448
455
|
### Commands
|
|
449
456
|
|
|
450
457
|
```bash
|
|
451
|
-
# Step 1 — Executor with worktree
|
|
458
|
+
# Step 1 — Executor with worktree (enters waiting after first turn)
|
|
452
459
|
specialists run executor --worktree --bead unitAI-impl --context-depth 2 --background
|
|
453
460
|
# -> Job started: exec-job (e.g. 49adda)
|
|
461
|
+
# DO NOT sp stop — executor stays alive for the entire review cycle
|
|
454
462
|
|
|
455
|
-
# Step 2 — Reviewer enters same worktree
|
|
463
|
+
# Step 2 — Reviewer enters same worktree
|
|
456
464
|
specialists run reviewer --job 49adda --keep-alive --background --prompt "Review impl changes"
|
|
457
465
|
# -> Job started: rev-job
|
|
458
466
|
specialists result rev-job
|
|
459
|
-
# PARTIAL → go to step 3b
|
|
460
467
|
|
|
461
|
-
# Step
|
|
462
|
-
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
specialists
|
|
466
|
-
|
|
467
|
-
|
|
468
|
+
# Step 3a — PASS: resume executor to commit, then stop both
|
|
469
|
+
specialists resume 49adda "Reviewer PASS. Git add and commit your changes."
|
|
470
|
+
# Wait for commit, verify with: git log feature/unitAI-impl-executor --oneline -1
|
|
471
|
+
specialists stop rev-job
|
|
472
|
+
specialists stop 49adda
|
|
473
|
+
sp merge unitAI-impl --rebuild
|
|
474
|
+
|
|
475
|
+
# Step 3b — PARTIAL: resume executor with fix instructions (same session, full context)
|
|
476
|
+
specialists resume 49adda "Reviewer PARTIAL. Fix: <paste specific findings here>"
|
|
477
|
+
# Executor applies fixes, enters waiting again
|
|
478
|
+
# Dispatch new reviewer:
|
|
468
479
|
specialists run reviewer --job 49adda --keep-alive --background --prompt "Re-review after fix"
|
|
469
|
-
#
|
|
480
|
+
# Repeat until PASS
|
|
481
|
+
|
|
482
|
+
# After final PASS + commit + stop:
|
|
470
483
|
bd close unitAI-task --reason "Reviewer PASS. All findings addressed."
|
|
471
484
|
```
|
|
472
485
|
|
|
486
|
+
### Why resume instead of re-dispatch
|
|
487
|
+
|
|
488
|
+
Resuming the original executor/debugger is **always preferred** over dispatching a new fix executor:
|
|
489
|
+
|
|
490
|
+
- **Full context**: the specialist remembers what it changed and why — no re-discovery
|
|
491
|
+
- **No new bead needed**: no fix bead creation, no dep wiring overhead
|
|
492
|
+
- **Same worktree**: no `--job` coordination needed, it's already there
|
|
493
|
+
- **Cheaper**: one resumed turn vs a full new specialist session with context injection
|
|
494
|
+
|
|
495
|
+
Only dispatch a new fix executor when the original specialist is dead (crashed, stopped prematurely, or context exhausted at >80%).
|
|
496
|
+
|
|
473
497
|
### Key invariants
|
|
474
|
-
-
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
- Multiple reviewer →
|
|
498
|
+
- **Never stop the executor/debugger before reviewer verdict.** The specialist stays in `waiting` throughout the review cycle. Stopping prematurely kills the resume path and risks uncommitted changes.
|
|
499
|
+
- **Executors do not auto-commit.** After reviewer PASS, you must resume the executor with explicit commit instructions. Verify the commit landed before stopping.
|
|
500
|
+
- Each fix iteration uses `resume` on the same specialist — not a new child bead or new executor.
|
|
501
|
+
- Multiple reviewer → resume → re-review cycles are expected. The worktree and specialist session are stable across all cycles.
|
|
502
|
+
- Only stop after: (1) reviewer PASS, (2) executor committed, (3) commit verified on branch.
|
|
478
503
|
|
|
479
504
|
---
|
|
480
505
|
|
|
@@ -648,7 +673,7 @@ Run `specialists list` to see what's available. Match by task type:
|
|
|
648
673
|
### Specialist selection notes
|
|
649
674
|
|
|
650
675
|
- **executor does not run tests** — it runs `lint + tsc` only. Tests belong to the reviewer or test-runner phase.
|
|
651
|
-
- **executor enters `waiting` after first turn** — `interactive: true` is now default.
|
|
676
|
+
- **executor enters `waiting` after first turn** — `interactive: true` is now default. **Never stop the executor before reviewer verdict.** Keep it alive so you can: (1) resume with fix instructions if reviewer says PARTIAL, (2) resume with "commit your changes" after reviewer PASS. Executors do not auto-commit — you must explicitly resume them to commit. Only `sp stop` after the commit is verified on the branch.
|
|
652
677
|
- **explorer** is READ_ONLY — its output auto-appends to the input bead's notes. No implementation.
|
|
653
678
|
- **reviewer** is best dispatched via `--job <exec-job> --prompt "..."` — it enters the same worktree to see exactly what was written. `--job` alone is not enough; `--prompt` or `--bead` is always required.
|
|
654
679
|
- **debugger** over **explorer** when you need root cause analysis — GitNexus call-chain tracing, ranked hypotheses, evidence-backed remediation.
|
|
@@ -775,23 +800,27 @@ specialists steer a1b2c3 "Do NOT audit. Write the actual file to disk now."
|
|
|
775
800
|
|
|
776
801
|
| Specialist | Enters `waiting` after | What to send via `resume` |
|
|
777
802
|
|-----------|----------------------|--------------------------|
|
|
778
|
-
| **executor** | First turn completion (may be partial if bailed early) | "proceed, this is additive", "
|
|
803
|
+
| **executor** | First turn completion (may be partial if bailed early) | "proceed, this is additive", "Reviewer PARTIAL. Fix: <findings>", or "Reviewer PASS. Git add and commit your changes." |
|
|
779
804
|
| **researcher** | Delivering research findings | Follow-up question, new angle, or "done, thanks" |
|
|
780
805
|
| **reviewer** | Delivering verdict (PASS/PARTIAL/FAIL) | Your response, clarification, or "accepted, close out" |
|
|
781
806
|
| **overthinker** | Phase 4 conclusion | Follow-up question, counter-argument, or "done, thanks" |
|
|
782
|
-
| **debugger** | Phase 3 fix attempt or Phase 4 verify result | Follow-up fix, "try different approach", or "done" |
|
|
807
|
+
| **debugger** | Phase 3 fix attempt or Phase 4 verify result | Follow-up fix, "try different approach", "Reviewer PASS. Git add and commit your changes.", or "done" |
|
|
783
808
|
| **sync-docs** | Audit report or targeted update result | "approve", "deny", or specific instructions |
|
|
784
809
|
|
|
785
810
|
> **Warning:** A job in `waiting` looks identical to a stalled job. **Always check with `sp ps`
|
|
786
811
|
> before killing a keep-alive job.**
|
|
787
812
|
|
|
813
|
+
> **Critical:** Never stop an executor or debugger before the reviewer delivers its verdict.
|
|
814
|
+
> Stopping prematurely: (1) kills the resume path for fix loops, (2) risks uncommitted changes
|
|
815
|
+
> (executors don't auto-commit), and (3) forces dispatching a new specialist instead of resuming.
|
|
816
|
+
|
|
788
817
|
```bash
|
|
789
818
|
# Check before stopping
|
|
790
819
|
specialists ps d4e5f6
|
|
791
820
|
# -> status: waiting ← healthy, expecting input
|
|
792
821
|
|
|
793
822
|
specialists resume d4e5f6 "What about backward compatibility?"
|
|
794
|
-
specialists stop d4e5f6 # only when truly done iterating
|
|
823
|
+
specialists stop d4e5f6 # only when truly done iterating — after reviewer PASS + commit verified
|
|
795
824
|
```
|
|
796
825
|
|
|
797
826
|
---
|
package/dist/index.js
CHANGED
|
@@ -17792,7 +17792,7 @@ import { createHash } from "crypto";
|
|
|
17792
17792
|
import { spawn } from "child_process";
|
|
17793
17793
|
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
|
|
17794
17794
|
import { homedir, tmpdir } from "os";
|
|
17795
|
-
import { isAbsolute, resolve, sep, join as join2 } from "path";
|
|
17795
|
+
import { isAbsolute, resolve, sep, join as join2, dirname } from "path";
|
|
17796
17796
|
function mapPermissionToTools(level) {
|
|
17797
17797
|
switch (level?.toUpperCase()) {
|
|
17798
17798
|
case "READ_ONLY":
|
|
@@ -17807,6 +17807,16 @@ function mapPermissionToTools(level) {
|
|
|
17807
17807
|
return;
|
|
17808
17808
|
}
|
|
17809
17809
|
}
|
|
17810
|
+
function resolveGlobalNodeModulesDir() {
|
|
17811
|
+
const candidates = [
|
|
17812
|
+
process.env.PI_NPM_GLOBAL_DIR,
|
|
17813
|
+
process.env.NPM_CONFIG_PREFIX ? join2(process.env.NPM_CONFIG_PREFIX, "lib", "node_modules") : undefined,
|
|
17814
|
+
process.env.npm_config_prefix ? join2(process.env.npm_config_prefix, "lib", "node_modules") : undefined,
|
|
17815
|
+
process.env.NVM_BIN ? join2(dirname(process.env.NVM_BIN), "lib", "node_modules") : undefined,
|
|
17816
|
+
join2(homedir(), ".nvm/versions/node", process.version, "lib", "node_modules")
|
|
17817
|
+
].filter((candidate) => Boolean(candidate));
|
|
17818
|
+
return candidates.find((candidate) => existsSync2(candidate));
|
|
17819
|
+
}
|
|
17810
17820
|
function asNumber(value) {
|
|
17811
17821
|
if (typeof value === "number" && Number.isFinite(value))
|
|
17812
17822
|
return value;
|
|
@@ -18089,13 +18099,15 @@ class PiAgentSession {
|
|
|
18089
18099
|
const ssPath = join2(piExtDir, "service-skills");
|
|
18090
18100
|
if (existsSync2(ssPath))
|
|
18091
18101
|
args.push("-e", ssPath);
|
|
18092
|
-
const npmGlobalDir =
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
|
|
18102
|
+
const npmGlobalDir = resolveGlobalNodeModulesDir();
|
|
18103
|
+
if (npmGlobalDir) {
|
|
18104
|
+
const gitnexusPath = join2(npmGlobalDir, "pi-gitnexus");
|
|
18105
|
+
if (existsSync2(gitnexusPath))
|
|
18106
|
+
args.push("-e", gitnexusPath);
|
|
18107
|
+
const serenaPath = join2(npmGlobalDir, "pi-serena-tools");
|
|
18108
|
+
if (existsSync2(serenaPath))
|
|
18109
|
+
args.push("-e", serenaPath);
|
|
18110
|
+
}
|
|
18099
18111
|
if (this.options.systemPrompt) {
|
|
18100
18112
|
args.push("--append-system-prompt", this.options.systemPrompt);
|
|
18101
18113
|
}
|
|
@@ -19607,7 +19619,7 @@ var init_runner = __esm(() => {
|
|
|
19607
19619
|
|
|
19608
19620
|
// src/specialist/hooks.ts
|
|
19609
19621
|
import { appendFile, mkdir } from "fs/promises";
|
|
19610
|
-
import { dirname } from "path";
|
|
19622
|
+
import { dirname as dirname2 } from "path";
|
|
19611
19623
|
|
|
19612
19624
|
class HookEmitter {
|
|
19613
19625
|
tracePath;
|
|
@@ -19615,7 +19627,7 @@ class HookEmitter {
|
|
|
19615
19627
|
ready;
|
|
19616
19628
|
constructor(options) {
|
|
19617
19629
|
this.tracePath = options.tracePath;
|
|
19618
|
-
this.ready = mkdir(
|
|
19630
|
+
this.ready = mkdir(dirname2(options.tracePath), { recursive: true }).then(() => {});
|
|
19619
19631
|
}
|
|
19620
19632
|
async emit(hook, invocationId, specialistName, specialistVersion, payload) {
|
|
19621
19633
|
await this.ready;
|
|
@@ -19669,11 +19681,11 @@ __export(exports_version, {
|
|
|
19669
19681
|
});
|
|
19670
19682
|
import { createRequire } from "module";
|
|
19671
19683
|
import { fileURLToPath } from "url";
|
|
19672
|
-
import { dirname as
|
|
19684
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
19673
19685
|
import { existsSync as existsSync4 } from "fs";
|
|
19674
19686
|
async function run2() {
|
|
19675
19687
|
const req = createRequire(import.meta.url);
|
|
19676
|
-
const here =
|
|
19688
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
19677
19689
|
const bundlePkgPath = join4(here, "..", "package.json");
|
|
19678
19690
|
const sourcePkgPath = join4(here, "..", "..", "package.json");
|
|
19679
19691
|
let pkg;
|
|
@@ -19691,7 +19703,7 @@ var init_version = () => {};
|
|
|
19691
19703
|
|
|
19692
19704
|
// src/specialist/job-root.ts
|
|
19693
19705
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
19694
|
-
import { dirname as
|
|
19706
|
+
import { dirname as dirname4, join as join5, resolve as resolve3 } from "path";
|
|
19695
19707
|
function resolveCommonGitRoot(cwd) {
|
|
19696
19708
|
const result = spawnSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
19697
19709
|
cwd,
|
|
@@ -19703,7 +19715,7 @@ function resolveCommonGitRoot(cwd) {
|
|
|
19703
19715
|
const gitCommonDir = result.stdout?.trim();
|
|
19704
19716
|
if (!gitCommonDir)
|
|
19705
19717
|
return;
|
|
19706
|
-
return
|
|
19718
|
+
return dirname4(resolve3(cwd, gitCommonDir));
|
|
19707
19719
|
}
|
|
19708
19720
|
function resolveJobsDir(cwd = process.cwd()) {
|
|
19709
19721
|
const commonRoot = resolveCommonGitRoot(cwd) ?? cwd;
|
|
@@ -23548,7 +23560,7 @@ __export(exports_init, {
|
|
|
23548
23560
|
});
|
|
23549
23561
|
import { copyFileSync, cpSync, existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, readlinkSync, renameSync as renameSync2, symlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
23550
23562
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
23551
|
-
import { basename as basename3, dirname as
|
|
23563
|
+
import { basename as basename3, dirname as dirname5, join as join10, relative, resolve as resolve4 } from "path";
|
|
23552
23564
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
23553
23565
|
function ok(msg) {
|
|
23554
23566
|
console.log(` ${green4("\u2713")} ${msg}`);
|
|
@@ -23724,7 +23736,7 @@ function installProjectHooks(cwd) {
|
|
|
23724
23736
|
skippedLinks++;
|
|
23725
23737
|
continue;
|
|
23726
23738
|
}
|
|
23727
|
-
const currentTarget = resolve4(
|
|
23739
|
+
const currentTarget = resolve4(dirname5(claudeHookPath), readlinkSync(claudeHookPath));
|
|
23728
23740
|
if (currentTarget !== xtrmDest) {
|
|
23729
23741
|
skippedLinks++;
|
|
23730
23742
|
continue;
|
|
@@ -23752,10 +23764,20 @@ function ensureProjectHookWiring(cwd) {
|
|
|
23752
23764
|
mkdirSync4(settingsDir, { recursive: true });
|
|
23753
23765
|
}
|
|
23754
23766
|
const settings = loadJson(settingsPath, {});
|
|
23767
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
23768
|
+
settings.hooks = {};
|
|
23769
|
+
}
|
|
23770
|
+
const hooksObj = settings.hooks;
|
|
23755
23771
|
let changed = false;
|
|
23772
|
+
for (const event of ["UserPromptSubmit", "PostToolUse", "SessionStart"]) {
|
|
23773
|
+
if (Array.isArray(settings[event])) {
|
|
23774
|
+
delete settings[event];
|
|
23775
|
+
changed = true;
|
|
23776
|
+
}
|
|
23777
|
+
}
|
|
23756
23778
|
function addHook(event, command) {
|
|
23757
|
-
const eventList =
|
|
23758
|
-
|
|
23779
|
+
const eventList = hooksObj[event] ?? [];
|
|
23780
|
+
hooksObj[event] = eventList;
|
|
23759
23781
|
const alreadyWired = eventList.some((entry) => entry?.hooks?.some?.((h) => h?.command === command));
|
|
23760
23782
|
if (!alreadyWired) {
|
|
23761
23783
|
eventList.push({ matcher: "", hooks: [{ type: "command", command }] });
|
|
@@ -23774,10 +23796,10 @@ function ensureProjectHookWiring(cwd) {
|
|
|
23774
23796
|
}
|
|
23775
23797
|
function ensureRootSymlink(rootPath, expectedTargetPath) {
|
|
23776
23798
|
if (!existsSync9(rootPath)) {
|
|
23777
|
-
mkdirSync4(
|
|
23778
|
-
const relTarget = relative(
|
|
23799
|
+
mkdirSync4(dirname5(rootPath), { recursive: true });
|
|
23800
|
+
const relTarget = relative(dirname5(rootPath), expectedTargetPath);
|
|
23779
23801
|
symlinkSync(relTarget, rootPath);
|
|
23780
|
-
ok(`created ${basename3(
|
|
23802
|
+
ok(`created ${basename3(dirname5(rootPath))}/${basename3(rootPath)} \u2192 ${relTarget}`);
|
|
23781
23803
|
return;
|
|
23782
23804
|
}
|
|
23783
23805
|
const stats = lstatSync(rootPath);
|
|
@@ -23785,7 +23807,7 @@ function ensureRootSymlink(rootPath, expectedTargetPath) {
|
|
|
23785
23807
|
throw new Error(`${rootPath} must be a symlink to ${expectedTargetPath}. Aborting.`);
|
|
23786
23808
|
}
|
|
23787
23809
|
const linkTarget = readlinkSync(rootPath);
|
|
23788
|
-
const resolvedTarget = resolve4(
|
|
23810
|
+
const resolvedTarget = resolve4(dirname5(rootPath), linkTarget);
|
|
23789
23811
|
const resolvedExpected = resolve4(expectedTargetPath);
|
|
23790
23812
|
if (resolvedTarget !== resolvedExpected) {
|
|
23791
23813
|
throw new Error(`${rootPath} points to ${linkTarget}, expected ${expectedTargetPath}. Aborting.`);
|
|
@@ -23807,7 +23829,7 @@ function ensureActiveSkillSymlink(defaultSkillPath, activeLinkPath) {
|
|
|
23807
23829
|
if (!stats.isSymbolicLink()) {
|
|
23808
23830
|
throw new Error(`${activeLinkPath} already exists and is not a symlink.`);
|
|
23809
23831
|
}
|
|
23810
|
-
const currentTarget = resolve4(
|
|
23832
|
+
const currentTarget = resolve4(dirname5(activeLinkPath), readlinkSync(activeLinkPath));
|
|
23811
23833
|
if (currentTarget !== resolve4(defaultSkillPath)) {
|
|
23812
23834
|
throw new Error(`${activeLinkPath} points to an unexpected target.`);
|
|
23813
23835
|
}
|
|
@@ -25408,7 +25430,6 @@ async function parseArgs6(argv) {
|
|
|
25408
25430
|
let outputMode = "human";
|
|
25409
25431
|
let contextDepth = 1;
|
|
25410
25432
|
let worktree = false;
|
|
25411
|
-
let noWorktree = false;
|
|
25412
25433
|
let reuseJobId;
|
|
25413
25434
|
let forceJob = false;
|
|
25414
25435
|
let epicId;
|
|
@@ -25465,8 +25486,8 @@ async function parseArgs6(argv) {
|
|
|
25465
25486
|
continue;
|
|
25466
25487
|
}
|
|
25467
25488
|
if (token === "--no-worktree") {
|
|
25468
|
-
|
|
25469
|
-
|
|
25489
|
+
console.error("Error: --no-worktree has been removed. " + "Edit-capable specialists now auto-provision worktrees. " + "Use --job <id> to reuse an existing worktree.");
|
|
25490
|
+
process.exit(1);
|
|
25470
25491
|
}
|
|
25471
25492
|
if (token === "--job" && argv[i + 1]) {
|
|
25472
25493
|
reuseJobId = argv[++i];
|
|
@@ -25508,8 +25529,8 @@ async function parseArgs6(argv) {
|
|
|
25508
25529
|
process.stdin.on("end", () => resolve6(buf.trim()));
|
|
25509
25530
|
});
|
|
25510
25531
|
}
|
|
25511
|
-
if (!prompt && !beadId) {
|
|
25512
|
-
console.error("Error: provide --prompt, pipe stdin,
|
|
25532
|
+
if (!prompt && !beadId && !reuseJobId) {
|
|
25533
|
+
console.error("Error: provide --prompt, pipe stdin, use --bead <id>, or provide --job <id> for bead inference.");
|
|
25513
25534
|
process.exit(1);
|
|
25514
25535
|
}
|
|
25515
25536
|
return {
|
|
@@ -25527,7 +25548,6 @@ async function parseArgs6(argv) {
|
|
|
25527
25548
|
worktree,
|
|
25528
25549
|
reuseJobId,
|
|
25529
25550
|
forceJob,
|
|
25530
|
-
noWorktree,
|
|
25531
25551
|
epicId
|
|
25532
25552
|
};
|
|
25533
25553
|
}
|
|
@@ -25578,7 +25598,8 @@ function resolveWorkingDirectory(args, jobsDir, permissionRequired, readStatus)
|
|
|
25578
25598
|
return {
|
|
25579
25599
|
workingDirectory: worktreePath,
|
|
25580
25600
|
reusedFromJobId: args.reuseJobId,
|
|
25581
|
-
worktreeOwnerJobId
|
|
25601
|
+
worktreeOwnerJobId,
|
|
25602
|
+
inferredBeadId: targetStatus.bead_id
|
|
25582
25603
|
};
|
|
25583
25604
|
}
|
|
25584
25605
|
return {};
|
|
@@ -25661,12 +25682,11 @@ async function run11() {
|
|
|
25661
25682
|
const requiresWorktree = specialist.specialist.execution.requires_worktree ?? true;
|
|
25662
25683
|
const perm = permission === "LOW" || permission === "MEDIUM" || permission === "HIGH" ? permission : "READ_ONLY";
|
|
25663
25684
|
const editCapable = perm === "MEDIUM" || perm === "HIGH";
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25667
|
-
`
|
|
25668
|
-
` + `
|
|
25669
|
-
` + ` --no-worktree bypass this guard (you accept last-writer-wins risk)
|
|
25685
|
+
const shouldAutoProvisionWorktree = editCapable && requiresWorktree && !args.reuseJobId;
|
|
25686
|
+
const useWorktree = args.worktree || shouldAutoProvisionWorktree;
|
|
25687
|
+
if (shouldAutoProvisionWorktree && !args.beadId) {
|
|
25688
|
+
process.stderr.write(`Error: specialist '${args.name}' has permission_required=${perm} and requires worktree isolation.
|
|
25689
|
+
` + `Provide --bead <id> for automatic worktree provisioning, or use --job <id> to reuse an existing worktree.
|
|
25670
25690
|
`);
|
|
25671
25691
|
process.exit(1);
|
|
25672
25692
|
}
|
|
@@ -25728,12 +25748,47 @@ async function run11() {
|
|
|
25728
25748
|
let prompt = args.prompt;
|
|
25729
25749
|
let variables;
|
|
25730
25750
|
let epicId;
|
|
25731
|
-
|
|
25732
|
-
|
|
25751
|
+
let effectiveBeadId = args.beadId;
|
|
25752
|
+
const runner = new SpecialistRunner({
|
|
25753
|
+
loader,
|
|
25754
|
+
hooks,
|
|
25755
|
+
circuitBreaker,
|
|
25756
|
+
beadsClient
|
|
25757
|
+
});
|
|
25758
|
+
const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
|
|
25759
|
+
const jobsDir = resolveJobsDir();
|
|
25760
|
+
const statusReader = new Supervisor({
|
|
25761
|
+
runner,
|
|
25762
|
+
runOptions: {
|
|
25763
|
+
name: args.name,
|
|
25764
|
+
prompt
|
|
25765
|
+
},
|
|
25766
|
+
jobsDir
|
|
25767
|
+
});
|
|
25768
|
+
const {
|
|
25769
|
+
workingDirectory,
|
|
25770
|
+
reusedFromJobId,
|
|
25771
|
+
worktreeOwnerJobId,
|
|
25772
|
+
inferredBeadId
|
|
25773
|
+
} = resolveWorkingDirectory({
|
|
25774
|
+
...args,
|
|
25775
|
+
worktree: useWorktree
|
|
25776
|
+
}, jobsDir, perm, (jobId2) => statusReader.readStatus(jobId2));
|
|
25777
|
+
await statusReader.dispose();
|
|
25778
|
+
if (!effectiveBeadId && inferredBeadId) {
|
|
25779
|
+
effectiveBeadId = inferredBeadId;
|
|
25780
|
+
console.error(`[input bead auto-resolved from job ${args.reuseJobId}: ${inferredBeadId}]`);
|
|
25781
|
+
}
|
|
25782
|
+
if (effectiveBeadId) {
|
|
25783
|
+
const bead = beadReader.readBead(effectiveBeadId);
|
|
25733
25784
|
if (!bead) {
|
|
25734
|
-
|
|
25785
|
+
const inferredFromJob = !args.beadId && inferredBeadId && effectiveBeadId === inferredBeadId;
|
|
25786
|
+
if (inferredFromJob) {
|
|
25787
|
+
throw new Error(`Unable to read inferred bead '${effectiveBeadId}' from --job '${args.reuseJobId}' via bd show --json`);
|
|
25788
|
+
}
|
|
25789
|
+
throw new Error(`Unable to read bead '${effectiveBeadId}' via bd show --json`);
|
|
25735
25790
|
}
|
|
25736
|
-
const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(
|
|
25791
|
+
const blockers = args.contextDepth > 0 ? beadReader.getCompletedBlockers(effectiveBeadId, args.contextDepth) : [];
|
|
25737
25792
|
if (blockers.length > 0) {
|
|
25738
25793
|
process.stderr.write(dim9(`
|
|
25739
25794
|
[context: ${blockers.length} completed dep${blockers.length > 1 ? "s" : ""} injected]
|
|
@@ -25743,11 +25798,11 @@ async function run11() {
|
|
|
25743
25798
|
prompt = beadContext;
|
|
25744
25799
|
epicId = args.epicId ?? bead.parent;
|
|
25745
25800
|
variables = {
|
|
25801
|
+
...variables ?? {},
|
|
25746
25802
|
bead_context: beadContext,
|
|
25747
|
-
bead_id:
|
|
25803
|
+
bead_id: effectiveBeadId
|
|
25748
25804
|
};
|
|
25749
|
-
}
|
|
25750
|
-
if (!args.beadId && args.epicId) {
|
|
25805
|
+
} else if (args.epicId) {
|
|
25751
25806
|
epicId = args.epicId;
|
|
25752
25807
|
}
|
|
25753
25808
|
if (args.reuseJobId) {
|
|
@@ -25756,28 +25811,10 @@ async function run11() {
|
|
|
25756
25811
|
reviewed_job_id: args.reuseJobId
|
|
25757
25812
|
};
|
|
25758
25813
|
}
|
|
25759
|
-
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
beadsClient
|
|
25764
|
-
});
|
|
25765
|
-
const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
|
|
25766
|
-
const jobsDir = resolveJobsDir();
|
|
25767
|
-
const statusReader = new Supervisor({
|
|
25768
|
-
runner,
|
|
25769
|
-
runOptions: {
|
|
25770
|
-
name: args.name,
|
|
25771
|
-
prompt
|
|
25772
|
-
},
|
|
25773
|
-
jobsDir
|
|
25774
|
-
});
|
|
25775
|
-
const {
|
|
25776
|
-
workingDirectory,
|
|
25777
|
-
reusedFromJobId,
|
|
25778
|
-
worktreeOwnerJobId
|
|
25779
|
-
} = resolveWorkingDirectory(args, jobsDir, perm, (jobId2) => statusReader.readStatus(jobId2));
|
|
25780
|
-
await statusReader.dispose();
|
|
25814
|
+
if (!prompt && !effectiveBeadId) {
|
|
25815
|
+
console.error("Error: provide --prompt, pipe stdin, use --bead <id>, or provide --job <id> for bead inference.");
|
|
25816
|
+
process.exit(1);
|
|
25817
|
+
}
|
|
25781
25818
|
let stopTailer;
|
|
25782
25819
|
const supervisor = new Supervisor({
|
|
25783
25820
|
runner,
|
|
@@ -25786,7 +25823,7 @@ async function run11() {
|
|
|
25786
25823
|
prompt,
|
|
25787
25824
|
variables,
|
|
25788
25825
|
backendOverride: args.model,
|
|
25789
|
-
inputBeadId:
|
|
25826
|
+
inputBeadId: effectiveBeadId,
|
|
25790
25827
|
epicId,
|
|
25791
25828
|
keepAlive: args.keepAlive,
|
|
25792
25829
|
noKeepAlive: args.noKeepAlive,
|
|
@@ -25806,13 +25843,13 @@ async function run11() {
|
|
|
25806
25843
|
process.stderr.write(dim9(`[job started: ${id}]
|
|
25807
25844
|
`));
|
|
25808
25845
|
if (args.outputMode !== "raw") {
|
|
25809
|
-
stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name,
|
|
25846
|
+
stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name, effectiveBeadId);
|
|
25810
25847
|
}
|
|
25811
25848
|
}
|
|
25812
25849
|
});
|
|
25813
|
-
if (
|
|
25850
|
+
if (effectiveBeadId && workingDirectory) {
|
|
25814
25851
|
try {
|
|
25815
|
-
execSync2(`bd kv set "bead-claim:${
|
|
25852
|
+
execSync2(`bd kv set "bead-claim:${effectiveBeadId}" "active"`, {
|
|
25816
25853
|
cwd: workingDirectory,
|
|
25817
25854
|
stdio: "pipe",
|
|
25818
25855
|
timeout: 5000
|
|
@@ -25832,9 +25869,9 @@ ${bold10(`Running ${cyan6(args.name)}`)}
|
|
|
25832
25869
|
stopTailer?.();
|
|
25833
25870
|
}
|
|
25834
25871
|
stopTailer?.();
|
|
25835
|
-
if (
|
|
25872
|
+
if (effectiveBeadId && workingDirectory) {
|
|
25836
25873
|
try {
|
|
25837
|
-
execSync2(`bd kv clear "bead-claim:${
|
|
25874
|
+
execSync2(`bd kv clear "bead-claim:${effectiveBeadId}"`, {
|
|
25838
25875
|
cwd: workingDirectory,
|
|
25839
25876
|
stdio: "pipe",
|
|
25840
25877
|
timeout: 5000
|
|
@@ -27982,7 +28019,7 @@ ${JSON.stringify(recoveryDigest, null, 2)}`
|
|
|
27982
28019
|
}
|
|
27983
28020
|
}
|
|
27984
28021
|
const canResumeCoordinator = this.coordinatorResumesInFlight < MAX_IN_FLIGHT_COORDINATOR_RESUMES;
|
|
27985
|
-
const shouldResumeCoordinator = changes.length > 0 && coordinatorStatus?.status === "waiting" && !this.resumePending && canResumeCoordinator && Boolean(this.coordinatorJobId) && Boolean(this.coordinatorController);
|
|
28022
|
+
const shouldResumeCoordinator = changes.length > 0 && coordinatorStatus?.status === "waiting" && !TERMINAL_NODE_STATUSES.has(this.status) && !this.resumePending && canResumeCoordinator && Boolean(this.coordinatorJobId) && Boolean(this.coordinatorController);
|
|
27986
28023
|
if (changes.length > 0 && !shouldResumeCoordinator) {
|
|
27987
28024
|
const skipReasons = [];
|
|
27988
28025
|
if (coordinatorStatus?.status !== "waiting")
|
|
@@ -33300,7 +33337,7 @@ __export(exports_doctor, {
|
|
|
33300
33337
|
import { createHash as createHash4 } from "crypto";
|
|
33301
33338
|
import { spawnSync as spawnSync21 } from "child_process";
|
|
33302
33339
|
import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readdirSync as readdirSync13, readFileSync as readFileSync22, readlinkSync as readlinkSync2, writeFileSync as writeFileSync9 } from "fs";
|
|
33303
|
-
import { dirname as
|
|
33340
|
+
import { dirname as dirname6, join as join28, relative as relative2, resolve as resolve7 } from "path";
|
|
33304
33341
|
function ok3(msg) {
|
|
33305
33342
|
console.log(` ${green14("\u2713")} ${msg}`);
|
|
33306
33343
|
}
|
|
@@ -33410,8 +33447,9 @@ function checkHooks() {
|
|
|
33410
33447
|
fix("specialists install");
|
|
33411
33448
|
return false;
|
|
33412
33449
|
}
|
|
33413
|
-
const
|
|
33414
|
-
const
|
|
33450
|
+
const hooksObj = settings.hooks ?? {};
|
|
33451
|
+
const userPromptSubmit = hooksObj.UserPromptSubmit ?? settings.UserPromptSubmit ?? [];
|
|
33452
|
+
const sessionStart = hooksObj.SessionStart ?? settings.SessionStart ?? [];
|
|
33415
33453
|
const wiredCommands = new Set([...userPromptSubmit, ...sessionStart].flatMap((entry) => (entry.hooks ?? []).map((hook) => hook.command ?? "")));
|
|
33416
33454
|
for (const name of HOOK_NAMES) {
|
|
33417
33455
|
const expectedRelative = `node .specialists/default/hooks/${name}`;
|
|
@@ -33474,7 +33512,7 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
|
33474
33512
|
return { ok: false, reason: "not-symlink" };
|
|
33475
33513
|
try {
|
|
33476
33514
|
const rawTarget = readlinkSync2(linkPath);
|
|
33477
|
-
const resolvedTarget = resolve7(
|
|
33515
|
+
const resolvedTarget = resolve7(dirname6(linkPath), rawTarget);
|
|
33478
33516
|
const resolvedExpected = resolve7(expectedTargetPath);
|
|
33479
33517
|
if (resolvedTarget !== resolvedExpected) {
|
|
33480
33518
|
return { ok: false, reason: "wrong-target", target: rawTarget };
|
|
@@ -33569,7 +33607,7 @@ function checkSkillDrift() {
|
|
|
33569
33607
|
for (const check2 of skillRootChecks) {
|
|
33570
33608
|
const state = isSymlinkTo(check2.root, check2.expected);
|
|
33571
33609
|
if (state.ok) {
|
|
33572
|
-
ok3(`${relative2(CWD, check2.root)} -> ${relative2(
|
|
33610
|
+
ok3(`${relative2(CWD, check2.root)} -> ${relative2(dirname6(check2.root), check2.expected)}`);
|
|
33573
33611
|
continue;
|
|
33574
33612
|
}
|
|
33575
33613
|
rootLinksOk = false;
|
|
@@ -41731,7 +41769,7 @@ async function run29() {
|
|
|
41731
41769
|
"Primary modes:",
|
|
41732
41770
|
" tracked: specialists run <name> --bead <id>",
|
|
41733
41771
|
' ad-hoc: specialists run <name> --prompt "..."',
|
|
41734
|
-
"
|
|
41772
|
+
" explicit wt specialists run <name> --bead <id> --worktree",
|
|
41735
41773
|
" reuse job: specialists run <name> --bead <id> --job <prior-job-id>",
|
|
41736
41774
|
"",
|
|
41737
41775
|
"Options:",
|
|
@@ -41742,7 +41780,7 @@ async function run29() {
|
|
|
41742
41780
|
" --no-bead-notes Do not append completion notes to an external --bead",
|
|
41743
41781
|
" --model <model> Override the configured model for this run",
|
|
41744
41782
|
" --keep-alive Keep session alive for follow-up prompts",
|
|
41745
|
-
" --worktree
|
|
41783
|
+
" --worktree Explicitly provision (or reuse) a bd-managed worktree derived from --bead.",
|
|
41746
41784
|
" Requires --bead. Mutually exclusive with --job.",
|
|
41747
41785
|
" --job <id> Reuse the workspace of a prior job (must have been started with",
|
|
41748
41786
|
" --worktree). Caller bead context remains authoritative.",
|
|
@@ -41758,7 +41796,7 @@ async function run29() {
|
|
|
41758
41796
|
"",
|
|
41759
41797
|
"Rules:",
|
|
41760
41798
|
" Use --bead for tracked work.",
|
|
41761
|
-
"
|
|
41799
|
+
" MEDIUM/HIGH specialists auto-provision a worktree when requires_worktree=true.",
|
|
41762
41800
|
" Use --job to reuse a prior worktree without re-provisioning.",
|
|
41763
41801
|
" --worktree and --job are mutually exclusive.",
|
|
41764
41802
|
" --worktree requires --bead to derive a deterministic branch name.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaggerxtrm/specialists",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.1",
|
|
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",
|