squadrant 0.16.3 → 0.16.4

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
@@ -2332,13 +2332,14 @@ function projectHealth(input) {
2332
2332
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
2333
2333
  const out = [];
2334
2334
  const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
2335
+ const deferral = input.captainDeferral;
2335
2336
  out.push({
2336
2337
  kind: "captain",
2337
2338
  project,
2338
2339
  ref: captainName,
2339
2340
  state: captainState,
2340
2341
  lastSeenMs: null,
2341
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : void 0
2342
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries) \u2014 draft/ghost text blocking captain pane; input never touched, delivers automatically once cleared` : void 0
2342
2343
  });
2343
2344
  if (commandPresent !== null) {
2344
2345
  out.push({
@@ -3005,6 +3006,8 @@ function buildContext(opts) {
3005
3006
  opencodeBridge: null,
3006
3007
  cmuxEventsBridge: null,
3007
3008
  telegramBridge: void 0,
3009
+ notifyFault: opts.notifyFault ?? (() => {
3010
+ }),
3008
3011
  lifecycleSources: opts.lifecycleSources ?? [],
3009
3012
  broadcast: () => {
3010
3013
  },
@@ -3345,7 +3348,7 @@ var init_captain_delivery = __esm({
3345
3348
  const seq = entry.seq;
3346
3349
  const deferCount = this.deferCounts.get(seq) ?? 0;
3347
3350
  const stable = (this.stableCounts.get(seq) ?? 0) >= this.opts.stableProbePolls;
3348
- const probe = stable || deferCount >= this.opts.maxDefers;
3351
+ const probe = stable;
3349
3352
  try {
3350
3353
  await send(msg, probe ? { probe: true } : void 0);
3351
3354
  this.deferCounts.delete(seq);
@@ -3469,7 +3472,9 @@ async function runLivenessTick(deps) {
3469
3472
  }
3470
3473
  }
3471
3474
  function createDelivery(ctx, daemonCmux) {
3472
- const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
3475
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
3476
+ const notifyFault = ctx.notifyFault ?? (() => {
3477
+ });
3473
3478
  const defaultNotify = async (args) => {
3474
3479
  try {
3475
3480
  await appendToMailbox({
@@ -3492,6 +3497,7 @@ function createDelivery(ctx, daemonCmux) {
3492
3497
  const cfg = loadConfig();
3493
3498
  const deliveries = /* @__PURE__ */ new Map();
3494
3499
  const deliveryStats = (project) => deliveries.get(project)?.stats();
3500
+ const stuckNotified = /* @__PURE__ */ new Set();
3495
3501
  const sessionStartMs = Date.now();
3496
3502
  let delivering = false;
3497
3503
  const deliveryCore = async () => {
@@ -3563,6 +3569,18 @@ function createDelivery(ctx, daemonCmux) {
3563
3569
  break;
3564
3570
  }
3565
3571
  }
3572
+ const stuck = d.stats().stuck;
3573
+ if (stuck && !stuckNotified.has(project)) {
3574
+ stuckNotified.add(project);
3575
+ const { maxDeferCount } = d.stats();
3576
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
3577
+ const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
3578
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
3579
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
3580
+ telegramBridge?.pushRaw(project, text);
3581
+ } else if (!stuck && stuckNotified.has(project)) {
3582
+ stuckNotified.delete(project);
3583
+ }
3566
3584
  }
3567
3585
  };
3568
3586
  const deliveryTick = async () => {
@@ -3839,7 +3857,12 @@ function startDaemon(ctx, opts, pkgVersion) {
3839
3857
  captainStopped: null,
3840
3858
  captainState: deriveCaptainState(capEntry),
3841
3859
  commandPresent: null,
3842
- crews: store.list(project)
3860
+ crews: store.list(project),
3861
+ // #579/#484 Gap 3: surface the same deferral stats already exposed to
3862
+ // the snapshot (line ~135 below) on the health row too, so `squadrant
3863
+ // doctor` / `squadrant status --detailed` show a stuck delivery with
3864
+ // zero configuration.
3865
+ captainDeferral: deliveryStats(project)
3843
3866
  }));
3844
3867
  }
3845
3868
  return out;
@@ -4740,6 +4763,14 @@ function createTelegramBridge(opts) {
4740
4763
  s.offset = next;
4741
4764
  saveState(stateRoot, s);
4742
4765
  }
4766
+ async function sendToTopic(project, text) {
4767
+ let threadId = loadState(stateRoot).topics[topicKey(project)];
4768
+ if (threadId === void 0) {
4769
+ threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
4770
+ setTopic(stateRoot, project, threadId);
4771
+ }
4772
+ await client.sendMessage(cfg.supergroupId, threadId, text);
4773
+ }
4743
4774
  async function deliverOutbound(project, ev) {
4744
4775
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
4745
4776
  const live = loadState(stateRoot).notify[project];
@@ -4748,12 +4779,10 @@ function createTelegramBridge(opts) {
4748
4779
  return;
4749
4780
  if (!tierIncludes(resolved.crew, ev.type))
4750
4781
  return;
4751
- let threadId = loadState(stateRoot).topics[topicKey(project)];
4752
- if (threadId === void 0) {
4753
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
4754
- setTopic(stateRoot, project, threadId);
4755
- }
4756
- await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
4782
+ await sendToTopic(project, formatLifecycle(project, ev));
4783
+ }
4784
+ async function deliverRawOutbound(project, text) {
4785
+ await sendToTopic(project, text);
4757
4786
  }
4758
4787
  function resolveLiveNotify(project) {
4759
4788
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -5061,6 +5090,11 @@ function createTelegramBridge(opts) {
5061
5090
  log(`telegram outbound failed project=${project}: ${e.message}`);
5062
5091
  });
5063
5092
  },
5093
+ pushRaw(project, text) {
5094
+ void deliverRawOutbound(project, text).catch((e) => {
5095
+ log(`telegram raw push failed project=${project}: ${e.message}`);
5096
+ });
5097
+ },
5064
5098
  health() {
5065
5099
  return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
5066
5100
  }
@@ -6738,7 +6772,8 @@ var init_runtimes = __esm({
6738
6772
  });
6739
6773
 
6740
6774
  // packages/workspaces/dist/notifiers/cmux.js
6741
- import { execFileSync as execFileSync6, execSync as execSync2 } from "child_process";
6775
+ import { execFile as execFileCb, execSync as execSync2 } from "child_process";
6776
+ import { promisify as promisify2 } from "util";
6742
6777
  function createCmuxNotifier(_scope) {
6743
6778
  return {
6744
6779
  name: "cmux",
@@ -6755,13 +6790,15 @@ function createCmuxNotifier(_scope) {
6755
6790
  }
6756
6791
  },
6757
6792
  async notify(message) {
6758
- execFileSync6("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
6793
+ await execFile3("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
6759
6794
  }
6760
6795
  };
6761
6796
  }
6797
+ var execFile3;
6762
6798
  var init_cmux2 = __esm({
6763
6799
  "packages/workspaces/dist/notifiers/cmux.js"() {
6764
6800
  init_cmux();
6801
+ execFile3 = promisify2(execFileCb);
6765
6802
  }
6766
6803
  });
6767
6804
 
@@ -10799,9 +10836,18 @@ function progressBar(completed, total) {
10799
10836
  }
10800
10837
  function captainIndicator(state) {
10801
10838
  if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
10839
+ if (state === "stopped") return chalk6.magenta("\u23FB");
10802
10840
  if (state === void 0 || state === "unknown") return chalk6.dim("?");
10803
10841
  return chalk6.dim("\u25CB");
10804
10842
  }
10843
+ function formatProjectRow(name, captainName, fm, statusMdState, captainState) {
10844
+ const sessionIndicator = captainIndicator(captainState);
10845
+ const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
10846
+ const crew = statusMdState === "ok" ? String(fm.active_crew ?? 0).padEnd(6) : chalk6.dim("?").padEnd(6);
10847
+ const progress = statusMdState === "ok" ? progressBar(fm.tasks_completed ?? 0, fm.tasks_total ?? 0).padEnd(25) : statusMdState === "unreadable" ? chalk6.red("status.md unreadable").padEnd(25) : chalk6.dim("no notes").padEnd(25);
10848
+ const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
10849
+ return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
10850
+ }
10805
10851
  var statusCommand = new Command4("status").description("Show status of all projects from spoke vault status files").option("--detailed", "also show live per-component service health from the daemon (#77)").action(async (opts) => {
10806
10852
  const config = loadConfig();
10807
10853
  const projects = Object.entries(config.projects);
@@ -10826,28 +10872,19 @@ var statusCommand = new Command4("status").description("Show status of all proje
10826
10872
  console.log(chalk6.dim(" " + "\u2500".repeat(85)));
10827
10873
  for (const [name, project] of projects) {
10828
10874
  const workspace = registry.forProject(name, config);
10829
- if (!await workspace.exists("status.md")) {
10830
- console.log(` ${name.padEnd(18)} ${chalk6.dim("no status.md")}`);
10831
- continue;
10832
- }
10833
10875
  let fm = {};
10834
- try {
10835
- const raw = await workspace.read("status.md");
10836
- fm = matter2(raw).data;
10837
- } catch {
10838
- console.log(` ${name.padEnd(18)} ${chalk6.red("error reading status.md")}`);
10839
- continue;
10876
+ let statusMdState = "missing";
10877
+ if (await workspace.exists("status.md")) {
10878
+ try {
10879
+ const raw = await workspace.read("status.md");
10880
+ fm = matter2(raw).data;
10881
+ statusMdState = "ok";
10882
+ } catch {
10883
+ statusMdState = "unreadable";
10884
+ }
10840
10885
  }
10841
- const sessionIndicator = captainIndicator(captainStateByProject.get(name));
10842
- const captainDisplay = `${project.captainName.padEnd(11)} ${sessionIndicator}`;
10843
- const crew = String(fm.active_crew ?? 0).padEnd(6);
10844
- const progress = progressBar(
10845
- fm.tasks_completed ?? 0,
10846
- fm.tasks_total ?? 0
10847
- ).padEnd(25);
10848
- const updated = timeAgo(fm.last_updated);
10849
10886
  console.log(
10850
- ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`
10887
+ formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
10851
10888
  );
10852
10889
  }
10853
10890
  console.log("");
@@ -11982,7 +12019,7 @@ init_dist();
11982
12019
  import { join as join22 } from "path";
11983
12020
  import { homedir as homedir17 } from "os";
11984
12021
  import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
11985
- import { execFile as execFile3 } from "child_process";
12022
+ import { execFile as execFile4 } from "child_process";
11986
12023
  var DEFAULT_TIMEOUT_MS = 2e3;
11987
12024
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
11988
12025
  function withTimeout2(p, ms) {
@@ -12099,7 +12136,7 @@ function defaultProbeRunners() {
12099
12136
  return {
12100
12137
  probeCmuxBin: () => new Promise((resolve3) => {
12101
12138
  try {
12102
- execFile3(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12139
+ execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve3(!err));
12103
12140
  } catch {
12104
12141
  resolve3(false);
12105
12142
  }