squadrant 0.14.1 → 0.14.3

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
@@ -1601,11 +1601,11 @@ var init_session_freshness = __esm({
1601
1601
  });
1602
1602
 
1603
1603
  // packages/core/dist/crew-protocol.js
1604
- function isTurnAccepted(preSendScreen, afterScreen, config) {
1605
- if (config?.splashMarker) {
1606
- return !afterScreen.includes(config.splashMarker);
1607
- }
1608
- return afterScreen !== preSendScreen;
1604
+ function normalizeForSplashMatch(text) {
1605
+ return text.toLowerCase().replace(/…/g, "...").replace(/\s+/g, " ").trim();
1606
+ }
1607
+ function screenHasSplashMarker(screen, marker) {
1608
+ return normalizeForSplashMatch(screen).includes(normalizeForSplashMatch(marker));
1609
1609
  }
1610
1610
  function buildCompletionProtocol(taskId, project) {
1611
1611
  return [
@@ -2186,15 +2186,16 @@ async function dispatchToSibling(opts) {
2186
2186
  if (!toCfg) {
2187
2187
  throw new Error(`target project '${opts.toProject}' not found in config`);
2188
2188
  }
2189
- if (!fromCfg.group || !toCfg.group || fromCfg.group !== toCfg.group) {
2190
- throw new Error(`cannot dispatch: '${opts.toProject}' (group: ${toCfg.group ?? "none"}) is not in the same group as '${opts.fromProject}' (group: ${fromCfg.group ?? "none"})`);
2191
- }
2189
+ const sameGroup = !!fromCfg?.group && !!toCfg.group && fromCfg.group === toCfg.group;
2192
2190
  if (toCfg.acceptDelegations === false) {
2193
2191
  throw new Error(`cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`);
2194
2192
  }
2195
2193
  const sockPath = opts.sockPath ?? DEFAULT_SOCK_PATH2;
2196
2194
  const alive = await isCaptainAlive(opts.toProject, sockPath);
2197
2195
  if (!alive) {
2196
+ if (!sameGroup) {
2197
+ throw new Error(`cannot dispatch to '${opts.toProject}': captain is not running and cross-group dispatch does not auto-boot it. Use 'squadrant ping ${opts.toProject} "<msg>"' or start it manually with 'squadrant launch ${opts.toProject}', then retry.`);
2198
+ }
2198
2199
  if (opts.bootCaptain) {
2199
2200
  await opts.bootCaptain(opts.toProject);
2200
2201
  }
@@ -2643,10 +2644,16 @@ ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
2643
2644
  const opencodeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
2644
2645
 
2645
2646
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen, {
2646
- // #235: confirm-on-delivery — sendFirstTurnWhenReady polls until "Ask
2647
- // anything…" leaves the screen, re-sending every ~3s to cover slow boots
2647
+ // #235: confirm-on-delivery — sendFirstTurnWhenReady polls until the idle
2648
+ // splash leaves the screen, re-sending every ~3s to cover slow boots
2648
2649
  // without duplicating the task. See crew-pane.ts SPLASH_MAX_CHECKS/EVERY_N.
2649
- splashMarker: "Ask anything\u2026"
2650
+ // #499: match a stable substring ("ask anything", case/whitespace/ellipsis
2651
+ // -insensitive via screenHasSplashMarker) rather than the exact wording —
2652
+ // opencode's real placeholder rotates through example prompts and uses
2653
+ // three ASCII dots ("Ask anything...") or a longer command hint ("Ask
2654
+ // anything, / for commands, @ for context..."), never the literal
2655
+ // "Ask anything…" (U+2026) this used to hardcode, which never matched.
2656
+ splashMarker: "Ask anything"
2650
2657
  });
2651
2658
  if (!opencodeResult.delivered) {
2652
2659
  process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
@@ -4146,6 +4153,21 @@ async function confirmedSendToPane(runtime, pane, message) {
4146
4153
  }
4147
4154
  return { delivered: false };
4148
4155
  }
4156
+ async function resendCrewFirstTurn(runtime, captainName, project, name, message) {
4157
+ const captain = await runtime.status(captainName);
4158
+ if (!captain)
4159
+ return { delivered: false };
4160
+ const surfaces = await runtime.listSurfaces(captain.id);
4161
+ const want = titleFor(project, name);
4162
+ const pane = surfaces.find((s) => s.title === want);
4163
+ if (!pane)
4164
+ return { delivered: false };
4165
+ const screen = await runtime.readPaneScreen(pane) ?? "";
4166
+ if (!hasCCInputBox(screen) || classifyStartupSurface(screen) !== "idle") {
4167
+ return { delivered: false };
4168
+ }
4169
+ return confirmedSendToPane(runtime, pane, message);
4170
+ }
4149
4171
  async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acceptanceConfig) {
4150
4172
  await new Promise((r) => setTimeout(r, SEND_FIRST_TURN_FLOOR_MS));
4151
4173
  const maxPolls = Math.floor((SEND_FIRST_TURN_TIMEOUT_MS - SEND_FIRST_TURN_FLOOR_MS) / POLL_INTERVAL_MS);
@@ -4153,7 +4175,7 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
4153
4175
  let stable = false;
4154
4176
  for (let i = 0; i < maxPolls && !stable; i++) {
4155
4177
  const screen = await runtime.readPaneScreen(pane) ?? "";
4156
- const ready = acceptanceConfig?.splashMarker ? true : hasCCInputBox(screen) && classifyStartupSurface(screen) === "idle";
4178
+ const ready = acceptanceConfig?.splashMarker ? screenHasSplashMarker(screen, acceptanceConfig.splashMarker) : hasCCInputBox(screen) && classifyStartupSurface(screen) === "idle";
4157
4179
  if (screen.length > 0 && screen === previousScreen && screen !== preLaunchScreen && ready) {
4158
4180
  stable = true;
4159
4181
  } else {
@@ -4163,11 +4185,14 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
4163
4185
  }
4164
4186
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
4165
4187
  if (acceptanceConfig?.splashMarker) {
4188
+ let sawSplash = screenHasSplashMarker(preSendScreen, acceptanceConfig.splashMarker);
4166
4189
  await runtime.sendToPane(pane, task);
4167
4190
  for (let check2 = 0; check2 < SPLASH_MAX_CHECKS; check2++) {
4168
4191
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
4169
4192
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
4170
- if (isTurnAccepted(preSendScreen, afterScreen, acceptanceConfig)) {
4193
+ if (screenHasSplashMarker(afterScreen, acceptanceConfig.splashMarker)) {
4194
+ sawSplash = true;
4195
+ } else if (sawSplash) {
4171
4196
  return { delivered: true };
4172
4197
  }
4173
4198
  if ((check2 + 1) % SPLASH_RESEND_EVERY_N === 0 && check2 < SPLASH_MAX_CHECKS - 1) {
@@ -4251,6 +4276,7 @@ __export(dist_exports, {
4251
4276
  isInsideCmux: () => isInsideCmux,
4252
4277
  listProjectCrews: () => listProjectCrews,
4253
4278
  mapSubToLifecycle: () => mapSubToLifecycle,
4279
+ resendCrewFirstTurn: () => resendCrewFirstTurn,
4254
4280
  resolveCaptainWorkspace: () => resolveCaptainWorkspace,
4255
4281
  sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
4256
4282
  });
@@ -5713,7 +5739,7 @@ var init_sse_bridge = __esm({
5713
5739
  const fetchImpl = this.deps.fetchImpl ?? fetch;
5714
5740
  const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5715
5741
  const reconnectMs = this.deps.reconnectMs ?? 500;
5716
- const maxBoot = this.deps.maxBootAttempts ?? 60;
5742
+ const maxBoot = this.deps.maxBootAttempts ?? 240;
5717
5743
  const url = `http://127.0.0.1:${port}/event`;
5718
5744
  let booted = false;
5719
5745
  let bootAttempts = 0;
@@ -6335,7 +6361,7 @@ var init_dist4 = __esm({
6335
6361
  // packages/cli/src/index.ts
6336
6362
  init_dist();
6337
6363
  init_dist2();
6338
- import { Command as Command29 } from "commander";
6364
+ import { Command as Command31 } from "commander";
6339
6365
  import { readFileSync as readFileSync13, existsSync as existsSync12, writeFileSync as writeFileSync10 } from "fs";
6340
6366
  import { fileURLToPath as fileURLToPath6 } from "url";
6341
6367
  import { dirname as dirname7, join as join26 } from "path";
@@ -8571,7 +8597,11 @@ function collect(snap) {
8571
8597
  env.vaults.hub.state,
8572
8598
  ...env.vaults.spokes.map((s) => s.state),
8573
8599
  env.config.parseable.state,
8574
- env.config.sessions.state,
8600
+ // Template-hash drift (env.config.sessions.state) is demoted to soft/info:
8601
+ // projects registered but idle/never-launched legitimately sit on older
8602
+ // template versions, so drift alone is not actionable trouble. Its own
8603
+ // status still renders in the Environment tab; it just no longer rolls up
8604
+ // into envT/overall/masterClass.
8575
8605
  ...env.config.projectPaths.map((p) => p.state)
8576
8606
  ];
8577
8607
  const daemonStates = [];
@@ -8591,7 +8621,8 @@ function collect(snap) {
8591
8621
  undelivered++;
8592
8622
  }
8593
8623
  for (const p of snap.daemon.tier2.projects) {
8594
- if (p.delivery.behind > 0)
8624
+ const captain = snap.daemon.tier1.find((c) => c.kind === "captain" && c.project === p.project);
8625
+ if (captain?.state === "alive" && p.delivery.behind > 0)
8595
8626
  projStates.push("stale");
8596
8627
  if (p.store.corruptCount > 0)
8597
8628
  projStates.push("gone");
@@ -10642,49 +10673,85 @@ var healCommand = new Command23("heal").description("Targeted, idempotent remedi
10642
10673
  );
10643
10674
 
10644
10675
  // packages/cli/src/commands/group.ts
10676
+ import { Command as Command25 } from "commander";
10677
+ import chalk25 from "chalk";
10678
+
10679
+ // packages/cli/src/commands/dispatch.ts
10645
10680
  init_dist();
10646
10681
  init_dist2();
10647
- init_dist2();
10648
10682
  import { Command as Command24 } from "commander";
10649
10683
  import { execSync as execSync13 } from "child_process";
10650
10684
  import chalk24 from "chalk";
10651
- var groupCommand = new Command24("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
10652
- 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) => {
10653
- const fromProject = resolveCurrentProject(loadConfig());
10654
- if (!fromProject) {
10655
- console.error(chalk24.red("Could not determine current project from cwd. Run from inside a registered project directory."));
10656
- process.exit(1);
10657
- }
10658
- try {
10659
- const result = await dispatchToSibling({
10660
- fromProject,
10661
- toProject,
10662
- task,
10663
- provider: opts.provider,
10664
- mode: opts.mode,
10665
- warmupTimeoutMs: opts.warmupTimeout,
10666
- bootCaptain: async (project) => {
10667
- try {
10668
- execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
10669
- } catch {
10670
- throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
10671
- }
10672
- }
10673
- });
10674
- console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
10675
- console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
10676
- console.log(chalk24.dim(" You will be notified when the task settles (done/blocked/failed)."));
10677
- } catch (e) {
10678
- console.error(chalk24.red(`\u2718 ${e.message}`));
10679
- process.exit(1);
10685
+ async function runDispatch(toProject, task, opts) {
10686
+ const fromProject = resolveCurrentProject(loadConfig());
10687
+ if (!fromProject) {
10688
+ throw new Error("Could not determine current project from cwd. Run from inside a registered project directory.");
10689
+ }
10690
+ return dispatchToSibling({
10691
+ fromProject,
10692
+ toProject,
10693
+ task,
10694
+ provider: opts.provider,
10695
+ mode: opts.mode,
10696
+ warmupTimeoutMs: opts.warmupTimeout,
10697
+ bootCaptain: async (project) => {
10698
+ try {
10699
+ execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
10700
+ } catch {
10701
+ throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
10702
+ }
10680
10703
  }
10704
+ });
10705
+ }
10706
+ async function dispatchAction(toProject, task, opts) {
10707
+ try {
10708
+ const result = await runDispatch(toProject, task, opts);
10709
+ console.log(chalk24.green(`\u2714 Dispatched to '${toProject}' (task ${result.id.slice(0, 8)})`));
10710
+ console.log(chalk24.dim(` originProject: ${result.originProject ?? "none"}`));
10711
+ console.log(chalk24.dim(" You will be notified when the task settles (done/blocked/failed)."));
10712
+ } catch (e) {
10713
+ console.error(chalk24.red(`\u2718 ${e.message}`));
10714
+ process.exit(1);
10715
+ }
10716
+ }
10717
+ var dispatchCommand = new Command24("dispatch").description("Dispatch a task to any registered project (tracked, reports back on settle)").argument("<project>", "Target project name").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 to boot (same-group only; default: 120)", (v) => parseInt(v, 10) * 1e3).action(dispatchAction);
10718
+
10719
+ // packages/cli/src/commands/group.ts
10720
+ init_dist2();
10721
+ var groupCommand = new Command25("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
10722
+ new Command25("dispatch").description("[DEPRECATED \u2014 use 'squadrant dispatch'] Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name").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 to boot (default: 120)", (v) => parseInt(v, 10) * 1e3).action(async (toProject, task, opts) => {
10723
+ console.error(chalk25.yellow(
10724
+ `\u26A0 'squadrant group dispatch' is deprecated \u2014 use 'squadrant dispatch <project> "<task>"' instead.`
10725
+ ));
10726
+ await dispatchAction(toProject, task, opts);
10681
10727
  })
10682
10728
  );
10683
10729
 
10730
+ // packages/cli/src/commands/ping.ts
10731
+ init_dist();
10732
+ import { Command as Command26 } from "commander";
10733
+ import chalk26 from "chalk";
10734
+ async function runPing(project, message) {
10735
+ const config = loadConfig();
10736
+ const registry = buildRegistry();
10737
+ const resolved = resolveTarget(registry, config, project, false);
10738
+ const ref = await needRef(resolved);
10739
+ await resolved.driver.send(ref, message);
10740
+ }
10741
+ var pingCommand = new Command26("ping").description("Fire-and-forget: deliver a message into a registered project's captain pane (no tracked task, no report-back)").argument("<project>", "Target project name (must be registered)").argument("<message>", "Message to deliver").action(async (project, message) => {
10742
+ try {
10743
+ await runPing(project, message);
10744
+ console.log(chalk26.green(`\u2714 Pinged '${project}'`));
10745
+ } catch (err) {
10746
+ console.error(chalk26.red(err.message));
10747
+ process.exit(1);
10748
+ }
10749
+ });
10750
+
10684
10751
  // packages/cli/src/commands/cmux.ts
10685
10752
  init_dist();
10686
- import { Command as Command25 } from "commander";
10687
- import chalk25 from "chalk";
10753
+ import { Command as Command27 } from "commander";
10754
+ import chalk27 from "chalk";
10688
10755
  async function runCmuxAutoconfig(opts) {
10689
10756
  const { json, stdout, stderr } = opts;
10690
10757
  let r;
@@ -10703,19 +10770,19 @@ async function runCmuxAutoconfig(opts) {
10703
10770
  stdout.write(`wrote cmux automation config \u2192 ${r.configPath}
10704
10771
  `);
10705
10772
  } else {
10706
- stdout.write(chalk25.dim(`cmux automation config already in place (${r.configPath})
10773
+ stdout.write(chalk27.dim(`cmux automation config already in place (${r.configPath})
10707
10774
  `));
10708
10775
  }
10709
10776
  if (r.verdict === "reachable") {
10710
- stdout.write(chalk25.green("\u2714 daemon-direct delivery is reachable \u2014 cmux control socket accepts the daemon\n"));
10777
+ stdout.write(chalk27.green("\u2714 daemon-direct delivery is reachable \u2014 cmux control socket accepts the daemon\n"));
10711
10778
  return 0;
10712
10779
  }
10713
10780
  if (r.needsRestart) {
10714
10781
  stdout.write(
10715
- chalk25.yellow("\u26A0 cmux is still on the old socket mode \u2014 restart cmux to enable daemon-direct delivery.\n")
10782
+ chalk27.yellow("\u26A0 cmux is still on the old socket mode \u2014 restart cmux to enable daemon-direct delivery.\n")
10716
10783
  );
10717
10784
  if (r.promptedThisRun) {
10718
- stdout.write(chalk25.dim(" (one-time prompt \u2014 you won't be nagged again)\n"));
10785
+ stdout.write(chalk27.dim(" (one-time prompt \u2014 you won't be nagged again)\n"));
10719
10786
  }
10720
10787
  return 2;
10721
10788
  }
@@ -10724,8 +10791,8 @@ async function runCmuxAutoconfig(opts) {
10724
10791
  );
10725
10792
  return 1;
10726
10793
  }
10727
- var cmuxCommand = new Command25("cmux").description("cmux integration helpers").addCommand(
10728
- new Command25("autoconfig").description(
10794
+ var cmuxCommand = new Command27("cmux").description("cmux integration helpers").addCommand(
10795
+ new Command27("autoconfig").description(
10729
10796
  "Write the cmux automation socket config and probe whether daemon-direct\ndelivery is reachable. Idempotent; prompts once if a cmux restart is needed."
10730
10797
  ).option("--json", "output machine-readable JSON (exit 0=reachable, 1=unknown, 2=restart-needed)").action(async (opts) => {
10731
10798
  const code = await runCmuxAutoconfig({
@@ -10741,8 +10808,8 @@ var cmuxCommand = new Command25("cmux").description("cmux integration helpers").
10741
10808
  init_dist();
10742
10809
  import fs26 from "fs";
10743
10810
  import path28 from "path";
10744
- import { Command as Command26 } from "commander";
10745
- import chalk26 from "chalk";
10811
+ import { Command as Command28 } from "commander";
10812
+ import chalk28 from "chalk";
10746
10813
  var VALID_EFFORTS = ["max", "balance", "low"];
10747
10814
  var EFFORT_MEANING = {
10748
10815
  max: "tokens are plentiful \u2014 bias crew spawns toward claude/opus",
@@ -10786,22 +10853,22 @@ async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(
10786
10853
  }
10787
10854
  }
10788
10855
  }
10789
- var effortCommand = new Command26("effort").description("Get or set the global crew tokenomics dial (max | balance | low)").argument("[value]", "effort level to set: max | balance | low").action(async (value) => {
10856
+ var effortCommand = new Command28("effort").description("Get or set the global crew tokenomics dial (max | balance | low)").argument("[value]", "effort level to set: max | balance | low").action(async (value) => {
10790
10857
  if (value === void 0) {
10791
10858
  const { effort: effort2, description } = runEffortGet();
10792
- console.log(chalk26.bold("Current effort:"), chalk26.cyan(effort2));
10793
- console.log(chalk26.dim(EFFORT_MEANING[effort2]));
10859
+ console.log(chalk28.bold("Current effort:"), chalk28.cyan(effort2));
10860
+ console.log(chalk28.dim(EFFORT_MEANING[effort2]));
10794
10861
  return;
10795
10862
  }
10796
10863
  try {
10797
10864
  runEffortSet(value);
10798
10865
  } catch (err) {
10799
- console.error(chalk26.red(err.message));
10866
+ console.error(chalk28.red(err.message));
10800
10867
  process.exit(1);
10801
10868
  }
10802
10869
  const effort = value;
10803
- console.log(chalk26.green(`\u2714 effort \u2192 ${effort}`));
10804
- console.log(chalk26.dim(EFFORT_MEANING[effort]));
10870
+ console.log(chalk28.green(`\u2714 effort \u2192 ${effort}`));
10871
+ console.log(chalk28.dim(EFFORT_MEANING[effort]));
10805
10872
  try {
10806
10873
  const { createCmuxDriver: createCmuxDriver2, RuntimeRegistry: RuntimeRegistry2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
10807
10874
  const config = loadConfig();
@@ -10809,7 +10876,7 @@ var effortCommand = new Command26("effort").description("Get or set the global c
10809
10876
  const driver = registry.global(config);
10810
10877
  await notifyCaptainsOfEffort(effort, config, driver);
10811
10878
  } catch {
10812
- console.log(chalk26.dim("(no running captain detected \u2014 change applies on next launch)"));
10879
+ console.log(chalk28.dim("(no running captain detected \u2014 change applies on next launch)"));
10813
10880
  }
10814
10881
  });
10815
10882
 
@@ -10818,8 +10885,8 @@ init_dist();
10818
10885
  init_dist2();
10819
10886
  import { join as join24, dirname as dirname6 } from "path";
10820
10887
  import { emitKeypressEvents } from "readline";
10821
- import { Command as Command27 } from "commander";
10822
- import chalk27 from "chalk";
10888
+ import { Command as Command29 } from "commander";
10889
+ import chalk29 from "chalk";
10823
10890
  function defaultStateRoot() {
10824
10891
  return join24(dirname6(DEFAULT_CONFIG_PATH), "state");
10825
10892
  }
@@ -10866,11 +10933,11 @@ async function questionYesNo(prompt) {
10866
10933
  });
10867
10934
  });
10868
10935
  }
10869
- var telegramCommand = new Command27("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
10936
+ var telegramCommand = new Command29("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
10870
10937
  telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
10871
10938
  const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
10872
- console.log(`token: ${tokenSet ? chalk27.green("set") : chalk27.yellow("unset")}`);
10873
- console.log(`supergroup: ${supergroupId ?? chalk27.yellow("unset")}`);
10939
+ console.log(`token: ${tokenSet ? chalk29.green("set") : chalk29.yellow("unset")}`);
10940
+ console.log(`supergroup: ${supergroupId ?? chalk29.yellow("unset")}`);
10874
10941
  if (links.length === 0) {
10875
10942
  console.log("no projects linked");
10876
10943
  return;
@@ -10880,32 +10947,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
10880
10947
  telegramCommand.command("link").argument("<project>", "project to bind to a Telegram topic").description("Create (or reuse) a forum topic for a project and bind it").action(async (project) => {
10881
10948
  const cfg = loadConfig().telegram;
10882
10949
  if (!cfg) {
10883
- console.error(chalk27.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
10950
+ console.error(chalk29.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
10884
10951
  process.exit(1);
10885
10952
  }
10886
10953
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
10887
10954
  if (!token) {
10888
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
10955
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
10889
10956
  process.exit(1);
10890
10957
  }
10891
10958
  const client = createTelegramClient({ token });
10892
10959
  const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
10893
- console.log(chalk27.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
10960
+ console.log(chalk29.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
10894
10961
  });
10895
10962
  telegramCommand.command("setup").description("Interactive wizard \u2014 bot token, validate, auto-detect supergroup, write config").option("--reset-token", "force re-entry of the bot token even if one already exists").option("--redetect", "force group re-detection even when a supergroup is already configured").option("--user-id <id>", "allowlist user-id \u2014 enables remote control on a re-run without getUpdates detection", (v) => parseInt(v, 10)).action(async (opts) => {
10896
10963
  if (!process.stdin.isTTY) {
10897
- console.error(chalk27.red("setup requires a TTY \u2014 pipe input is not supported"));
10964
+ console.error(chalk29.red("setup requires a TTY \u2014 pipe input is not supported"));
10898
10965
  process.exit(1);
10899
10966
  }
10900
10967
  console.log();
10901
- console.log(chalk27.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
10968
+ console.log(chalk29.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
10902
10969
  console.log();
10903
10970
  console.log("Before you start you need:");
10904
10971
  console.log(" 1. A bot token from @BotFather (send /newbot)");
10905
10972
  console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
10906
10973
  console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
10907
10974
  console.log();
10908
- console.log(chalk27.bold("Step 1/3 \u2014 Bot token"));
10975
+ console.log(chalk29.bold("Step 1/3 \u2014 Bot token"));
10909
10976
  const existingCfg = loadConfig().telegram;
10910
10977
  const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
10911
10978
  const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
@@ -10917,67 +10984,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
10917
10984
  try {
10918
10985
  botUser = await client.getMe();
10919
10986
  token = existingToken;
10920
- console.log(chalk27.green(`Using existing bot token (@${botUser.username})`));
10987
+ console.log(chalk29.green(`Using existing bot token (@${botUser.username})`));
10921
10988
  console.log();
10922
10989
  } catch {
10923
- console.log(chalk27.yellow("Existing token is invalid \u2014 please enter a new one."));
10990
+ console.log(chalk29.yellow("Existing token is invalid \u2014 please enter a new one."));
10924
10991
  console.log("Paste your bot token then press Enter (input is hidden):");
10925
10992
  token = await questionMasked();
10926
10993
  if (!token) {
10927
- console.error(chalk27.red("token required"));
10994
+ console.error(chalk29.red("token required"));
10928
10995
  process.exit(1);
10929
10996
  }
10930
10997
  client = createTelegramClient({ token });
10931
10998
  try {
10932
10999
  botUser = await client.getMe();
10933
11000
  } catch (e) {
10934
- console.error(chalk27.red(`token rejected: ${e.message}`));
11001
+ console.error(chalk29.red(`token rejected: ${e.message}`));
10935
11002
  process.exit(1);
10936
11003
  }
10937
- console.log(chalk27.green(`Connected as @${botUser.username}`));
11004
+ console.log(chalk29.green(`Connected as @${botUser.username}`));
10938
11005
  console.log();
10939
11006
  }
10940
11007
  } else {
10941
11008
  console.log("Paste your bot token then press Enter (input is hidden):");
10942
11009
  token = await questionMasked();
10943
11010
  if (!token) {
10944
- console.error(chalk27.red("token required"));
11011
+ console.error(chalk29.red("token required"));
10945
11012
  process.exit(1);
10946
11013
  }
10947
11014
  client = createTelegramClient({ token });
10948
11015
  try {
10949
11016
  botUser = await client.getMe();
10950
11017
  } catch (e) {
10951
- console.error(chalk27.red(`token rejected: ${e.message}`));
11018
+ console.error(chalk29.red(`token rejected: ${e.message}`));
10952
11019
  process.exit(1);
10953
11020
  }
10954
- console.log(chalk27.green(`Connected as @${botUser.username}`));
11021
+ console.log(chalk29.green(`Connected as @${botUser.username}`));
10955
11022
  console.log();
10956
11023
  }
10957
- console.log(chalk27.bold("Step 2/3 \u2014 Supergroup"));
11024
+ console.log(chalk29.bold("Step 2/3 \u2014 Supergroup"));
10958
11025
  const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
10959
11026
  let supergroupId;
10960
11027
  let detectedUserId;
10961
11028
  if (groupDecision === "reuse") {
10962
11029
  supergroupId = existingCfg.supergroupId;
10963
- console.log(chalk27.green(`Using existing group: ${supergroupId}`));
11030
+ console.log(chalk29.green(`Using existing group: ${supergroupId}`));
10964
11031
  console.log();
10965
11032
  } else {
10966
11033
  console.log("Add the bot to your forum supergroup, then send any message in it.");
10967
- console.log(chalk27.dim("Waiting for a message (up to 60s)\u2026"));
11034
+ console.log(chalk29.dim("Waiting for a message (up to 60s)\u2026"));
10968
11035
  try {
10969
11036
  ({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
10970
11037
  } catch {
10971
- console.error(chalk27.red("Timed out \u2014 no supergroup message received within 60s."));
10972
- console.error(chalk27.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
11038
+ console.error(chalk29.red("Timed out \u2014 no supergroup message received within 60s."));
11039
+ console.error(chalk29.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
10973
11040
  process.exit(1);
10974
11041
  }
10975
- console.log(chalk27.green(`Found group: ${supergroupId}`));
11042
+ console.log(chalk29.green(`Found group: ${supergroupId}`));
10976
11043
  console.log();
10977
11044
  }
10978
- console.log(chalk27.bold("Step 3/3 \u2014 Remote control + Save"));
10979
- console.log(chalk27.dim("Remote control enables auto-launching captains and the General command channel"));
10980
- console.log(chalk27.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
11045
+ console.log(chalk29.bold("Step 3/3 \u2014 Remote control + Save"));
11046
+ console.log(chalk29.dim("Remote control enables auto-launching captains and the General command channel"));
11047
+ console.log(chalk29.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
10981
11048
  const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
10982
11049
  let users;
10983
11050
  let remoteControl;
@@ -10991,32 +11058,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
10991
11058
  remoteControl = true;
10992
11059
  }
10993
11060
  } else if (groupDecision === "detect") {
10994
- console.log(chalk27.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
10995
- console.log(chalk27.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
11061
+ console.log(chalk29.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
11062
+ console.log(chalk29.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
10996
11063
  printedRemoteControlState = true;
10997
11064
  } else {
10998
11065
  const existingUsers = existingCfg?.users;
10999
11066
  if (existingUsers && existingUsers.length > 0) {
11000
- console.log(chalk27.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
11067
+ console.log(chalk29.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
11001
11068
  } else {
11002
- console.log(chalk27.dim("Remote control: off. Re-run with --user-id <id> to enable."));
11069
+ console.log(chalk29.dim("Remote control: off. Re-run with --user-id <id> to enable."));
11003
11070
  }
11004
11071
  printedRemoteControlState = true;
11005
11072
  }
11006
11073
  writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
11007
- console.log(chalk27.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
11074
+ console.log(chalk29.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
11008
11075
  if (!printedRemoteControlState) {
11009
11076
  if (remoteControl) {
11010
- console.log(chalk27.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
11077
+ console.log(chalk29.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
11011
11078
  } else {
11012
- console.log(chalk27.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
11079
+ console.log(chalk29.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
11013
11080
  }
11014
11081
  }
11015
11082
  try {
11016
11083
  await runRegisterCommands({ client });
11017
- console.log(chalk27.dim("Registered the /command menu."));
11084
+ console.log(chalk29.dim("Registered the /command menu."));
11018
11085
  } catch (e) {
11019
- console.log(chalk27.yellow(`command-menu registration skipped: ${e.message}`));
11086
+ console.log(chalk29.yellow(`command-menu registration skipped: ${e.message}`));
11020
11087
  }
11021
11088
  const topics = loadState(defaultStateRoot()).topics;
11022
11089
  const topicEntries = Object.entries(topics);
@@ -11025,28 +11092,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
11025
11092
  const project = key.slice(0, key.indexOf("::"));
11026
11093
  return `${project}\u2192${id}`;
11027
11094
  }).join(", ");
11028
- console.log(chalk27.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
11095
+ console.log(chalk29.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
11029
11096
  } else {
11030
- console.log(chalk27.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
11097
+ console.log(chalk29.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
11031
11098
  }
11032
11099
  runTelegramPostSetup({});
11033
11100
  console.log();
11034
- console.log(`Next: ${chalk27.cyan("squadrant telegram link <project>")}`);
11101
+ console.log(`Next: ${chalk29.cyan("squadrant telegram link <project>")}`);
11035
11102
  });
11036
11103
  telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
11037
11104
  const cfg = loadConfig().telegram;
11038
11105
  if (!cfg) {
11039
- console.error(chalk27.red("telegram config absent \u2014 run: squadrant telegram setup"));
11106
+ console.error(chalk29.red("telegram config absent \u2014 run: squadrant telegram setup"));
11040
11107
  process.exit(1);
11041
11108
  }
11042
11109
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11043
11110
  if (!token) {
11044
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11111
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11045
11112
  process.exit(1);
11046
11113
  }
11047
11114
  const client = createTelegramClient({ token });
11048
11115
  await runRegisterCommands({ client });
11049
- console.log(chalk27.green(`registered ${BOT_COMMANDS.length} bot commands`));
11116
+ console.log(chalk29.green(`registered ${BOT_COMMANDS.length} bot commands`));
11050
11117
  });
11051
11118
  telegramCommand.command("notify").argument("[project]", "project to toggle").argument("[state]", "on | off | crew | cap").argument("[value]", "tier for crew (all|alert_only|done_only|none) or on|off for cap").option("--status", "list notification state for all projects").description("Live on|off (state), or crew <tier> / cap <on|off> preference (per-project config)").action(async (project, state, value, opts) => {
11052
11119
  const stateRoot = defaultStateRoot();
@@ -11057,7 +11124,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
11057
11124
  return;
11058
11125
  }
11059
11126
  for (const r of rows) {
11060
- console.log(` ${r.project}: ${r.active ? chalk27.green("on") : chalk27.dim("off (muted)")}`);
11127
+ console.log(` ${r.project}: ${r.active ? chalk29.green("on") : chalk29.dim("off (muted)")}`);
11061
11128
  }
11062
11129
  return;
11063
11130
  }
@@ -11066,53 +11133,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
11066
11133
  const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11067
11134
  if (state === "crew" || state === "cap") {
11068
11135
  if (value === void 0) {
11069
- console.error(chalk27.red(`usage: squadrant telegram notify <project> ${state} <value>`));
11136
+ console.error(chalk29.red(`usage: squadrant telegram notify <project> ${state} <value>`));
11070
11137
  process.exit(1);
11071
11138
  }
11072
11139
  const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
11073
11140
  const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
11074
11141
  const res = runTelegramNotifyPref({ project, dimension: state, value });
11075
11142
  if (!res.ok) {
11076
- console.error(chalk27.red(res.message));
11143
+ console.error(chalk29.red(res.message));
11077
11144
  process.exit(1);
11078
11145
  }
11079
- console.log(chalk27.green(`${project} ${state} = ${value}`));
11146
+ console.log(chalk29.green(`${project} ${state} = ${value}`));
11080
11147
  const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
11081
11148
  if (tgCfg && token) {
11082
11149
  const client = createTelegramClient({ token });
11083
11150
  const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
11084
- if (sent) console.log(chalk27.dim(`\u2192 notified ${project} topic`));
11151
+ if (sent) console.log(chalk29.dim(`\u2192 notified ${project} topic`));
11085
11152
  }
11086
11153
  return;
11087
11154
  }
11088
11155
  if (state !== "on" && state !== "off") {
11089
- console.error(chalk27.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
11156
+ console.error(chalk29.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
11090
11157
  process.exit(1);
11091
11158
  }
11092
11159
  const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
11093
11160
  const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
11094
11161
  const after = { ...before, active: state === "on" };
11095
11162
  runTelegramNotifySet({ project, active: state === "on", stateRoot });
11096
- console.log(chalk27.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
11163
+ console.log(chalk29.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
11097
11164
  if (tgCfg && token) {
11098
11165
  const client = createTelegramClient({ token });
11099
11166
  const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
11100
- if (sent) console.log(chalk27.dim(`\u2192 notified ${project} topic`));
11167
+ if (sent) console.log(chalk29.dim(`\u2192 notified ${project} topic`));
11101
11168
  }
11102
11169
  });
11103
11170
  telegramCommand.command("send").argument("<project>", "project whose topic receives the message").argument("[message...]", "message text (omit to read from stdin)").description("Send a message to a project's linked Telegram topic").action(async (project, messageParts) => {
11104
11171
  const cfg = loadConfig().telegram;
11105
11172
  if (!cfg) {
11106
- console.error(chalk27.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
11173
+ console.error(chalk29.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
11107
11174
  process.exit(1);
11108
11175
  }
11109
11176
  const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
11110
11177
  if (!token) {
11111
- console.error(chalk27.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11178
+ console.error(chalk29.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
11112
11179
  process.exit(1);
11113
11180
  }
11114
11181
  if (!capAllowed(project, cfg.notify)) {
11115
- console.log(chalk27.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
11182
+ console.log(chalk29.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
11116
11183
  return;
11117
11184
  }
11118
11185
  let message;
@@ -11125,19 +11192,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
11125
11192
  for await (const line of rl) lines.push(line);
11126
11193
  message = lines.join("\n").trimEnd();
11127
11194
  if (!message) {
11128
- console.error(chalk27.red("no message provided (stdin was empty)"));
11195
+ console.error(chalk29.red("no message provided (stdin was empty)"));
11129
11196
  process.exit(1);
11130
11197
  }
11131
11198
  } else {
11132
- console.error(chalk27.red("message required \u2014 pass as argument or pipe via stdin"));
11199
+ console.error(chalk29.red("message required \u2014 pass as argument or pipe via stdin"));
11133
11200
  process.exit(1);
11134
11201
  }
11135
11202
  const client = createTelegramClient({ token });
11136
11203
  try {
11137
11204
  const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
11138
- console.log(chalk27.green(`sent to group ${chatId} topic ${topicId}`));
11205
+ console.log(chalk29.green(`sent to group ${chatId} topic ${topicId}`));
11139
11206
  } catch (e) {
11140
- console.error(chalk27.red(e.message));
11207
+ console.error(chalk29.red(e.message));
11141
11208
  process.exit(1);
11142
11209
  }
11143
11210
  });
@@ -11145,7 +11212,7 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
11145
11212
  // packages/cli/src/commands/hooks.ts
11146
11213
  init_dist2();
11147
11214
  init_dist4();
11148
- import { Command as Command28 } from "commander";
11215
+ import { Command as Command30 } from "commander";
11149
11216
  import { join as join25 } from "path";
11150
11217
  import { homedir as homedir17 } from "os";
11151
11218
  var SOCK4 = join25(homedir17(), ".config", "squadrant", "squadrant.sock");
@@ -11174,7 +11241,7 @@ function mapHookSub(sub, payload, taskId) {
11174
11241
  }
11175
11242
  }
11176
11243
  function hooksCommand() {
11177
- const hooks = new Command28("hooks").description("(internal) receive lifecycle hook events from agent processes");
11244
+ const hooks = new Command30("hooks").description("(internal) receive lifecycle hook events from agent processes");
11178
11245
  hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
11179
11246
  const taskId = process.env.SQUADRANT_CREW_TASK_ID;
11180
11247
  const project = process.env.SQUADRANT_CREW_PROJECT;
@@ -11243,7 +11310,7 @@ if (process.argv[2] !== "config") {
11243
11310
  if (!process.env.SQUADRANT_DAEMON_SKIP) {
11244
11311
  ensureDaemon();
11245
11312
  }
11246
- var program = new Command29();
11313
+ var program = new Command31();
11247
11314
  program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
11248
11315
  program.addCommand(doctorCommand);
11249
11316
  program.addCommand(initCommand);
@@ -11267,6 +11334,8 @@ program.addCommand(codexChatSmokeCommand);
11267
11334
  program.addCommand(configCommand);
11268
11335
  program.addCommand(healCommand);
11269
11336
  program.addCommand(groupCommand);
11337
+ program.addCommand(pingCommand);
11338
+ program.addCommand(dispatchCommand);
11270
11339
  program.addCommand(cmuxCommand);
11271
11340
  program.addCommand(effortCommand);
11272
11341
  program.addCommand(telegramCommand);