squadrant 0.14.2 → 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.
@@ -588,6 +588,8 @@ function recoverStall(rec, now) {
588
588
  }
589
589
 
590
590
  // packages/core/dist/daemon/reduce.js
591
+ var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
592
+ var DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
591
593
  var DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
592
594
  var TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
593
595
  var TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -708,6 +710,71 @@ function createDaemon(deps) {
708
710
  const { store, now } = deps;
709
711
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
710
712
  const quietNotifiedAt = /* @__PURE__ */ new Map();
713
+ const resendAttemptedAt = /* @__PURE__ */ new Map();
714
+ const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
715
+ const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
716
+ async function applyEvent(project, event) {
717
+ if (!KNOWN_EVENT_TYPES.has(event.type)) {
718
+ throw new Error(`unknown event type '${event.type}' \u2014 not a valid ControlEvent`);
719
+ }
720
+ const cur = store.get(project, event.id);
721
+ if (!cur)
722
+ throw new Error(`unknown task ${event.id}`);
723
+ if (event.type === "task.started")
724
+ lastCaptainTurnAt.set(event.id, now());
725
+ if (event.type === "task.session.ended" && !TERMINAL_STATES.has(cur.state)) {
726
+ const liveness = deps.isSurfaceAlive ? await deps.isSurfaceAlive(cur) : "unknown";
727
+ if (liveness !== "gone")
728
+ return cur;
729
+ }
730
+ const next = reduce(cur, event, now());
731
+ if (next !== cur) {
732
+ store.put(next);
733
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
734
+ }
735
+ return next;
736
+ }
737
+ async function attemptFirstTurnRecovery(r, undeliveredMs) {
738
+ const lastAttempt = resendAttemptedAt.get(r.id);
739
+ if (lastAttempt != null && now() - lastAttempt < firstTurnResendCooldownMs)
740
+ return;
741
+ resendAttemptedAt.set(r.id, now());
742
+ const fresh = store.get(r.project, r.id);
743
+ if (!fresh || fresh.firstTurnConfirmedAt)
744
+ return;
745
+ const tag = crewTag(fresh);
746
+ const fireNotify = (message) => {
747
+ if (!deps.notify)
748
+ return;
749
+ const synthEvent = { type: "task.quiet", id: fresh.id, quietMs: undeliveredMs };
750
+ try {
751
+ const p = deps.notify({ project: fresh.project, message, record: store.get(fresh.project, fresh.id) ?? fresh, event: synthEvent });
752
+ if (p && typeof p.catch === "function")
753
+ p.catch(() => {
754
+ });
755
+ } catch {
756
+ }
757
+ };
758
+ if (!deps.resendFirstTurn) {
759
+ fireNotify(`\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (0 activity) \u2014 re-send the task or check the spawn.`);
760
+ return;
761
+ }
762
+ let result;
763
+ try {
764
+ result = await deps.resendFirstTurn(fresh);
765
+ } catch {
766
+ result = { delivered: false };
767
+ }
768
+ if (result.delivered) {
769
+ try {
770
+ await applyEvent(fresh.project, { type: "task.first-turn.confirmed", id: fresh.id });
771
+ } catch {
772
+ }
773
+ fireNotify(`\u{1F501} CREW FIRST-TURN AUTO-RESENT ${tag}: first turn had not landed after ${Math.round(undeliveredMs / 1e3)}s \u2014 re-sent automatically.`);
774
+ } else {
775
+ fireNotify(`\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed \u2014 auto-resend attempted but the pane wasn't ready; will retry.`);
776
+ }
777
+ }
711
778
  return {
712
779
  async handle(req) {
713
780
  switch (req.kind) {
@@ -754,22 +821,7 @@ function createDaemon(deps) {
754
821
  if (!KNOWN_EVENT_TYPES.has(req.event.type)) {
755
822
  throw new Error(`unknown event type '${req.event.type}' \u2014 not a valid ControlEvent`);
756
823
  }
757
- const cur = store.get(req.project, req.event.id);
758
- if (!cur)
759
- throw new Error(`unknown task ${req.event.id}`);
760
- if (req.event.type === "task.started")
761
- lastCaptainTurnAt.set(req.event.id, now());
762
- if (req.event.type === "task.session.ended" && !TERMINAL_STATES.has(cur.state)) {
763
- const liveness = deps.isSurfaceAlive ? await deps.isSurfaceAlive(cur) : "unknown";
764
- if (liveness !== "gone")
765
- return cur;
766
- }
767
- const next = reduce(cur, req.event, now());
768
- if (next !== cur) {
769
- store.put(next);
770
- firePush(deps, req.project, cur.state, next, req.event, lastCaptainTurnAt.get(next.id));
771
- }
772
- return next;
824
+ return applyEvent(req.project, req.event);
773
825
  }
774
826
  case "status": {
775
827
  const r = store.get(req.project, req.id);
@@ -828,6 +880,7 @@ function createDaemon(deps) {
828
880
  store.delete(r.project, r.id);
829
881
  }
830
882
  }
883
+ const recoveryPromises = [];
831
884
  for (const r of store.listAll()) {
832
885
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
833
886
  store.delete(r.project, r.id);
@@ -869,21 +922,9 @@ function createDaemon(deps) {
869
922
  }
870
923
  }
871
924
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
872
- const elapsed = t - r.lastHeartbeat;
873
- if (elapsed > r.heartbeatBudgetMs) {
874
- if (deps.notify && quietNotifiedAt.get(r.id) !== r.lastHeartbeat) {
875
- quietNotifiedAt.set(r.id, r.lastHeartbeat);
876
- const tag = crewTag(r);
877
- const synthEvent = { type: "task.quiet", id: r.id, quietMs: elapsed };
878
- const message = `\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (crew never started) \u2014 re-send the task or check the spawn.`;
879
- try {
880
- const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
881
- if (p && typeof p.catch === "function")
882
- p.catch(() => {
883
- });
884
- } catch {
885
- }
886
- }
925
+ const undeliveredMs = t - r.createdAt;
926
+ if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
927
+ recoveryPromises.push(attemptFirstTurnRecovery(r, undeliveredMs));
887
928
  continue;
888
929
  }
889
930
  }
@@ -894,7 +935,14 @@ function createDaemon(deps) {
894
935
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
895
936
  continue;
896
937
  }
897
- if (r.mode === "interactive" && r.state === "working" && !r.pendingTool) {
938
+ if (r.mode === "interactive" && r.state === "working" && !r.pendingTool && !r.firstTurnConfirmedAt) {
939
+ const undeliveredMs = t - r.createdAt;
940
+ if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
941
+ recoveryPromises.push(attemptFirstTurnRecovery(r, undeliveredMs));
942
+ continue;
943
+ }
944
+ }
945
+ if (r.mode === "interactive" && r.state === "working" && !r.pendingTool && r.firstTurnConfirmedAt) {
898
946
  const liveness = r.attempts.at(-1)?.lastHeartbeatAt ?? r.lastHeartbeat;
899
947
  const quiet = t - liveness;
900
948
  if (quiet > r.heartbeatBudgetMs) {
@@ -902,13 +950,8 @@ function createDaemon(deps) {
902
950
  quietNotifiedAt.set(r.id, liveness);
903
951
  const tag = crewTag(r);
904
952
  const synthEvent = { type: "task.quiet", id: r.id, quietMs: quiet };
905
- let message;
906
- if (!r.firstTurnConfirmedAt) {
907
- message = `\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (0 activity) \u2014 re-send the task or check the spawn.`;
908
- } else {
909
- const mins = Math.max(1, Math.round(quiet / 6e4));
910
- message = `CREW QUIET ${tag}: working ~${mins}min with no tool activity \u2014 likely deep thinking (no reply expected yet).`;
911
- }
953
+ const mins = Math.max(1, Math.round(quiet / 6e4));
954
+ const message = `CREW QUIET ${tag}: working ~${mins}min with no tool activity \u2014 likely deep thinking (no reply expected yet).`;
912
955
  try {
913
956
  const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
914
957
  if (p && typeof p.catch === "function")
@@ -926,6 +969,7 @@ function createDaemon(deps) {
926
969
  if (recovered && t - r.lastHeartbeat <= r.heartbeatBudgetMs)
927
970
  store.put(recovered);
928
971
  }
972
+ await Promise.all(recoveryPromises);
929
973
  },
930
974
  async reconcile() {
931
975
  const alive = deps.isPidAlive ?? (() => true);
@@ -1639,6 +1683,7 @@ function buildContext(opts) {
1639
1683
  activeHeadlessKills: /* @__PURE__ */ new Set(),
1640
1684
  captainMissingStreak: /* @__PURE__ */ new Map(),
1641
1685
  stoppedProjects: /* @__PURE__ */ new Set(),
1686
+ resendFirstTurn: opts.resendFirstTurn,
1642
1687
  // Late-bound — start.ts fills these before first use:
1643
1688
  d: null,
1644
1689
  notify: null,
@@ -2309,6 +2354,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2309
2354
  notify,
2310
2355
  taskTimeoutMs,
2311
2356
  isSurfaceAlive: surfaceProbe,
2357
+ resendFirstTurn: ctx.resendFirstTurn,
2312
2358
  launchHeadless: opts.launchHeadless,
2313
2359
  isHeadlessInFlight: (id) => inFlightHeadlessIds.has(id),
2314
2360
  launchInteractive: async (rec) => {
@@ -2549,6 +2595,21 @@ import crypto from "crypto";
2549
2595
  import fs8 from "fs";
2550
2596
  import path7 from "path";
2551
2597
 
2598
+ // packages/core/dist/crew-protocol.js
2599
+ function buildCompletionProtocol(taskId, project) {
2600
+ return [
2601
+ "---",
2602
+ "COMPLETION PROTOCOL (required): When this task is fully complete, your FINAL action MUST be to run exactly:",
2603
+ ` squadrant crew signal done --task-id ${taskId} --project ${project} --message "<one-line summary>"`,
2604
+ "Run it as a discrete final step AFTER you report your results. If you are blocked or need a decision, instead run:",
2605
+ ` squadrant crew signal blocked --task-id ${taskId} --project ${project} --question "<your question>"`,
2606
+ "If this task failed because of a defect in squadrant itself (not an API/infra blip, a config/user error, or an expected failure), say so in your signal done/blocked message so the captain can check tu11aa/squadrant and file it. Don't file issues from the crew."
2607
+ ].join("\n");
2608
+ }
2609
+ function titleFor(project, name) {
2610
+ return `\u{1F527} ${project}:${name}`;
2611
+ }
2612
+
2552
2613
  // packages/core/dist/crew-lifecycle.js
2553
2614
  import { exec as nodeExec } from "child_process";
2554
2615
 
@@ -4079,7 +4140,7 @@ var OpencodeSseBridge = class {
4079
4140
  const fetchImpl = this.deps.fetchImpl ?? fetch;
4080
4141
  const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4081
4142
  const reconnectMs = this.deps.reconnectMs ?? 500;
4082
- const maxBoot = this.deps.maxBootAttempts ?? 60;
4143
+ const maxBoot = this.deps.maxBootAttempts ?? 240;
4083
4144
  const url = `http://127.0.0.1:${port}/event`;
4084
4145
  let booted = false;
4085
4146
  let bootAttempts = 0;
@@ -4433,6 +4494,27 @@ function parseDraftFromScreen(screen) {
4433
4494
  }
4434
4495
  return "";
4435
4496
  }
4497
+ function hasCCInputBox(screen) {
4498
+ if (!screen)
4499
+ return false;
4500
+ const lines = screen.split(/\r?\n/);
4501
+ const HR_RE = /^\s*─{10,}\s*$/;
4502
+ let bottomHR = -1;
4503
+ let topHR = -1;
4504
+ for (let i = lines.length - 1; i >= 0; i--) {
4505
+ if (HR_RE.test(lines[i])) {
4506
+ if (bottomHR === -1)
4507
+ bottomHR = i;
4508
+ else {
4509
+ topHR = i;
4510
+ break;
4511
+ }
4512
+ }
4513
+ }
4514
+ if (topHR === -1)
4515
+ return false;
4516
+ return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
4517
+ }
4436
4518
  function hasModalOptionList(screen) {
4437
4519
  if (!screen)
4438
4520
  return false;
@@ -4483,6 +4565,15 @@ function readInputBoxRaw(screen, opts) {
4483
4565
  const joined = parts.join("");
4484
4566
  return opts?.trim === false ? joined : joined.replace(/\s+$/, "");
4485
4567
  }
4568
+ var CC_INITIALIZED_RE = /⏵⏵|Ctx Used|for shortcuts|accept edits/i;
4569
+ var CC_WORKING_RE = /↓\s*[\d.]+\s*k?\s*tokens?\b|esc to interrupt|\bshell still running\b|·\s*\d+\s*shell\b|\(\d+m?\s*\d*s\b/i;
4570
+ function classifyStartupSurface(screen) {
4571
+ if (CC_WORKING_RE.test(screen))
4572
+ return "working";
4573
+ if (CC_INITIALIZED_RE.test(screen))
4574
+ return "idle";
4575
+ return "loading";
4576
+ }
4486
4577
  function sendDebugEnabled() {
4487
4578
  return !!process.env.SQUADRANT_DEBUG_SEND;
4488
4579
  }
@@ -5315,6 +5406,67 @@ function defaultWriteFile(path17, content) {
5315
5406
 
5316
5407
  // packages/workspaces/dist/crew-pane.js
5317
5408
  import net from "net";
5409
+ var POST_SEND_CHECK_MS = 750;
5410
+ var SETTLE_POLL_MS = 400;
5411
+ var SETTLE_MAX_POLLS = 8;
5412
+ var SUBMIT_RETRY_LIMIT = 4;
5413
+ async function settleInputBox(runtime, pane) {
5414
+ let prev = await runtime.readPaneScreen(pane) ?? "";
5415
+ let sawContent = parseDraftFromScreen(prev) !== "" && parseDraftFromScreen(prev) !== null;
5416
+ for (let i = 0; i < SETTLE_MAX_POLLS; i++) {
5417
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
5418
+ const cur = await runtime.readPaneScreen(pane) ?? "";
5419
+ const draft = parseDraftFromScreen(cur);
5420
+ if (draft !== "" && draft !== null)
5421
+ sawContent = true;
5422
+ if (cur === prev)
5423
+ return sawContent;
5424
+ prev = cur;
5425
+ }
5426
+ return sawContent;
5427
+ }
5428
+ async function confirmedSendToPane(runtime, pane, message) {
5429
+ const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
5430
+ await runtime.pasteToPane(pane, message);
5431
+ let sawDraft = await settleInputBox(runtime, pane);
5432
+ await runtime.sendKeyToPane(pane, "Enter");
5433
+ let repasted = false;
5434
+ for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
5435
+ await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
5436
+ const afterScreen = await runtime.readPaneScreen(pane) ?? "";
5437
+ const draft = parseDraftFromScreen(afterScreen);
5438
+ if (draft !== "" && draft !== null)
5439
+ sawDraft = true;
5440
+ if (draft === "" && sawDraft)
5441
+ return { delivered: true };
5442
+ if (draft === null && afterScreen !== preSendScreen && sawDraft)
5443
+ return { delivered: true };
5444
+ const settled = await settleInputBox(runtime, pane);
5445
+ if (settled)
5446
+ sawDraft = true;
5447
+ if (!sawDraft && !repasted) {
5448
+ repasted = true;
5449
+ await runtime.pasteToPane(pane, message);
5450
+ }
5451
+ await runtime.sendKeyToPane(pane, "Enter");
5452
+ }
5453
+ return { delivered: false };
5454
+ }
5455
+ async function resendCrewFirstTurn(runtime, captainName, project, name, message) {
5456
+ const captain = await runtime.status(captainName);
5457
+ if (!captain)
5458
+ return { delivered: false };
5459
+ const surfaces = await runtime.listSurfaces(captain.id);
5460
+ const want = titleFor(project, name);
5461
+ const pane = surfaces.find((s) => s.title === want);
5462
+ if (!pane)
5463
+ return { delivered: false };
5464
+ const screen = await runtime.readPaneScreen(pane) ?? "";
5465
+ if (!hasCCInputBox(screen) || classifyStartupSurface(screen) !== "idle") {
5466
+ return { delivered: false };
5467
+ }
5468
+ return confirmedSendToPane(runtime, pane, message);
5469
+ }
5318
5470
 
5319
5471
  // packages/cli/src/squadrantd.ts
5320
5472
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
@@ -5418,6 +5570,16 @@ function startSquadrantd(opts = {}) {
5418
5570
  const tgCfg = loadConfig().telegram;
5419
5571
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
5420
5572
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
5573
+ const resendRuntime = createCmuxDriver();
5574
+ ctx.resendFirstTurn = opts.resendFirstTurn ?? (async (rec) => {
5575
+ if (rec.provider !== "claude" || !rec.name) return { delivered: false };
5576
+ const proj = loadConfig().projects[rec.project];
5577
+ const captainName = proj?.captainName ?? `${rec.project}-captain`;
5578
+ const message = `${rec.task}
5579
+
5580
+ ${buildCompletionProtocol(rec.id, rec.project)}`;
5581
+ return resendCrewFirstTurn(resendRuntime, captainName, rec.project, rec.name, message);
5582
+ });
5421
5583
  const launchHeadless = opts.launchHeadless ?? (async (rec) => {
5422
5584
  const ingest = (e) => void ctx.d.handle({ kind: "event", project: rec.project, event: e });
5423
5585
  const handle = runHeadless({