squadrant 0.12.1 → 0.13.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 +764 -79
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +582 -10
- package/dist/squadrantd.js.map +1 -1
- package/package.json +2 -1
package/dist/squadrantd.js
CHANGED
|
@@ -57,10 +57,10 @@ var init_snapshot = __esm({
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
// packages/cli/src/squadrantd.ts
|
|
60
|
-
import { join as
|
|
61
|
-
import { homedir as
|
|
60
|
+
import { join as join17, dirname as dirname5 } from "path";
|
|
61
|
+
import { homedir as homedir12 } from "os";
|
|
62
62
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
63
|
-
import { readFileSync as
|
|
63
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
64
64
|
|
|
65
65
|
// packages/shared/dist/config.js
|
|
66
66
|
import fs from "fs";
|
|
@@ -1688,6 +1688,7 @@ var ERROR_BANNER_RE = [
|
|
|
1688
1688
|
/\bmaximum\s+retries\b/i
|
|
1689
1689
|
];
|
|
1690
1690
|
var OPTION_RE = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
|
|
1691
|
+
var PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
|
|
1691
1692
|
var PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
|
|
1692
1693
|
var STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
|
|
1693
1694
|
function stripChrome(raw) {
|
|
@@ -1731,6 +1732,18 @@ function classifyPaneTail(tail) {
|
|
|
1731
1732
|
}
|
|
1732
1733
|
return { kind: "approval", text: "Crew is awaiting permission approval." };
|
|
1733
1734
|
}
|
|
1735
|
+
const hasPickerFooter = cleaned.some((c) => c != null && PICKER_FOOTER_RE.test(c));
|
|
1736
|
+
if (options.length >= 2 && hasPickerFooter) {
|
|
1737
|
+
const firstOptCi = options[0].ci;
|
|
1738
|
+
for (let i = firstOptCi - 1; i >= 0; i--) {
|
|
1739
|
+
const c = cleaned[i];
|
|
1740
|
+
if (c == null)
|
|
1741
|
+
continue;
|
|
1742
|
+
if (c.endsWith("?"))
|
|
1743
|
+
return { kind: "question", text: c };
|
|
1744
|
+
}
|
|
1745
|
+
return { kind: "question", text: "Crew is awaiting a choice." };
|
|
1746
|
+
}
|
|
1734
1747
|
const region = cleaned.filter((c) => c != null).join("\n");
|
|
1735
1748
|
const q = detectTrailingQuestion(region);
|
|
1736
1749
|
if (q)
|
|
@@ -3262,6 +3275,23 @@ import path10 from "path";
|
|
|
3262
3275
|
var TEMPLATES_DIR = path10.join(os4.homedir(), ".config", "squadrant", "templates");
|
|
3263
3276
|
var STATE_ROOT = path10.join(os4.homedir(), ".config", "squadrant", "state");
|
|
3264
3277
|
|
|
3278
|
+
// packages/core/dist/lifecycle-source.js
|
|
3279
|
+
function reduceLifecycle(prev, next) {
|
|
3280
|
+
if (next.origin === "agent") {
|
|
3281
|
+
return next.state;
|
|
3282
|
+
}
|
|
3283
|
+
if (next.state === "needsInput") {
|
|
3284
|
+
return prev?.state ?? "unknown";
|
|
3285
|
+
}
|
|
3286
|
+
if (prev?.state === "needsInput") {
|
|
3287
|
+
return "needsInput";
|
|
3288
|
+
}
|
|
3289
|
+
if (prev?.origin === "agent" && prev.at >= next.at) {
|
|
3290
|
+
return prev.state;
|
|
3291
|
+
}
|
|
3292
|
+
return next.state;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3265
3295
|
// packages/agents/dist/drivers/claude.js
|
|
3266
3296
|
import { execSync as execSync2 } from "child_process";
|
|
3267
3297
|
|
|
@@ -3480,6 +3510,87 @@ function defaultSpawn() {
|
|
|
3480
3510
|
return nodeSpawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
3481
3511
|
}
|
|
3482
3512
|
|
|
3513
|
+
// packages/agents/dist/codex/codex-app-server-source.js
|
|
3514
|
+
var CodexAppServerSource = class {
|
|
3515
|
+
name = "codex-appserver";
|
|
3516
|
+
deps;
|
|
3517
|
+
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
3518
|
+
cache = /* @__PURE__ */ new Map();
|
|
3519
|
+
start(deps) {
|
|
3520
|
+
this.deps = deps;
|
|
3521
|
+
}
|
|
3522
|
+
stop() {
|
|
3523
|
+
this.deps = void 0;
|
|
3524
|
+
this.cache.clear();
|
|
3525
|
+
}
|
|
3526
|
+
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
3527
|
+
snapshot(taskId) {
|
|
3528
|
+
return this.cache.get(taskId);
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Feed a ControlEvent from CodexInteractiveDriver into this source.
|
|
3532
|
+
* The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
|
|
3533
|
+
*
|
|
3534
|
+
* All events that carry lifecycle meaning for a codex crew are mapped to a
|
|
3535
|
+
* LifecycleSnapshot and reported. Events that are terminal signals (task.done,
|
|
3536
|
+
* task.cancelled, task.blocked) or notify-only (task.stalled, task.quiet, etc.)
|
|
3537
|
+
* are ignored — terminal state still comes exclusively from `squadrant crew signal`
|
|
3538
|
+
* (anti-#2576 invariant).
|
|
3539
|
+
*/
|
|
3540
|
+
observe(ev) {
|
|
3541
|
+
const snap = toSnapshot(ev);
|
|
3542
|
+
if (!snap || !this.deps)
|
|
3543
|
+
return;
|
|
3544
|
+
this.cache.set(snap.taskId, snap);
|
|
3545
|
+
this.deps.report(snap);
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3548
|
+
function toSnapshot(ev) {
|
|
3549
|
+
const now = Date.now();
|
|
3550
|
+
switch (ev.type) {
|
|
3551
|
+
// ── running: a turn is live ──────────────────────────────────────────────
|
|
3552
|
+
case "task.started":
|
|
3553
|
+
case "task.reattached":
|
|
3554
|
+
case "task.turn.started":
|
|
3555
|
+
case "task.delta":
|
|
3556
|
+
case "task.progress":
|
|
3557
|
+
return { taskId: ev.id, state: "running", alive: true, origin: "agent", at: now };
|
|
3558
|
+
// ── idle: turn ended, crew alive, awaiting next input ────────────────────
|
|
3559
|
+
// task.failed: the turn ended with an error, but the crew process is alive.
|
|
3560
|
+
// task.session.ended: process is gone (alive:false) — signals liveness loss.
|
|
3561
|
+
case "task.turn.completed":
|
|
3562
|
+
return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
|
|
3563
|
+
case "task.failed":
|
|
3564
|
+
return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
|
|
3565
|
+
case "task.session.ended":
|
|
3566
|
+
return { taskId: ev.id, state: "idle", alive: false, origin: "agent", at: now };
|
|
3567
|
+
// ── needsInput: crew is blocked on a human ───────────────────────────────
|
|
3568
|
+
case "task.approval.requested":
|
|
3569
|
+
return {
|
|
3570
|
+
taskId: ev.id,
|
|
3571
|
+
state: "needsInput",
|
|
3572
|
+
alive: true,
|
|
3573
|
+
origin: "agent",
|
|
3574
|
+
at: now,
|
|
3575
|
+
detail: { note: ev.question, reason: ev.kind }
|
|
3576
|
+
};
|
|
3577
|
+
case "task.input.requested":
|
|
3578
|
+
return {
|
|
3579
|
+
taskId: ev.id,
|
|
3580
|
+
state: "needsInput",
|
|
3581
|
+
alive: true,
|
|
3582
|
+
origin: "agent",
|
|
3583
|
+
at: now,
|
|
3584
|
+
detail: { note: ev.question }
|
|
3585
|
+
};
|
|
3586
|
+
// ── terminal / notify-only — ignored ────────────────────────────────────
|
|
3587
|
+
// task.done, task.blocked, task.cancelled: terminal state from crew signal only.
|
|
3588
|
+
// task.session, task.stalled, task.quiet, task.idle, task.timeout, etc.: no-op.
|
|
3589
|
+
default:
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3483
3594
|
// packages/agents/dist/codex/config.js
|
|
3484
3595
|
import { readFile as readFile5 } from "fs/promises";
|
|
3485
3596
|
import { homedir as homedir8 } from "os";
|
|
@@ -4709,17 +4820,346 @@ var DaemonCmux = class {
|
|
|
4709
4820
|
}
|
|
4710
4821
|
};
|
|
4711
4822
|
|
|
4823
|
+
// packages/workspaces/dist/cmux-daemon/cmux-store-source.js
|
|
4824
|
+
import { join as join15 } from "path";
|
|
4825
|
+
import { homedir as homedir10 } from "os";
|
|
4826
|
+
import { watch, readdirSync as readdirSync3, readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
|
|
4827
|
+
var CmuxStoreSource = class {
|
|
4828
|
+
name = "cmux-store";
|
|
4829
|
+
stateDir;
|
|
4830
|
+
debounceMs;
|
|
4831
|
+
isPidAlive;
|
|
4832
|
+
listFiles;
|
|
4833
|
+
readFile;
|
|
4834
|
+
fileExists;
|
|
4835
|
+
watchDir;
|
|
4836
|
+
scheduleTimer;
|
|
4837
|
+
cancelTimer;
|
|
4838
|
+
log;
|
|
4839
|
+
deps;
|
|
4840
|
+
stopWatcher;
|
|
4841
|
+
debounceTimer;
|
|
4842
|
+
/** taskId → last reported snapshot (for snapshot() liveness floor). */
|
|
4843
|
+
cache = /* @__PURE__ */ new Map();
|
|
4844
|
+
constructor(opts = {}) {
|
|
4845
|
+
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
|
|
4846
|
+
this.debounceMs = opts.debounceMs ?? 50;
|
|
4847
|
+
this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
|
|
4848
|
+
this.listFiles = opts.listFiles ?? defaultListFiles;
|
|
4849
|
+
this.readFile = opts.readFile ?? defaultReadFile;
|
|
4850
|
+
this.fileExists = opts.fileExists ?? existsSync10;
|
|
4851
|
+
this.watchDir = opts.watchDir ?? defaultWatchDir;
|
|
4852
|
+
this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
|
|
4853
|
+
this.cancelTimer = opts.cancelTimer ?? clearTimeout;
|
|
4854
|
+
this.log = opts.log ?? (() => {
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
start(deps) {
|
|
4858
|
+
this.deps = deps;
|
|
4859
|
+
this.scan();
|
|
4860
|
+
try {
|
|
4861
|
+
this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
|
|
4862
|
+
} catch (e) {
|
|
4863
|
+
this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
stop() {
|
|
4867
|
+
if (this.debounceTimer !== void 0) {
|
|
4868
|
+
this.cancelTimer(this.debounceTimer);
|
|
4869
|
+
this.debounceTimer = void 0;
|
|
4870
|
+
}
|
|
4871
|
+
this.stopWatcher?.();
|
|
4872
|
+
this.stopWatcher = void 0;
|
|
4873
|
+
this.deps = void 0;
|
|
4874
|
+
this.cache.clear();
|
|
4875
|
+
}
|
|
4876
|
+
/** Returns the last-reported snapshot for a known crew (liveness floor). */
|
|
4877
|
+
snapshot(taskId) {
|
|
4878
|
+
return this.cache.get(taskId);
|
|
4879
|
+
}
|
|
4880
|
+
// ── private ─────────────────────────────────────────────────────────────────
|
|
4881
|
+
scheduleDebounced() {
|
|
4882
|
+
if (this.debounceTimer !== void 0) {
|
|
4883
|
+
this.cancelTimer(this.debounceTimer);
|
|
4884
|
+
}
|
|
4885
|
+
this.debounceTimer = this.scheduleTimer(() => {
|
|
4886
|
+
this.debounceTimer = void 0;
|
|
4887
|
+
this.scan();
|
|
4888
|
+
}, this.debounceMs);
|
|
4889
|
+
}
|
|
4890
|
+
scan() {
|
|
4891
|
+
if (!this.deps)
|
|
4892
|
+
return;
|
|
4893
|
+
for (const filename of this.listFiles(this.stateDir)) {
|
|
4894
|
+
this.scanFile(filename);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
scanFile(filename) {
|
|
4898
|
+
const deps = this.deps;
|
|
4899
|
+
const filePath = join15(this.stateDir, filename);
|
|
4900
|
+
const lockPath = `${filePath}.lock`;
|
|
4901
|
+
if (this.fileExists(lockPath)) {
|
|
4902
|
+
this.log(`cmux-store: skipping ${filename} (locked)`);
|
|
4903
|
+
return;
|
|
4904
|
+
}
|
|
4905
|
+
const raw = this.readFile(filePath);
|
|
4906
|
+
if (!raw)
|
|
4907
|
+
return;
|
|
4908
|
+
let parsed;
|
|
4909
|
+
try {
|
|
4910
|
+
parsed = JSON.parse(raw);
|
|
4911
|
+
} catch {
|
|
4912
|
+
this.log(`cmux-store: failed to parse ${filename}`);
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
for (const session of Object.values(parsed.sessions ?? {})) {
|
|
4916
|
+
this.processSession(session, deps);
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
processSession(session, deps) {
|
|
4920
|
+
if (!session.sessionId || !session.cwd || typeof session.pid !== "number")
|
|
4921
|
+
return;
|
|
4922
|
+
const hint = {
|
|
4923
|
+
cwd: session.cwd,
|
|
4924
|
+
pid: session.pid,
|
|
4925
|
+
sessionId: session.sessionId
|
|
4926
|
+
};
|
|
4927
|
+
const resolved = deps.resolve(hint);
|
|
4928
|
+
if (!resolved)
|
|
4929
|
+
return;
|
|
4930
|
+
let alive = this.isPidAlive(session.pid);
|
|
4931
|
+
if (!alive && session.isRestorable === true && session.agentLifecycle === "idle") {
|
|
4932
|
+
alive = true;
|
|
4933
|
+
}
|
|
4934
|
+
const snap = {
|
|
4935
|
+
taskId: resolved.id,
|
|
4936
|
+
state: parseLifecycleState(session.agentLifecycle),
|
|
4937
|
+
alive,
|
|
4938
|
+
// "agent": the store carries the agent's own reported lifecycle state,
|
|
4939
|
+
// not a scan inference. needsInput from the store is authoritative.
|
|
4940
|
+
origin: "agent",
|
|
4941
|
+
at: Math.floor((session.updatedAt ?? 0) * 1e3),
|
|
4942
|
+
pid: session.pid,
|
|
4943
|
+
...session.lastBody ? { detail: { note: session.lastBody } } : {}
|
|
4944
|
+
};
|
|
4945
|
+
this.cache.set(resolved.id, snap);
|
|
4946
|
+
deps.report(snap);
|
|
4947
|
+
}
|
|
4948
|
+
};
|
|
4949
|
+
function parseLifecycleState(s) {
|
|
4950
|
+
if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
|
|
4951
|
+
return s;
|
|
4952
|
+
}
|
|
4953
|
+
return "unknown";
|
|
4954
|
+
}
|
|
4955
|
+
function defaultIsPidAlive2(pid) {
|
|
4956
|
+
try {
|
|
4957
|
+
process.kill(pid, 0);
|
|
4958
|
+
return true;
|
|
4959
|
+
} catch {
|
|
4960
|
+
return false;
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
function defaultListFiles(dir) {
|
|
4964
|
+
try {
|
|
4965
|
+
return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
|
|
4966
|
+
} catch {
|
|
4967
|
+
return [];
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
function defaultReadFile(path17) {
|
|
4971
|
+
try {
|
|
4972
|
+
return readFileSync9(path17, "utf-8");
|
|
4973
|
+
} catch {
|
|
4974
|
+
return void 0;
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
function defaultWatchDir(dir, cb) {
|
|
4978
|
+
const w = watch(dir, (_event, filename) => {
|
|
4979
|
+
if (typeof filename === "string" && filename.endsWith("-hook-sessions.json")) {
|
|
4980
|
+
cb();
|
|
4981
|
+
}
|
|
4982
|
+
});
|
|
4983
|
+
return () => w.close();
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
// packages/workspaces/dist/native-hooks/native-hook-source.js
|
|
4987
|
+
import { join as join16 } from "path";
|
|
4988
|
+
import { homedir as homedir11 } from "os";
|
|
4989
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
4990
|
+
var CLAUDE_HOOK_EVENTS = [
|
|
4991
|
+
["SessionStart", "session-start"],
|
|
4992
|
+
["UserPromptSubmit", "prompt-submit"],
|
|
4993
|
+
["PreToolUse", "pre-tool-use"],
|
|
4994
|
+
["Stop", "stop"],
|
|
4995
|
+
["Notification", "notification"],
|
|
4996
|
+
["PreToolUse", "ask-question", "AskUserQuestion"],
|
|
4997
|
+
["SessionEnd", "session-end"]
|
|
4998
|
+
];
|
|
4999
|
+
var DEFAULT_HOOK_CMD = "squadrant hooks";
|
|
5000
|
+
function installClaudeHooks(opts = {}) {
|
|
5001
|
+
const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
|
|
5002
|
+
const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
|
|
5003
|
+
const readFile6 = opts.readFile ?? defaultReadFile2;
|
|
5004
|
+
const writeFile5 = opts.writeFile ?? defaultWriteFile;
|
|
5005
|
+
const log = opts.log ?? (() => {
|
|
5006
|
+
});
|
|
5007
|
+
let settings = {};
|
|
5008
|
+
const raw = readFile6(settingsPath);
|
|
5009
|
+
if (raw) {
|
|
5010
|
+
try {
|
|
5011
|
+
settings = JSON.parse(raw);
|
|
5012
|
+
} catch {
|
|
5013
|
+
log(`native-hook: failed to parse ${settingsPath} \u2014 hooks section will be reset`);
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
if (typeof settings.hooks !== "object" || settings.hooks === null || Array.isArray(settings.hooks)) {
|
|
5017
|
+
settings.hooks = {};
|
|
5018
|
+
}
|
|
5019
|
+
const hooks = settings.hooks;
|
|
5020
|
+
let changed = false;
|
|
5021
|
+
for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
|
|
5022
|
+
if (!Array.isArray(hooks[eventName])) {
|
|
5023
|
+
hooks[eventName] = [];
|
|
5024
|
+
}
|
|
5025
|
+
const entries = hooks[eventName];
|
|
5026
|
+
const command = `${hookCmd} claude ${sub}`;
|
|
5027
|
+
const hookMatcher = matcher ?? "";
|
|
5028
|
+
const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
|
|
5029
|
+
if (!alreadyPresent) {
|
|
5030
|
+
entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
|
|
5031
|
+
changed = true;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
if (changed) {
|
|
5035
|
+
writeFile5(settingsPath, JSON.stringify(settings, null, 2));
|
|
5036
|
+
}
|
|
5037
|
+
return settingsPath;
|
|
5038
|
+
}
|
|
5039
|
+
function mapSubToLifecycle(sub) {
|
|
5040
|
+
switch (sub) {
|
|
5041
|
+
case "session-start":
|
|
5042
|
+
return "running";
|
|
5043
|
+
case "prompt-submit":
|
|
5044
|
+
return "running";
|
|
5045
|
+
case "pre-tool-use":
|
|
5046
|
+
return "running";
|
|
5047
|
+
case "stop":
|
|
5048
|
+
return "idle";
|
|
5049
|
+
case "notification":
|
|
5050
|
+
return "needsInput";
|
|
5051
|
+
case "ask-question":
|
|
5052
|
+
return "needsInput";
|
|
5053
|
+
case "session-end":
|
|
5054
|
+
return "session-end";
|
|
5055
|
+
default:
|
|
5056
|
+
return null;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
var NativeHookSource = class {
|
|
5060
|
+
name = "native-hook";
|
|
5061
|
+
hookInstall;
|
|
5062
|
+
log;
|
|
5063
|
+
deps;
|
|
5064
|
+
/** taskId → last-reported snapshot, for snapshot() liveness floor. */
|
|
5065
|
+
cache = /* @__PURE__ */ new Map();
|
|
5066
|
+
constructor(opts = {}) {
|
|
5067
|
+
this.hookInstall = opts.hookInstall ?? {};
|
|
5068
|
+
this.log = opts.log ?? (() => {
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
start(deps) {
|
|
5072
|
+
this.deps = deps;
|
|
5073
|
+
}
|
|
5074
|
+
stop() {
|
|
5075
|
+
this.deps = void 0;
|
|
5076
|
+
this.cache.clear();
|
|
5077
|
+
}
|
|
5078
|
+
/** Returns the last-reported snapshot for a known crew (liveness floor poll). */
|
|
5079
|
+
snapshot(taskId) {
|
|
5080
|
+
return this.cache.get(taskId);
|
|
5081
|
+
}
|
|
5082
|
+
/**
|
|
5083
|
+
* Install squadrant-owned hooks into ~/.claude/settings.json.
|
|
5084
|
+
* Idempotent — safe to call on every project init or crew spawn.
|
|
5085
|
+
* Returns the path to the settings file.
|
|
5086
|
+
*/
|
|
5087
|
+
install() {
|
|
5088
|
+
return installClaudeHooks(this.hookInstall);
|
|
5089
|
+
}
|
|
5090
|
+
/**
|
|
5091
|
+
* Receive a lifecycle hook event from the daemon and report a LifecycleSnapshot.
|
|
5092
|
+
*
|
|
5093
|
+
* The daemon's 'squadrant hooks claude <sub>' CLI subcommand calls this after
|
|
5094
|
+
* reading SQUADRANT_CREW_TASK_ID from the hook's process environment — the only
|
|
5095
|
+
* collision-proof correlation key (blueprint §2.2 priority 1).
|
|
5096
|
+
*
|
|
5097
|
+
* @param sub Sub-event alias: "session-start" | "prompt-submit" | "stop" | …
|
|
5098
|
+
* @param taskId SQUADRANT_CREW_TASK_ID extracted from the hook process env.
|
|
5099
|
+
* @param pid Optional: OS pid from the hook's process env or argv.
|
|
5100
|
+
* @param payload Optional: parsed JSON payload from hook stdin (best-effort detail).
|
|
5101
|
+
*/
|
|
5102
|
+
handleHook(sub, taskId, pid, payload) {
|
|
5103
|
+
if (!this.deps)
|
|
5104
|
+
return;
|
|
5105
|
+
const mapped = mapSubToLifecycle(sub);
|
|
5106
|
+
if (mapped === null) {
|
|
5107
|
+
this.log(`native-hook: unknown sub '${sub}' for task ${taskId} \u2014 ignored`);
|
|
5108
|
+
return;
|
|
5109
|
+
}
|
|
5110
|
+
const isSessionEnd = mapped === "session-end";
|
|
5111
|
+
const state = isSessionEnd ? "unknown" : mapped;
|
|
5112
|
+
const detail = extractDetail(sub, payload);
|
|
5113
|
+
const snap = {
|
|
5114
|
+
taskId,
|
|
5115
|
+
state,
|
|
5116
|
+
alive: !isSessionEnd,
|
|
5117
|
+
origin: "agent",
|
|
5118
|
+
at: Date.now(),
|
|
5119
|
+
...pid !== void 0 ? { pid } : {},
|
|
5120
|
+
...detail ? { detail } : {}
|
|
5121
|
+
};
|
|
5122
|
+
this.cache.set(taskId, snap);
|
|
5123
|
+
this.deps.report(snap);
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
function extractDetail(sub, payload) {
|
|
5127
|
+
if (!payload || typeof payload !== "object")
|
|
5128
|
+
return void 0;
|
|
5129
|
+
const p = payload;
|
|
5130
|
+
if (sub === "notification") {
|
|
5131
|
+
const note = typeof p.message === "string" ? p.message : void 0;
|
|
5132
|
+
return note ? { note } : void 0;
|
|
5133
|
+
}
|
|
5134
|
+
if (sub === "pre-tool-use") {
|
|
5135
|
+
const tool = typeof p.tool_name === "string" ? p.tool_name : void 0;
|
|
5136
|
+
return tool ? { tool } : void 0;
|
|
5137
|
+
}
|
|
5138
|
+
return void 0;
|
|
5139
|
+
}
|
|
5140
|
+
function defaultReadFile2(path17) {
|
|
5141
|
+
try {
|
|
5142
|
+
return readFileSync10(path17, "utf-8");
|
|
5143
|
+
} catch {
|
|
5144
|
+
return void 0;
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
function defaultWriteFile(path17, content) {
|
|
5148
|
+
mkdirSync6(path17.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
5149
|
+
writeFileSync7(path17, content, "utf-8");
|
|
5150
|
+
}
|
|
5151
|
+
|
|
4712
5152
|
// packages/workspaces/dist/crew-pane.js
|
|
4713
5153
|
import net from "net";
|
|
4714
5154
|
|
|
4715
5155
|
// packages/cli/src/squadrantd.ts
|
|
4716
5156
|
var SELF_PATH2 = fileURLToPath3(import.meta.url);
|
|
4717
|
-
var CLI_BIN =
|
|
4718
|
-
var DAEMON_SOCK =
|
|
5157
|
+
var CLI_BIN = join17(dirname5(SELF_PATH2), "index.js");
|
|
5158
|
+
var DAEMON_SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
4719
5159
|
function readPkgVersion() {
|
|
4720
5160
|
try {
|
|
4721
|
-
const pkgPath =
|
|
4722
|
-
return JSON.parse(
|
|
5161
|
+
const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
|
|
5162
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8")).version ?? "unknown";
|
|
4723
5163
|
} catch {
|
|
4724
5164
|
return "unknown";
|
|
4725
5165
|
}
|
|
@@ -4757,6 +5197,7 @@ function startSquadrantd(opts = {}) {
|
|
|
4757
5197
|
ctx.broadcast = broadcast;
|
|
4758
5198
|
ctx.schedulePromotion = schedulePromotion;
|
|
4759
5199
|
ctx.cancelPromotionsFor = cancelPromotionsFor;
|
|
5200
|
+
const codexAppServerSource = new CodexAppServerSource();
|
|
4760
5201
|
const codexDriver = opts.codexDriver ?? new CodexInteractiveDriver({
|
|
4761
5202
|
emit: (ev) => {
|
|
4762
5203
|
const found = ctx.store.listAll().find((r) => r.id === ev.id);
|
|
@@ -4776,6 +5217,7 @@ function startSquadrantd(opts = {}) {
|
|
|
4776
5217
|
ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
|
|
4777
5218
|
} else if (ev.type === "task.reattached")
|
|
4778
5219
|
ctx.broadcast(ev.id, { type: "reattached", taskId: ev.id });
|
|
5220
|
+
codexAppServerSource.observe(ev);
|
|
4779
5221
|
}
|
|
4780
5222
|
});
|
|
4781
5223
|
const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
|
|
@@ -4800,9 +5242,11 @@ function startSquadrantd(opts = {}) {
|
|
|
4800
5242
|
(r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
|
|
4801
5243
|
);
|
|
4802
5244
|
},
|
|
4803
|
-
cursorFile:
|
|
5245
|
+
cursorFile: join17(stateRoot, "cmux-events.seq"),
|
|
4804
5246
|
log
|
|
4805
5247
|
});
|
|
5248
|
+
const cmuxStoreSource = new CmuxStoreSource({ log });
|
|
5249
|
+
const nativeHookSource = new NativeHookSource({ log });
|
|
4806
5250
|
ctx.codexDriver = codexDriver;
|
|
4807
5251
|
ctx.opencodeBridge = opencodeBridge;
|
|
4808
5252
|
ctx.cmuxEventsBridge = cmuxEventsBridge;
|
|
@@ -4830,7 +5274,135 @@ function startSquadrantd(opts = {}) {
|
|
|
4830
5274
|
activeHeadlessKills.delete(handle.kill);
|
|
4831
5275
|
}
|
|
4832
5276
|
});
|
|
4833
|
-
|
|
5277
|
+
const h = startDaemon(ctx, { ...opts, launchHeadless }, PKG_VERSION);
|
|
5278
|
+
if (!process.env.VITEST) {
|
|
5279
|
+
const prevSnaps = /* @__PURE__ */ new Map();
|
|
5280
|
+
const storeDeps = {
|
|
5281
|
+
resolve: (hint) => {
|
|
5282
|
+
if (!hint.cwd && hint.pid == null) return void 0;
|
|
5283
|
+
return store.listAll().find(
|
|
5284
|
+
(r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && (r.cwd === hint.cwd || hint.pid != null && r.pid === hint.pid)
|
|
5285
|
+
);
|
|
5286
|
+
},
|
|
5287
|
+
report: (snap) => {
|
|
5288
|
+
const found = store.listAll().find((r) => r.id === snap.taskId);
|
|
5289
|
+
if (!found) return;
|
|
5290
|
+
const prev = prevSnaps.get(snap.taskId);
|
|
5291
|
+
const newState = reduceLifecycle(prev, snap);
|
|
5292
|
+
const changed = !prev || newState !== prev.state;
|
|
5293
|
+
prevSnaps.set(snap.taskId, snap);
|
|
5294
|
+
if (!snap.alive) {
|
|
5295
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
if (!changed) return;
|
|
5299
|
+
if (newState === "idle") {
|
|
5300
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "cmux-store" } });
|
|
5301
|
+
} else if (newState === "running") {
|
|
5302
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
|
|
5303
|
+
} else if (newState === "needsInput") {
|
|
5304
|
+
const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
|
|
5305
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
|
|
5306
|
+
}
|
|
5307
|
+
},
|
|
5308
|
+
log
|
|
5309
|
+
};
|
|
5310
|
+
try {
|
|
5311
|
+
cmuxStoreSource.start(storeDeps);
|
|
5312
|
+
} catch (e) {
|
|
5313
|
+
log(`cmux store source start failed: ${e.message}`);
|
|
5314
|
+
}
|
|
5315
|
+
try {
|
|
5316
|
+
nativeHookSource.install();
|
|
5317
|
+
} catch (e) {
|
|
5318
|
+
log(`native hook install failed: ${e.message}`);
|
|
5319
|
+
}
|
|
5320
|
+
const hookPrevSnaps = /* @__PURE__ */ new Map();
|
|
5321
|
+
const hookDeps = {
|
|
5322
|
+
resolve: (hint) => {
|
|
5323
|
+
if (!hint.cwd && hint.pid == null) return void 0;
|
|
5324
|
+
return store.listAll().find(
|
|
5325
|
+
(r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && (r.cwd === hint.cwd || hint.pid != null && r.pid === hint.pid)
|
|
5326
|
+
);
|
|
5327
|
+
},
|
|
5328
|
+
report: (snap) => {
|
|
5329
|
+
const found = store.listAll().find((r) => r.id === snap.taskId);
|
|
5330
|
+
if (!found) return;
|
|
5331
|
+
const prev = hookPrevSnaps.get(snap.taskId);
|
|
5332
|
+
const newState = reduceLifecycle(prev, snap);
|
|
5333
|
+
const changed = !prev || newState !== prev.state;
|
|
5334
|
+
hookPrevSnaps.set(snap.taskId, snap);
|
|
5335
|
+
if (!snap.alive) {
|
|
5336
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
|
|
5337
|
+
return;
|
|
5338
|
+
}
|
|
5339
|
+
if (!changed) return;
|
|
5340
|
+
if (newState === "idle") {
|
|
5341
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "native-hook" } });
|
|
5342
|
+
} else if (newState === "running") {
|
|
5343
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
|
|
5344
|
+
} else if (newState === "needsInput") {
|
|
5345
|
+
const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
|
|
5346
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
|
|
5347
|
+
}
|
|
5348
|
+
},
|
|
5349
|
+
log
|
|
5350
|
+
};
|
|
5351
|
+
try {
|
|
5352
|
+
nativeHookSource.start(hookDeps);
|
|
5353
|
+
} catch (e) {
|
|
5354
|
+
log(`native hook source start failed: ${e.message}`);
|
|
5355
|
+
}
|
|
5356
|
+
const codexPrevSnaps = /* @__PURE__ */ new Map();
|
|
5357
|
+
const codexSourceDeps = {
|
|
5358
|
+
resolve: () => void 0,
|
|
5359
|
+
// taskId comes from ControlEvent.id; resolve() unused
|
|
5360
|
+
report: (snap) => {
|
|
5361
|
+
const found = store.listAll().find((r) => r.id === snap.taskId);
|
|
5362
|
+
if (!found) return;
|
|
5363
|
+
const prev = codexPrevSnaps.get(snap.taskId);
|
|
5364
|
+
const newState = reduceLifecycle(prev, snap);
|
|
5365
|
+
const changed = !prev || newState !== prev.state;
|
|
5366
|
+
codexPrevSnaps.set(snap.taskId, snap);
|
|
5367
|
+
if (!snap.alive) {
|
|
5368
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.session.ended", id: snap.taskId } });
|
|
5369
|
+
return;
|
|
5370
|
+
}
|
|
5371
|
+
if (!changed) return;
|
|
5372
|
+
if (newState === "idle") {
|
|
5373
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.turn.completed", id: snap.taskId, turnId: "codex-appserver" } });
|
|
5374
|
+
} else if (newState === "running") {
|
|
5375
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.progress", id: snap.taskId } });
|
|
5376
|
+
} else if (newState === "needsInput") {
|
|
5377
|
+
const question = snap.detail?.note ?? snap.detail?.reason ?? "crew awaiting input";
|
|
5378
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: { type: "task.blocked", id: snap.taskId, reason: "needsInput", question } });
|
|
5379
|
+
}
|
|
5380
|
+
},
|
|
5381
|
+
log
|
|
5382
|
+
};
|
|
5383
|
+
try {
|
|
5384
|
+
codexAppServerSource.start(codexSourceDeps);
|
|
5385
|
+
} catch (e) {
|
|
5386
|
+
log(`codex app-server source start failed: ${e.message}`);
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
const origStop = h.stop.bind(h);
|
|
5390
|
+
h.stop = async () => {
|
|
5391
|
+
try {
|
|
5392
|
+
cmuxStoreSource.stop();
|
|
5393
|
+
} catch {
|
|
5394
|
+
}
|
|
5395
|
+
try {
|
|
5396
|
+
nativeHookSource.stop();
|
|
5397
|
+
} catch {
|
|
5398
|
+
}
|
|
5399
|
+
try {
|
|
5400
|
+
codexAppServerSource.stop();
|
|
5401
|
+
} catch {
|
|
5402
|
+
}
|
|
5403
|
+
return origStop();
|
|
5404
|
+
};
|
|
5405
|
+
return h;
|
|
4834
5406
|
}
|
|
4835
5407
|
if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
|
|
4836
5408
|
void (async () => {
|
|
@@ -4839,7 +5411,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
|
|
|
4839
5411
|
process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
|
|
4840
5412
|
process.exit(0);
|
|
4841
5413
|
}
|
|
4842
|
-
const sock =
|
|
5414
|
+
const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
4843
5415
|
if (await isDaemonSocketLive(sock)) {
|
|
4844
5416
|
process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
|
|
4845
5417
|
`);
|