squadrant 0.16.5 → 0.16.6
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 +108 -34
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +91 -33
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +9 -3
package/dist/squadrantd.js
CHANGED
|
@@ -500,6 +500,11 @@ function nextPendingTool(current, ev, now) {
|
|
|
500
500
|
return void 0;
|
|
501
501
|
return current;
|
|
502
502
|
}
|
|
503
|
+
function nextPendingMonitor(current, ev, now) {
|
|
504
|
+
if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
|
|
505
|
+
return { since: now };
|
|
506
|
+
return current;
|
|
507
|
+
}
|
|
503
508
|
function reduce(rec, ev, now) {
|
|
504
509
|
if (ev.type === "task.reopened") {
|
|
505
510
|
return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
|
|
@@ -516,14 +521,17 @@ function reduce(rec, ev, now) {
|
|
|
516
521
|
sessionId: ev.sessionId ?? rec.sessionId,
|
|
517
522
|
question: void 0,
|
|
518
523
|
// resuming after a blocked→reply clears the question
|
|
519
|
-
pendingTool: void 0
|
|
524
|
+
pendingTool: void 0,
|
|
520
525
|
// #354: a new turn closes any prior tool window
|
|
526
|
+
pendingMonitor: void 0
|
|
527
|
+
// #594a: same reset — a new turn moots any prior watch
|
|
521
528
|
};
|
|
522
529
|
case "task.progress": {
|
|
523
530
|
const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
|
|
531
|
+
const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
|
|
524
532
|
if (isStickyAttention(rec.state))
|
|
525
|
-
return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
|
|
526
|
-
const b = { ...base, pendingTool };
|
|
533
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
|
|
534
|
+
const b = { ...base, pendingTool, pendingMonitor };
|
|
527
535
|
if (rec.state === "awaiting-input" || rec.state === "stalled")
|
|
528
536
|
return { ...stampAttempt(b, {}, now), state: "working" };
|
|
529
537
|
return stampAttempt(b, {}, now);
|
|
@@ -537,9 +545,9 @@ function reduce(rec, ev, now) {
|
|
|
537
545
|
case "task.blocked":
|
|
538
546
|
if (rec.state === "blocked")
|
|
539
547
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
540
|
-
return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
548
|
+
return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
|
|
541
549
|
case "task.review":
|
|
542
|
-
return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0 };
|
|
550
|
+
return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
|
|
543
551
|
case "task.done":
|
|
544
552
|
if (rec.state === "review" && ev.source !== "approve") {
|
|
545
553
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
@@ -554,19 +562,19 @@ function reduce(rec, ev, now) {
|
|
|
554
562
|
case "task.session":
|
|
555
563
|
return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
|
|
556
564
|
case "task.turn.started":
|
|
557
|
-
return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
|
|
565
|
+
return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
|
|
558
566
|
case "task.turn.completed":
|
|
559
567
|
if (isStickyAttention(rec.state))
|
|
560
568
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
561
|
-
if (rec.pendingTool)
|
|
569
|
+
if (rec.pendingTool || rec.pendingMonitor)
|
|
562
570
|
return stampAttempt(base, {}, now);
|
|
563
|
-
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
|
|
571
|
+
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
|
|
564
572
|
case "task.delta":
|
|
565
573
|
return stampAttempt(base, {}, now);
|
|
566
574
|
// heartbeat-only
|
|
567
575
|
case "task.input.requested":
|
|
568
576
|
case "task.approval.requested":
|
|
569
|
-
return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
577
|
+
return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
|
|
570
578
|
case "task.reattached":
|
|
571
579
|
return stampAttempt(base, {}, now);
|
|
572
580
|
case "task.first-turn.confirmed":
|
|
@@ -587,15 +595,22 @@ function reduce(rec, ev, now) {
|
|
|
587
595
|
|
|
588
596
|
// packages/core/dist/watchdog.js
|
|
589
597
|
var TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
|
|
590
|
-
|
|
598
|
+
var MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
|
|
599
|
+
function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
|
|
591
600
|
if (rec.state !== "working")
|
|
592
601
|
return null;
|
|
593
602
|
if (rec.mode === "interactive") {
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return
|
|
598
|
-
|
|
603
|
+
if (rec.pendingTool) {
|
|
604
|
+
if (now - rec.pendingTool.since <= toolStallMs)
|
|
605
|
+
return null;
|
|
606
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
|
|
607
|
+
}
|
|
608
|
+
if (rec.pendingMonitor) {
|
|
609
|
+
if (now - rec.pendingMonitor.since <= monitorStallMs)
|
|
610
|
+
return null;
|
|
611
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
599
614
|
}
|
|
600
615
|
const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
|
|
601
616
|
if (now - liveness <= rec.heartbeatBudgetMs)
|
|
@@ -605,7 +620,7 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
|
|
|
605
620
|
function recoverStall(rec, now) {
|
|
606
621
|
if (rec.state !== "stalled")
|
|
607
622
|
return null;
|
|
608
|
-
return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
|
|
623
|
+
return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
|
|
609
624
|
}
|
|
610
625
|
|
|
611
626
|
// packages/core/dist/daemon/reduce.js
|
|
@@ -912,7 +927,7 @@ function createDaemon(deps) {
|
|
|
912
927
|
store.delete(r.project, r.id);
|
|
913
928
|
continue;
|
|
914
929
|
}
|
|
915
|
-
if (!TERMINAL_STATES.has(r.state)) {
|
|
930
|
+
if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
|
|
916
931
|
const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
|
|
917
932
|
if (t - r.createdAt > ceiling) {
|
|
918
933
|
const prevState = r.state;
|
|
@@ -957,7 +972,7 @@ function createDaemon(deps) {
|
|
|
957
972
|
const idle = evaluateStall(r, t);
|
|
958
973
|
if (idle) {
|
|
959
974
|
store.put(idle);
|
|
960
|
-
const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
|
|
975
|
+
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 };
|
|
961
976
|
firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
|
|
962
977
|
continue;
|
|
963
978
|
}
|
|
@@ -2087,9 +2102,11 @@ function buildSurfaceProbe(ctx, probes, daemonCmux) {
|
|
|
2087
2102
|
// packages/core/dist/delivery/defer-delivery.js
|
|
2088
2103
|
var DeferDelivery = class extends Error {
|
|
2089
2104
|
draft;
|
|
2090
|
-
|
|
2105
|
+
reason;
|
|
2106
|
+
constructor(draft = null, reason = "draft") {
|
|
2091
2107
|
super("deferred: captain composing");
|
|
2092
2108
|
this.draft = draft;
|
|
2109
|
+
this.reason = reason;
|
|
2093
2110
|
this.name = "DeferDelivery";
|
|
2094
2111
|
}
|
|
2095
2112
|
};
|
|
@@ -2107,6 +2124,7 @@ var CaptainDelivery = class {
|
|
|
2107
2124
|
deferCounts = /* @__PURE__ */ new Map();
|
|
2108
2125
|
lastContent = /* @__PURE__ */ new Map();
|
|
2109
2126
|
stableCounts = /* @__PURE__ */ new Map();
|
|
2127
|
+
lastReason = /* @__PURE__ */ new Map();
|
|
2110
2128
|
constructor(opts) {
|
|
2111
2129
|
this.opts = opts;
|
|
2112
2130
|
}
|
|
@@ -2129,29 +2147,40 @@ var CaptainDelivery = class {
|
|
|
2129
2147
|
this.deferCounts.delete(seq);
|
|
2130
2148
|
this.stableCounts.delete(seq);
|
|
2131
2149
|
this.lastContent.delete(seq);
|
|
2150
|
+
this.lastReason.delete(seq);
|
|
2132
2151
|
return { delivered: true };
|
|
2133
2152
|
} catch (e) {
|
|
2134
2153
|
if (e instanceof DeferDelivery) {
|
|
2135
2154
|
this.deferCounts.set(seq, deferCount + 1);
|
|
2136
2155
|
const content = e.draft;
|
|
2156
|
+
let stableCount;
|
|
2137
2157
|
if (content && content === this.lastContent.get(seq)) {
|
|
2138
|
-
|
|
2158
|
+
stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
|
|
2159
|
+
this.stableCounts.set(seq, stableCount);
|
|
2139
2160
|
} else {
|
|
2161
|
+
stableCount = 0;
|
|
2140
2162
|
this.stableCounts.set(seq, 0);
|
|
2141
2163
|
}
|
|
2142
2164
|
this.lastContent.set(seq, content);
|
|
2143
|
-
|
|
2165
|
+
const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
|
|
2166
|
+
this.lastReason.set(seq, reason);
|
|
2167
|
+
return { deferred: true, reason };
|
|
2144
2168
|
}
|
|
2145
|
-
|
|
2169
|
+
this.lastReason.set(seq, "unknown");
|
|
2170
|
+
return { deferred: true, reason: "unknown" };
|
|
2146
2171
|
}
|
|
2147
2172
|
}
|
|
2148
2173
|
/** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
|
|
2149
2174
|
stats() {
|
|
2150
2175
|
let maxDeferCount = 0;
|
|
2151
|
-
|
|
2152
|
-
|
|
2176
|
+
let reason;
|
|
2177
|
+
for (const [seq, c] of this.deferCounts) {
|
|
2178
|
+
if (c > maxDeferCount) {
|
|
2153
2179
|
maxDeferCount = c;
|
|
2154
|
-
|
|
2180
|
+
reason = this.lastReason.get(seq);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
|
|
2155
2184
|
}
|
|
2156
2185
|
};
|
|
2157
2186
|
|
|
@@ -2251,6 +2280,10 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2251
2280
|
const notifyFault = ctx.notifyFault ?? (() => {
|
|
2252
2281
|
});
|
|
2253
2282
|
const defaultNotify = async (args) => {
|
|
2283
|
+
const fresh = store.get(args.project, args.record.id);
|
|
2284
|
+
if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2254
2287
|
try {
|
|
2255
2288
|
await appendToMailbox({
|
|
2256
2289
|
stateRoot,
|
|
@@ -2340,16 +2373,19 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
2340
2373
|
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
|
|
2341
2374
|
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
2342
2375
|
} else {
|
|
2343
|
-
|
|
2376
|
+
const { maxDeferCount } = d.stats();
|
|
2377
|
+
if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
|
|
2378
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
|
|
2379
|
+
}
|
|
2344
2380
|
break;
|
|
2345
2381
|
}
|
|
2346
2382
|
}
|
|
2347
2383
|
const stuck = d.stats().stuck;
|
|
2348
2384
|
if (stuck && !stuckNotified.has(project)) {
|
|
2349
2385
|
stuckNotified.add(project);
|
|
2350
|
-
const { maxDeferCount } = d.stats();
|
|
2351
|
-
log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
|
|
2352
|
-
const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
|
|
2386
|
+
const { maxDeferCount, reason } = d.stats();
|
|
2387
|
+
log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
|
|
2388
|
+
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.`;
|
|
2353
2389
|
appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
|
|
2354
2390
|
Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
|
|
2355
2391
|
telegramBridge?.pushRaw(project, text);
|
|
@@ -5049,9 +5085,9 @@ function createCmuxDriver() {
|
|
|
5049
5085
|
}
|
|
5050
5086
|
const draft = parseDraftFromScreen(screen);
|
|
5051
5087
|
if (draft === null)
|
|
5052
|
-
throw new DeferDelivery(null);
|
|
5088
|
+
throw new DeferDelivery(null, "no-box");
|
|
5053
5089
|
if (hasModalOptionList(screen))
|
|
5054
|
-
throw new DeferDelivery(null);
|
|
5090
|
+
throw new DeferDelivery(null, "modal");
|
|
5055
5091
|
if (draft === "") {
|
|
5056
5092
|
await deliver();
|
|
5057
5093
|
return;
|
|
@@ -5712,6 +5748,7 @@ function installClaudeHooks(opts = {}) {
|
|
|
5712
5748
|
});
|
|
5713
5749
|
let settings = {};
|
|
5714
5750
|
const raw = readFile6(settingsPath);
|
|
5751
|
+
const hadExistingSettings = raw !== void 0;
|
|
5715
5752
|
if (raw) {
|
|
5716
5753
|
try {
|
|
5717
5754
|
settings = JSON.parse(raw);
|
|
@@ -5724,6 +5761,7 @@ function installClaudeHooks(opts = {}) {
|
|
|
5724
5761
|
}
|
|
5725
5762
|
const hooks = settings.hooks;
|
|
5726
5763
|
let changed = false;
|
|
5764
|
+
const repaired = [];
|
|
5727
5765
|
for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
|
|
5728
5766
|
if (!Array.isArray(hooks[eventName])) {
|
|
5729
5767
|
hooks[eventName] = [];
|
|
@@ -5735,6 +5773,26 @@ function installClaudeHooks(opts = {}) {
|
|
|
5735
5773
|
if (!alreadyPresent) {
|
|
5736
5774
|
entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
|
|
5737
5775
|
changed = true;
|
|
5776
|
+
repaired.push(`${eventName}/${sub}`);
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
if (repaired.length > 0 && hadExistingSettings) {
|
|
5780
|
+
log(`native-hook: repaired ${repaired.length} missing squadrant hook(s) in ${settingsPath} [${repaired.join(", ")}] \u2014 WARNING: blocked-signalling or lifecycle tracking may have been broken until this run`);
|
|
5781
|
+
}
|
|
5782
|
+
if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
|
|
5783
|
+
if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
|
|
5784
|
+
settings.env = {};
|
|
5785
|
+
}
|
|
5786
|
+
const env = settings.env;
|
|
5787
|
+
for (const [key, value] of Object.entries(opts.claudeEnv)) {
|
|
5788
|
+
if (key in env) {
|
|
5789
|
+
if (env[key] !== value) {
|
|
5790
|
+
log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
|
|
5791
|
+
}
|
|
5792
|
+
continue;
|
|
5793
|
+
}
|
|
5794
|
+
env[key] = value;
|
|
5795
|
+
changed = true;
|
|
5738
5796
|
}
|
|
5739
5797
|
}
|
|
5740
5798
|
if (changed) {
|
|
@@ -5771,9 +5829,9 @@ var NativeHookSource = class {
|
|
|
5771
5829
|
cache = /* @__PURE__ */ new Map();
|
|
5772
5830
|
active = false;
|
|
5773
5831
|
constructor(opts = {}) {
|
|
5774
|
-
this.hookInstall = opts.hookInstall ?? {};
|
|
5775
5832
|
this.log = opts.log ?? (() => {
|
|
5776
5833
|
});
|
|
5834
|
+
this.hookInstall = { log: this.log, ...opts.hookInstall };
|
|
5777
5835
|
}
|
|
5778
5836
|
start(deps) {
|
|
5779
5837
|
this.deps = deps;
|
|
@@ -6085,7 +6143,7 @@ function startSquadrantd(opts = {}) {
|
|
|
6085
6143
|
log
|
|
6086
6144
|
});
|
|
6087
6145
|
const cmuxStoreSource = new CmuxStoreSource({ log });
|
|
6088
|
-
const nativeHookSource = new NativeHookSource({ log });
|
|
6146
|
+
const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
|
|
6089
6147
|
ctx.codexDriver = codexDriver;
|
|
6090
6148
|
ctx.opencodeBridge = opencodeBridge;
|
|
6091
6149
|
ctx.cmuxEventsBridge = cmuxEventsBridge;
|