squadrant 0.19.0 → 0.19.1
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 +859 -242
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +582 -159
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +4 -3
- package/templates/captain.claude.md +1 -0
- package/templates/captain.generic.md +1 -0
- package/templates/crew.claude.md +2 -0
- package/templates/crew.generic.md +2 -0
- package/templates/crew.opencode.md +2 -0
package/dist/index.js
CHANGED
|
@@ -86,6 +86,15 @@ var init_config_io = __esm({
|
|
|
86
86
|
import path2 from "path";
|
|
87
87
|
import os from "os";
|
|
88
88
|
import chalk from "chalk";
|
|
89
|
+
function isThinkingLevel(v) {
|
|
90
|
+
return THINKING_LEVELS.includes(v);
|
|
91
|
+
}
|
|
92
|
+
function parseThinkingLevel(v) {
|
|
93
|
+
if (!isThinkingLevel(v)) {
|
|
94
|
+
throw new Error(`Invalid --thinking value '${v}'. Valid values: ${THINKING_LEVELS.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
return v;
|
|
97
|
+
}
|
|
89
98
|
function resolveControlChannelMode(cfg, agent) {
|
|
90
99
|
const v = cfg?.[agent];
|
|
91
100
|
return v && CONTROL_CHANNEL_MODES.has(v) ? v : "off";
|
|
@@ -178,10 +187,11 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
|
|
|
178
187
|
function resolveHome(p) {
|
|
179
188
|
return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
|
|
180
189
|
}
|
|
181
|
-
var CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
|
|
190
|
+
var THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
|
|
182
191
|
var init_config = __esm({
|
|
183
192
|
"packages/shared/dist/config.js"() {
|
|
184
193
|
init_config_io();
|
|
194
|
+
THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
185
195
|
CONTROL_CHANNEL_MODES = /* @__PURE__ */ new Set(["off", "shadow", "on"]);
|
|
186
196
|
DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
|
|
187
197
|
CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
|
|
@@ -1374,6 +1384,7 @@ __export(dist_exports, {
|
|
|
1374
1384
|
SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
|
|
1375
1385
|
TERMINAL_STATES: () => TERMINAL_STATES,
|
|
1376
1386
|
TERMINAL_WORK_STATES: () => TERMINAL_WORK_STATES,
|
|
1387
|
+
THINKING_LEVELS: () => THINKING_LEVELS,
|
|
1377
1388
|
UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
|
|
1378
1389
|
addWorktree: () => addWorktree,
|
|
1379
1390
|
applySafeFixes: () => applySafeFixes,
|
|
@@ -1407,6 +1418,7 @@ __export(dist_exports, {
|
|
|
1407
1418
|
isDaemonCachedKey: () => isDaemonCachedKey,
|
|
1408
1419
|
isNewerVersion: () => isNewerVersion,
|
|
1409
1420
|
isQuieter: () => isQuieter,
|
|
1421
|
+
isThinkingLevel: () => isThinkingLevel,
|
|
1410
1422
|
isUpdateCheckDisabled: () => isUpdateCheckDisabled,
|
|
1411
1423
|
iso: () => iso,
|
|
1412
1424
|
loadConfig: () => loadConfig,
|
|
@@ -1417,6 +1429,7 @@ __export(dist_exports, {
|
|
|
1417
1429
|
needsCheck: () => needsCheck,
|
|
1418
1430
|
notifyIfUpdateAvailable: () => notifyIfUpdateAvailable,
|
|
1419
1431
|
parseSection: () => parseSection,
|
|
1432
|
+
parseThinkingLevel: () => parseThinkingLevel,
|
|
1420
1433
|
probeCmuxDaemonDirect: () => probeCmuxDaemonDirect,
|
|
1421
1434
|
projectConfigPath: () => projectConfigPath,
|
|
1422
1435
|
readConfigFileSync: () => readConfigFileSync,
|
|
@@ -1491,8 +1504,9 @@ function isStickyAttention(state) {
|
|
|
1491
1504
|
function nextPendingTool(current, ev, now) {
|
|
1492
1505
|
if (ev.note === "agent.hook.PreToolUse")
|
|
1493
1506
|
return { name: ev.tool ?? "tool", since: now };
|
|
1494
|
-
if (ev.note === "posttooluse" || ev.note === "agent.hook.UserPromptSubmit")
|
|
1507
|
+
if (ev.note === "posttooluse" || ev.note === "agent.hook.PostToolUse" || ev.note === "agent.hook.UserPromptSubmit") {
|
|
1495
1508
|
return void 0;
|
|
1509
|
+
}
|
|
1496
1510
|
return current;
|
|
1497
1511
|
}
|
|
1498
1512
|
function nextPendingMonitor(current, ev, now) {
|
|
@@ -1595,6 +1609,7 @@ function reduce(rec, ev, now) {
|
|
|
1595
1609
|
case "task.stalled":
|
|
1596
1610
|
case "task.idle":
|
|
1597
1611
|
case "task.quiet":
|
|
1612
|
+
case "task.warn":
|
|
1598
1613
|
case "task.timeout":
|
|
1599
1614
|
case "task.reconcile-failed":
|
|
1600
1615
|
return rec;
|
|
@@ -1616,6 +1631,15 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStal
|
|
|
1616
1631
|
if (rec.pendingTool) {
|
|
1617
1632
|
if (now - rec.pendingTool.since <= toolStallMs)
|
|
1618
1633
|
return null;
|
|
1634
|
+
if (rec.lastEvent === "task.turn.completed") {
|
|
1635
|
+
return {
|
|
1636
|
+
...rec,
|
|
1637
|
+
state: "awaiting-input",
|
|
1638
|
+
pendingTool: void 0,
|
|
1639
|
+
pendingMonitor: void 0,
|
|
1640
|
+
lastEvent: "watchdog.tool-stall-recovered"
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1619
1643
|
return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
|
|
1620
1644
|
}
|
|
1621
1645
|
if (rec.pendingMonitor) {
|
|
@@ -1937,7 +1961,7 @@ function createDaemon(deps) {
|
|
|
1937
1961
|
}
|
|
1938
1962
|
}
|
|
1939
1963
|
}
|
|
1940
|
-
if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
|
|
1964
|
+
if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state) && r.state !== "awaiting-input") {
|
|
1941
1965
|
const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
|
|
1942
1966
|
const refTime = r.workingStretchStartedAt ?? r.createdAt;
|
|
1943
1967
|
if (t - refTime > ceiling) {
|
|
@@ -1983,6 +2007,11 @@ function createDaemon(deps) {
|
|
|
1983
2007
|
const idle = evaluateStall(r, t);
|
|
1984
2008
|
if (idle) {
|
|
1985
2009
|
store.put(idle);
|
|
2010
|
+
if (idle.state === "awaiting-input") {
|
|
2011
|
+
const recoveredEvent = { type: "task.turn.completed", id: r.id, turnId: "watchdog-recover" };
|
|
2012
|
+
firePush(deps, r.project, r.state, idle, recoveredEvent, lastCaptainTurnAt.get(r.id));
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
1986
2015
|
const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
|
|
1987
2016
|
firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
|
|
1988
2017
|
continue;
|
|
@@ -2090,6 +2119,7 @@ var init_reduce = __esm({
|
|
|
2090
2119
|
"task.stalled",
|
|
2091
2120
|
"task.idle",
|
|
2092
2121
|
"task.quiet",
|
|
2122
|
+
"task.warn",
|
|
2093
2123
|
"task.timeout",
|
|
2094
2124
|
"task.reconcile-failed",
|
|
2095
2125
|
"task.cancelled",
|
|
@@ -2588,7 +2618,18 @@ function projectHealth(input) {
|
|
|
2588
2618
|
ref: captainName,
|
|
2589
2619
|
state: captainState,
|
|
2590
2620
|
lastSeenMs: null,
|
|
2591
|
-
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
|
|
2621
|
+
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, reason: ${deferral.reason ?? "unknown"})` : void 0
|
|
2622
|
+
});
|
|
2623
|
+
const deliveryState = captainState === "stopped" ? "stopped" : deferral?.stuck || deferral && deferral.maxDeferCount > 0 ? "stale" : "alive";
|
|
2624
|
+
const deliveryDetail = captainState === "stopped" ? "delivery paused (captain stopped)" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries, reason: ${deferral.reason ?? "unknown"})` : deferral && deferral.maxDeferCount > 0 ? `delivery deferred (${deferral.maxDeferCount} retries, reason: ${deferral.reason ?? "unknown"})` : void 0;
|
|
2625
|
+
out.push({
|
|
2626
|
+
kind: "delivery",
|
|
2627
|
+
project,
|
|
2628
|
+
ref: "delivery",
|
|
2629
|
+
state: deliveryState,
|
|
2630
|
+
lastSeenMs: null,
|
|
2631
|
+
detail: deliveryDetail,
|
|
2632
|
+
stuck: deferral?.stuck
|
|
2592
2633
|
});
|
|
2593
2634
|
if (commandPresent !== null) {
|
|
2594
2635
|
out.push({
|
|
@@ -2690,8 +2731,23 @@ function createStore(root) {
|
|
|
2690
2731
|
};
|
|
2691
2732
|
const projDir = (p) => assertUnderRoot(join6(root, safeSegment("project", p)));
|
|
2692
2733
|
const taskFile = (p, id) => assertUnderRoot(join6(projDir(p), `${safeSegment("id", id)}.json`));
|
|
2734
|
+
const readRecord = (project, id) => {
|
|
2735
|
+
const f = taskFile(project, id);
|
|
2736
|
+
if (!existsSync6(f))
|
|
2737
|
+
return void 0;
|
|
2738
|
+
try {
|
|
2739
|
+
return JSON.parse(readFileSync4(f, "utf-8"));
|
|
2740
|
+
} catch {
|
|
2741
|
+
return void 0;
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2693
2744
|
return {
|
|
2694
2745
|
put(rec) {
|
|
2746
|
+
const existing = readRecord(rec.project, rec.id);
|
|
2747
|
+
if (existing && TERMINAL_STATES.has(existing.state) && TERMINAL_STATES.has(rec.state) && (existing.state !== rec.state || existing.lastEvent !== rec.lastEvent)) {
|
|
2748
|
+
console.error(`[squadrant] REJECTED terminal\u2192terminal overwrite of ${rec.project}/${rec.id}: already ${existing.state}/${existing.lastEvent} \u2014 refusing ${rec.state}/${rec.lastEvent}; original terminal record preserved (#595)`);
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2695
2751
|
mkdirSync2(projDir(rec.project), { recursive: true });
|
|
2696
2752
|
const dest = taskFile(rec.project, rec.id);
|
|
2697
2753
|
const tmp = `${dest}.tmp`;
|
|
@@ -2699,14 +2755,7 @@ function createStore(root) {
|
|
|
2699
2755
|
renameSync(tmp, dest);
|
|
2700
2756
|
},
|
|
2701
2757
|
get(project, id) {
|
|
2702
|
-
|
|
2703
|
-
if (!existsSync6(f))
|
|
2704
|
-
return void 0;
|
|
2705
|
-
try {
|
|
2706
|
-
return JSON.parse(readFileSync4(f, "utf-8"));
|
|
2707
|
-
} catch {
|
|
2708
|
-
return void 0;
|
|
2709
|
-
}
|
|
2758
|
+
return readRecord(project, id);
|
|
2710
2759
|
},
|
|
2711
2760
|
list(project) {
|
|
2712
2761
|
const d = projDir(project);
|
|
@@ -2745,6 +2794,7 @@ function createStore(root) {
|
|
|
2745
2794
|
}
|
|
2746
2795
|
var init_store = __esm({
|
|
2747
2796
|
"packages/core/dist/store.js"() {
|
|
2797
|
+
init_dist();
|
|
2748
2798
|
}
|
|
2749
2799
|
});
|
|
2750
2800
|
|
|
@@ -3111,7 +3161,7 @@ function computeDaemonDrift(nodeBin) {
|
|
|
3111
3161
|
const foreignInstall = detectForeignInstall(parsedCurrent, entry, parsedCurrent !== null && existsSync8(parsedCurrent.daemonEntry));
|
|
3112
3162
|
return { plistPath: p, target, desired, current, changed, programChanged, foreignInstall };
|
|
3113
3163
|
}
|
|
3114
|
-
function
|
|
3164
|
+
function reconcilePlistAndService(drift) {
|
|
3115
3165
|
if (drift.changed) {
|
|
3116
3166
|
mkdirSync4(dirname2(drift.plistPath), { recursive: true });
|
|
3117
3167
|
writeFileSync5(drift.plistPath, drift.desired);
|
|
@@ -3127,8 +3177,47 @@ function applyDaemonDrift(drift) {
|
|
|
3127
3177
|
execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
|
|
3128
3178
|
} catch {
|
|
3129
3179
|
}
|
|
3180
|
+
}
|
|
3181
|
+
function applyDaemonDrift(drift) {
|
|
3182
|
+
reconcilePlistAndService(drift);
|
|
3130
3183
|
execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
|
|
3131
3184
|
}
|
|
3185
|
+
function getDaemonPid(target) {
|
|
3186
|
+
try {
|
|
3187
|
+
const out = execFileSync3("launchctl", ["print", target], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
3188
|
+
const m = out.match(/\bpid\s*=\s*(\d+)/);
|
|
3189
|
+
return m ? parseInt(m[1], 10) : null;
|
|
3190
|
+
} catch {
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
function forceKickstartAndVerify(target, opts = {}) {
|
|
3195
|
+
const pollAttempts = opts.pollAttempts ?? 15;
|
|
3196
|
+
const pollDelayMs = opts.pollDelayMs ?? 300;
|
|
3197
|
+
const kickstartRetries = opts.kickstartRetries ?? 5;
|
|
3198
|
+
const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
|
|
3199
|
+
const pidBefore = getDaemonPid(target);
|
|
3200
|
+
for (let i = 0; i < kickstartRetries; i++) {
|
|
3201
|
+
try {
|
|
3202
|
+
execFileSync3("launchctl", ["kickstart", "-k", target], { stdio: "ignore" });
|
|
3203
|
+
break;
|
|
3204
|
+
} catch (e) {
|
|
3205
|
+
if (i === kickstartRetries - 1)
|
|
3206
|
+
throw e;
|
|
3207
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, kickstartRetryDelayMs);
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
let pidAfter = null;
|
|
3211
|
+
for (let i = 0; i < pollAttempts; i++) {
|
|
3212
|
+
pidAfter = getDaemonPid(target);
|
|
3213
|
+
if (pidAfter !== null && pidAfter !== pidBefore)
|
|
3214
|
+
break;
|
|
3215
|
+
if (i < pollAttempts - 1) {
|
|
3216
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollDelayMs);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
return { target, pidBefore, pidAfter, restarted: pidAfter !== null && pidAfter !== pidBefore };
|
|
3220
|
+
}
|
|
3132
3221
|
function isOperatorInitiatedCommand(topLevelArg) {
|
|
3133
3222
|
return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
|
|
3134
3223
|
}
|
|
@@ -3170,11 +3259,18 @@ function printForeignInstallError(foreign) {
|
|
|
3170
3259
|
Two squadrant installs on this machine will keep fighting over the daemon (#670). Uninstall the one you don't use, then run \`squadrant heal daemon\` to reconcile.
|
|
3171
3260
|
`;
|
|
3172
3261
|
}
|
|
3173
|
-
function reregisterDaemon(nodeBin = process.execPath) {
|
|
3174
|
-
if (!tryAcquireDaemonLock())
|
|
3175
|
-
|
|
3262
|
+
function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
|
|
3263
|
+
if (!tryAcquireDaemonLock()) {
|
|
3264
|
+
throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
|
|
3265
|
+
}
|
|
3176
3266
|
try {
|
|
3177
|
-
|
|
3267
|
+
const drift = computeDaemonDrift(nodeBin);
|
|
3268
|
+
reconcilePlistAndService(drift);
|
|
3269
|
+
const result = forceKickstartAndVerify(drift.target, kickstartOpts);
|
|
3270
|
+
if (!result.restarted) {
|
|
3271
|
+
throw new Error(`daemon did not restart (pid before=${result.pidBefore ?? "none"}, after=${result.pidAfter ?? "none"}) \u2014 \`launchctl kickstart -k ${drift.target}\` ran but the pid never changed`);
|
|
3272
|
+
}
|
|
3273
|
+
return result;
|
|
3178
3274
|
} finally {
|
|
3179
3275
|
releaseDaemonLock();
|
|
3180
3276
|
}
|
|
@@ -3562,6 +3658,10 @@ function detectTrailingQuestion(text) {
|
|
|
3562
3658
|
return lastLine;
|
|
3563
3659
|
return null;
|
|
3564
3660
|
}
|
|
3661
|
+
function isQuotedLine(raw) {
|
|
3662
|
+
const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
|
|
3663
|
+
return QUOTED_PREFIX_RE.test(noAnsi);
|
|
3664
|
+
}
|
|
3565
3665
|
function stripChrome(raw) {
|
|
3566
3666
|
let line = raw.replace(/\[[0-9;]*m/g, "");
|
|
3567
3667
|
line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
|
|
@@ -3620,8 +3720,11 @@ function classifyPaneTail(tail) {
|
|
|
3620
3720
|
if (q)
|
|
3621
3721
|
return { kind: "question", text: q };
|
|
3622
3722
|
let errLine = null;
|
|
3623
|
-
for (
|
|
3624
|
-
|
|
3723
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
3724
|
+
const c = cleaned[i];
|
|
3725
|
+
if (c == null || isQuotedLine(raw[i]))
|
|
3726
|
+
continue;
|
|
3727
|
+
if (ERROR_BANNER_RE.some((re) => re.test(c)))
|
|
3625
3728
|
errLine = c;
|
|
3626
3729
|
}
|
|
3627
3730
|
if (errLine)
|
|
@@ -3664,6 +3767,22 @@ function createInteractiveProbe(deps) {
|
|
|
3664
3767
|
const verdict = classifyPaneTail(tail);
|
|
3665
3768
|
if (!verdict)
|
|
3666
3769
|
continue;
|
|
3770
|
+
if (verdict.kind === "error") {
|
|
3771
|
+
const alive = deps.checkAlive ? await deps.checkAlive(rec) : "unknown";
|
|
3772
|
+
if (alive !== "gone") {
|
|
3773
|
+
const message = `CREW WARN ${rec.name}: pane shows an error string \u2014 crew still ${alive}, not terminalized (pane-detected): ${verdict.text}`;
|
|
3774
|
+
deps.log(`probe -> ${message}`);
|
|
3775
|
+
if (deps.notify) {
|
|
3776
|
+
const warnEvent = { type: "task.warn", id: rec.id, message };
|
|
3777
|
+
try {
|
|
3778
|
+
await deps.notify({ project: rec.project, message, record: rec, event: warnEvent });
|
|
3779
|
+
} catch (e) {
|
|
3780
|
+
deps.log(`probe notify failed for ${rec.id}: ${e.message}`);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3667
3786
|
const event = verdict.kind === "error" ? {
|
|
3668
3787
|
type: "task.failed",
|
|
3669
3788
|
id: rec.id,
|
|
@@ -3685,7 +3804,7 @@ function createInteractiveProbe(deps) {
|
|
|
3685
3804
|
}
|
|
3686
3805
|
return { tick };
|
|
3687
3806
|
}
|
|
3688
|
-
var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
|
|
3807
|
+
var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE, QUOTED_PREFIX_RE;
|
|
3689
3808
|
var init_interactive_probe = __esm({
|
|
3690
3809
|
"packages/core/dist/daemon/interactive-probe.js"() {
|
|
3691
3810
|
STALE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
@@ -3701,6 +3820,7 @@ var init_interactive_probe = __esm({
|
|
|
3701
3820
|
PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
|
|
3702
3821
|
PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
|
|
3703
3822
|
STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
|
|
3823
|
+
QUOTED_PREFIX_RE = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
|
|
3704
3824
|
}
|
|
3705
3825
|
});
|
|
3706
3826
|
|
|
@@ -3716,6 +3836,7 @@ function createProbes(ctx) {
|
|
|
3716
3836
|
};
|
|
3717
3837
|
function buildInteractiveProbe(deps) {
|
|
3718
3838
|
const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
|
|
3839
|
+
const checkAlive = createDirectSurfaceLivenessProbe(deps.cmux, captainNameForProject);
|
|
3719
3840
|
const probe = createInteractiveProbe({
|
|
3720
3841
|
project: "_all_",
|
|
3721
3842
|
listTasks: async () => store.listAll(),
|
|
@@ -3727,7 +3848,9 @@ function createProbes(ctx) {
|
|
|
3727
3848
|
}
|
|
3728
3849
|
},
|
|
3729
3850
|
now: () => Date.now(),
|
|
3730
|
-
log
|
|
3851
|
+
log,
|
|
3852
|
+
checkAlive,
|
|
3853
|
+
notify: ctx.notify
|
|
3731
3854
|
});
|
|
3732
3855
|
let probing = false;
|
|
3733
3856
|
return async () => {
|
|
@@ -4107,6 +4230,7 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
4107
4230
|
const crewRole = config.defaults.roles?.crew;
|
|
4108
4231
|
const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
|
|
4109
4232
|
const crewModel = input.model ?? route?.model ?? configModel;
|
|
4233
|
+
const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
|
|
4110
4234
|
if (agentName !== "claude") {
|
|
4111
4235
|
deps.onModelResolved?.({ agentName, model: crewModel });
|
|
4112
4236
|
}
|
|
@@ -4142,7 +4266,8 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
4142
4266
|
// crew apart from an unrelated session instead of an auto-derived cwd
|
|
4143
4267
|
// basename (only the claude driver reads this — other agents ignore it).
|
|
4144
4268
|
sessionName: crewSessionName(input.project, name),
|
|
4145
|
-
...crewModel ? { model: crewModel } : {}
|
|
4269
|
+
...crewModel ? { model: crewModel } : {},
|
|
4270
|
+
...crewThinking ? { thinking: crewThinking } : {}
|
|
4146
4271
|
});
|
|
4147
4272
|
const direction2 = input.direction ?? "tab";
|
|
4148
4273
|
const title2 = titleFor(input.project, name);
|
|
@@ -4150,7 +4275,13 @@ async function runCrewSpawn(input, config, deps) {
|
|
|
4150
4275
|
const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
|
|
4151
4276
|
await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
|
|
4152
4277
|
const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
|
|
4153
|
-
|
|
4278
|
+
let claudeFirstTurn = firstTurnTask;
|
|
4279
|
+
if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
|
|
4280
|
+
const spillFile = path9.join(os4.tmpdir(), `squadrant-task-${rec.id}.md`);
|
|
4281
|
+
fs9.writeFileSync(spillFile, claudeFirstTurn, "utf8");
|
|
4282
|
+
claudeFirstTurn = `Full task is at ${spillFile} \u2014 cat it and follow it exactly.`;
|
|
4283
|
+
}
|
|
4284
|
+
const claudeResult = await deps.sendFirstTurn(pane2, `${claudeFirstTurn}
|
|
4154
4285
|
|
|
4155
4286
|
${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
|
|
4156
4287
|
if (!claudeResult.delivered) {
|
|
@@ -4250,7 +4381,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
|
|
|
4250
4381
|
if (!crew) {
|
|
4251
4382
|
throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
|
|
4252
4383
|
}
|
|
4253
|
-
const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option.
|
|
4384
|
+
const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option. To answer it deliberately: squadrant crew read ${project} ${name} to see the options, then squadrant crew answer ${project} ${name} <n>.`;
|
|
4254
4385
|
if (deps.isBlockedByModal && await deps.isBlockedByModal(crew)) {
|
|
4255
4386
|
throw new Error(blockedByModalMessage());
|
|
4256
4387
|
}
|
|
@@ -4264,10 +4395,12 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
|
|
|
4264
4395
|
const heldForMin = Math.round((Date.now() - task.operatorHold.since) / 6e4);
|
|
4265
4396
|
throw new Error(`Crew '${name}' is under operator takeover (held ${heldForMin}m${task.operatorHold.note ? `: ${task.operatorHold.note}` : ""}). The operator is working in that tab \u2014 sending a message disrupts their conversation. Ask them to run 'squadrant crew handback ${project} ${name}', or pass --force if they told you to.`);
|
|
4266
4397
|
}
|
|
4398
|
+
let reopened = false;
|
|
4267
4399
|
try {
|
|
4268
4400
|
if (task) {
|
|
4269
4401
|
if (TERMINAL_STATES.has(task.state)) {
|
|
4270
4402
|
await deps.emitEvent(project, { type: "task.reopened", id: task.id });
|
|
4403
|
+
reopened = true;
|
|
4271
4404
|
} else if (task.state === "blocked" || task.state === "awaiting-input" || task.state === "review") {
|
|
4272
4405
|
await deps.emitEvent(project, { type: "task.started", id: task.id });
|
|
4273
4406
|
}
|
|
@@ -4293,7 +4426,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
|
|
|
4293
4426
|
throw new Error(`Message to crew '${name}' is held: ${outcome.reason}. Resolve it in the crew's session, then re-send.`);
|
|
4294
4427
|
}
|
|
4295
4428
|
if (!fallsBackToPane(outcome)) {
|
|
4296
|
-
return;
|
|
4429
|
+
return { reopened };
|
|
4297
4430
|
}
|
|
4298
4431
|
}
|
|
4299
4432
|
if (mode === "shadow" && channel && task) {
|
|
@@ -4316,7 +4449,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
|
|
|
4316
4449
|
if (!paneOk) {
|
|
4317
4450
|
throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
|
|
4318
4451
|
}
|
|
4319
|
-
return;
|
|
4452
|
+
return { reopened };
|
|
4320
4453
|
}
|
|
4321
4454
|
const { delivered, blockedByModal } = await deliver(crew, message);
|
|
4322
4455
|
if (blockedByModal) {
|
|
@@ -4325,6 +4458,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
|
|
|
4325
4458
|
if (!delivered) {
|
|
4326
4459
|
throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
|
|
4327
4460
|
}
|
|
4461
|
+
return { reopened };
|
|
4328
4462
|
}
|
|
4329
4463
|
async function runCrewRead(project, name, runtime, workspaceId) {
|
|
4330
4464
|
const crew = await findCrewPane(runtime, workspaceId, project, name);
|
|
@@ -4427,7 +4561,7 @@ async function runCrewList(project, runtime, workspaceId) {
|
|
|
4427
4561
|
surfaceId: c.surfaceId
|
|
4428
4562
|
}));
|
|
4429
4563
|
}
|
|
4430
|
-
var CC_SOCKS_DIR, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
|
|
4564
|
+
var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
|
|
4431
4565
|
var init_crew_spawn = __esm({
|
|
4432
4566
|
"packages/core/dist/crew-spawn.js"() {
|
|
4433
4567
|
init_control_channel();
|
|
@@ -4436,6 +4570,7 @@ var init_crew_spawn = __esm({
|
|
|
4436
4570
|
init_crew_protocol();
|
|
4437
4571
|
init_crew_lifecycle();
|
|
4438
4572
|
CC_SOCKS_DIR = "/tmp/cc-socks";
|
|
4573
|
+
FIRST_TURN_INLINE_MAX_BYTES = 1200;
|
|
4439
4574
|
TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
|
|
4440
4575
|
STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
|
|
4441
4576
|
CLOSE_LOOKUP_RETRIES = 3;
|
|
@@ -4482,13 +4617,16 @@ var init_captain_channel = __esm({
|
|
|
4482
4617
|
function discoverCaptainSurface(surfaces, captainTitle) {
|
|
4483
4618
|
return surfaces.find((s) => s.title === captainTitle) ?? null;
|
|
4484
4619
|
}
|
|
4485
|
-
function reapOrphanedCrews(store, project) {
|
|
4620
|
+
async function reapOrphanedCrews(store, project, isSurfaceAlive) {
|
|
4486
4621
|
let reaped = 0;
|
|
4487
4622
|
for (const r of store.list(project)) {
|
|
4488
4623
|
if (TERMINAL_STATES.has(r.state))
|
|
4489
4624
|
continue;
|
|
4490
4625
|
if (r.mode !== "interactive")
|
|
4491
4626
|
continue;
|
|
4627
|
+
const liveness = await isSurfaceAlive(r);
|
|
4628
|
+
if (liveness !== "gone")
|
|
4629
|
+
continue;
|
|
4492
4630
|
store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
|
|
4493
4631
|
reaped++;
|
|
4494
4632
|
}
|
|
@@ -4542,10 +4680,13 @@ async function runLivenessTick(deps) {
|
|
|
4542
4680
|
const prev = deps.registry.get(project);
|
|
4543
4681
|
if (prev && prev.lastState === "start")
|
|
4544
4682
|
entry.startedAt = prev.startedAt;
|
|
4683
|
+
const prevState = deriveCaptainState(prev);
|
|
4545
4684
|
deps.registry.apply(entry);
|
|
4546
4685
|
if (winner.pid != null)
|
|
4547
4686
|
deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
|
|
4548
|
-
|
|
4687
|
+
const updated = deps.registry.get(project);
|
|
4688
|
+
if (deriveCaptainState(updated) !== prevState)
|
|
4689
|
+
logEntry(deps.log, project, updated);
|
|
4549
4690
|
}
|
|
4550
4691
|
for (const e of deps.registry.all()) {
|
|
4551
4692
|
if (e.role !== "captain" || e.lastState !== "start" || seen.has(e.project))
|
|
@@ -4563,12 +4704,13 @@ async function runLivenessTick(deps) {
|
|
|
4563
4704
|
continue;
|
|
4564
4705
|
const state = deriveCaptainState(e);
|
|
4565
4706
|
if (state === "stopped" || state === "gone")
|
|
4566
|
-
deps.reap(e.project);
|
|
4707
|
+
await deps.reap(e.project);
|
|
4567
4708
|
}
|
|
4568
4709
|
}
|
|
4569
4710
|
}
|
|
4570
|
-
function createDelivery(ctx, daemonCmux) {
|
|
4711
|
+
function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
|
|
4571
4712
|
const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
|
|
4713
|
+
const surfaceProbe = isSurfaceAlive ?? (async () => "unknown");
|
|
4572
4714
|
const notifyFault = ctx.notifyFault ?? (() => {
|
|
4573
4715
|
});
|
|
4574
4716
|
const defaultNotify = async (args) => {
|
|
@@ -4591,12 +4733,22 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
4591
4733
|
}
|
|
4592
4734
|
};
|
|
4593
4735
|
if (!daemonCmux) {
|
|
4594
|
-
return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
|
|
4736
|
+
return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0, inFlightDelivery: () => null };
|
|
4595
4737
|
}
|
|
4596
4738
|
const cmux2 = daemonCmux;
|
|
4597
4739
|
const cfg = loadConfig();
|
|
4598
4740
|
const deliveries = /* @__PURE__ */ new Map();
|
|
4599
4741
|
const deliveryStats = (project) => deliveries.get(project)?.stats();
|
|
4742
|
+
const lastDeferred = /* @__PURE__ */ new Map();
|
|
4743
|
+
const inFlightDelivery = () => {
|
|
4744
|
+
let worst2 = null;
|
|
4745
|
+
for (const [project, v] of lastDeferred) {
|
|
4746
|
+
if (!worst2 || v.deferCount > worst2.deferCount)
|
|
4747
|
+
worst2 = { project, ...v };
|
|
4748
|
+
}
|
|
4749
|
+
return worst2;
|
|
4750
|
+
};
|
|
4751
|
+
const projectBackoff = /* @__PURE__ */ new Map();
|
|
4600
4752
|
const stuckNotified = /* @__PURE__ */ new Set();
|
|
4601
4753
|
const sessionStartMs = Date.now();
|
|
4602
4754
|
let delivering = false;
|
|
@@ -4607,8 +4759,8 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
4607
4759
|
isPidAlive,
|
|
4608
4760
|
now: () => Date.now(),
|
|
4609
4761
|
log,
|
|
4610
|
-
reap: (project) => {
|
|
4611
|
-
const reaped = reapOrphanedCrews(store, project);
|
|
4762
|
+
reap: async (project) => {
|
|
4763
|
+
const reaped = await reapOrphanedCrews(store, project, surfaceProbe);
|
|
4612
4764
|
if (reaped > 0) {
|
|
4613
4765
|
const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
|
|
4614
4766
|
log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
|
|
@@ -4624,82 +4776,116 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
4624
4776
|
cfg.commandName
|
|
4625
4777
|
])];
|
|
4626
4778
|
for (const project of allProjects) {
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
const
|
|
4633
|
-
surface =
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4779
|
+
try {
|
|
4780
|
+
const backoff = projectBackoff.get(project);
|
|
4781
|
+
if (backoff && Date.now() < backoff.nextAttemptAt)
|
|
4782
|
+
continue;
|
|
4783
|
+
const projCfg = cfg.projects?.[project];
|
|
4784
|
+
const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
|
|
4785
|
+
let surface = null;
|
|
4786
|
+
const resolveCaptainSurface = async () => {
|
|
4787
|
+
const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
|
|
4788
|
+
if (!wsId)
|
|
4789
|
+
return injectedSurfaces[project] ?? null;
|
|
4790
|
+
const surfaces = await cmux2.listSurfaces(wsId);
|
|
4791
|
+
return discoverCaptainSurface(surfaces, captainTitle) ?? injectedSurfaces[project] ?? null;
|
|
4792
|
+
};
|
|
4793
|
+
surface = await resolveCaptainSurface();
|
|
4794
|
+
if (!surface)
|
|
4795
|
+
continue;
|
|
4796
|
+
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
|
|
4797
|
+
const lastAcked = cursor?.lastAckedSeq ?? 0;
|
|
4798
|
+
let d = deliveries.get(project);
|
|
4799
|
+
if (!d) {
|
|
4800
|
+
d = new CaptainDelivery({
|
|
4801
|
+
maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
|
|
4802
|
+
stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
|
|
4803
|
+
});
|
|
4804
|
+
deliveries.set(project, d);
|
|
4805
|
+
}
|
|
4806
|
+
for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
|
|
4807
|
+
if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
|
|
4808
|
+
if (!TERMINAL_KINDS.has(entry.kind)) {
|
|
4809
|
+
const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
|
|
4810
|
+
if (!isExemptMessage) {
|
|
4811
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
|
|
4812
|
+
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
4813
|
+
continue;
|
|
4814
|
+
}
|
|
4815
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
|
|
4816
|
+
} else {
|
|
4817
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
|
|
4657
4818
|
}
|
|
4658
|
-
|
|
4819
|
+
}
|
|
4820
|
+
const result = await d.deliver(entry, async (text, sendOpts) => {
|
|
4821
|
+
let handledByChannel = false;
|
|
4822
|
+
try {
|
|
4823
|
+
const mode = ctx.captainChannelMode?.() ?? "off";
|
|
4824
|
+
const r = await deliverToCaptain(project, text, {
|
|
4825
|
+
channel: ctx.captainChannel,
|
|
4826
|
+
mode,
|
|
4827
|
+
log
|
|
4828
|
+
});
|
|
4829
|
+
handledByChannel = r.handled;
|
|
4830
|
+
} catch (e) {
|
|
4831
|
+
log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
|
|
4832
|
+
}
|
|
4833
|
+
if (handledByChannel) {
|
|
4834
|
+
return;
|
|
4835
|
+
}
|
|
4836
|
+
try {
|
|
4837
|
+
return await cmux2.send(surface, text, sendOpts);
|
|
4838
|
+
} catch (e) {
|
|
4839
|
+
if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
|
|
4840
|
+
throw e;
|
|
4841
|
+
const next = await resolveCaptainSurface();
|
|
4842
|
+
const same = next !== null && next.workspaceId === surface.workspaceId && next.surfaceId === surface.surfaceId;
|
|
4843
|
+
if (!next || same) {
|
|
4844
|
+
log(`delivery project=${project}: probe-failed but surface re-resolution found ${next ? "the same dead surface" : "no captain surface"} \u2014 deferring`);
|
|
4845
|
+
throw e;
|
|
4846
|
+
}
|
|
4847
|
+
log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
|
|
4848
|
+
surface = next;
|
|
4849
|
+
return cmux2.send(next, text, sendOpts);
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
if ("delivered" in result) {
|
|
4853
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
|
|
4854
|
+
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
4855
|
+
lastDeferred.delete(project);
|
|
4856
|
+
projectBackoff.delete(project);
|
|
4659
4857
|
} else {
|
|
4660
|
-
|
|
4858
|
+
const { maxDeferCount, stuck: stuck2 } = d.stats();
|
|
4859
|
+
if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
|
|
4860
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
|
|
4861
|
+
}
|
|
4862
|
+
lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
|
|
4863
|
+
if (stuck2) {
|
|
4864
|
+
const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
|
|
4865
|
+
const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
|
|
4866
|
+
projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
|
|
4867
|
+
}
|
|
4868
|
+
break;
|
|
4661
4869
|
}
|
|
4662
4870
|
}
|
|
4663
|
-
const
|
|
4664
|
-
|
|
4871
|
+
const stuck = d.stats().stuck;
|
|
4872
|
+
if (stuck && !stuckNotified.has(project)) {
|
|
4873
|
+
stuckNotified.add(project);
|
|
4874
|
+
const { maxDeferCount, reason } = d.stats();
|
|
4875
|
+
log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
|
|
4876
|
+
const text = STUCK_ALERT_TEXT[reason ?? "unknown"](maxDeferCount);
|
|
4665
4877
|
try {
|
|
4666
|
-
|
|
4667
|
-
const r = await deliverToCaptain(project, text, {
|
|
4668
|
-
channel: ctx.captainChannel,
|
|
4669
|
-
mode,
|
|
4670
|
-
log
|
|
4671
|
-
});
|
|
4672
|
-
handledByChannel = r.handled;
|
|
4878
|
+
await appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
|
|
4673
4879
|
} catch (e) {
|
|
4674
|
-
log(`
|
|
4675
|
-
}
|
|
4676
|
-
if (handledByChannel) {
|
|
4677
|
-
return;
|
|
4678
|
-
}
|
|
4679
|
-
return cmux2.send(surface, text, sendOpts);
|
|
4680
|
-
});
|
|
4681
|
-
if ("delivered" in result) {
|
|
4682
|
-
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
|
|
4683
|
-
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
4684
|
-
} else {
|
|
4685
|
-
const { maxDeferCount } = d.stats();
|
|
4686
|
-
if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
|
|
4687
|
-
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
|
|
4880
|
+
log(`delivery stuck alert failed project=${project}: ${e.message}`);
|
|
4688
4881
|
}
|
|
4689
|
-
|
|
4882
|
+
Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
|
|
4883
|
+
telegramBridge?.pushRaw(project, text);
|
|
4884
|
+
} else if (!stuck && stuckNotified.has(project)) {
|
|
4885
|
+
stuckNotified.delete(project);
|
|
4690
4886
|
}
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
if (stuck && !stuckNotified.has(project)) {
|
|
4694
|
-
stuckNotified.add(project);
|
|
4695
|
-
const { maxDeferCount, reason } = d.stats();
|
|
4696
|
-
log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
|
|
4697
|
-
const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\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.`;
|
|
4698
|
-
appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
|
|
4699
|
-
Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
|
|
4700
|
-
telegramBridge?.pushRaw(project, text);
|
|
4701
|
-
} else if (!stuck && stuckNotified.has(project)) {
|
|
4702
|
-
stuckNotified.delete(project);
|
|
4887
|
+
} catch (e) {
|
|
4888
|
+
log(`delivery project=${project}: unhandled error \u2014 ${e.message}`);
|
|
4703
4889
|
}
|
|
4704
4890
|
}
|
|
4705
4891
|
};
|
|
@@ -4713,19 +4899,28 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
4713
4899
|
delivering = false;
|
|
4714
4900
|
}
|
|
4715
4901
|
};
|
|
4716
|
-
return { defaultNotify, deliveryTick, deliveryStats };
|
|
4902
|
+
return { defaultNotify, deliveryTick, deliveryStats, inFlightDelivery };
|
|
4717
4903
|
}
|
|
4718
|
-
var CURSOR_SUBSCRIBER, TERMINAL_KINDS;
|
|
4904
|
+
var CURSOR_SUBSCRIBER, TERMINAL_KINDS, STUCK_ALERT_TEXT;
|
|
4719
4905
|
var init_delivery_loop = __esm({
|
|
4720
4906
|
"packages/core/dist/daemon/delivery-loop.js"() {
|
|
4721
4907
|
init_mailbox();
|
|
4722
4908
|
init_captain_delivery();
|
|
4909
|
+
init_defer_delivery();
|
|
4723
4910
|
init_dist();
|
|
4724
4911
|
init_interactive_probe();
|
|
4725
4912
|
init_liveness2();
|
|
4726
4913
|
init_captain_channel();
|
|
4727
4914
|
CURSOR_SUBSCRIBER = "captain";
|
|
4728
4915
|
TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
|
|
4916
|
+
STUCK_ALERT_TEXT = {
|
|
4917
|
+
"no-box": (n) => `\u26A0\uFE0F DELIVERY STUCK: your captain pane's input box could not be confirmed visible (an overlay, menu, or scrolled view may be covering it) and has blocked pending notification(s) for ${n}+ retries. This keeps retrying safely and will deliver automatically once the input box is visible again.`,
|
|
4918
|
+
modal: (n) => `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${n}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.`,
|
|
4919
|
+
draft: (n) => `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${n}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`,
|
|
4920
|
+
"probe-failed": (n) => `\u26A0\uFE0F DELIVERY STUCK: reading your captain pane failed (stale/dead surface reference or cmux unavailable) and has blocked pending notification(s) for ${n}+ retries. Delivery re-resolves the pane automatically; if this persists after a captain restart, bounce the daemon to refresh its surface references.`,
|
|
4921
|
+
stable: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`,
|
|
4922
|
+
unknown: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`
|
|
4923
|
+
};
|
|
4729
4924
|
}
|
|
4730
4925
|
});
|
|
4731
4926
|
|
|
@@ -4813,10 +5008,72 @@ var init_server = __esm({
|
|
|
4813
5008
|
}
|
|
4814
5009
|
});
|
|
4815
5010
|
|
|
5011
|
+
// packages/core/dist/daemon/exit-marker.js
|
|
5012
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
|
|
5013
|
+
import { join as join11 } from "path";
|
|
5014
|
+
function exitMarkerPath(stateRoot) {
|
|
5015
|
+
return join11(stateRoot, "exit-marker.json");
|
|
5016
|
+
}
|
|
5017
|
+
function writeExitMarker(stateRoot, marker, log) {
|
|
5018
|
+
try {
|
|
5019
|
+
writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
|
|
5020
|
+
} catch (e) {
|
|
5021
|
+
log(`exit marker write failed: ${e.message}`);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function consumeExitMarker(stateRoot, now = Date.now) {
|
|
5025
|
+
const p = exitMarkerPath(stateRoot);
|
|
5026
|
+
if (!existsSync9(p))
|
|
5027
|
+
return { marker: null };
|
|
5028
|
+
let marker = null;
|
|
5029
|
+
try {
|
|
5030
|
+
marker = JSON.parse(readFileSync8(p, "utf-8"));
|
|
5031
|
+
} catch {
|
|
5032
|
+
marker = null;
|
|
5033
|
+
}
|
|
5034
|
+
try {
|
|
5035
|
+
unlinkSync3(p);
|
|
5036
|
+
} catch {
|
|
5037
|
+
}
|
|
5038
|
+
if (!marker)
|
|
5039
|
+
return { marker: null };
|
|
5040
|
+
const gapMs = Math.max(0, now() - new Date(marker.ts).getTime());
|
|
5041
|
+
return { marker, gapMs };
|
|
5042
|
+
}
|
|
5043
|
+
function runningMarkerPath(stateRoot) {
|
|
5044
|
+
return join11(stateRoot, "running-marker.json");
|
|
5045
|
+
}
|
|
5046
|
+
function writeRunningMarker(stateRoot, marker, log) {
|
|
5047
|
+
try {
|
|
5048
|
+
writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
|
|
5049
|
+
} catch (e) {
|
|
5050
|
+
log(`running marker write failed: ${e.message}`);
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
function readRunningMarker(stateRoot) {
|
|
5054
|
+
try {
|
|
5055
|
+
return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
|
|
5056
|
+
} catch {
|
|
5057
|
+
return null;
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
function removeRunningMarker(stateRoot, log) {
|
|
5061
|
+
try {
|
|
5062
|
+
unlinkSync3(runningMarkerPath(stateRoot));
|
|
5063
|
+
} catch (e) {
|
|
5064
|
+
if (e.code !== "ENOENT")
|
|
5065
|
+
log(`running marker remove failed: ${e.message}`);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
var init_exit_marker = __esm({
|
|
5069
|
+
"packages/core/dist/daemon/exit-marker.js"() {
|
|
5070
|
+
}
|
|
5071
|
+
});
|
|
5072
|
+
|
|
4816
5073
|
// packages/core/dist/daemon/snapshot-gather.js
|
|
4817
5074
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4818
|
-
import { join as
|
|
4819
|
-
import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as
|
|
5075
|
+
import { join as join12 } from "path";
|
|
5076
|
+
import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
4820
5077
|
function distBuiltAt() {
|
|
4821
5078
|
try {
|
|
4822
5079
|
return statSync3(SELF_PATH).mtimeMs;
|
|
@@ -4869,7 +5126,7 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
4869
5126
|
for (const r of store.list(project))
|
|
4870
5127
|
byState[r.state] = (byState[r.state] ?? 0) + 1;
|
|
4871
5128
|
let corruptCount = 0;
|
|
4872
|
-
const dir =
|
|
5129
|
+
const dir = join12(stateRoot, project);
|
|
4873
5130
|
try {
|
|
4874
5131
|
for (const n of readdirSync3(dir)) {
|
|
4875
5132
|
if (n.includes(".corrupt.")) {
|
|
@@ -4879,7 +5136,7 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
4879
5136
|
if (!n.endsWith(".json"))
|
|
4880
5137
|
continue;
|
|
4881
5138
|
try {
|
|
4882
|
-
JSON.parse(
|
|
5139
|
+
JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
|
|
4883
5140
|
} catch {
|
|
4884
5141
|
corruptCount++;
|
|
4885
5142
|
}
|
|
@@ -4894,7 +5151,7 @@ function gatherResults(resultsDir) {
|
|
|
4894
5151
|
try {
|
|
4895
5152
|
for (const n of readdirSync3(resultsDir)) {
|
|
4896
5153
|
try {
|
|
4897
|
-
const s = statSync3(
|
|
5154
|
+
const s = statSync3(join12(resultsDir, n));
|
|
4898
5155
|
if (s.isFile()) {
|
|
4899
5156
|
fileCount++;
|
|
4900
5157
|
totalBytes += s.size;
|
|
@@ -4914,19 +5171,20 @@ var init_snapshot_gather = __esm({
|
|
|
4914
5171
|
});
|
|
4915
5172
|
|
|
4916
5173
|
// packages/core/dist/daemon/start.js
|
|
4917
|
-
import { join as
|
|
5174
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
4918
5175
|
import { readdir } from "fs/promises";
|
|
4919
5176
|
function startDaemon(ctx, opts, pkgVersion) {
|
|
4920
5177
|
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
4921
5178
|
const { daemonCmux } = ctx;
|
|
4922
5179
|
const probes = createProbes(ctx);
|
|
4923
|
-
const
|
|
5180
|
+
const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
|
|
5181
|
+
const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats, inFlightDelivery } = createDelivery(ctx, daemonCmux, surfaceProbe);
|
|
4924
5182
|
const baseNotify = opts.notify ?? defaultNotify;
|
|
4925
5183
|
const notify = ctx.telegramBridge ? async (args) => {
|
|
4926
5184
|
await baseNotify(args);
|
|
4927
5185
|
ctx.telegramBridge.pushLifecycle(args.project, args.event);
|
|
4928
5186
|
} : baseNotify;
|
|
4929
|
-
|
|
5187
|
+
ctx.notify = notify;
|
|
4930
5188
|
const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
|
|
4931
5189
|
const d = createDaemon({
|
|
4932
5190
|
store,
|
|
@@ -4990,7 +5248,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
4990
5248
|
return out;
|
|
4991
5249
|
}
|
|
4992
5250
|
async function gatherSnapshotInputs(now) {
|
|
4993
|
-
const logPath2 =
|
|
5251
|
+
const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
|
|
4994
5252
|
const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
|
|
4995
5253
|
const projects = await Promise.all(tier2Projects.map(async (project) => {
|
|
4996
5254
|
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
|
|
@@ -5083,6 +5341,33 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
5083
5341
|
})();
|
|
5084
5342
|
const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
|
|
5085
5343
|
log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
|
|
5344
|
+
const bootTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
5345
|
+
{
|
|
5346
|
+
const sendDownAlert = (minutes, reasonText) => {
|
|
5347
|
+
const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
|
|
5348
|
+
const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
|
|
5349
|
+
for (const project of alertProjects) {
|
|
5350
|
+
appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
|
|
5351
|
+
}
|
|
5352
|
+
};
|
|
5353
|
+
const { marker, gapMs } = consumeExitMarker(stateRoot);
|
|
5354
|
+
const prevRunning = readRunningMarker(stateRoot);
|
|
5355
|
+
if (marker) {
|
|
5356
|
+
log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
|
|
5357
|
+
if ((gapMs ?? 0) > 6e4)
|
|
5358
|
+
sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
|
|
5359
|
+
} else if (prevRunning) {
|
|
5360
|
+
const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
|
|
5361
|
+
const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
|
|
5362
|
+
log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
|
|
5363
|
+
if (uncleanGapMs > 6e4) {
|
|
5364
|
+
sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
|
|
5365
|
+
}
|
|
5366
|
+
} else {
|
|
5367
|
+
log("previous exit: none (clean or first boot)");
|
|
5368
|
+
}
|
|
5369
|
+
writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: bootTs }, log);
|
|
5370
|
+
}
|
|
5086
5371
|
let deliveryTick = initialDeliveryTick;
|
|
5087
5372
|
let probeTick;
|
|
5088
5373
|
if (daemonCmux) {
|
|
@@ -5123,9 +5408,10 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
5123
5408
|
keepCount: opts.mailboxConfig?.keepCount ?? 3
|
|
5124
5409
|
};
|
|
5125
5410
|
let rotationTimer;
|
|
5411
|
+
let rotationTick;
|
|
5126
5412
|
if (rotationInterval > 0) {
|
|
5127
|
-
const inboxPath =
|
|
5128
|
-
|
|
5413
|
+
const inboxPath = join13(stateRoot, "inbox");
|
|
5414
|
+
rotationTick = async () => {
|
|
5129
5415
|
try {
|
|
5130
5416
|
let entries;
|
|
5131
5417
|
try {
|
|
@@ -5138,13 +5424,23 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
5138
5424
|
await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
|
|
5139
5425
|
} catch (e) {
|
|
5140
5426
|
log(`rotation timer error: ${e.message}`);
|
|
5427
|
+
} finally {
|
|
5428
|
+
writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: (/* @__PURE__ */ new Date()).toISOString() }, log);
|
|
5141
5429
|
}
|
|
5430
|
+
};
|
|
5431
|
+
rotationTimer = setInterval(() => {
|
|
5432
|
+
void rotationTick();
|
|
5142
5433
|
}, rotationInterval);
|
|
5143
5434
|
rotationTimer.unref?.();
|
|
5144
5435
|
}
|
|
5145
5436
|
return {
|
|
5146
5437
|
stop(reason = "requested") {
|
|
5147
|
-
|
|
5438
|
+
const ppid = process.ppid;
|
|
5439
|
+
const uptimeMs = Math.round(process.uptime() * 1e3);
|
|
5440
|
+
const inFlight = inFlightDelivery();
|
|
5441
|
+
log(`exit pid=${process.pid} reason=${reason} ppid=${ppid} launchd=${ppid === 1} uptimeMs=${uptimeMs} inFlightDelivery=${inFlight ? `${inFlight.project}#${inFlight.seq}(defers=${inFlight.deferCount})` : "none"}`);
|
|
5442
|
+
writeExitMarker(stateRoot, { ts: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, reason, ppid, uptimeMs, inFlightDelivery: inFlight }, log);
|
|
5443
|
+
removeRunningMarker(stateRoot, log);
|
|
5148
5444
|
if (deliveryTimer)
|
|
5149
5445
|
clearInterval(deliveryTimer);
|
|
5150
5446
|
if (probeTimer)
|
|
@@ -5173,7 +5469,8 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
5173
5469
|
}));
|
|
5174
5470
|
},
|
|
5175
5471
|
tickDelivery: deliveryTick,
|
|
5176
|
-
tickProbe: probeTick
|
|
5472
|
+
tickProbe: probeTick,
|
|
5473
|
+
tickRotation: rotationTick
|
|
5177
5474
|
};
|
|
5178
5475
|
}
|
|
5179
5476
|
var CURSOR_SUBSCRIBER2, SNAPSHOT_LOG_WINDOW_MS;
|
|
@@ -5185,6 +5482,7 @@ var init_start = __esm({
|
|
|
5185
5482
|
init_gates();
|
|
5186
5483
|
init_server();
|
|
5187
5484
|
init_mailbox();
|
|
5485
|
+
init_exit_marker();
|
|
5188
5486
|
init_liveness2();
|
|
5189
5487
|
init_dist();
|
|
5190
5488
|
init_snapshot_gather();
|
|
@@ -6179,9 +6477,9 @@ var init_bridge = __esm({
|
|
|
6179
6477
|
|
|
6180
6478
|
// packages/core/dist/restart-daemon.js
|
|
6181
6479
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
6182
|
-
import { existsSync as
|
|
6480
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6183
6481
|
function defaultIsRunning() {
|
|
6184
|
-
return
|
|
6482
|
+
return existsSync10(DAEMON_SOCK_PATH);
|
|
6185
6483
|
}
|
|
6186
6484
|
function defaultRunKickstart() {
|
|
6187
6485
|
const uid = process.getuid?.() ?? 0;
|
|
@@ -6719,6 +7017,76 @@ var init_side_session = __esm({
|
|
|
6719
7017
|
}
|
|
6720
7018
|
});
|
|
6721
7019
|
|
|
7020
|
+
// packages/core/dist/crew-answer.js
|
|
7021
|
+
function describeOptions(options) {
|
|
7022
|
+
return options.map((o) => ` ${o.highlighted ? "\u276F" : " "} ${o.index}. ${o.label}`).join("\n");
|
|
7023
|
+
}
|
|
7024
|
+
function resolveOption(options, selector) {
|
|
7025
|
+
const trimmed = selector.trim();
|
|
7026
|
+
if (/^\d+$/.test(trimmed)) {
|
|
7027
|
+
const byIndex = options.find((o) => o.index === Number(trimmed));
|
|
7028
|
+
if (!byIndex) {
|
|
7029
|
+
throw new Error(`No option ${trimmed} in the visible prompt. Visible options:
|
|
7030
|
+
${describeOptions(options)}`);
|
|
7031
|
+
}
|
|
7032
|
+
return byIndex;
|
|
7033
|
+
}
|
|
7034
|
+
const lower = trimmed.toLowerCase();
|
|
7035
|
+
const exact = options.filter((o) => o.label.toLowerCase() === lower);
|
|
7036
|
+
if (exact.length === 1)
|
|
7037
|
+
return exact[0];
|
|
7038
|
+
if (exact.length > 1) {
|
|
7039
|
+
throw new Error(`Option text "${selector}" matches multiple options ambiguously:
|
|
7040
|
+
${describeOptions(exact)}`);
|
|
7041
|
+
}
|
|
7042
|
+
const prefix = options.filter((o) => o.label.toLowerCase().startsWith(lower));
|
|
7043
|
+
if (prefix.length === 1)
|
|
7044
|
+
return prefix[0];
|
|
7045
|
+
if (prefix.length > 1) {
|
|
7046
|
+
throw new Error(`Option text "${selector}" matches multiple options ambiguously:
|
|
7047
|
+
${describeOptions(prefix)}`);
|
|
7048
|
+
}
|
|
7049
|
+
throw new Error(`No option matches "${selector}". Visible options:
|
|
7050
|
+
${describeOptions(options)}`);
|
|
7051
|
+
}
|
|
7052
|
+
async function runCrewAnswer(project, name, option, runtime, workspaceId, deps, opts) {
|
|
7053
|
+
const crew = await findCrewPane(runtime, workspaceId, project, name);
|
|
7054
|
+
if (!crew) {
|
|
7055
|
+
throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
|
|
7056
|
+
}
|
|
7057
|
+
const options = await deps.readModalOptions(crew);
|
|
7058
|
+
if (!options) {
|
|
7059
|
+
throw new Error(`Crew '${name}' has no interactive option prompt visible right now \u2014 nothing to answer. Read its screen with 'squadrant crew read ${project} ${name}' to check its state.`);
|
|
7060
|
+
}
|
|
7061
|
+
const target = resolveOption(options, option);
|
|
7062
|
+
if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
|
|
7063
|
+
throw new Error(`Refusing: option ${target.index} is "${target.label}", which does not contain expected text "${opts.expect}". Option order is model-generated and can shift between renders \u2014 re-check with 'squadrant crew read ${project} ${name}'.
|
|
7064
|
+
Visible options:
|
|
7065
|
+
${describeOptions(options)}`);
|
|
7066
|
+
}
|
|
7067
|
+
const log = deps.log ?? (() => {
|
|
7068
|
+
});
|
|
7069
|
+
log(`\u2192 selecting ${target.index}. "${target.label}"`);
|
|
7070
|
+
const current = options.find((o) => o.highlighted) ?? options[0];
|
|
7071
|
+
const steps = target.index - current.index;
|
|
7072
|
+
const key = steps >= 0 ? "Down" : "Up";
|
|
7073
|
+
for (let i = 0; i < Math.abs(steps); i++) {
|
|
7074
|
+
await runtime.sendKeyToPane(crew, key);
|
|
7075
|
+
}
|
|
7076
|
+
await runtime.sendKeyToPane(crew, "Enter");
|
|
7077
|
+
if (opts?.text) {
|
|
7078
|
+
await runtime.pasteToPane(crew, opts.text);
|
|
7079
|
+
await runtime.sendKeyToPane(crew, "Enter");
|
|
7080
|
+
}
|
|
7081
|
+
const after = await deps.readModalOptions(crew);
|
|
7082
|
+
return { selected: target, closed: after === null };
|
|
7083
|
+
}
|
|
7084
|
+
var init_crew_answer = __esm({
|
|
7085
|
+
"packages/core/dist/crew-answer.js"() {
|
|
7086
|
+
init_crew_spawn();
|
|
7087
|
+
}
|
|
7088
|
+
});
|
|
7089
|
+
|
|
6722
7090
|
// packages/core/dist/lifecycle-source.js
|
|
6723
7091
|
function reduceLifecycle(prev, next) {
|
|
6724
7092
|
if (next.origin === "agent") {
|
|
@@ -6753,6 +7121,7 @@ __export(dist_exports2, {
|
|
|
6753
7121
|
DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
|
|
6754
7122
|
DEFAULT_TASK_TIMEOUT_MS: () => DEFAULT_TASK_TIMEOUT_MS,
|
|
6755
7123
|
DeferDelivery: () => DeferDelivery,
|
|
7124
|
+
FIRST_TURN_INLINE_MAX_BYTES: () => FIRST_TURN_INLINE_MAX_BYTES,
|
|
6756
7125
|
GROUP_DISPATCH_WARMUP_POLL_MS: () => GROUP_DISPATCH_WARMUP_POLL_MS,
|
|
6757
7126
|
GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
|
|
6758
7127
|
IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
|
|
@@ -6785,6 +7154,7 @@ __export(dist_exports2, {
|
|
|
6785
7154
|
classifyHealth: () => classifyHealth,
|
|
6786
7155
|
closeWorkItem: () => closeWorkItem,
|
|
6787
7156
|
computeTemplateHash: () => computeTemplateHash,
|
|
7157
|
+
consumeExitMarker: () => consumeExitMarker,
|
|
6788
7158
|
createAttach: () => createAttach,
|
|
6789
7159
|
createCrewPaneReader: () => createCrewPaneReader,
|
|
6790
7160
|
createDaemon: () => createDaemon,
|
|
@@ -6825,13 +7195,17 @@ __export(dist_exports2, {
|
|
|
6825
7195
|
encodeMsg: () => encodeMsg,
|
|
6826
7196
|
ensureDaemon: () => ensureDaemon,
|
|
6827
7197
|
evaluateStall: () => evaluateStall,
|
|
7198
|
+
exitMarkerPath: () => exitMarkerPath,
|
|
6828
7199
|
fallsBackToPane: () => fallsBackToPane,
|
|
7200
|
+
findCrewPane: () => findCrewPane,
|
|
6829
7201
|
findOpenChildren: () => findOpenChildren,
|
|
6830
7202
|
findProjectByThread: () => findProjectByThread,
|
|
6831
7203
|
findWorkItemById: () => findWorkItemById,
|
|
7204
|
+
forceKickstartAndVerify: () => forceKickstartAndVerify,
|
|
6832
7205
|
formatInbound: () => formatInbound,
|
|
6833
7206
|
formatInboundReceipt: () => formatInboundReceipt,
|
|
6834
7207
|
formatLifecycle: () => formatLifecycle,
|
|
7208
|
+
getDaemonPid: () => getDaemonPid,
|
|
6835
7209
|
healCmdFor: () => healCmdFor,
|
|
6836
7210
|
isAuthorized: () => isAuthorized,
|
|
6837
7211
|
isBareSpawn: () => isBareSpawn,
|
|
@@ -6866,6 +7240,7 @@ __export(dist_exports2, {
|
|
|
6866
7240
|
purgeExpiredWorkItems: () => purgeExpiredWorkItems,
|
|
6867
7241
|
readCursor: () => readCursor,
|
|
6868
7242
|
readFromCursor: () => readFromCursor,
|
|
7243
|
+
readRunningMarker: () => readRunningMarker,
|
|
6869
7244
|
reapCrewChildren: () => reapCrewChildren,
|
|
6870
7245
|
reapOrphanedCrews: () => reapOrphanedCrews,
|
|
6871
7246
|
reconcileLiveness: () => reconcileLiveness,
|
|
@@ -6874,6 +7249,7 @@ __export(dist_exports2, {
|
|
|
6874
7249
|
reduce: () => reduce,
|
|
6875
7250
|
reduceLifecycle: () => reduceLifecycle,
|
|
6876
7251
|
releaseDaemonLock: () => releaseDaemonLock,
|
|
7252
|
+
removeRunningMarker: () => removeRunningMarker,
|
|
6877
7253
|
renderPlist: () => renderPlist,
|
|
6878
7254
|
reregisterDaemon: () => reregisterDaemon,
|
|
6879
7255
|
resolveAgentBinDirs: () => resolveAgentBinDirs,
|
|
@@ -6885,6 +7261,7 @@ __export(dist_exports2, {
|
|
|
6885
7261
|
resolveSetupUserId: () => resolveSetupUserId,
|
|
6886
7262
|
restartDaemonIfRunning: () => restartDaemonIfRunning,
|
|
6887
7263
|
rotateIfNeeded: () => rotateIfNeeded,
|
|
7264
|
+
runCrewAnswer: () => runCrewAnswer,
|
|
6888
7265
|
runCrewClose: () => runCrewClose,
|
|
6889
7266
|
runCrewList: () => runCrewList,
|
|
6890
7267
|
runCrewRead: () => runCrewRead,
|
|
@@ -6904,6 +7281,7 @@ __export(dist_exports2, {
|
|
|
6904
7281
|
runTelegramPostSetup: () => runTelegramPostSetup,
|
|
6905
7282
|
runTelegramSend: () => runTelegramSend,
|
|
6906
7283
|
runTelegramStatus: () => runTelegramStatus,
|
|
7284
|
+
runningMarkerPath: () => runningMarkerPath,
|
|
6907
7285
|
sanitizePathForPlist: () => sanitizePathForPlist,
|
|
6908
7286
|
saveSessions: () => saveSessions,
|
|
6909
7287
|
saveState: () => saveState,
|
|
@@ -6929,6 +7307,8 @@ __export(dist_exports2, {
|
|
|
6929
7307
|
waitForCaptainDelivery: () => waitForCaptainDelivery,
|
|
6930
7308
|
waitForWarmup: () => waitForWarmup,
|
|
6931
7309
|
writeCursor: () => writeCursor,
|
|
7310
|
+
writeExitMarker: () => writeExitMarker,
|
|
7311
|
+
writeRunningMarker: () => writeRunningMarker,
|
|
6932
7312
|
writeTelegramConfig: () => writeTelegramConfig
|
|
6933
7313
|
});
|
|
6934
7314
|
var init_dist2 = __esm({
|
|
@@ -6951,6 +7331,7 @@ var init_dist2 = __esm({
|
|
|
6951
7331
|
init_attach();
|
|
6952
7332
|
init_start();
|
|
6953
7333
|
init_delivery_loop();
|
|
7334
|
+
init_exit_marker();
|
|
6954
7335
|
init_interactive_probe();
|
|
6955
7336
|
init_captain_delivery();
|
|
6956
7337
|
init_defer_delivery();
|
|
@@ -6964,6 +7345,7 @@ var init_dist2 = __esm({
|
|
|
6964
7345
|
init_launch_workspace();
|
|
6965
7346
|
init_side_session();
|
|
6966
7347
|
init_crew_spawn();
|
|
7348
|
+
init_crew_answer();
|
|
6967
7349
|
init_lifecycle_source();
|
|
6968
7350
|
init_control_channel();
|
|
6969
7351
|
init_captain_channel();
|
|
@@ -7120,6 +7502,35 @@ function hasModalOptionList(screen) {
|
|
|
7120
7502
|
return false;
|
|
7121
7503
|
return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
|
|
7122
7504
|
}
|
|
7505
|
+
function parseModalOptions(screen) {
|
|
7506
|
+
if (!hasModalOptionList(screen))
|
|
7507
|
+
return null;
|
|
7508
|
+
const lines = screen.split(/\r?\n/);
|
|
7509
|
+
const HR_RE = /^\s*─{10,}\s*$/;
|
|
7510
|
+
let bottomHR = -1;
|
|
7511
|
+
let topHR = -1;
|
|
7512
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
7513
|
+
if (HR_RE.test(lines[i])) {
|
|
7514
|
+
if (bottomHR === -1)
|
|
7515
|
+
bottomHR = i;
|
|
7516
|
+
else {
|
|
7517
|
+
topHR = i;
|
|
7518
|
+
break;
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
if (topHR === -1)
|
|
7523
|
+
return null;
|
|
7524
|
+
const OPTION_RE3 = /^\s*(❯)?\s*(\d+)\.\s*(.*?)\s*$/;
|
|
7525
|
+
const options = [];
|
|
7526
|
+
for (const line of lines.slice(topHR + 1, bottomHR)) {
|
|
7527
|
+
const m = line.match(OPTION_RE3);
|
|
7528
|
+
if (!m)
|
|
7529
|
+
continue;
|
|
7530
|
+
options.push({ index: Number(m[2]), label: m[3], highlighted: m[1] === "\u276F" });
|
|
7531
|
+
}
|
|
7532
|
+
return options.length > 0 ? options : null;
|
|
7533
|
+
}
|
|
7123
7534
|
function readInputBoxRaw(screen, opts) {
|
|
7124
7535
|
if (!screen)
|
|
7125
7536
|
return null;
|
|
@@ -7366,7 +7777,10 @@ function createCmuxDriver() {
|
|
|
7366
7777
|
let screen = "";
|
|
7367
7778
|
try {
|
|
7368
7779
|
screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
|
|
7369
|
-
} catch {
|
|
7780
|
+
} catch (e) {
|
|
7781
|
+
process.stderr.write(`[squadrant] read-screen failed for ${ws}/${sf}: ${e.message}
|
|
7782
|
+
`);
|
|
7783
|
+
throw new DeferDelivery(null, "probe-failed");
|
|
7370
7784
|
}
|
|
7371
7785
|
const draft = parseDraftFromScreen(screen);
|
|
7372
7786
|
if (draft === null)
|
|
@@ -7616,7 +8030,7 @@ var init_notifiers = __esm({
|
|
|
7616
8030
|
|
|
7617
8031
|
// packages/workspaces/dist/workspaces/obsidian.js
|
|
7618
8032
|
import fs13 from "fs/promises";
|
|
7619
|
-
import { existsSync as
|
|
8033
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7620
8034
|
import path13 from "path";
|
|
7621
8035
|
function resolveInRoot(root, relative) {
|
|
7622
8036
|
const joined = path13.resolve(root, relative);
|
|
@@ -7636,7 +8050,7 @@ function createObsidianDriver(scope) {
|
|
|
7636
8050
|
async probe() {
|
|
7637
8051
|
return {
|
|
7638
8052
|
installed: true,
|
|
7639
|
-
rootExists:
|
|
8053
|
+
rootExists: existsSync11(root)
|
|
7640
8054
|
};
|
|
7641
8055
|
},
|
|
7642
8056
|
async read(rel) {
|
|
@@ -7834,6 +8248,16 @@ var init_events_bridge = __esm({
|
|
|
7834
8248
|
}
|
|
7835
8249
|
if (f?.type !== "event" || f.category !== "agent")
|
|
7836
8250
|
return;
|
|
8251
|
+
if (f.name === "agent.hook.PostToolUse") {
|
|
8252
|
+
const p2 = f.payload ?? {};
|
|
8253
|
+
if (p2.phase === "received")
|
|
8254
|
+
return;
|
|
8255
|
+
const rec2 = this.deps.resolve({ cwd: p2.cwd, source: p2._source ?? f.source, sessionId: p2.session_id });
|
|
8256
|
+
if (!rec2)
|
|
8257
|
+
return;
|
|
8258
|
+
this.deps.emit({ type: "task.progress", id: rec2.id, note: f.name });
|
|
8259
|
+
return;
|
|
8260
|
+
}
|
|
7837
8261
|
const runState = f.name ? deriveRunState(f.name) : null;
|
|
7838
8262
|
if (!runState)
|
|
7839
8263
|
return;
|
|
@@ -7960,11 +8384,11 @@ var init_store_fingerprint = __esm({
|
|
|
7960
8384
|
});
|
|
7961
8385
|
|
|
7962
8386
|
// packages/workspaces/dist/cmux-daemon/daemon-cmux.js
|
|
7963
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
7964
|
-
import { join as
|
|
8387
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
|
|
8388
|
+
import { join as join14 } from "path";
|
|
7965
8389
|
import { homedir as homedir7 } from "os";
|
|
7966
8390
|
async function readCmuxLiveness() {
|
|
7967
|
-
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ??
|
|
8391
|
+
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir7(), ".cmuxterm");
|
|
7968
8392
|
const projects = loadConfig().projects;
|
|
7969
8393
|
let files;
|
|
7970
8394
|
try {
|
|
@@ -7972,7 +8396,7 @@ async function readCmuxLiveness() {
|
|
|
7972
8396
|
} catch (e) {
|
|
7973
8397
|
throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
|
|
7974
8398
|
}
|
|
7975
|
-
return readLivenessSnapshot(files, (f) =>
|
|
8399
|
+
return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
|
|
7976
8400
|
}
|
|
7977
8401
|
var DaemonCmux;
|
|
7978
8402
|
var init_daemon_cmux = __esm({
|
|
@@ -8043,9 +8467,9 @@ var init_daemon_cmux = __esm({
|
|
|
8043
8467
|
});
|
|
8044
8468
|
|
|
8045
8469
|
// packages/workspaces/dist/cmux-daemon/cmux-store-source.js
|
|
8046
|
-
import { join as
|
|
8470
|
+
import { join as join15 } from "path";
|
|
8047
8471
|
import { homedir as homedir8 } from "os";
|
|
8048
|
-
import { watch, readdirSync as readdirSync5, readFileSync as
|
|
8472
|
+
import { watch, readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
|
|
8049
8473
|
function parseLifecycleState(s) {
|
|
8050
8474
|
if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
|
|
8051
8475
|
return s;
|
|
@@ -8069,7 +8493,7 @@ function defaultListFiles(dir) {
|
|
|
8069
8493
|
}
|
|
8070
8494
|
function defaultReadFile(path36) {
|
|
8071
8495
|
try {
|
|
8072
|
-
return
|
|
8496
|
+
return readFileSync11(path36, "utf-8");
|
|
8073
8497
|
} catch {
|
|
8074
8498
|
return void 0;
|
|
8075
8499
|
}
|
|
@@ -8105,12 +8529,12 @@ var init_cmux_store_source = __esm({
|
|
|
8105
8529
|
active = false;
|
|
8106
8530
|
lastError = null;
|
|
8107
8531
|
constructor(opts = {}) {
|
|
8108
|
-
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ??
|
|
8532
|
+
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir8(), ".cmuxterm");
|
|
8109
8533
|
this.debounceMs = opts.debounceMs ?? 50;
|
|
8110
8534
|
this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
|
|
8111
8535
|
this.listFiles = opts.listFiles ?? defaultListFiles;
|
|
8112
8536
|
this.readFile = opts.readFile ?? defaultReadFile;
|
|
8113
|
-
this.fileExists = opts.fileExists ??
|
|
8537
|
+
this.fileExists = opts.fileExists ?? existsSync12;
|
|
8114
8538
|
this.watchDir = opts.watchDir ?? defaultWatchDir;
|
|
8115
8539
|
this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
|
|
8116
8540
|
this.cancelTimer = opts.cancelTimer ?? clearTimeout;
|
|
@@ -8167,7 +8591,7 @@ var init_cmux_store_source = __esm({
|
|
|
8167
8591
|
}
|
|
8168
8592
|
scanFile(filename) {
|
|
8169
8593
|
const deps = this.deps;
|
|
8170
|
-
const filePath =
|
|
8594
|
+
const filePath = join15(this.stateDir, filename);
|
|
8171
8595
|
const lockPath = `${filePath}.lock`;
|
|
8172
8596
|
if (this.fileExists(lockPath)) {
|
|
8173
8597
|
this.log(`cmux-store: skipping ${filename} (locked)`);
|
|
@@ -8221,11 +8645,11 @@ var init_cmux_store_source = __esm({
|
|
|
8221
8645
|
});
|
|
8222
8646
|
|
|
8223
8647
|
// packages/workspaces/dist/native-hooks/native-hook-source.js
|
|
8224
|
-
import { join as
|
|
8648
|
+
import { join as join16 } from "path";
|
|
8225
8649
|
import { homedir as homedir9 } from "os";
|
|
8226
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
8650
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
8227
8651
|
function installClaudeHooks(opts = {}) {
|
|
8228
|
-
const settingsPath = opts.settingsPath ??
|
|
8652
|
+
const settingsPath = opts.settingsPath ?? join16(homedir9(), ".claude", "settings.json");
|
|
8229
8653
|
const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
|
|
8230
8654
|
const readFile7 = opts.readFile ?? defaultReadFile2;
|
|
8231
8655
|
const writeFile6 = opts.writeFile ?? defaultWriteFile;
|
|
@@ -8321,14 +8745,14 @@ function extractDetail(sub, payload) {
|
|
|
8321
8745
|
}
|
|
8322
8746
|
function defaultReadFile2(path36) {
|
|
8323
8747
|
try {
|
|
8324
|
-
return
|
|
8748
|
+
return readFileSync12(path36, "utf-8");
|
|
8325
8749
|
} catch {
|
|
8326
8750
|
return void 0;
|
|
8327
8751
|
}
|
|
8328
8752
|
}
|
|
8329
8753
|
function defaultWriteFile(path36, content) {
|
|
8330
8754
|
mkdirSync6(path36.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
8331
|
-
|
|
8755
|
+
writeFileSync9(path36, content, "utf-8");
|
|
8332
8756
|
}
|
|
8333
8757
|
var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
|
|
8334
8758
|
var init_native_hook_source = __esm({
|
|
@@ -8474,6 +8898,10 @@ async function paneHasOpenModal(runtime, pane) {
|
|
|
8474
8898
|
const screen = await runtime.readPaneScreen(pane) ?? "";
|
|
8475
8899
|
return hasModalOptionList(screen);
|
|
8476
8900
|
}
|
|
8901
|
+
async function readModalOptions(runtime, pane) {
|
|
8902
|
+
const screen = await runtime.readPaneScreen(pane) ?? "";
|
|
8903
|
+
return parseModalOptions(screen);
|
|
8904
|
+
}
|
|
8477
8905
|
async function confirmedSendToPane(runtime, pane, message) {
|
|
8478
8906
|
const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
|
|
8479
8907
|
if (hasModalOptionList(preSendScreen)) {
|
|
@@ -8629,6 +9057,7 @@ __export(dist_exports3, {
|
|
|
8629
9057
|
mapSubToLifecycle: () => mapSubToLifecycle,
|
|
8630
9058
|
paneHasOpenModal: () => paneHasOpenModal,
|
|
8631
9059
|
readCmuxLiveness: () => readCmuxLiveness,
|
|
9060
|
+
readModalOptions: () => readModalOptions,
|
|
8632
9061
|
resendCrewFirstTurn: () => resendCrewFirstTurn,
|
|
8633
9062
|
resolveCaptainWorkspace: () => resolveCaptainWorkspace,
|
|
8634
9063
|
sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
|
|
@@ -8680,6 +9109,9 @@ function createClaudeDriver() {
|
|
|
8680
9109
|
if (opts.model) {
|
|
8681
9110
|
cmd += ` --model ${opts.model}`;
|
|
8682
9111
|
}
|
|
9112
|
+
if (opts.thinking) {
|
|
9113
|
+
cmd += ` --effort ${opts.thinking}`;
|
|
9114
|
+
}
|
|
8683
9115
|
if (opts.autoApprove) {
|
|
8684
9116
|
cmd += " --dangerously-skip-permissions";
|
|
8685
9117
|
} else if (opts.permissionMode) {
|
|
@@ -8956,7 +9388,7 @@ var init_registry4 = __esm({
|
|
|
8956
9388
|
// packages/agents/dist/drivers/launch-cmd.js
|
|
8957
9389
|
import fs14 from "fs";
|
|
8958
9390
|
import path14 from "path";
|
|
8959
|
-
function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName) {
|
|
9391
|
+
function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking) {
|
|
8960
9392
|
const driver = registry.getDriver(agentName);
|
|
8961
9393
|
if (driver.name === "claude") {
|
|
8962
9394
|
let cmd = fresh ? "claude" : "claude -c";
|
|
@@ -8973,6 +9405,9 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
|
|
|
8973
9405
|
if (model) {
|
|
8974
9406
|
cmd += ` --model ${model}`;
|
|
8975
9407
|
}
|
|
9408
|
+
if (thinking) {
|
|
9409
|
+
cmd += ` --effort ${thinking}`;
|
|
9410
|
+
}
|
|
8976
9411
|
if (templatesDir) {
|
|
8977
9412
|
const roleFile2 = path14.join(templatesDir, `${role}.claude.md`);
|
|
8978
9413
|
const legacyRoleFile = path14.join(templatesDir, `${role}.CLAUDE.md`);
|
|
@@ -9753,10 +10188,10 @@ var init_codex_app_server_source = __esm({
|
|
|
9753
10188
|
// packages/agents/dist/codex/config.js
|
|
9754
10189
|
import { readFile as readFile6 } from "fs/promises";
|
|
9755
10190
|
import { homedir as homedir10 } from "os";
|
|
9756
|
-
import { join as
|
|
10191
|
+
import { join as join17 } from "path";
|
|
9757
10192
|
async function resolveCodexModel() {
|
|
9758
|
-
const home = process.env["CODEX_HOME"] ??
|
|
9759
|
-
const configPath =
|
|
10193
|
+
const home = process.env["CODEX_HOME"] ?? join17(homedir10(), ".codex");
|
|
10194
|
+
const configPath = join17(home, "config.toml");
|
|
9760
10195
|
let text;
|
|
9761
10196
|
try {
|
|
9762
10197
|
text = await readFile6(configPath, "utf8");
|
|
@@ -10277,9 +10712,9 @@ var init_sse_bridge = __esm({
|
|
|
10277
10712
|
|
|
10278
10713
|
// packages/agents/dist/interactive/claude.js
|
|
10279
10714
|
import { execSync as execSync7 } from "child_process";
|
|
10280
|
-
import { readFileSync as
|
|
10715
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
10281
10716
|
import { homedir as homedir11 } from "os";
|
|
10282
|
-
import { join as
|
|
10717
|
+
import { join as join18 } from "path";
|
|
10283
10718
|
function probeClaudeSettingsFlag() {
|
|
10284
10719
|
try {
|
|
10285
10720
|
const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -10337,11 +10772,11 @@ function deriveTranscriptPath(sessionId, cwd) {
|
|
|
10337
10772
|
if (!sessionId || !cwd)
|
|
10338
10773
|
return null;
|
|
10339
10774
|
const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
10340
|
-
return
|
|
10775
|
+
return join18(homedir11(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
|
|
10341
10776
|
}
|
|
10342
10777
|
function readLastAssistantText(transcriptPath) {
|
|
10343
10778
|
try {
|
|
10344
|
-
const raw =
|
|
10779
|
+
const raw = readFileSync13(transcriptPath, "utf-8");
|
|
10345
10780
|
const lines = raw.split(/\r?\n/);
|
|
10346
10781
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
10347
10782
|
const line = lines[i].trim();
|
|
@@ -10407,6 +10842,29 @@ function formatAskUserQuestionPrompt(toolInput) {
|
|
|
10407
10842
|
}
|
|
10408
10843
|
return parts.length > 0 ? parts.join(" | ") : null;
|
|
10409
10844
|
}
|
|
10845
|
+
function decideCaptainMemoryWrite(toolName, toolInput, env, homeDir) {
|
|
10846
|
+
if (!env.SQUADRANT_CREW_TASK_ID)
|
|
10847
|
+
return { decision: "allow" };
|
|
10848
|
+
if (toolName === "Bash") {
|
|
10849
|
+
const command = toolInput?.command;
|
|
10850
|
+
if (typeof command === "string" && CAPTAIN_MEMORY_PATH_RE.test(command)) {
|
|
10851
|
+
return { decision: "deny", reason: DENY_REASON };
|
|
10852
|
+
}
|
|
10853
|
+
return { decision: "allow" };
|
|
10854
|
+
}
|
|
10855
|
+
const field = FILE_PATH_FIELD_BY_TOOL[toolName];
|
|
10856
|
+
if (!field)
|
|
10857
|
+
return { decision: "allow" };
|
|
10858
|
+
const filePath = toolInput?.[field];
|
|
10859
|
+
if (typeof filePath !== "string" || !filePath)
|
|
10860
|
+
return { decision: "allow" };
|
|
10861
|
+
const home = homeDir.endsWith("/") ? homeDir.slice(0, -1) : homeDir;
|
|
10862
|
+
if (!filePath.startsWith(home))
|
|
10863
|
+
return { decision: "allow" };
|
|
10864
|
+
if (!CAPTAIN_MEMORY_PATH_RE.test(filePath))
|
|
10865
|
+
return { decision: "allow" };
|
|
10866
|
+
return { decision: "deny", reason: DENY_REASON };
|
|
10867
|
+
}
|
|
10410
10868
|
function mapClaudeHookToEvent(event, payload, taskId) {
|
|
10411
10869
|
switch (event) {
|
|
10412
10870
|
case "PreToolUse": {
|
|
@@ -10442,7 +10900,7 @@ function mapClaudeHookToEvent(event, payload, taskId) {
|
|
|
10442
10900
|
return null;
|
|
10443
10901
|
}
|
|
10444
10902
|
}
|
|
10445
|
-
var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, claudeInteractive;
|
|
10903
|
+
var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, CAPTAIN_MEMORY_PATH_RE, DENY_REASON, FILE_PATH_FIELD_BY_TOOL, claudeInteractive;
|
|
10446
10904
|
var init_claude3 = __esm({
|
|
10447
10905
|
"packages/agents/dist/interactive/claude.js"() {
|
|
10448
10906
|
EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
|
|
@@ -10450,6 +10908,14 @@ var init_claude3 = __esm({
|
|
|
10450
10908
|
["PreToolUse", "AskUserQuestion"]
|
|
10451
10909
|
];
|
|
10452
10910
|
nextAskUserQuestionRequestId = Date.now();
|
|
10911
|
+
CAPTAIN_MEMORY_PATH_RE = /\.claude\/projects\/.*\/memory\//;
|
|
10912
|
+
DENY_REASON = "Crews do not write captain memory. Put the finding in your done/blocked message; the captain decides what is durable (#556).";
|
|
10913
|
+
FILE_PATH_FIELD_BY_TOOL = {
|
|
10914
|
+
Write: "file_path",
|
|
10915
|
+
Edit: "file_path",
|
|
10916
|
+
MultiEdit: "file_path",
|
|
10917
|
+
NotebookEdit: "notebook_path"
|
|
10918
|
+
};
|
|
10453
10919
|
claudeInteractive = {
|
|
10454
10920
|
provider: "claude",
|
|
10455
10921
|
tier: "strong",
|
|
@@ -10505,8 +10971,11 @@ function classifyPaneTail2(tail) {
|
|
|
10505
10971
|
if (q)
|
|
10506
10972
|
return { kind: "question", text: q };
|
|
10507
10973
|
let errLine = null;
|
|
10508
|
-
for (
|
|
10509
|
-
|
|
10974
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
10975
|
+
const c = cleaned[i];
|
|
10976
|
+
if (c == null || isQuotedLine2(raw[i]))
|
|
10977
|
+
continue;
|
|
10978
|
+
if (ERROR_BANNER_RE2.some((re) => re.test(c)))
|
|
10510
10979
|
errLine = c;
|
|
10511
10980
|
}
|
|
10512
10981
|
if (errLine) {
|
|
@@ -10518,6 +10987,10 @@ function classifyPaneTail2(tail) {
|
|
|
10518
10987
|
}
|
|
10519
10988
|
return null;
|
|
10520
10989
|
}
|
|
10990
|
+
function isQuotedLine2(raw) {
|
|
10991
|
+
const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
|
|
10992
|
+
return QUOTED_PREFIX_RE2.test(noAnsi);
|
|
10993
|
+
}
|
|
10521
10994
|
function stripChrome2(raw) {
|
|
10522
10995
|
let line = raw.replace(/\[[0-9;]*m/g, "");
|
|
10523
10996
|
line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
|
|
@@ -10532,7 +11005,7 @@ function stripChrome2(raw) {
|
|
|
10532
11005
|
return null;
|
|
10533
11006
|
return trimmed;
|
|
10534
11007
|
}
|
|
10535
|
-
var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
|
|
11008
|
+
var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, QUOTED_PREFIX_RE2, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
|
|
10536
11009
|
var init_pane_classifier = __esm({
|
|
10537
11010
|
"packages/agents/dist/interactive/pane-classifier.js"() {
|
|
10538
11011
|
init_claude3();
|
|
@@ -10547,6 +11020,7 @@ var init_pane_classifier = __esm({
|
|
|
10547
11020
|
];
|
|
10548
11021
|
RETRYING_RE = /\bRetrying\b|\battempt\s+\d+\s*\/\s*\d+/i;
|
|
10549
11022
|
EXHAUSTED_RE = /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b|\bmaximum\s+retries\b/i;
|
|
11023
|
+
QUOTED_PREFIX_RE2 = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
|
|
10550
11024
|
OPTION_RE2 = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
|
|
10551
11025
|
PICKER_FOOTER_RE2 = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
|
|
10552
11026
|
PURE_CHROME_RE2 = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
|
|
@@ -10776,7 +11250,7 @@ var init_headless_launcher = __esm({
|
|
|
10776
11250
|
|
|
10777
11251
|
// packages/agents/dist/claude/registry.js
|
|
10778
11252
|
import fs15 from "fs";
|
|
10779
|
-
import { join as
|
|
11253
|
+
import { join as join19 } from "path";
|
|
10780
11254
|
import { homedir as homedir12 } from "os";
|
|
10781
11255
|
function parseRegistryDir(files, readFile7) {
|
|
10782
11256
|
const out = [];
|
|
@@ -10824,7 +11298,7 @@ function readClaudeStatus(task) {
|
|
|
10824
11298
|
} catch {
|
|
10825
11299
|
return void 0;
|
|
10826
11300
|
}
|
|
10827
|
-
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(
|
|
11301
|
+
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
|
|
10828
11302
|
let entry;
|
|
10829
11303
|
if (task.messagingSocketPath) {
|
|
10830
11304
|
entry = entries.find((e) => e.messagingSocketPath === task.messagingSocketPath);
|
|
@@ -10843,7 +11317,7 @@ function readClaudeStatusByCwd(cwd) {
|
|
|
10843
11317
|
} catch {
|
|
10844
11318
|
return void 0;
|
|
10845
11319
|
}
|
|
10846
|
-
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(
|
|
11320
|
+
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
|
|
10847
11321
|
const entry = entries.find((e) => e.cwd === cwd);
|
|
10848
11322
|
if (!entry)
|
|
10849
11323
|
return void 0;
|
|
@@ -10856,7 +11330,7 @@ function readClaudeStatusBySocketPath(socketPath2) {
|
|
|
10856
11330
|
} catch {
|
|
10857
11331
|
return void 0;
|
|
10858
11332
|
}
|
|
10859
|
-
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(
|
|
11333
|
+
const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
|
|
10860
11334
|
const entry = entries.find((e) => e.messagingSocketPath === socketPath2);
|
|
10861
11335
|
if (!entry)
|
|
10862
11336
|
return void 0;
|
|
@@ -10865,14 +11339,14 @@ function readClaudeStatusBySocketPath(socketPath2) {
|
|
|
10865
11339
|
var CLAUDE_SESSIONS_DIR, PID_JSON;
|
|
10866
11340
|
var init_registry8 = __esm({
|
|
10867
11341
|
"packages/agents/dist/claude/registry.js"() {
|
|
10868
|
-
CLAUDE_SESSIONS_DIR =
|
|
11342
|
+
CLAUDE_SESSIONS_DIR = join19(homedir12(), ".claude", "sessions");
|
|
10869
11343
|
PID_JSON = /^(\d+)\.json$/;
|
|
10870
11344
|
}
|
|
10871
11345
|
});
|
|
10872
11346
|
|
|
10873
11347
|
// packages/agents/dist/claude/peer-registry-source.js
|
|
10874
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
10875
|
-
import { join as
|
|
11348
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14 } from "fs";
|
|
11349
|
+
import { join as join20 } from "path";
|
|
10876
11350
|
function defaultIsAlive(pid) {
|
|
10877
11351
|
try {
|
|
10878
11352
|
process.kill(pid, 0);
|
|
@@ -10901,7 +11375,7 @@ var init_peer_registry_source = __esm({
|
|
|
10901
11375
|
log;
|
|
10902
11376
|
constructor(o = {}) {
|
|
10903
11377
|
this.readdir = o.readdir ?? (() => readdirSync6(CLAUDE_SESSIONS_DIR));
|
|
10904
|
-
this.readFile = o.readFile ?? ((n) =>
|
|
11378
|
+
this.readFile = o.readFile ?? ((n) => readFileSync14(join20(CLAUDE_SESSIONS_DIR, n), "utf8"));
|
|
10905
11379
|
this.isAlive = o.isAlive ?? defaultIsAlive;
|
|
10906
11380
|
this.now = o.now ?? Date.now;
|
|
10907
11381
|
this.pollMs = o.pollMs ?? 2e3;
|
|
@@ -11447,6 +11921,7 @@ __export(dist_exports4, {
|
|
|
11447
11921
|
createGeminiEmitter: () => createGeminiEmitter,
|
|
11448
11922
|
createOpencodeDriver: () => createOpencodeDriver,
|
|
11449
11923
|
createOpencodeEmitter: () => createOpencodeEmitter,
|
|
11924
|
+
decideCaptainMemoryWrite: () => decideCaptainMemoryWrite,
|
|
11450
11925
|
deriveTranscriptPath: () => deriveTranscriptPath,
|
|
11451
11926
|
detectTrailingQuestion: () => detectTrailingQuestion2,
|
|
11452
11927
|
formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
|
|
@@ -11568,9 +12043,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
|
|
|
11568
12043
|
const resolved = resolveTarget(registry, config, target, !!opts.command);
|
|
11569
12044
|
await needRef(resolved);
|
|
11570
12045
|
const finalProject = opts.command ? config.commandName : target;
|
|
11571
|
-
const { join:
|
|
12046
|
+
const { join: join30, dirname: dirname9 } = await import("path");
|
|
11572
12047
|
const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
11573
|
-
const stateRoot =
|
|
12048
|
+
const stateRoot = join30(dirname9(DEFAULT_CONFIG_PATH2), "state");
|
|
11574
12049
|
const seq = await appendCaptainMessage2({
|
|
11575
12050
|
stateRoot,
|
|
11576
12051
|
project: finalProject,
|
|
@@ -11676,10 +12151,10 @@ var init_runtime2 = __esm({
|
|
|
11676
12151
|
init_dist();
|
|
11677
12152
|
init_dist2();
|
|
11678
12153
|
import { Command as Command35 } from "commander";
|
|
11679
|
-
import { existsSync as
|
|
12154
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
|
|
11680
12155
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
11681
|
-
import { dirname as dirname8, join as
|
|
11682
|
-
import { homedir as
|
|
12156
|
+
import { dirname as dirname8, join as join29 } from "path";
|
|
12157
|
+
import { homedir as homedir18 } from "os";
|
|
11683
12158
|
|
|
11684
12159
|
// packages/cli/src/commands/doctor.ts
|
|
11685
12160
|
init_dist();
|
|
@@ -12085,8 +12560,8 @@ import chalk4 from "chalk";
|
|
|
12085
12560
|
|
|
12086
12561
|
// packages/cli/src/lib/per-crew-settings.ts
|
|
12087
12562
|
init_dist4();
|
|
12088
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
12089
|
-
import { dirname as dirname4, join as
|
|
12563
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
12564
|
+
import { dirname as dirname4, join as join21 } from "path";
|
|
12090
12565
|
import { homedir as homedir13 } from "os";
|
|
12091
12566
|
var CREW_PERMISSION_ALLOWLIST = [
|
|
12092
12567
|
// git — read + safe mutations (reset/clean/config intentionally excluded)
|
|
@@ -12178,21 +12653,21 @@ function mergeCrewPermissions(settings) {
|
|
|
12178
12653
|
return next;
|
|
12179
12654
|
}
|
|
12180
12655
|
function writePerCrewSettingsLocal(o) {
|
|
12181
|
-
const dir =
|
|
12656
|
+
const dir = join21(o.projectCwd, ".claude");
|
|
12182
12657
|
mkdirSync7(dir, { recursive: true });
|
|
12183
|
-
const file =
|
|
12658
|
+
const file = join21(dir, "settings.local.json");
|
|
12184
12659
|
let existing = {};
|
|
12185
12660
|
try {
|
|
12186
|
-
const raw = healStaleCockpitRefs(
|
|
12661
|
+
const raw = healStaleCockpitRefs(readFileSync15(file, "utf-8"));
|
|
12187
12662
|
existing = JSON.parse(raw);
|
|
12188
12663
|
} catch {
|
|
12189
12664
|
}
|
|
12190
12665
|
const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
|
|
12191
12666
|
const merged = mergeCrewPermissions(withHooks);
|
|
12192
|
-
|
|
12667
|
+
writeFileSync10(file, JSON.stringify(merged, null, 2));
|
|
12193
12668
|
return file;
|
|
12194
12669
|
}
|
|
12195
|
-
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH =
|
|
12670
|
+
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join21(homedir13(), ".config", "opencode", "opencode.json");
|
|
12196
12671
|
function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
|
|
12197
12672
|
mkdirSync7(dirname4(configPath), { recursive: true });
|
|
12198
12673
|
const defaultConfig = {
|
|
@@ -12200,7 +12675,7 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
|
|
|
12200
12675
|
model: "anthropic/claude-sonnet-4-5"
|
|
12201
12676
|
};
|
|
12202
12677
|
try {
|
|
12203
|
-
|
|
12678
|
+
writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
|
|
12204
12679
|
return configPath;
|
|
12205
12680
|
} catch (err) {
|
|
12206
12681
|
if (err.code === "EEXIST") return null;
|
|
@@ -12209,16 +12684,16 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
|
|
|
12209
12684
|
}
|
|
12210
12685
|
function readGlobalOpencodeModel(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
|
|
12211
12686
|
try {
|
|
12212
|
-
const parsed = JSON.parse(
|
|
12687
|
+
const parsed = JSON.parse(readFileSync15(configPath, "utf-8"));
|
|
12213
12688
|
return typeof parsed.model === "string" ? parsed.model : void 0;
|
|
12214
12689
|
} catch {
|
|
12215
12690
|
return void 0;
|
|
12216
12691
|
}
|
|
12217
12692
|
}
|
|
12218
12693
|
function writePerCrewOpencodeConfig(o) {
|
|
12219
|
-
const dir =
|
|
12694
|
+
const dir = join21(o.stateRoot, o.project, o.taskId);
|
|
12220
12695
|
mkdirSync7(dir, { recursive: true });
|
|
12221
|
-
const file =
|
|
12696
|
+
const file = join21(dir, "opencode.json");
|
|
12222
12697
|
const config = {
|
|
12223
12698
|
permission: {
|
|
12224
12699
|
read: "allow",
|
|
@@ -12233,7 +12708,7 @@ function writePerCrewOpencodeConfig(o) {
|
|
|
12233
12708
|
external_directory: { "**": "allow" }
|
|
12234
12709
|
}
|
|
12235
12710
|
};
|
|
12236
|
-
|
|
12711
|
+
writeFileSync10(file, JSON.stringify(config, null, 2));
|
|
12237
12712
|
return file;
|
|
12238
12713
|
}
|
|
12239
12714
|
|
|
@@ -12721,8 +13196,8 @@ import { createConnection as createConnection3 } from "net";
|
|
|
12721
13196
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
12722
13197
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
12723
13198
|
import { homedir as homedir14 } from "os";
|
|
12724
|
-
import { join as
|
|
12725
|
-
import { mkdirSync as mkdirSync8, writeFileSync as
|
|
13199
|
+
import { join as join22 } from "path";
|
|
13200
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
12726
13201
|
|
|
12727
13202
|
// packages/cli/src/commands/crew-output.ts
|
|
12728
13203
|
function tailLines(text, maxLines = 40, maxBytes = 4096) {
|
|
@@ -13180,10 +13655,10 @@ function buildSignalRequest(signal, o) {
|
|
|
13180
13655
|
return { kind: "event", project, event };
|
|
13181
13656
|
}
|
|
13182
13657
|
function defaultWriteResult(id, payload) {
|
|
13183
|
-
const dir =
|
|
13658
|
+
const dir = join22(homedir14(), ".config", "squadrant", "state", "_results");
|
|
13184
13659
|
mkdirSync8(dir, { recursive: true });
|
|
13185
|
-
const file =
|
|
13186
|
-
|
|
13660
|
+
const file = join22(dir, `${id}.txt`);
|
|
13661
|
+
writeFileSync11(file, payload);
|
|
13187
13662
|
return file;
|
|
13188
13663
|
}
|
|
13189
13664
|
async function runCrewSignal(signal, o, deps) {
|
|
@@ -13196,7 +13671,7 @@ async function runCrewSignal(signal, o, deps) {
|
|
|
13196
13671
|
const current = await deps.call(buildStatusRequest(project, taskId));
|
|
13197
13672
|
if (current && TERMINAL_STATES.has(current.state)) {
|
|
13198
13673
|
throw new Error(
|
|
13199
|
-
`Task ${taskId} is already terminal (state=${current.state}) \u2014 signal '${signal}' would be silently ignored by the daemon. Stop here: your task record was never reopened for this turn. Ask the captain to run 'squadrant crew send' to reopen it before signaling again.`
|
|
13674
|
+
`Task ${taskId} is already terminal (state=${current.state}) \u2014 signal '${signal}' would be silently ignored by the daemon. Stop here: your task record was never reopened for this turn. Ask the captain to run 'squadrant crew send' to reopen it \u2014 its output confirms with "\u21BB Task was terminal \u2014 reopened to working" (#595) \u2014 before signaling again.`
|
|
13200
13675
|
);
|
|
13201
13676
|
}
|
|
13202
13677
|
const req = buildSignalRequest(signal, { ...o, writeResult: o.writeResult ?? defaultWriteResult });
|
|
@@ -13259,6 +13734,20 @@ async function runCrewApprove(project, crew, deps) {
|
|
|
13259
13734
|
});
|
|
13260
13735
|
return prUrl;
|
|
13261
13736
|
}
|
|
13737
|
+
async function runCrewReply(project, id, message, deps) {
|
|
13738
|
+
const tasks = await deps.listTasks(project);
|
|
13739
|
+
const matches = tasks.filter((t) => t.id === id || t.id.startsWith(id));
|
|
13740
|
+
if (matches.length === 0) throw new Error(`unknown task ${id}`);
|
|
13741
|
+
if (matches.length > 1) {
|
|
13742
|
+
throw new Error(`task id '${id}' is ambiguous \u2014 matches ${matches.map((t) => t.id).join(", ")}`);
|
|
13743
|
+
}
|
|
13744
|
+
const target = matches[0];
|
|
13745
|
+
if (!target.name) {
|
|
13746
|
+
throw new Error(`task ${id} has no crew name on record \u2014 cannot deliver via 'crew reply'`);
|
|
13747
|
+
}
|
|
13748
|
+
await deps.sendCrew(project, target.name, message);
|
|
13749
|
+
return deps.getStatus(project, target.id);
|
|
13750
|
+
}
|
|
13262
13751
|
function addControlPlaneCrewCommands(crew) {
|
|
13263
13752
|
crew.command("dispatch <project> <task>").description("Dispatch a crew task via the control-plane daemon").requiredOption("--provider <p>", "claude|opencode|codex (gemini: experimental, headless not supported)").option("--mode <m>", "headless|interactive", "interactive").option("--cwd <dir>", "working dir for the crew (project/worktree); required for codex to edit code").action(async (project, task, opts) => {
|
|
13264
13753
|
const req = buildDispatchRequest({ project, task, provider: opts.provider, mode: opts.mode, cwd: opts.cwd });
|
|
@@ -13295,14 +13784,18 @@ function addControlPlaneCrewCommands(crew) {
|
|
|
13295
13784
|
const compact = opts.json !== true;
|
|
13296
13785
|
process.stdout.write(formatCompactTasks(records, { compact }) + "\n");
|
|
13297
13786
|
});
|
|
13298
|
-
crew.command("reply <project> <id>
|
|
13787
|
+
crew.command("reply <project> <id> [message]").description("Reply to a blocked crew task \u2014 delivers the message first, then transitions state; never transitions on a dropped message. Resolve a gate via --gate.").option("--message-file <path>", "Read message from file instead of positional arg ('-' for stdin)").option("--gate <gateId>", "resolve a pending gate by id (codex interactive, spec \xA74.9)").action(async (project, id, message, opts) => {
|
|
13788
|
+
const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
|
|
13299
13789
|
if (opts.gate) {
|
|
13300
|
-
const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message }));
|
|
13790
|
+
const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message: resolvedMessage }));
|
|
13301
13791
|
process.stdout.write(JSON.stringify(r2) + "\n");
|
|
13302
13792
|
return;
|
|
13303
13793
|
}
|
|
13304
|
-
|
|
13305
|
-
|
|
13794
|
+
const r = await runCrewReply(project, id, resolvedMessage, {
|
|
13795
|
+
listTasks: async (p) => await squadrantdCall({ kind: "list", project: p }),
|
|
13796
|
+
sendCrew: (p, name, msg) => runCrewSend2(p, name, msg),
|
|
13797
|
+
getStatus: (p, taskId) => squadrantdCall(buildStatusRequest(p, taskId))
|
|
13798
|
+
});
|
|
13306
13799
|
process.stdout.write(JSON.stringify(r) + "\n");
|
|
13307
13800
|
});
|
|
13308
13801
|
crew.command("_hook <event>", { hidden: true }).description("internal: bridge from claude Stop/SubagentStop/SessionEnd hooks to squadrantd").action(async (event) => {
|
|
@@ -13556,14 +14049,30 @@ async function runCrewList2(project) {
|
|
|
13556
14049
|
const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
|
|
13557
14050
|
return runCrewList(project, runtime, workspaceId);
|
|
13558
14051
|
}
|
|
14052
|
+
async function runCrewAnswer2(project, name, option, opts) {
|
|
14053
|
+
const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
|
|
14054
|
+
return runCrewAnswer(
|
|
14055
|
+
project,
|
|
14056
|
+
name,
|
|
14057
|
+
option,
|
|
14058
|
+
runtime,
|
|
14059
|
+
workspaceId,
|
|
14060
|
+
{
|
|
14061
|
+
readModalOptions: (pane) => readModalOptions(runtime, pane),
|
|
14062
|
+
log: (m) => console.log(chalk10.dim(m))
|
|
14063
|
+
},
|
|
14064
|
+
opts
|
|
14065
|
+
);
|
|
14066
|
+
}
|
|
13559
14067
|
var crewCommand = new Command10("crew").description(
|
|
13560
14068
|
"Spawn and manage interactive crew sessions next to the project's captain"
|
|
13561
14069
|
);
|
|
13562
14070
|
crewCommand.command("spawn").description(
|
|
13563
14071
|
"Spawn an interactive crew session as a tab in the captain's workspace (use --direction to split into a pane instead)"
|
|
13564
|
-
).argument("<project>", "Project name (must be registered)").argument("[task]", "Initial task prompt for the crew session (omit with --task-file)").option("--name <name>", "Crew name (default: auto-generated crew-N)").option("--direction <dir>", "Placement: tab (default) or split direction (right|left|up|down)", "tab").option("--agent <name>", "Agent CLI to use (claude|codex|gemini|opencode)", "claude").option("--approval", "gate risky tools so the captain approves them (codex: approvalPolicy='untrusted'; opencode: bash:'ask')", false).option("--shared", "run the crew in the root checkout instead of an isolated worktree (for small/one-off tasks)", false).option("--task-file <path>", "Read task prompt from file instead of positional arg ('-' for stdin)").option("--model <alias>", "Override crew model for this spawn (e.g. sonnet, opus); takes precedence over config defaults.roles.crew.model").action(
|
|
14072
|
+
).argument("<project>", "Project name (must be registered)").argument("[task]", "Initial task prompt for the crew session (omit with --task-file)").option("--name <name>", "Crew name (default: auto-generated crew-N)").option("--direction <dir>", "Placement: tab (default) or split direction (right|left|up|down)", "tab").option("--agent <name>", "Agent CLI to use (claude|codex|gemini|opencode)", "claude").option("--approval", "gate risky tools so the captain approves them (codex: approvalPolicy='untrusted'; opencode: bash:'ask')", false).option("--shared", "run the crew in the root checkout instead of an isolated worktree (for small/one-off tasks)", false).option("--task-file <path>", "Read task prompt from file instead of positional arg ('-' for stdin)").option("--model <alias>", "Override crew model for this spawn (e.g. sonnet, opus); takes precedence over config defaults.roles.crew.model").option("--thinking <level>", `Override crew thinking level for this spawn (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over config defaults.roles.crew.thinking`).action(
|
|
13565
14073
|
async (project, task, opts, cmd) => {
|
|
13566
14074
|
try {
|
|
14075
|
+
const thinking = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
|
|
13567
14076
|
const resolvedTask = await resolveTextInput({ positional: task, filePath: opts.taskFile, label: "task" });
|
|
13568
14077
|
const agentExplicit = cmd.getOptionValueSource("agent") === "cli";
|
|
13569
14078
|
const pane = await runCrewSpawn2({
|
|
@@ -13578,6 +14087,7 @@ crewCommand.command("spawn").description(
|
|
|
13578
14087
|
...opts.approval ? { approvalPolicy: "untrusted", approval: true } : {},
|
|
13579
14088
|
...opts.shared ? { shared: true } : {},
|
|
13580
14089
|
...opts.model ? { model: opts.model } : {},
|
|
14090
|
+
...thinking ? { thinking } : {},
|
|
13581
14091
|
// #458: pass the raw file path (not stdin) so runCrewSpawn can copy it
|
|
13582
14092
|
// into the isolated worktree root for relative-path access.
|
|
13583
14093
|
...opts.taskFile && opts.taskFile !== "-" ? { taskFile: opts.taskFile } : {}
|
|
@@ -13631,13 +14141,29 @@ crewCommand.command("list").description("List live crew sessions for a project")
|
|
|
13631
14141
|
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)").option("--force", "override an operator takeover (only when the operator told you to)", false).action(async (project, name, message, opts) => {
|
|
13632
14142
|
try {
|
|
13633
14143
|
const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
|
|
13634
|
-
await runCrewSend2(project, name, resolvedMessage, opts);
|
|
14144
|
+
const { reopened } = await runCrewSend2(project, name, resolvedMessage, opts);
|
|
14145
|
+
if (reopened) console.log(chalk10.cyan(`\u21BB Task was terminal \u2014 reopened to working`));
|
|
13635
14146
|
console.log(chalk10.green(`\u2714 Sent to ${project}:${name}`));
|
|
13636
14147
|
} catch (e) {
|
|
13637
14148
|
console.error(chalk10.red(e.message));
|
|
13638
14149
|
process.exit(1);
|
|
13639
14150
|
}
|
|
13640
14151
|
});
|
|
14152
|
+
crewCommand.command("answer").description(
|
|
14153
|
+
"Deliberately answer a crew's open AskUserQuestion/permission prompt (#592) \u2014 never an implicit default"
|
|
14154
|
+
).argument("<project>", "Project name").argument("<name>", "Crew name").argument("<option>", "1-based option index, or an exact/prefix match of the option's text").option("--expect <text>", "Refuse unless the resolved option's label contains this text (guards against option order shifting)").option("--text <answer>", "For a free-text option (e.g. 'Type something.'): select it, then type this answer and submit").action(async (project, name, option, opts) => {
|
|
14155
|
+
try {
|
|
14156
|
+
const { selected, closed } = await runCrewAnswer2(project, name, option, opts);
|
|
14157
|
+
if (closed) {
|
|
14158
|
+
console.log(chalk10.green(`\u2714 Answered ${project}:${name} with ${selected.index}. "${selected.label}" \u2014 prompt closed`));
|
|
14159
|
+
} else {
|
|
14160
|
+
console.log(chalk10.yellow(`\u26A0 Sent ${selected.index}. "${selected.label}" to ${project}:${name}, but the prompt still appears open \u2014 read it again with 'squadrant crew read ${project} ${name}'`));
|
|
14161
|
+
}
|
|
14162
|
+
} catch (err) {
|
|
14163
|
+
console.error(chalk10.red(err.message));
|
|
14164
|
+
process.exit(1);
|
|
14165
|
+
}
|
|
14166
|
+
});
|
|
13641
14167
|
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) => {
|
|
13642
14168
|
try {
|
|
13643
14169
|
const screen = await runCrewRead2(project, name);
|
|
@@ -14214,9 +14740,9 @@ function mergeSnapshot(daemon, external, now) {
|
|
|
14214
14740
|
// packages/web/dist/probes.js
|
|
14215
14741
|
init_dist();
|
|
14216
14742
|
init_dist();
|
|
14217
|
-
import { join as
|
|
14743
|
+
import { join as join23 } from "path";
|
|
14218
14744
|
import { homedir as homedir15 } from "os";
|
|
14219
|
-
import { existsSync as
|
|
14745
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
|
|
14220
14746
|
import { execFile as execFile4 } from "child_process";
|
|
14221
14747
|
var DEFAULT_TIMEOUT_MS2 = 2e3;
|
|
14222
14748
|
var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
|
|
@@ -14252,7 +14778,7 @@ function vaultProbe(run, dir) {
|
|
|
14252
14778
|
return { state: "unknown", detail: "no vault configured" };
|
|
14253
14779
|
if (!run.pathExists(dir))
|
|
14254
14780
|
return { state: "gone", detail: "vault directory missing" };
|
|
14255
|
-
if (!run.pathExists(
|
|
14781
|
+
if (!run.pathExists(join23(dir, ".obsidian")))
|
|
14256
14782
|
return { state: "gone", detail: "no .obsidian/ (not a vault)" };
|
|
14257
14783
|
return { state: "alive" };
|
|
14258
14784
|
} catch {
|
|
@@ -14320,13 +14846,13 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS2) {
|
|
|
14320
14846
|
const sessions = probeSessions(run);
|
|
14321
14847
|
return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
|
|
14322
14848
|
}
|
|
14323
|
-
var SESSIONS_PATH =
|
|
14849
|
+
var SESSIONS_PATH = join23(homedir15(), ".config", "squadrant", "sessions.json");
|
|
14324
14850
|
function onPath(cli) {
|
|
14325
14851
|
const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
14326
|
-
return dirs.some((d) =>
|
|
14852
|
+
return dirs.some((d) => existsSync13(join23(d, cli)));
|
|
14327
14853
|
}
|
|
14328
14854
|
function readSessionsHashes() {
|
|
14329
|
-
const raw = JSON.parse(
|
|
14855
|
+
const raw = JSON.parse(readFileSync16(SESSIONS_PATH, "utf-8"));
|
|
14330
14856
|
const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
|
|
14331
14857
|
return [...new Set(hashes)];
|
|
14332
14858
|
}
|
|
@@ -14340,7 +14866,7 @@ function defaultProbeRunners() {
|
|
|
14340
14866
|
}
|
|
14341
14867
|
}),
|
|
14342
14868
|
probeOnPath: async (cli) => onPath(cli),
|
|
14343
|
-
pathExists: (p) =>
|
|
14869
|
+
pathExists: (p) => existsSync13(p),
|
|
14344
14870
|
loadConfig: () => loadConfig(),
|
|
14345
14871
|
loadSessionsHashes: () => readSessionsHashes()
|
|
14346
14872
|
};
|
|
@@ -15398,7 +15924,9 @@ async function selectCaptainsInteractive(entries, yesterday = getYesterday()) {
|
|
|
15398
15924
|
function resolveLaunchAgent(overrides, roleConfig, roleModelDefault) {
|
|
15399
15925
|
return {
|
|
15400
15926
|
agentName: overrides.agent ?? roleConfig?.agent ?? "claude",
|
|
15401
|
-
model: overrides.model ?? roleConfig?.model ?? roleModelDefault
|
|
15927
|
+
model: overrides.model ?? roleConfig?.model ?? roleModelDefault,
|
|
15928
|
+
// No built-in default: unset ⇒ flag omitted ⇒ the agent's own effort.
|
|
15929
|
+
thinking: overrides.thinking ?? roleConfig?.thinking
|
|
15402
15930
|
};
|
|
15403
15931
|
}
|
|
15404
15932
|
|
|
@@ -15435,11 +15963,20 @@ function ensureCmuxReady(headless) {
|
|
|
15435
15963
|
}
|
|
15436
15964
|
var launchCommand = new Command14("launch").description(
|
|
15437
15965
|
"Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
|
|
15438
|
-
).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").action(async (project, opts) => {
|
|
15966
|
+
).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").option("--thinking <level>", `Override captain thinking level for this launch (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over defaults.roles.captain.thinking`).action(async (project, opts) => {
|
|
15439
15967
|
if (opts.fresh && opts.keep) {
|
|
15440
15968
|
console.error(chalk15.red("\n \u2718 --fresh and --keep are mutually exclusive\n"));
|
|
15441
15969
|
process.exit(1);
|
|
15442
15970
|
}
|
|
15971
|
+
let thinkingOverride;
|
|
15972
|
+
try {
|
|
15973
|
+
thinkingOverride = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
|
|
15974
|
+
} catch (err) {
|
|
15975
|
+
console.error(chalk15.red(`
|
|
15976
|
+
\u2718 ${err.message}
|
|
15977
|
+
`));
|
|
15978
|
+
process.exit(1);
|
|
15979
|
+
}
|
|
15443
15980
|
const config = loadConfig();
|
|
15444
15981
|
let hadFailure = false;
|
|
15445
15982
|
const drivers = {
|
|
@@ -15452,8 +15989,8 @@ var launchCommand = new Command14("launch").description(
|
|
|
15452
15989
|
const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
|
|
15453
15990
|
async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
|
|
15454
15991
|
const roleConfig = config.defaults.roles?.[role];
|
|
15455
|
-
const { agentName, model } = resolveLaunchAgent(
|
|
15456
|
-
{ agent: opts.agent, model: opts.model },
|
|
15992
|
+
const { agentName, model, thinking } = resolveLaunchAgent(
|
|
15993
|
+
{ agent: opts.agent, model: opts.model, thinking: thinkingOverride },
|
|
15457
15994
|
roleConfig,
|
|
15458
15995
|
config.defaults.models?.[role]
|
|
15459
15996
|
);
|
|
@@ -15496,7 +16033,8 @@ var launchCommand = new Command14("launch").description(
|
|
|
15496
16033
|
model,
|
|
15497
16034
|
TEMPLATES_DIR4,
|
|
15498
16035
|
resolveCaptainSocketPath(captainChannelEnabled, projectName, workspaceName),
|
|
15499
|
-
resolveCaptainSessionName(agentName, projectName)
|
|
16036
|
+
resolveCaptainSessionName(agentName, projectName),
|
|
16037
|
+
thinking
|
|
15500
16038
|
);
|
|
15501
16039
|
},
|
|
15502
16040
|
initialPrompt,
|
|
@@ -16403,7 +16941,7 @@ init_dist2();
|
|
|
16403
16941
|
import { Command as Command23 } from "commander";
|
|
16404
16942
|
import fs24 from "fs";
|
|
16405
16943
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
16406
|
-
import { dirname as dirname5, join as
|
|
16944
|
+
import { dirname as dirname5, join as join24 } from "path";
|
|
16407
16945
|
import chalk23 from "chalk";
|
|
16408
16946
|
function runConfigCheck(opts) {
|
|
16409
16947
|
const raw = JSON.parse(fs24.readFileSync(opts.configPath, "utf-8"));
|
|
@@ -16539,14 +17077,14 @@ configCommand.command("set").description("Write a config value by dotted key (e.
|
|
|
16539
17077
|
}
|
|
16540
17078
|
});
|
|
16541
17079
|
function readPkgVersion2() {
|
|
16542
|
-
const pkgPath =
|
|
17080
|
+
const pkgPath = join24(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
|
|
16543
17081
|
return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version;
|
|
16544
17082
|
}
|
|
16545
17083
|
|
|
16546
17084
|
// packages/cli/src/commands/heal.ts
|
|
16547
17085
|
init_dist();
|
|
16548
17086
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
16549
|
-
import { join as
|
|
17087
|
+
import { join as join25 } from "path";
|
|
16550
17088
|
import { homedir as homedir16 } from "os";
|
|
16551
17089
|
import { Command as Command24 } from "commander";
|
|
16552
17090
|
import chalk24 from "chalk";
|
|
@@ -16562,9 +17100,16 @@ function buildHealStatus(components) {
|
|
|
16562
17100
|
project: c.project,
|
|
16563
17101
|
ref: c.ref,
|
|
16564
17102
|
state: c.state,
|
|
17103
|
+
detail: c.detail,
|
|
17104
|
+
stuck: c.stuck,
|
|
16565
17105
|
healCmd: healCmdFor(c)
|
|
16566
17106
|
}));
|
|
16567
|
-
const healthy = out.every((c) =>
|
|
17107
|
+
const healthy = out.every((c) => {
|
|
17108
|
+
if (c.kind === "delivery") {
|
|
17109
|
+
return c.stuck !== true;
|
|
17110
|
+
}
|
|
17111
|
+
return c.healCmd === null;
|
|
17112
|
+
});
|
|
16568
17113
|
return { healthy, components: out };
|
|
16569
17114
|
}
|
|
16570
17115
|
async function runHealStatus(opts) {
|
|
@@ -16601,15 +17146,37 @@ async function runHealStatus(opts) {
|
|
|
16601
17146
|
}
|
|
16602
17147
|
if (result.healthy) {
|
|
16603
17148
|
stdout.write(chalk24.green("\u2714 all components healthy\n"));
|
|
17149
|
+
for (const c of result.components) {
|
|
17150
|
+
if (c.kind === "delivery" && c.state === "stale") {
|
|
17151
|
+
const glyph = chalk24.yellow("\u2022");
|
|
17152
|
+
const stateColor2 = chalk24.yellow;
|
|
17153
|
+
stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
|
|
17154
|
+
`);
|
|
17155
|
+
if (c.detail) {
|
|
17156
|
+
stdout.write(` detail: ${c.detail}
|
|
17157
|
+
`);
|
|
17158
|
+
}
|
|
17159
|
+
}
|
|
17160
|
+
}
|
|
16604
17161
|
return 0;
|
|
16605
17162
|
}
|
|
16606
17163
|
stdout.write(chalk24.bold("Unhealthy components:\n\n"));
|
|
16607
17164
|
for (const c of result.components) {
|
|
16608
|
-
|
|
16609
|
-
|
|
17165
|
+
const isUnhealthy = c.kind === "delivery" ? c.stuck === true : c.healCmd !== null;
|
|
17166
|
+
const isAdvisoryDelivery = c.kind === "delivery" && c.state === "stale" && c.stuck !== true;
|
|
17167
|
+
if (isUnhealthy || isAdvisoryDelivery) {
|
|
17168
|
+
const glyph = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow("\u2022") : chalk24.red("\u2718");
|
|
17169
|
+
const stateColor2 = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow : chalk24.red;
|
|
17170
|
+
stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
|
|
16610
17171
|
`);
|
|
16611
|
-
|
|
17172
|
+
if (c.detail) {
|
|
17173
|
+
stdout.write(` detail: ${c.detail}
|
|
17174
|
+
`);
|
|
17175
|
+
}
|
|
17176
|
+
if (c.healCmd) {
|
|
17177
|
+
stdout.write(` heal: ${chalk24.cyan(c.healCmd)}
|
|
16612
17178
|
`);
|
|
17179
|
+
}
|
|
16613
17180
|
}
|
|
16614
17181
|
}
|
|
16615
17182
|
return 2;
|
|
@@ -16755,8 +17322,8 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
|
|
|
16755
17322
|
}
|
|
16756
17323
|
const config = loadConfig();
|
|
16757
17324
|
const projects = opts.all ? Object.keys(config.projects) : [project];
|
|
16758
|
-
const stateRoot =
|
|
16759
|
-
const registry = new LivenessRegistry({ path:
|
|
17325
|
+
const stateRoot = join25(homedir16(), ".config", "squadrant", "state");
|
|
17326
|
+
const registry = new LivenessRegistry({ path: join25(stateRoot, "liveness.json") });
|
|
16760
17327
|
registry.load();
|
|
16761
17328
|
const code = await runHealCaptain(projects, {
|
|
16762
17329
|
liveness: readCmuxLiveness,
|
|
@@ -16840,7 +17407,7 @@ init_dist();
|
|
|
16840
17407
|
init_dist2();
|
|
16841
17408
|
init_runtime2();
|
|
16842
17409
|
init_require_daemon();
|
|
16843
|
-
import { join as
|
|
17410
|
+
import { join as join27, dirname as dirname6 } from "path";
|
|
16844
17411
|
import { Command as Command27 } from "commander";
|
|
16845
17412
|
import chalk28 from "chalk";
|
|
16846
17413
|
|
|
@@ -16848,26 +17415,55 @@ import chalk28 from "chalk";
|
|
|
16848
17415
|
init_dist4();
|
|
16849
17416
|
init_dist2();
|
|
16850
17417
|
import { createServer as createServer5, connect as netConnect2 } from "net";
|
|
16851
|
-
import
|
|
17418
|
+
import fs25 from "fs";
|
|
17419
|
+
import { join as join26 } from "path";
|
|
16852
17420
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
16853
17421
|
import chalk27 from "chalk";
|
|
16854
17422
|
var shared;
|
|
17423
|
+
var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
|
|
17424
|
+
function unregisterSenderIdentity() {
|
|
17425
|
+
try {
|
|
17426
|
+
fs25.unlinkSync(registryEntryPath());
|
|
17427
|
+
} catch {
|
|
17428
|
+
}
|
|
17429
|
+
}
|
|
17430
|
+
function registerSenderIdentity(socketPath2) {
|
|
17431
|
+
try {
|
|
17432
|
+
unregisterSenderIdentity();
|
|
17433
|
+
fs25.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
|
|
17434
|
+
fs25.writeFileSync(
|
|
17435
|
+
registryEntryPath(),
|
|
17436
|
+
JSON.stringify({
|
|
17437
|
+
pid: process.pid,
|
|
17438
|
+
sessionId: randomUUID7(),
|
|
17439
|
+
name: "squadrantd",
|
|
17440
|
+
messagingSocketPath: socketPath2,
|
|
17441
|
+
kind: "daemon",
|
|
17442
|
+
peerProtocol: 1
|
|
17443
|
+
})
|
|
17444
|
+
);
|
|
17445
|
+
process.on("exit", unregisterSenderIdentity);
|
|
17446
|
+
} catch {
|
|
17447
|
+
}
|
|
17448
|
+
}
|
|
16855
17449
|
async function sharedReceiptListener() {
|
|
16856
17450
|
if (shared) return shared;
|
|
17451
|
+
const socketPath2 = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
|
|
16857
17452
|
const listener = new ClaudeReceiptListener({
|
|
16858
|
-
socketPath:
|
|
17453
|
+
socketPath: socketPath2,
|
|
16859
17454
|
createServer: (h) => createServer5(h),
|
|
16860
17455
|
// A UDS path is not cleaned up when a process is killed, so our own leftover
|
|
16861
17456
|
// must never be the reason we refuse to start.
|
|
16862
17457
|
unlinkStale: (p) => {
|
|
16863
17458
|
try {
|
|
16864
|
-
|
|
17459
|
+
fs25.unlinkSync(p);
|
|
16865
17460
|
} catch {
|
|
16866
17461
|
}
|
|
16867
17462
|
},
|
|
16868
17463
|
log: (m) => console.error(chalk27.dim(m))
|
|
16869
17464
|
});
|
|
16870
17465
|
await listener.start();
|
|
17466
|
+
registerSenderIdentity(socketPath2);
|
|
16871
17467
|
shared = listener;
|
|
16872
17468
|
return shared;
|
|
16873
17469
|
}
|
|
@@ -16922,7 +17518,7 @@ async function runPing(project, message) {
|
|
|
16922
17518
|
log: (m) => console.error(chalk28.dim(m))
|
|
16923
17519
|
});
|
|
16924
17520
|
if (!handled) {
|
|
16925
|
-
const stateRoot =
|
|
17521
|
+
const stateRoot = join27(dirname6(DEFAULT_CONFIG_PATH), "state");
|
|
16926
17522
|
await appendCaptainMessage({
|
|
16927
17523
|
stateRoot,
|
|
16928
17524
|
project,
|
|
@@ -17001,7 +17597,7 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
|
|
|
17001
17597
|
// packages/cli/src/commands/effort.ts
|
|
17002
17598
|
init_dist();
|
|
17003
17599
|
init_dist2();
|
|
17004
|
-
import
|
|
17600
|
+
import fs26 from "fs";
|
|
17005
17601
|
import path29 from "path";
|
|
17006
17602
|
import { Command as Command29 } from "commander";
|
|
17007
17603
|
import chalk30 from "chalk";
|
|
@@ -17045,7 +17641,7 @@ function effortScopeLabel(projectName) {
|
|
|
17045
17641
|
}
|
|
17046
17642
|
function canonical(p) {
|
|
17047
17643
|
try {
|
|
17048
|
-
return
|
|
17644
|
+
return fs26.realpathSync(p);
|
|
17049
17645
|
} catch {
|
|
17050
17646
|
return path29.resolve(p);
|
|
17051
17647
|
}
|
|
@@ -17107,7 +17703,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
|
|
|
17107
17703
|
|
|
17108
17704
|
// packages/cli/src/commands/tokens.ts
|
|
17109
17705
|
init_dist();
|
|
17110
|
-
import
|
|
17706
|
+
import fs27 from "fs";
|
|
17111
17707
|
import path30 from "path";
|
|
17112
17708
|
import os16 from "os";
|
|
17113
17709
|
import readline3 from "readline";
|
|
@@ -17163,7 +17759,7 @@ function foldTranscriptLine(agg, rawLine, state) {
|
|
|
17163
17759
|
async function aggregateTranscriptFile(filePath) {
|
|
17164
17760
|
const agg = emptySessionAggregate();
|
|
17165
17761
|
const state = { lastCacheRead: null };
|
|
17166
|
-
const rl = readline3.createInterface({ input:
|
|
17762
|
+
const rl = readline3.createInterface({ input: fs27.createReadStream(filePath), crlfDelay: Infinity });
|
|
17167
17763
|
for await (const line of rl) {
|
|
17168
17764
|
foldTranscriptLine(agg, line, state);
|
|
17169
17765
|
}
|
|
@@ -17231,7 +17827,7 @@ function buildRoleReport(role, sessions) {
|
|
|
17231
17827
|
}
|
|
17232
17828
|
async function readdirSafe(dir) {
|
|
17233
17829
|
try {
|
|
17234
|
-
return await
|
|
17830
|
+
return await fs27.promises.readdir(dir);
|
|
17235
17831
|
} catch {
|
|
17236
17832
|
return [];
|
|
17237
17833
|
}
|
|
@@ -17398,12 +17994,12 @@ var tokensCommand = new Command30("tokens").description(
|
|
|
17398
17994
|
// packages/cli/src/commands/telegram.ts
|
|
17399
17995
|
init_dist();
|
|
17400
17996
|
init_dist2();
|
|
17401
|
-
import { join as
|
|
17997
|
+
import { join as join28, dirname as dirname7 } from "path";
|
|
17402
17998
|
import { emitKeypressEvents } from "readline";
|
|
17403
17999
|
import { Command as Command31 } from "commander";
|
|
17404
18000
|
import chalk32 from "chalk";
|
|
17405
18001
|
function defaultStateRoot() {
|
|
17406
|
-
return
|
|
18002
|
+
return join28(dirname7(DEFAULT_CONFIG_PATH), "state");
|
|
17407
18003
|
}
|
|
17408
18004
|
async function questionMasked() {
|
|
17409
18005
|
return new Promise((resolve4) => {
|
|
@@ -17729,9 +18325,10 @@ init_dist2();
|
|
|
17729
18325
|
init_dist4();
|
|
17730
18326
|
init_dist();
|
|
17731
18327
|
import { Command as Command32 } from "commander";
|
|
18328
|
+
import { homedir as homedir17 } from "os";
|
|
17732
18329
|
|
|
17733
18330
|
// packages/cli/src/lib/captain-session-registry.ts
|
|
17734
|
-
import
|
|
18331
|
+
import fs28 from "fs";
|
|
17735
18332
|
import path31 from "path";
|
|
17736
18333
|
|
|
17737
18334
|
// packages/cli/src/lib/handoff-facts.ts
|
|
@@ -17783,15 +18380,15 @@ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, ext
|
|
|
17783
18380
|
// packages/cli/src/lib/captain-session-registry.ts
|
|
17784
18381
|
var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
|
|
17785
18382
|
function appendCaptainSession(spokeVault, record) {
|
|
17786
|
-
|
|
18383
|
+
fs28.mkdirSync(spokeVault, { recursive: true });
|
|
17787
18384
|
const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
|
|
17788
|
-
|
|
18385
|
+
fs28.appendFileSync(file, JSON.stringify(record) + "\n");
|
|
17789
18386
|
}
|
|
17790
18387
|
function readCaptainSessionRegistry(spokeVault) {
|
|
17791
18388
|
const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
|
|
17792
|
-
if (!
|
|
18389
|
+
if (!fs28.existsSync(file)) return [];
|
|
17793
18390
|
const records = [];
|
|
17794
|
-
for (const line of
|
|
18391
|
+
for (const line of fs28.readFileSync(file, "utf-8").split("\n")) {
|
|
17795
18392
|
if (!line.trim()) continue;
|
|
17796
18393
|
try {
|
|
17797
18394
|
records.push(JSON.parse(line));
|
|
@@ -17878,6 +18475,26 @@ function hooksCommand() {
|
|
|
17878
18475
|
if (!taskId || !project) {
|
|
17879
18476
|
process.exit(0);
|
|
17880
18477
|
}
|
|
18478
|
+
if (sub === "pre-tool-use") {
|
|
18479
|
+
const p = payload;
|
|
18480
|
+
const guard = decideCaptainMemoryWrite(
|
|
18481
|
+
typeof p?.tool_name === "string" ? p.tool_name : "",
|
|
18482
|
+
p?.tool_input,
|
|
18483
|
+
process.env,
|
|
18484
|
+
homedir17()
|
|
18485
|
+
);
|
|
18486
|
+
if (guard.decision === "deny") {
|
|
18487
|
+
console.error(`[squadrant] denied crew write into captain memory (task ${taskId}): ${guard.reason}`);
|
|
18488
|
+
process.stdout.write(JSON.stringify({
|
|
18489
|
+
hookSpecificOutput: {
|
|
18490
|
+
hookEventName: "PreToolUse",
|
|
18491
|
+
permissionDecision: "deny",
|
|
18492
|
+
permissionDecisionReason: guard.reason
|
|
18493
|
+
}
|
|
18494
|
+
}));
|
|
18495
|
+
process.exit(0);
|
|
18496
|
+
}
|
|
18497
|
+
}
|
|
17881
18498
|
const ev = mapHookSub(sub, payload, taskId);
|
|
17882
18499
|
if (!ev) {
|
|
17883
18500
|
process.exit(0);
|
|
@@ -18038,7 +18655,7 @@ import os17 from "os";
|
|
|
18038
18655
|
// packages/cli/src/lib/handoff-live-repo.ts
|
|
18039
18656
|
init_dist();
|
|
18040
18657
|
import { execFileSync as execFileSync10 } from "child_process";
|
|
18041
|
-
import
|
|
18658
|
+
import fs29 from "fs";
|
|
18042
18659
|
import path33 from "path";
|
|
18043
18660
|
|
|
18044
18661
|
// packages/cli/src/lib/handoff-branch-state.ts
|
|
@@ -18178,7 +18795,7 @@ function localAheadOfBase(runner, projectPath, base) {
|
|
|
18178
18795
|
}
|
|
18179
18796
|
function readFetchAgeMs(projectPath, now) {
|
|
18180
18797
|
try {
|
|
18181
|
-
const stat2 =
|
|
18798
|
+
const stat2 = fs29.statSync(path33.join(projectPath, ".git", "FETCH_HEAD"));
|
|
18182
18799
|
return Math.max(0, now - stat2.mtime.getTime());
|
|
18183
18800
|
} catch {
|
|
18184
18801
|
return null;
|
|
@@ -18249,7 +18866,7 @@ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = de
|
|
|
18249
18866
|
|
|
18250
18867
|
// packages/cli/src/lib/handoff-claude-mem.ts
|
|
18251
18868
|
import { createRequire } from "module";
|
|
18252
|
-
import
|
|
18869
|
+
import fs30 from "fs";
|
|
18253
18870
|
var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
18254
18871
|
var CLAUDE_MEM_RECENCY_LIMIT = 20;
|
|
18255
18872
|
function decisionText(row) {
|
|
@@ -18263,7 +18880,7 @@ function decisionText(row) {
|
|
|
18263
18880
|
return row.narrative ?? "";
|
|
18264
18881
|
}
|
|
18265
18882
|
function queryClaudeMem(dbPath, project) {
|
|
18266
|
-
if (!
|
|
18883
|
+
if (!fs30.existsSync(dbPath)) return null;
|
|
18267
18884
|
let db;
|
|
18268
18885
|
try {
|
|
18269
18886
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
@@ -18306,7 +18923,7 @@ function queryClaudeMem(dbPath, project) {
|
|
|
18306
18923
|
}
|
|
18307
18924
|
|
|
18308
18925
|
// packages/cli/src/lib/handoff-transcript.ts
|
|
18309
|
-
import
|
|
18926
|
+
import fs31 from "fs";
|
|
18310
18927
|
var TRANSCRIPT_BYTE_CAP = 2e5;
|
|
18311
18928
|
function tailOf(content, byteCap) {
|
|
18312
18929
|
const buf = Buffer.from(content, "utf-8");
|
|
@@ -18335,27 +18952,27 @@ function extractMessages(tailText) {
|
|
|
18335
18952
|
return { lastUserMessage, lastAssistantText };
|
|
18336
18953
|
}
|
|
18337
18954
|
function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
|
|
18338
|
-
if (!
|
|
18339
|
-
const content =
|
|
18955
|
+
if (!fs31.existsSync(transcriptPath)) return null;
|
|
18956
|
+
const content = fs31.readFileSync(transcriptPath, "utf-8");
|
|
18340
18957
|
const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
|
|
18341
|
-
const mtimeIso =
|
|
18958
|
+
const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
|
|
18342
18959
|
return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
|
|
18343
18960
|
}
|
|
18344
18961
|
|
|
18345
18962
|
// packages/cli/src/lib/handoff-archive.ts
|
|
18346
|
-
import
|
|
18963
|
+
import fs32 from "fs";
|
|
18347
18964
|
import path34 from "path";
|
|
18348
18965
|
function readNewestArchivedHandoff(spokeVault, now) {
|
|
18349
18966
|
const dir = path34.join(spokeVault, "handoffs");
|
|
18350
|
-
if (!
|
|
18351
|
-
const candidates =
|
|
18967
|
+
if (!fs32.existsSync(dir)) return null;
|
|
18968
|
+
const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
|
|
18352
18969
|
const full = path34.join(dir, e.name);
|
|
18353
|
-
return { name: e.name, full, mtime:
|
|
18970
|
+
return { name: e.name, full, mtime: fs32.statSync(full).mtime };
|
|
18354
18971
|
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
18355
18972
|
for (const candidate of candidates) {
|
|
18356
18973
|
let content;
|
|
18357
18974
|
try {
|
|
18358
|
-
content = JSON.parse(
|
|
18975
|
+
content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
|
|
18359
18976
|
} catch {
|
|
18360
18977
|
continue;
|
|
18361
18978
|
}
|
|
@@ -18425,15 +19042,15 @@ init_dist();
|
|
|
18425
19042
|
init_dist();
|
|
18426
19043
|
init_dist();
|
|
18427
19044
|
var __dirname = dirname8(fileURLToPath6(import.meta.url));
|
|
18428
|
-
var pkg = JSON.parse(
|
|
19045
|
+
var pkg = JSON.parse(readFileSync17(join29(__dirname, "..", "package.json"), "utf-8"));
|
|
18429
19046
|
ensureRuntimeSynced({
|
|
18430
|
-
sourceRoot:
|
|
18431
|
-
runtimeRoot:
|
|
19047
|
+
sourceRoot: join29(__dirname, ".."),
|
|
19048
|
+
runtimeRoot: join29(homedir18(), ".config", "squadrant")
|
|
18432
19049
|
});
|
|
18433
19050
|
if (process.argv[2] !== "config") {
|
|
18434
19051
|
try {
|
|
18435
|
-
const cfgPath =
|
|
18436
|
-
if (
|
|
19052
|
+
const cfgPath = join29(homedir18(), ".config", "squadrant", "config.json");
|
|
19053
|
+
if (existsSync14(cfgPath)) {
|
|
18437
19054
|
const cfg = JSON.parse(readConfigFileSync(cfgPath));
|
|
18438
19055
|
if (needsCheck(cfg, pkg.version)) {
|
|
18439
19056
|
const items = detectDrift(cfg, getDefaultConfig());
|