squadrant 0.12.0 → 0.13.0
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 +757 -90
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +592 -14
- 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";
|
|
@@ -89,14 +89,14 @@ function getDefaultConfig() {
|
|
|
89
89
|
models: {
|
|
90
90
|
command: "opus",
|
|
91
91
|
captain: "opus",
|
|
92
|
-
crew: "
|
|
92
|
+
crew: "sonnet",
|
|
93
93
|
exploration: "haiku",
|
|
94
94
|
review: "opus"
|
|
95
95
|
},
|
|
96
96
|
roles: {
|
|
97
97
|
command: { agent: "claude", model: "opus" },
|
|
98
98
|
captain: { agent: "claude", model: "opus" },
|
|
99
|
-
crew: { agent: "claude", model: "
|
|
99
|
+
crew: { agent: "claude", model: "sonnet" },
|
|
100
100
|
exploration: { agent: "claude", model: "haiku" },
|
|
101
101
|
side: { agent: "claude", model: "opus" }
|
|
102
102
|
},
|
|
@@ -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";
|
|
@@ -4394,8 +4505,14 @@ function createCmuxDriver() {
|
|
|
4394
4505
|
}
|
|
4395
4506
|
},
|
|
4396
4507
|
async sendToPane(pane, message) {
|
|
4397
|
-
await
|
|
4398
|
-
await
|
|
4508
|
+
await this.pasteToPane(pane, message);
|
|
4509
|
+
await this.sendKeyToPane(pane, "Enter");
|
|
4510
|
+
},
|
|
4511
|
+
async pasteToPane(pane, text) {
|
|
4512
|
+
await cmux(["send", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, sanitizeForCmuxSend(text)]);
|
|
4513
|
+
},
|
|
4514
|
+
async sendKeyToPane(pane, key) {
|
|
4515
|
+
await cmux(["send-key", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, key]);
|
|
4399
4516
|
},
|
|
4400
4517
|
async readPaneScreen(pane) {
|
|
4401
4518
|
try {
|
|
@@ -4703,17 +4820,346 @@ var DaemonCmux = class {
|
|
|
4703
4820
|
}
|
|
4704
4821
|
};
|
|
4705
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
|
+
|
|
4706
5152
|
// packages/workspaces/dist/crew-pane.js
|
|
4707
5153
|
import net from "net";
|
|
4708
5154
|
|
|
4709
5155
|
// packages/cli/src/squadrantd.ts
|
|
4710
5156
|
var SELF_PATH2 = fileURLToPath3(import.meta.url);
|
|
4711
|
-
var CLI_BIN =
|
|
4712
|
-
var DAEMON_SOCK =
|
|
5157
|
+
var CLI_BIN = join17(dirname5(SELF_PATH2), "index.js");
|
|
5158
|
+
var DAEMON_SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
4713
5159
|
function readPkgVersion() {
|
|
4714
5160
|
try {
|
|
4715
|
-
const pkgPath =
|
|
4716
|
-
return JSON.parse(
|
|
5161
|
+
const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
|
|
5162
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8")).version ?? "unknown";
|
|
4717
5163
|
} catch {
|
|
4718
5164
|
return "unknown";
|
|
4719
5165
|
}
|
|
@@ -4751,6 +5197,7 @@ function startSquadrantd(opts = {}) {
|
|
|
4751
5197
|
ctx.broadcast = broadcast;
|
|
4752
5198
|
ctx.schedulePromotion = schedulePromotion;
|
|
4753
5199
|
ctx.cancelPromotionsFor = cancelPromotionsFor;
|
|
5200
|
+
const codexAppServerSource = new CodexAppServerSource();
|
|
4754
5201
|
const codexDriver = opts.codexDriver ?? new CodexInteractiveDriver({
|
|
4755
5202
|
emit: (ev) => {
|
|
4756
5203
|
const found = ctx.store.listAll().find((r) => r.id === ev.id);
|
|
@@ -4770,6 +5217,7 @@ function startSquadrantd(opts = {}) {
|
|
|
4770
5217
|
ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
|
|
4771
5218
|
} else if (ev.type === "task.reattached")
|
|
4772
5219
|
ctx.broadcast(ev.id, { type: "reattached", taskId: ev.id });
|
|
5220
|
+
codexAppServerSource.observe(ev);
|
|
4773
5221
|
}
|
|
4774
5222
|
});
|
|
4775
5223
|
const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
|
|
@@ -4794,9 +5242,11 @@ function startSquadrantd(opts = {}) {
|
|
|
4794
5242
|
(r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
|
|
4795
5243
|
);
|
|
4796
5244
|
},
|
|
4797
|
-
cursorFile:
|
|
5245
|
+
cursorFile: join17(stateRoot, "cmux-events.seq"),
|
|
4798
5246
|
log
|
|
4799
5247
|
});
|
|
5248
|
+
const cmuxStoreSource = new CmuxStoreSource({ log });
|
|
5249
|
+
const nativeHookSource = new NativeHookSource({ log });
|
|
4800
5250
|
ctx.codexDriver = codexDriver;
|
|
4801
5251
|
ctx.opencodeBridge = opencodeBridge;
|
|
4802
5252
|
ctx.cmuxEventsBridge = cmuxEventsBridge;
|
|
@@ -4824,7 +5274,135 @@ function startSquadrantd(opts = {}) {
|
|
|
4824
5274
|
activeHeadlessKills.delete(handle.kill);
|
|
4825
5275
|
}
|
|
4826
5276
|
});
|
|
4827
|
-
|
|
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;
|
|
4828
5406
|
}
|
|
4829
5407
|
if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
|
|
4830
5408
|
void (async () => {
|
|
@@ -4833,7 +5411,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
|
|
|
4833
5411
|
process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
|
|
4834
5412
|
process.exit(0);
|
|
4835
5413
|
}
|
|
4836
|
-
const sock =
|
|
5414
|
+
const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
4837
5415
|
if (await isDaemonSocketLive(sock)) {
|
|
4838
5416
|
process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
|
|
4839
5417
|
`);
|