squadrant 0.11.3 → 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
@@ -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 });
@@ -4951,7 +5748,7 @@ function settingsHaveAgentTeams() {
4951
5748
  try {
4952
5749
  const home = process.env.HOME || "";
4953
5750
  const settings = JSON.parse(
4954
- fs13.readFileSync(`${home}/.claude/settings.json`, "utf-8")
5751
+ fs15.readFileSync(`${home}/.claude/settings.json`, "utf-8")
4955
5752
  );
4956
5753
  return settings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
4957
5754
  } catch {
@@ -4962,7 +5759,7 @@ function pluginInstalled(pluginKey) {
4962
5759
  try {
4963
5760
  const home = process.env.HOME || "";
4964
5761
  const plugins = JSON.parse(
4965
- fs13.readFileSync(
5762
+ fs15.readFileSync(
4966
5763
  `${home}/.claude/plugins/installed_plugins.json`,
4967
5764
  "utf-8"
4968
5765
  )
@@ -5007,7 +5804,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5007
5804
  ));
5008
5805
  results.push(check(
5009
5806
  "Obsidian installed",
5010
- commandExists("obsidian") || fs13.existsSync("/Applications/Obsidian.app"),
5807
+ commandExists("obsidian") || fs15.existsSync("/Applications/Obsidian.app"),
5011
5808
  "Install from: https://obsidian.md"
5012
5809
  ));
5013
5810
  results.push(check(
@@ -5101,7 +5898,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5101
5898
  const emitter = projectionRegistry.get(name);
5102
5899
  const [userDest] = emitter.destinations("user");
5103
5900
  if (!userDest) continue;
5104
- const dir = path16.dirname(userDest.path);
5901
+ const dir = path17.dirname(userDest.path);
5105
5902
  let status;
5106
5903
  try {
5107
5904
  await stat(dir);
@@ -5114,7 +5911,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
5114
5911
  results.push(
5115
5912
  check(
5116
5913
  "Squadrant config exists",
5117
- fs13.existsSync(
5914
+ fs15.existsSync(
5118
5915
  process.env.SQUADRANT_CONFIG || `${process.env.HOME}/.config/squadrant/config.json`
5119
5916
  ),
5120
5917
  "Run: squadrant init"
@@ -5189,28 +5986,28 @@ init_dist3();
5189
5986
  init_dist();
5190
5987
  init_dist4();
5191
5988
  import { Command as Command2 } from "commander";
5192
- import fs14 from "fs";
5193
- import path17 from "path";
5194
- import os8 from "os";
5989
+ import fs16 from "fs";
5990
+ import path18 from "path";
5991
+ import os9 from "os";
5195
5992
  import readline from "readline";
5196
5993
  import chalk4 from "chalk";
5197
5994
  function findPackageRoot() {
5198
- let dir = path17.dirname(new URL(import.meta.url).pathname);
5995
+ let dir = path18.dirname(new URL(import.meta.url).pathname);
5199
5996
  while (dir !== "/") {
5200
- if (fs14.existsSync(path17.join(dir, "package.json"))) return dir;
5201
- dir = path17.dirname(dir);
5997
+ if (fs16.existsSync(path18.join(dir, "package.json"))) return dir;
5998
+ dir = path18.dirname(dir);
5202
5999
  }
5203
6000
  return process.cwd();
5204
6001
  }
5205
6002
  function copyDirRecursive(src, dest) {
5206
- fs14.mkdirSync(dest, { recursive: true });
5207
- for (const entry of fs14.readdirSync(src, { withFileTypes: true })) {
5208
- const srcPath = path17.join(src, entry.name);
5209
- 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);
5210
6007
  if (entry.isDirectory()) {
5211
6008
  copyDirRecursive(srcPath, destPath);
5212
6009
  } else {
5213
- fs14.copyFileSync(srcPath, destPath);
6010
+ fs16.copyFileSync(srcPath, destPath);
5214
6011
  }
5215
6012
  }
5216
6013
  }
@@ -5230,7 +6027,7 @@ function promptLine(question) {
5230
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) => {
5231
6028
  const hubPath = resolveHome(opts.hub);
5232
6029
  const pkgRoot = findPackageRoot();
5233
- const configDir = path17.join(os8.homedir(), ".config", "squadrant");
6030
+ const configDir = path18.join(os9.homedir(), ".config", "squadrant");
5234
6031
  const isTTY = process.stdin.isTTY === true;
5235
6032
  console.log(chalk4.bold("\nSquadrant Init\n"));
5236
6033
  if (!isTTY) {
@@ -5253,8 +6050,8 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
5253
6050
  }
5254
6051
  const wsRegistry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
5255
6052
  try {
5256
- if (fs14.existsSync(DEFAULT_CONFIG_PATH)) {
5257
- const existing = JSON.parse(fs14.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
6053
+ if (fs16.existsSync(DEFAULT_CONFIG_PATH)) {
6054
+ const existing = JSON.parse(fs16.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
5258
6055
  wsRegistry.get(existing.workspace ?? "obsidian");
5259
6056
  }
5260
6057
  } catch (err) {
@@ -5262,7 +6059,7 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
5262
6059
  return;
5263
6060
  }
5264
6061
  stepHeader(1, 5, "Hub vault");
5265
- if (fs14.existsSync(DEFAULT_CONFIG_PATH)) {
6062
+ if (fs16.existsSync(DEFAULT_CONFIG_PATH)) {
5266
6063
  console.log(chalk4.yellow(" \u26A0 Config already exists, skipping creation"));
5267
6064
  } else {
5268
6065
  const config = getDefaultConfig();
@@ -5270,37 +6067,37 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
5270
6067
  saveConfig(config);
5271
6068
  console.log(chalk4.green(` \u2714 Config created at ${DEFAULT_CONFIG_PATH}`));
5272
6069
  }
5273
- const hubTemplate = path17.join(pkgRoot, "obsidian", "hub");
5274
- if (fs14.existsSync(hubPath)) {
6070
+ const hubTemplate = path18.join(pkgRoot, "obsidian", "hub");
6071
+ if (fs16.existsSync(hubPath)) {
5275
6072
  console.log(chalk4.yellow(` \u26A0 Hub vault already exists at ${hubPath}`));
5276
- } else if (fs14.existsSync(hubTemplate)) {
6073
+ } else if (fs16.existsSync(hubTemplate)) {
5277
6074
  copyDirRecursive(hubTemplate, hubPath);
5278
6075
  console.log(chalk4.green(` \u2714 Hub vault scaffolded at ${hubPath}`));
5279
6076
  } else {
5280
- fs14.mkdirSync(hubPath, { recursive: true });
6077
+ fs16.mkdirSync(hubPath, { recursive: true });
5281
6078
  console.log(chalk4.yellow(` \u26A0 Hub template not found; created empty directory at ${hubPath}`));
5282
6079
  }
5283
- const hubDashboardSrc = path17.join(pkgRoot, "obsidian", "hub", "dashboard.md");
5284
- const hubDashboardDest = path17.join(hubPath, "dashboard.md");
5285
- if (fs14.existsSync(hubDashboardSrc)) {
5286
- fs14.copyFileSync(hubDashboardSrc, 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);
5287
6084
  console.log(chalk4.green(` \u2714 Dashboard refreshed`));
5288
6085
  }
5289
- fs14.mkdirSync(path17.join(hubPath, "projects"), { recursive: true });
6086
+ fs16.mkdirSync(path18.join(hubPath, "projects"), { recursive: true });
5290
6087
  ensureRuntimeSynced({ sourceRoot: pkgRoot, runtimeRoot: configDir });
5291
6088
  console.log(chalk4.green(` \u2714 Runtime assets synced to ${configDir}`));
5292
6089
  stepHeader(2, 5, "Agent + projection setup");
5293
- const settingsPath = path17.join(os8.homedir(), ".claude", "settings.json");
6090
+ const settingsPath = path18.join(os9.homedir(), ".claude", "settings.json");
5294
6091
  try {
5295
6092
  let settings = {};
5296
- if (fs14.existsSync(settingsPath)) {
5297
- settings = JSON.parse(fs14.readFileSync(settingsPath, "utf-8"));
6093
+ if (fs16.existsSync(settingsPath)) {
6094
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
5298
6095
  }
5299
6096
  const env = settings.env || {};
5300
6097
  if (env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
5301
6098
  settings.env = { ...env, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
5302
- fs14.mkdirSync(path17.dirname(settingsPath), { recursive: true });
5303
- fs14.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
6099
+ fs16.mkdirSync(path18.dirname(settingsPath), { recursive: true });
6100
+ fs16.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5304
6101
  console.log(chalk4.green(" \u2714 Agent Teams enabled in ~/.claude/settings.json"));
5305
6102
  } else {
5306
6103
  console.log(chalk4.green(" \u2714 Agent Teams already enabled"));
@@ -5343,7 +6140,7 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
5343
6140
  chalk4.cyan(" Absolute path to your first project (Enter to skip): ")
5344
6141
  );
5345
6142
  if (projectPath) {
5346
- const projectName = path17.basename(projectPath);
6143
+ const projectName = path18.basename(projectPath);
5347
6144
  console.log(chalk4.bold(`
5348
6145
  Run this to register it:`));
5349
6146
  console.log(chalk4.cyan(` squadrant projects add ${projectName} ${projectPath}
@@ -5369,45 +6166,11 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
5369
6166
 
5370
6167
  // packages/cli/src/commands/projects.ts
5371
6168
  init_dist();
6169
+ init_dist2();
5372
6170
  import { Command as Command3 } from "commander";
5373
- import fs15 from "fs";
5374
- import path18 from "path";
6171
+ import fs17 from "fs";
6172
+ import path19 from "path";
5375
6173
  import chalk5 from "chalk";
5376
-
5377
- // packages/cli/src/control/restart-daemon.ts
5378
- init_dist2();
5379
- import { execFileSync as execFileSync6 } from "child_process";
5380
- import { existsSync as existsSync9 } from "fs";
5381
- import { homedir as homedir9 } from "os";
5382
- import { join as join14 } from "path";
5383
- var DEFAULT_SOCK_PATH = join14(homedir9(), ".config", "squadrant", "squadrant.sock");
5384
- function defaultIsRunning() {
5385
- return existsSync9(DEFAULT_SOCK_PATH);
5386
- }
5387
- function defaultRunKickstart() {
5388
- const uid = process.getuid?.() ?? 0;
5389
- const target = `gui/${uid}/${LABEL}`;
5390
- if (tryAcquireDaemonLock()) {
5391
- try {
5392
- execFileSync6("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
5393
- } finally {
5394
- releaseDaemonLock();
5395
- }
5396
- }
5397
- }
5398
- function restartDaemonIfRunning(opts) {
5399
- const env = opts.env ?? process.env;
5400
- if (env["VITEST"] || opts.noRestart) return "skipped-opt-out";
5401
- const isRunning = opts.isRunning ?? defaultIsRunning;
5402
- if (!isRunning()) return "skipped-not-running";
5403
- const log = opts.log ?? console.log;
5404
- log(`\u21BB restarting daemon to apply ${opts.reason}\u2026`);
5405
- const runKickstart = opts.runKickstart ?? defaultRunKickstart;
5406
- runKickstart();
5407
- return "restarted";
5408
- }
5409
-
5410
- // packages/cli/src/commands/projects.ts
5411
6174
  function restartAfterProjectsAdd(opts) {
5412
6175
  const doRestart = opts.doRestart ?? restartDaemonIfRunning;
5413
6176
  const outcome = doRestart({ reason: "project registration", noRestart: opts.noRestart });
@@ -5418,22 +6181,22 @@ function restartAfterProjectsAdd(opts) {
5418
6181
  }
5419
6182
  }
5420
6183
  function findPackageRoot2() {
5421
- let dir = path18.dirname(new URL(import.meta.url).pathname);
6184
+ let dir = path19.dirname(new URL(import.meta.url).pathname);
5422
6185
  while (dir !== "/") {
5423
- if (fs15.existsSync(path18.join(dir, "package.json"))) return dir;
5424
- dir = path18.dirname(dir);
6186
+ if (fs17.existsSync(path19.join(dir, "package.json"))) return dir;
6187
+ dir = path19.dirname(dir);
5425
6188
  }
5426
6189
  return process.cwd();
5427
6190
  }
5428
6191
  function copyDirRecursive2(src, dest) {
5429
- fs15.mkdirSync(dest, { recursive: true });
5430
- for (const entry of fs15.readdirSync(src, { withFileTypes: true })) {
5431
- const srcPath = path18.join(src, entry.name);
5432
- 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);
5433
6196
  if (entry.isDirectory()) {
5434
6197
  copyDirRecursive2(srcPath, destPath);
5435
6198
  } else {
5436
- fs15.copyFileSync(srcPath, destPath);
6199
+ fs17.copyFileSync(srcPath, destPath);
5437
6200
  }
5438
6201
  }
5439
6202
  }
@@ -5469,7 +6232,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5469
6232
  process.exit(1);
5470
6233
  }
5471
6234
  const resolvedPath = resolveHome(projectPath);
5472
- if (!fs15.existsSync(path18.join(resolvedPath, ".git"))) {
6235
+ if (!fs17.existsSync(path19.join(resolvedPath, ".git"))) {
5473
6236
  console.log(chalk5.yellow(`
5474
6237
  \u26A0 No .git found at ${resolvedPath}. Make sure this is the project root, not a parent directory.
5475
6238
  `));
@@ -5522,7 +6285,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5522
6285
  \u26A0 Group '${group}' already has '${primary[0]}' as primary. Overriding.`));
5523
6286
  }
5524
6287
  }
5525
- 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);
5526
6289
  const project = {
5527
6290
  path: resolvedPath,
5528
6291
  captainName,
@@ -5537,20 +6300,20 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
5537
6300
  \u2714 Project '${name}' registered`));
5538
6301
  restartAfterProjectsAdd({ noRestart: opts.restart === false });
5539
6302
  const pkgRoot = findPackageRoot2();
5540
- const spokeTemplate = path18.join(pkgRoot, "obsidian", "spoke");
5541
- if (fs15.existsSync(spokeVault)) {
6303
+ const spokeTemplate = path19.join(pkgRoot, "obsidian", "spoke");
6304
+ if (fs17.existsSync(spokeVault)) {
5542
6305
  console.log(chalk5.yellow(` \u26A0 Spoke vault already exists at ${spokeVault}, skipping scaffold`));
5543
- } else if (fs15.existsSync(spokeTemplate)) {
6306
+ } else if (fs17.existsSync(spokeTemplate)) {
5544
6307
  copyDirRecursive2(spokeTemplate, spokeVault);
5545
- const statusPath = path18.join(spokeVault, "status.md");
5546
- if (fs15.existsSync(statusPath)) {
5547
- 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");
5548
6311
  const updated = content.replace(/^project: unnamed/m, `project: ${name}`);
5549
- fs15.writeFileSync(statusPath, updated);
6312
+ fs17.writeFileSync(statusPath, updated);
5550
6313
  }
5551
6314
  console.log(chalk5.green(` \u2714 Spoke vault scaffolded at ${spokeVault}`));
5552
6315
  } else {
5553
- fs15.mkdirSync(spokeVault, { recursive: true });
6316
+ fs17.mkdirSync(spokeVault, { recursive: true });
5554
6317
  console.log(chalk5.yellow(` \u26A0 Spoke template not found; created empty dir at ${spokeVault}`));
5555
6318
  }
5556
6319
  console.log("");
@@ -5651,10 +6414,10 @@ init_dist4();
5651
6414
  init_dist();
5652
6415
  import { Command as Command5 } from "commander";
5653
6416
  import { execSync as execSync9 } from "child_process";
5654
- import path19 from "path";
5655
- import os9 from "os";
6417
+ import path20 from "path";
6418
+ import os10 from "os";
5656
6419
  import chalk7 from "chalk";
5657
- var TEMPLATES_DIR = path19.join(os9.homedir(), ".config", "squadrant", "templates");
6420
+ var TEMPLATES_DIR2 = path20.join(os10.homedir(), ".config", "squadrant", "templates");
5658
6421
  var TASK_PROMPTS = {
5659
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.",
5660
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.",
@@ -5687,7 +6450,7 @@ async function runCommandSpawn(input) {
5687
6450
  if (!agent) {
5688
6451
  throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
5689
6452
  }
5690
- const promptFile = path19.join(TEMPLATES_DIR, `command.${agent.templateSuffix}.md`);
6453
+ const promptFile = path20.join(TEMPLATES_DIR2, `command.${agent.templateSuffix}.md`);
5691
6454
  const cliCommand = agent.buildCommand({
5692
6455
  prompt,
5693
6456
  workdir: process.cwd(),
@@ -5712,35 +6475,11 @@ var commandCommand = new Command5("command").description("Spawn a one-shot Comma
5712
6475
 
5713
6476
  // packages/cli/src/commands/crew.ts
5714
6477
  init_dist();
5715
- init_dist();
5716
- import { Command as Command9 } from "commander";
5717
- import fs16 from "fs";
5718
- import path20 from "path";
5719
- import os10 from "os";
5720
- import chalk9 from "chalk";
5721
-
5722
- // packages/cli/src/control/crew-routing.ts
5723
- function resolveCrewRoute(taskText, config) {
5724
- const rules = config.defaults.crewRouting?.rules;
5725
- if (!rules || rules.length === 0) return null;
5726
- for (const rule2 of rules) {
5727
- const re = new RegExp(rule2.match, "i");
5728
- if (re.test(taskText)) {
5729
- return {
5730
- agent: rule2.agent,
5731
- ...rule2.model !== void 0 ? { model: rule2.model } : {},
5732
- tier: rule2.tier,
5733
- matchedRule: rule2.match
5734
- };
5735
- }
5736
- }
5737
- return null;
5738
- }
5739
-
5740
- // packages/cli/src/commands/crew.ts
5741
- init_dist3();
5742
6478
  init_dist3();
5743
6479
  init_dist4();
6480
+ init_dist2();
6481
+ import { Command as Command9 } from "commander";
6482
+ import chalk9 from "chalk";
5744
6483
 
5745
6484
  // packages/cli/src/commands/crew-control.ts
5746
6485
  init_dist2();
@@ -5748,9 +6487,9 @@ init_dist2();
5748
6487
  init_dist4();
5749
6488
  import { Command as Command8 } from "commander";
5750
6489
  import { createConnection as createConnection3 } from "net";
5751
- import { randomUUID as randomUUID3 } from "crypto";
5752
- import { homedir as homedir11 } from "os";
5753
- 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";
5754
6493
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
5755
6494
 
5756
6495
  // packages/cli/src/commands/crew-output.ts
@@ -5809,11 +6548,11 @@ init_dist2();
5809
6548
  import { Command as Command6 } from "commander";
5810
6549
  import chalk8 from "chalk";
5811
6550
  import { createConnection as createConnection2 } from "net";
5812
- import { homedir as homedir10 } from "os";
5813
- import { join as join15 } from "path";
6551
+ import { homedir as homedir11 } from "os";
6552
+ import { join as join16 } from "path";
5814
6553
  import { createInterface } from "readline";
5815
6554
  function socketPath() {
5816
- return process.env.SQUADRANTD_SOCK ?? join15(homedir10(), ".config", "squadrant", "squadrant.sock");
6555
+ return process.env.SQUADRANTD_SOCK ?? join16(homedir11(), ".config", "squadrant", "squadrant.sock");
5817
6556
  }
5818
6557
  function rule(width, ch = "\u2500") {
5819
6558
  return ch.repeat(Math.max(0, width));
@@ -6038,7 +6777,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
6038
6777
  if (opts.provider !== "codex") {
6039
6778
  throw new Error(`crew chat is implemented for provider=codex only (got '${opts.provider}')`);
6040
6779
  }
6041
- const pane = await runCrewSpawn({
6780
+ const pane = await runCrewSpawn2({
6042
6781
  project: opts.project,
6043
6782
  task: "(interactive)",
6044
6783
  agent: "codex",
@@ -6049,7 +6788,7 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
6049
6788
  });
6050
6789
 
6051
6790
  // packages/cli/src/commands/crew-control.ts
6052
- var SOCK2 = join16(homedir11(), ".config", "squadrant", "squadrant.sock");
6791
+ var SOCK2 = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
6053
6792
  var CODEX_FIRST_TURN_DELAY_MS = 1500;
6054
6793
  async function sendCodexFirstTurn(taskId, text) {
6055
6794
  await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
@@ -6076,11 +6815,11 @@ async function sendCodexFirstTurn(taskId, text) {
6076
6815
  }
6077
6816
  function buildDispatchRequest(o) {
6078
6817
  const now = Date.now();
6079
- const attemptId = randomUUID3();
6818
+ const attemptId = randomUUID4();
6080
6819
  return {
6081
6820
  kind: "dispatch",
6082
6821
  record: {
6083
- id: randomUUID3(),
6822
+ id: randomUUID4(),
6084
6823
  project: o.project,
6085
6824
  provider: o.provider,
6086
6825
  mode: o.mode,
@@ -6154,9 +6893,9 @@ function buildSignalRequest(signal, o) {
6154
6893
  return { kind: "event", project, event };
6155
6894
  }
6156
6895
  function defaultWriteResult(id, payload) {
6157
- const dir = join16(homedir11(), ".config", "squadrant", "state", "_results");
6896
+ const dir = join17(homedir12(), ".config", "squadrant", "state", "_results");
6158
6897
  mkdirSync6(dir, { recursive: true });
6159
- const file = join16(dir, `${id}.txt`);
6898
+ const file = join17(dir, `${id}.txt`);
6160
6899
  writeFileSync7(file, payload);
6161
6900
  return file;
6162
6901
  }
@@ -6257,7 +6996,7 @@ addControlPlaneCrewCommands(crewControlCommand);
6257
6996
  // packages/cli/src/lib/per-crew-settings.ts
6258
6997
  init_dist4();
6259
6998
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
6260
- import { join as join17 } from "path";
6999
+ import { join as join18 } from "path";
6261
7000
  var CREW_PERMISSION_ALLOWLIST = [
6262
7001
  // git — read + safe mutations (reset/clean/config intentionally excluded)
6263
7002
  "Bash(git status:*)",
@@ -6348,9 +7087,9 @@ function mergeCrewPermissions(settings) {
6348
7087
  return next;
6349
7088
  }
6350
7089
  function writePerCrewSettingsLocal(o) {
6351
- const dir = join17(o.projectCwd, ".claude");
7090
+ const dir = join18(o.projectCwd, ".claude");
6352
7091
  mkdirSync7(dir, { recursive: true });
6353
- const file = join17(dir, "settings.local.json");
7092
+ const file = join18(dir, "settings.local.json");
6354
7093
  let existing = {};
6355
7094
  try {
6356
7095
  const raw = healStaleCockpitRefs(readFileSync9(file, "utf-8"));
@@ -6363,304 +7102,86 @@ function writePerCrewSettingsLocal(o) {
6363
7102
  return file;
6364
7103
  }
6365
7104
  function writePerCrewOpencodeConfig(o) {
6366
- const dir = join17(o.stateRoot, o.project, o.taskId);
7105
+ const dir = join18(o.stateRoot, o.project, o.taskId);
6367
7106
  mkdirSync7(dir, { recursive: true });
6368
- const file = join17(dir, "opencode.json");
7107
+ const file = join18(dir, "opencode.json");
6369
7108
  const config = {
6370
7109
  permission: {
6371
7110
  read: "allow",
6372
7111
  edit: "allow",
6373
- glob: "allow",
6374
- grep: "allow",
6375
- bash: o.gateBash ? "ask" : "allow",
6376
- webfetch: "allow",
6377
- websearch: "allow",
6378
- task: "allow",
6379
- lsp: "allow",
6380
- external_directory: { "**": "allow" }
6381
- }
6382
- };
6383
- writeFileSync8(file, JSON.stringify(config, null, 2));
6384
- return file;
6385
- }
6386
-
6387
- // packages/cli/src/commands/crew.ts
6388
- init_dist2();
6389
- var TEMPLATES_DIR2 = path20.join(os10.homedir(), ".config", "squadrant", "templates");
6390
- async function runCrewSpawn(input) {
6391
- const config = loadConfig();
6392
- const proj = config.projects[input.project];
6393
- if (!proj) {
6394
- throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
6395
- }
6396
- const runtime = new RuntimeRegistry({ cmux: createCmuxDriver() }).forProject(input.project, config);
6397
- const captain = await runtime.status(proj.captainName);
6398
- if (!captain) {
6399
- throw new Error(
6400
- `Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`
6401
- );
6402
- }
6403
- const existing = await listProjectCrews(runtime, captain.id, input.project);
6404
- const existingTitles = existing.map((s) => s.title);
6405
- if (input.name) {
6406
- const wantTitle = titleFor(input.project, input.name);
6407
- if (existingTitles.includes(wantTitle)) {
6408
- throw new Error(
6409
- `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.`
6410
- );
6411
- }
6412
- }
6413
- const name = input.name ?? nextAutoName(existingTitles, input.project);
6414
- const spawnCwd = !input.shared ? addWorktree({
6415
- repoRoot: proj.path,
6416
- worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
6417
- project: input.project,
6418
- name,
6419
- base: resolveWorktreeBase(proj.path)
6420
- }) : proj.path;
6421
- const route = !input.agentExplicit && !input.model ? resolveCrewRoute(input.task, config) : null;
6422
- if (route) {
6423
- console.log(chalk9.dim(`routed: tier=${route.tier} \u2192 ${route.agent}${route.model ? `/${route.model}` : ""} (rule: "${route.matchedRule}")`));
6424
- }
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);
6425
7130
  const agents = new CapabilityRegistry({
6426
7131
  claude: createClaudeDriver(),
6427
7132
  codex: createCodexDriver(),
6428
7133
  gemini: createGeminiDriver(),
6429
7134
  opencode: createOpencodeDriver()
6430
7135
  });
6431
- const agentName = route?.agent ?? input.agent ?? "claude";
6432
- const agent = agents.get(agentName);
6433
- if (!agent) {
6434
- throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
6435
- }
6436
- if (agentName === "codex") {
6437
- const codexRoleFile = path20.join(TEMPLATES_DIR2, `crew.${agent.templateSuffix}.md`);
6438
- const roleInstructions = fs16.existsSync(codexRoleFile) ? fs16.readFileSync(codexRoleFile, "utf8") : void 0;
6439
- return runCodexInteractiveSpawn({
6440
- project: input.project,
6441
- task: input.task,
6442
- cwd: spawnCwd,
6443
- runtime,
6444
- workspaceId: captain.id,
6445
- name,
6446
- direction: input.direction ?? "tab",
6447
- approvalPolicy: input.approvalPolicy,
6448
- roleInstructions
6449
- });
6450
- }
6451
- const promptFile = path20.join(TEMPLATES_DIR2, `crew.${agent.templateSuffix}.md`);
6452
- const interactive = agent.name === "claude" || agent.name === "opencode";
6453
- const crewRole = config.defaults.roles?.crew;
6454
- const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
6455
- const crewModel = input.model ?? route?.model ?? configModel;
6456
- if (agentName === "claude") {
6457
- const req = buildDispatchRequest({
6458
- provider: "claude",
6459
- mode: "interactive",
6460
- project: input.project,
6461
- cwd: spawnCwd,
6462
- task: input.task,
6463
- name
6464
- });
6465
- const rec = await squadrantdCall(req);
6466
- writePerCrewSettingsLocal({ projectCwd: spawnCwd });
6467
- const cliCommand2 = agent.buildCommand({
6468
- prompt: input.task,
6469
- workdir: spawnCwd,
6470
- role: "crew",
6471
- promptFile,
6472
- interactive: true,
6473
- // Permission mode is config-driven (defaults.permissions.crew) so squadrant
6474
- // can default crews to 'auto' or keep the semi-automatic 'acceptEdits'
6475
- // gate (auto-accept edits, still prompt for risky ops). Falls back to
6476
- // 'acceptEdits' when unset.
6477
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
6478
- ...crewModel ? { model: crewModel } : {}
6479
- });
6480
- const direction2 = input.direction ?? "tab";
6481
- const title2 = titleFor(input.project, name);
6482
- const pane2 = await runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
6483
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
6484
- await runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${cliCommand2}`);
6485
- const preLaunchScreen = await runtime.readPaneScreen(pane2) ?? "";
6486
- await sendFirstTurnWhenReady(runtime, pane2, `${input.task}
6487
-
6488
- ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
6489
- return { ...pane2, title: title2 };
6490
- }
6491
- if (agentName === "opencode") {
6492
- const serverPort = await getFreePort();
6493
- const req = buildDispatchRequest({
6494
- provider: "opencode",
6495
- mode: "interactive",
6496
- project: input.project,
6497
- cwd: spawnCwd,
6498
- task: input.task,
6499
- name,
6500
- // opencode has no heartbeat hook, so a normal budget would false-stall
6501
- // every crew after 5min; use a 24h budget to effectively disable stall
6502
- // detection. The SSE bridge (serverPort) provides turn-end liveness.
6503
- budgetMs: 864e5,
6504
- serverPort
6505
- });
6506
- const rec = await squadrantdCall(req);
6507
- const opencodeConfigPath = writePerCrewOpencodeConfig({
6508
- stateRoot: path20.join(os10.homedir(), ".config", "squadrant", "state"),
6509
- project: input.project,
6510
- taskId: rec.id,
6511
- // CP3 opt-in: --approval gates bash so the captain approves shell commands.
6512
- // Without it, bash stays auto-approved (default behavior unchanged).
6513
- ...input.approval ? { gateBash: true } : {}
6514
- });
6515
- const cliCommand2 = agent.buildCommand({
6516
- prompt: input.task,
6517
- workdir: spawnCwd,
6518
- role: "crew",
6519
- promptFile,
6520
- interactive: true,
6521
- model: crewModel,
6522
- port: serverPort
6523
- });
6524
- const direction2 = input.direction ?? "tab";
6525
- const title2 = titleFor(input.project, name);
6526
- const pane2 = await runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
6527
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
6528
- await runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} OPENCODE_CONFIG=${opencodeConfigPath} ${cliCommand2}`);
6529
- const preLaunchScreen = await runtime.readPaneScreen(pane2) ?? "";
6530
- await sendFirstTurnWhenReady(runtime, pane2, `${input.task}
6531
-
6532
- ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
6533
- // #235: opencode's idle splash ("Ask anything…") keeps mutating (cursor
6534
- // blink, status line toggle), so the old screen-changed check would always
6535
- // see a *different* screen and never re-send a dropped turn. The splashMarker
6536
- // confirms the TUI actually left splash before declaring acceptance.
6537
- splashMarker: "Ask anything\u2026",
6538
- // opencode has a wider boot-race window than claude, so allow 3 retries
6539
- // instead of the default 2.
6540
- retryLimit: 3
6541
- });
6542
- return { ...pane2, title: title2 };
6543
- }
6544
- const cliCommand = agent.buildCommand({
6545
- prompt: input.task,
6546
- workdir: spawnCwd,
6547
- role: "crew",
6548
- promptFile,
6549
- interactive,
6550
- model: crewModel
6551
- });
6552
- const direction = input.direction ?? "tab";
6553
- const title = titleFor(input.project, name);
6554
- const pane = await runtime.newPane({ workspaceId: captain.id, direction, title });
6555
- await runtime.sendToPane(pane, cliCommand);
6556
- if (interactive) {
6557
- const preLaunchScreen = await runtime.readPaneScreen(pane) ?? "";
6558
- await sendFirstTurnWhenReady(runtime, pane, input.task, preLaunchScreen);
6559
- }
6560
- return { ...pane, title };
6561
- }
6562
- async function runCodexInteractiveSpawn(o) {
6563
- const req = buildDispatchRequest({
6564
- provider: "codex",
6565
- mode: "interactive",
6566
- project: o.project,
6567
- cwd: o.cwd,
6568
- task: o.task,
6569
- name: o.name,
6570
- ...o.approvalPolicy ? { approvalPolicy: o.approvalPolicy } : {},
6571
- ...o.roleInstructions ? { roleInstructions: o.roleInstructions } : {}
6572
- });
6573
- const rec = await squadrantdCall(req);
6574
- const title = titleFor(o.project, o.name);
6575
- const pane = await o.runtime.newPane({
6576
- workspaceId: o.workspaceId,
6577
- direction: o.direction,
6578
- 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
+ )
6579
7155
  });
6580
- await o.runtime.sendToPane(pane, `squadrant crew attach ${rec.id}`);
6581
- if (o.task && o.task !== "(interactive)") {
6582
- void sendCodexFirstTurn(rec.id, o.task).catch((e) => {
6583
- process.stderr.write(`(first-turn delivery failed: ${e.message})
6584
- `);
6585
- });
6586
- }
6587
- return { ...pane, title };
6588
7156
  }
6589
- async function runCrewSend(project, name, message) {
7157
+ async function runCrewSend2(project, name, message) {
6590
7158
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6591
- const crew = await findCrew(runtime, workspaceId, project, name);
6592
- if (!crew) {
6593
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6594
- }
6595
- try {
6596
- const tasks = await squadrantdCall({ kind: "list", project });
6597
- const task = tasks.find((t) => t.name === name);
6598
- if (task) {
6599
- if (TERMINAL_STATES.has(task.state)) {
6600
- await squadrantdCall({ kind: "event", project, event: { type: "task.reopened", id: task.id } });
6601
- } else if (task.state === "blocked" || task.state === "awaiting-input") {
6602
- await squadrantdCall({ kind: "event", project, event: { type: "task.started", id: task.id } });
6603
- }
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 });
6604
7163
  }
6605
- } catch {
6606
- }
6607
- await runtime.sendToPane(crew, message);
7164
+ });
6608
7165
  }
6609
- async function runCrewRead(project, name) {
7166
+ async function runCrewRead2(project, name) {
6610
7167
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6611
- const crew = await findCrew(runtime, workspaceId, project, name);
6612
- if (!crew) {
6613
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6614
- }
6615
- return runtime.readPaneScreen(crew);
7168
+ return runCrewRead(project, name, runtime, workspaceId);
6616
7169
  }
6617
- async function runCrewClose(project, name) {
7170
+ async function runCrewClose2(project, name) {
6618
7171
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6619
- const projRoot = loadConfig().projects[project]?.path;
6620
- let taskId;
6621
- let worktreeCwd;
6622
- try {
6623
- const tasks = await squadrantdCall({ kind: "list", project });
6624
- const task = tasks.find((t) => t.name === name);
6625
- if (task) {
6626
- taskId = task.id;
6627
- if (task.cwd && projRoot && task.cwd !== projRoot) {
6628
- worktreeCwd = task.cwd;
6629
- }
6630
- if (!TERMINAL_STATES.has(task.state)) {
6631
- await squadrantdCall({ kind: "event", project, event: { type: "task.cancelled", id: task.id, reason: "closed by captain" } });
6632
- }
6633
- if (task.provider === "codex") {
6634
- await squadrantdCall({ kind: "codex-close", taskId: task.id });
6635
- }
6636
- }
6637
- } catch {
6638
- }
6639
- const crew = await findCrew(runtime, workspaceId, project, name);
6640
- if (crew) {
6641
- await runtime.closePane(crew);
6642
- } else if (taskId === void 0) {
6643
- throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6644
- }
6645
- if (taskId !== void 0) {
6646
- await reapCrewChildren(taskId);
6647
- }
6648
- if (worktreeCwd && projRoot) {
6649
- try {
6650
- removeWorktree(projRoot, worktreeCwd);
6651
- } catch (e) {
6652
- process.stderr.write(`(worktree remove failed: ${e.message})
6653
- `);
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 });
6654
7179
  }
6655
- }
7180
+ });
6656
7181
  }
6657
- async function runCrewList(project) {
7182
+ async function runCrewList2(project) {
6658
7183
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6659
- const crews = await listProjectCrews(runtime, workspaceId, project);
6660
- return crews.map((c) => ({
6661
- name: nameFromTitle(project, c.title),
6662
- surfaceId: c.surfaceId
6663
- }));
7184
+ return runCrewList(project, runtime, workspaceId);
6664
7185
  }
6665
7186
  var crewCommand = new Command9("crew").description(
6666
7187
  "Spawn and manage interactive crew sessions next to the project's captain"
@@ -6672,7 +7193,7 @@ crewCommand.command("spawn").description(
6672
7193
  try {
6673
7194
  const resolvedTask = await resolveTextInput({ positional: task, filePath: opts.taskFile, label: "task" });
6674
7195
  const agentExplicit = cmd.getOptionValueSource("agent") === "cli";
6675
- const pane = await runCrewSpawn({
7196
+ const pane = await runCrewSpawn2({
6676
7197
  project,
6677
7198
  task: resolvedTask,
6678
7199
  name: opts.name,
@@ -6694,7 +7215,7 @@ crewCommand.command("spawn").description(
6694
7215
  );
6695
7216
  crewCommand.command("list").description("List live crew sessions for a project").argument("<project>", "Project name").action(async (project) => {
6696
7217
  try {
6697
- const crews = await runCrewList(project);
7218
+ const crews = await runCrewList2(project);
6698
7219
  if (crews.length === 0) {
6699
7220
  console.log(chalk9.yellow(`No live crew sessions for ${project}.`));
6700
7221
  return;
@@ -6710,7 +7231,7 @@ crewCommand.command("list").description("List live crew sessions for a project")
6710
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) => {
6711
7232
  try {
6712
7233
  const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
6713
- await runCrewSend(project, name, resolvedMessage);
7234
+ await runCrewSend2(project, name, resolvedMessage);
6714
7235
  console.log(chalk9.green(`\u2714 Sent to ${project}:${name}`));
6715
7236
  } catch (err) {
6716
7237
  console.error(chalk9.red(err.message));
@@ -6719,7 +7240,7 @@ crewCommand.command("send").description("Send a follow-up message to an existing
6719
7240
  });
6720
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) => {
6721
7242
  try {
6722
- const screen = await runCrewRead(project, name);
7243
+ const screen = await runCrewRead2(project, name);
6723
7244
  const out = opts.full ? screen : tailLines(screen, Number(opts.lines ?? 40));
6724
7245
  console.log(out);
6725
7246
  } catch (err) {
@@ -6729,7 +7250,7 @@ crewCommand.command("read").description("Read the current screen of a crew sessi
6729
7250
  });
6730
7251
  crewCommand.command("close").description("Shutdown a crew session (closes its tab)").argument("<project>", "Project name").argument("<name>", "Crew name").action(async (project, name) => {
6731
7252
  try {
6732
- await runCrewClose(project, name);
7253
+ await runCrewClose2(project, name);
6733
7254
  console.log(chalk9.green(`\u2714 Closed ${project}:${name}`));
6734
7255
  } catch (err) {
6735
7256
  console.error(chalk9.red(err.message));
@@ -6743,90 +7264,23 @@ init_dist3();
6743
7264
  init_dist4();
6744
7265
  init_dist3();
6745
7266
  init_dist();
6746
- init_dist();
7267
+ init_dist2();
6747
7268
  import { Command as Command10 } from "commander";
6748
- import fs17 from "fs";
7269
+ import fs18 from "fs";
6749
7270
  import path21 from "path";
6750
7271
  import os11 from "os";
6751
7272
  import chalk10 from "chalk";
6752
7273
  var TEMPLATES_DIR3 = path21.join(os11.homedir(), ".config", "squadrant", "templates");
6753
- var SIDE_ROLES = ["research", "debug"];
6754
- function shellQuote2(p) {
6755
- return "'" + p.replace(/'/g, "'\\''") + "'";
6756
- }
6757
- function titleFor2(project, name) {
6758
- return `\u{1F5D2} ${project}:${name}`;
6759
- }
6760
- function isSideTitle(project, title) {
6761
- return title.startsWith(`\u{1F5D2} ${project}:`);
6762
- }
6763
- function nameFromTitle2(project, title) {
6764
- return title.slice(`\u{1F5D2} ${project}:`.length);
6765
- }
6766
- function nextAutoName2(existingTitles, project) {
6767
- const used = /* @__PURE__ */ new Set();
6768
- for (const title of existingTitles) {
6769
- const n = nameFromTitle2(project, title).match(/^side-(\d+)$/);
6770
- if (n) used.add(Number(n[1]));
6771
- }
6772
- let i = 1;
6773
- while (used.has(i)) i++;
6774
- return `side-${i}`;
6775
- }
6776
- function buildSideFirstTurn(topic, project, role, spokeVault, scratchWorktree) {
6777
- const lines = [
6778
- topic,
6779
- "",
6780
- "---",
6781
- "Side-session context (for handoff use):",
6782
- `Project: ${project}`,
6783
- `Role: ${role}`,
6784
- `Spoke vault: ${spokeVault}`
6785
- ];
6786
- if (scratchWorktree) {
6787
- lines.push(`Scratch worktree: ${scratchWorktree}`);
6788
- }
6789
- return lines.join("\n");
6790
- }
6791
- async function runSideSpawn(input) {
7274
+ async function runSideSpawn2(input) {
6792
7275
  const config = loadConfig();
6793
7276
  const proj = config.projects[input.project];
6794
7277
  if (!proj) {
6795
7278
  throw new Error(`Project '${input.project}' not found. Run 'squadrant projects list'.`);
6796
7279
  }
6797
- if (!SIDE_ROLES.includes(input.role)) {
6798
- throw new Error(
6799
- `Unknown side role '${input.role}'. Valid roles: ${SIDE_ROLES.join(", ")}.`
6800
- );
6801
- }
6802
7280
  const runtime = new RuntimeRegistry({ cmux: createCmuxDriver() }).forProject(
6803
7281
  input.project,
6804
7282
  config
6805
7283
  );
6806
- const captain = await runtime.status(proj.captainName);
6807
- if (!captain) {
6808
- throw new Error(
6809
- `Captain workspace '${proj.captainName}' is not running. Run 'squadrant launch ${input.project}' first.`
6810
- );
6811
- }
6812
- const existing = await runtime.listSurfaces(captain.id);
6813
- const existingTitles = existing.filter((s) => s.title && isSideTitle(input.project, s.title)).map((s) => s.title);
6814
- if (input.name) {
6815
- const wantTitle = titleFor2(input.project, input.name);
6816
- if (existingTitles.includes(wantTitle)) {
6817
- throw new Error(
6818
- `Side session '${input.name}' already exists for ${input.project}.`
6819
- );
6820
- }
6821
- }
6822
- const name = input.name ?? nextAutoName2(existingTitles, input.project);
6823
- const spawnCwd = input.role === "debug" ? addWorktree({
6824
- repoRoot: proj.path,
6825
- worktreeDir: config.defaults.worktreeDir ?? ".worktrees",
6826
- project: input.project,
6827
- name,
6828
- base: resolveWorktreeBase(proj.path)
6829
- }) : proj.path;
6830
7284
  const agents = new CapabilityRegistry({
6831
7285
  claude: createClaudeDriver(),
6832
7286
  codex: createCodexDriver(),
@@ -6843,80 +7297,39 @@ async function runSideSpawn(input) {
6843
7297
  const promptFile = path21.join(
6844
7298
  TEMPLATES_DIR3,
6845
7299
  `side.${input.role}.${agent.templateSuffix}.md`
6846
- );
6847
- const direction = input.direction ?? "tab";
6848
- const title = titleFor2(input.project, name);
6849
- const pane = await runtime.newPane({ workspaceId: captain.id, direction, title });
6850
- const cliCommand = agent.buildCommand({
7300
+ );
7301
+ const agentCmdFactory = (spawnCwd) => agent.buildCommand({
6851
7302
  prompt: input.topic,
6852
7303
  workdir: spawnCwd,
6853
7304
  role: "side",
6854
- promptFile: fs17.existsSync(promptFile) ? promptFile : void 0,
7305
+ promptFile: fs18.existsSync(promptFile) ? promptFile : void 0,
6855
7306
  interactive: true,
6856
7307
  permissionMode: config.defaults.permissions?.crew ?? "auto",
6857
7308
  ...sideModel ? { model: sideModel } : {}
6858
7309
  });
6859
- await runtime.sendToPane(pane, `cd ${shellQuote2(spawnCwd)} && ${cliCommand}`);
6860
- const preLaunchScreen = await runtime.readPaneScreen(pane) ?? "";
6861
- const firstTurn = buildSideFirstTurn(
6862
- input.topic,
6863
- input.project,
6864
- input.role,
6865
- proj.spokeVault ?? "",
6866
- input.role === "debug" ? spawnCwd : void 0
6867
- );
6868
- await sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen);
6869
- return { ...pane, title };
7310
+ const sendFirstTurn = (pane, firstTurn, preLaunchScreen) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen);
7311
+ return runSideSpawn(input, config, { runtime, agentCmdFactory, sendFirstTurn });
6870
7312
  }
6871
- async function runSideSend(project, name, message) {
7313
+ async function runSideSend2(project, name, message) {
6872
7314
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6873
- const want = titleFor2(project, name);
6874
- const surfaces = await runtime.listSurfaces(workspaceId);
6875
- const pane = surfaces.find((s) => s.title === want) ?? null;
6876
- if (!pane) {
6877
- throw new Error(
6878
- `Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`
6879
- );
6880
- }
6881
- await runtime.sendToPane(pane, message);
7315
+ await runSideSend(runtime, workspaceId, project, name, message);
6882
7316
  }
6883
- async function runSideList(project) {
7317
+ async function runSideList2(project) {
6884
7318
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6885
- const surfaces = await runtime.listSurfaces(workspaceId);
6886
- return surfaces.filter((s) => s.title && isSideTitle(project, s.title)).map((s) => ({
6887
- name: nameFromTitle2(project, s.title),
6888
- surfaceId: s.surfaceId
6889
- }));
7319
+ return runSideList(runtime, workspaceId, project);
6890
7320
  }
6891
- async function runSideClose(project, name) {
7321
+ async function runSideClose2(project, name) {
6892
7322
  const config = loadConfig();
6893
7323
  const proj = config.projects[project];
6894
7324
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
6895
- const want = titleFor2(project, name);
6896
- const surfaces = await runtime.listSurfaces(workspaceId);
6897
- const pane = surfaces.find((s) => s.title === want) ?? null;
6898
- if (!pane) {
6899
- throw new Error(
6900
- `Side session '${name}' not found for ${project}. Run 'squadrant side list ${project}'.`
6901
- );
6902
- }
6903
- await runtime.closePane(pane);
6904
- if (proj) {
6905
- const wtPath = worktreePath(
6906
- proj.path,
6907
- config.defaults.worktreeDir ?? ".worktrees",
6908
- project,
6909
- name
6910
- );
6911
- if (fs17.existsSync(wtPath)) {
6912
- try {
6913
- removeWorktree(proj.path, wtPath);
6914
- } catch (e) {
6915
- process.stderr.write(`(worktree remove failed: ${e.message})
6916
- `);
6917
- }
6918
- }
6919
- }
7325
+ await runSideClose(
7326
+ runtime,
7327
+ workspaceId,
7328
+ project,
7329
+ name,
7330
+ proj?.path,
7331
+ config.defaults.worktreeDir ?? ".worktrees"
7332
+ );
6920
7333
  }
6921
7334
  var sideCommand = new Command10("side").description(
6922
7335
  "Spawn and manage side-sessions (research/debug) \u2014 fresh-context tabs off the daemon lifecycle"
@@ -6935,7 +7348,7 @@ sideCommand.command("spawn").description(
6935
7348
  filePath: opts.topicFile,
6936
7349
  label: "topic"
6937
7350
  });
6938
- const pane = await runSideSpawn({
7351
+ const pane = await runSideSpawn2({
6939
7352
  project,
6940
7353
  topic: resolvedTopic,
6941
7354
  role: opts.role,
@@ -6952,7 +7365,7 @@ sideCommand.command("spawn").description(
6952
7365
  );
6953
7366
  sideCommand.command("list").description("List live side-sessions for a project").argument("<project>", "Project name").action(async (project) => {
6954
7367
  try {
6955
- const sessions = await runSideList(project);
7368
+ const sessions = await runSideList2(project);
6956
7369
  if (sessions.length === 0) {
6957
7370
  console.log(chalk10.yellow(`No live side-sessions for ${project}.`));
6958
7371
  return;
@@ -6973,7 +7386,7 @@ sideCommand.command("send").description("Send a follow-up message to an existing
6973
7386
  filePath: opts.messageFile,
6974
7387
  label: "message"
6975
7388
  });
6976
- await runSideSend(project, name, resolvedMessage);
7389
+ await runSideSend2(project, name, resolvedMessage);
6977
7390
  console.log(chalk10.green(`\u2714 Sent to ${project}:${name}`));
6978
7391
  } catch (err) {
6979
7392
  console.error(chalk10.red(err.message));
@@ -6983,7 +7396,7 @@ sideCommand.command("send").description("Send a follow-up message to an existing
6983
7396
  );
6984
7397
  sideCommand.command("close").description("Close a side-session (closes its tab)").argument("<project>", "Project name").argument("<name>", "Session name").action(async (project, name) => {
6985
7398
  try {
6986
- await runSideClose(project, name);
7399
+ await runSideClose2(project, name);
6987
7400
  console.log(chalk10.green(`\u2714 Closed ${project}:${name}`));
6988
7401
  } catch (err) {
6989
7402
  console.error(chalk10.red(err.message));
@@ -6996,8 +7409,8 @@ init_dist();
6996
7409
  init_dist3();
6997
7410
  import { Command as Command11 } from "commander";
6998
7411
  import { execSync as execSync10 } from "child_process";
6999
- import { homedir as homedir13 } from "os";
7000
- import { join as join19 } from "path";
7412
+ import { homedir as homedir14 } from "os";
7413
+ import { join as join20 } from "path";
7001
7414
  import chalk12 from "chalk";
7002
7415
 
7003
7416
  // packages/web/dist/read-status.js
@@ -7136,7 +7549,7 @@ function renderDashboard(rows, opts) {
7136
7549
 
7137
7550
  // packages/web/dist/sync-hub.js
7138
7551
  init_dist();
7139
- import fs18 from "fs";
7552
+ import fs19 from "fs";
7140
7553
  import path22 from "path";
7141
7554
  function buildMirrorMarkdown(s) {
7142
7555
  const fenced = "```";
@@ -7163,8 +7576,8 @@ function buildMirrorMarkdown(s) {
7163
7576
  function syncHub(deps) {
7164
7577
  if (!deps.config.hubVault)
7165
7578
  return [];
7166
- const writeFile5 = deps.writeFile ?? ((p, c) => fs18.writeFileSync(p, c));
7167
- 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 }));
7168
7581
  const projectsDir = path22.join(resolveHome(deps.config.hubVault), "projects");
7169
7582
  mkdir5(projectsDir);
7170
7583
  const out = [];
@@ -7193,10 +7606,10 @@ function mergeSnapshot(daemon, external, now) {
7193
7606
  // packages/web/dist/probes.js
7194
7607
  init_dist();
7195
7608
  init_dist();
7196
- import { join as join18 } from "path";
7197
- import { homedir as homedir12 } from "os";
7609
+ import { join as join19 } from "path";
7610
+ import { homedir as homedir13 } from "os";
7198
7611
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
7199
- import { execFile as execFile2 } from "child_process";
7612
+ import { execFile as execFile3 } from "child_process";
7200
7613
  var DEFAULT_TIMEOUT_MS = 2e3;
7201
7614
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
7202
7615
  function withTimeout2(p, ms) {
@@ -7231,7 +7644,7 @@ function vaultProbe(run, dir) {
7231
7644
  return { state: "unknown", detail: "no vault configured" };
7232
7645
  if (!run.pathExists(dir))
7233
7646
  return { state: "gone", detail: "vault directory missing" };
7234
- if (!run.pathExists(join18(dir, ".obsidian")))
7647
+ if (!run.pathExists(join19(dir, ".obsidian")))
7235
7648
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
7236
7649
  return { state: "alive" };
7237
7650
  } catch {
@@ -7299,10 +7712,10 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
7299
7712
  const sessions = probeSessions(run);
7300
7713
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
7301
7714
  }
7302
- var SESSIONS_PATH = join18(homedir12(), ".config", "squadrant", "sessions.json");
7715
+ var SESSIONS_PATH = join19(homedir13(), ".config", "squadrant", "sessions.json");
7303
7716
  function onPath(cli) {
7304
7717
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
7305
- return dirs.some((d) => existsSync10(join18(d, cli)));
7718
+ return dirs.some((d) => existsSync10(join19(d, cli)));
7306
7719
  }
7307
7720
  function readSessionsHashes() {
7308
7721
  const raw = JSON.parse(readFileSync10(SESSIONS_PATH, "utf-8"));
@@ -7313,7 +7726,7 @@ function defaultProbeRunners() {
7313
7726
  return {
7314
7727
  probeCmuxBin: () => new Promise((resolve3) => {
7315
7728
  try {
7316
- execFile2(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
7729
+ execFile3(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
7317
7730
  } catch {
7318
7731
  resolve3(false);
7319
7732
  }
@@ -7972,7 +8385,7 @@ async function startWebServer(opts) {
7972
8385
 
7973
8386
  // packages/cli/src/commands/dashboard.ts
7974
8387
  init_dist();
7975
- var SOCK3 = join19(homedir13(), ".config", "squadrant", "squadrant.sock");
8388
+ var SOCK3 = join20(homedir14(), ".config", "squadrant", "squadrant.sock");
7976
8389
  function detectCurrentWorkspace2() {
7977
8390
  const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
7978
8391
  const match = out.match(/workspace:\d+/);
@@ -8055,12 +8468,11 @@ dashboardCommand.command("sync-hub").description("Mirror each spoke status.md in
8055
8468
  init_dist();
8056
8469
  init_dist4();
8057
8470
  init_dist3();
8058
- init_dist();
8059
- init_dist3();
8471
+ init_dist2();
8060
8472
  init_dist2();
8061
8473
  import { Command as Command12 } from "commander";
8062
8474
  import { execSync as execSync11 } from "child_process";
8063
- import fs19 from "fs";
8475
+ import fs20 from "fs";
8064
8476
  import path23 from "path";
8065
8477
  import os12 from "os";
8066
8478
  import chalk13 from "chalk";
@@ -8074,64 +8486,6 @@ function ensureCmuxReady() {
8074
8486
  console.log(chalk13.bold(" Run `squadrant launch` from inside a cmux workspace.\n"));
8075
8487
  process.exit(0);
8076
8488
  }
8077
- async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
8078
- const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
8079
- const settleMs = opts.settleMs ?? 2500;
8080
- const pollMs = opts.pollMs ?? 1e3;
8081
- const maxAttempts = opts.maxAttempts ?? 3;
8082
- const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
8083
- const read = async () => runtime.readScreen(refId).catch(() => "");
8084
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
8085
- const deadline = Date.now() + readyTimeoutMs;
8086
- let preSend = await read();
8087
- let state = classifyStartupSurface(preSend);
8088
- while (state === "loading" && Date.now() < deadline) {
8089
- await sleep2(pollMs);
8090
- preSend = await read();
8091
- state = classifyStartupSurface(preSend);
8092
- }
8093
- if (state === "working") return;
8094
- await runtime.send(refId, prompt).catch(() => {
8095
- });
8096
- if (state === "loading") return;
8097
- await sleep2(settleMs);
8098
- const after = await read();
8099
- if (after !== preSend) return;
8100
- }
8101
- }
8102
- async function launchWorkspace(runtime, name, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt) {
8103
- ensureCmuxReady();
8104
- const existing = await runtime.status(name);
8105
- if (existing && forceFresh) {
8106
- console.log(chalk13.yellow(` Closing stale workspace '${name}' for fresh start`));
8107
- await runtime.stop(existing.id);
8108
- } else if (existing) {
8109
- console.log(chalk13.yellow(` Workspace '${name}' already exists \u2014 switching to it`));
8110
- cmuxLocal(["select-workspace", "--workspace", existing.id]);
8111
- return;
8112
- }
8113
- let currentRef;
8114
- try {
8115
- const cur = cmuxLocal(["current-workspace"]);
8116
- currentRef = cur.match(/workspace:\d+/)?.[0];
8117
- } catch {
8118
- }
8119
- const ref = await runtime.spawn({
8120
- name,
8121
- workdir: cwd ?? process.cwd(),
8122
- command: agentCmd,
8123
- pinToTop
8124
- });
8125
- if (initialPrompt) {
8126
- void deliverStartupPrompt(runtime, ref.id, initialPrompt);
8127
- }
8128
- if (navigate) {
8129
- cmuxLocal(["select-workspace", "--workspace", ref.id]);
8130
- } else if (currentRef) {
8131
- cmuxLocal(["select-workspace", "--workspace", currentRef]);
8132
- }
8133
- console.log(chalk13.green(` \u2714 Workspace '${name}' created`));
8134
- }
8135
8489
  var launchCommand = new Command12("launch").description(
8136
8490
  "Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
8137
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) => {
@@ -8145,19 +8499,10 @@ var launchCommand = new Command12("launch").description(
8145
8499
  const registry = new CapabilityRegistry(drivers);
8146
8500
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
8147
8501
  async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
8148
- let forceFresh = !!opts.fresh;
8149
- if (!forceFresh) {
8150
- const auto = shouldStartFresh(workspaceName, role, { sessionsPath: SESSIONS_PATH2, templatesDir: TEMPLATES_DIR4 });
8151
- if (auto.fresh) {
8152
- console.log(chalk13.cyan(` \u21BB ${auto.reason}`));
8153
- forceFresh = true;
8154
- }
8155
- }
8502
+ ensureCmuxReady();
8156
8503
  const roleConfig = config.defaults.roles?.[role];
8157
8504
  const agentName = roleConfig?.agent || "claude";
8158
8505
  const model = roleConfig?.model || config.defaults.models?.[role];
8159
- const agentCmd = buildAgentCmd(agentName, registry, role, forceFresh, permissionMode, model, TEMPLATES_DIR4);
8160
- recordSession(workspaceName, role, { sessionsPath: SESSIONS_PATH2, templatesDir: TEMPLATES_DIR4 });
8161
8506
  let initialPrompt;
8162
8507
  if (role === "captain") {
8163
8508
  initialPrompt = "Run your startup checklist: use the squadrant:captain-ops skill, complete all startup steps, then report ready.";
@@ -8166,19 +8511,44 @@ var launchCommand = new Command12("launch").description(
8166
8511
  }
8167
8512
  const runtime = projectName ? runtimes.forProject(projectName, config) : runtimes.global(config);
8168
8513
  try {
8169
- 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
+ });
8170
8540
  } catch (err) {
8171
8541
  console.error(chalk13.red(` \u2718 Failed: ${err.message}`));
8172
8542
  }
8173
8543
  }
8174
8544
  if (opts.all) {
8175
8545
  const hubPath = resolveHome(config.hubVault);
8176
- fs19.mkdirSync(hubPath, { recursive: true });
8546
+ fs20.mkdirSync(hubPath, { recursive: true });
8177
8547
  console.log(chalk13.bold("\nLaunching all captain workspaces\n"));
8178
8548
  for (const [name, proj] of Object.entries(config.projects)) {
8179
8549
  const projPath = resolveHome(proj.path);
8180
8550
  const spokePath = resolveHome(proj.spokeVault);
8181
- if (!fs19.existsSync(spokePath)) {
8551
+ if (!fs20.existsSync(spokePath)) {
8182
8552
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
8183
8553
  await ensureSpokeLayout(spokeDriver);
8184
8554
  console.log(chalk13.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -8209,7 +8579,7 @@ var launchCommand = new Command12("launch").description(
8209
8579
  const proj = config.projects[project];
8210
8580
  const projPath = resolveHome(proj.path);
8211
8581
  const spokePath = resolveHome(proj.spokeVault);
8212
- if (!fs19.existsSync(spokePath)) {
8582
+ if (!fs20.existsSync(spokePath)) {
8213
8583
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(project, config);
8214
8584
  await ensureSpokeLayout(spokeDriver);
8215
8585
  console.log(chalk13.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -8344,7 +8714,7 @@ Shutting down captain workspace for '${project}'...
8344
8714
  // packages/cli/src/commands/feedback.ts
8345
8715
  init_dist();
8346
8716
  import { Command as Command14 } from "commander";
8347
- import fs20 from "fs";
8717
+ import fs21 from "fs";
8348
8718
  import os13 from "os";
8349
8719
  import path24 from "path";
8350
8720
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -8354,14 +8724,14 @@ var REPO_URL = "https://github.com/tu11aa/squadrant";
8354
8724
  function readPkgVersion() {
8355
8725
  try {
8356
8726
  const pkgPath = path24.join(path24.dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
8357
- return JSON.parse(fs20.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
8727
+ return JSON.parse(fs21.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
8358
8728
  } catch {
8359
8729
  return "unknown";
8360
8730
  }
8361
8731
  }
8362
8732
  function readMetrics(metricsPath) {
8363
8733
  try {
8364
- return JSON.parse(fs20.readFileSync(metricsPath, "utf-8"));
8734
+ return JSON.parse(fs21.readFileSync(metricsPath, "utf-8"));
8365
8735
  } catch {
8366
8736
  return {};
8367
8737
  }
@@ -8421,7 +8791,7 @@ init_dist();
8421
8791
  init_dist();
8422
8792
  init_dist3();
8423
8793
  import { Command as Command15 } from "commander";
8424
- import fs21 from "fs";
8794
+ import fs22 from "fs";
8425
8795
  import path25 from "path";
8426
8796
  import chalk16 from "chalk";
8427
8797
  import matter3 from "gray-matter";
@@ -8433,9 +8803,9 @@ async function getProjectStandup(name, project, dateStr, registry, config) {
8433
8803
  const spokeVault = resolveHome(project.spokeVault);
8434
8804
  const statusFile = path25.join(spokeVault, "status.md");
8435
8805
  let status = {};
8436
- if (fs21.existsSync(statusFile)) {
8806
+ if (fs22.existsSync(statusFile)) {
8437
8807
  try {
8438
- status = matter3(fs21.readFileSync(statusFile, "utf-8")).data;
8808
+ status = matter3(fs22.readFileSync(statusFile, "utf-8")).data;
8439
8809
  } catch {
8440
8810
  }
8441
8811
  }
@@ -8553,15 +8923,15 @@ init_dist();
8553
8923
  init_dist();
8554
8924
  init_dist3();
8555
8925
  import { Command as Command16 } from "commander";
8556
- import fs22 from "fs";
8926
+ import fs23 from "fs";
8557
8927
  import path26 from "path";
8558
8928
  import chalk17 from "chalk";
8559
8929
  import matter4 from "gray-matter";
8560
8930
  function readStatus(spokeVault) {
8561
8931
  const statusFile = path26.join(spokeVault, "status.md");
8562
- if (!fs22.existsSync(statusFile)) return {};
8932
+ if (!fs23.existsSync(statusFile)) return {};
8563
8933
  try {
8564
- return matter4(fs22.readFileSync(statusFile, "utf-8")).data;
8934
+ return matter4(fs23.readFileSync(statusFile, "utf-8")).data;
8565
8935
  } catch {
8566
8936
  return {};
8567
8937
  }
@@ -8981,7 +9351,7 @@ init_dist3();
8981
9351
  init_dist();
8982
9352
  import { Command as Command20 } from "commander";
8983
9353
  import chalk21 from "chalk";
8984
- import fs23 from "fs";
9354
+ import fs24 from "fs";
8985
9355
  import path27 from "path";
8986
9356
  import { fileURLToPath as fileURLToPath4 } from "url";
8987
9357
  function parseScope(v) {
@@ -8993,7 +9363,7 @@ function parseScope(v) {
8993
9363
  function findPackageRoot3() {
8994
9364
  let dir = path27.dirname(fileURLToPath4(import.meta.url));
8995
9365
  while (dir !== "/" && dir !== "") {
8996
- if (fs23.existsSync(path27.join(dir, "package.json"))) return dir;
9366
+ if (fs24.existsSync(path27.join(dir, "package.json"))) return dir;
8997
9367
  dir = path27.dirname(dir);
8998
9368
  }
8999
9369
  return process.cwd();
@@ -9187,13 +9557,14 @@ ${transcript.join("\n")}`
9187
9557
  init_dist();
9188
9558
  init_dist();
9189
9559
  init_dist();
9560
+ init_dist2();
9190
9561
  import { Command as Command22 } from "commander";
9191
- import fs24 from "fs";
9562
+ import fs25 from "fs";
9192
9563
  import { fileURLToPath as fileURLToPath5 } from "url";
9193
- import { dirname as dirname5, join as join20 } from "path";
9564
+ import { dirname as dirname5, join as join21 } from "path";
9194
9565
  import chalk22 from "chalk";
9195
9566
  function runConfigCheck(opts) {
9196
- const raw = JSON.parse(fs24.readFileSync(opts.configPath, "utf-8"));
9567
+ const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
9197
9568
  const def = getDefaultConfig();
9198
9569
  const items = detectDrift(raw, def);
9199
9570
  let working = raw;
@@ -9210,7 +9581,7 @@ function runConfigCheck(opts) {
9210
9581
  stamped = true;
9211
9582
  }
9212
9583
  if (opts.fix || opts.accept || stamped) {
9213
- fs24.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
9584
+ fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
9214
9585
  }
9215
9586
  return { items, applied, remaining, stamped };
9216
9587
  }
@@ -9281,7 +9652,7 @@ function printItems(items) {
9281
9652
  var configCommand = new Command22("config").description("Inspect and reconcile squadrant config");
9282
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) => {
9283
9654
  const pkgVersion = readPkgVersion2();
9284
- if (!fs24.existsSync(DEFAULT_CONFIG_PATH)) {
9655
+ if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
9285
9656
  console.log(chalk22.yellow("No config found \u2014 run `squadrant init` first."));
9286
9657
  return;
9287
9658
  }
@@ -9326,14 +9697,15 @@ configCommand.command("set").description("Write a config value by dotted key (e.
9326
9697
  }
9327
9698
  });
9328
9699
  function readPkgVersion2() {
9329
- const pkgPath = join20(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
9330
- 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;
9331
9702
  }
9332
9703
 
9333
9704
  // packages/cli/src/commands/heal.ts
9334
9705
  import { Command as Command23 } from "commander";
9335
9706
  import chalk23 from "chalk";
9336
9707
  init_dist2();
9708
+ init_dist2();
9337
9709
  function buildHealStatus(components) {
9338
9710
  if (components === null) {
9339
9711
  return { healthy: false, daemonUnreachable: true, components: [] };
@@ -9424,90 +9796,10 @@ var healCommand = new Command23("heal").description("Targeted, idempotent remedi
9424
9796
  // packages/cli/src/commands/group.ts
9425
9797
  init_dist();
9426
9798
  init_dist2();
9799
+ init_dist2();
9427
9800
  import { Command as Command24 } from "commander";
9428
9801
  import { execSync as execSync13 } from "child_process";
9429
- import { randomUUID as randomUUID4 } from "crypto";
9430
- import { homedir as homedir14 } from "os";
9431
- import { join as join21 } from "path";
9432
9802
  import chalk24 from "chalk";
9433
- var SOCK4 = join21(homedir14(), ".config", "squadrant", "squadrant.sock");
9434
- var WARMUP_TIMEOUT_MS = 12e4;
9435
- var WARMUP_POLL_MS = 1e3;
9436
- function resolveCurrentProject(config) {
9437
- const cwd = process.cwd();
9438
- for (const [name, proj] of Object.entries(config.projects)) {
9439
- const resolvedPath = resolveHome(proj.path);
9440
- if (cwd.startsWith(resolvedPath)) return name;
9441
- }
9442
- return null;
9443
- }
9444
- async function isCaptainAlive(project) {
9445
- try {
9446
- const health = await sendRequest(SOCK4, { kind: "health", project }, 5e3);
9447
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
9448
- return captain != null && captain.state !== "gone" && captain.state !== "unknown";
9449
- } catch {
9450
- return false;
9451
- }
9452
- }
9453
- async function waitForWarmup(project, timeoutMs = WARMUP_TIMEOUT_MS, pollMs = WARMUP_POLL_MS) {
9454
- const deadline = Date.now() + timeoutMs;
9455
- while (Date.now() < deadline) {
9456
- if (await isCaptainAlive(project)) return true;
9457
- await new Promise((r) => setTimeout(r, pollMs));
9458
- }
9459
- return false;
9460
- }
9461
- async function runGroupDispatch(opts) {
9462
- const config = loadConfig();
9463
- const fromCfg = config.projects[opts.fromProject];
9464
- const toCfg = config.projects[opts.toProject];
9465
- if (!toCfg) {
9466
- throw new Error(`target project '${opts.toProject}' not found in config`);
9467
- }
9468
- if (!fromCfg.group || !toCfg.group || fromCfg.group !== toCfg.group) {
9469
- throw new Error(
9470
- `cannot dispatch: '${opts.toProject}' (group: ${toCfg.group ?? "none"}) is not in the same group as '${opts.fromProject}' (group: ${fromCfg.group ?? "none"})`
9471
- );
9472
- }
9473
- if (toCfg.acceptDelegations === false) {
9474
- throw new Error(
9475
- `cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`
9476
- );
9477
- }
9478
- const alive = await isCaptainAlive(opts.toProject);
9479
- if (!alive) {
9480
- try {
9481
- execSync13(`squadrant launch ${opts.toProject}`, { stdio: "ignore", timeout: 15e3 });
9482
- } catch {
9483
- throw new Error(`failed to launch captain for '${opts.toProject}' \u2014 is squadrant installed?`);
9484
- }
9485
- const warmed = await waitForWarmup(opts.toProject, opts.warmupTimeoutMs, opts.warmupPollMs);
9486
- if (!warmed) {
9487
- throw new Error(
9488
- `dispatch to '${opts.toProject}' timed out waiting for captain warmup (>${(opts.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS) / 1e3}s)`
9489
- );
9490
- }
9491
- }
9492
- const now = Date.now();
9493
- const attemptId = randomUUID4();
9494
- const record = {
9495
- id: randomUUID4(),
9496
- project: opts.toProject,
9497
- originProject: opts.fromProject,
9498
- provider: opts.provider ?? "claude",
9499
- mode: opts.mode ?? "headless",
9500
- state: "submitted",
9501
- task: opts.task,
9502
- createdAt: now,
9503
- lastHeartbeat: now,
9504
- lastEvent: "dispatch",
9505
- heartbeatBudgetMs: 3e5,
9506
- attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
9507
- };
9508
- const result = await sendRequest(SOCK4, { kind: "dispatch", record });
9509
- return result;
9510
- }
9511
9803
  var groupCommand = new Command24("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
9512
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) => {
9513
9805
  const fromProject = resolveCurrentProject(loadConfig());
@@ -9516,13 +9808,20 @@ var groupCommand = new Command24("group").description("Cross-project intra-group
9516
9808
  process.exit(1);
9517
9809
  }
9518
9810
  try {
9519
- const result = await runGroupDispatch({
9811
+ const result = await dispatchToSibling({
9520
9812
  fromProject,
9521
9813
  toProject,
9522
9814
  task,
9523
9815
  provider: opts.provider,
9524
9816
  mode: opts.mode,
9525
- 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
+ }
9526
9825
  });
9527
9826
  console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
9528
9827
  console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
@@ -9592,7 +9891,7 @@ var cmuxCommand = new Command25("cmux").description("cmux integration helpers").
9592
9891
 
9593
9892
  // packages/cli/src/commands/effort.ts
9594
9893
  init_dist();
9595
- import fs25 from "fs";
9894
+ import fs26 from "fs";
9596
9895
  import path28 from "path";
9597
9896
  import { Command as Command26 } from "commander";
9598
9897
  import chalk26 from "chalk";
@@ -9620,7 +9919,7 @@ function runEffortSet(value, configPath = DEFAULT_CONFIG_PATH) {
9620
9919
  }
9621
9920
  function canonical(p) {
9622
9921
  try {
9623
- return fs25.realpathSync(p);
9922
+ return fs26.realpathSync(p);
9624
9923
  } catch {
9625
9924
  return path28.resolve(p);
9626
9925
  }
@@ -9676,59 +9975,6 @@ import chalk27 from "chalk";
9676
9975
  function defaultStateRoot() {
9677
9976
  return join22(dirname6(DEFAULT_CONFIG_PATH), "state");
9678
9977
  }
9679
- function runTelegramStatus(opts) {
9680
- const tg = opts.config.telegram;
9681
- const env = opts.env ?? process.env;
9682
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
9683
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
9684
- const sep2 = key.indexOf("::");
9685
- return { project: key.slice(0, sep2), scope: key.slice(sep2 + 2), topicId };
9686
- });
9687
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
9688
- }
9689
- async function runTelegramSend(opts) {
9690
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9691
- if (topicId === void 0) {
9692
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
9693
- }
9694
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
9695
- return { chatId: opts.cfg.supergroupId, topicId };
9696
- }
9697
- function runTelegramNotifySet(opts) {
9698
- setNotify(opts.stateRoot, opts.project, opts.active);
9699
- }
9700
- function runTelegramNotifyPref(args) {
9701
- const { project, dimension, value, root } = args;
9702
- if (dimension === "crew") {
9703
- if (!["all", "alert_only", "done_only", "none"].includes(value))
9704
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
9705
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
9706
- return { ok: true };
9707
- }
9708
- if (value !== "on" && value !== "off") return { ok: false, message: "cap must be on|off" };
9709
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
9710
- return { ok: true };
9711
- }
9712
- function capAllowed(project, globalNotify, root) {
9713
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
9714
- }
9715
- function runTelegramNotifyStatus(opts) {
9716
- const s = loadState(opts.stateRoot);
9717
- const projects = /* @__PURE__ */ new Set();
9718
- for (const key of Object.keys(s.topics)) {
9719
- const sep2 = key.indexOf("::");
9720
- projects.add(sep2 === -1 ? key : key.slice(0, sep2));
9721
- }
9722
- for (const p of Object.keys(s.notify)) projects.add(p);
9723
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
9724
- }
9725
- async function runTelegramLink(opts) {
9726
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9727
- if (existing !== void 0) return { topicId: existing, created: false };
9728
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
9729
- setTopic(opts.stateRoot, opts.project, topicId);
9730
- return { topicId, created: true };
9731
- }
9732
9978
  async function questionMasked() {
9733
9979
  return new Promise((resolve3) => {
9734
9980
  emitKeypressEvents(process.stdin);
@@ -9772,44 +10018,6 @@ async function questionYesNo(prompt) {
9772
10018
  });
9773
10019
  });
9774
10020
  }
9775
- function confirmationText(project, before, after, dim) {
9776
- if (dim === "active") return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
9777
- if (dim === "cap") return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
9778
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
9779
- }
9780
- async function runNotifyConfirmation(opts) {
9781
- const { quieter, dim } = isQuieter(opts.before, opts.after);
9782
- if (!quieter || dim === null) return false;
9783
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
9784
- if (topicId === void 0) return false;
9785
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
9786
- try {
9787
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
9788
- return true;
9789
- } catch {
9790
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
9791
- return false;
9792
- }
9793
- }
9794
- function resolveSetupToken(existingToken, opts) {
9795
- if (opts.resetToken || !existingToken) return "prompt";
9796
- return "try-reuse";
9797
- }
9798
- function resolveSetupUserId(flagUserId, detectedUserId, stateRoot) {
9799
- return flagUserId ?? detectedUserId ?? loadState(stateRoot).lastUserId;
9800
- }
9801
- async function runRegisterCommands(opts) {
9802
- await opts.client.setMyCommands(BOT_COMMANDS);
9803
- }
9804
- function runTelegramPostSetup(opts) {
9805
- const doRestart = opts.doRestart ?? restartDaemonIfRunning;
9806
- const outcome = doRestart({ reason: "telegram config" });
9807
- if (outcome === "skipped-not-running") {
9808
- console.log(chalk27.dim("(daemon not running \u2014 change applies on next start)"));
9809
- } else if (outcome === "skipped-opt-out") {
9810
- console.log(chalk27.dim("(run 'squadrant heal daemon' to apply)"));
9811
- }
9812
- }
9813
10021
  var telegramCommand = new Command27("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
9814
10022
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
9815
10023
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });