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.
- package/dist/index.js +168 -113
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +202 -40
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +10 -7
package/dist/squadrantd.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
873
|
-
if (
|
|
874
|
-
|
|
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
|
-
|
|
906
|
-
|
|
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 ??
|
|
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({
|