squadrant 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -434,7 +434,7 @@ var init_compat_manifest = __esm({
434
434
  // presence-checked; no floor enforced yet
435
435
  codex: { lastVerified: "0.139.0" },
436
436
  gemini: { lastVerified: "0.38.2" },
437
- opencode: { lastVerified: "1.17.4" }
437
+ opencode: { lastVerified: "1.17.9" }
438
438
  }
439
439
  };
440
440
  }
@@ -1043,10 +1043,10 @@ var init_watchdog = __esm({
1043
1043
  }
1044
1044
  });
1045
1045
 
1046
- // packages/core/dist/daemon.js
1046
+ // packages/core/dist/daemon/reduce.js
1047
1047
  var DEFAULT_TASK_TIMEOUT_MS, TERMINAL_RECORD_TTL_MS;
1048
- var init_daemon = __esm({
1049
- "packages/core/dist/daemon.js"() {
1048
+ var init_reduce = __esm({
1049
+ "packages/core/dist/daemon/reduce.js"() {
1050
1050
  init_dist();
1051
1051
  init_state_machine();
1052
1052
  init_watchdog();
@@ -1449,9 +1449,9 @@ var init_captain_delivery = __esm({
1449
1449
  }
1450
1450
  });
1451
1451
 
1452
- // packages/core/dist/daemon/delivery.js
1453
- var init_delivery = __esm({
1454
- "packages/core/dist/daemon/delivery.js"() {
1452
+ // packages/core/dist/daemon/delivery-loop.js
1453
+ var init_delivery_loop = __esm({
1454
+ "packages/core/dist/daemon/delivery-loop.js"() {
1455
1455
  init_mailbox();
1456
1456
  init_captain_delivery();
1457
1457
  init_dist();
@@ -1489,9 +1489,9 @@ import { readdir } from "fs/promises";
1489
1489
  var SNAPSHOT_LOG_WINDOW_MS;
1490
1490
  var init_start = __esm({
1491
1491
  "packages/core/dist/daemon/start.js"() {
1492
- init_daemon();
1492
+ init_reduce();
1493
1493
  init_probes();
1494
- init_delivery();
1494
+ init_delivery_loop();
1495
1495
  init_gates();
1496
1496
  init_server();
1497
1497
  init_mailbox();
@@ -1683,6 +1683,17 @@ var init_commands = __esm({
1683
1683
  }
1684
1684
  });
1685
1685
 
1686
+ // packages/core/dist/telegram/control.js
1687
+ import { execFile } from "child_process";
1688
+ import { promisify } from "util";
1689
+ var pExecFile;
1690
+ var init_control2 = __esm({
1691
+ "packages/core/dist/telegram/control.js"() {
1692
+ init_protocol();
1693
+ pExecFile = promisify(execFile);
1694
+ }
1695
+ });
1696
+
1686
1697
  // packages/core/dist/telegram/ensure-captain.js
1687
1698
  var init_ensure_captain = __esm({
1688
1699
  "packages/core/dist/telegram/ensure-captain.js"() {
@@ -1799,6 +1810,12 @@ function createTelegramClient(opts) {
1799
1810
  },
1800
1811
  async setMyCommands(commands) {
1801
1812
  await call("setMyCommands", { commands });
1813
+ },
1814
+ async sendChatAction(chatId, threadId, action) {
1815
+ const body = { chat_id: chatId, action };
1816
+ if (threadId !== void 0)
1817
+ body.message_thread_id = threadId;
1818
+ await call("sendChatAction", body);
1802
1819
  }
1803
1820
  };
1804
1821
  }
@@ -1843,6 +1860,46 @@ var init_bridge = __esm({
1843
1860
  }
1844
1861
  });
1845
1862
 
1863
+ // packages/core/dist/restart-daemon.js
1864
+ import { execFileSync as execFileSync4 } from "child_process";
1865
+ import { existsSync as existsSync8 } from "fs";
1866
+ import { homedir as homedir6 } from "os";
1867
+ import { join as join11 } from "path";
1868
+ function defaultIsRunning() {
1869
+ return existsSync8(DEFAULT_SOCK_PATH);
1870
+ }
1871
+ function defaultRunKickstart() {
1872
+ const uid = process.getuid?.() ?? 0;
1873
+ const target = `gui/${uid}/${LABEL}`;
1874
+ if (tryAcquireDaemonLock()) {
1875
+ try {
1876
+ execFileSync4("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
1877
+ } finally {
1878
+ releaseDaemonLock();
1879
+ }
1880
+ }
1881
+ }
1882
+ function restartDaemonIfRunning(opts) {
1883
+ const env = opts.env ?? process.env;
1884
+ if (env["VITEST"] || opts.noRestart)
1885
+ return "skipped-opt-out";
1886
+ const isRunning = opts.isRunning ?? defaultIsRunning;
1887
+ if (!isRunning())
1888
+ return "skipped-not-running";
1889
+ const log = opts.log ?? console.log;
1890
+ log(`\u21BB restarting daemon to apply ${opts.reason}\u2026`);
1891
+ const runKickstart = opts.runKickstart ?? defaultRunKickstart;
1892
+ runKickstart();
1893
+ return "restarted";
1894
+ }
1895
+ var DEFAULT_SOCK_PATH;
1896
+ var init_restart_daemon = __esm({
1897
+ "packages/core/dist/restart-daemon.js"() {
1898
+ init_launchd();
1899
+ DEFAULT_SOCK_PATH = join11(homedir6(), ".config", "squadrant", "squadrant.sock");
1900
+ }
1901
+ });
1902
+
1846
1903
  // packages/core/dist/telegram/setup.js
1847
1904
  import fs10 from "fs";
1848
1905
  function resolveSetupGroup(existingSupergroupId, opts) {
@@ -1868,6 +1925,26 @@ async function detectGroupAndUser(client, opts = {}) {
1868
1925
  }
1869
1926
  throw new Error("Timed out waiting for the bot to receive a message in a supergroup");
1870
1927
  }
1928
+ function resolveSetupToken(existingToken, opts) {
1929
+ if (opts.resetToken || !existingToken)
1930
+ return "prompt";
1931
+ return "try-reuse";
1932
+ }
1933
+ function resolveSetupUserId(flagUserId, detectedUserId, stateRoot) {
1934
+ return flagUserId ?? detectedUserId ?? loadState(stateRoot).lastUserId;
1935
+ }
1936
+ async function runRegisterCommands(opts) {
1937
+ await opts.client.setMyCommands(BOT_COMMANDS);
1938
+ }
1939
+ function runTelegramPostSetup(opts) {
1940
+ const doRestart = opts.doRestart ?? restartDaemonIfRunning;
1941
+ const outcome = doRestart({ reason: "telegram config" });
1942
+ if (outcome === "skipped-not-running") {
1943
+ console.log("(daemon not running \u2014 change applies on next start)");
1944
+ } else if (outcome === "skipped-opt-out") {
1945
+ console.log("(run 'squadrant heal daemon' to apply)");
1946
+ }
1947
+ }
1871
1948
  function writeTelegramConfig(configPath, opts) {
1872
1949
  let config;
1873
1950
  let raw = null;
@@ -1904,6 +1981,97 @@ function writeTelegramConfig(configPath, opts) {
1904
1981
  }
1905
1982
  var init_setup = __esm({
1906
1983
  "packages/core/dist/telegram/setup.js"() {
1984
+ init_state();
1985
+ init_bot_commands();
1986
+ init_restart_daemon();
1987
+ }
1988
+ });
1989
+
1990
+ // packages/core/dist/telegram/notify.js
1991
+ function runTelegramStatus(opts) {
1992
+ const tg = opts.config.telegram;
1993
+ const env = opts.env ?? process.env;
1994
+ const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
1995
+ const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
1996
+ const sep2 = key.indexOf("::");
1997
+ return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2), topicId };
1998
+ });
1999
+ return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
2000
+ }
2001
+ function runTelegramNotifySet(opts) {
2002
+ setNotify(opts.stateRoot, opts.project, opts.active);
2003
+ }
2004
+ function runTelegramNotifyPref(args) {
2005
+ const { project, dimension, value, root } = args;
2006
+ if (dimension === "crew") {
2007
+ if (!["all", "alert_only", "done_only", "none"].includes(value))
2008
+ return { ok: false, message: "crew must be all|alert_only|done_only|none" };
2009
+ saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
2010
+ return { ok: true };
2011
+ }
2012
+ if (value !== "on" && value !== "off")
2013
+ return { ok: false, message: "cap must be on|off" };
2014
+ saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
2015
+ return { ok: true };
2016
+ }
2017
+ function runTelegramNotifyStatus(opts) {
2018
+ const s = loadState(opts.stateRoot);
2019
+ const projects = /* @__PURE__ */ new Set();
2020
+ for (const key of Object.keys(s.topics)) {
2021
+ const sep2 = key.indexOf("::");
2022
+ projects.add(sep2 === -1 ? key : key.slice(0, sep2));
2023
+ }
2024
+ for (const p of Object.keys(s.notify))
2025
+ projects.add(p);
2026
+ return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
2027
+ }
2028
+ function capAllowed(project, globalNotify, root) {
2029
+ return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
2030
+ }
2031
+ function confirmationText(project, before, after, dim) {
2032
+ if (dim === "active")
2033
+ return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
2034
+ if (dim === "cap")
2035
+ return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
2036
+ return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
2037
+ }
2038
+ async function runNotifyConfirmation(opts) {
2039
+ const { quieter, dim } = isQuieter(opts.before, opts.after);
2040
+ if (!quieter || dim === null)
2041
+ return false;
2042
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
2043
+ if (topicId === void 0)
2044
+ return false;
2045
+ const text = confirmationText(opts.project, opts.before, opts.after, dim);
2046
+ try {
2047
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
2048
+ return true;
2049
+ } catch {
2050
+ console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
2051
+ return false;
2052
+ }
2053
+ }
2054
+ async function runTelegramSend(opts) {
2055
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
2056
+ if (topicId === void 0) {
2057
+ throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
2058
+ }
2059
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
2060
+ return { chatId: opts.cfg.supergroupId, topicId };
2061
+ }
2062
+ async function runTelegramLink(opts) {
2063
+ const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
2064
+ if (existing !== void 0)
2065
+ return { topicId: existing, created: false };
2066
+ const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
2067
+ setTopic(opts.stateRoot, opts.project, topicId);
2068
+ return { topicId, created: true };
2069
+ }
2070
+ var init_notify = __esm({
2071
+ "packages/core/dist/telegram/notify.js"() {
2072
+ init_dist();
2073
+ init_state();
2074
+ init_format();
1907
2075
  }
1908
2076
  });
1909
2077
 
@@ -1913,121 +2081,736 @@ var init_telegram = __esm({
1913
2081
  init_bot_commands();
1914
2082
  init_auth();
1915
2083
  init_commands();
2084
+ init_control2();
1916
2085
  init_ensure_captain();
1917
2086
  init_format();
1918
2087
  init_state();
1919
2088
  init_client();
1920
2089
  init_bridge();
1921
2090
  init_setup();
2091
+ init_notify();
1922
2092
  }
1923
2093
  });
1924
2094
 
1925
- // packages/core/dist/index.js
1926
- var init_dist2 = __esm({
1927
- "packages/core/dist/index.js"() {
1928
- init_daemon();
1929
- init_mailbox();
1930
- init_protocol();
1931
- init_state_machine();
1932
- init_liveness();
1933
- init_watchdog();
1934
- init_store();
1935
- init_snapshot();
1936
- init_launchd();
1937
- init_crew_pane_reader();
1938
- init_interfaces();
1939
- init_gate();
1940
- init_context();
1941
- init_attach();
1942
- init_start();
1943
- init_delivery();
1944
- init_interactive_probe();
1945
- init_captain_delivery();
1946
- init_defer_delivery();
1947
- init_session_freshness();
1948
- init_crew_protocol();
1949
- init_crew_lifecycle();
1950
- init_telegram();
2095
+ // packages/core/dist/crew-routing.js
2096
+ function resolveCrewRoute(taskText, config) {
2097
+ const rules = config.defaults.crewRouting?.rules;
2098
+ if (!rules || rules.length === 0)
2099
+ return null;
2100
+ for (const rule2 of rules) {
2101
+ const re = new RegExp(rule2.match, "i");
2102
+ if (re.test(taskText)) {
2103
+ return {
2104
+ agent: rule2.agent,
2105
+ ...rule2.model !== void 0 ? { model: rule2.model } : {},
2106
+ tier: rule2.tier,
2107
+ matchedRule: rule2.match
2108
+ };
2109
+ }
2110
+ }
2111
+ return null;
2112
+ }
2113
+ var init_crew_routing = __esm({
2114
+ "packages/core/dist/crew-routing.js"() {
1951
2115
  }
1952
2116
  });
1953
2117
 
1954
- // packages/workspaces/dist/runtimes/cmux.js
1955
- import { execFile, execFileSync as execFileSync4 } from "child_process";
1956
- function isInsideCmux() {
1957
- return !!process.env.CMUX_WORKSPACE_ID;
1958
- }
1959
- function cmuxLocal(args) {
1960
- return execFileSync4(resolveCmuxBin(), args, {
1961
- encoding: "utf-8",
1962
- stdio: ["ignore", "pipe", "pipe"],
1963
- timeout: CMUX_TIMEOUT
1964
- }).trim();
1965
- }
1966
- function cmux(args) {
1967
- return new Promise((resolve3, reject) => {
1968
- execFile(
1969
- resolveCmuxBin(),
1970
- args,
1971
- // CMUX_QUIET=1 silences cmux 0.64's one-time deprecation hints (e.g. the
1972
- // "list-workspaces is now an alias for cmux workspace list" notice). Those
1973
- // notices print to the command's stdout and would otherwise pollute the
1974
- // output we parse. Inherit the rest of the environment unchanged.
1975
- { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } },
1976
- (err, stdout) => {
1977
- if (err) {
1978
- reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
1979
- return;
1980
- }
1981
- resolve3(stdout.trim());
1982
- }
1983
- );
1984
- });
2118
+ // packages/core/dist/group-dispatch.js
2119
+ import { randomUUID as randomUUID3 } from "crypto";
2120
+ import { homedir as homedir7 } from "os";
2121
+ import { join as join12 } from "path";
2122
+ function resolveCurrentProject(config) {
2123
+ const cwd = process.cwd();
2124
+ for (const [name, proj] of Object.entries(config.projects)) {
2125
+ const resolvedPath = resolveHome(proj.path);
2126
+ if (cwd.startsWith(resolvedPath))
2127
+ return name;
2128
+ }
2129
+ return null;
1985
2130
  }
1986
- function parseList(output) {
1987
- let parsed;
2131
+ async function isCaptainAlive(project, sockPath = DEFAULT_SOCK_PATH2) {
1988
2132
  try {
1989
- parsed = JSON.parse(output);
2133
+ const health = await sendRequest(sockPath, { kind: "health", project }, 5e3);
2134
+ const captain = health?.find((h) => h.kind === "captain" && h.project === project);
2135
+ return captain != null && captain.state !== "gone" && captain.state !== "unknown";
1990
2136
  } catch {
1991
- return [];
1992
- }
1993
- const refs = [];
1994
- for (const ws of parsed.workspaces ?? []) {
1995
- if (!ws.ref)
1996
- continue;
1997
- refs.push({
1998
- id: ws.ref,
1999
- name: ws.has_custom_title && ws.custom_title ? ws.custom_title : ws.current_directory ?? ws.ref,
2000
- status: "running"
2001
- });
2137
+ return false;
2002
2138
  }
2003
- return refs;
2004
2139
  }
2005
- function sanitizeForCmuxSend(text) {
2006
- return text.replace(/\\[nrt]/g, " ").replace(/[\n\r\t]+/g, " ").replace(/ {2,}/g, " ").trim();
2140
+ async function waitForWarmup(project, sockPath = DEFAULT_SOCK_PATH2, timeoutMs = GROUP_DISPATCH_WARMUP_TIMEOUT_MS, pollMs = GROUP_DISPATCH_WARMUP_POLL_MS) {
2141
+ const deadline = Date.now() + timeoutMs;
2142
+ while (Date.now() < deadline) {
2143
+ if (await isCaptainAlive(project, sockPath))
2144
+ return true;
2145
+ await new Promise((r) => setTimeout(r, pollMs));
2146
+ }
2147
+ return false;
2007
2148
  }
2008
- function parseDraftFromScreen(screen) {
2009
- if (!screen)
2010
- return null;
2011
- const lines = screen.split(/\r?\n/);
2012
- const HR_RE = /^\s*─{10,}\s*$/;
2013
- let bottomHR = -1;
2014
- let topHR = -1;
2015
- for (let i = lines.length - 1; i >= 0; i--) {
2016
- if (HR_RE.test(lines[i])) {
2017
- if (bottomHR === -1) {
2018
- bottomHR = i;
2019
- } else {
2020
- topHR = i;
2021
- break;
2022
- }
2023
- }
2149
+ async function dispatchToSibling(opts) {
2150
+ const config = loadConfig();
2151
+ const fromCfg = config.projects[opts.fromProject];
2152
+ const toCfg = config.projects[opts.toProject];
2153
+ if (!toCfg) {
2154
+ throw new Error(`target project '${opts.toProject}' not found in config`);
2024
2155
  }
2025
- if (topHR === -1)
2026
- return null;
2027
- const inputLines = lines.slice(topHR + 1, bottomHR);
2028
- for (const line of inputLines) {
2029
- let extracted;
2030
- const boxMatch = line.match(/│\s*[>❯]\s+(.*?)\s*│/);
2156
+ if (!fromCfg.group || !toCfg.group || fromCfg.group !== toCfg.group) {
2157
+ throw new Error(`cannot dispatch: '${opts.toProject}' (group: ${toCfg.group ?? "none"}) is not in the same group as '${opts.fromProject}' (group: ${fromCfg.group ?? "none"})`);
2158
+ }
2159
+ if (toCfg.acceptDelegations === false) {
2160
+ throw new Error(`cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`);
2161
+ }
2162
+ const sockPath = opts.sockPath ?? DEFAULT_SOCK_PATH2;
2163
+ const alive = await isCaptainAlive(opts.toProject, sockPath);
2164
+ if (!alive) {
2165
+ if (opts.bootCaptain) {
2166
+ await opts.bootCaptain(opts.toProject);
2167
+ }
2168
+ const warmed = await waitForWarmup(opts.toProject, sockPath, opts.warmupTimeoutMs, opts.warmupPollMs);
2169
+ if (!warmed) {
2170
+ throw new Error(`dispatch to '${opts.toProject}' timed out waiting for captain warmup (>${(opts.warmupTimeoutMs ?? GROUP_DISPATCH_WARMUP_TIMEOUT_MS) / 1e3}s)`);
2171
+ }
2172
+ }
2173
+ const now = Date.now();
2174
+ const attemptId = randomUUID3();
2175
+ const record = {
2176
+ id: randomUUID3(),
2177
+ project: opts.toProject,
2178
+ originProject: opts.fromProject,
2179
+ provider: opts.provider ?? "claude",
2180
+ mode: opts.mode ?? "headless",
2181
+ state: "submitted",
2182
+ task: opts.task,
2183
+ createdAt: now,
2184
+ lastHeartbeat: now,
2185
+ lastEvent: "dispatch",
2186
+ heartbeatBudgetMs: 3e5,
2187
+ attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
2188
+ };
2189
+ const result = await sendRequest(sockPath, { kind: "dispatch", record });
2190
+ return result;
2191
+ }
2192
+ var DEFAULT_SOCK_PATH2, GROUP_DISPATCH_WARMUP_TIMEOUT_MS, GROUP_DISPATCH_WARMUP_POLL_MS;
2193
+ var init_group_dispatch = __esm({
2194
+ "packages/core/dist/group-dispatch.js"() {
2195
+ init_dist();
2196
+ init_protocol();
2197
+ DEFAULT_SOCK_PATH2 = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
2198
+ GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
2199
+ GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
2200
+ }
2201
+ });
2202
+
2203
+ // packages/core/dist/launch-workspace.js
2204
+ async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
2205
+ const classify = opts.classifyScreen ?? (() => "idle");
2206
+ const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
2207
+ const settleMs = opts.settleMs ?? 2500;
2208
+ const pollMs = opts.pollMs ?? 1e3;
2209
+ const maxAttempts = opts.maxAttempts ?? 3;
2210
+ const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2211
+ const read = async () => runtime.readScreen(refId).catch(() => "");
2212
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2213
+ const deadline = Date.now() + readyTimeoutMs;
2214
+ let preSend = await read();
2215
+ let state = classify(preSend);
2216
+ while (state === "loading" && Date.now() < deadline) {
2217
+ await sleep2(pollMs);
2218
+ preSend = await read();
2219
+ state = classify(preSend);
2220
+ }
2221
+ if (state === "working")
2222
+ return;
2223
+ await runtime.send(refId, prompt).catch(() => {
2224
+ });
2225
+ if (state === "loading")
2226
+ return;
2227
+ await sleep2(settleMs);
2228
+ const after = await read();
2229
+ if (after !== preSend)
2230
+ return;
2231
+ }
2232
+ }
2233
+ async function bootWorkspace(opts) {
2234
+ const { runtime, workspaceName, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt } = opts;
2235
+ const existing = await runtime.status(workspaceName);
2236
+ if (existing && forceFresh) {
2237
+ opts.onStoppingStale?.(workspaceName);
2238
+ await runtime.stop(existing.id);
2239
+ } else if (existing) {
2240
+ opts.onAlreadyExists?.(workspaceName);
2241
+ opts.selectWorkspace?.(existing.id);
2242
+ return;
2243
+ }
2244
+ const rawCurrent = opts.getCurrentWorkspace?.() ?? null;
2245
+ const currentRef = rawCurrent?.match(/workspace:\d+/)?.[0];
2246
+ const ref = await runtime.spawn({
2247
+ name: workspaceName,
2248
+ workdir: cwd ?? process.cwd(),
2249
+ command: agentCmd,
2250
+ pinToTop
2251
+ });
2252
+ if (initialPrompt) {
2253
+ void deliverStartupPrompt(runtime, ref.id, initialPrompt, {
2254
+ classifyScreen: opts.classifyScreen
2255
+ });
2256
+ }
2257
+ if (navigate) {
2258
+ opts.selectWorkspace?.(ref.id);
2259
+ } else if (currentRef) {
2260
+ opts.selectWorkspace?.(currentRef);
2261
+ }
2262
+ opts.onCreated?.(workspaceName);
2263
+ }
2264
+ async function launchOneWorkspace(opts) {
2265
+ let forceFresh = !!opts.forceFreshOverride;
2266
+ if (!forceFresh) {
2267
+ const auto = shouldStartFresh(opts.workspaceName, opts.role, {
2268
+ sessionsPath: opts.sessionsPath,
2269
+ templatesDir: opts.templatesDir
2270
+ });
2271
+ if (auto.fresh) {
2272
+ opts.onFreshReason?.(auto.reason ?? "starting fresh");
2273
+ forceFresh = true;
2274
+ }
2275
+ }
2276
+ const agentCmd = opts.agentCmdFactory(forceFresh);
2277
+ recordSession(opts.workspaceName, opts.role, {
2278
+ sessionsPath: opts.sessionsPath,
2279
+ templatesDir: opts.templatesDir
2280
+ });
2281
+ await bootWorkspace({
2282
+ runtime: opts.runtime,
2283
+ workspaceName: opts.workspaceName,
2284
+ agentCmd,
2285
+ cwd: opts.cwd,
2286
+ navigate: opts.navigate,
2287
+ forceFresh,
2288
+ pinToTop: opts.pinToTop,
2289
+ initialPrompt: opts.initialPrompt,
2290
+ classifyScreen: opts.classifyScreen,
2291
+ selectWorkspace: opts.selectWorkspace,
2292
+ getCurrentWorkspace: opts.getCurrentWorkspace,
2293
+ onStoppingStale: opts.onStoppingStale,
2294
+ onAlreadyExists: opts.onAlreadyExists,
2295
+ onCreated: opts.onCreated
2296
+ });
2297
+ }
2298
+ var init_launch_workspace = __esm({
2299
+ "packages/core/dist/launch-workspace.js"() {
2300
+ init_session_freshness();
2301
+ }
2302
+ });
2303
+
2304
+ // packages/core/dist/side-session.js
2305
+ import fs11 from "fs";
2306
+ function sideTitleFor(project, name) {
2307
+ return `\u{1F5D2} ${project}:${name}`;
2308
+ }
2309
+ function isSideTitle(project, title) {
2310
+ return title.startsWith(`\u{1F5D2} ${project}:`);
2311
+ }
2312
+ function sideNameFromTitle(project, title) {
2313
+ return title.slice(`\u{1F5D2} ${project}:`.length);
2314
+ }
2315
+ function sideNextAutoName(existingTitles, project) {
2316
+ const used = /* @__PURE__ */ new Set();
2317
+ for (const title of existingTitles) {
2318
+ const n = sideNameFromTitle(project, title).match(/^side-(\d+)$/);
2319
+ if (n)
2320
+ used.add(Number(n[1]));
2321
+ }
2322
+ let i = 1;
2323
+ while (used.has(i))
2324
+ i++;
2325
+ return `side-${i}`;
2326
+ }
2327
+ function buildSideFirstTurn(topic, project, role, spokeVault, scratchWorktree) {
2328
+ const lines = [
2329
+ topic,
2330
+ "",
2331
+ "---",
2332
+ "Side-session context (for handoff use):",
2333
+ `Project: ${project}`,
2334
+ `Role: ${role}`,
2335
+ `Spoke vault: ${spokeVault}`
2336
+ ];
2337
+ if (scratchWorktree) {
2338
+ lines.push(`Scratch worktree: ${scratchWorktree}`);
2339
+ }
2340
+ return lines.join("\n");
2341
+ }
2342
+ async function runSideSpawn(input, config, deps) {
2343
+ const proj = config.projects[input.project];
2344
+ if (!proj) {
2345
+ throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
2346
+ }
2347
+ if (!SIDE_ROLES.includes(input.role)) {
2348
+ throw new Error(`Unknown side role '${input.role}'. Valid roles: ${SIDE_ROLES.join(", ")}.`);
2349
+ }
2350
+ const { runtime } = deps;
2351
+ const captain = await runtime.status(proj.captainName);
2352
+ if (!captain) {
2353
+ throw new Error(`Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`);
2354
+ }
2355
+ const existing = await runtime.listSurfaces(captain.id);
2356
+ const existingTitles = existing.filter((s) => s.title && isSideTitle(input.project, s.title)).map((s) => s.title);
2357
+ if (input.name) {
2358
+ const wantTitle = sideTitleFor(input.project, input.name);
2359
+ if (existingTitles.includes(wantTitle)) {
2360
+ throw new Error(`Side session '${input.name}' already exists for ${input.project}.`);
2361
+ }
2362
+ }
2363
+ const name = input.name ?? sideNextAutoName(existingTitles, input.project);
2364
+ const spawnCwd = input.role === "debug" ? addWorktree({
2365
+ repoRoot: proj.path,
2366
+ worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
2367
+ project: input.project,
2368
+ name,
2369
+ base: resolveWorktreeBase(proj.path)
2370
+ }) : proj.path;
2371
+ const agentCmd = deps.agentCmdFactory(spawnCwd);
2372
+ const direction = input.direction ?? "tab";
2373
+ const title = sideTitleFor(input.project, name);
2374
+ const pane = await runtime.newPane({ workspaceId: captain.id, direction, title });
2375
+ await runtime.sendToPane(pane, `cd ${shellQuote(spawnCwd)} && ${agentCmd}`);
2376
+ const preLaunchScreen = await runtime.readPaneScreen(pane) ?? "";
2377
+ const firstTurn = buildSideFirstTurn(input.topic, input.project, input.role, proj.spokeVault ?? "", input.role === "debug" ? spawnCwd : void 0);
2378
+ await deps.sendFirstTurn(pane, firstTurn, preLaunchScreen);
2379
+ return { ...pane, title };
2380
+ }
2381
+ async function runSideSend(runtime, workspaceId, project, name, message) {
2382
+ const want = sideTitleFor(project, name);
2383
+ const surfaces = await runtime.listSurfaces(workspaceId);
2384
+ const pane = surfaces.find((s) => s.title === want) ?? null;
2385
+ if (!pane) {
2386
+ throw new Error(`Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`);
2387
+ }
2388
+ await runtime.sendToPane(pane, message);
2389
+ }
2390
+ async function runSideList(runtime, workspaceId, project) {
2391
+ const surfaces = await runtime.listSurfaces(workspaceId);
2392
+ return surfaces.filter((s) => s.title && isSideTitle(project, s.title)).map((s) => ({
2393
+ name: sideNameFromTitle(project, s.title),
2394
+ surfaceId: s.surfaceId
2395
+ }));
2396
+ }
2397
+ async function runSideClose(runtime, workspaceId, project, name, projPath, worktreeDir) {
2398
+ const want = sideTitleFor(project, name);
2399
+ const surfaces = await runtime.listSurfaces(workspaceId);
2400
+ const pane = surfaces.find((s) => s.title === want) ?? null;
2401
+ if (!pane) {
2402
+ throw new Error(`Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`);
2403
+ }
2404
+ await runtime.closePane(pane);
2405
+ if (projPath) {
2406
+ const wtPath = worktreePath(projPath, worktreeDir, project, name);
2407
+ if (fs11.existsSync(wtPath)) {
2408
+ try {
2409
+ removeWorktree(projPath, wtPath);
2410
+ } catch (e) {
2411
+ process.stderr.write(`(worktree remove failed: ${e.message})
2412
+ `);
2413
+ }
2414
+ }
2415
+ }
2416
+ }
2417
+ var SIDE_ROLES;
2418
+ var init_side_session = __esm({
2419
+ "packages/core/dist/side-session.js"() {
2420
+ init_dist();
2421
+ init_crew_protocol();
2422
+ SIDE_ROLES = ["research", "debug"];
2423
+ }
2424
+ });
2425
+
2426
+ // packages/core/dist/crew-spawn.js
2427
+ import fs12 from "fs";
2428
+ import os4 from "os";
2429
+ import path10 from "path";
2430
+ async function listCrewPanes(runtime, workspaceId, project) {
2431
+ const surfaces = await runtime.listSurfaces(workspaceId);
2432
+ return surfaces.filter((s) => s.title && isCrewTitle(project, s.title));
2433
+ }
2434
+ async function findCrewPane(runtime, workspaceId, project, name) {
2435
+ const want = titleFor(project, name);
2436
+ const surfaces = await runtime.listSurfaces(workspaceId);
2437
+ return surfaces.find((s) => s.title === want) ?? null;
2438
+ }
2439
+ async function runCodexInteractiveSpawn(o) {
2440
+ const rec = await o.dispatchCrew({
2441
+ provider: "codex",
2442
+ mode: "interactive",
2443
+ project: o.project,
2444
+ cwd: o.cwd,
2445
+ task: o.task,
2446
+ name: o.name,
2447
+ ...o.approvalPolicy ? { approvalPolicy: o.approvalPolicy } : {},
2448
+ ...o.roleInstructions ? { roleInstructions: o.roleInstructions } : {}
2449
+ });
2450
+ const title = titleFor(o.project, o.name);
2451
+ const pane = await o.runtime.newPane({
2452
+ workspaceId: o.workspaceId,
2453
+ direction: o.direction,
2454
+ title
2455
+ });
2456
+ await o.runtime.sendToPane(pane, `squadrant crew attach ${rec.id}`);
2457
+ if (o.task && o.task !== "(interactive)") {
2458
+ void o.sendCodexFirstTurn(rec.id, o.task).catch((e) => {
2459
+ process.stderr.write(`(first-turn delivery failed: ${e.message})
2460
+ `);
2461
+ });
2462
+ }
2463
+ return { ...pane, title };
2464
+ }
2465
+ async function runCrewSpawn(input, config, deps) {
2466
+ const proj = config.projects[input.project];
2467
+ if (!proj) {
2468
+ throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
2469
+ }
2470
+ const captain = await deps.runtime.status(proj.captainName);
2471
+ if (!captain) {
2472
+ throw new Error(`Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`);
2473
+ }
2474
+ const existing = await listCrewPanes(deps.runtime, captain.id, input.project);
2475
+ const existingTitles = existing.map((s) => s.title);
2476
+ if (input.name) {
2477
+ const wantTitle = titleFor(input.project, input.name);
2478
+ if (existingTitles.includes(wantTitle)) {
2479
+ throw new Error(`Crew '${input.name}' already exists for ${input.project}. Use 'squadrant crew send ${input.project} ${input.name}' to send a follow-up, or pick a different --name.`);
2480
+ }
2481
+ }
2482
+ const name = input.name ?? nextAutoName(existingTitles, input.project);
2483
+ const spawnCwd = !input.shared ? addWorktree({
2484
+ repoRoot: proj.path,
2485
+ worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
2486
+ project: input.project,
2487
+ name,
2488
+ base: resolveWorktreeBase(proj.path)
2489
+ }) : proj.path;
2490
+ const route = !input.agentExplicit && !input.model ? resolveCrewRoute(input.task, config) : null;
2491
+ if (route) {
2492
+ deps.onRouted?.(route);
2493
+ }
2494
+ const agentName = route?.agent ?? input.agent ?? "claude";
2495
+ const agent = deps.resolveAgent(agentName);
2496
+ if (!agent) {
2497
+ throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
2498
+ }
2499
+ if (agentName === "codex") {
2500
+ const codexRoleFile = path10.join(TEMPLATES_DIR, `crew.${agent.templateSuffix}.md`);
2501
+ const roleInstructions = fs12.existsSync(codexRoleFile) ? fs12.readFileSync(codexRoleFile, "utf8") : void 0;
2502
+ return runCodexInteractiveSpawn({
2503
+ project: input.project,
2504
+ task: input.task,
2505
+ cwd: spawnCwd,
2506
+ runtime: deps.runtime,
2507
+ workspaceId: captain.id,
2508
+ name,
2509
+ direction: input.direction ?? "tab",
2510
+ approvalPolicy: input.approvalPolicy,
2511
+ roleInstructions,
2512
+ dispatchCrew: deps.dispatchCrew,
2513
+ sendCodexFirstTurn: deps.sendCodexFirstTurn
2514
+ });
2515
+ }
2516
+ const promptFile = path10.join(TEMPLATES_DIR, `crew.${agent.templateSuffix}.md`);
2517
+ const interactive = agent.name === "claude" || agent.name === "opencode";
2518
+ const crewRole = config.defaults.roles?.crew;
2519
+ const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
2520
+ const crewModel = input.model ?? route?.model ?? configModel;
2521
+ if (agentName === "claude") {
2522
+ const rec = await deps.dispatchCrew({
2523
+ provider: "claude",
2524
+ mode: "interactive",
2525
+ project: input.project,
2526
+ cwd: spawnCwd,
2527
+ task: input.task,
2528
+ name
2529
+ });
2530
+ deps.writeSettingsLocal(spawnCwd);
2531
+ const cliCommand2 = agent.buildCommand({
2532
+ prompt: input.task,
2533
+ workdir: spawnCwd,
2534
+ role: "crew",
2535
+ promptFile,
2536
+ interactive: true,
2537
+ // Permission mode is config-driven so squadrant can default crews to 'auto'
2538
+ // or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
2539
+ permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
2540
+ ...crewModel ? { model: crewModel } : {}
2541
+ });
2542
+ const direction2 = input.direction ?? "tab";
2543
+ const title2 = titleFor(input.project, name);
2544
+ const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
2545
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
2546
+ await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${cliCommand2}`);
2547
+ const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
2548
+ await deps.sendFirstTurn(pane2, `${input.task}
2549
+
2550
+ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
2551
+ return { ...pane2, title: title2 };
2552
+ }
2553
+ if (agentName === "opencode") {
2554
+ const serverPort = await deps.getFreePort();
2555
+ const rec = await deps.dispatchCrew({
2556
+ provider: "opencode",
2557
+ mode: "interactive",
2558
+ project: input.project,
2559
+ cwd: spawnCwd,
2560
+ task: input.task,
2561
+ name,
2562
+ // opencode has no heartbeat hook, so a normal budget would false-stall
2563
+ // every crew after 5min; use a 24h budget to effectively disable stall
2564
+ // detection. The SSE bridge (serverPort) provides turn-end liveness.
2565
+ budgetMs: 864e5,
2566
+ serverPort
2567
+ });
2568
+ const opencodeConfigPath = deps.writeOpencodeConfig({
2569
+ stateRoot: STATE_ROOT,
2570
+ project: input.project,
2571
+ taskId: rec.id,
2572
+ // CP3 opt-in: --approval gates bash so the captain approves shell commands.
2573
+ ...input.approval ? { gateBash: true } : {}
2574
+ });
2575
+ const cliCommand2 = agent.buildCommand({
2576
+ prompt: input.task,
2577
+ workdir: spawnCwd,
2578
+ role: "crew",
2579
+ promptFile,
2580
+ interactive: true,
2581
+ model: crewModel,
2582
+ port: serverPort
2583
+ });
2584
+ const direction2 = input.direction ?? "tab";
2585
+ const title2 = titleFor(input.project, name);
2586
+ const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
2587
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
2588
+ await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} OPENCODE_CONFIG=${opencodeConfigPath} ${cliCommand2}`);
2589
+ const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
2590
+ await deps.sendFirstTurn(pane2, `${input.task}
2591
+
2592
+ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
2593
+ // #235: confirm-on-delivery — sendFirstTurnWhenReady polls until "Ask
2594
+ // anything…" leaves the screen, re-sending every ~3s to cover slow boots
2595
+ // without duplicating the task. See crew-pane.ts SPLASH_MAX_CHECKS/EVERY_N.
2596
+ splashMarker: "Ask anything\u2026"
2597
+ });
2598
+ return { ...pane2, title: title2 };
2599
+ }
2600
+ const cliCommand = agent.buildCommand({
2601
+ prompt: input.task,
2602
+ workdir: spawnCwd,
2603
+ role: "crew",
2604
+ promptFile,
2605
+ interactive,
2606
+ model: crewModel
2607
+ });
2608
+ const direction = input.direction ?? "tab";
2609
+ const title = titleFor(input.project, name);
2610
+ const pane = await deps.runtime.newPane({ workspaceId: captain.id, direction, title });
2611
+ await deps.runtime.sendToPane(pane, cliCommand);
2612
+ if (interactive) {
2613
+ const preLaunchScreen = await deps.runtime.readPaneScreen(pane) ?? "";
2614
+ await deps.sendFirstTurn(pane, input.task, preLaunchScreen);
2615
+ }
2616
+ return { ...pane, title };
2617
+ }
2618
+ async function runCrewSend(project, name, message, runtime, workspaceId, deps) {
2619
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
2620
+ if (!crew) {
2621
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
2622
+ }
2623
+ try {
2624
+ const tasks = await deps.listTasks(project);
2625
+ const task = tasks.find((t) => t.name === name);
2626
+ if (task) {
2627
+ if (TERMINAL_STATES.has(task.state)) {
2628
+ await deps.emitEvent(project, { type: "task.reopened", id: task.id });
2629
+ } else if (task.state === "blocked" || task.state === "awaiting-input") {
2630
+ await deps.emitEvent(project, { type: "task.started", id: task.id });
2631
+ }
2632
+ }
2633
+ } catch {
2634
+ }
2635
+ await runtime.sendToPane(crew, message);
2636
+ }
2637
+ async function runCrewRead(project, name, runtime, workspaceId) {
2638
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
2639
+ if (!crew) {
2640
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
2641
+ }
2642
+ return runtime.readPaneScreen(crew);
2643
+ }
2644
+ async function runCrewClose(project, name, runtime, workspaceId, deps) {
2645
+ const projRoot = loadConfig().projects[project]?.path;
2646
+ let taskId;
2647
+ let worktreeCwd;
2648
+ try {
2649
+ const tasks = await deps.listTasks(project);
2650
+ const task = tasks.find((t) => t.name === name);
2651
+ if (task) {
2652
+ taskId = task.id;
2653
+ if (task.cwd && projRoot && task.cwd !== projRoot) {
2654
+ worktreeCwd = task.cwd;
2655
+ }
2656
+ if (!TERMINAL_STATES.has(task.state)) {
2657
+ await deps.emitEvent(project, { type: "task.cancelled", id: task.id, reason: "closed by captain" });
2658
+ }
2659
+ if (task.provider === "codex") {
2660
+ await deps.closeCodexThread(task.id);
2661
+ }
2662
+ }
2663
+ } catch {
2664
+ }
2665
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
2666
+ if (crew) {
2667
+ await runtime.closePane(crew);
2668
+ } else if (taskId === void 0) {
2669
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
2670
+ }
2671
+ if (taskId !== void 0) {
2672
+ await reapCrewChildren(taskId);
2673
+ }
2674
+ if (worktreeCwd && projRoot) {
2675
+ try {
2676
+ removeWorktree(projRoot, worktreeCwd);
2677
+ } catch (e) {
2678
+ process.stderr.write(`(worktree remove failed: ${e.message})
2679
+ `);
2680
+ }
2681
+ }
2682
+ }
2683
+ async function runCrewList(project, runtime, workspaceId) {
2684
+ const crews = await listCrewPanes(runtime, workspaceId, project);
2685
+ return crews.map((c) => ({
2686
+ name: nameFromTitle(project, c.title),
2687
+ surfaceId: c.surfaceId
2688
+ }));
2689
+ }
2690
+ var TEMPLATES_DIR, STATE_ROOT;
2691
+ var init_crew_spawn = __esm({
2692
+ "packages/core/dist/crew-spawn.js"() {
2693
+ init_dist();
2694
+ init_crew_routing();
2695
+ init_crew_protocol();
2696
+ init_crew_lifecycle();
2697
+ TEMPLATES_DIR = path10.join(os4.homedir(), ".config", "squadrant", "templates");
2698
+ STATE_ROOT = path10.join(os4.homedir(), ".config", "squadrant", "state");
2699
+ }
2700
+ });
2701
+
2702
+ // packages/core/dist/index.js
2703
+ var init_dist2 = __esm({
2704
+ "packages/core/dist/index.js"() {
2705
+ init_reduce();
2706
+ init_mailbox();
2707
+ init_protocol();
2708
+ init_state_machine();
2709
+ init_liveness();
2710
+ init_watchdog();
2711
+ init_store();
2712
+ init_snapshot();
2713
+ init_launchd();
2714
+ init_crew_pane_reader();
2715
+ init_interfaces();
2716
+ init_gate();
2717
+ init_context();
2718
+ init_attach();
2719
+ init_start();
2720
+ init_delivery_loop();
2721
+ init_interactive_probe();
2722
+ init_captain_delivery();
2723
+ init_defer_delivery();
2724
+ init_session_freshness();
2725
+ init_crew_protocol();
2726
+ init_crew_lifecycle();
2727
+ init_telegram();
2728
+ init_crew_routing();
2729
+ init_restart_daemon();
2730
+ init_group_dispatch();
2731
+ init_launch_workspace();
2732
+ init_side_session();
2733
+ init_crew_spawn();
2734
+ }
2735
+ });
2736
+
2737
+ // packages/workspaces/dist/runtimes/cmux.js
2738
+ import { execFile as execFile2, execFileSync as execFileSync5 } from "child_process";
2739
+ function isInsideCmux() {
2740
+ return !!process.env.CMUX_WORKSPACE_ID;
2741
+ }
2742
+ function cmuxLocal(args) {
2743
+ return execFileSync5(resolveCmuxBin(), args, {
2744
+ encoding: "utf-8",
2745
+ stdio: ["ignore", "pipe", "pipe"],
2746
+ timeout: CMUX_TIMEOUT
2747
+ }).trim();
2748
+ }
2749
+ function cmux(args) {
2750
+ return new Promise((resolve3, reject) => {
2751
+ execFile2(
2752
+ resolveCmuxBin(),
2753
+ args,
2754
+ // CMUX_QUIET=1 silences cmux 0.64's one-time deprecation hints (e.g. the
2755
+ // "list-workspaces is now an alias for cmux workspace list" notice). Those
2756
+ // notices print to the command's stdout and would otherwise pollute the
2757
+ // output we parse. Inherit the rest of the environment unchanged.
2758
+ { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } },
2759
+ (err, stdout) => {
2760
+ if (err) {
2761
+ reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
2762
+ return;
2763
+ }
2764
+ resolve3(stdout.trim());
2765
+ }
2766
+ );
2767
+ });
2768
+ }
2769
+ function parseList(output) {
2770
+ let parsed;
2771
+ try {
2772
+ parsed = JSON.parse(output);
2773
+ } catch {
2774
+ return [];
2775
+ }
2776
+ const refs = [];
2777
+ for (const ws of parsed.workspaces ?? []) {
2778
+ if (!ws.ref)
2779
+ continue;
2780
+ refs.push({
2781
+ id: ws.ref,
2782
+ name: ws.has_custom_title && ws.custom_title ? ws.custom_title : ws.current_directory ?? ws.ref,
2783
+ status: "running"
2784
+ });
2785
+ }
2786
+ return refs;
2787
+ }
2788
+ function sanitizeForCmuxSend(text) {
2789
+ return text.replace(/\\[nrt]/g, " ").replace(/[\n\r\t]+/g, " ").replace(/ {2,}/g, " ").trim();
2790
+ }
2791
+ function parseDraftFromScreen(screen) {
2792
+ if (!screen)
2793
+ return null;
2794
+ const lines = screen.split(/\r?\n/);
2795
+ const HR_RE = /^\s*─{10,}\s*$/;
2796
+ let bottomHR = -1;
2797
+ let topHR = -1;
2798
+ for (let i = lines.length - 1; i >= 0; i--) {
2799
+ if (HR_RE.test(lines[i])) {
2800
+ if (bottomHR === -1) {
2801
+ bottomHR = i;
2802
+ } else {
2803
+ topHR = i;
2804
+ break;
2805
+ }
2806
+ }
2807
+ }
2808
+ if (topHR === -1)
2809
+ return null;
2810
+ const inputLines = lines.slice(topHR + 1, bottomHR);
2811
+ for (const line of inputLines) {
2812
+ let extracted;
2813
+ const boxMatch = line.match(/│\s*[>❯]\s+(.*?)\s*│/);
2031
2814
  if (boxMatch) {
2032
2815
  extracted = boxMatch[1].trim();
2033
2816
  } else {
@@ -2394,7 +3177,7 @@ var init_runtimes = __esm({
2394
3177
  });
2395
3178
 
2396
3179
  // packages/workspaces/dist/notifiers/cmux.js
2397
- import { execFileSync as execFileSync5, execSync as execSync2 } from "child_process";
3180
+ import { execFileSync as execFileSync6, execSync as execSync2 } from "child_process";
2398
3181
  function createCmuxNotifier(_scope) {
2399
3182
  return {
2400
3183
  name: "cmux",
@@ -2411,7 +3194,7 @@ function createCmuxNotifier(_scope) {
2411
3194
  }
2412
3195
  },
2413
3196
  async notify(message) {
2414
- execFileSync5("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
3197
+ execFileSync6("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
2415
3198
  }
2416
3199
  };
2417
3200
  }
@@ -2466,13 +3249,13 @@ var init_notifiers = __esm({
2466
3249
  });
2467
3250
 
2468
3251
  // packages/workspaces/dist/workspaces/obsidian.js
2469
- import fs11 from "fs/promises";
2470
- import { existsSync as existsSync8 } from "fs";
2471
- import path10 from "path";
3252
+ import fs13 from "fs/promises";
3253
+ import { existsSync as existsSync9 } from "fs";
3254
+ import path11 from "path";
2472
3255
  function resolveInRoot(root, relative) {
2473
- const joined = path10.resolve(root, relative);
2474
- const normalized = path10.resolve(root) + path10.sep;
2475
- if (joined !== path10.resolve(root) && !joined.startsWith(normalized)) {
3256
+ const joined = path11.resolve(root, relative);
3257
+ const normalized = path11.resolve(root) + path11.sep;
3258
+ if (joined !== path11.resolve(root) && !joined.startsWith(normalized)) {
2476
3259
  throw new Error(`Path '${relative}' escapes workspace root`);
2477
3260
  }
2478
3261
  return joined;
@@ -2487,20 +3270,20 @@ function createObsidianDriver(scope) {
2487
3270
  async probe() {
2488
3271
  return {
2489
3272
  installed: true,
2490
- rootExists: existsSync8(root)
3273
+ rootExists: existsSync9(root)
2491
3274
  };
2492
3275
  },
2493
3276
  async read(rel) {
2494
- return fs11.readFile(resolveInRoot(root, rel), "utf-8");
3277
+ return fs13.readFile(resolveInRoot(root, rel), "utf-8");
2495
3278
  },
2496
3279
  async write(rel, content) {
2497
3280
  const abs = resolveInRoot(root, rel);
2498
- await fs11.mkdir(path10.dirname(abs), { recursive: true });
2499
- await fs11.writeFile(abs, content);
3281
+ await fs13.mkdir(path11.dirname(abs), { recursive: true });
3282
+ await fs13.writeFile(abs, content);
2500
3283
  },
2501
3284
  async exists(rel) {
2502
3285
  try {
2503
- await fs11.access(resolveInRoot(root, rel));
3286
+ await fs13.access(resolveInRoot(root, rel));
2504
3287
  return true;
2505
3288
  } catch {
2506
3289
  return false;
@@ -2508,13 +3291,13 @@ function createObsidianDriver(scope) {
2508
3291
  },
2509
3292
  async list(rel) {
2510
3293
  try {
2511
- return await fs11.readdir(resolveInRoot(root, rel));
3294
+ return await fs13.readdir(resolveInRoot(root, rel));
2512
3295
  } catch {
2513
3296
  return [];
2514
3297
  }
2515
3298
  },
2516
3299
  async mkdir(rel) {
2517
- await fs11.mkdir(resolveInRoot(root, rel), { recursive: true });
3300
+ await fs13.mkdir(resolveInRoot(root, rel), { recursive: true });
2518
3301
  }
2519
3302
  };
2520
3303
  }
@@ -2572,7 +3355,7 @@ var init_workspaces2 = __esm({
2572
3355
  }
2573
3356
  });
2574
3357
 
2575
- // packages/workspaces/dist/cmux/events-bridge.js
3358
+ // packages/workspaces/dist/cmux-daemon/events-bridge.js
2576
3359
  import { spawn as nodeSpawn } from "child_process";
2577
3360
  function deriveRunState(eventName) {
2578
3361
  switch (eventName) {
@@ -2587,7 +3370,7 @@ function deriveRunState(eventName) {
2587
3370
  }
2588
3371
  var CmuxEventsBridge;
2589
3372
  var init_events_bridge = __esm({
2590
- "packages/workspaces/dist/cmux/events-bridge.js"() {
3373
+ "packages/workspaces/dist/cmux-daemon/events-bridge.js"() {
2591
3374
  init_dist();
2592
3375
  CmuxEventsBridge = class {
2593
3376
  child = null;
@@ -2712,11 +3495,11 @@ var init_events_bridge = __esm({
2712
3495
  }
2713
3496
  });
2714
3497
 
2715
- // packages/workspaces/dist/cmux/daemon-cmux.js
3498
+ // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
2716
3499
  var DaemonCmux;
2717
3500
  var init_daemon_cmux = __esm({
2718
- "packages/workspaces/dist/cmux/daemon-cmux.js"() {
2719
- init_cmux();
3501
+ "packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
3502
+ init_dist2();
2720
3503
  DaemonCmux = class {
2721
3504
  driver;
2722
3505
  constructor(driver) {
@@ -2822,19 +3605,32 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
2822
3605
  }
2823
3606
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
2824
3607
  await runtime.sendToPane(pane, task);
2825
- const retryLimit = acceptanceConfig?.retryLimit ?? 2;
2826
- for (let attempt = 0; attempt < retryLimit; attempt++) {
2827
- await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
2828
- const afterScreen = await runtime.readPaneScreen(pane) ?? "";
2829
- if (isTurnAccepted(preSendScreen, afterScreen, acceptanceConfig)) {
2830
- return;
3608
+ if (acceptanceConfig?.splashMarker) {
3609
+ for (let check2 = 0; check2 < SPLASH_MAX_CHECKS; check2++) {
3610
+ await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3611
+ const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3612
+ if (isTurnAccepted(preSendScreen, afterScreen, acceptanceConfig)) {
3613
+ return;
3614
+ }
3615
+ if ((check2 + 1) % SPLASH_RESEND_EVERY_N === 0 && check2 < SPLASH_MAX_CHECKS - 1) {
3616
+ await runtime.sendToPane(pane, task);
3617
+ }
2831
3618
  }
2832
- if (attempt < retryLimit - 1) {
2833
- await runtime.sendToPane(pane, task);
3619
+ } else {
3620
+ const retryLimit = acceptanceConfig?.retryLimit ?? 2;
3621
+ for (let attempt = 0; attempt < retryLimit; attempt++) {
3622
+ await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
3623
+ const afterScreen = await runtime.readPaneScreen(pane) ?? "";
3624
+ if (isTurnAccepted(preSendScreen, afterScreen, acceptanceConfig)) {
3625
+ return;
3626
+ }
3627
+ if (attempt < retryLimit - 1) {
3628
+ await runtime.sendToPane(pane, task);
3629
+ }
2834
3630
  }
2835
3631
  }
2836
3632
  }
2837
- var SEND_FIRST_TURN_FLOOR_MS, POLL_INTERVAL_MS, SEND_FIRST_TURN_TIMEOUT_MS, POST_SEND_CHECK_MS;
3633
+ var SEND_FIRST_TURN_FLOOR_MS, POLL_INTERVAL_MS, SEND_FIRST_TURN_TIMEOUT_MS, POST_SEND_CHECK_MS, SPLASH_MAX_CHECKS, SPLASH_RESEND_EVERY_N;
2838
3634
  var init_crew_pane = __esm({
2839
3635
  "packages/workspaces/dist/crew-pane.js"() {
2840
3636
  init_dist();
@@ -2845,6 +3641,8 @@ var init_crew_pane = __esm({
2845
3641
  POLL_INTERVAL_MS = 750;
2846
3642
  SEND_FIRST_TURN_TIMEOUT_MS = 2e4;
2847
3643
  POST_SEND_CHECK_MS = 750;
3644
+ SPLASH_MAX_CHECKS = 20;
3645
+ SPLASH_RESEND_EVERY_N = 4;
2848
3646
  }
2849
3647
  });
2850
3648
 
@@ -2854,7 +3652,6 @@ __export(dist_exports, {
2854
3652
  CMUX_TIMEOUT: () => CMUX_TIMEOUT,
2855
3653
  CmuxEventsBridge: () => CmuxEventsBridge,
2856
3654
  DaemonCmux: () => DaemonCmux,
2857
- DeferDelivery: () => DeferDelivery,
2858
3655
  NotifierRegistry: () => NotifierRegistry,
2859
3656
  RuntimeRegistry: () => RuntimeRegistry,
2860
3657
  WorkspaceRegistry: () => WorkspaceRegistry,
@@ -3183,8 +3980,8 @@ var init_registry4 = __esm({
3183
3980
  });
3184
3981
 
3185
3982
  // packages/agents/dist/drivers/launch-cmd.js
3186
- import fs12 from "fs";
3187
- import path11 from "path";
3983
+ import fs14 from "fs";
3984
+ import path12 from "path";
3188
3985
  function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir) {
3189
3986
  const driver = registry.getDriver(agentName);
3190
3987
  if (driver.name === "claude") {
@@ -3200,27 +3997,27 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
3200
3997
  cmd += ` --model ${model}`;
3201
3998
  }
3202
3999
  if (templatesDir) {
3203
- const roleFile2 = path11.join(templatesDir, `${role}.claude.md`);
3204
- const legacyRoleFile = path11.join(templatesDir, `${role}.CLAUDE.md`);
3205
- const actualRoleFile = fs12.existsSync(roleFile2) ? roleFile2 : fs12.existsSync(legacyRoleFile) ? legacyRoleFile : null;
4000
+ const roleFile2 = path12.join(templatesDir, `${role}.claude.md`);
4001
+ const legacyRoleFile = path12.join(templatesDir, `${role}.CLAUDE.md`);
4002
+ const actualRoleFile = fs14.existsSync(roleFile2) ? roleFile2 : fs14.existsSync(legacyRoleFile) ? legacyRoleFile : null;
3206
4003
  if (actualRoleFile) {
3207
4004
  cmd += ` --append-system-prompt-file ${actualRoleFile}`;
3208
4005
  }
3209
- const pluginDir = path11.join(templatesDir, "..", "plugin");
3210
- if (fs12.existsSync(pluginDir)) {
4006
+ const pluginDir = path12.join(templatesDir, "..", "plugin");
4007
+ if (fs14.existsSync(pluginDir)) {
3211
4008
  cmd += ` --plugin-dir ${pluginDir}`;
3212
4009
  }
3213
4010
  }
3214
4011
  return cmd;
3215
4012
  }
3216
- const roleFile = templatesDir ? path11.join(templatesDir, `${role}.${driver.templateSuffix}.md`) : void 0;
4013
+ const roleFile = templatesDir ? path12.join(templatesDir, `${role}.${driver.templateSuffix}.md`) : void 0;
3217
4014
  return driver.buildCommand({
3218
4015
  prompt: `You are a squadrant ${role}. Read your instructions from ${roleFile ?? role} and begin.`,
3219
4016
  workdir: process.cwd(),
3220
4017
  role,
3221
4018
  model,
3222
4019
  autoApprove: true,
3223
- promptFile: roleFile && fs12.existsSync(roleFile) ? roleFile : void 0
4020
+ promptFile: roleFile && fs14.existsSync(roleFile) ? roleFile : void 0
3224
4021
  });
3225
4022
  }
3226
4023
  var init_launch_cmd = __esm({
@@ -3243,8 +4040,8 @@ var init_drivers = __esm({
3243
4040
 
3244
4041
  // packages/agents/dist/projection/cursor.js
3245
4042
  import { mkdir, readFile, writeFile } from "fs/promises";
3246
- import path12 from "path";
3247
- import os4 from "os";
4043
+ import path13 from "path";
4044
+ import os5 from "os";
3248
4045
  function renderMdc(source) {
3249
4046
  const skillSections = source.skills.map((s) => `## Skill: ${s.name}
3250
4047
 
@@ -3292,7 +4089,7 @@ function createCursorEmitter() {
3292
4089
  if (scope === "user") {
3293
4090
  return [
3294
4091
  {
3295
- path: path12.join(os4.homedir(), ".cursor/rules/squadrant-global.mdc"),
4092
+ path: path13.join(os5.homedir(), ".cursor/rules/squadrant-global.mdc"),
3296
4093
  shared: false,
3297
4094
  format: "mdc"
3298
4095
  }
@@ -3302,7 +4099,7 @@ function createCursorEmitter() {
3302
4099
  return [];
3303
4100
  return [
3304
4101
  {
3305
- path: path12.join(projectRoot, ".cursor/rules/squadrant.mdc"),
4102
+ path: path13.join(projectRoot, ".cursor/rules/squadrant.mdc"),
3306
4103
  shared: false,
3307
4104
  format: "mdc"
3308
4105
  }
@@ -3319,7 +4116,7 @@ function createCursorEmitter() {
3319
4116
  diff: buildDiff(existing, generated)
3320
4117
  };
3321
4118
  }
3322
- await mkdir(path12.dirname(dest.path), { recursive: true });
4119
+ await mkdir(path13.dirname(dest.path), { recursive: true });
3323
4120
  await writeFile(dest.path, generated, "utf-8");
3324
4121
  return {
3325
4122
  written: true,
@@ -3370,8 +4167,8 @@ var init_marker = __esm({
3370
4167
 
3371
4168
  // packages/agents/dist/projection/codex.js
3372
4169
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
3373
- import path13 from "path";
3374
- import os5 from "os";
4170
+ import path14 from "path";
4171
+ import os6 from "os";
3375
4172
  function renderMarkdown(source) {
3376
4173
  const skillSections = source.skills.map((s) => `## Skill: ${s.name}
3377
4174
 
@@ -3395,7 +4192,7 @@ function createCodexEmitter() {
3395
4192
  destinations(scope, projectRoot) {
3396
4193
  if (scope === "user") {
3397
4194
  return [{
3398
- path: path13.join(os5.homedir(), ".codex/AGENTS.md"),
4195
+ path: path14.join(os6.homedir(), ".codex/AGENTS.md"),
3399
4196
  shared: true,
3400
4197
  format: "markdown"
3401
4198
  }];
@@ -3403,7 +4200,7 @@ function createCodexEmitter() {
3403
4200
  if (!projectRoot)
3404
4201
  return [];
3405
4202
  return [{
3406
- path: path13.join(projectRoot, "AGENTS.md"),
4203
+ path: path14.join(projectRoot, "AGENTS.md"),
3407
4204
  shared: true,
3408
4205
  format: "markdown"
3409
4206
  }];
@@ -3424,7 +4221,7 @@ ${existing ?? ""}
3424
4221
  ${generated}`
3425
4222
  };
3426
4223
  }
3427
- await mkdir2(path13.dirname(dest.path), { recursive: true });
4224
+ await mkdir2(path14.dirname(dest.path), { recursive: true });
3428
4225
  await writeFile2(dest.path, generated, "utf-8");
3429
4226
  return {
3430
4227
  written: true,
@@ -3442,8 +4239,8 @@ var init_codex2 = __esm({
3442
4239
 
3443
4240
  // packages/agents/dist/projection/gemini.js
3444
4241
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
3445
- import path14 from "path";
3446
- import os6 from "os";
4242
+ import path15 from "path";
4243
+ import os7 from "os";
3447
4244
  function renderMarkdown2(source) {
3448
4245
  const skillSections = source.skills.map((s) => `## Skill: ${s.name}
3449
4246
 
@@ -3467,7 +4264,7 @@ function createGeminiEmitter() {
3467
4264
  destinations(scope, projectRoot) {
3468
4265
  if (scope === "user") {
3469
4266
  return [{
3470
- path: path14.join(os6.homedir(), ".gemini/GEMINI.md"),
4267
+ path: path15.join(os7.homedir(), ".gemini/GEMINI.md"),
3471
4268
  shared: true,
3472
4269
  format: "markdown"
3473
4270
  }];
@@ -3475,7 +4272,7 @@ function createGeminiEmitter() {
3475
4272
  if (!projectRoot)
3476
4273
  return [];
3477
4274
  return [{
3478
- path: path14.join(projectRoot, "GEMINI.md"),
4275
+ path: path15.join(projectRoot, "GEMINI.md"),
3479
4276
  shared: true,
3480
4277
  format: "markdown"
3481
4278
  }];
@@ -3496,7 +4293,7 @@ ${existing ?? ""}
3496
4293
  ${generated}`
3497
4294
  };
3498
4295
  }
3499
- await mkdir3(path14.dirname(dest.path), { recursive: true });
4296
+ await mkdir3(path15.dirname(dest.path), { recursive: true });
3500
4297
  await writeFile3(dest.path, generated, "utf-8");
3501
4298
  return {
3502
4299
  written: true,
@@ -3514,8 +4311,8 @@ var init_gemini2 = __esm({
3514
4311
 
3515
4312
  // packages/agents/dist/projection/opencode.js
3516
4313
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
3517
- import path15 from "path";
3518
- import os7 from "os";
4314
+ import path16 from "path";
4315
+ import os8 from "os";
3519
4316
  function renderMarkdown3(source) {
3520
4317
  const skillSections = source.skills.map((s) => `## Skill: ${s.name}
3521
4318
 
@@ -3539,7 +4336,7 @@ function createOpencodeEmitter() {
3539
4336
  destinations(scope, projectRoot) {
3540
4337
  if (scope === "user") {
3541
4338
  return [{
3542
- path: path15.join(os7.homedir(), ".config", "opencode", "AGENTS.md"),
4339
+ path: path16.join(os8.homedir(), ".config", "opencode", "AGENTS.md"),
3543
4340
  shared: true,
3544
4341
  format: "markdown"
3545
4342
  }];
@@ -3547,7 +4344,7 @@ function createOpencodeEmitter() {
3547
4344
  if (!projectRoot)
3548
4345
  return [];
3549
4346
  return [{
3550
- path: path15.join(projectRoot, "AGENTS.md"),
4347
+ path: path16.join(projectRoot, "AGENTS.md"),
3551
4348
  shared: true,
3552
4349
  format: "markdown"
3553
4350
  }];
@@ -3568,7 +4365,7 @@ ${existing ?? ""}
3568
4365
  ${generated}`
3569
4366
  };
3570
4367
  }
3571
- await mkdir4(path15.dirname(dest.path), { recursive: true });
4368
+ await mkdir4(path16.dirname(dest.path), { recursive: true });
3572
4369
  await writeFile4(dest.path, generated, "utf-8");
3573
4370
  return {
3574
4371
  written: true,
@@ -3809,11 +4606,11 @@ var init_app_server_client = __esm({
3809
4606
 
3810
4607
  // packages/agents/dist/codex/config.js
3811
4608
  import { readFile as readFile5 } from "fs/promises";
3812
- import { homedir as homedir7 } from "os";
3813
- import { join as join12 } from "path";
4609
+ import { homedir as homedir9 } from "os";
4610
+ import { join as join14 } from "path";
3814
4611
  async function resolveCodexModel() {
3815
- const home = process.env["CODEX_HOME"] ?? join12(homedir7(), ".codex");
3816
- const configPath = join12(home, "config.toml");
4612
+ const home = process.env["CODEX_HOME"] ?? join14(homedir9(), ".codex");
4613
+ const configPath = join14(home, "config.toml");
3817
4614
  let text;
3818
4615
  try {
3819
4616
  text = await readFile5(configPath, "utf8");
@@ -4335,8 +5132,8 @@ var init_sse_bridge = __esm({
4335
5132
  // packages/agents/dist/interactive/claude.js
4336
5133
  import { execSync as execSync7 } from "child_process";
4337
5134
  import { readFileSync as readFileSync8 } from "fs";
4338
- import { homedir as homedir8 } from "os";
4339
- import { join as join13 } from "path";
5135
+ import { homedir as homedir10 } from "os";
5136
+ import { join as join15 } from "path";
4340
5137
  function probeClaudeSettingsFlag() {
4341
5138
  try {
4342
5139
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -4387,7 +5184,7 @@ function deriveTranscriptPath(sessionId, cwd) {
4387
5184
  if (!sessionId || !cwd)
4388
5185
  return null;
4389
5186
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
4390
- return join13(homedir8(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
5187
+ return join15(homedir10(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
4391
5188
  }
4392
5189
  function readLastAssistantText(transcriptPath) {
4393
5190
  try {
@@ -4846,18 +5643,18 @@ init_dist();
4846
5643
  init_dist3();
4847
5644
  import { Command } from "commander";
4848
5645
  import { execSync as execSync8 } from "child_process";
4849
- import fs13 from "fs";
5646
+ import fs15 from "fs";
4850
5647
  import { stat } from "fs/promises";
4851
- import path16 from "path";
5648
+ import path17 from "path";
4852
5649
  import chalk3 from "chalk";
4853
5650
 
4854
5651
  // packages/cli/src/commands/health-view.ts
4855
5652
  init_dist2();
4856
5653
  init_dist2();
4857
- import { homedir as homedir6 } from "os";
4858
- import { join as join11 } from "path";
5654
+ import { homedir as homedir8 } from "os";
5655
+ import { join as join13 } from "path";
4859
5656
  import chalk2 from "chalk";
4860
- var SOCK = join11(homedir6(), ".config", "squadrant", "squadrant.sock");
5657
+ var SOCK = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
4861
5658
  async function queryHealth(project) {
4862
5659
  try {
4863
5660
  const reply = await sendRequest(SOCK, { kind: "health", project });
@@ -4902,7 +5699,8 @@ function healthRow(c, now) {
4902
5699
  function printServiceHealth(rows, now = Date.now()) {
4903
5700
  console.log(chalk2.bold("\nService Health\n"));
4904
5701
  if (rows == null) {
4905
- console.log(` ${chalk2.red("\u2718")} daemon unreachable \u2014 squadrant liveness is unknown (start the daemon)`);
5702
+ console.log(` ${chalk2.red("\u2718")} daemon unreachable \u2014 squadrant liveness is unknown`);
5703
+ console.log(` ${chalk2.cyan("\u2192")} ${chalk2.dim("Run: squadrant heal daemon")}`);
4906
5704
  return;
4907
5705
  }
4908
5706
  if (rows.length === 0) {
@@ -4950,7 +5748,7 @@ function settingsHaveAgentTeams() {
4950
5748
  try {
4951
5749
  const home = process.env.HOME || "";
4952
5750
  const settings = JSON.parse(
4953
- fs13.readFileSync(`${home}/.claude/settings.json`, "utf-8")
5751
+ fs15.readFileSync(`${home}/.claude/settings.json`, "utf-8")
4954
5752
  );
4955
5753
  return settings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
4956
5754
  } catch {
@@ -4961,7 +5759,7 @@ function pluginInstalled(pluginKey) {
4961
5759
  try {
4962
5760
  const home = process.env.HOME || "";
4963
5761
  const plugins = JSON.parse(
4964
- fs13.readFileSync(
5762
+ fs15.readFileSync(
4965
5763
  `${home}/.claude/plugins/installed_plugins.json`,
4966
5764
  "utf-8"
4967
5765
  )
@@ -4983,29 +5781,61 @@ function tryGetVersion(cmd) {
4983
5781
  return "";
4984
5782
  }
4985
5783
  }
4986
- function check(label, pass) {
5784
+ function check(label, pass, hint) {
4987
5785
  const icon = pass ? chalk3.green("\u2714 PASS") : chalk3.red("\u2718 FAIL");
4988
5786
  console.log(` ${icon} ${label}`);
5787
+ if (!pass && hint) {
5788
+ console.log(` ${chalk3.cyan("\u2192")} ${chalk3.dim(hint)}`);
5789
+ }
4989
5790
  return pass;
4990
5791
  }
4991
5792
  var doctorCommand = new Command("doctor").description("Check system health and prerequisites").action(async () => {
4992
5793
  console.log(chalk3.bold("\nSquadrant Doctor\n"));
4993
5794
  const results = [];
4994
- results.push(check("Claude Code installed", commandExists("claude")));
4995
- results.push(check(`Claude Code version >= ${compatManifest.tools.claude.min}`, claudeVersionOk()));
4996
- results.push(check("Obsidian installed", commandExists("obsidian") || fs13.existsSync("/Applications/Obsidian.app")));
4997
- results.push(check("Node.js >= 18", nodeVersionOk()));
5795
+ results.push(check(
5796
+ "Claude Code installed",
5797
+ commandExists("claude"),
5798
+ "Install from: https://claude.ai/code"
5799
+ ));
5800
+ results.push(check(
5801
+ `Claude Code version >= ${compatManifest.tools.claude.min}`,
5802
+ claudeVersionOk(),
5803
+ `Update Claude Code to >= ${compatManifest.tools.claude.min}`
5804
+ ));
5805
+ results.push(check(
5806
+ "Obsidian installed",
5807
+ commandExists("obsidian") || fs15.existsSync("/Applications/Obsidian.app"),
5808
+ "Install from: https://obsidian.md"
5809
+ ));
5810
+ results.push(check(
5811
+ "Node.js >= 18",
5812
+ nodeVersionOk(),
5813
+ "Install from: https://nodejs.org"
5814
+ ));
4998
5815
  results.push(
4999
5816
  check(
5000
5817
  "Agent Teams enabled (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)",
5001
- settingsHaveAgentTeams()
5818
+ settingsHaveAgentTeams(),
5819
+ "Run: squadrant init (enables automatically), or add to shell profile: export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"
5002
5820
  )
5003
5821
  );
5004
- results.push(check("Plugin: superpowers", pluginInstalled("superpowers@claude-plugins-official")));
5822
+ results.push(check(
5823
+ "Plugin: superpowers",
5824
+ pluginInstalled("superpowers@claude-plugins-official"),
5825
+ "In Claude Code, run: /plugin marketplace add superpowers"
5826
+ ));
5005
5827
  results.push(
5006
- check("Plugin: claude-mem", pluginInstalled("claude-mem@thedotmack"))
5828
+ check(
5829
+ "Plugin: claude-mem",
5830
+ pluginInstalled("claude-mem@thedotmack"),
5831
+ "In Claude Code, run: /plugin marketplace add thedotmack/claude-mem"
5832
+ )
5007
5833
  );
5008
- results.push(check("Plugin: context7", pluginInstalled("context7@claude-plugins-official")));
5834
+ results.push(check(
5835
+ "Plugin: context7",
5836
+ pluginInstalled("context7@claude-plugins-official"),
5837
+ "In Claude Code, run: /plugin marketplace add context7"
5838
+ ));
5009
5839
  const config = loadConfig();
5010
5840
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
5011
5841
  const probeResults = await runtimes.probeAll();
@@ -5013,7 +5843,8 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5013
5843
  const globalProbe = probeResults[globalRuntimeName];
5014
5844
  results.push(check(
5015
5845
  `Runtime '${globalRuntimeName}' installed`,
5016
- !!globalProbe?.installed
5846
+ !!globalProbe?.installed,
5847
+ globalRuntimeName === "cmux" ? "Install: npm install -g cmux or download from https://cmux.dev" : void 0
5017
5848
  ));
5018
5849
  const overrides = /* @__PURE__ */ new Set();
5019
5850
  for (const proj of Object.values(config.projects)) {
@@ -5031,7 +5862,8 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5031
5862
  const hubProbe = await hubDriver.probe();
5032
5863
  results.push(check(
5033
5864
  `Workspace '${config.workspace ?? "obsidian"}' \u2014 hub reachable`,
5034
- hubProbe.installed && hubProbe.rootExists
5865
+ hubProbe.installed && hubProbe.rootExists,
5866
+ "Run: squadrant init to scaffold the hub vault"
5035
5867
  ));
5036
5868
  for (const [name] of Object.entries(config.projects)) {
5037
5869
  const spokeDriver = workspaces.forProject(name, config);
@@ -5066,7 +5898,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5066
5898
  const emitter = projectionRegistry.get(name);
5067
5899
  const [userDest] = emitter.destinations("user");
5068
5900
  if (!userDest) continue;
5069
- const dir = path16.dirname(userDest.path);
5901
+ const dir = path17.dirname(userDest.path);
5070
5902
  let status;
5071
5903
  try {
5072
5904
  await stat(dir);
@@ -5079,9 +5911,10 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5079
5911
  results.push(
5080
5912
  check(
5081
5913
  "Squadrant config exists",
5082
- fs13.existsSync(
5914
+ fs15.existsSync(
5083
5915
  process.env.SQUADRANT_CONFIG || `${process.env.HOME}/.config/squadrant/config.json`
5084
- )
5916
+ ),
5917
+ "Run: squadrant init"
5085
5918
  )
5086
5919
  );
5087
5920
  const passed = results.filter(Boolean).length;
@@ -5151,152 +5984,193 @@ ${passed === total ? chalk3.green("All checks passed") : chalk3.yellow(`${passed
5151
5984
  init_dist();
5152
5985
  init_dist3();
5153
5986
  init_dist();
5987
+ init_dist4();
5154
5988
  import { Command as Command2 } from "commander";
5155
- import fs14 from "fs";
5156
- import path17 from "path";
5157
- import os8 from "os";
5989
+ import fs16 from "fs";
5990
+ import path18 from "path";
5991
+ import os9 from "os";
5992
+ import readline from "readline";
5158
5993
  import chalk4 from "chalk";
5159
5994
  function findPackageRoot() {
5160
- let dir = path17.dirname(new URL(import.meta.url).pathname);
5995
+ let dir = path18.dirname(new URL(import.meta.url).pathname);
5161
5996
  while (dir !== "/") {
5162
- if (fs14.existsSync(path17.join(dir, "package.json"))) return dir;
5163
- dir = path17.dirname(dir);
5997
+ if (fs16.existsSync(path18.join(dir, "package.json"))) return dir;
5998
+ dir = path18.dirname(dir);
5164
5999
  }
5165
6000
  return process.cwd();
5166
6001
  }
5167
6002
  function copyDirRecursive(src, dest) {
5168
- fs14.mkdirSync(dest, { recursive: true });
5169
- for (const entry of fs14.readdirSync(src, { withFileTypes: true })) {
5170
- const srcPath = path17.join(src, entry.name);
5171
- const destPath = path17.join(dest, entry.name);
6003
+ fs16.mkdirSync(dest, { recursive: true });
6004
+ for (const entry of fs16.readdirSync(src, { withFileTypes: true })) {
6005
+ const srcPath = path18.join(src, entry.name);
6006
+ const destPath = path18.join(dest, entry.name);
5172
6007
  if (entry.isDirectory()) {
5173
6008
  copyDirRecursive(srcPath, destPath);
5174
6009
  } else {
5175
- fs14.copyFileSync(srcPath, destPath);
6010
+ fs16.copyFileSync(srcPath, destPath);
5176
6011
  }
5177
6012
  }
5178
6013
  }
5179
- var initCommand = new Command2("init").description("First-time setup: scaffold hub vault, scripts, and config").option("--hub <path>", "Hub vault path", "~/squadrant-hub").action((opts) => {
6014
+ function stepHeader(n, total, label) {
6015
+ console.log(chalk4.bold(`
6016
+ ${n}/${total} ${label}`));
6017
+ }
6018
+ function promptLine(question) {
6019
+ return new Promise((resolve3) => {
6020
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6021
+ rl.question(question, (answer) => {
6022
+ rl.close();
6023
+ resolve3(answer.trim());
6024
+ });
6025
+ });
6026
+ }
6027
+ var initCommand = new Command2("init").description("Guided first-time setup: hub vault, agents, plugins, projects (re-run-safe)").option("--hub <path>", "Hub vault path", "~/squadrant-hub").action(async (opts) => {
5180
6028
  const hubPath = resolveHome(opts.hub);
5181
6029
  const pkgRoot = findPackageRoot();
5182
- const configDir = path17.join(os8.homedir(), ".config", "squadrant");
6030
+ const configDir = path18.join(os9.homedir(), ".config", "squadrant");
6031
+ const isTTY = process.stdin.isTTY === true;
5183
6032
  console.log(chalk4.bold("\nSquadrant Init\n"));
5184
- const registry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
6033
+ if (!isTTY) {
6034
+ console.log(" Run these steps to get started:\n");
6035
+ console.log(chalk4.bold(" 1/5 Hub vault"));
6036
+ console.log(chalk4.cyan(` squadrant init --hub ${opts.hub}`));
6037
+ console.log(chalk4.bold("\n 2/5 Agent + projection setup"));
6038
+ console.log(" (handled automatically by: " + chalk4.cyan("squadrant init") + ")");
6039
+ console.log(chalk4.bold("\n 3/5 Plugins \u2014 open Claude Code and run:"));
6040
+ console.log(chalk4.cyan(" /plugin marketplace add superpowers"));
6041
+ console.log(chalk4.cyan(" /plugin marketplace add thedotmack/claude-mem"));
6042
+ console.log(chalk4.cyan(" /plugin marketplace add context7"));
6043
+ console.log(chalk4.bold("\n 4/5 Register first project"));
6044
+ console.log(chalk4.cyan(" squadrant projects add <name> <path>"));
6045
+ console.log(chalk4.bold("\n 5/5 Telegram (optional)"));
6046
+ console.log(chalk4.cyan(" squadrant telegram setup"));
6047
+ console.log(chalk4.bold("\n Then:"));
6048
+ console.log(chalk4.cyan(" squadrant launch <projectname>\n"));
6049
+ return;
6050
+ }
6051
+ const wsRegistry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
5185
6052
  try {
5186
- if (fs14.existsSync(DEFAULT_CONFIG_PATH)) {
5187
- const existing = JSON.parse(fs14.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
5188
- const wsName = existing.workspace ?? "obsidian";
5189
- registry.get(wsName);
6053
+ if (fs16.existsSync(DEFAULT_CONFIG_PATH)) {
6054
+ const existing = JSON.parse(fs16.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
6055
+ wsRegistry.get(existing.workspace ?? "obsidian");
5190
6056
  }
5191
6057
  } catch (err) {
5192
6058
  console.log(chalk4.red(` \u2718 ${err.message}`));
5193
6059
  return;
5194
6060
  }
5195
- if (fs14.existsSync(DEFAULT_CONFIG_PATH)) {
5196
- console.log(chalk4.yellow(" \u26A0 Config already exists, skipping creation"));
6061
+ stepHeader(1, 5, "Hub vault");
6062
+ if (fs16.existsSync(DEFAULT_CONFIG_PATH)) {
6063
+ console.log(chalk4.yellow(" \u26A0 Config already exists, skipping creation"));
5197
6064
  } else {
5198
6065
  const config = getDefaultConfig();
5199
6066
  config.hubVault = hubPath;
5200
6067
  saveConfig(config);
5201
- console.log(chalk4.green(` \u2714 Config created at ${DEFAULT_CONFIG_PATH}`));
6068
+ console.log(chalk4.green(` \u2714 Config created at ${DEFAULT_CONFIG_PATH}`));
5202
6069
  }
5203
- const hubTemplate = path17.join(pkgRoot, "obsidian", "hub");
5204
- if (fs14.existsSync(hubPath)) {
5205
- console.log(chalk4.yellow(` \u26A0 Hub vault already exists at ${hubPath}, skipping`));
5206
- } else if (fs14.existsSync(hubTemplate)) {
6070
+ const hubTemplate = path18.join(pkgRoot, "obsidian", "hub");
6071
+ if (fs16.existsSync(hubPath)) {
6072
+ console.log(chalk4.yellow(` \u26A0 Hub vault already exists at ${hubPath}`));
6073
+ } else if (fs16.existsSync(hubTemplate)) {
5207
6074
  copyDirRecursive(hubTemplate, hubPath);
5208
- console.log(chalk4.green(` \u2714 Hub vault scaffolded at ${hubPath}`));
6075
+ console.log(chalk4.green(` \u2714 Hub vault scaffolded at ${hubPath}`));
5209
6076
  } else {
5210
- fs14.mkdirSync(hubPath, { recursive: true });
5211
- console.log(chalk4.yellow(` \u26A0 Hub template not found; created empty dir at ${hubPath}`));
6077
+ fs16.mkdirSync(hubPath, { recursive: true });
6078
+ console.log(chalk4.yellow(` \u26A0 Hub template not found; created empty directory at ${hubPath}`));
5212
6079
  }
5213
- const hubDashboardSrc = path17.join(pkgRoot, "obsidian", "hub", "dashboard.md");
5214
- const hubDashboardDest = path17.join(hubPath, "dashboard.md");
5215
- if (fs14.existsSync(hubDashboardSrc)) {
5216
- fs14.copyFileSync(hubDashboardSrc, hubDashboardDest);
5217
- console.log(chalk4.green(` \u2714 Dashboard page refreshed at ${hubDashboardDest}`));
6080
+ const hubDashboardSrc = path18.join(pkgRoot, "obsidian", "hub", "dashboard.md");
6081
+ const hubDashboardDest = path18.join(hubPath, "dashboard.md");
6082
+ if (fs16.existsSync(hubDashboardSrc)) {
6083
+ fs16.copyFileSync(hubDashboardSrc, hubDashboardDest);
6084
+ console.log(chalk4.green(` \u2714 Dashboard refreshed`));
5218
6085
  }
5219
- const projectsDir = path17.join(hubPath, "projects");
5220
- fs14.mkdirSync(projectsDir, { recursive: true });
6086
+ fs16.mkdirSync(path18.join(hubPath, "projects"), { recursive: true });
5221
6087
  ensureRuntimeSynced({ sourceRoot: pkgRoot, runtimeRoot: configDir });
5222
- console.log(chalk4.green(` \u2714 Runtime assets synced to ${configDir}`));
5223
- const settingsPath = path17.join(os8.homedir(), ".claude", "settings.json");
6088
+ console.log(chalk4.green(` \u2714 Runtime assets synced to ${configDir}`));
6089
+ stepHeader(2, 5, "Agent + projection setup");
6090
+ const settingsPath = path18.join(os9.homedir(), ".claude", "settings.json");
5224
6091
  try {
5225
6092
  let settings = {};
5226
- if (fs14.existsSync(settingsPath)) {
5227
- settings = JSON.parse(fs14.readFileSync(settingsPath, "utf-8"));
6093
+ if (fs16.existsSync(settingsPath)) {
6094
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
5228
6095
  }
5229
6096
  const env = settings.env || {};
5230
6097
  if (env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
5231
6098
  settings.env = { ...env, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
5232
- fs14.mkdirSync(path17.dirname(settingsPath), { recursive: true });
5233
- fs14.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5234
- console.log(chalk4.green(" \u2714 Agent Teams enabled in ~/.claude/settings.json"));
6099
+ fs16.mkdirSync(path18.dirname(settingsPath), { recursive: true });
6100
+ fs16.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
6101
+ console.log(chalk4.green(" \u2714 Agent Teams enabled in ~/.claude/settings.json"));
5235
6102
  } else {
5236
- console.log(chalk4.green(" \u2714 Agent Teams already enabled"));
6103
+ console.log(chalk4.green(" \u2714 Agent Teams already enabled"));
5237
6104
  }
5238
6105
  } catch {
5239
- console.log(chalk4.yellow(" \u26A0 Could not update ~/.claude/settings.json"));
6106
+ console.log(chalk4.yellow(" \u26A0 Could not update ~/.claude/settings.json"));
6107
+ console.log(chalk4.dim(" Add manually to shell profile: export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"));
5240
6108
  }
5241
- console.log(chalk4.bold("\nManual steps required:\n"));
5242
- console.log(" 1. Install plugins in Claude Code:");
5243
- console.log(chalk4.cyan(" /plugin marketplace add thedotmack/claude-mem"));
5244
- console.log(chalk4.cyan(" /plugin install claude-mem"));
5245
- console.log(" Install context7 and superpowers from /plugin marketplace");
5246
- console.log("");
5247
- console.log(" 2. Install cmux if not already installed:");
5248
- console.log(chalk4.cyan(" npm install -g cmux"));
5249
- console.log("");
5250
- console.log(" 3. Open Obsidian and add the hub vault:");
5251
- console.log(chalk4.cyan(` ${hubPath}`));
5252
- console.log("");
5253
- console.log(" 4. Run " + chalk4.cyan("squadrant doctor") + " to verify setup\n");
5254
- if (!fs14.existsSync("/Applications/cmux.app")) {
5255
- console.log(chalk4.yellow(" \u26A0 cmux not found \u2014 download from https://cmux.dev\n"));
6109
+ try {
6110
+ const projRegistry = new ProjectionRegistry({
6111
+ cursor: createCursorEmitter,
6112
+ codex: createCodexEmitter,
6113
+ gemini: createGeminiEmitter,
6114
+ opencode: createOpencodeEmitter
6115
+ });
6116
+ const workspace = createObsidianDriver({ root: process.cwd() });
6117
+ const source = await readUserLevelSource(workspace, { pkgRoot });
6118
+ for (const name of projRegistry.list()) {
6119
+ const emitter = projRegistry.get(name);
6120
+ for (const dest of emitter.destinations("user")) {
6121
+ const result = await emitter.emit(source, dest);
6122
+ if (result.written) {
6123
+ console.log(chalk4.green(` \u2714 ${name} \u2192 ${dest.path}`));
6124
+ } else {
6125
+ console.log(chalk4.dim(` - ${name} \u2192 ${dest.path} (unchanged)`));
6126
+ }
6127
+ }
6128
+ }
6129
+ } catch (err) {
6130
+ console.log(chalk4.yellow(` \u26A0 Projection emit skipped: ${err.message}`));
6131
+ }
6132
+ stepHeader(3, 5, "Plugins");
6133
+ console.log(" Install these plugins inside Claude Code:\n");
6134
+ console.log(chalk4.cyan(" /plugin marketplace add superpowers"));
6135
+ console.log(chalk4.cyan(" /plugin marketplace add thedotmack/claude-mem"));
6136
+ console.log(chalk4.cyan(" /plugin marketplace add context7\n"));
6137
+ console.log(chalk4.dim(" Squadrant never auto-installs plugins \u2014 open Claude Code and run the commands above."));
6138
+ stepHeader(4, 5, "Register first project");
6139
+ const projectPath = await promptLine(
6140
+ chalk4.cyan(" Absolute path to your first project (Enter to skip): ")
6141
+ );
6142
+ if (projectPath) {
6143
+ const projectName = path18.basename(projectPath);
6144
+ console.log(chalk4.bold(`
6145
+ Run this to register it:`));
6146
+ console.log(chalk4.cyan(` squadrant projects add ${projectName} ${projectPath}
6147
+ `));
6148
+ } else {
6149
+ console.log(chalk4.dim(" Skipped. Register later with:"));
6150
+ console.log(chalk4.cyan(" squadrant projects add <name> <path>"));
6151
+ }
6152
+ stepHeader(5, 5, "Telegram (optional)");
6153
+ console.log(" Get notified on your phone when crews complete tasks:");
6154
+ console.log(chalk4.cyan(" squadrant telegram setup\n"));
6155
+ let launchTarget = "<projectname>";
6156
+ try {
6157
+ const cfg = loadConfig();
6158
+ const first = Object.keys(cfg.projects)[0];
6159
+ if (first) launchTarget = first;
6160
+ } catch {
5256
6161
  }
6162
+ console.log(chalk4.bold.green("\n \u2714 You are ready!\n"));
6163
+ console.log(` Run: ${chalk4.cyan(`squadrant launch ${launchTarget}`)}
6164
+ `);
5257
6165
  });
5258
6166
 
5259
6167
  // packages/cli/src/commands/projects.ts
5260
6168
  init_dist();
6169
+ init_dist2();
5261
6170
  import { Command as Command3 } from "commander";
5262
- import fs15 from "fs";
5263
- import path18 from "path";
6171
+ import fs17 from "fs";
6172
+ import path19 from "path";
5264
6173
  import chalk5 from "chalk";
5265
-
5266
- // packages/cli/src/control/restart-daemon.ts
5267
- init_dist2();
5268
- import { execFileSync as execFileSync6 } from "child_process";
5269
- import { existsSync as existsSync9 } from "fs";
5270
- import { homedir as homedir9 } from "os";
5271
- import { join as join14 } from "path";
5272
- var DEFAULT_SOCK_PATH = join14(homedir9(), ".config", "squadrant", "squadrant.sock");
5273
- function defaultIsRunning() {
5274
- return existsSync9(DEFAULT_SOCK_PATH);
5275
- }
5276
- function defaultRunKickstart() {
5277
- const uid = process.getuid?.() ?? 0;
5278
- const target = `gui/${uid}/${LABEL}`;
5279
- if (tryAcquireDaemonLock()) {
5280
- try {
5281
- execFileSync6("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
5282
- } finally {
5283
- releaseDaemonLock();
5284
- }
5285
- }
5286
- }
5287
- function restartDaemonIfRunning(opts) {
5288
- const env = opts.env ?? process.env;
5289
- if (env["VITEST"] || opts.noRestart) return "skipped-opt-out";
5290
- const isRunning = opts.isRunning ?? defaultIsRunning;
5291
- if (!isRunning()) return "skipped-not-running";
5292
- const log = opts.log ?? console.log;
5293
- log(`\u21BB restarting daemon to apply ${opts.reason}\u2026`);
5294
- const runKickstart = opts.runKickstart ?? defaultRunKickstart;
5295
- runKickstart();
5296
- return "restarted";
5297
- }
5298
-
5299
- // packages/cli/src/commands/projects.ts
5300
6174
  function restartAfterProjectsAdd(opts) {
5301
6175
  const doRestart = opts.doRestart ?? restartDaemonIfRunning;
5302
6176
  const outcome = doRestart({ reason: "project registration", noRestart: opts.noRestart });
@@ -5307,22 +6181,22 @@ function restartAfterProjectsAdd(opts) {
5307
6181
  }
5308
6182
  }
5309
6183
  function findPackageRoot2() {
5310
- let dir = path18.dirname(new URL(import.meta.url).pathname);
6184
+ let dir = path19.dirname(new URL(import.meta.url).pathname);
5311
6185
  while (dir !== "/") {
5312
- if (fs15.existsSync(path18.join(dir, "package.json"))) return dir;
5313
- dir = path18.dirname(dir);
6186
+ if (fs17.existsSync(path19.join(dir, "package.json"))) return dir;
6187
+ dir = path19.dirname(dir);
5314
6188
  }
5315
6189
  return process.cwd();
5316
6190
  }
5317
6191
  function copyDirRecursive2(src, dest) {
5318
- fs15.mkdirSync(dest, { recursive: true });
5319
- for (const entry of fs15.readdirSync(src, { withFileTypes: true })) {
5320
- const srcPath = path18.join(src, entry.name);
5321
- const destPath = path18.join(dest, entry.name);
6192
+ fs17.mkdirSync(dest, { recursive: true });
6193
+ for (const entry of fs17.readdirSync(src, { withFileTypes: true })) {
6194
+ const srcPath = path19.join(src, entry.name);
6195
+ const destPath = path19.join(dest, entry.name);
5322
6196
  if (entry.isDirectory()) {
5323
6197
  copyDirRecursive2(srcPath, destPath);
5324
6198
  } else {
5325
- fs15.copyFileSync(srcPath, destPath);
6199
+ fs17.copyFileSync(srcPath, destPath);
5326
6200
  }
5327
6201
  }
5328
6202
  }
@@ -5358,7 +6232,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5358
6232
  process.exit(1);
5359
6233
  }
5360
6234
  const resolvedPath = resolveHome(projectPath);
5361
- if (!fs15.existsSync(path18.join(resolvedPath, ".git"))) {
6235
+ if (!fs17.existsSync(path19.join(resolvedPath, ".git"))) {
5362
6236
  console.log(chalk5.yellow(`
5363
6237
  \u26A0 No .git found at ${resolvedPath}. Make sure this is the project root, not a parent directory.
5364
6238
  `));
@@ -5411,7 +6285,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5411
6285
  \u26A0 Group '${group}' already has '${primary[0]}' as primary. Overriding.`));
5412
6286
  }
5413
6287
  }
5414
- const spokeVault = opts.spoke ? resolveHome(opts.spoke) : path18.join(config.hubVault, "spokes", name);
6288
+ const spokeVault = opts.spoke ? resolveHome(opts.spoke) : path19.join(config.hubVault, "spokes", name);
5415
6289
  const project = {
5416
6290
  path: resolvedPath,
5417
6291
  captainName,
@@ -5426,20 +6300,20 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5426
6300
  \u2714 Project '${name}' registered`));
5427
6301
  restartAfterProjectsAdd({ noRestart: opts.restart === false });
5428
6302
  const pkgRoot = findPackageRoot2();
5429
- const spokeTemplate = path18.join(pkgRoot, "obsidian", "spoke");
5430
- if (fs15.existsSync(spokeVault)) {
6303
+ const spokeTemplate = path19.join(pkgRoot, "obsidian", "spoke");
6304
+ if (fs17.existsSync(spokeVault)) {
5431
6305
  console.log(chalk5.yellow(` \u26A0 Spoke vault already exists at ${spokeVault}, skipping scaffold`));
5432
- } else if (fs15.existsSync(spokeTemplate)) {
6306
+ } else if (fs17.existsSync(spokeTemplate)) {
5433
6307
  copyDirRecursive2(spokeTemplate, spokeVault);
5434
- const statusPath = path18.join(spokeVault, "status.md");
5435
- if (fs15.existsSync(statusPath)) {
5436
- const content = fs15.readFileSync(statusPath, "utf-8");
6308
+ const statusPath = path19.join(spokeVault, "status.md");
6309
+ if (fs17.existsSync(statusPath)) {
6310
+ const content = fs17.readFileSync(statusPath, "utf-8");
5437
6311
  const updated = content.replace(/^project: unnamed/m, `project: ${name}`);
5438
- fs15.writeFileSync(statusPath, updated);
6312
+ fs17.writeFileSync(statusPath, updated);
5439
6313
  }
5440
6314
  console.log(chalk5.green(` \u2714 Spoke vault scaffolded at ${spokeVault}`));
5441
6315
  } else {
5442
- fs15.mkdirSync(spokeVault, { recursive: true });
6316
+ fs17.mkdirSync(spokeVault, { recursive: true });
5443
6317
  console.log(chalk5.yellow(` \u26A0 Spoke template not found; created empty dir at ${spokeVault}`));
5444
6318
  }
5445
6319
  console.log("");
@@ -5540,10 +6414,10 @@ init_dist4();
5540
6414
  init_dist();
5541
6415
  import { Command as Command5 } from "commander";
5542
6416
  import { execSync as execSync9 } from "child_process";
5543
- import path19 from "path";
5544
- import os9 from "os";
6417
+ import path20 from "path";
6418
+ import os10 from "os";
5545
6419
  import chalk7 from "chalk";
5546
- var TEMPLATES_DIR = path19.join(os9.homedir(), ".config", "squadrant", "templates");
6420
+ var TEMPLATES_DIR2 = path20.join(os10.homedir(), ".config", "squadrant", "templates");
5547
6421
  var TASK_PROMPTS = {
5548
6422
  briefing: "Run your daily briefing using the squadrant:command-ops skill. Read all spoke handoffs, yesterday's logs, current status; produce a concise cross-project briefing; save to {hubVault}/daily-logs/YYYY-MM-DD.md; then exit.",
5549
6423
  "learnings-review": "Run a learnings review using the squadrant:command-ops skill. Scan {spokeVault}/learnings across all projects, identify cross-project patterns, propose captured-skill or fix actions, and exit when done.",
@@ -5576,7 +6450,7 @@ async function runCommandSpawn(input) {
5576
6450
  if (!agent) {
5577
6451
  throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
5578
6452
  }
5579
- const promptFile = path19.join(TEMPLATES_DIR, `command.${agent.templateSuffix}.md`);
6453
+ const promptFile = path20.join(TEMPLATES_DIR2, `command.${agent.templateSuffix}.md`);
5580
6454
  const cliCommand = agent.buildCommand({
5581
6455
  prompt,
5582
6456
  workdir: process.cwd(),
@@ -5601,35 +6475,11 @@ var commandCommand = new Command5("command").description("Spawn a one-shot Comma
5601
6475
 
5602
6476
  // packages/cli/src/commands/crew.ts
5603
6477
  init_dist();
5604
- init_dist();
5605
- import { Command as Command9 } from "commander";
5606
- import fs16 from "fs";
5607
- import path20 from "path";
5608
- import os10 from "os";
5609
- import chalk9 from "chalk";
5610
-
5611
- // packages/cli/src/control/crew-routing.ts
5612
- function resolveCrewRoute(taskText, config) {
5613
- const rules = config.defaults.crewRouting?.rules;
5614
- if (!rules || rules.length === 0) return null;
5615
- for (const rule2 of rules) {
5616
- const re = new RegExp(rule2.match, "i");
5617
- if (re.test(taskText)) {
5618
- return {
5619
- agent: rule2.agent,
5620
- ...rule2.model !== void 0 ? { model: rule2.model } : {},
5621
- tier: rule2.tier,
5622
- matchedRule: rule2.match
5623
- };
5624
- }
5625
- }
5626
- return null;
5627
- }
5628
-
5629
- // packages/cli/src/commands/crew.ts
5630
- init_dist3();
5631
6478
  init_dist3();
5632
6479
  init_dist4();
6480
+ init_dist2();
6481
+ import { Command as Command9 } from "commander";
6482
+ import chalk9 from "chalk";
5633
6483
 
5634
6484
  // packages/cli/src/commands/crew-control.ts
5635
6485
  init_dist2();
@@ -5637,9 +6487,9 @@ init_dist2();
5637
6487
  init_dist4();
5638
6488
  import { Command as Command8 } from "commander";
5639
6489
  import { createConnection as createConnection3 } from "net";
5640
- import { randomUUID as randomUUID3 } from "crypto";
5641
- import { homedir as homedir11 } from "os";
5642
- import { join as join16 } from "path";
6490
+ import { randomUUID as randomUUID4 } from "crypto";
6491
+ import { homedir as homedir12 } from "os";
6492
+ import { join as join17 } from "path";
5643
6493
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
5644
6494
 
5645
6495
  // packages/cli/src/commands/crew-output.ts
@@ -5698,11 +6548,11 @@ init_dist2();
5698
6548
  import { Command as Command6 } from "commander";
5699
6549
  import chalk8 from "chalk";
5700
6550
  import { createConnection as createConnection2 } from "net";
5701
- import { homedir as homedir10 } from "os";
5702
- import { join as join15 } from "path";
6551
+ import { homedir as homedir11 } from "os";
6552
+ import { join as join16 } from "path";
5703
6553
  import { createInterface } from "readline";
5704
6554
  function socketPath() {
5705
- return process.env.SQUADRANTD_SOCK ?? join15(homedir10(), ".config", "squadrant", "squadrant.sock");
6555
+ return process.env.SQUADRANTD_SOCK ?? join16(homedir11(), ".config", "squadrant", "squadrant.sock");
5706
6556
  }
5707
6557
  function rule(width, ch = "\u2500") {
5708
6558
  return ch.repeat(Math.max(0, width));
@@ -5927,7 +6777,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
5927
6777
  if (opts.provider !== "codex") {
5928
6778
  throw new Error(`crew chat is implemented for provider=codex only (got '${opts.provider}')`);
5929
6779
  }
5930
- const pane = await runCrewSpawn({
6780
+ const pane = await runCrewSpawn2({
5931
6781
  project: opts.project,
5932
6782
  task: "(interactive)",
5933
6783
  agent: "codex",
@@ -5938,7 +6788,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
5938
6788
  });
5939
6789
 
5940
6790
  // packages/cli/src/commands/crew-control.ts
5941
- var SOCK2 = join16(homedir11(), ".config", "squadrant", "squadrant.sock");
6791
+ var SOCK2 = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
5942
6792
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
5943
6793
  async function sendCodexFirstTurn(taskId, text) {
5944
6794
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
@@ -5965,11 +6815,11 @@ async function sendCodexFirstTurn(taskId, text) {
5965
6815
  }
5966
6816
  function buildDispatchRequest(o) {
5967
6817
  const now = Date.now();
5968
- const attemptId = randomUUID3();
6818
+ const attemptId = randomUUID4();
5969
6819
  return {
5970
6820
  kind: "dispatch",
5971
6821
  record: {
5972
- id: randomUUID3(),
6822
+ id: randomUUID4(),
5973
6823
  project: o.project,
5974
6824
  provider: o.provider,
5975
6825
  mode: o.mode,
@@ -6043,9 +6893,9 @@ function buildSignalRequest(signal, o) {
6043
6893
  return { kind: "event", project, event };
6044
6894
  }
6045
6895
  function defaultWriteResult(id, payload) {
6046
- const dir = join16(homedir11(), ".config", "squadrant", "state", "_results");
6896
+ const dir = join17(homedir12(), ".config", "squadrant", "state", "_results");
6047
6897
  mkdirSync6(dir, { recursive: true });
6048
- const file = join16(dir, `${id}.txt`);
6898
+ const file = join17(dir, `${id}.txt`);
6049
6899
  writeFileSync7(file, payload);
6050
6900
  return file;
6051
6901
  }
@@ -6146,7 +6996,7 @@ addControlPlaneCrewCommands(crewControlCommand);
6146
6996
  // packages/cli/src/lib/per-crew-settings.ts
6147
6997
  init_dist4();
6148
6998
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
6149
- import { join as join17 } from "path";
6999
+ import { join as join18 } from "path";
6150
7000
  var CREW_PERMISSION_ALLOWLIST = [
6151
7001
  // git — read + safe mutations (reset/clean/config intentionally excluded)
6152
7002
  "Bash(git status:*)",
@@ -6226,6 +7076,9 @@ var CREW_PERMISSION_ALLOWLIST = [
6226
7076
  "Bash(sleep:*)",
6227
7077
  "Bash(printf:*)"
6228
7078
  ];
7079
+ function healStaleCockpitRefs(raw) {
7080
+ return raw.replaceAll("cockpit crew _hook", "squadrant crew _hook").replaceAll(".config/cockpit", ".config/squadrant").replaceAll("cockpit-hub", "squadrant-hub");
7081
+ }
6229
7082
  function mergeCrewPermissions(settings) {
6230
7083
  const next = structuredClone(settings ?? {});
6231
7084
  const permissions = next.permissions ??= {};
@@ -6234,12 +7087,12 @@ function mergeCrewPermissions(settings) {
6234
7087
  return next;
6235
7088
  }
6236
7089
  function writePerCrewSettingsLocal(o) {
6237
- const dir = join17(o.projectCwd, ".claude");
7090
+ const dir = join18(o.projectCwd, ".claude");
6238
7091
  mkdirSync7(dir, { recursive: true });
6239
- const file = join17(dir, "settings.local.json");
7092
+ const file = join18(dir, "settings.local.json");
6240
7093
  let existing = {};
6241
7094
  try {
6242
- const raw = readFileSync9(file, "utf-8");
7095
+ const raw = healStaleCockpitRefs(readFileSync9(file, "utf-8"));
6243
7096
  existing = JSON.parse(raw);
6244
7097
  } catch {
6245
7098
  }
@@ -6249,304 +7102,86 @@ function writePerCrewSettingsLocal(o) {
6249
7102
  return file;
6250
7103
  }
6251
7104
  function writePerCrewOpencodeConfig(o) {
6252
- const dir = join17(o.stateRoot, o.project, o.taskId);
7105
+ const dir = join18(o.stateRoot, o.project, o.taskId);
6253
7106
  mkdirSync7(dir, { recursive: true });
6254
- const file = join17(dir, "opencode.json");
7107
+ const file = join18(dir, "opencode.json");
6255
7108
  const config = {
6256
7109
  permission: {
6257
7110
  read: "allow",
6258
7111
  edit: "allow",
6259
- glob: "allow",
6260
- grep: "allow",
6261
- bash: o.gateBash ? "ask" : "allow",
6262
- webfetch: "allow",
6263
- websearch: "allow",
6264
- task: "allow",
6265
- lsp: "allow",
6266
- external_directory: { "**": "allow" }
6267
- }
6268
- };
6269
- writeFileSync8(file, JSON.stringify(config, null, 2));
6270
- return file;
6271
- }
6272
-
6273
- // packages/cli/src/commands/crew.ts
6274
- init_dist2();
6275
- var TEMPLATES_DIR2 = path20.join(os10.homedir(), ".config", "squadrant", "templates");
6276
- async function runCrewSpawn(input) {
6277
- const config = loadConfig();
6278
- const proj = config.projects[input.project];
6279
- if (!proj) {
6280
- throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
6281
- }
6282
- const runtime = new RuntimeRegistry({ cmux: createCmuxDriver() }).forProject(input.project, config);
6283
- const captain = await runtime.status(proj.captainName);
6284
- if (!captain) {
6285
- throw new Error(
6286
- `Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`
6287
- );
6288
- }
6289
- const existing = await listProjectCrews(runtime, captain.id, input.project);
6290
- const existingTitles = existing.map((s) => s.title);
6291
- if (input.name) {
6292
- const wantTitle = titleFor(input.project, input.name);
6293
- if (existingTitles.includes(wantTitle)) {
6294
- throw new Error(
6295
- `Crew '${input.name}' already exists for ${input.project}. Use 'squadrant crew send ${input.project} ${input.name}' to send a follow-up, or pick a different --name.`
6296
- );
6297
- }
6298
- }
6299
- const name = input.name ?? nextAutoName(existingTitles, input.project);
6300
- const spawnCwd = !input.shared ? addWorktree({
6301
- repoRoot: proj.path,
6302
- worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
6303
- project: input.project,
6304
- name,
6305
- base: resolveWorktreeBase(proj.path)
6306
- }) : proj.path;
6307
- const route = !input.agentExplicit && !input.model ? resolveCrewRoute(input.task, config) : null;
6308
- if (route) {
6309
- console.log(chalk9.dim(`routed: tier=${route.tier} \u2192 ${route.agent}${route.model ? `/${route.model}` : ""} (rule: "${route.matchedRule}")`));
6310
- }
7112
+ glob: "allow",
7113
+ grep: "allow",
7114
+ bash: o.gateBash ? "ask" : "allow",
7115
+ webfetch: "allow",
7116
+ websearch: "allow",
7117
+ task: "allow",
7118
+ lsp: "allow",
7119
+ external_directory: { "**": "allow" }
7120
+ }
7121
+ };
7122
+ writeFileSync8(file, JSON.stringify(config, null, 2));
7123
+ return file;
7124
+ }
7125
+
7126
+ // packages/cli/src/commands/crew.ts
7127
+ async function runCrewSpawn2(input) {
7128
+ const config = loadConfig();
7129
+ const runtime = new RuntimeRegistry({ cmux: createCmuxDriver() }).forProject(input.project, config);
6311
7130
  const agents = new CapabilityRegistry({
6312
7131
  claude: createClaudeDriver(),
6313
7132
  codex: createCodexDriver(),
6314
7133
  gemini: createGeminiDriver(),
6315
7134
  opencode: createOpencodeDriver()
6316
7135
  });
6317
- const agentName = route?.agent ?? input.agent ?? "claude";
6318
- const agent = agents.get(agentName);
6319
- if (!agent) {
6320
- throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
6321
- }
6322
- if (agentName === "codex") {
6323
- const codexRoleFile = path20.join(TEMPLATES_DIR2, `crew.${agent.templateSuffix}.md`);
6324
- const roleInstructions = fs16.existsSync(codexRoleFile) ? fs16.readFileSync(codexRoleFile, "utf8") : void 0;
6325
- return runCodexInteractiveSpawn({
6326
- project: input.project,
6327
- task: input.task,
6328
- cwd: spawnCwd,
6329
- runtime,
6330
- workspaceId: captain.id,
6331
- name,
6332
- direction: input.direction ?? "tab",
6333
- approvalPolicy: input.approvalPolicy,
6334
- roleInstructions
6335
- });
6336
- }
6337
- const promptFile = path20.join(TEMPLATES_DIR2, `crew.${agent.templateSuffix}.md`);
6338
- const interactive = agent.name === "claude" || agent.name === "opencode";
6339
- const crewRole = config.defaults.roles?.crew;
6340
- const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
6341
- const crewModel = input.model ?? route?.model ?? configModel;
6342
- if (agentName === "claude") {
6343
- const req = buildDispatchRequest({
6344
- provider: "claude",
6345
- mode: "interactive",
6346
- project: input.project,
6347
- cwd: spawnCwd,
6348
- task: input.task,
6349
- name
6350
- });
6351
- const rec = await squadrantdCall(req);
6352
- writePerCrewSettingsLocal({ projectCwd: spawnCwd });
6353
- const cliCommand2 = agent.buildCommand({
6354
- prompt: input.task,
6355
- workdir: spawnCwd,
6356
- role: "crew",
6357
- promptFile,
6358
- interactive: true,
6359
- // Permission mode is config-driven (defaults.permissions.crew) so squadrant
6360
- // can default crews to 'auto' or keep the semi-automatic 'acceptEdits'
6361
- // gate (auto-accept edits, still prompt for risky ops). Falls back to
6362
- // 'acceptEdits' when unset.
6363
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
6364
- ...crewModel ? { model: crewModel } : {}
6365
- });
6366
- const direction2 = input.direction ?? "tab";
6367
- const title2 = titleFor(input.project, name);
6368
- const pane2 = await runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
6369
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
6370
- await runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${cliCommand2}`);
6371
- const preLaunchScreen = await runtime.readPaneScreen(pane2) ?? "";
6372
- await sendFirstTurnWhenReady(runtime, pane2, `${input.task}
6373
-
6374
- ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
6375
- return { ...pane2, title: title2 };
6376
- }
6377
- if (agentName === "opencode") {
6378
- const serverPort = await getFreePort();
6379
- const req = buildDispatchRequest({
6380
- provider: "opencode",
6381
- mode: "interactive",
6382
- project: input.project,
6383
- cwd: spawnCwd,
6384
- task: input.task,
6385
- name,
6386
- // opencode has no heartbeat hook, so a normal budget would false-stall
6387
- // every crew after 5min; use a 24h budget to effectively disable stall
6388
- // detection. The SSE bridge (serverPort) provides turn-end liveness.
6389
- budgetMs: 864e5,
6390
- serverPort
6391
- });
6392
- const rec = await squadrantdCall(req);
6393
- const opencodeConfigPath = writePerCrewOpencodeConfig({
6394
- stateRoot: path20.join(os10.homedir(), ".config", "squadrant", "state"),
6395
- project: input.project,
6396
- taskId: rec.id,
6397
- // CP3 opt-in: --approval gates bash so the captain approves shell commands.
6398
- // Without it, bash stays auto-approved (default behavior unchanged).
6399
- ...input.approval ? { gateBash: true } : {}
6400
- });
6401
- const cliCommand2 = agent.buildCommand({
6402
- prompt: input.task,
6403
- workdir: spawnCwd,
6404
- role: "crew",
6405
- promptFile,
6406
- interactive: true,
6407
- model: crewModel,
6408
- port: serverPort
6409
- });
6410
- const direction2 = input.direction ?? "tab";
6411
- const title2 = titleFor(input.project, name);
6412
- const pane2 = await runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
6413
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
6414
- await runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} OPENCODE_CONFIG=${opencodeConfigPath} ${cliCommand2}`);
6415
- const preLaunchScreen = await runtime.readPaneScreen(pane2) ?? "";
6416
- await sendFirstTurnWhenReady(runtime, pane2, `${input.task}
6417
-
6418
- ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
6419
- // #235: opencode's idle splash ("Ask anything…") keeps mutating (cursor
6420
- // blink, status line toggle), so the old screen-changed check would always
6421
- // see a *different* screen and never re-send a dropped turn. The splashMarker
6422
- // confirms the TUI actually left splash before declaring acceptance.
6423
- splashMarker: "Ask anything\u2026",
6424
- // opencode has a wider boot-race window than claude, so allow 3 retries
6425
- // instead of the default 2.
6426
- retryLimit: 3
6427
- });
6428
- return { ...pane2, title: title2 };
6429
- }
6430
- const cliCommand = agent.buildCommand({
6431
- prompt: input.task,
6432
- workdir: spawnCwd,
6433
- role: "crew",
6434
- promptFile,
6435
- interactive,
6436
- model: crewModel
6437
- });
6438
- const direction = input.direction ?? "tab";
6439
- const title = titleFor(input.project, name);
6440
- const pane = await runtime.newPane({ workspaceId: captain.id, direction, title });
6441
- await runtime.sendToPane(pane, cliCommand);
6442
- if (interactive) {
6443
- const preLaunchScreen = await runtime.readPaneScreen(pane) ?? "";
6444
- await sendFirstTurnWhenReady(runtime, pane, input.task, preLaunchScreen);
6445
- }
6446
- return { ...pane, title };
6447
- }
6448
- async function runCodexInteractiveSpawn(o) {
6449
- const req = buildDispatchRequest({
6450
- provider: "codex",
6451
- mode: "interactive",
6452
- project: o.project,
6453
- cwd: o.cwd,
6454
- task: o.task,
6455
- name: o.name,
6456
- ...o.approvalPolicy ? { approvalPolicy: o.approvalPolicy } : {},
6457
- ...o.roleInstructions ? { roleInstructions: o.roleInstructions } : {}
6458
- });
6459
- const rec = await squadrantdCall(req);
6460
- const title = titleFor(o.project, o.name);
6461
- const pane = await o.runtime.newPane({
6462
- workspaceId: o.workspaceId,
6463
- direction: o.direction,
6464
- title
7136
+ return runCrewSpawn(input, config, {
7137
+ runtime,
7138
+ // AgentDriver satisfies ResolvedAgent structurally; `role: any` in ResolvedAgent
7139
+ // bridges the Role vs string gap only "crew" is ever passed at call sites.
7140
+ resolveAgent: (name) => agents.get(name) ?? null,
7141
+ dispatchCrew: async (o) => {
7142
+ const req = buildDispatchRequest(o);
7143
+ return await squadrantdCall(req);
7144
+ },
7145
+ writeSettingsLocal: (cwd) => writePerCrewSettingsLocal({ projectCwd: cwd }),
7146
+ writeOpencodeConfig: writePerCrewOpencodeConfig,
7147
+ sendFirstTurn: (pane, firstTurn, preLaunchScreen, opts) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen, opts),
7148
+ getFreePort,
7149
+ sendCodexFirstTurn,
7150
+ onRouted: (route) => console.log(
7151
+ chalk9.dim(
7152
+ `routed: tier=${route.tier} \u2192 ${route.agent}${route.model ? `/${route.model}` : ""} (rule: "${route.matchedRule}")`
7153
+ )
7154
+ )
6465
7155
  });
6466
- await o.runtime.sendToPane(pane, `squadrant crew attach ${rec.id}`);
6467
- if (o.task && o.task !== "(interactive)") {
6468
- void sendCodexFirstTurn(rec.id, o.task).catch((e) => {
6469
- process.stderr.write(`(first-turn delivery failed: ${e.message})
6470
- `);
6471
- });
6472
- }
6473
- return { ...pane, title };
6474
7156
  }
6475
- async function runCrewSend(project, name, message) {
7157
+ async function runCrewSend2(project, name, message) {
6476
7158
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6477
- const crew = await findCrew(runtime, workspaceId, project, name);
6478
- if (!crew) {
6479
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6480
- }
6481
- try {
6482
- const tasks = await squadrantdCall({ kind: "list", project });
6483
- const task = tasks.find((t) => t.name === name);
6484
- if (task) {
6485
- if (TERMINAL_STATES.has(task.state)) {
6486
- await squadrantdCall({ kind: "event", project, event: { type: "task.reopened", id: task.id } });
6487
- } else if (task.state === "blocked" || task.state === "awaiting-input") {
6488
- await squadrantdCall({ kind: "event", project, event: { type: "task.started", id: task.id } });
6489
- }
7159
+ return runCrewSend(project, name, message, runtime, workspaceId, {
7160
+ listTasks: async (p) => await squadrantdCall({ kind: "list", project: p }),
7161
+ emitEvent: async (p, event) => {
7162
+ await squadrantdCall({ kind: "event", project: p, event });
6490
7163
  }
6491
- } catch {
6492
- }
6493
- await runtime.sendToPane(crew, message);
7164
+ });
6494
7165
  }
6495
- async function runCrewRead(project, name) {
7166
+ async function runCrewRead2(project, name) {
6496
7167
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6497
- const crew = await findCrew(runtime, workspaceId, project, name);
6498
- if (!crew) {
6499
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6500
- }
6501
- return runtime.readPaneScreen(crew);
7168
+ return runCrewRead(project, name, runtime, workspaceId);
6502
7169
  }
6503
- async function runCrewClose(project, name) {
7170
+ async function runCrewClose2(project, name) {
6504
7171
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6505
- const projRoot = loadConfig().projects[project]?.path;
6506
- let taskId;
6507
- let worktreeCwd;
6508
- try {
6509
- const tasks = await squadrantdCall({ kind: "list", project });
6510
- const task = tasks.find((t) => t.name === name);
6511
- if (task) {
6512
- taskId = task.id;
6513
- if (task.cwd && projRoot && task.cwd !== projRoot) {
6514
- worktreeCwd = task.cwd;
6515
- }
6516
- if (!TERMINAL_STATES.has(task.state)) {
6517
- await squadrantdCall({ kind: "event", project, event: { type: "task.cancelled", id: task.id, reason: "closed by captain" } });
6518
- }
6519
- if (task.provider === "codex") {
6520
- await squadrantdCall({ kind: "codex-close", taskId: task.id });
6521
- }
6522
- }
6523
- } catch {
6524
- }
6525
- const crew = await findCrew(runtime, workspaceId, project, name);
6526
- if (crew) {
6527
- await runtime.closePane(crew);
6528
- } else if (taskId === void 0) {
6529
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6530
- }
6531
- if (taskId !== void 0) {
6532
- await reapCrewChildren(taskId);
6533
- }
6534
- if (worktreeCwd && projRoot) {
6535
- try {
6536
- removeWorktree(projRoot, worktreeCwd);
6537
- } catch (e) {
6538
- process.stderr.write(`(worktree remove failed: ${e.message})
6539
- `);
7172
+ return runCrewClose(project, name, runtime, workspaceId, {
7173
+ listTasks: async (p) => await squadrantdCall({ kind: "list", project: p }),
7174
+ emitEvent: async (p, event) => {
7175
+ await squadrantdCall({ kind: "event", project: p, event });
7176
+ },
7177
+ closeCodexThread: async (taskId) => {
7178
+ await squadrantdCall({ kind: "codex-close", taskId });
6540
7179
  }
6541
- }
7180
+ });
6542
7181
  }
6543
- async function runCrewList(project) {
7182
+ async function runCrewList2(project) {
6544
7183
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6545
- const crews = await listProjectCrews(runtime, workspaceId, project);
6546
- return crews.map((c) => ({
6547
- name: nameFromTitle(project, c.title),
6548
- surfaceId: c.surfaceId
6549
- }));
7184
+ return runCrewList(project, runtime, workspaceId);
6550
7185
  }
6551
7186
  var crewCommand = new Command9("crew").description(
6552
7187
  "Spawn and manage interactive crew sessions next to the project's captain"
@@ -6558,7 +7193,7 @@ crewCommand.command("spawn").description(
6558
7193
  try {
6559
7194
  const resolvedTask = await resolveTextInput({ positional: task, filePath: opts.taskFile, label: "task" });
6560
7195
  const agentExplicit = cmd.getOptionValueSource("agent") === "cli";
6561
- const pane = await runCrewSpawn({
7196
+ const pane = await runCrewSpawn2({
6562
7197
  project,
6563
7198
  task: resolvedTask,
6564
7199
  name: opts.name,
@@ -6580,7 +7215,7 @@ crewCommand.command("spawn").description(
6580
7215
  );
6581
7216
  crewCommand.command("list").description("List live crew sessions for a project").argument("<project>", "Project name").action(async (project) => {
6582
7217
  try {
6583
- const crews = await runCrewList(project);
7218
+ const crews = await runCrewList2(project);
6584
7219
  if (crews.length === 0) {
6585
7220
  console.log(chalk9.yellow(`No live crew sessions for ${project}.`));
6586
7221
  return;
@@ -6596,7 +7231,7 @@ crewCommand.command("list").description("List live crew sessions for a project")
6596
7231
  crewCommand.command("send").description("Send a follow-up message to an existing crew session").argument("<project>", "Project name").argument("<name>", "Crew name (e.g. crew-1)").argument("[message]", "Message to send (omit with --message-file)").option("--message-file <path>", "Read message from file instead of positional arg ('-' for stdin)").action(async (project, name, message, opts) => {
6597
7232
  try {
6598
7233
  const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
6599
- await runCrewSend(project, name, resolvedMessage);
7234
+ await runCrewSend2(project, name, resolvedMessage);
6600
7235
  console.log(chalk9.green(`\u2714 Sent to ${project}:${name}`));
6601
7236
  } catch (err) {
6602
7237
  console.error(chalk9.red(err.message));
@@ -6605,7 +7240,7 @@ crewCommand.command("send").description("Send a follow-up message to an existing
6605
7240
  });
6606
7241
  crewCommand.command("read").description("Read the current screen of a crew session (tail by default; use --full for the entire scrollback)").argument("<project>", "Project name").argument("<name>", "Crew name").option("--lines <N>", "Number of trailing lines to show", "40").option("--full", "Show the entire scrollback (overrides --lines)").action(async (project, name, opts) => {
6607
7242
  try {
6608
- const screen = await runCrewRead(project, name);
7243
+ const screen = await runCrewRead2(project, name);
6609
7244
  const out = opts.full ? screen : tailLines(screen, Number(opts.lines ?? 40));
6610
7245
  console.log(out);
6611
7246
  } catch (err) {
@@ -6615,7 +7250,7 @@ crewCommand.command("read").description("Read the current screen of a crew sessi
6615
7250
  });
6616
7251
  crewCommand.command("close").description("Shutdown a crew session (closes its tab)").argument("<project>", "Project name").argument("<name>", "Crew name").action(async (project, name) => {
6617
7252
  try {
6618
- await runCrewClose(project, name);
7253
+ await runCrewClose2(project, name);
6619
7254
  console.log(chalk9.green(`\u2714 Closed ${project}:${name}`));
6620
7255
  } catch (err) {
6621
7256
  console.error(chalk9.red(err.message));
@@ -6629,90 +7264,23 @@ init_dist3();
6629
7264
  init_dist4();
6630
7265
  init_dist3();
6631
7266
  init_dist();
6632
- init_dist();
7267
+ init_dist2();
6633
7268
  import { Command as Command10 } from "commander";
6634
- import fs17 from "fs";
7269
+ import fs18 from "fs";
6635
7270
  import path21 from "path";
6636
7271
  import os11 from "os";
6637
7272
  import chalk10 from "chalk";
6638
7273
  var TEMPLATES_DIR3 = path21.join(os11.homedir(), ".config", "squadrant", "templates");
6639
- var SIDE_ROLES = ["research", "debug"];
6640
- function shellQuote2(p) {
6641
- return "'" + p.replace(/'/g, "'\\''") + "'";
6642
- }
6643
- function titleFor2(project, name) {
6644
- return `\u{1F5D2} ${project}:${name}`;
6645
- }
6646
- function isSideTitle(project, title) {
6647
- return title.startsWith(`\u{1F5D2} ${project}:`);
6648
- }
6649
- function nameFromTitle2(project, title) {
6650
- return title.slice(`\u{1F5D2} ${project}:`.length);
6651
- }
6652
- function nextAutoName2(existingTitles, project) {
6653
- const used = /* @__PURE__ */ new Set();
6654
- for (const title of existingTitles) {
6655
- const n = nameFromTitle2(project, title).match(/^side-(\d+)$/);
6656
- if (n) used.add(Number(n[1]));
6657
- }
6658
- let i = 1;
6659
- while (used.has(i)) i++;
6660
- return `side-${i}`;
6661
- }
6662
- function buildSideFirstTurn(topic, project, role, spokeVault, scratchWorktree) {
6663
- const lines = [
6664
- topic,
6665
- "",
6666
- "---",
6667
- "Side-session context (for handoff use):",
6668
- `Project: ${project}`,
6669
- `Role: ${role}`,
6670
- `Spoke vault: ${spokeVault}`
6671
- ];
6672
- if (scratchWorktree) {
6673
- lines.push(`Scratch worktree: ${scratchWorktree}`);
6674
- }
6675
- return lines.join("\n");
6676
- }
6677
- async function runSideSpawn(input) {
7274
+ async function runSideSpawn2(input) {
6678
7275
  const config = loadConfig();
6679
7276
  const proj = config.projects[input.project];
6680
7277
  if (!proj) {
6681
7278
  throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
6682
7279
  }
6683
- if (!SIDE_ROLES.includes(input.role)) {
6684
- throw new Error(
6685
- `Unknown side role '${input.role}'. Valid roles: ${SIDE_ROLES.join(", ")}.`
6686
- );
6687
- }
6688
7280
  const runtime = new RuntimeRegistry({ cmux: createCmuxDriver() }).forProject(
6689
7281
  input.project,
6690
7282
  config
6691
7283
  );
6692
- const captain = await runtime.status(proj.captainName);
6693
- if (!captain) {
6694
- throw new Error(
6695
- `Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`
6696
- );
6697
- }
6698
- const existing = await runtime.listSurfaces(captain.id);
6699
- const existingTitles = existing.filter((s) => s.title && isSideTitle(input.project, s.title)).map((s) => s.title);
6700
- if (input.name) {
6701
- const wantTitle = titleFor2(input.project, input.name);
6702
- if (existingTitles.includes(wantTitle)) {
6703
- throw new Error(
6704
- `Side session '${input.name}' already exists for ${input.project}.`
6705
- );
6706
- }
6707
- }
6708
- const name = input.name ?? nextAutoName2(existingTitles, input.project);
6709
- const spawnCwd = input.role === "debug" ? addWorktree({
6710
- repoRoot: proj.path,
6711
- worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
6712
- project: input.project,
6713
- name,
6714
- base: resolveWorktreeBase(proj.path)
6715
- }) : proj.path;
6716
7284
  const agents = new CapabilityRegistry({
6717
7285
  claude: createClaudeDriver(),
6718
7286
  codex: createCodexDriver(),
@@ -6729,80 +7297,39 @@ async function runSideSpawn(input) {
6729
7297
  const promptFile = path21.join(
6730
7298
  TEMPLATES_DIR3,
6731
7299
  `side.${input.role}.${agent.templateSuffix}.md`
6732
- );
6733
- const direction = input.direction ?? "tab";
6734
- const title = titleFor2(input.project, name);
6735
- const pane = await runtime.newPane({ workspaceId: captain.id, direction, title });
6736
- const cliCommand = agent.buildCommand({
7300
+ );
7301
+ const agentCmdFactory = (spawnCwd) => agent.buildCommand({
6737
7302
  prompt: input.topic,
6738
7303
  workdir: spawnCwd,
6739
7304
  role: "side",
6740
- promptFile: fs17.existsSync(promptFile) ? promptFile : void 0,
7305
+ promptFile: fs18.existsSync(promptFile) ? promptFile : void 0,
6741
7306
  interactive: true,
6742
7307
  permissionMode: config.defaults.permissions?.crew ?? "auto",
6743
7308
  ...sideModel ? { model: sideModel } : {}
6744
7309
  });
6745
- await runtime.sendToPane(pane, `cd ${shellQuote2(spawnCwd)} && ${cliCommand}`);
6746
- const preLaunchScreen = await runtime.readPaneScreen(pane) ?? "";
6747
- const firstTurn = buildSideFirstTurn(
6748
- input.topic,
6749
- input.project,
6750
- input.role,
6751
- proj.spokeVault ?? "",
6752
- input.role === "debug" ? spawnCwd : void 0
6753
- );
6754
- await sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen);
6755
- return { ...pane, title };
7310
+ const sendFirstTurn = (pane, firstTurn, preLaunchScreen) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen);
7311
+ return runSideSpawn(input, config, { runtime, agentCmdFactory, sendFirstTurn });
6756
7312
  }
6757
- async function runSideSend(project, name, message) {
7313
+ async function runSideSend2(project, name, message) {
6758
7314
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6759
- const want = titleFor2(project, name);
6760
- const surfaces = await runtime.listSurfaces(workspaceId);
6761
- const pane = surfaces.find((s) => s.title === want) ?? null;
6762
- if (!pane) {
6763
- throw new Error(
6764
- `Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`
6765
- );
6766
- }
6767
- await runtime.sendToPane(pane, message);
7315
+ await runSideSend(runtime, workspaceId, project, name, message);
6768
7316
  }
6769
- async function runSideList(project) {
7317
+ async function runSideList2(project) {
6770
7318
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6771
- const surfaces = await runtime.listSurfaces(workspaceId);
6772
- return surfaces.filter((s) => s.title && isSideTitle(project, s.title)).map((s) => ({
6773
- name: nameFromTitle2(project, s.title),
6774
- surfaceId: s.surfaceId
6775
- }));
7319
+ return runSideList(runtime, workspaceId, project);
6776
7320
  }
6777
- async function runSideClose(project, name) {
7321
+ async function runSideClose2(project, name) {
6778
7322
  const config = loadConfig();
6779
7323
  const proj = config.projects[project];
6780
7324
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6781
- const want = titleFor2(project, name);
6782
- const surfaces = await runtime.listSurfaces(workspaceId);
6783
- const pane = surfaces.find((s) => s.title === want) ?? null;
6784
- if (!pane) {
6785
- throw new Error(
6786
- `Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`
6787
- );
6788
- }
6789
- await runtime.closePane(pane);
6790
- if (proj) {
6791
- const wtPath = worktreePath(
6792
- proj.path,
6793
- config.defaults.worktreeDir ?? ".worktrees",
6794
- project,
6795
- name
6796
- );
6797
- if (fs17.existsSync(wtPath)) {
6798
- try {
6799
- removeWorktree(proj.path, wtPath);
6800
- } catch (e) {
6801
- process.stderr.write(`(worktree remove failed: ${e.message})
6802
- `);
6803
- }
6804
- }
6805
- }
7325
+ await runSideClose(
7326
+ runtime,
7327
+ workspaceId,
7328
+ project,
7329
+ name,
7330
+ proj?.path,
7331
+ config.defaults.worktreeDir ?? ".worktrees"
7332
+ );
6806
7333
  }
6807
7334
  var sideCommand = new Command10("side").description(
6808
7335
  "Spawn and manage side-sessions (research/debug) \u2014 fresh-context tabs off the daemon lifecycle"
@@ -6821,7 +7348,7 @@ sideCommand.command("spawn").description(
6821
7348
  filePath: opts.topicFile,
6822
7349
  label: "topic"
6823
7350
  });
6824
- const pane = await runSideSpawn({
7351
+ const pane = await runSideSpawn2({
6825
7352
  project,
6826
7353
  topic: resolvedTopic,
6827
7354
  role: opts.role,
@@ -6838,7 +7365,7 @@ sideCommand.command("spawn").description(
6838
7365
  );
6839
7366
  sideCommand.command("list").description("List live side-sessions for a project").argument("<project>", "Project name").action(async (project) => {
6840
7367
  try {
6841
- const sessions = await runSideList(project);
7368
+ const sessions = await runSideList2(project);
6842
7369
  if (sessions.length === 0) {
6843
7370
  console.log(chalk10.yellow(`No live side-sessions for ${project}.`));
6844
7371
  return;
@@ -6859,7 +7386,7 @@ sideCommand.command("send").description("Send a follow-up message to an existing
6859
7386
  filePath: opts.messageFile,
6860
7387
  label: "message"
6861
7388
  });
6862
- await runSideSend(project, name, resolvedMessage);
7389
+ await runSideSend2(project, name, resolvedMessage);
6863
7390
  console.log(chalk10.green(`\u2714 Sent to ${project}:${name}`));
6864
7391
  } catch (err) {
6865
7392
  console.error(chalk10.red(err.message));
@@ -6869,7 +7396,7 @@ sideCommand.command("send").description("Send a follow-up message to an existing
6869
7396
  );
6870
7397
  sideCommand.command("close").description("Close a side-session (closes its tab)").argument("<project>", "Project name").argument("<name>", "Session name").action(async (project, name) => {
6871
7398
  try {
6872
- await runSideClose(project, name);
7399
+ await runSideClose2(project, name);
6873
7400
  console.log(chalk10.green(`\u2714 Closed ${project}:${name}`));
6874
7401
  } catch (err) {
6875
7402
  console.error(chalk10.red(err.message));
@@ -6882,8 +7409,8 @@ init_dist();
6882
7409
  init_dist3();
6883
7410
  import { Command as Command11 } from "commander";
6884
7411
  import { execSync as execSync10 } from "child_process";
6885
- import { homedir as homedir13 } from "os";
6886
- import { join as join19 } from "path";
7412
+ import { homedir as homedir14 } from "os";
7413
+ import { join as join20 } from "path";
6887
7414
  import chalk12 from "chalk";
6888
7415
 
6889
7416
  // packages/web/dist/read-status.js
@@ -7022,7 +7549,7 @@ function renderDashboard(rows, opts) {
7022
7549
 
7023
7550
  // packages/web/dist/sync-hub.js
7024
7551
  init_dist();
7025
- import fs18 from "fs";
7552
+ import fs19 from "fs";
7026
7553
  import path22 from "path";
7027
7554
  function buildMirrorMarkdown(s) {
7028
7555
  const fenced = "```";
@@ -7049,8 +7576,8 @@ function buildMirrorMarkdown(s) {
7049
7576
  function syncHub(deps) {
7050
7577
  if (!deps.config.hubVault)
7051
7578
  return [];
7052
- const writeFile5 = deps.writeFile ?? ((p, c) => fs18.writeFileSync(p, c));
7053
- const mkdir5 = deps.mkdir ?? ((p) => fs18.mkdirSync(p, { recursive: true }));
7579
+ const writeFile5 = deps.writeFile ?? ((p, c) => fs19.writeFileSync(p, c));
7580
+ const mkdir5 = deps.mkdir ?? ((p) => fs19.mkdirSync(p, { recursive: true }));
7054
7581
  const projectsDir = path22.join(resolveHome(deps.config.hubVault), "projects");
7055
7582
  mkdir5(projectsDir);
7056
7583
  const out = [];
@@ -7079,10 +7606,10 @@ function mergeSnapshot(daemon, external, now) {
7079
7606
  // packages/web/dist/probes.js
7080
7607
  init_dist();
7081
7608
  init_dist();
7082
- import { join as join18 } from "path";
7083
- import { homedir as homedir12 } from "os";
7609
+ import { join as join19 } from "path";
7610
+ import { homedir as homedir13 } from "os";
7084
7611
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
7085
- import { execFile as execFile2 } from "child_process";
7612
+ import { execFile as execFile3 } from "child_process";
7086
7613
  var DEFAULT_TIMEOUT_MS = 2e3;
7087
7614
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
7088
7615
  function withTimeout2(p, ms) {
@@ -7117,7 +7644,7 @@ function vaultProbe(run, dir) {
7117
7644
  return { state: "unknown", detail: "no vault configured" };
7118
7645
  if (!run.pathExists(dir))
7119
7646
  return { state: "gone", detail: "vault directory missing" };
7120
- if (!run.pathExists(join18(dir, ".obsidian")))
7647
+ if (!run.pathExists(join19(dir, ".obsidian")))
7121
7648
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
7122
7649
  return { state: "alive" };
7123
7650
  } catch {
@@ -7185,10 +7712,10 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
7185
7712
  const sessions = probeSessions(run);
7186
7713
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
7187
7714
  }
7188
- var SESSIONS_PATH = join18(homedir12(), ".config", "squadrant", "sessions.json");
7715
+ var SESSIONS_PATH = join19(homedir13(), ".config", "squadrant", "sessions.json");
7189
7716
  function onPath(cli) {
7190
7717
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
7191
- return dirs.some((d) => existsSync10(join18(d, cli)));
7718
+ return dirs.some((d) => existsSync10(join19(d, cli)));
7192
7719
  }
7193
7720
  function readSessionsHashes() {
7194
7721
  const raw = JSON.parse(readFileSync10(SESSIONS_PATH, "utf-8"));
@@ -7199,7 +7726,7 @@ function defaultProbeRunners() {
7199
7726
  return {
7200
7727
  probeCmuxBin: () => new Promise((resolve3) => {
7201
7728
  try {
7202
- execFile2(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
7729
+ execFile3(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
7203
7730
  } catch {
7204
7731
  resolve3(false);
7205
7732
  }
@@ -7858,7 +8385,7 @@ async function startWebServer(opts) {
7858
8385
 
7859
8386
  // packages/cli/src/commands/dashboard.ts
7860
8387
  init_dist();
7861
- var SOCK3 = join19(homedir13(), ".config", "squadrant", "squadrant.sock");
8388
+ var SOCK3 = join20(homedir14(), ".config", "squadrant", "squadrant.sock");
7862
8389
  function detectCurrentWorkspace2() {
7863
8390
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
7864
8391
  const match = out.match(/workspace:\d+/);
@@ -7941,12 +8468,11 @@ dashboardCommand.command("sync-hub").description("Mirror each spoke status.md in
7941
8468
  init_dist();
7942
8469
  init_dist4();
7943
8470
  init_dist3();
7944
- init_dist();
7945
- init_dist3();
8471
+ init_dist2();
7946
8472
  init_dist2();
7947
8473
  import { Command as Command12 } from "commander";
7948
8474
  import { execSync as execSync11 } from "child_process";
7949
- import fs19 from "fs";
8475
+ import fs20 from "fs";
7950
8476
  import path23 from "path";
7951
8477
  import os12 from "os";
7952
8478
  import chalk13 from "chalk";
@@ -7960,64 +8486,6 @@ function ensureCmuxReady() {
7960
8486
  console.log(chalk13.bold(" Run `squadrant launch` from inside a cmux workspace.\n"));
7961
8487
  process.exit(0);
7962
8488
  }
7963
- async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
7964
- const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
7965
- const settleMs = opts.settleMs ?? 2500;
7966
- const pollMs = opts.pollMs ?? 1e3;
7967
- const maxAttempts = opts.maxAttempts ?? 3;
7968
- const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
7969
- const read = async () => runtime.readScreen(refId).catch(() => "");
7970
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
7971
- const deadline = Date.now() + readyTimeoutMs;
7972
- let preSend = await read();
7973
- let state = classifyStartupSurface(preSend);
7974
- while (state === "loading" && Date.now() < deadline) {
7975
- await sleep2(pollMs);
7976
- preSend = await read();
7977
- state = classifyStartupSurface(preSend);
7978
- }
7979
- if (state === "working") return;
7980
- await runtime.send(refId, prompt).catch(() => {
7981
- });
7982
- if (state === "loading") return;
7983
- await sleep2(settleMs);
7984
- const after = await read();
7985
- if (after !== preSend) return;
7986
- }
7987
- }
7988
- async function launchWorkspace(runtime, name, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt) {
7989
- ensureCmuxReady();
7990
- const existing = await runtime.status(name);
7991
- if (existing && forceFresh) {
7992
- console.log(chalk13.yellow(` Closing stale workspace '${name}' for fresh start`));
7993
- await runtime.stop(existing.id);
7994
- } else if (existing) {
7995
- console.log(chalk13.yellow(` Workspace '${name}' already exists \u2014 switching to it`));
7996
- cmuxLocal(["select-workspace", "--workspace", existing.id]);
7997
- return;
7998
- }
7999
- let currentRef;
8000
- try {
8001
- const cur = cmuxLocal(["current-workspace"]);
8002
- currentRef = cur.match(/workspace:\d+/)?.[0];
8003
- } catch {
8004
- }
8005
- const ref = await runtime.spawn({
8006
- name,
8007
- workdir: cwd ?? process.cwd(),
8008
- command: agentCmd,
8009
- pinToTop
8010
- });
8011
- if (initialPrompt) {
8012
- void deliverStartupPrompt(runtime, ref.id, initialPrompt);
8013
- }
8014
- if (navigate) {
8015
- cmuxLocal(["select-workspace", "--workspace", ref.id]);
8016
- } else if (currentRef) {
8017
- cmuxLocal(["select-workspace", "--workspace", currentRef]);
8018
- }
8019
- console.log(chalk13.green(` \u2714 Workspace '${name}' created`));
8020
- }
8021
8489
  var launchCommand = new Command12("launch").description(
8022
8490
  "Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
8023
8491
  ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--all", "Launch all captain workspaces").action(async (project, opts) => {
@@ -8031,19 +8499,10 @@ var launchCommand = new Command12("launch").description(
8031
8499
  const registry = new CapabilityRegistry(drivers);
8032
8500
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
8033
8501
  async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
8034
- let forceFresh = !!opts.fresh;
8035
- if (!forceFresh) {
8036
- const auto = shouldStartFresh(workspaceName, role, { sessionsPath: SESSIONS_PATH2, templatesDir: TEMPLATES_DIR4 });
8037
- if (auto.fresh) {
8038
- console.log(chalk13.cyan(` \u21BB ${auto.reason}`));
8039
- forceFresh = true;
8040
- }
8041
- }
8502
+ ensureCmuxReady();
8042
8503
  const roleConfig = config.defaults.roles?.[role];
8043
8504
  const agentName = roleConfig?.agent || "claude";
8044
8505
  const model = roleConfig?.model || config.defaults.models?.[role];
8045
- const agentCmd = buildAgentCmd(agentName, registry, role, forceFresh, permissionMode, model, TEMPLATES_DIR4);
8046
- recordSession(workspaceName, role, { sessionsPath: SESSIONS_PATH2, templatesDir: TEMPLATES_DIR4 });
8047
8506
  let initialPrompt;
8048
8507
  if (role === "captain") {
8049
8508
  initialPrompt = "Run your startup checklist: use the squadrant:captain-ops skill, complete all startup steps, then report ready.";
@@ -8052,19 +8511,44 @@ var launchCommand = new Command12("launch").description(
8052
8511
  }
8053
8512
  const runtime = projectName ? runtimes.forProject(projectName, config) : runtimes.global(config);
8054
8513
  try {
8055
- await launchWorkspace(runtime, workspaceName, agentCmd, cwd, navigate, forceFresh, pinToTop, initialPrompt);
8514
+ await launchOneWorkspace({
8515
+ workspaceName,
8516
+ role,
8517
+ cwd,
8518
+ forceFreshOverride: opts.fresh,
8519
+ sessionsPath: SESSIONS_PATH2,
8520
+ templatesDir: TEMPLATES_DIR4,
8521
+ agentCmdFactory: (forceFresh) => buildAgentCmd(agentName, registry, role, forceFresh, permissionMode, model, TEMPLATES_DIR4),
8522
+ initialPrompt,
8523
+ runtime,
8524
+ navigate,
8525
+ pinToTop,
8526
+ classifyScreen: classifyStartupSurface,
8527
+ selectWorkspace: (id) => cmuxLocal(["select-workspace", "--workspace", id]),
8528
+ getCurrentWorkspace: () => {
8529
+ try {
8530
+ return cmuxLocal(["current-workspace"]);
8531
+ } catch {
8532
+ return null;
8533
+ }
8534
+ },
8535
+ onFreshReason: (reason) => console.log(chalk13.cyan(` \u21BB ${reason}`)),
8536
+ onStoppingStale: (name) => console.log(chalk13.yellow(` Closing stale workspace '${name}' for fresh start`)),
8537
+ onAlreadyExists: (name) => console.log(chalk13.yellow(` Workspace '${name}' already exists \u2014 switching to it`)),
8538
+ onCreated: (name) => console.log(chalk13.green(` \u2714 Workspace '${name}' created`))
8539
+ });
8056
8540
  } catch (err) {
8057
8541
  console.error(chalk13.red(` \u2718 Failed: ${err.message}`));
8058
8542
  }
8059
8543
  }
8060
8544
  if (opts.all) {
8061
8545
  const hubPath = resolveHome(config.hubVault);
8062
- fs19.mkdirSync(hubPath, { recursive: true });
8546
+ fs20.mkdirSync(hubPath, { recursive: true });
8063
8547
  console.log(chalk13.bold("\nLaunching all captain workspaces\n"));
8064
8548
  for (const [name, proj] of Object.entries(config.projects)) {
8065
8549
  const projPath = resolveHome(proj.path);
8066
8550
  const spokePath = resolveHome(proj.spokeVault);
8067
- if (!fs19.existsSync(spokePath)) {
8551
+ if (!fs20.existsSync(spokePath)) {
8068
8552
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
8069
8553
  await ensureSpokeLayout(spokeDriver);
8070
8554
  console.log(chalk13.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -8095,7 +8579,7 @@ var launchCommand = new Command12("launch").description(
8095
8579
  const proj = config.projects[project];
8096
8580
  const projPath = resolveHome(proj.path);
8097
8581
  const spokePath = resolveHome(proj.spokeVault);
8098
- if (!fs19.existsSync(spokePath)) {
8582
+ if (!fs20.existsSync(spokePath)) {
8099
8583
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(project, config);
8100
8584
  await ensureSpokeLayout(spokeDriver);
8101
8585
  console.log(chalk13.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -8230,7 +8714,7 @@ Shutting down captain workspace for '${project}'...
8230
8714
  // packages/cli/src/commands/feedback.ts
8231
8715
  init_dist();
8232
8716
  import { Command as Command14 } from "commander";
8233
- import fs20 from "fs";
8717
+ import fs21 from "fs";
8234
8718
  import os13 from "os";
8235
8719
  import path24 from "path";
8236
8720
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -8240,14 +8724,14 @@ var REPO_URL = "https://github.com/tu11aa/squadrant";
8240
8724
  function readPkgVersion() {
8241
8725
  try {
8242
8726
  const pkgPath = path24.join(path24.dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
8243
- return JSON.parse(fs20.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
8727
+ return JSON.parse(fs21.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
8244
8728
  } catch {
8245
8729
  return "unknown";
8246
8730
  }
8247
8731
  }
8248
8732
  function readMetrics(metricsPath) {
8249
8733
  try {
8250
- return JSON.parse(fs20.readFileSync(metricsPath, "utf-8"));
8734
+ return JSON.parse(fs21.readFileSync(metricsPath, "utf-8"));
8251
8735
  } catch {
8252
8736
  return {};
8253
8737
  }
@@ -8307,7 +8791,7 @@ init_dist();
8307
8791
  init_dist();
8308
8792
  init_dist3();
8309
8793
  import { Command as Command15 } from "commander";
8310
- import fs21 from "fs";
8794
+ import fs22 from "fs";
8311
8795
  import path25 from "path";
8312
8796
  import chalk16 from "chalk";
8313
8797
  import matter3 from "gray-matter";
@@ -8319,9 +8803,9 @@ async function getProjectStandup(name, project, dateStr, registry, config) {
8319
8803
  const spokeVault = resolveHome(project.spokeVault);
8320
8804
  const statusFile = path25.join(spokeVault, "status.md");
8321
8805
  let status = {};
8322
- if (fs21.existsSync(statusFile)) {
8806
+ if (fs22.existsSync(statusFile)) {
8323
8807
  try {
8324
- status = matter3(fs21.readFileSync(statusFile, "utf-8")).data;
8808
+ status = matter3(fs22.readFileSync(statusFile, "utf-8")).data;
8325
8809
  } catch {
8326
8810
  }
8327
8811
  }
@@ -8439,15 +8923,15 @@ init_dist();
8439
8923
  init_dist();
8440
8924
  init_dist3();
8441
8925
  import { Command as Command16 } from "commander";
8442
- import fs22 from "fs";
8926
+ import fs23 from "fs";
8443
8927
  import path26 from "path";
8444
8928
  import chalk17 from "chalk";
8445
8929
  import matter4 from "gray-matter";
8446
8930
  function readStatus(spokeVault) {
8447
8931
  const statusFile = path26.join(spokeVault, "status.md");
8448
- if (!fs22.existsSync(statusFile)) return {};
8932
+ if (!fs23.existsSync(statusFile)) return {};
8449
8933
  try {
8450
- return matter4(fs22.readFileSync(statusFile, "utf-8")).data;
8934
+ return matter4(fs23.readFileSync(statusFile, "utf-8")).data;
8451
8935
  } catch {
8452
8936
  return {};
8453
8937
  }
@@ -8867,7 +9351,7 @@ init_dist3();
8867
9351
  init_dist();
8868
9352
  import { Command as Command20 } from "commander";
8869
9353
  import chalk21 from "chalk";
8870
- import fs23 from "fs";
9354
+ import fs24 from "fs";
8871
9355
  import path27 from "path";
8872
9356
  import { fileURLToPath as fileURLToPath4 } from "url";
8873
9357
  function parseScope(v) {
@@ -8879,7 +9363,7 @@ function parseScope(v) {
8879
9363
  function findPackageRoot3() {
8880
9364
  let dir = path27.dirname(fileURLToPath4(import.meta.url));
8881
9365
  while (dir !== "/" && dir !== "") {
8882
- if (fs23.existsSync(path27.join(dir, "package.json"))) return dir;
9366
+ if (fs24.existsSync(path27.join(dir, "package.json"))) return dir;
8883
9367
  dir = path27.dirname(dir);
8884
9368
  }
8885
9369
  return process.cwd();
@@ -9073,13 +9557,14 @@ ${transcript.join("\n")}`
9073
9557
  init_dist();
9074
9558
  init_dist();
9075
9559
  init_dist();
9560
+ init_dist2();
9076
9561
  import { Command as Command22 } from "commander";
9077
- import fs24 from "fs";
9562
+ import fs25 from "fs";
9078
9563
  import { fileURLToPath as fileURLToPath5 } from "url";
9079
- import { dirname as dirname5, join as join20 } from "path";
9564
+ import { dirname as dirname5, join as join21 } from "path";
9080
9565
  import chalk22 from "chalk";
9081
9566
  function runConfigCheck(opts) {
9082
- const raw = JSON.parse(fs24.readFileSync(opts.configPath, "utf-8"));
9567
+ const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
9083
9568
  const def = getDefaultConfig();
9084
9569
  const items = detectDrift(raw, def);
9085
9570
  let working = raw;
@@ -9096,7 +9581,7 @@ function runConfigCheck(opts) {
9096
9581
  stamped = true;
9097
9582
  }
9098
9583
  if (opts.fix || opts.accept || stamped) {
9099
- fs24.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
9584
+ fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
9100
9585
  }
9101
9586
  return { items, applied, remaining, stamped };
9102
9587
  }
@@ -9167,7 +9652,7 @@ function printItems(items) {
9167
9652
  var configCommand = new Command22("config").description("Inspect and reconcile squadrant config");
9168
9653
  configCommand.command("check").description("Detect config drift vs the current default schema").option("--fix", "Apply the safe tier (add missing, remove deprecated)", false).option("--accept", "Stamp the current version without changing config (dismiss advisories)", false).option("--json", "Output drift items as JSON", false).action((opts) => {
9169
9654
  const pkgVersion = readPkgVersion2();
9170
- if (!fs24.existsSync(DEFAULT_CONFIG_PATH)) {
9655
+ if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
9171
9656
  console.log(chalk22.yellow("No config found \u2014 run `squadrant init` first."));
9172
9657
  return;
9173
9658
  }
@@ -9212,14 +9697,15 @@ configCommand.command("set").description("Write a config value by dotted key (e.
9212
9697
  }
9213
9698
  });
9214
9699
  function readPkgVersion2() {
9215
- const pkgPath = join20(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
9216
- return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version;
9700
+ const pkgPath = join21(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
9701
+ return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
9217
9702
  }
9218
9703
 
9219
9704
  // packages/cli/src/commands/heal.ts
9220
9705
  import { Command as Command23 } from "commander";
9221
9706
  import chalk23 from "chalk";
9222
9707
  init_dist2();
9708
+ init_dist2();
9223
9709
  function buildHealStatus(components) {
9224
9710
  if (components === null) {
9225
9711
  return { healthy: false, daemonUnreachable: true, components: [] };
@@ -9310,90 +9796,10 @@ var healCommand = new Command23("heal").description("Targeted, idempotent remedi
9310
9796
  // packages/cli/src/commands/group.ts
9311
9797
  init_dist();
9312
9798
  init_dist2();
9799
+ init_dist2();
9313
9800
  import { Command as Command24 } from "commander";
9314
9801
  import { execSync as execSync13 } from "child_process";
9315
- import { randomUUID as randomUUID4 } from "crypto";
9316
- import { homedir as homedir14 } from "os";
9317
- import { join as join21 } from "path";
9318
9802
  import chalk24 from "chalk";
9319
- var SOCK4 = join21(homedir14(), ".config", "squadrant", "squadrant.sock");
9320
- var WARMUP_TIMEOUT_MS = 12e4;
9321
- var WARMUP_POLL_MS = 1e3;
9322
- function resolveCurrentProject(config) {
9323
- const cwd = process.cwd();
9324
- for (const [name, proj] of Object.entries(config.projects)) {
9325
- const resolvedPath = resolveHome(proj.path);
9326
- if (cwd.startsWith(resolvedPath)) return name;
9327
- }
9328
- return null;
9329
- }
9330
- async function isCaptainAlive(project) {
9331
- try {
9332
- const health = await sendRequest(SOCK4, { kind: "health", project }, 5e3);
9333
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
9334
- return captain != null && captain.state !== "gone" && captain.state !== "unknown";
9335
- } catch {
9336
- return false;
9337
- }
9338
- }
9339
- async function waitForWarmup(project, timeoutMs = WARMUP_TIMEOUT_MS, pollMs = WARMUP_POLL_MS) {
9340
- const deadline = Date.now() + timeoutMs;
9341
- while (Date.now() < deadline) {
9342
- if (await isCaptainAlive(project)) return true;
9343
- await new Promise((r) => setTimeout(r, pollMs));
9344
- }
9345
- return false;
9346
- }
9347
- async function runGroupDispatch(opts) {
9348
- const config = loadConfig();
9349
- const fromCfg = config.projects[opts.fromProject];
9350
- const toCfg = config.projects[opts.toProject];
9351
- if (!toCfg) {
9352
- throw new Error(`target project '${opts.toProject}' not found in config`);
9353
- }
9354
- if (!fromCfg.group || !toCfg.group || fromCfg.group !== toCfg.group) {
9355
- throw new Error(
9356
- `cannot dispatch: '${opts.toProject}' (group: ${toCfg.group ?? "none"}) is not in the same group as '${opts.fromProject}' (group: ${fromCfg.group ?? "none"})`
9357
- );
9358
- }
9359
- if (toCfg.acceptDelegations === false) {
9360
- throw new Error(
9361
- `cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`
9362
- );
9363
- }
9364
- const alive = await isCaptainAlive(opts.toProject);
9365
- if (!alive) {
9366
- try {
9367
- execSync13(`squadrant launch ${opts.toProject}`, { stdio: "ignore", timeout: 15e3 });
9368
- } catch {
9369
- throw new Error(`failed to launch captain for '${opts.toProject}' \u2014 is squadrant installed?`);
9370
- }
9371
- const warmed = await waitForWarmup(opts.toProject, opts.warmupTimeoutMs, opts.warmupPollMs);
9372
- if (!warmed) {
9373
- throw new Error(
9374
- `dispatch to '${opts.toProject}' timed out waiting for captain warmup (>${(opts.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS) / 1e3}s)`
9375
- );
9376
- }
9377
- }
9378
- const now = Date.now();
9379
- const attemptId = randomUUID4();
9380
- const record = {
9381
- id: randomUUID4(),
9382
- project: opts.toProject,
9383
- originProject: opts.fromProject,
9384
- provider: opts.provider ?? "claude",
9385
- mode: opts.mode ?? "headless",
9386
- state: "submitted",
9387
- task: opts.task,
9388
- createdAt: now,
9389
- lastHeartbeat: now,
9390
- lastEvent: "dispatch",
9391
- heartbeatBudgetMs: 3e5,
9392
- attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
9393
- };
9394
- const result = await sendRequest(SOCK4, { kind: "dispatch", record });
9395
- return result;
9396
- }
9397
9803
  var groupCommand = new Command24("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
9398
9804
  new Command24("dispatch").description("Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name (must be in the same group)").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain relay to boot (default: 120)", (v) => parseInt(v, 10) * 1e3).action(async (toProject, task, opts) => {
9399
9805
  const fromProject = resolveCurrentProject(loadConfig());
@@ -9402,13 +9808,20 @@ var groupCommand = new Command24("group").description("Cross-project intra-group
9402
9808
  process.exit(1);
9403
9809
  }
9404
9810
  try {
9405
- const result = await runGroupDispatch({
9811
+ const result = await dispatchToSibling({
9406
9812
  fromProject,
9407
9813
  toProject,
9408
9814
  task,
9409
9815
  provider: opts.provider,
9410
9816
  mode: opts.mode,
9411
- warmupTimeoutMs: opts.warmupTimeout
9817
+ warmupTimeoutMs: opts.warmupTimeout,
9818
+ bootCaptain: async (project) => {
9819
+ try {
9820
+ execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
9821
+ } catch {
9822
+ throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
9823
+ }
9824
+ }
9412
9825
  });
9413
9826
  console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
9414
9827
  console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
@@ -9478,7 +9891,7 @@ var cmuxCommand = new Command25("cmux").description("cmux integration helpers").
9478
9891
 
9479
9892
  // packages/cli/src/commands/effort.ts
9480
9893
  init_dist();
9481
- import fs25 from "fs";
9894
+ import fs26 from "fs";
9482
9895
  import path28 from "path";
9483
9896
  import { Command as Command26 } from "commander";
9484
9897
  import chalk26 from "chalk";
@@ -9506,7 +9919,7 @@ function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH) {
9506
9919
  }
9507
9920
  function canonical(p) {
9508
9921
  try {
9509
- return fs25.realpathSync(p);
9922
+ return fs26.realpathSync(p);
9510
9923
  } catch {
9511
9924
  return path28.resolve(p);
9512
9925
  }
@@ -9562,59 +9975,6 @@ import chalk27 from "chalk";
9562
9975
  function defaultStateRoot() {
9563
9976
  return join22(dirname6(DEFAULT_CONFIG_PATH), "state");
9564
9977
  }
9565
- function runTelegramStatus(opts) {
9566
- const tg = opts.config.telegram;
9567
- const env = opts.env ?? process.env;
9568
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
9569
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
9570
- const sep2 = key.indexOf("::");
9571
- return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2), topicId };
9572
- });
9573
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
9574
- }
9575
- async function runTelegramSend(opts) {
9576
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9577
- if (topicId === void 0) {
9578
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
9579
- }
9580
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
9581
- return { chatId: opts.cfg.supergroupId, topicId };
9582
- }
9583
- function runTelegramNotifySet(opts) {
9584
- setNotify(opts.stateRoot, opts.project, opts.active);
9585
- }
9586
- function runTelegramNotifyPref(args) {
9587
- const { project, dimension, value, root } = args;
9588
- if (dimension === "crew") {
9589
- if (!["all", "alert_only", "done_only", "none"].includes(value))
9590
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
9591
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
9592
- return { ok: true };
9593
- }
9594
- if (value !== "on" && value !== "off") return { ok: false, message: "cap must be on|off" };
9595
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
9596
- return { ok: true };
9597
- }
9598
- function capAllowed(project, globalNotify, root) {
9599
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
9600
- }
9601
- function runTelegramNotifyStatus(opts) {
9602
- const s = loadState(opts.stateRoot);
9603
- const projects = /* @__PURE__ */ new Set();
9604
- for (const key of Object.keys(s.topics)) {
9605
- const sep2 = key.indexOf("::");
9606
- projects.add(sep2 === -1 ? key : key.slice(0, sep2));
9607
- }
9608
- for (const p of Object.keys(s.notify)) projects.add(p);
9609
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
9610
- }
9611
- async function runTelegramLink(opts) {
9612
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9613
- if (existing !== void 0) return { topicId: existing, created: false };
9614
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
9615
- setTopic(opts.stateRoot, opts.project, topicId);
9616
- return { topicId, created: true };
9617
- }
9618
9978
  async function questionMasked() {
9619
9979
  return new Promise((resolve3) => {
9620
9980
  emitKeypressEvents(process.stdin);
@@ -9658,44 +10018,6 @@ async function questionYesNo(prompt) {
9658
10018
  });
9659
10019
  });
9660
10020
  }
9661
- function confirmationText(project, before, after, dim) {
9662
- if (dim === "active") return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
9663
- if (dim === "cap") return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
9664
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
9665
- }
9666
- async function runNotifyConfirmation(opts) {
9667
- const { quieter, dim } = isQuieter(opts.before, opts.after);
9668
- if (!quieter || dim === null) return false;
9669
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9670
- if (topicId === void 0) return false;
9671
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
9672
- try {
9673
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
9674
- return true;
9675
- } catch {
9676
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
9677
- return false;
9678
- }
9679
- }
9680
- function resolveSetupToken(existingToken, opts) {
9681
- if (opts.resetToken || !existingToken) return "prompt";
9682
- return "try-reuse";
9683
- }
9684
- function resolveSetupUserId(flagUserId, detectedUserId, stateRoot) {
9685
- return flagUserId ?? detectedUserId ?? loadState(stateRoot).lastUserId;
9686
- }
9687
- async function runRegisterCommands(opts) {
9688
- await opts.client.setMyCommands(BOT_COMMANDS);
9689
- }
9690
- function runTelegramPostSetup(opts) {
9691
- const doRestart = opts.doRestart ?? restartDaemonIfRunning;
9692
- const outcome = doRestart({ reason: "telegram config" });
9693
- if (outcome === "skipped-not-running") {
9694
- console.log(chalk27.dim("(daemon not running \u2014 change applies on next start)"));
9695
- } else if (outcome === "skipped-opt-out") {
9696
- console.log(chalk27.dim("(run 'squadrant heal daemon' to apply)"));
9697
- }
9698
- }
9699
10021
  var telegramCommand = new Command27("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
9700
10022
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
9701
10023
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });