@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.
@@ -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.5
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 claims impl bead, provisions --worktree, implements, closes bead.
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
- -> Reads task bead from exec-job status.json automatically.
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: orchestrator closes parent task bead.
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
- -> Create fix bead as child of impl bead.
440
- -> Run executor --bead fix-bead --job exec-job --context-depth 2.
441
- -> Fix executor sees: fix-bead + impl (with reviewer verdict) + explore.
442
- -> Fix executor closes fix-bead on completion.
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 (--prompt required when no --bead)
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 3bCreate fix bead + run fix executor in same worktree
462
- bd create --title "Fix: address reviewer findings on impl" --type bug --priority 1
463
- # -> unitAI-fix1
464
- bd dep add fix1 impl
465
- specialists run executor --bead fix1 --job 49adda --context-depth 2 --background
466
-
467
- # Re-review
468
+ # Step 3aPASS: 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
- # PASS close parent
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
- - Reviewer never re-opens the impl bead it was closed by the executor. The reviewer's verdict lives on the bead notes as appended output.
475
- - Each fix iteration creates a **new child bead** never reopen or re-claim the completed impl bead.
476
- - The fix executor inherits the full context chain: fix-bead + impl (executor output + reviewer findings) + explore, via `--context-depth 2`.
477
- - Multiple reviewer → fix cycles are expected for complex changes. The worktree is stable across all cycles.
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. If executor bails early (e.g. GitNexus CRITICAL risk warning), orchestrator can `resume` with "proceed, this is additive" instead of re-dispatching. Always `stop` executor explicitly when work is complete.
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", "address the risk warning and continue", or "done, close bead" |
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 = join2(homedir(), ".nvm/versions/node", process.version, "lib/node_modules");
18093
- const gitnexusPath = join2(npmGlobalDir, "pi-gitnexus");
18094
- if (existsSync2(gitnexusPath))
18095
- args.push("-e", gitnexusPath);
18096
- const serenaPath = join2(npmGlobalDir, "pi-serena-tools");
18097
- if (existsSync2(serenaPath))
18098
- args.push("-e", serenaPath);
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(dirname(options.tracePath), { recursive: true }).then(() => {});
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 dirname2, join as join4 } from "path";
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 = dirname2(fileURLToPath(import.meta.url));
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 dirname3, join as join5, resolve as resolve3 } from "path";
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 dirname3(resolve3(cwd, gitCommonDir));
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 dirname4, join as join10, relative, resolve as resolve4 } from "path";
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(dirname4(claudeHookPath), readlinkSync(claudeHookPath));
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 = settings[event] ?? [];
23758
- settings[event] = eventList;
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(dirname4(rootPath), { recursive: true });
23778
- const relTarget = relative(dirname4(rootPath), expectedTargetPath);
23799
+ mkdirSync4(dirname5(rootPath), { recursive: true });
23800
+ const relTarget = relative(dirname5(rootPath), expectedTargetPath);
23779
23801
  symlinkSync(relTarget, rootPath);
23780
- ok(`created ${basename3(dirname4(rootPath))}/${basename3(rootPath)} \u2192 ${relTarget}`);
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(dirname4(rootPath), linkTarget);
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(dirname4(activeLinkPath), readlinkSync(activeLinkPath));
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
- noWorktree = true;
25469
- continue;
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, or use --bead <id>.");
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
- if (editCapable && requiresWorktree && !args.worktree && !args.reuseJobId && !args.noWorktree) {
25665
- process.stderr.write(`Error: specialist '${args.name}' has permission_required=${perm} and can edit files.
25666
- ` + `Edit-capable specialists must run in isolation. Use one of:
25667
- ` + ` --worktree provision an isolated worktree (recommended)
25668
- ` + ` --job <id> reuse an existing job's worktree
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
- if (args.beadId) {
25732
- const bead = beadReader.readBead(args.beadId);
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
- throw new Error(`Unable to read bead '${args.beadId}' via bd show --json`);
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(args.beadId, args.contextDepth) : [];
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: args.beadId
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
- const runner = new SpecialistRunner({
25760
- loader,
25761
- hooks,
25762
- circuitBreaker,
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: args.beadId,
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, args.beadId);
25846
+ stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name, effectiveBeadId);
25810
25847
  }
25811
25848
  }
25812
25849
  });
25813
- if (args.beadId && workingDirectory) {
25850
+ if (effectiveBeadId && workingDirectory) {
25814
25851
  try {
25815
- execSync2(`bd kv set "bead-claim:${args.beadId}" "active"`, {
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 (args.beadId && workingDirectory) {
25872
+ if (effectiveBeadId && workingDirectory) {
25836
25873
  try {
25837
- execSync2(`bd kv clear "bead-claim:${args.beadId}"`, {
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 dirname5, join as join28, relative as relative2, resolve as resolve7 } from "path";
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 userPromptSubmit = settings.UserPromptSubmit ?? [];
33414
- const sessionStart = settings.SessionStart ?? [];
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(dirname5(linkPath), rawTarget);
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(dirname5(check2.root), check2.expected)}`);
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
- " worktree: specialists run <name> --bead <id> --worktree",
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 Provision (or reuse) a bd-managed worktree derived from --bead.",
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
- " Use --worktree to isolate the run in its own git branch/directory.",
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.0",
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",