@jaggerxtrm/specialists 3.4.1 → 3.4.2

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.
Files changed (2) hide show
  1. package/dist/index.js +463 -189
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19111,7 +19111,8 @@ class Supervisor {
19111
19111
  status: "starting",
19112
19112
  started_at_ms: startedAtMs,
19113
19113
  pid: process.pid,
19114
- ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {}
19114
+ ...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
19115
+ ...process.env.SPECIALISTS_TMUX_SESSION ? { tmux_session: process.env.SPECIALISTS_TMUX_SESSION } : {}
19115
19116
  };
19116
19117
  this.writeStatusFile(id, initialStatus);
19117
19118
  writeFileSync(join3(this.opts.jobsDir, "latest"), `${id}
@@ -19381,6 +19382,9 @@ class Supervisor {
19381
19382
  if (existsSync4(fifoPath))
19382
19383
  rmSync(fifoPath);
19383
19384
  } catch {}
19385
+ if (statusSnapshot.tmux_session) {
19386
+ spawnSync3("tmux", ["kill-session", "-t", statusSnapshot.tmux_session], { stdio: "ignore" });
19387
+ }
19384
19388
  }
19385
19389
  }
19386
19390
  }
@@ -19577,6 +19581,132 @@ __export(exports_list, {
19577
19581
  parseArgs: () => parseArgs,
19578
19582
  ArgParseError: () => ArgParseError
19579
19583
  });
19584
+ import { spawnSync as spawnSync5 } from "node:child_process";
19585
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
19586
+ import { join as join13 } from "node:path";
19587
+ import readline from "node:readline";
19588
+ function toLiveJob(status) {
19589
+ if (!status)
19590
+ return null;
19591
+ if (status.status !== "running" && status.status !== "waiting" || !status.tmux_session) {
19592
+ return null;
19593
+ }
19594
+ const elapsedS = status.elapsed_s ?? Math.max(0, Math.floor((Date.now() - status.started_at_ms) / 1000));
19595
+ return {
19596
+ id: status.id,
19597
+ specialist: status.specialist,
19598
+ status: status.status,
19599
+ tmuxSession: status.tmux_session,
19600
+ elapsedS,
19601
+ startedAtMs: status.started_at_ms
19602
+ };
19603
+ }
19604
+ function readJobStatus(statusPath) {
19605
+ try {
19606
+ return JSON.parse(readFileSync5(statusPath, "utf-8"));
19607
+ } catch {
19608
+ return null;
19609
+ }
19610
+ }
19611
+ function listLiveJobs() {
19612
+ const jobsDir = join13(process.cwd(), ".specialists", "jobs");
19613
+ if (!existsSync9(jobsDir))
19614
+ return [];
19615
+ const jobs = readdirSync3(jobsDir).map((entry) => toLiveJob(readJobStatus(join13(jobsDir, entry, "status.json")))).filter((job) => job !== null).sort((a, b) => b.startedAtMs - a.startedAtMs);
19616
+ return jobs;
19617
+ }
19618
+ function formatLiveChoice(job) {
19619
+ return `${job.tmuxSession} ${job.specialist} ${job.elapsedS}s ${job.status}`;
19620
+ }
19621
+ function renderLiveSelector(jobs, selectedIndex) {
19622
+ return [
19623
+ "",
19624
+ bold2("Select tmux session (↑/↓, Enter to attach, Ctrl+C to cancel)"),
19625
+ "",
19626
+ ...jobs.map((job, index) => `${index === selectedIndex ? cyan("❯") : " "} ${formatLiveChoice(job)}`),
19627
+ ""
19628
+ ];
19629
+ }
19630
+ function selectLiveJob(jobs) {
19631
+ return new Promise((resolve2) => {
19632
+ const input = process.stdin;
19633
+ const output = process.stdout;
19634
+ const wasRawMode = input.isTTY ? input.isRaw : false;
19635
+ let selectedIndex = 0;
19636
+ let renderedLineCount = 0;
19637
+ const cleanup = (selected) => {
19638
+ input.off("keypress", onKeypress);
19639
+ if (input.isTTY && !wasRawMode) {
19640
+ input.setRawMode(false);
19641
+ }
19642
+ output.write("\x1B[?25h");
19643
+ if (renderedLineCount > 0) {
19644
+ readline.moveCursor(output, 0, -renderedLineCount);
19645
+ readline.clearScreenDown(output);
19646
+ }
19647
+ resolve2(selected);
19648
+ };
19649
+ const render = () => {
19650
+ if (renderedLineCount > 0) {
19651
+ readline.moveCursor(output, 0, -renderedLineCount);
19652
+ readline.clearScreenDown(output);
19653
+ }
19654
+ const lines = renderLiveSelector(jobs, selectedIndex);
19655
+ output.write(lines.join(`
19656
+ `));
19657
+ renderedLineCount = lines.length;
19658
+ };
19659
+ const onKeypress = (_, key) => {
19660
+ if (key.ctrl && key.name === "c") {
19661
+ cleanup(null);
19662
+ return;
19663
+ }
19664
+ if (key.name === "up") {
19665
+ selectedIndex = (selectedIndex - 1 + jobs.length) % jobs.length;
19666
+ render();
19667
+ return;
19668
+ }
19669
+ if (key.name === "down") {
19670
+ selectedIndex = (selectedIndex + 1) % jobs.length;
19671
+ render();
19672
+ return;
19673
+ }
19674
+ if (key.name === "return") {
19675
+ cleanup(jobs[selectedIndex]);
19676
+ }
19677
+ };
19678
+ readline.emitKeypressEvents(input);
19679
+ if (input.isTTY && !wasRawMode) {
19680
+ input.setRawMode(true);
19681
+ }
19682
+ output.write("\x1B[?25l");
19683
+ input.on("keypress", onKeypress);
19684
+ render();
19685
+ });
19686
+ }
19687
+ async function runLiveMode() {
19688
+ const jobs = listLiveJobs();
19689
+ if (jobs.length === 0) {
19690
+ console.log("No running tmux sessions found.");
19691
+ return;
19692
+ }
19693
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
19694
+ for (const job of jobs) {
19695
+ console.log(`${job.id} ${job.tmuxSession} ${job.status}`);
19696
+ }
19697
+ return;
19698
+ }
19699
+ const selected = await selectLiveJob(jobs);
19700
+ if (!selected)
19701
+ return;
19702
+ const attach = spawnSync5("tmux", ["attach-session", "-t", selected.tmuxSession], {
19703
+ stdio: "inherit"
19704
+ });
19705
+ if (attach.error) {
19706
+ console.error(`Failed to attach tmux session ${selected.tmuxSession}: ${attach.error.message}`);
19707
+ process.exit(1);
19708
+ }
19709
+ }
19580
19710
  function parseArgs(argv) {
19581
19711
  const result = {};
19582
19712
  for (let i = 0;i < argv.length; i++) {
@@ -19601,6 +19731,10 @@ function parseArgs(argv) {
19601
19731
  result.json = true;
19602
19732
  continue;
19603
19733
  }
19734
+ if (token === "--live") {
19735
+ result.live = true;
19736
+ continue;
19737
+ }
19604
19738
  }
19605
19739
  return result;
19606
19740
  }
@@ -19615,6 +19749,10 @@ async function run3() {
19615
19749
  }
19616
19750
  throw err;
19617
19751
  }
19752
+ if (args.live) {
19753
+ await runLiveMode();
19754
+ return;
19755
+ }
19618
19756
  const loader = new SpecialistLoader;
19619
19757
  let specialists = await loader.list(args.category);
19620
19758
  if (args.scope) {
@@ -19658,9 +19796,9 @@ var exports_models = {};
19658
19796
  __export(exports_models, {
19659
19797
  run: () => run4
19660
19798
  });
19661
- import { spawnSync as spawnSync5 } from "node:child_process";
19799
+ import { spawnSync as spawnSync6 } from "node:child_process";
19662
19800
  function parsePiModels() {
19663
- const r = spawnSync5("pi", ["--list-models"], {
19801
+ const r = spawnSync6("pi", ["--list-models"], {
19664
19802
  encoding: "utf8",
19665
19803
  stdio: "pipe",
19666
19804
  timeout: 8000
@@ -19763,8 +19901,8 @@ var exports_init = {};
19763
19901
  __export(exports_init, {
19764
19902
  run: () => run5
19765
19903
  });
19766
- import { copyFileSync, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19767
- import { join as join13 } from "node:path";
19904
+ import { copyFileSync, cpSync, existsSync as existsSync10, mkdirSync as mkdirSync2, readdirSync as readdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
19905
+ import { join as join14 } from "node:path";
19768
19906
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19769
19907
  function ok(msg) {
19770
19908
  console.log(` ${green3("✓")} ${msg}`);
@@ -19773,10 +19911,10 @@ function skip(msg) {
19773
19911
  console.log(` ${yellow4("○")} ${msg}`);
19774
19912
  }
19775
19913
  function loadJson(path, fallback) {
19776
- if (!existsSync9(path))
19914
+ if (!existsSync10(path))
19777
19915
  return structuredClone(fallback);
19778
19916
  try {
19779
- return JSON.parse(readFileSync5(path, "utf-8"));
19917
+ return JSON.parse(readFileSync6(path, "utf-8"));
19780
19918
  } catch {
19781
19919
  return structuredClone(fallback);
19782
19920
  }
@@ -19788,30 +19926,30 @@ function saveJson(path, value) {
19788
19926
  function resolvePackagePath(relativePath) {
19789
19927
  const configPath = `config/${relativePath}`;
19790
19928
  let resolved = fileURLToPath2(new URL(`../${configPath}`, import.meta.url));
19791
- if (existsSync9(resolved))
19929
+ if (existsSync10(resolved))
19792
19930
  return resolved;
19793
19931
  resolved = fileURLToPath2(new URL(`../../${configPath}`, import.meta.url));
19794
- if (existsSync9(resolved))
19932
+ if (existsSync10(resolved))
19795
19933
  return resolved;
19796
19934
  return null;
19797
19935
  }
19798
19936
  function migrateLegacySpecialists(cwd, scope) {
19799
- const sourceDir = join13(cwd, ".specialists", scope, "specialists");
19800
- if (!existsSync9(sourceDir))
19937
+ const sourceDir = join14(cwd, ".specialists", scope, "specialists");
19938
+ if (!existsSync10(sourceDir))
19801
19939
  return;
19802
- const targetDir = join13(cwd, ".specialists", scope);
19803
- if (!existsSync9(targetDir)) {
19940
+ const targetDir = join14(cwd, ".specialists", scope);
19941
+ if (!existsSync10(targetDir)) {
19804
19942
  mkdirSync2(targetDir, { recursive: true });
19805
19943
  }
19806
- const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19944
+ const files = readdirSync4(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19807
19945
  if (files.length === 0)
19808
19946
  return;
19809
19947
  let moved = 0;
19810
19948
  let skipped = 0;
19811
19949
  for (const file of files) {
19812
- const src = join13(sourceDir, file);
19813
- const dest = join13(targetDir, file);
19814
- if (existsSync9(dest)) {
19950
+ const src = join14(sourceDir, file);
19951
+ const dest = join14(targetDir, file);
19952
+ if (existsSync10(dest)) {
19815
19953
  skipped++;
19816
19954
  continue;
19817
19955
  }
@@ -19831,21 +19969,21 @@ function copyCanonicalSpecialists(cwd) {
19831
19969
  skip("no canonical specialists found in package");
19832
19970
  return;
19833
19971
  }
19834
- const targetDir = join13(cwd, ".specialists", "default");
19835
- const files = readdirSync3(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19972
+ const targetDir = join14(cwd, ".specialists", "default");
19973
+ const files = readdirSync4(sourceDir).filter((f) => f.endsWith(".specialist.yaml"));
19836
19974
  if (files.length === 0) {
19837
19975
  skip("no specialist files found in package");
19838
19976
  return;
19839
19977
  }
19840
- if (!existsSync9(targetDir)) {
19978
+ if (!existsSync10(targetDir)) {
19841
19979
  mkdirSync2(targetDir, { recursive: true });
19842
19980
  }
19843
19981
  let copied = 0;
19844
19982
  let skipped = 0;
19845
19983
  for (const file of files) {
19846
- const src = join13(sourceDir, file);
19847
- const dest = join13(targetDir, file);
19848
- if (existsSync9(dest)) {
19984
+ const src = join14(sourceDir, file);
19985
+ const dest = join14(targetDir, file);
19986
+ if (existsSync10(dest)) {
19849
19987
  skipped++;
19850
19988
  } else {
19851
19989
  copyFileSync(src, dest);
@@ -19865,21 +20003,21 @@ function installProjectHooks(cwd) {
19865
20003
  skip("no canonical hooks found in package");
19866
20004
  return;
19867
20005
  }
19868
- const targetDir = join13(cwd, ".claude", "hooks");
19869
- const hooks = readdirSync3(sourceDir).filter((f) => f.endsWith(".mjs"));
20006
+ const targetDir = join14(cwd, ".claude", "hooks");
20007
+ const hooks = readdirSync4(sourceDir).filter((f) => f.endsWith(".mjs"));
19870
20008
  if (hooks.length === 0) {
19871
20009
  skip("no hook files found in package");
19872
20010
  return;
19873
20011
  }
19874
- if (!existsSync9(targetDir)) {
20012
+ if (!existsSync10(targetDir)) {
19875
20013
  mkdirSync2(targetDir, { recursive: true });
19876
20014
  }
19877
20015
  let copied = 0;
19878
20016
  let skipped = 0;
19879
20017
  for (const file of hooks) {
19880
- const src = join13(sourceDir, file);
19881
- const dest = join13(targetDir, file);
19882
- if (existsSync9(dest)) {
20018
+ const src = join14(sourceDir, file);
20019
+ const dest = join14(targetDir, file);
20020
+ if (existsSync10(dest)) {
19883
20021
  skipped++;
19884
20022
  } else {
19885
20023
  copyFileSync(src, dest);
@@ -19894,9 +20032,9 @@ function installProjectHooks(cwd) {
19894
20032
  }
19895
20033
  }
19896
20034
  function ensureProjectHookWiring(cwd) {
19897
- const settingsPath = join13(cwd, ".claude", "settings.json");
19898
- const settingsDir = join13(cwd, ".claude");
19899
- if (!existsSync9(settingsDir)) {
20035
+ const settingsPath = join14(cwd, ".claude", "settings.json");
20036
+ const settingsDir = join14(cwd, ".claude");
20037
+ if (!existsSync10(settingsDir)) {
19900
20038
  mkdirSync2(settingsDir, { recursive: true });
19901
20039
  }
19902
20040
  const settings = loadJson(settingsPath, {});
@@ -19925,25 +20063,25 @@ function installProjectSkills(cwd) {
19925
20063
  skip("no canonical skills found in package");
19926
20064
  return;
19927
20065
  }
19928
- const skills = readdirSync3(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
20066
+ const skills = readdirSync4(sourceDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
19929
20067
  if (skills.length === 0) {
19930
20068
  skip("no skill directories found in package");
19931
20069
  return;
19932
20070
  }
19933
20071
  const targetDirs = [
19934
- join13(cwd, ".claude", "skills"),
19935
- join13(cwd, ".pi", "skills")
20072
+ join14(cwd, ".claude", "skills"),
20073
+ join14(cwd, ".pi", "skills")
19936
20074
  ];
19937
20075
  let totalCopied = 0;
19938
20076
  let totalSkipped = 0;
19939
20077
  for (const targetDir of targetDirs) {
19940
- if (!existsSync9(targetDir)) {
20078
+ if (!existsSync10(targetDir)) {
19941
20079
  mkdirSync2(targetDir, { recursive: true });
19942
20080
  }
19943
20081
  for (const skill of skills) {
19944
- const src = join13(sourceDir, skill);
19945
- const dest = join13(targetDir, skill);
19946
- if (existsSync9(dest)) {
20082
+ const src = join14(sourceDir, skill);
20083
+ const dest = join14(targetDir, skill);
20084
+ if (existsSync10(dest)) {
19947
20085
  totalSkipped++;
19948
20086
  } else {
19949
20087
  cpSync(src, dest, { recursive: true });
@@ -19959,20 +20097,20 @@ function installProjectSkills(cwd) {
19959
20097
  }
19960
20098
  }
19961
20099
  function createUserDirs(cwd) {
19962
- const userDir = join13(cwd, ".specialists", "user");
19963
- if (!existsSync9(userDir)) {
20100
+ const userDir = join14(cwd, ".specialists", "user");
20101
+ if (!existsSync10(userDir)) {
19964
20102
  mkdirSync2(userDir, { recursive: true });
19965
20103
  ok("created .specialists/user/ for custom specialists");
19966
20104
  }
19967
20105
  }
19968
20106
  function createRuntimeDirs(cwd) {
19969
20107
  const runtimeDirs = [
19970
- join13(cwd, ".specialists", "jobs"),
19971
- join13(cwd, ".specialists", "ready")
20108
+ join14(cwd, ".specialists", "jobs"),
20109
+ join14(cwd, ".specialists", "ready")
19972
20110
  ];
19973
20111
  let created = 0;
19974
20112
  for (const dir of runtimeDirs) {
19975
- if (!existsSync9(dir)) {
20113
+ if (!existsSync10(dir)) {
19976
20114
  mkdirSync2(dir, { recursive: true });
19977
20115
  created++;
19978
20116
  }
@@ -19982,7 +20120,7 @@ function createRuntimeDirs(cwd) {
19982
20120
  }
19983
20121
  }
19984
20122
  function ensureProjectMcp(cwd) {
19985
- const mcpPath = join13(cwd, MCP_FILE);
20123
+ const mcpPath = join14(cwd, MCP_FILE);
19986
20124
  const mcp = loadJson(mcpPath, { mcpServers: {} });
19987
20125
  mcp.mcpServers ??= {};
19988
20126
  const existing = mcp.mcpServers[MCP_SERVER_NAME];
@@ -19995,8 +20133,8 @@ function ensureProjectMcp(cwd) {
19995
20133
  ok("registered specialists in project .mcp.json");
19996
20134
  }
19997
20135
  function ensureGitignore(cwd) {
19998
- const gitignorePath = join13(cwd, ".gitignore");
19999
- const existing = existsSync9(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
20136
+ const gitignorePath = join14(cwd, ".gitignore");
20137
+ const existing = existsSync10(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
20000
20138
  let added = 0;
20001
20139
  const lines = existing.split(`
20002
20140
  `);
@@ -20016,9 +20154,9 @@ function ensureGitignore(cwd) {
20016
20154
  }
20017
20155
  }
20018
20156
  function ensureAgentsMd(cwd) {
20019
- const agentsPath = join13(cwd, "AGENTS.md");
20020
- if (existsSync9(agentsPath)) {
20021
- const existing = readFileSync5(agentsPath, "utf-8");
20157
+ const agentsPath = join14(cwd, "AGENTS.md");
20158
+ if (existsSync10(agentsPath)) {
20159
+ const existing = readFileSync6(agentsPath, "utf-8");
20022
20160
  if (existing.includes(AGENTS_MARKER)) {
20023
20161
  skip("AGENTS.md already has Specialists section");
20024
20162
  } else {
@@ -20111,8 +20249,8 @@ __export(exports_validate, {
20111
20249
  ArgParseError: () => ArgParseError2
20112
20250
  });
20113
20251
  import { readFile as readFile2 } from "node:fs/promises";
20114
- import { existsSync as existsSync10 } from "node:fs";
20115
- import { join as join14 } from "node:path";
20252
+ import { existsSync as existsSync11 } from "node:fs";
20253
+ import { join as join15 } from "node:path";
20116
20254
  function parseArgs3(argv) {
20117
20255
  const name = argv[0];
20118
20256
  if (!name || name.startsWith("--")) {
@@ -20123,15 +20261,15 @@ function parseArgs3(argv) {
20123
20261
  }
20124
20262
  function findSpecialistFile(name) {
20125
20263
  const scanDirs = [
20126
- join14(process.cwd(), ".specialists", "user"),
20127
- join14(process.cwd(), ".specialists", "user", "specialists"),
20128
- join14(process.cwd(), ".specialists", "default"),
20129
- join14(process.cwd(), ".specialists", "default", "specialists"),
20130
- join14(process.cwd(), "specialists")
20264
+ join15(process.cwd(), ".specialists", "user"),
20265
+ join15(process.cwd(), ".specialists", "user", "specialists"),
20266
+ join15(process.cwd(), ".specialists", "default"),
20267
+ join15(process.cwd(), ".specialists", "default", "specialists"),
20268
+ join15(process.cwd(), "specialists")
20131
20269
  ];
20132
20270
  for (const dir of scanDirs) {
20133
- const candidate = join14(dir, `${name}.specialist.yaml`);
20134
- if (existsSync10(candidate)) {
20271
+ const candidate = join15(dir, `${name}.specialist.yaml`);
20272
+ if (existsSync11(candidate)) {
20135
20273
  return candidate;
20136
20274
  }
20137
20275
  }
@@ -20223,9 +20361,9 @@ var exports_edit = {};
20223
20361
  __export(exports_edit, {
20224
20362
  run: () => run7
20225
20363
  });
20226
- import { spawnSync as spawnSync6 } from "node:child_process";
20227
- import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
20228
- import { join as join15 } from "node:path";
20364
+ import { spawnSync as spawnSync7 } from "node:child_process";
20365
+ import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
20366
+ import { join as join16 } from "node:path";
20229
20367
  function parseArgs4(argv) {
20230
20368
  const name = argv[0];
20231
20369
  if (!name || name.startsWith("--")) {
@@ -20289,18 +20427,18 @@ function setIn(doc2, path, value) {
20289
20427
  }
20290
20428
  }
20291
20429
  function openAllConfigSpecialistsInEditor() {
20292
- const configDir = join15(process.cwd(), "config", "specialists");
20293
- if (!existsSync11(configDir)) {
20430
+ const configDir = join16(process.cwd(), "config", "specialists");
20431
+ if (!existsSync12(configDir)) {
20294
20432
  console.error(`Error: missing directory: ${configDir}`);
20295
20433
  process.exit(1);
20296
20434
  }
20297
- const files = readdirSync4(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join15(configDir, file));
20435
+ const files = readdirSync5(configDir).filter((file) => file.endsWith(".specialist.yaml")).sort().map((file) => join16(configDir, file));
20298
20436
  if (files.length === 0) {
20299
20437
  console.error("Error: no specialist YAML files found in config/specialists/");
20300
20438
  process.exit(1);
20301
20439
  }
20302
20440
  const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
20303
- const result = spawnSync6(editor, files, { stdio: "inherit", shell: true });
20441
+ const result = spawnSync7(editor, files, { stdio: "inherit", shell: true });
20304
20442
  if (result.status !== 0) {
20305
20443
  process.exit(result.status ?? 1);
20306
20444
  }
@@ -20322,7 +20460,7 @@ async function run7() {
20322
20460
  console.error(` Run ${yellow6("specialists list")} to see available specialists`);
20323
20461
  process.exit(1);
20324
20462
  }
20325
- const raw = readFileSync6(match.filePath, "utf-8");
20463
+ const raw = readFileSync7(match.filePath, "utf-8");
20326
20464
  const doc2 = $parseDocument(raw);
20327
20465
  const yamlPath = FIELD_MAP[field];
20328
20466
  let typedValue = value;
@@ -20377,9 +20515,9 @@ var exports_config = {};
20377
20515
  __export(exports_config, {
20378
20516
  run: () => run8
20379
20517
  });
20380
- import { existsSync as existsSync12 } from "node:fs";
20518
+ import { existsSync as existsSync13 } from "node:fs";
20381
20519
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
20382
- import { basename as basename2, join as join16 } from "node:path";
20520
+ import { basename as basename2, join as join17 } from "node:path";
20383
20521
  function usage() {
20384
20522
  return [
20385
20523
  "Usage:",
@@ -20449,22 +20587,22 @@ function splitKeyPath(key) {
20449
20587
  return path;
20450
20588
  }
20451
20589
  function getSpecialistDir(projectDir) {
20452
- return join16(projectDir, "config", "specialists");
20590
+ return join17(projectDir, "config", "specialists");
20453
20591
  }
20454
20592
  function getSpecialistNameFromPath(path) {
20455
20593
  return path.replace(/\.specialist\.yaml$/, "");
20456
20594
  }
20457
20595
  async function listSpecialistFiles(projectDir) {
20458
20596
  const specialistDir = getSpecialistDir(projectDir);
20459
- if (!existsSync12(specialistDir)) {
20597
+ if (!existsSync13(specialistDir)) {
20460
20598
  throw new Error(`Missing directory: ${specialistDir}`);
20461
20599
  }
20462
20600
  const entries = await readdir2(specialistDir);
20463
- return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join16(specialistDir, entry));
20601
+ return entries.filter((entry) => entry.endsWith(".specialist.yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => join17(specialistDir, entry));
20464
20602
  }
20465
20603
  async function findNamedSpecialistFile(projectDir, name) {
20466
- const path = join16(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
20467
- if (!existsSync12(path)) {
20604
+ const path = join17(getSpecialistDir(projectDir), `${name}.specialist.yaml`);
20605
+ if (!existsSync13(path)) {
20468
20606
  throw new Error(`Specialist not found in config/specialists/: ${name}`);
20469
20607
  }
20470
20608
  return path;
@@ -20693,13 +20831,47 @@ var init_format_helpers = __esm(() => {
20693
20831
  };
20694
20832
  });
20695
20833
 
20834
+ // src/cli/tmux-utils.ts
20835
+ import { spawnSync as spawnSync8 } from "node:child_process";
20836
+ function escapeForSingleQuotedBash(script) {
20837
+ return script.replace(/'/g, "'\\''");
20838
+ }
20839
+ function quoteShellValue(value) {
20840
+ return `'${escapeForSingleQuotedBash(value)}'`;
20841
+ }
20842
+ function isTmuxAvailable() {
20843
+ return spawnSync8("which", ["tmux"], { encoding: "utf8", timeout: 2000 }).status === 0;
20844
+ }
20845
+ function buildSessionName(specialist, suffix) {
20846
+ return `${TMUX_SESSION_PREFIX}-${specialist}-${suffix}`;
20847
+ }
20848
+ function createTmuxSession(name, cwd, cmd, extraEnv = {}) {
20849
+ const exports = [
20850
+ "unset CLAUDECODE CLAUDE_CODE_SSE_PORT CLAUDE_CODE_ENTRYPOINT",
20851
+ `export SPECIALISTS_TMUX_SESSION=${quoteShellValue(name)}`
20852
+ ];
20853
+ for (const [key, value] of Object.entries(extraEnv)) {
20854
+ exports.push(`export ${key}=${quoteShellValue(value)}`);
20855
+ }
20856
+ const startupScript = `${exports.join("; ")}; exec ${cmd}`;
20857
+ const wrappedCommand = `/bin/bash -c '${escapeForSingleQuotedBash(startupScript)}'`;
20858
+ const result = spawnSync8("tmux", ["new-session", "-d", "-s", name, "-c", cwd, wrappedCommand], { encoding: "utf8", stdio: "pipe" });
20859
+ if (result.status !== 0) {
20860
+ const errorOutput = (result.stderr ?? "").trim() || (result.error?.message ?? "unknown error");
20861
+ throw new Error(`Failed to create tmux session "${name}": ${errorOutput}`);
20862
+ }
20863
+ }
20864
+ var TMUX_SESSION_PREFIX = "sp";
20865
+ var init_tmux_utils = () => {};
20866
+
20696
20867
  // src/cli/run.ts
20697
20868
  var exports_run = {};
20698
20869
  __export(exports_run, {
20699
20870
  run: () => run9
20700
20871
  });
20701
- import { join as join17 } from "node:path";
20702
- import { readFileSync as readFileSync7 } from "node:fs";
20872
+ import { join as join18 } from "node:path";
20873
+ import { readFileSync as readFileSync8 } from "node:fs";
20874
+ import { randomBytes } from "node:crypto";
20703
20875
  import { spawn as cpSpawn } from "node:child_process";
20704
20876
  async function parseArgs6(argv) {
20705
20877
  const name = argv[0];
@@ -20787,13 +20959,13 @@ async function parseArgs6(argv) {
20787
20959
  return { name, prompt, beadId, model, noBeads, noBeadNotes, keepAlive, noKeepAlive, background, contextDepth, outputMode };
20788
20960
  }
20789
20961
  function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
20790
- const eventsPath = join17(jobsDir, jobId, "events.jsonl");
20962
+ const eventsPath = join18(jobsDir, jobId, "events.jsonl");
20791
20963
  let linesRead = 0;
20792
20964
  let activeInlinePhase = null;
20793
20965
  const drain = () => {
20794
20966
  let content;
20795
20967
  try {
20796
- content = readFileSync7(eventsPath, "utf-8");
20968
+ content = readFileSync8(eventsPath, "utf-8");
20797
20969
  } catch {
20798
20970
  return;
20799
20971
  }
@@ -20849,31 +21021,44 @@ function formatFooterModel(backend, model) {
20849
21021
  return model;
20850
21022
  return model.startsWith(`${backend}/`) ? model : `${backend}/${model}`;
20851
21023
  }
21024
+ function shellQuote(value) {
21025
+ return `'${value.replace(/'/g, `'\\''`)}'`;
21026
+ }
20852
21027
  async function run9() {
20853
21028
  const args = await parseArgs6(process.argv.slice(3));
20854
21029
  if (args.background) {
20855
- const latestPath = join17(process.cwd(), ".specialists", "jobs", "latest");
21030
+ const latestPath = join18(process.cwd(), ".specialists", "jobs", "latest");
20856
21031
  const oldLatest = (() => {
20857
21032
  try {
20858
- return readFileSync7(latestPath, "utf-8").trim();
21033
+ return readFileSync8(latestPath, "utf-8").trim();
20859
21034
  } catch {
20860
21035
  return "";
20861
21036
  }
20862
21037
  })();
20863
- const childArgs = process.argv.slice(2).filter((a) => a !== "--background");
20864
- const child = cpSpawn(process.execPath, [process.argv[1], ...childArgs], {
20865
- detached: true,
20866
- stdio: "ignore",
20867
- cwd: process.cwd(),
20868
- env: process.env
20869
- });
20870
- child.unref();
21038
+ const cwd = process.cwd();
21039
+ const innerArgs = process.argv.slice(2).filter((a) => a !== "--background");
21040
+ const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(shellQuote).join(" ")}`;
21041
+ let childPid;
21042
+ if (isTmuxAvailable()) {
21043
+ const suffix = randomBytes(3).toString("hex");
21044
+ const sessionName = buildSessionName(args.name, suffix);
21045
+ createTmuxSession(sessionName, cwd, cmd);
21046
+ } else {
21047
+ const child = cpSpawn(process.execPath, [process.argv[1], ...innerArgs], {
21048
+ detached: true,
21049
+ stdio: "ignore",
21050
+ cwd,
21051
+ env: process.env
21052
+ });
21053
+ child.unref();
21054
+ childPid = child.pid;
21055
+ }
20871
21056
  const deadline = Date.now() + 5000;
20872
21057
  let jobId2 = "";
20873
21058
  while (Date.now() < deadline) {
20874
21059
  await new Promise((r) => setTimeout(r, 100));
20875
21060
  try {
20876
- const current = readFileSync7(latestPath, "utf-8").trim();
21061
+ const current = readFileSync8(latestPath, "utf-8").trim();
20877
21062
  if (current && current !== oldLatest) {
20878
21063
  jobId2 = current;
20879
21064
  break;
@@ -20886,14 +21071,14 @@ async function run9() {
20886
21071
  } else {
20887
21072
  process.stderr.write(`Warning: job started but ID not yet available. Check specialists status.
20888
21073
  `);
20889
- process.stdout.write(`${child.pid}
21074
+ process.stdout.write(`${childPid ?? ""}
20890
21075
  `);
20891
21076
  }
20892
21077
  process.exit(0);
20893
21078
  }
20894
21079
  const loader = new SpecialistLoader;
20895
21080
  const circuitBreaker = new CircuitBreaker;
20896
- const hooks = new HookEmitter({ tracePath: join17(process.cwd(), ".specialists", "trace.jsonl") });
21081
+ const hooks = new HookEmitter({ tracePath: join18(process.cwd(), ".specialists", "trace.jsonl") });
20897
21082
  const beadsClient = args.noBeads ? undefined : new BeadsClient;
20898
21083
  const beadReader = beadsClient ?? new BeadsClient;
20899
21084
  let prompt = args.prompt;
@@ -20928,7 +21113,7 @@ async function run9() {
20928
21113
  beadsClient
20929
21114
  });
20930
21115
  const beadsWriteNotes = args.noBeadNotes ? false : specialist.specialist.beads_write_notes ?? true;
20931
- const jobsDir = join17(process.cwd(), ".specialists", "jobs");
21116
+ const jobsDir = join18(process.cwd(), ".specialists", "jobs");
20932
21117
  let stopTailer;
20933
21118
  const supervisor = new Supervisor({
20934
21119
  runner,
@@ -20999,6 +21184,7 @@ var init_run = __esm(() => {
20999
21184
  init_beads();
21000
21185
  init_supervisor();
21001
21186
  init_format_helpers();
21187
+ init_tmux_utils();
21002
21188
  });
21003
21189
 
21004
21190
  // src/cli/status.ts
@@ -21006,9 +21192,9 @@ var exports_status = {};
21006
21192
  __export(exports_status, {
21007
21193
  run: () => run10
21008
21194
  });
21009
- import { spawnSync as spawnSync7 } from "node:child_process";
21010
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
21011
- import { join as join18 } from "node:path";
21195
+ import { spawnSync as spawnSync9 } from "node:child_process";
21196
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
21197
+ import { join as join19 } from "node:path";
21012
21198
  function ok2(msg) {
21013
21199
  console.log(` ${green7("✓")} ${msg}`);
21014
21200
  }
@@ -21027,7 +21213,7 @@ function section(label) {
21027
21213
  ${bold7(`── ${label} ${line}`)}`);
21028
21214
  }
21029
21215
  function cmd(bin, args) {
21030
- const r = spawnSync7(bin, args, {
21216
+ const r = spawnSync9(bin, args, {
21031
21217
  encoding: "utf8",
21032
21218
  stdio: "pipe",
21033
21219
  timeout: 5000
@@ -21035,7 +21221,7 @@ function cmd(bin, args) {
21035
21221
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
21036
21222
  }
21037
21223
  function isInstalled(bin) {
21038
- return spawnSync7("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21224
+ return spawnSync9("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
21039
21225
  }
21040
21226
  function formatElapsed2(s) {
21041
21227
  if (s.elapsed_s === undefined)
@@ -21087,10 +21273,10 @@ function parseStatusArgs(argv) {
21087
21273
  return { jsonMode, jobId };
21088
21274
  }
21089
21275
  function countJobEvents(jobsDir, jobId) {
21090
- const eventsFile = join18(jobsDir, jobId, "events.jsonl");
21091
- if (!existsSync13(eventsFile))
21276
+ const eventsFile = join19(jobsDir, jobId, "events.jsonl");
21277
+ if (!existsSync14(eventsFile))
21092
21278
  return 0;
21093
- const raw = readFileSync8(eventsFile, "utf-8").trim();
21279
+ const raw = readFileSync9(eventsFile, "utf-8").trim();
21094
21280
  if (!raw)
21095
21281
  return 0;
21096
21282
  return raw.split(`
@@ -21133,12 +21319,12 @@ async function run10() {
21133
21319
  `).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
21134
21320
  const bdInstalled = isInstalled("bd");
21135
21321
  const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
21136
- const beadsPresent = existsSync13(join18(process.cwd(), ".beads"));
21322
+ const beadsPresent = existsSync14(join19(process.cwd(), ".beads"));
21137
21323
  const specialistsBin = cmd("which", ["specialists"]);
21138
- const jobsDir = join18(process.cwd(), ".specialists", "jobs");
21324
+ const jobsDir = join19(process.cwd(), ".specialists", "jobs");
21139
21325
  let jobs = [];
21140
21326
  let supervisor = null;
21141
- if (existsSync13(jobsDir)) {
21327
+ if (existsSync14(jobsDir)) {
21142
21328
  supervisor = new Supervisor({
21143
21329
  runner: null,
21144
21330
  runOptions: null,
@@ -21283,8 +21469,8 @@ var exports_result = {};
21283
21469
  __export(exports_result, {
21284
21470
  run: () => run11
21285
21471
  });
21286
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
21287
- import { join as join19 } from "node:path";
21472
+ import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
21473
+ import { join as join20 } from "node:path";
21288
21474
  function parseArgs7(argv) {
21289
21475
  const jobId = argv[0];
21290
21476
  if (!jobId || jobId.startsWith("--")) {
@@ -21314,9 +21500,9 @@ function parseArgs7(argv) {
21314
21500
  async function run11() {
21315
21501
  const args = parseArgs7(process.argv.slice(3));
21316
21502
  const { jobId } = args;
21317
- const jobsDir = join19(process.cwd(), ".specialists", "jobs");
21503
+ const jobsDir = join20(process.cwd(), ".specialists", "jobs");
21318
21504
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21319
- const resultPath = join19(jobsDir, jobId, "result.txt");
21505
+ const resultPath = join20(jobsDir, jobId, "result.txt");
21320
21506
  if (args.wait) {
21321
21507
  const startMs = Date.now();
21322
21508
  while (true) {
@@ -21326,11 +21512,11 @@ async function run11() {
21326
21512
  process.exit(1);
21327
21513
  }
21328
21514
  if (status2.status === "done") {
21329
- if (!existsSync14(resultPath)) {
21515
+ if (!existsSync15(resultPath)) {
21330
21516
  console.error(`Result file not found for job ${jobId}`);
21331
21517
  process.exit(1);
21332
21518
  }
21333
- process.stdout.write(readFileSync9(resultPath, "utf-8"));
21519
+ process.stdout.write(readFileSync10(resultPath, "utf-8"));
21334
21520
  return;
21335
21521
  }
21336
21522
  if (status2.status === "error") {
@@ -21355,14 +21541,14 @@ async function run11() {
21355
21541
  process.exit(1);
21356
21542
  }
21357
21543
  if (status.status === "running" || status.status === "starting") {
21358
- if (!existsSync14(resultPath)) {
21544
+ if (!existsSync15(resultPath)) {
21359
21545
  process.stderr.write(`${dim9(`Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`)}
21360
21546
  `);
21361
21547
  process.exit(1);
21362
21548
  }
21363
21549
  process.stderr.write(`${dim9(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
21364
21550
  `);
21365
- process.stdout.write(readFileSync9(resultPath, "utf-8"));
21551
+ process.stdout.write(readFileSync10(resultPath, "utf-8"));
21366
21552
  return;
21367
21553
  }
21368
21554
  if (status.status === "error") {
@@ -21370,11 +21556,11 @@ async function run11() {
21370
21556
  `);
21371
21557
  process.exit(1);
21372
21558
  }
21373
- if (!existsSync14(resultPath)) {
21559
+ if (!existsSync15(resultPath)) {
21374
21560
  console.error(`Result file not found for job ${jobId}`);
21375
21561
  process.exit(1);
21376
21562
  }
21377
- process.stdout.write(readFileSync9(resultPath, "utf-8"));
21563
+ process.stdout.write(readFileSync10(resultPath, "utf-8"));
21378
21564
  }
21379
21565
  var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
21380
21566
  var init_result = __esm(() => {
@@ -21386,8 +21572,8 @@ var exports_feed = {};
21386
21572
  __export(exports_feed, {
21387
21573
  run: () => run12
21388
21574
  });
21389
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
21390
- import { join as join20 } from "node:path";
21575
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
21576
+ import { join as join21 } from "node:path";
21391
21577
  function getHumanEventKey(event) {
21392
21578
  switch (event.type) {
21393
21579
  case "meta":
@@ -21451,9 +21637,9 @@ function parseSince(value) {
21451
21637
  return;
21452
21638
  }
21453
21639
  function isTerminalJobStatus(jobsDir, jobId) {
21454
- const statusPath = join20(jobsDir, jobId, "status.json");
21640
+ const statusPath = join21(jobsDir, jobId, "status.json");
21455
21641
  try {
21456
- const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21642
+ const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21457
21643
  return status.status === "done" || status.status === "error";
21458
21644
  } catch {
21459
21645
  return false;
@@ -21464,10 +21650,10 @@ function makeJobMetaReader(jobsDir) {
21464
21650
  return (jobId) => {
21465
21651
  if (cache.has(jobId))
21466
21652
  return cache.get(jobId);
21467
- const statusPath = join20(jobsDir, jobId, "status.json");
21653
+ const statusPath = join21(jobsDir, jobId, "status.json");
21468
21654
  let meta = { startedAtMs: Date.now() };
21469
21655
  try {
21470
- const status = JSON.parse(readFileSync10(statusPath, "utf-8"));
21656
+ const status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21471
21657
  meta = {
21472
21658
  model: status.model,
21473
21659
  backend: status.backend,
@@ -21659,8 +21845,8 @@ async function followMerged(jobsDir, options) {
21659
21845
  }
21660
21846
  async function run12() {
21661
21847
  const options = parseArgs8(process.argv.slice(3));
21662
- const jobsDir = join20(process.cwd(), ".specialists", "jobs");
21663
- if (!existsSync15(jobsDir)) {
21848
+ const jobsDir = join21(process.cwd(), ".specialists", "jobs");
21849
+ if (!existsSync16(jobsDir)) {
21664
21850
  console.log(dim7("No jobs directory found."));
21665
21851
  return;
21666
21852
  }
@@ -21687,8 +21873,8 @@ var exports_poll = {};
21687
21873
  __export(exports_poll, {
21688
21874
  run: () => run13
21689
21875
  });
21690
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
21691
- import { join as join21 } from "node:path";
21876
+ import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
21877
+ import { join as join22 } from "node:path";
21692
21878
  function parseArgs9(argv) {
21693
21879
  let jobId;
21694
21880
  let cursor = 0;
@@ -21725,19 +21911,19 @@ function parseArgs9(argv) {
21725
21911
  return { jobId, cursor, outputCursor };
21726
21912
  }
21727
21913
  function readJobState(jobsDir, jobId, cursor, outputCursor) {
21728
- const jobDir = join21(jobsDir, jobId);
21729
- const statusPath = join21(jobDir, "status.json");
21914
+ const jobDir = join22(jobsDir, jobId);
21915
+ const statusPath = join22(jobDir, "status.json");
21730
21916
  let status = null;
21731
- if (existsSync16(statusPath)) {
21917
+ if (existsSync17(statusPath)) {
21732
21918
  try {
21733
- status = JSON.parse(readFileSync11(statusPath, "utf-8"));
21919
+ status = JSON.parse(readFileSync12(statusPath, "utf-8"));
21734
21920
  } catch {}
21735
21921
  }
21736
- const resultPath = join21(jobDir, "result.txt");
21922
+ const resultPath = join22(jobDir, "result.txt");
21737
21923
  let fullOutput = "";
21738
- if (existsSync16(resultPath)) {
21924
+ if (existsSync17(resultPath)) {
21739
21925
  try {
21740
- fullOutput = readFileSync11(resultPath, "utf-8");
21926
+ fullOutput = readFileSync12(resultPath, "utf-8");
21741
21927
  } catch {}
21742
21928
  }
21743
21929
  const events = readJobEventsById(jobsDir, jobId);
@@ -21769,9 +21955,9 @@ function readJobState(jobsDir, jobId, cursor, outputCursor) {
21769
21955
  }
21770
21956
  async function run13() {
21771
21957
  const { jobId, cursor, outputCursor } = parseArgs9(process.argv.slice(3));
21772
- const jobsDir = join21(process.cwd(), ".specialists", "jobs");
21773
- const jobDir = join21(jobsDir, jobId);
21774
- if (!existsSync16(jobDir)) {
21958
+ const jobsDir = join22(process.cwd(), ".specialists", "jobs");
21959
+ const jobDir = join22(jobsDir, jobId);
21960
+ if (!existsSync17(jobDir)) {
21775
21961
  const result2 = {
21776
21962
  job_id: jobId,
21777
21963
  status: "error",
@@ -21802,7 +21988,7 @@ var exports_steer = {};
21802
21988
  __export(exports_steer, {
21803
21989
  run: () => run14
21804
21990
  });
21805
- import { join as join22 } from "node:path";
21991
+ import { join as join23 } from "node:path";
21806
21992
  import { writeFileSync as writeFileSync6 } from "node:fs";
21807
21993
  async function run14() {
21808
21994
  const jobId = process.argv[3];
@@ -21811,7 +21997,7 @@ async function run14() {
21811
21997
  console.error('Usage: specialists|sp steer <job-id> "<message>"');
21812
21998
  process.exit(1);
21813
21999
  }
21814
- const jobsDir = join22(process.cwd(), ".specialists", "jobs");
22000
+ const jobsDir = join23(process.cwd(), ".specialists", "jobs");
21815
22001
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21816
22002
  const status = supervisor.readStatus(jobId);
21817
22003
  if (!status) {
@@ -21852,7 +22038,7 @@ var exports_resume = {};
21852
22038
  __export(exports_resume, {
21853
22039
  run: () => run15
21854
22040
  });
21855
- import { join as join23 } from "node:path";
22041
+ import { join as join24 } from "node:path";
21856
22042
  import { writeFileSync as writeFileSync7 } from "node:fs";
21857
22043
  async function run15() {
21858
22044
  const jobId = process.argv[3];
@@ -21861,7 +22047,7 @@ async function run15() {
21861
22047
  console.error('Usage: specialists|sp resume <job-id> "<task>"');
21862
22048
  process.exit(1);
21863
22049
  }
21864
- const jobsDir = join23(process.cwd(), ".specialists", "jobs");
22050
+ const jobsDir = join24(process.cwd(), ".specialists", "jobs");
21865
22051
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
21866
22052
  const status = supervisor.readStatus(jobId);
21867
22053
  if (!status) {
@@ -21916,13 +22102,13 @@ __export(exports_clean, {
21916
22102
  run: () => run17
21917
22103
  });
21918
22104
  import {
21919
- existsSync as existsSync17,
21920
- readdirSync as readdirSync5,
21921
- readFileSync as readFileSync12,
22105
+ existsSync as existsSync18,
22106
+ readdirSync as readdirSync6,
22107
+ readFileSync as readFileSync13,
21922
22108
  rmSync as rmSync2,
21923
22109
  statSync as statSync2
21924
22110
  } from "node:fs";
21925
- import { join as join24 } from "node:path";
22111
+ import { join as join25 } from "node:path";
21926
22112
  function parseTtlDaysFromEnvironment() {
21927
22113
  const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
21928
22114
  if (!rawValue)
@@ -21976,9 +22162,9 @@ function parseOptions(argv) {
21976
22162
  }
21977
22163
  function readDirectorySizeBytes(directoryPath) {
21978
22164
  let totalBytes = 0;
21979
- const entries = readdirSync5(directoryPath, { withFileTypes: true });
22165
+ const entries = readdirSync6(directoryPath, { withFileTypes: true });
21980
22166
  for (const entry of entries) {
21981
- const entryPath = join24(directoryPath, entry.name);
22167
+ const entryPath = join25(directoryPath, entry.name);
21982
22168
  const stats = statSync2(entryPath);
21983
22169
  if (stats.isDirectory()) {
21984
22170
  totalBytes += readDirectorySizeBytes(entryPath);
@@ -21991,13 +22177,13 @@ function readDirectorySizeBytes(directoryPath) {
21991
22177
  function readCompletedJobDirectory(baseDirectory, entry) {
21992
22178
  if (!entry.isDirectory())
21993
22179
  return null;
21994
- const directoryPath = join24(baseDirectory, entry.name);
21995
- const statusFilePath = join24(directoryPath, "status.json");
21996
- if (!existsSync17(statusFilePath))
22180
+ const directoryPath = join25(baseDirectory, entry.name);
22181
+ const statusFilePath = join25(directoryPath, "status.json");
22182
+ if (!existsSync18(statusFilePath))
21997
22183
  return null;
21998
22184
  let statusData;
21999
22185
  try {
22000
- statusData = JSON.parse(readFileSync12(statusFilePath, "utf-8"));
22186
+ statusData = JSON.parse(readFileSync13(statusFilePath, "utf-8"));
22001
22187
  } catch {
22002
22188
  return null;
22003
22189
  }
@@ -22013,7 +22199,7 @@ function readCompletedJobDirectory(baseDirectory, entry) {
22013
22199
  };
22014
22200
  }
22015
22201
  function collectCompletedJobDirectories(jobsDirectoryPath) {
22016
- const entries = readdirSync5(jobsDirectoryPath, { withFileTypes: true });
22202
+ const entries = readdirSync6(jobsDirectoryPath, { withFileTypes: true });
22017
22203
  const completedJobs = [];
22018
22204
  for (const entry of entries) {
22019
22205
  const completedJob = readCompletedJobDirectory(jobsDirectoryPath, entry);
@@ -22078,8 +22264,8 @@ async function run17() {
22078
22264
  const message = error2 instanceof Error ? error2.message : String(error2);
22079
22265
  printUsageAndExit(message);
22080
22266
  }
22081
- const jobsDirectoryPath = join24(process.cwd(), ".specialists", "jobs");
22082
- if (!existsSync17(jobsDirectoryPath)) {
22267
+ const jobsDirectoryPath = join25(process.cwd(), ".specialists", "jobs");
22268
+ if (!existsSync18(jobsDirectoryPath)) {
22083
22269
  console.log("No jobs directory found.");
22084
22270
  return;
22085
22271
  }
@@ -22106,14 +22292,14 @@ var exports_stop = {};
22106
22292
  __export(exports_stop, {
22107
22293
  run: () => run18
22108
22294
  });
22109
- import { join as join25 } from "node:path";
22295
+ import { join as join26 } from "node:path";
22110
22296
  async function run18() {
22111
22297
  const jobId = process.argv[3];
22112
22298
  if (!jobId) {
22113
22299
  console.error("Usage: specialists|sp stop <job-id>");
22114
22300
  process.exit(1);
22115
22301
  }
22116
- const jobsDir = join25(process.cwd(), ".specialists", "jobs");
22302
+ const jobsDir = join26(process.cwd(), ".specialists", "jobs");
22117
22303
  const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
22118
22304
  const status = supervisor.readStatus(jobId);
22119
22305
  if (!status) {
@@ -22150,10 +22336,60 @@ var init_stop = __esm(() => {
22150
22336
  init_supervisor();
22151
22337
  });
22152
22338
 
22339
+ // src/cli/attach.ts
22340
+ var exports_attach = {};
22341
+ __export(exports_attach, {
22342
+ run: () => run19
22343
+ });
22344
+ import { execFileSync as execFileSync2, spawnSync as spawnSync10 } from "node:child_process";
22345
+ import { readFileSync as readFileSync14 } from "node:fs";
22346
+ import { join as join27 } from "node:path";
22347
+ function exitWithError(message) {
22348
+ console.error(message);
22349
+ process.exit(1);
22350
+ }
22351
+ function readStatus(statusPath, jobId) {
22352
+ try {
22353
+ return JSON.parse(readFileSync14(statusPath, "utf-8"));
22354
+ } catch (error2) {
22355
+ if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
22356
+ exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs.`);
22357
+ }
22358
+ const details = error2 instanceof Error ? error2.message : String(error2);
22359
+ exitWithError(`Failed to read status for job \`${jobId}\`: ${details}`);
22360
+ }
22361
+ }
22362
+ async function run19() {
22363
+ const [jobId] = process.argv.slice(3);
22364
+ if (!jobId) {
22365
+ exitWithError("Usage: specialists attach <job-id>");
22366
+ }
22367
+ const jobsDir = join27(process.cwd(), ".specialists", "jobs");
22368
+ const statusPath = join27(jobsDir, jobId, "status.json");
22369
+ const status = readStatus(statusPath, jobId);
22370
+ if (status.status === "done" || status.status === "error") {
22371
+ exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
22372
+ }
22373
+ const sessionName = status.tmux_session?.trim();
22374
+ if (!sessionName) {
22375
+ exitWithError("Job `" + jobId + "` has no tmux session. It may have been started without tmux or tmux was not installed.");
22376
+ }
22377
+ const whichTmux = spawnSync10("which", ["tmux"], { stdio: "ignore" });
22378
+ if (whichTmux.status !== 0) {
22379
+ exitWithError("tmux is not installed. Install tmux to use `specialists attach`.");
22380
+ }
22381
+ try {
22382
+ execFileSync2("tmux", ["attach-session", "-t", sessionName], { stdio: "inherit" });
22383
+ } catch {
22384
+ process.exit(1);
22385
+ }
22386
+ }
22387
+ var init_attach = () => {};
22388
+
22153
22389
  // src/cli/quickstart.ts
22154
22390
  var exports_quickstart = {};
22155
22391
  __export(exports_quickstart, {
22156
- run: () => run19
22392
+ run: () => run20
22157
22393
  });
22158
22394
  function section2(title) {
22159
22395
  const bar = "─".repeat(60);
@@ -22167,7 +22403,7 @@ function cmd2(s) {
22167
22403
  function flag(s) {
22168
22404
  return green12(s);
22169
22405
  }
22170
- async function run19() {
22406
+ async function run20() {
22171
22407
  const lines = [
22172
22408
  "",
22173
22409
  bold9("specialists · Quick Start Guide"),
@@ -22383,11 +22619,11 @@ var bold9 = (s) => `\x1B[1m${s}\x1B[0m`, dim11 = (s) => `\x1B[2m${s}\x1B[0m`, ye
22383
22619
  // src/cli/doctor.ts
22384
22620
  var exports_doctor = {};
22385
22621
  __export(exports_doctor, {
22386
- run: () => run20
22622
+ run: () => run21
22387
22623
  });
22388
- import { spawnSync as spawnSync8 } from "node:child_process";
22389
- import { existsSync as existsSync18, mkdirSync as mkdirSync3, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "node:fs";
22390
- import { join as join26 } from "node:path";
22624
+ import { spawnSync as spawnSync11 } from "node:child_process";
22625
+ import { existsSync as existsSync19, mkdirSync as mkdirSync3, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "node:fs";
22626
+ import { join as join28 } from "node:path";
22391
22627
  function ok3(msg) {
22392
22628
  console.log(` ${green13("✓")} ${msg}`);
22393
22629
  }
@@ -22409,17 +22645,17 @@ function section3(label) {
22409
22645
  ${bold10(`── ${label} ${line}`)}`);
22410
22646
  }
22411
22647
  function sp(bin, args) {
22412
- const r = spawnSync8(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22648
+ const r = spawnSync11(bin, args, { encoding: "utf8", stdio: "pipe", timeout: 5000 });
22413
22649
  return { ok: r.status === 0 && !r.error, stdout: (r.stdout ?? "").trim() };
22414
22650
  }
22415
22651
  function isInstalled2(bin) {
22416
- return spawnSync8("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22652
+ return spawnSync11("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
22417
22653
  }
22418
22654
  function loadJson2(path) {
22419
- if (!existsSync18(path))
22655
+ if (!existsSync19(path))
22420
22656
  return null;
22421
22657
  try {
22422
- return JSON.parse(readFileSync13(path, "utf8"));
22658
+ return JSON.parse(readFileSync15(path, "utf8"));
22423
22659
  } catch {
22424
22660
  return null;
22425
22661
  }
@@ -22462,7 +22698,7 @@ function checkBd() {
22462
22698
  return false;
22463
22699
  }
22464
22700
  ok3(`bd installed ${dim12(sp("bd", ["--version"]).stdout || "")}`);
22465
- if (existsSync18(join26(CWD, ".beads")))
22701
+ if (existsSync19(join28(CWD, ".beads")))
22466
22702
  ok3(".beads/ present in project");
22467
22703
  else
22468
22704
  warn2(".beads/ not found in project");
@@ -22482,8 +22718,8 @@ function checkHooks() {
22482
22718
  section3("Claude Code hooks (2 expected)");
22483
22719
  let allPresent = true;
22484
22720
  for (const name of HOOK_NAMES) {
22485
- const dest = join26(HOOKS_DIR, name);
22486
- if (!existsSync18(dest)) {
22721
+ const dest = join28(HOOKS_DIR, name);
22722
+ if (!existsSync19(dest)) {
22487
22723
  fail2(`${name} ${red7("missing")}`);
22488
22724
  fix("specialists install");
22489
22725
  allPresent = false;
@@ -22527,18 +22763,18 @@ function checkMCP() {
22527
22763
  }
22528
22764
  function checkRuntimeDirs() {
22529
22765
  section3(".specialists/ runtime directories");
22530
- const rootDir = join26(CWD, ".specialists");
22531
- const jobsDir = join26(rootDir, "jobs");
22532
- const readyDir = join26(rootDir, "ready");
22766
+ const rootDir = join28(CWD, ".specialists");
22767
+ const jobsDir = join28(rootDir, "jobs");
22768
+ const readyDir = join28(rootDir, "ready");
22533
22769
  let allOk = true;
22534
- if (!existsSync18(rootDir)) {
22770
+ if (!existsSync19(rootDir)) {
22535
22771
  warn2(".specialists/ not found in current project");
22536
22772
  fix("specialists init");
22537
22773
  allOk = false;
22538
22774
  } else {
22539
22775
  ok3(".specialists/ present");
22540
22776
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
22541
- if (!existsSync18(subDir)) {
22777
+ if (!existsSync19(subDir)) {
22542
22778
  warn2(`.specialists/${label}/ missing — auto-creating`);
22543
22779
  mkdirSync3(subDir, { recursive: true });
22544
22780
  ok3(`.specialists/${label}/ created`);
@@ -22551,14 +22787,14 @@ function checkRuntimeDirs() {
22551
22787
  }
22552
22788
  function checkZombieJobs() {
22553
22789
  section3("Background jobs");
22554
- const jobsDir = join26(CWD, ".specialists", "jobs");
22555
- if (!existsSync18(jobsDir)) {
22790
+ const jobsDir = join28(CWD, ".specialists", "jobs");
22791
+ if (!existsSync19(jobsDir)) {
22556
22792
  hint("No .specialists/jobs/ — skipping");
22557
22793
  return true;
22558
22794
  }
22559
22795
  let entries;
22560
22796
  try {
22561
- entries = readdirSync6(jobsDir);
22797
+ entries = readdirSync7(jobsDir);
22562
22798
  } catch {
22563
22799
  entries = [];
22564
22800
  }
@@ -22570,11 +22806,11 @@ function checkZombieJobs() {
22570
22806
  let total = 0;
22571
22807
  let running = 0;
22572
22808
  for (const jobId of entries) {
22573
- const statusPath = join26(jobsDir, jobId, "status.json");
22574
- if (!existsSync18(statusPath))
22809
+ const statusPath = join28(jobsDir, jobId, "status.json");
22810
+ if (!existsSync19(statusPath))
22575
22811
  continue;
22576
22812
  try {
22577
- const status = JSON.parse(readFileSync13(statusPath, "utf8"));
22813
+ const status = JSON.parse(readFileSync15(statusPath, "utf8"));
22578
22814
  total++;
22579
22815
  if (status.status === "running" || status.status === "starting") {
22580
22816
  const pid = status.pid;
@@ -22601,7 +22837,7 @@ function checkZombieJobs() {
22601
22837
  }
22602
22838
  return zombies === 0;
22603
22839
  }
22604
- async function run20() {
22840
+ async function run21() {
22605
22841
  console.log(`
22606
22842
  ${bold10("specialists doctor")}
22607
22843
  `);
@@ -22626,11 +22862,11 @@ ${bold10("specialists doctor")}
22626
22862
  var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, green13 = (s) => `\x1B[32m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, SPECIALISTS_DIR, HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
22627
22863
  var init_doctor = __esm(() => {
22628
22864
  CWD = process.cwd();
22629
- CLAUDE_DIR = join26(CWD, ".claude");
22630
- SPECIALISTS_DIR = join26(CWD, ".specialists");
22631
- HOOKS_DIR = join26(SPECIALISTS_DIR, "default", "hooks");
22632
- SETTINGS_FILE = join26(CLAUDE_DIR, "settings.json");
22633
- MCP_FILE2 = join26(CWD, ".mcp.json");
22865
+ CLAUDE_DIR = join28(CWD, ".claude");
22866
+ SPECIALISTS_DIR = join28(CWD, ".specialists");
22867
+ HOOKS_DIR = join28(SPECIALISTS_DIR, "default", "hooks");
22868
+ SETTINGS_FILE = join28(CLAUDE_DIR, "settings.json");
22869
+ MCP_FILE2 = join28(CWD, ".mcp.json");
22634
22870
  HOOK_NAMES = [
22635
22871
  "specialists-complete.mjs",
22636
22872
  "specialists-session-start.mjs"
@@ -22640,9 +22876,9 @@ var init_doctor = __esm(() => {
22640
22876
  // src/cli/setup.ts
22641
22877
  var exports_setup = {};
22642
22878
  __export(exports_setup, {
22643
- run: () => run21
22879
+ run: () => run22
22644
22880
  });
22645
- async function run21() {
22881
+ async function run22() {
22646
22882
  console.log("");
22647
22883
  console.log(yellow11("⚠ DEPRECATED: `specialists setup` is deprecated"));
22648
22884
  console.log("");
@@ -22665,13 +22901,13 @@ var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, yellow11 = (s) => `\x1B[33m${s}\x1B[0m
22665
22901
  // src/cli/help.ts
22666
22902
  var exports_help = {};
22667
22903
  __export(exports_help, {
22668
- run: () => run22
22904
+ run: () => run23
22669
22905
  });
22670
22906
  function formatCommands(entries) {
22671
22907
  const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
22672
22908
  return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
22673
22909
  }
22674
- async function run22() {
22910
+ async function run23() {
22675
22911
  const lines = [
22676
22912
  "",
22677
22913
  "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
@@ -22710,6 +22946,12 @@ async function run22() {
22710
22946
  " specialists feed|poll|result <job-id> # observe/progress/final output",
22711
22947
  ' Shell: specialists run <name> --prompt "..." & # native shell backgrounding',
22712
22948
  "",
22949
+ " Background workflow",
22950
+ ' specialists run executor --background --prompt "..." # → prints job-id',
22951
+ " specialists list --live # → interactive session picker",
22952
+ " specialists attach <job-id> # → attach directly",
22953
+ " specialists feed <job-id> --follow # → structured event stream",
22954
+ "",
22713
22955
  bold12("Core commands:"),
22714
22956
  ...formatCommands(CORE_COMMANDS),
22715
22957
  "",
@@ -22729,6 +22971,7 @@ async function run22() {
22729
22971
  " specialists feed -f # stream all job events",
22730
22972
  ' specialists steer <job-id> "focus only on supervisor.ts"',
22731
22973
  ' specialists resume <job-id> "now write the fix"',
22974
+ " specialists attach <job-id> # open raw tmux session output",
22732
22975
  ' specialists run debugger --prompt "why does auth fail"',
22733
22976
  " specialists report list",
22734
22977
  " specialists report show --specialists",
@@ -22753,7 +22996,7 @@ var bold12 = (s) => `\x1B[1m${s}\x1B[0m`, dim14 = (s) => `\x1B[2m${s}\x1B[0m`, C
22753
22996
  var init_help = __esm(() => {
22754
22997
  CORE_COMMANDS = [
22755
22998
  ["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
22756
- ["list", "List specialists in this project"],
22999
+ ["list", "List specialists; --live for interactive tmux session picker"],
22757
23000
  ["validate", "Validate a specialist YAML against the schema"],
22758
23001
  ["config", "Batch get/set specialist YAML keys in config/specialists/"],
22759
23002
  ["run", "Run a specialist; --json for NDJSON event stream, --raw for legacy text"],
@@ -22765,6 +23008,7 @@ var init_help = __esm(() => {
22765
23008
  ["resume", "Resume a waiting keep-alive session with a next-turn prompt (retains full context)"],
22766
23009
  ["follow-up", "[deprecated] Use resume instead"],
22767
23010
  ["stop", "Stop a running job"],
23011
+ ["attach", "Attach terminal to a running background job tmux session"],
22768
23012
  ["report", "Generate/show/list/diff session reports in .xtrm/reports/"],
22769
23013
  ["status", "Show health, MCP state, and active jobs"],
22770
23014
  ["doctor", "Diagnose installation/runtime problems"],
@@ -30868,7 +31112,7 @@ var next = process.argv[3];
30868
31112
  function wantsHelp() {
30869
31113
  return next === "--help" || next === "-h";
30870
31114
  }
30871
- async function run23() {
31115
+ async function run24() {
30872
31116
  if (sub === "install") {
30873
31117
  if (wantsHelp()) {
30874
31118
  console.log([
@@ -30904,11 +31148,13 @@ async function run23() {
30904
31148
  "Options:",
30905
31149
  " --category <name> Filter by category tag",
30906
31150
  " --json Output as JSON array",
31151
+ " --live List running tmux-backed jobs and attach interactively",
30907
31152
  "",
30908
31153
  "Examples:",
30909
31154
  " specialists list",
30910
31155
  " specialists list --category analysis",
30911
31156
  " specialists list --json",
31157
+ " specialists list --live",
30912
31158
  "",
30913
31159
  "Project model:",
30914
31160
  " Specialists are project-only. User-scope discovery is deprecated.",
@@ -31360,6 +31606,34 @@ async function run23() {
31360
31606
  const { run: handler } = await Promise.resolve().then(() => (init_stop(), exports_stop));
31361
31607
  return handler();
31362
31608
  }
31609
+ if (sub === "attach") {
31610
+ if (wantsHelp()) {
31611
+ process.stdout.write([
31612
+ "Usage: specialists attach <job-id>",
31613
+ "",
31614
+ "Attach your terminal to the tmux session of a running background specialist job.",
31615
+ "The job must have been started with --background and tmux must be installed.",
31616
+ "",
31617
+ "Arguments:",
31618
+ " <job-id> The job ID returned by specialists run --background",
31619
+ "",
31620
+ "Exit codes:",
31621
+ " 0 — session attached and exited normally",
31622
+ " 1 — job not found, already done, or no tmux session",
31623
+ "",
31624
+ "Examples:",
31625
+ " specialists attach job_a1b2c3d4",
31626
+ ' specialists attach $(specialists run executor --background --prompt "...")',
31627
+ "",
31628
+ "See also: specialists list --live (interactive session picker)"
31629
+ ].join(`
31630
+ `) + `
31631
+ `);
31632
+ process.exit(0);
31633
+ }
31634
+ const { run: handler } = await Promise.resolve().then(() => (init_attach(), exports_attach));
31635
+ return handler();
31636
+ }
31363
31637
  if (sub === "quickstart") {
31364
31638
  const { run: handler } = await Promise.resolve().then(() => exports_quickstart);
31365
31639
  return handler();
@@ -31423,7 +31697,7 @@ Run 'specialists help' to see available commands.`);
31423
31697
  const server = new SpecialistsServer;
31424
31698
  await server.start();
31425
31699
  }
31426
- run23().catch((error2) => {
31700
+ run24().catch((error2) => {
31427
31701
  logger.error(`Fatal error: ${error2}`);
31428
31702
  process.exit(1);
31429
31703
  });