squadrant 0.16.5 → 0.17.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 +1619 -482
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +202 -137
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +35 -10
- package/scripts/read-handoff.sh +16 -4
package/dist/index.js
CHANGED
|
@@ -205,6 +205,14 @@ var init_control = __esm({
|
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
// packages/shared/dist/types/work.js
|
|
209
|
+
var TERMINAL_WORK_STATES;
|
|
210
|
+
var init_work = __esm({
|
|
211
|
+
"packages/shared/dist/types/work.js"() {
|
|
212
|
+
TERMINAL_WORK_STATES = /* @__PURE__ */ new Set(["done", "cancelled"]);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
208
216
|
// packages/shared/dist/types/projection.js
|
|
209
217
|
var init_projection = __esm({
|
|
210
218
|
"packages/shared/dist/types/projection.js"() {
|
|
@@ -226,22 +234,22 @@ function defaultCmuxConfigPath() {
|
|
|
226
234
|
return join(homedir(), ".config", "cmux", "cmux.json");
|
|
227
235
|
}
|
|
228
236
|
function ensureSocketAutomation(opts = {}) {
|
|
229
|
-
const
|
|
230
|
-
if (!existsSync(
|
|
231
|
-
mkdirSync(dirname(
|
|
232
|
-
writeFileSync(
|
|
233
|
-
return { path:
|
|
237
|
+
const path34 = opts.path ?? defaultCmuxConfigPath();
|
|
238
|
+
if (!existsSync(path34)) {
|
|
239
|
+
mkdirSync(dirname(path34), { recursive: true });
|
|
240
|
+
writeFileSync(path34, MINIMAL_TEMPLATE);
|
|
241
|
+
return { path: path34, changed: true, alreadySet: false };
|
|
234
242
|
}
|
|
235
|
-
const text = readFileSync(
|
|
243
|
+
const text = readFileSync(path34, "utf-8");
|
|
236
244
|
const current = parse(text)?.automation?.socketControlMode;
|
|
237
245
|
if (current === AUTOMATION_MODE) {
|
|
238
|
-
return { path:
|
|
246
|
+
return { path: path34, changed: false, alreadySet: true };
|
|
239
247
|
}
|
|
240
248
|
const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
|
|
241
249
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
242
250
|
});
|
|
243
|
-
writeFileSync(
|
|
244
|
-
return { path:
|
|
251
|
+
writeFileSync(path34, applyEdits(text, edits));
|
|
252
|
+
return { path: path34, changed: true, alreadySet: false };
|
|
245
253
|
}
|
|
246
254
|
var SOCKET_CONTROL_MODE_PATH, AUTOMATION_MODE, MINIMAL_TEMPLATE;
|
|
247
255
|
var init_cmux_config = __esm({
|
|
@@ -394,9 +402,9 @@ import { dirname as dirname2, join as join4 } from "path";
|
|
|
394
402
|
function defaultStatePath() {
|
|
395
403
|
return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
|
|
396
404
|
}
|
|
397
|
-
function readState(
|
|
405
|
+
function readState(path34) {
|
|
398
406
|
try {
|
|
399
|
-
return JSON.parse(readFileSync4(
|
|
407
|
+
return JSON.parse(readFileSync4(path34, "utf-8"));
|
|
400
408
|
} catch {
|
|
401
409
|
return {};
|
|
402
410
|
}
|
|
@@ -644,8 +652,8 @@ function formatUpdateNotice(latest, current) {
|
|
|
644
652
|
return `\u2B06 squadrant ${latest} available (you have ${current}) \u2014 npm i -g squadrant@latest`;
|
|
645
653
|
}
|
|
646
654
|
async function fetchLatestVersion(requestFn = requestJson, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
647
|
-
const timeout = new Promise((
|
|
648
|
-
const timer = setTimeout(() =>
|
|
655
|
+
const timeout = new Promise((resolve4) => {
|
|
656
|
+
const timer = setTimeout(() => resolve4(null), timeoutMs);
|
|
649
657
|
timer.unref?.();
|
|
650
658
|
});
|
|
651
659
|
const request = (async () => {
|
|
@@ -717,11 +725,11 @@ var init_update_check = __esm({
|
|
|
717
725
|
CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
718
726
|
FAILURE_RETRY_MS = 60 * 60 * 1e3;
|
|
719
727
|
FETCH_TIMEOUT_MS = 1500;
|
|
720
|
-
requestJson = (url, timeoutMs) => new Promise((
|
|
728
|
+
requestJson = (url, timeoutMs) => new Promise((resolve4) => {
|
|
721
729
|
const req = https.get(url, { headers: { "user-agent": "squadrant-update-check" } }, (res) => {
|
|
722
730
|
if (res.statusCode !== 200) {
|
|
723
731
|
res.resume();
|
|
724
|
-
|
|
732
|
+
resolve4(null);
|
|
725
733
|
return;
|
|
726
734
|
}
|
|
727
735
|
let body = "";
|
|
@@ -729,15 +737,15 @@ var init_update_check = __esm({
|
|
|
729
737
|
res.on("data", (chunk) => body += chunk);
|
|
730
738
|
res.on("end", () => {
|
|
731
739
|
try {
|
|
732
|
-
|
|
740
|
+
resolve4(JSON.parse(body));
|
|
733
741
|
} catch {
|
|
734
|
-
|
|
742
|
+
resolve4(null);
|
|
735
743
|
}
|
|
736
744
|
});
|
|
737
745
|
});
|
|
738
746
|
req.on("socket", (socket) => socket.unref());
|
|
739
747
|
req.setTimeout(timeoutMs, () => req.destroy());
|
|
740
|
-
req.on("error", () =>
|
|
748
|
+
req.on("error", () => resolve4(null));
|
|
741
749
|
});
|
|
742
750
|
}
|
|
743
751
|
});
|
|
@@ -929,6 +937,22 @@ function mirrorFlat(src, dest, match, chmod) {
|
|
|
929
937
|
}
|
|
930
938
|
}
|
|
931
939
|
}
|
|
940
|
+
function mirrorPluginSubset(src, dest, skills) {
|
|
941
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
942
|
+
mirrorDir(path5.join(src, ".claude-plugin"), path5.join(dest, ".claude-plugin"));
|
|
943
|
+
const skillsDest = path5.join(dest, "skills");
|
|
944
|
+
fs6.mkdirSync(skillsDest, { recursive: true });
|
|
945
|
+
for (const name of skills) {
|
|
946
|
+
const skillSrc = path5.join(src, "skills", name);
|
|
947
|
+
if (fs6.existsSync(skillSrc))
|
|
948
|
+
mirrorDir(skillSrc, path5.join(skillsDest, name));
|
|
949
|
+
}
|
|
950
|
+
for (const entry of fs6.readdirSync(skillsDest, { withFileTypes: true })) {
|
|
951
|
+
if (!skills.includes(entry.name)) {
|
|
952
|
+
fs6.rmSync(path5.join(skillsDest, entry.name), { recursive: true, force: true });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
932
956
|
function ensureRuntimeSynced(opts) {
|
|
933
957
|
const targets = opts.targets ?? MANAGED_TARGETS;
|
|
934
958
|
for (const t of targets) {
|
|
@@ -939,8 +963,10 @@ function ensureRuntimeSynced(opts) {
|
|
|
939
963
|
const destDir = path5.join(opts.runtimeRoot, t.name);
|
|
940
964
|
if (t.mode === "tree") {
|
|
941
965
|
mirrorDir(srcDir, destDir);
|
|
942
|
-
} else {
|
|
966
|
+
} else if (t.mode === "flat") {
|
|
943
967
|
mirrorFlat(srcDir, destDir, t.match, t.chmod);
|
|
968
|
+
} else {
|
|
969
|
+
mirrorPluginSubset(srcDir, destDir, t.skills);
|
|
944
970
|
}
|
|
945
971
|
} catch (err) {
|
|
946
972
|
process.stderr.write(`squadrant: runtime sync skipped for ${t.name}: ${err.message}
|
|
@@ -948,11 +974,13 @@ function ensureRuntimeSynced(opts) {
|
|
|
948
974
|
}
|
|
949
975
|
}
|
|
950
976
|
}
|
|
951
|
-
var MANAGED_TARGETS;
|
|
977
|
+
var CREW_SKILLS, MANAGED_TARGETS;
|
|
952
978
|
var init_runtime_sync = __esm({
|
|
953
979
|
"packages/shared/dist/lib/runtime-sync.js"() {
|
|
980
|
+
CREW_SKILLS = ["karpathy-principles"];
|
|
954
981
|
MANAGED_TARGETS = [
|
|
955
982
|
{ name: "plugin", srcRel: "plugin", mode: "tree" },
|
|
983
|
+
{ name: "plugin-crew", srcRel: "plugin", mode: "subset", skills: CREW_SKILLS },
|
|
956
984
|
{ name: "scripts", srcRel: "scripts", mode: "flat", match: /\.sh$/, chmod: 493 },
|
|
957
985
|
{
|
|
958
986
|
name: "templates",
|
|
@@ -1209,6 +1237,7 @@ var init_daemon_keys = __esm({
|
|
|
1209
1237
|
var dist_exports = {};
|
|
1210
1238
|
__export(dist_exports, {
|
|
1211
1239
|
AUTOMATION_MODE: () => AUTOMATION_MODE,
|
|
1240
|
+
CREW_SKILLS: () => CREW_SKILLS,
|
|
1212
1241
|
DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
|
|
1213
1242
|
DEFAULT_NOTIFY: () => DEFAULT_NOTIFY,
|
|
1214
1243
|
MANAGED_TARGETS: () => MANAGED_TARGETS,
|
|
@@ -1216,6 +1245,7 @@ __export(dist_exports, {
|
|
|
1216
1245
|
SOCKET_CONTROL_MODE_PATH: () => SOCKET_CONTROL_MODE_PATH,
|
|
1217
1246
|
SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
|
|
1218
1247
|
TERMINAL_STATES: () => TERMINAL_STATES,
|
|
1248
|
+
TERMINAL_WORK_STATES: () => TERMINAL_WORK_STATES,
|
|
1219
1249
|
UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
|
|
1220
1250
|
addWorktree: () => addWorktree,
|
|
1221
1251
|
applySafeFixes: () => applySafeFixes,
|
|
@@ -1283,6 +1313,7 @@ var init_dist = __esm({
|
|
|
1283
1313
|
init_runtime();
|
|
1284
1314
|
init_liveness();
|
|
1285
1315
|
init_control();
|
|
1316
|
+
init_work();
|
|
1286
1317
|
init_projection();
|
|
1287
1318
|
init_workspaces();
|
|
1288
1319
|
init_cmux_autoconfig();
|
|
@@ -1323,6 +1354,11 @@ function nextPendingTool(current, ev, now) {
|
|
|
1323
1354
|
return void 0;
|
|
1324
1355
|
return current;
|
|
1325
1356
|
}
|
|
1357
|
+
function nextPendingMonitor(current, ev, now) {
|
|
1358
|
+
if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
|
|
1359
|
+
return { since: now };
|
|
1360
|
+
return current;
|
|
1361
|
+
}
|
|
1326
1362
|
function reduce(rec, ev, now) {
|
|
1327
1363
|
if (ev.type === "task.reopened") {
|
|
1328
1364
|
return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
|
|
@@ -1339,14 +1375,17 @@ function reduce(rec, ev, now) {
|
|
|
1339
1375
|
sessionId: ev.sessionId ?? rec.sessionId,
|
|
1340
1376
|
question: void 0,
|
|
1341
1377
|
// resuming after a blocked→reply clears the question
|
|
1342
|
-
pendingTool: void 0
|
|
1378
|
+
pendingTool: void 0,
|
|
1343
1379
|
// #354: a new turn closes any prior tool window
|
|
1380
|
+
pendingMonitor: void 0
|
|
1381
|
+
// #594a: same reset — a new turn moots any prior watch
|
|
1344
1382
|
};
|
|
1345
1383
|
case "task.progress": {
|
|
1346
1384
|
const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
|
|
1385
|
+
const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
|
|
1347
1386
|
if (isStickyAttention(rec.state))
|
|
1348
|
-
return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
|
|
1349
|
-
const b = { ...base, pendingTool };
|
|
1387
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
|
|
1388
|
+
const b = { ...base, pendingTool, pendingMonitor };
|
|
1350
1389
|
if (rec.state === "awaiting-input" || rec.state === "stalled")
|
|
1351
1390
|
return { ...stampAttempt(b, {}, now), state: "working" };
|
|
1352
1391
|
return stampAttempt(b, {}, now);
|
|
@@ -1360,9 +1399,9 @@ function reduce(rec, ev, now) {
|
|
|
1360
1399
|
case "task.blocked":
|
|
1361
1400
|
if (rec.state === "blocked")
|
|
1362
1401
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
1363
|
-
return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
1402
|
+
return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
|
|
1364
1403
|
case "task.review":
|
|
1365
|
-
return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0 };
|
|
1404
|
+
return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
|
|
1366
1405
|
case "task.done":
|
|
1367
1406
|
if (rec.state === "review" && ev.source !== "approve") {
|
|
1368
1407
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
@@ -1377,19 +1416,19 @@ function reduce(rec, ev, now) {
|
|
|
1377
1416
|
case "task.session":
|
|
1378
1417
|
return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
|
|
1379
1418
|
case "task.turn.started":
|
|
1380
|
-
return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
|
|
1419
|
+
return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
|
|
1381
1420
|
case "task.turn.completed":
|
|
1382
1421
|
if (isStickyAttention(rec.state))
|
|
1383
1422
|
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
1384
|
-
if (rec.pendingTool)
|
|
1423
|
+
if (rec.pendingTool || rec.pendingMonitor)
|
|
1385
1424
|
return stampAttempt(base, {}, now);
|
|
1386
|
-
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
|
|
1425
|
+
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
|
|
1387
1426
|
case "task.delta":
|
|
1388
1427
|
return stampAttempt(base, {}, now);
|
|
1389
1428
|
// heartbeat-only
|
|
1390
1429
|
case "task.input.requested":
|
|
1391
1430
|
case "task.approval.requested":
|
|
1392
|
-
return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
1431
|
+
return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
|
|
1393
1432
|
case "task.reattached":
|
|
1394
1433
|
return stampAttempt(base, {}, now);
|
|
1395
1434
|
case "task.first-turn.confirmed":
|
|
@@ -1414,15 +1453,21 @@ var init_state_machine = __esm({
|
|
|
1414
1453
|
});
|
|
1415
1454
|
|
|
1416
1455
|
// packages/core/dist/watchdog.js
|
|
1417
|
-
function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
|
|
1456
|
+
function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
|
|
1418
1457
|
if (rec.state !== "working")
|
|
1419
1458
|
return null;
|
|
1420
1459
|
if (rec.mode === "interactive") {
|
|
1421
|
-
if (
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
return
|
|
1425
|
-
|
|
1460
|
+
if (rec.pendingTool) {
|
|
1461
|
+
if (now - rec.pendingTool.since <= toolStallMs)
|
|
1462
|
+
return null;
|
|
1463
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
|
|
1464
|
+
}
|
|
1465
|
+
if (rec.pendingMonitor) {
|
|
1466
|
+
if (now - rec.pendingMonitor.since <= monitorStallMs)
|
|
1467
|
+
return null;
|
|
1468
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
|
|
1469
|
+
}
|
|
1470
|
+
return null;
|
|
1426
1471
|
}
|
|
1427
1472
|
const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
|
|
1428
1473
|
if (now - liveness <= rec.heartbeatBudgetMs)
|
|
@@ -1432,12 +1477,13 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
|
|
|
1432
1477
|
function recoverStall(rec, now) {
|
|
1433
1478
|
if (rec.state !== "stalled")
|
|
1434
1479
|
return null;
|
|
1435
|
-
return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
|
|
1480
|
+
return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
|
|
1436
1481
|
}
|
|
1437
|
-
var TOOL_STALL_BUDGET_MS;
|
|
1482
|
+
var TOOL_STALL_BUDGET_MS, MONITOR_STALL_BUDGET_MS;
|
|
1438
1483
|
var init_watchdog = __esm({
|
|
1439
1484
|
"packages/core/dist/watchdog.js"() {
|
|
1440
1485
|
TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
|
|
1486
|
+
MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
|
|
1441
1487
|
}
|
|
1442
1488
|
});
|
|
1443
1489
|
|
|
@@ -1711,7 +1757,7 @@ function createDaemon(deps) {
|
|
|
1711
1757
|
store.delete(r.project, r.id);
|
|
1712
1758
|
continue;
|
|
1713
1759
|
}
|
|
1714
|
-
if (!TERMINAL_STATES.has(r.state)) {
|
|
1760
|
+
if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
|
|
1715
1761
|
const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
|
|
1716
1762
|
if (t - r.createdAt > ceiling) {
|
|
1717
1763
|
const prevState = r.state;
|
|
@@ -1756,7 +1802,7 @@ function createDaemon(deps) {
|
|
|
1756
1802
|
const idle = evaluateStall(r, t);
|
|
1757
1803
|
if (idle) {
|
|
1758
1804
|
store.put(idle);
|
|
1759
|
-
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 };
|
|
1805
|
+
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 };
|
|
1760
1806
|
firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
|
|
1761
1807
|
continue;
|
|
1762
1808
|
}
|
|
@@ -2251,9 +2297,9 @@ function startServer(sockPath, handlerOrCallbacks, onListenError = defaultListen
|
|
|
2251
2297
|
return server;
|
|
2252
2298
|
}
|
|
2253
2299
|
function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
2254
|
-
return new Promise((
|
|
2300
|
+
return new Promise((resolve4) => {
|
|
2255
2301
|
if (!existsSync5(sockPath)) {
|
|
2256
|
-
|
|
2302
|
+
resolve4(false);
|
|
2257
2303
|
return;
|
|
2258
2304
|
}
|
|
2259
2305
|
const conn = createConnection(sockPath);
|
|
@@ -2262,7 +2308,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
|
2262
2308
|
conn.destroy();
|
|
2263
2309
|
} catch {
|
|
2264
2310
|
}
|
|
2265
|
-
|
|
2311
|
+
resolve4(v);
|
|
2266
2312
|
};
|
|
2267
2313
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2268
2314
|
conn.on("connect", () => {
|
|
@@ -2276,7 +2322,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
|
2276
2322
|
});
|
|
2277
2323
|
}
|
|
2278
2324
|
function sendRequest(sockPath, msg, timeoutMs = 5e3) {
|
|
2279
|
-
return new Promise((
|
|
2325
|
+
return new Promise((resolve4, reject) => {
|
|
2280
2326
|
const conn = createConnection(sockPath);
|
|
2281
2327
|
const dec = createDecoder();
|
|
2282
2328
|
const timer = setTimeout(() => {
|
|
@@ -2292,7 +2338,7 @@ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
|
|
|
2292
2338
|
if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
|
|
2293
2339
|
reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
|
|
2294
2340
|
} else if (m.ok) {
|
|
2295
|
-
|
|
2341
|
+
resolve4(m.reply);
|
|
2296
2342
|
} else {
|
|
2297
2343
|
reject(new Error(m.error));
|
|
2298
2344
|
}
|
|
@@ -2512,6 +2558,151 @@ var init_store = __esm({
|
|
|
2512
2558
|
}
|
|
2513
2559
|
});
|
|
2514
2560
|
|
|
2561
|
+
// packages/core/dist/work-store.js
|
|
2562
|
+
import { homedir as homedir4 } from "os";
|
|
2563
|
+
import { join as join7, resolve as resolve2, sep as sep2 } from "path";
|
|
2564
|
+
import { randomBytes } from "crypto";
|
|
2565
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync5, existsSync as existsSync7, rmSync as rmSync4, statSync as statSync2 } from "fs";
|
|
2566
|
+
function defaultWorkRoot() {
|
|
2567
|
+
return join7(homedir4(), ".config", "squadrant", "work");
|
|
2568
|
+
}
|
|
2569
|
+
function safeSegment2(kind, s) {
|
|
2570
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
2571
|
+
throw new Error(`invalid ${kind}: must be a non-empty string`);
|
|
2572
|
+
}
|
|
2573
|
+
if (s.includes("\0"))
|
|
2574
|
+
throw new Error(`invalid ${kind}: NUL byte not allowed`);
|
|
2575
|
+
if (s === "." || s === ".." || /[/\\]/.test(s)) {
|
|
2576
|
+
throw new Error(`invalid ${kind}: '${s}' \u2014 path separators/traversal not allowed`);
|
|
2577
|
+
}
|
|
2578
|
+
return s;
|
|
2579
|
+
}
|
|
2580
|
+
function createWorkStore(root = defaultWorkRoot()) {
|
|
2581
|
+
const rootResolved = resolve2(root);
|
|
2582
|
+
const assertUnderRoot = (target) => {
|
|
2583
|
+
const r = resolve2(target);
|
|
2584
|
+
if (r !== rootResolved && !r.startsWith(rootResolved + sep2)) {
|
|
2585
|
+
throw new Error(`path escapes state root: ${target}`);
|
|
2586
|
+
}
|
|
2587
|
+
return target;
|
|
2588
|
+
};
|
|
2589
|
+
const projDir = (p) => assertUnderRoot(join7(root, safeSegment2("project", p)));
|
|
2590
|
+
const itemFile = (p, id) => assertUnderRoot(join7(projDir(p), `${safeSegment2("id", id)}.json`));
|
|
2591
|
+
return {
|
|
2592
|
+
put(item) {
|
|
2593
|
+
mkdirSync4(projDir(item.project), { recursive: true });
|
|
2594
|
+
const dest = itemFile(item.project, item.id);
|
|
2595
|
+
const tmp = `${dest}.tmp`;
|
|
2596
|
+
writeFileSync5(tmp, JSON.stringify(item, null, 2));
|
|
2597
|
+
renameSync2(tmp, dest);
|
|
2598
|
+
},
|
|
2599
|
+
get(project, id) {
|
|
2600
|
+
const f = itemFile(project, id);
|
|
2601
|
+
if (!existsSync7(f))
|
|
2602
|
+
return void 0;
|
|
2603
|
+
try {
|
|
2604
|
+
return JSON.parse(readFileSync6(f, "utf-8"));
|
|
2605
|
+
} catch {
|
|
2606
|
+
return void 0;
|
|
2607
|
+
}
|
|
2608
|
+
},
|
|
2609
|
+
list(project) {
|
|
2610
|
+
const d = projDir(project);
|
|
2611
|
+
if (!existsSync7(d))
|
|
2612
|
+
return [];
|
|
2613
|
+
return readdirSync2(d).filter((n) => n.endsWith(".json") && !n.endsWith(".json.tmp")).map((n) => {
|
|
2614
|
+
try {
|
|
2615
|
+
return JSON.parse(readFileSync6(join7(d, n), "utf-8"));
|
|
2616
|
+
} catch {
|
|
2617
|
+
return void 0;
|
|
2618
|
+
}
|
|
2619
|
+
}).filter((r) => r !== void 0);
|
|
2620
|
+
},
|
|
2621
|
+
listAll() {
|
|
2622
|
+
if (!existsSync7(root))
|
|
2623
|
+
return [];
|
|
2624
|
+
return readdirSync2(root).filter((p) => {
|
|
2625
|
+
try {
|
|
2626
|
+
return statSync2(join7(root, p)).isDirectory();
|
|
2627
|
+
} catch {
|
|
2628
|
+
return false;
|
|
2629
|
+
}
|
|
2630
|
+
}).flatMap((p) => this.list(p));
|
|
2631
|
+
},
|
|
2632
|
+
delete(project, id) {
|
|
2633
|
+
const f = itemFile(project, id);
|
|
2634
|
+
if (existsSync7(f))
|
|
2635
|
+
rmSync4(f);
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
function purgeExpiredWorkItems(store, now = Date.now()) {
|
|
2640
|
+
let purged = 0;
|
|
2641
|
+
for (const item of store.listAll()) {
|
|
2642
|
+
if (item.closedAt !== null && now - item.closedAt > WORK_ITEM_TTL_MS) {
|
|
2643
|
+
store.delete(item.project, item.id);
|
|
2644
|
+
purged++;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return purged;
|
|
2648
|
+
}
|
|
2649
|
+
function generateWorkId(store) {
|
|
2650
|
+
const existing = new Set(store.listAll().map((i) => i.id));
|
|
2651
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
2652
|
+
const id = `w_${randomBytes(2).toString("hex")}`;
|
|
2653
|
+
if (!existing.has(id))
|
|
2654
|
+
return id;
|
|
2655
|
+
}
|
|
2656
|
+
throw new Error("could not generate a unique work item id");
|
|
2657
|
+
}
|
|
2658
|
+
function createWorkItem(store, opts) {
|
|
2659
|
+
const now = opts.now ?? Date.now();
|
|
2660
|
+
const item = {
|
|
2661
|
+
id: generateWorkId(store),
|
|
2662
|
+
project: opts.project,
|
|
2663
|
+
title: opts.title,
|
|
2664
|
+
state: "working",
|
|
2665
|
+
parent: opts.parent ?? null,
|
|
2666
|
+
tags: opts.tags ?? [],
|
|
2667
|
+
note: "",
|
|
2668
|
+
crewTaskIds: [],
|
|
2669
|
+
issue: null,
|
|
2670
|
+
createdAt: now,
|
|
2671
|
+
updatedAt: now,
|
|
2672
|
+
closedAt: null
|
|
2673
|
+
};
|
|
2674
|
+
store.put(item);
|
|
2675
|
+
return item;
|
|
2676
|
+
}
|
|
2677
|
+
function findWorkItemById(store, id) {
|
|
2678
|
+
return store.listAll().find((i) => i.id === id);
|
|
2679
|
+
}
|
|
2680
|
+
function findOpenChildren(store, id) {
|
|
2681
|
+
return store.listAll().filter((i) => i.parent === id && !TERMINAL_WORK_STATES.has(i.state));
|
|
2682
|
+
}
|
|
2683
|
+
function closeWorkItem(store, id, state, opts = {}) {
|
|
2684
|
+
const item = findWorkItemById(store, id);
|
|
2685
|
+
if (!item)
|
|
2686
|
+
return void 0;
|
|
2687
|
+
const now = opts.now ?? Date.now();
|
|
2688
|
+
const updated = {
|
|
2689
|
+
...item,
|
|
2690
|
+
state,
|
|
2691
|
+
note: opts.note ?? item.note,
|
|
2692
|
+
updatedAt: now,
|
|
2693
|
+
closedAt: now
|
|
2694
|
+
};
|
|
2695
|
+
store.put(updated);
|
|
2696
|
+
return updated;
|
|
2697
|
+
}
|
|
2698
|
+
var WORK_ITEM_TTL_MS;
|
|
2699
|
+
var init_work_store = __esm({
|
|
2700
|
+
"packages/core/dist/work-store.js"() {
|
|
2701
|
+
init_dist();
|
|
2702
|
+
WORK_ITEM_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2515
2706
|
// packages/core/dist/snapshot.js
|
|
2516
2707
|
var snapshot_exports = {};
|
|
2517
2708
|
__export(snapshot_exports, {
|
|
@@ -2565,16 +2756,16 @@ var init_snapshot = __esm({
|
|
|
2565
2756
|
|
|
2566
2757
|
// packages/core/dist/launchd.js
|
|
2567
2758
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2568
|
-
import { mkdirSync as
|
|
2569
|
-
import { homedir as
|
|
2570
|
-
import { dirname as dirname3, join as
|
|
2759
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6, readFileSync as readFileSync7, existsSync as existsSync8, openSync, writeSync, closeSync, unlinkSync as unlinkSync2, constants } from "fs";
|
|
2760
|
+
import { homedir as homedir5 } from "os";
|
|
2761
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
2571
2762
|
import { fileURLToPath } from "url";
|
|
2572
2763
|
function plistPath() {
|
|
2573
|
-
return
|
|
2764
|
+
return join8(homedir5(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
2574
2765
|
}
|
|
2575
2766
|
function daemonEntryPath() {
|
|
2576
|
-
const p =
|
|
2577
|
-
if (!
|
|
2767
|
+
const p = join8(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
|
|
2768
|
+
if (!existsSync8(p)) {
|
|
2578
2769
|
throw new Error(`daemonEntryPath: compiled entry not found at '${p}'; run 'npm run build' \u2014 a src-tree or missing path in the launchd plist causes a MODULE_NOT_FOUND crash-loop (#259)`);
|
|
2579
2770
|
}
|
|
2580
2771
|
return p;
|
|
@@ -2582,10 +2773,10 @@ function daemonEntryPath() {
|
|
|
2582
2773
|
function xmlEscape(s) {
|
|
2583
2774
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2584
2775
|
}
|
|
2585
|
-
function sanitizePathForPlist(
|
|
2776
|
+
function sanitizePathForPlist(path34) {
|
|
2586
2777
|
const seen = /* @__PURE__ */ new Set();
|
|
2587
2778
|
const stable = [];
|
|
2588
|
-
for (const p of
|
|
2779
|
+
for (const p of path34.split(":")) {
|
|
2589
2780
|
if (!p)
|
|
2590
2781
|
continue;
|
|
2591
2782
|
if (p.includes("/.claude/plugins/"))
|
|
@@ -2631,7 +2822,7 @@ function buildDaemonPath(shellPath) {
|
|
|
2631
2822
|
}).join(":");
|
|
2632
2823
|
}
|
|
2633
2824
|
function renderPlist(nodeBin, daemonEntry, pathEnv = "") {
|
|
2634
|
-
const logPath2 =
|
|
2825
|
+
const logPath2 = join8(homedir5(), ".config", "squadrant", "squadrantd.log");
|
|
2635
2826
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2636
2827
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2637
2828
|
<plist version="1.0">
|
|
@@ -2660,13 +2851,13 @@ function _resetRestartInFlightForTest() {
|
|
|
2660
2851
|
restartInFlight = false;
|
|
2661
2852
|
}
|
|
2662
2853
|
function daemonLockPath() {
|
|
2663
|
-
return
|
|
2854
|
+
return join8(homedir5(), ".config", "squadrant", "daemon.lock");
|
|
2664
2855
|
}
|
|
2665
2856
|
function tryAcquireDaemonLock() {
|
|
2666
2857
|
const lp = daemonLockPath();
|
|
2667
|
-
if (
|
|
2858
|
+
if (existsSync8(lp)) {
|
|
2668
2859
|
try {
|
|
2669
|
-
const pid = parseInt(
|
|
2860
|
+
const pid = parseInt(readFileSync7(lp, "utf-8").trim(), 10);
|
|
2670
2861
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2671
2862
|
unlinkSync2(lp);
|
|
2672
2863
|
} else {
|
|
@@ -2699,37 +2890,57 @@ function releaseDaemonLock() {
|
|
|
2699
2890
|
} catch {
|
|
2700
2891
|
}
|
|
2701
2892
|
}
|
|
2702
|
-
function
|
|
2893
|
+
function computeDaemonDrift(nodeBin) {
|
|
2894
|
+
const p = plistPath();
|
|
2895
|
+
const entry = daemonEntryPath();
|
|
2896
|
+
const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
|
|
2897
|
+
const current = existsSync8(p) ? readFileSync7(p, "utf-8") : null;
|
|
2898
|
+
const uid = process.getuid?.() ?? 0;
|
|
2899
|
+
const target = `gui/${uid}/${LABEL}`;
|
|
2900
|
+
const changed = current !== desired;
|
|
2901
|
+
const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
|
|
2902
|
+
return { plistPath: p, target, desired, current, changed, programChanged };
|
|
2903
|
+
}
|
|
2904
|
+
function applyDaemonDrift(drift) {
|
|
2905
|
+
if (drift.changed) {
|
|
2906
|
+
mkdirSync5(dirname3(drift.plistPath), { recursive: true });
|
|
2907
|
+
writeFileSync6(drift.plistPath, drift.desired);
|
|
2908
|
+
}
|
|
2909
|
+
if (drift.programChanged) {
|
|
2910
|
+
try {
|
|
2911
|
+
execFileSync3("launchctl", ["bootout", drift.target], { stdio: "ignore" });
|
|
2912
|
+
} catch {
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
const uid = process.getuid?.() ?? 0;
|
|
2916
|
+
try {
|
|
2917
|
+
execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
|
|
2918
|
+
} catch {
|
|
2919
|
+
}
|
|
2920
|
+
execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
|
|
2921
|
+
}
|
|
2922
|
+
function isOperatorInitiatedCommand(topLevelArg) {
|
|
2923
|
+
return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
|
|
2924
|
+
}
|
|
2925
|
+
function ensureDaemon(nodeBin = process.execPath, opts = {}) {
|
|
2703
2926
|
if (restartInFlight)
|
|
2704
2927
|
return;
|
|
2705
2928
|
restartInFlight = true;
|
|
2929
|
+
const authorized = process.env.SQUADRANT_ROLE === "captain" || opts.operatorInitiated === true;
|
|
2930
|
+
if (!authorized) {
|
|
2931
|
+
try {
|
|
2932
|
+
if (computeDaemonDrift(nodeBin).changed) {
|
|
2933
|
+
process.stderr.write("[squadrant] note: this machine's registered squadrant daemon config is out of date for the version/PATH running right now (common right after an `npm update -g squadrant`) \u2014 NOT applying it automatically because this command isn't the captain and isn't `launch`/`init`. This is usually harmless: the next captain command reconciles it on its own. If something looks stale or broken right now, run `squadrant heal daemon` to fix it immediately.\n");
|
|
2934
|
+
}
|
|
2935
|
+
} catch {
|
|
2936
|
+
}
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2706
2939
|
if (!tryAcquireDaemonLock()) {
|
|
2707
2940
|
return;
|
|
2708
2941
|
}
|
|
2709
2942
|
try {
|
|
2710
|
-
|
|
2711
|
-
const entry = daemonEntryPath();
|
|
2712
|
-
const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
|
|
2713
|
-
const current = existsSync7(p) ? readFileSync6(p, "utf-8") : null;
|
|
2714
|
-
const uid = process.getuid?.() ?? 0;
|
|
2715
|
-
const target = `gui/${uid}/${LABEL}`;
|
|
2716
|
-
const changed = current !== desired;
|
|
2717
|
-
const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
|
|
2718
|
-
if (changed) {
|
|
2719
|
-
mkdirSync4(dirname3(p), { recursive: true });
|
|
2720
|
-
writeFileSync5(p, desired);
|
|
2721
|
-
}
|
|
2722
|
-
if (programChanged) {
|
|
2723
|
-
try {
|
|
2724
|
-
execFileSync3("launchctl", ["bootout", target], { stdio: "ignore" });
|
|
2725
|
-
} catch {
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
try {
|
|
2729
|
-
execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, p], { stdio: "ignore" });
|
|
2730
|
-
} catch {
|
|
2731
|
-
}
|
|
2732
|
-
execFileSync3("launchctl", ["kickstart", target], { stdio: "ignore" });
|
|
2943
|
+
applyDaemonDrift(computeDaemonDrift(nodeBin));
|
|
2733
2944
|
} catch (e) {
|
|
2734
2945
|
process.stderr.write(`[squadrant] warn: ensureDaemon failed (${e instanceof Error ? e.message : e})
|
|
2735
2946
|
`);
|
|
@@ -2737,12 +2948,22 @@ function ensureDaemon(nodeBin = process.execPath) {
|
|
|
2737
2948
|
releaseDaemonLock();
|
|
2738
2949
|
}
|
|
2739
2950
|
}
|
|
2740
|
-
|
|
2951
|
+
function reregisterDaemon(nodeBin = process.execPath) {
|
|
2952
|
+
if (!tryAcquireDaemonLock())
|
|
2953
|
+
return;
|
|
2954
|
+
try {
|
|
2955
|
+
applyDaemonDrift(computeDaemonDrift(nodeBin));
|
|
2956
|
+
} finally {
|
|
2957
|
+
releaseDaemonLock();
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS;
|
|
2741
2961
|
var init_launchd = __esm({
|
|
2742
2962
|
"packages/core/dist/launchd.js"() {
|
|
2743
2963
|
LABEL = "com.squadrant.daemon";
|
|
2744
2964
|
AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
|
|
2745
2965
|
restartInFlight = false;
|
|
2966
|
+
OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
|
|
2746
2967
|
}
|
|
2747
2968
|
});
|
|
2748
2969
|
|
|
@@ -2890,7 +3111,7 @@ var init_gate = __esm({
|
|
|
2890
3111
|
});
|
|
2891
3112
|
|
|
2892
3113
|
// packages/core/dist/daemon/liveness-registry.js
|
|
2893
|
-
import { writeFileSync as
|
|
3114
|
+
import { writeFileSync as writeFileSync7, readFileSync as readFileSync8, renameSync as renameSync3 } from "fs";
|
|
2894
3115
|
var LivenessRegistry;
|
|
2895
3116
|
var init_liveness_registry = __esm({
|
|
2896
3117
|
"packages/core/dist/daemon/liveness-registry.js"() {
|
|
@@ -2904,14 +3125,14 @@ var init_liveness_registry = __esm({
|
|
|
2904
3125
|
this.path = opts.path;
|
|
2905
3126
|
this.readFile = opts.readFile ?? ((p) => {
|
|
2906
3127
|
try {
|
|
2907
|
-
return
|
|
3128
|
+
return readFileSync8(p, "utf-8");
|
|
2908
3129
|
} catch {
|
|
2909
3130
|
return void 0;
|
|
2910
3131
|
}
|
|
2911
3132
|
});
|
|
2912
3133
|
this.writeFile = opts.writeFile ?? ((p, c) => {
|
|
2913
|
-
|
|
2914
|
-
|
|
3134
|
+
writeFileSync7(`${p}.tmp`, c);
|
|
3135
|
+
renameSync3(`${p}.tmp`, p);
|
|
2915
3136
|
});
|
|
2916
3137
|
}
|
|
2917
3138
|
load() {
|
|
@@ -2960,10 +3181,10 @@ var init_liveness_registry = __esm({
|
|
|
2960
3181
|
});
|
|
2961
3182
|
|
|
2962
3183
|
// packages/core/dist/daemon/context.js
|
|
2963
|
-
import { homedir as
|
|
2964
|
-
import { join as
|
|
3184
|
+
import { homedir as homedir6 } from "os";
|
|
3185
|
+
import { join as join9 } from "path";
|
|
2965
3186
|
import { spawn as realSpawn } from "child_process";
|
|
2966
|
-
import { writeFileSync as
|
|
3187
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
2967
3188
|
function defaultIsPidAlive(pid) {
|
|
2968
3189
|
try {
|
|
2969
3190
|
process.kill(pid, 0);
|
|
@@ -2973,18 +3194,18 @@ function defaultIsPidAlive(pid) {
|
|
|
2973
3194
|
}
|
|
2974
3195
|
}
|
|
2975
3196
|
function buildContext(opts) {
|
|
2976
|
-
const stateRoot = opts.stateRoot ??
|
|
2977
|
-
const sockPath = opts.sockPath ??
|
|
3197
|
+
const stateRoot = opts.stateRoot ?? join9(homedir6(), ".config", "squadrant", "state");
|
|
3198
|
+
const sockPath = opts.sockPath ?? join9(homedir6(), ".config", "squadrant", "squadrant.sock");
|
|
2978
3199
|
const store = createStore(stateRoot);
|
|
2979
3200
|
const bootedAt = Date.now();
|
|
2980
3201
|
const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
|
|
2981
3202
|
const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
|
|
2982
3203
|
const spawn2 = opts.spawn ?? realSpawn;
|
|
2983
|
-
const resultsDir =
|
|
2984
|
-
|
|
3204
|
+
const resultsDir = join9(stateRoot, "_results");
|
|
3205
|
+
mkdirSync6(resultsDir, { recursive: true });
|
|
2985
3206
|
const writeResult = (id, payload) => {
|
|
2986
|
-
const p =
|
|
2987
|
-
|
|
3207
|
+
const p = join9(resultsDir, `${id}.txt`);
|
|
3208
|
+
writeFileSync8(p, payload);
|
|
2988
3209
|
return p;
|
|
2989
3210
|
};
|
|
2990
3211
|
const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
|
|
@@ -3006,7 +3227,7 @@ function buildContext(opts) {
|
|
|
3006
3227
|
inFlightHeadlessIds: /* @__PURE__ */ new Set(),
|
|
3007
3228
|
activeHeadlessKills: /* @__PURE__ */ new Set(),
|
|
3008
3229
|
livenessRegistry: (() => {
|
|
3009
|
-
const r = new LivenessRegistry({ path:
|
|
3230
|
+
const r = new LivenessRegistry({ path: join9(stateRoot, "liveness.json") });
|
|
3010
3231
|
r.load();
|
|
3011
3232
|
return r;
|
|
3012
3233
|
})(),
|
|
@@ -3319,9 +3540,11 @@ var init_defer_delivery = __esm({
|
|
|
3319
3540
|
"packages/core/dist/delivery/defer-delivery.js"() {
|
|
3320
3541
|
DeferDelivery = class extends Error {
|
|
3321
3542
|
draft;
|
|
3322
|
-
|
|
3543
|
+
reason;
|
|
3544
|
+
constructor(draft = null, reason = "draft") {
|
|
3323
3545
|
super("deferred: captain composing");
|
|
3324
3546
|
this.draft = draft;
|
|
3547
|
+
this.reason = reason;
|
|
3325
3548
|
this.name = "DeferDelivery";
|
|
3326
3549
|
}
|
|
3327
3550
|
};
|
|
@@ -3345,6 +3568,7 @@ var init_captain_delivery = __esm({
|
|
|
3345
3568
|
deferCounts = /* @__PURE__ */ new Map();
|
|
3346
3569
|
lastContent = /* @__PURE__ */ new Map();
|
|
3347
3570
|
stableCounts = /* @__PURE__ */ new Map();
|
|
3571
|
+
lastReason = /* @__PURE__ */ new Map();
|
|
3348
3572
|
constructor(opts) {
|
|
3349
3573
|
this.opts = opts;
|
|
3350
3574
|
}
|
|
@@ -3367,29 +3591,40 @@ var init_captain_delivery = __esm({
|
|
|
3367
3591
|
this.deferCounts.delete(seq);
|
|
3368
3592
|
this.stableCounts.delete(seq);
|
|
3369
3593
|
this.lastContent.delete(seq);
|
|
3594
|
+
this.lastReason.delete(seq);
|
|
3370
3595
|
return { delivered: true };
|
|
3371
3596
|
} catch (e) {
|
|
3372
3597
|
if (e instanceof DeferDelivery) {
|
|
3373
3598
|
this.deferCounts.set(seq, deferCount + 1);
|
|
3374
3599
|
const content = e.draft;
|
|
3600
|
+
let stableCount;
|
|
3375
3601
|
if (content && content === this.lastContent.get(seq)) {
|
|
3376
|
-
|
|
3602
|
+
stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
|
|
3603
|
+
this.stableCounts.set(seq, stableCount);
|
|
3377
3604
|
} else {
|
|
3605
|
+
stableCount = 0;
|
|
3378
3606
|
this.stableCounts.set(seq, 0);
|
|
3379
3607
|
}
|
|
3380
3608
|
this.lastContent.set(seq, content);
|
|
3381
|
-
|
|
3609
|
+
const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
|
|
3610
|
+
this.lastReason.set(seq, reason);
|
|
3611
|
+
return { deferred: true, reason };
|
|
3382
3612
|
}
|
|
3383
|
-
|
|
3613
|
+
this.lastReason.set(seq, "unknown");
|
|
3614
|
+
return { deferred: true, reason: "unknown" };
|
|
3384
3615
|
}
|
|
3385
3616
|
}
|
|
3386
3617
|
/** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
|
|
3387
3618
|
stats() {
|
|
3388
3619
|
let maxDeferCount = 0;
|
|
3389
|
-
|
|
3390
|
-
|
|
3620
|
+
let reason;
|
|
3621
|
+
for (const [seq, c] of this.deferCounts) {
|
|
3622
|
+
if (c > maxDeferCount) {
|
|
3391
3623
|
maxDeferCount = c;
|
|
3392
|
-
|
|
3624
|
+
reason = this.lastReason.get(seq);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
|
|
3393
3628
|
}
|
|
3394
3629
|
};
|
|
3395
3630
|
}
|
|
@@ -3489,6 +3724,10 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
3489
3724
|
const notifyFault = ctx.notifyFault ?? (() => {
|
|
3490
3725
|
});
|
|
3491
3726
|
const defaultNotify = async (args) => {
|
|
3727
|
+
const fresh = store.get(args.project, args.record.id);
|
|
3728
|
+
if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3492
3731
|
try {
|
|
3493
3732
|
await appendToMailbox({
|
|
3494
3733
|
stateRoot,
|
|
@@ -3578,16 +3817,19 @@ function createDelivery(ctx, daemonCmux) {
|
|
|
3578
3817
|
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
|
|
3579
3818
|
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
3580
3819
|
} else {
|
|
3581
|
-
|
|
3820
|
+
const { maxDeferCount } = d.stats();
|
|
3821
|
+
if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
|
|
3822
|
+
log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
|
|
3823
|
+
}
|
|
3582
3824
|
break;
|
|
3583
3825
|
}
|
|
3584
3826
|
}
|
|
3585
3827
|
const stuck = d.stats().stuck;
|
|
3586
3828
|
if (stuck && !stuckNotified.has(project)) {
|
|
3587
3829
|
stuckNotified.add(project);
|
|
3588
|
-
const { maxDeferCount } = d.stats();
|
|
3589
|
-
log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
|
|
3590
|
-
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.`;
|
|
3830
|
+
const { maxDeferCount, reason } = d.stats();
|
|
3831
|
+
log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
|
|
3832
|
+
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.`;
|
|
3591
3833
|
appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
|
|
3592
3834
|
Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
|
|
3593
3835
|
telegramBridge?.pushRaw(project, text);
|
|
@@ -3707,19 +3949,19 @@ var init_server = __esm({
|
|
|
3707
3949
|
|
|
3708
3950
|
// packages/core/dist/daemon/snapshot-gather.js
|
|
3709
3951
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3710
|
-
import { join as
|
|
3711
|
-
import { statSync as
|
|
3952
|
+
import { join as join10 } from "path";
|
|
3953
|
+
import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
3712
3954
|
function distBuiltAt() {
|
|
3713
3955
|
try {
|
|
3714
|
-
return
|
|
3956
|
+
return statSync3(SELF_PATH).mtimeMs;
|
|
3715
3957
|
} catch {
|
|
3716
3958
|
return 0;
|
|
3717
3959
|
}
|
|
3718
3960
|
}
|
|
3719
|
-
function gatherLogStats(
|
|
3961
|
+
function gatherLogStats(path34, now, windowMs) {
|
|
3720
3962
|
let sizeBytes = 0;
|
|
3721
3963
|
try {
|
|
3722
|
-
sizeBytes =
|
|
3964
|
+
sizeBytes = statSync3(path34).size;
|
|
3723
3965
|
} catch {
|
|
3724
3966
|
return { errorCount: 0, sizeBytes: 0, windowMs };
|
|
3725
3967
|
}
|
|
@@ -3730,7 +3972,7 @@ function gatherLogStats(path30, now, windowMs) {
|
|
|
3730
3972
|
const len = sizeBytes - start;
|
|
3731
3973
|
let text = "";
|
|
3732
3974
|
try {
|
|
3733
|
-
const fd = openSync2(
|
|
3975
|
+
const fd = openSync2(path34, "r");
|
|
3734
3976
|
try {
|
|
3735
3977
|
const buf = Buffer.alloc(len);
|
|
3736
3978
|
readSync(fd, buf, 0, len, start);
|
|
@@ -3761,9 +4003,9 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
3761
4003
|
for (const r of store.list(project))
|
|
3762
4004
|
byState[r.state] = (byState[r.state] ?? 0) + 1;
|
|
3763
4005
|
let corruptCount = 0;
|
|
3764
|
-
const dir =
|
|
4006
|
+
const dir = join10(stateRoot, project);
|
|
3765
4007
|
try {
|
|
3766
|
-
for (const n of
|
|
4008
|
+
for (const n of readdirSync3(dir)) {
|
|
3767
4009
|
if (n.includes(".corrupt.")) {
|
|
3768
4010
|
corruptCount++;
|
|
3769
4011
|
continue;
|
|
@@ -3771,7 +4013,7 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
3771
4013
|
if (!n.endsWith(".json"))
|
|
3772
4014
|
continue;
|
|
3773
4015
|
try {
|
|
3774
|
-
JSON.parse(
|
|
4016
|
+
JSON.parse(readFileSync9(join10(dir, n), "utf-8"));
|
|
3775
4017
|
} catch {
|
|
3776
4018
|
corruptCount++;
|
|
3777
4019
|
}
|
|
@@ -3784,9 +4026,9 @@ function gatherResults(resultsDir) {
|
|
|
3784
4026
|
let fileCount = 0;
|
|
3785
4027
|
let totalBytes = 0;
|
|
3786
4028
|
try {
|
|
3787
|
-
for (const n of
|
|
4029
|
+
for (const n of readdirSync3(resultsDir)) {
|
|
3788
4030
|
try {
|
|
3789
|
-
const s =
|
|
4031
|
+
const s = statSync3(join10(resultsDir, n));
|
|
3790
4032
|
if (s.isFile()) {
|
|
3791
4033
|
fileCount++;
|
|
3792
4034
|
totalBytes += s.size;
|
|
@@ -3806,7 +4048,7 @@ var init_snapshot_gather = __esm({
|
|
|
3806
4048
|
});
|
|
3807
4049
|
|
|
3808
4050
|
// packages/core/dist/daemon/start.js
|
|
3809
|
-
import { join as
|
|
4051
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
3810
4052
|
import { readdir } from "fs/promises";
|
|
3811
4053
|
function startDaemon(ctx, opts, pkgVersion) {
|
|
3812
4054
|
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
@@ -3881,7 +4123,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
3881
4123
|
return out;
|
|
3882
4124
|
}
|
|
3883
4125
|
async function gatherSnapshotInputs(now) {
|
|
3884
|
-
const logPath2 =
|
|
4126
|
+
const logPath2 = join11(dirname4(stateRoot), "squadrantd.log");
|
|
3885
4127
|
const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
|
|
3886
4128
|
const projects = await Promise.all(tier2Projects.map(async (project) => {
|
|
3887
4129
|
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
|
|
@@ -4015,7 +4257,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
4015
4257
|
};
|
|
4016
4258
|
let rotationTimer;
|
|
4017
4259
|
if (rotationInterval > 0) {
|
|
4018
|
-
const inboxPath =
|
|
4260
|
+
const inboxPath = join11(stateRoot, "inbox");
|
|
4019
4261
|
rotationTimer = setInterval(async () => {
|
|
4020
4262
|
try {
|
|
4021
4263
|
let entries;
|
|
@@ -4058,9 +4300,9 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
4058
4300
|
}
|
|
4059
4301
|
for (const kill of ctx.activeHeadlessKills)
|
|
4060
4302
|
kill();
|
|
4061
|
-
return new Promise((
|
|
4303
|
+
return new Promise((resolve4) => server.close(() => {
|
|
4062
4304
|
log(`exit-complete pid=${process.pid}`);
|
|
4063
|
-
|
|
4305
|
+
resolve4();
|
|
4064
4306
|
}));
|
|
4065
4307
|
},
|
|
4066
4308
|
tickDelivery: deliveryTick,
|
|
@@ -4211,8 +4453,8 @@ import { exec as nodeExec } from "child_process";
|
|
|
4211
4453
|
async function reapCrewChildren(taskId, graceMs = 2e3, execFn = nodeExec) {
|
|
4212
4454
|
const marker = `SQUADRANT_CREW_TASK_ID=${taskId}`;
|
|
4213
4455
|
try {
|
|
4214
|
-
const stdout = await new Promise((
|
|
4215
|
-
execFn("ps auxE", { maxBuffer: 64 * 1024 * 1024 }, (err, out) => err ? reject(err) :
|
|
4456
|
+
const stdout = await new Promise((resolve4, reject) => {
|
|
4457
|
+
execFn("ps auxE", { maxBuffer: 64 * 1024 * 1024 }, (err, out) => err ? reject(err) : resolve4(out));
|
|
4216
4458
|
});
|
|
4217
4459
|
const pids = [];
|
|
4218
4460
|
for (const line of stdout.split("\n").slice(1)) {
|
|
@@ -4428,7 +4670,7 @@ function createIsCaptainAlive(sock) {
|
|
|
4428
4670
|
};
|
|
4429
4671
|
}
|
|
4430
4672
|
function createLaunch(cliBin, log) {
|
|
4431
|
-
return (project) => new Promise((
|
|
4673
|
+
return (project) => new Promise((resolve4, reject) => {
|
|
4432
4674
|
execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
|
|
4433
4675
|
const output = capOutput(stdout ?? "", stderr ?? "");
|
|
4434
4676
|
if (err) {
|
|
@@ -4438,7 +4680,7 @@ function createLaunch(cliBin, log) {
|
|
|
4438
4680
|
}
|
|
4439
4681
|
if (output !== "(no output)")
|
|
4440
4682
|
log?.(`launch ${project}: ${output}`);
|
|
4441
|
-
|
|
4683
|
+
resolve4();
|
|
4442
4684
|
});
|
|
4443
4685
|
});
|
|
4444
4686
|
}
|
|
@@ -4586,10 +4828,10 @@ function findProjectByThread(stateRoot, threadId) {
|
|
|
4586
4828
|
for (const [key, id] of Object.entries(s.topics)) {
|
|
4587
4829
|
if (id !== threadId)
|
|
4588
4830
|
continue;
|
|
4589
|
-
const
|
|
4590
|
-
if (
|
|
4831
|
+
const sep3 = key.indexOf("::");
|
|
4832
|
+
if (sep3 === -1)
|
|
4591
4833
|
continue;
|
|
4592
|
-
return { project: key.slice(0,
|
|
4834
|
+
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2) };
|
|
4593
4835
|
}
|
|
4594
4836
|
return null;
|
|
4595
4837
|
}
|
|
@@ -5140,11 +5382,11 @@ var init_bridge = __esm({
|
|
|
5140
5382
|
|
|
5141
5383
|
// packages/core/dist/restart-daemon.js
|
|
5142
5384
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
5143
|
-
import { existsSync as
|
|
5144
|
-
import { homedir as
|
|
5145
|
-
import { join as
|
|
5385
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5386
|
+
import { homedir as homedir7 } from "os";
|
|
5387
|
+
import { join as join12 } from "path";
|
|
5146
5388
|
function defaultIsRunning() {
|
|
5147
|
-
return
|
|
5389
|
+
return existsSync9(DEFAULT_SOCK_PATH);
|
|
5148
5390
|
}
|
|
5149
5391
|
function defaultRunKickstart() {
|
|
5150
5392
|
const uid = process.getuid?.() ?? 0;
|
|
@@ -5174,7 +5416,7 @@ var DEFAULT_SOCK_PATH;
|
|
|
5174
5416
|
var init_restart_daemon = __esm({
|
|
5175
5417
|
"packages/core/dist/restart-daemon.js"() {
|
|
5176
5418
|
init_launchd();
|
|
5177
|
-
DEFAULT_SOCK_PATH =
|
|
5419
|
+
DEFAULT_SOCK_PATH = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
|
|
5178
5420
|
}
|
|
5179
5421
|
});
|
|
5180
5422
|
|
|
@@ -5274,8 +5516,8 @@ function runTelegramStatus(opts) {
|
|
|
5274
5516
|
const env = opts.env ?? process.env;
|
|
5275
5517
|
const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
|
|
5276
5518
|
const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
|
|
5277
|
-
const
|
|
5278
|
-
return { project: key.slice(0,
|
|
5519
|
+
const sep3 = key.indexOf("::");
|
|
5520
|
+
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
|
|
5279
5521
|
});
|
|
5280
5522
|
return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
|
|
5281
5523
|
}
|
|
@@ -5299,8 +5541,8 @@ function runTelegramNotifyStatus(opts) {
|
|
|
5299
5541
|
const s = loadState(opts.stateRoot);
|
|
5300
5542
|
const projects = /* @__PURE__ */ new Set();
|
|
5301
5543
|
for (const key of Object.keys(s.topics)) {
|
|
5302
|
-
const
|
|
5303
|
-
projects.add(
|
|
5544
|
+
const sep3 = key.indexOf("::");
|
|
5545
|
+
projects.add(sep3 === -1 ? key : key.slice(0, sep3));
|
|
5304
5546
|
}
|
|
5305
5547
|
for (const p of Object.keys(s.notify))
|
|
5306
5548
|
projects.add(p);
|
|
@@ -5398,8 +5640,8 @@ var init_crew_routing = __esm({
|
|
|
5398
5640
|
|
|
5399
5641
|
// packages/core/dist/group-dispatch.js
|
|
5400
5642
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5401
|
-
import { homedir as
|
|
5402
|
-
import { join as
|
|
5643
|
+
import { homedir as homedir8 } from "os";
|
|
5644
|
+
import { join as join13 } from "path";
|
|
5403
5645
|
function resolveCurrentProject(config) {
|
|
5404
5646
|
const cwd = process.cwd();
|
|
5405
5647
|
for (const [name, proj] of Object.entries(config.projects)) {
|
|
@@ -5476,7 +5718,7 @@ var init_group_dispatch = __esm({
|
|
|
5476
5718
|
"packages/core/dist/group-dispatch.js"() {
|
|
5477
5719
|
init_dist();
|
|
5478
5720
|
init_protocol();
|
|
5479
|
-
DEFAULT_SOCK_PATH2 =
|
|
5721
|
+
DEFAULT_SOCK_PATH2 = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
|
|
5480
5722
|
GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
|
|
5481
5723
|
GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
|
|
5482
5724
|
}
|
|
@@ -5557,7 +5799,8 @@ async function launchOneWorkspace(opts) {
|
|
|
5557
5799
|
forceFresh = true;
|
|
5558
5800
|
}
|
|
5559
5801
|
}
|
|
5560
|
-
const
|
|
5802
|
+
const builtCmd = opts.agentCmdFactory(forceFresh);
|
|
5803
|
+
const agentCmd = opts.role === "captain" ? `SQUADRANT_ROLE=captain ${builtCmd}` : builtCmd;
|
|
5561
5804
|
recordSession(opts.workspaceName, opts.role, {
|
|
5562
5805
|
sessionsPath: opts.sessionsPath,
|
|
5563
5806
|
templatesDir: opts.templatesDir
|
|
@@ -6079,12 +6322,15 @@ __export(dist_exports2, {
|
|
|
6079
6322
|
GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
|
|
6080
6323
|
IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
|
|
6081
6324
|
LABEL: () => LABEL,
|
|
6325
|
+
MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
|
|
6326
|
+
OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
|
|
6082
6327
|
PROBE_QUIET_MS: () => PROBE_QUIET_MS,
|
|
6083
6328
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
6084
6329
|
STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
|
|
6085
6330
|
TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
|
|
6086
6331
|
TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
|
|
6087
6332
|
TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
|
|
6333
|
+
WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
|
|
6088
6334
|
WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
|
|
6089
6335
|
_resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
|
|
6090
6336
|
ageText: () => ageText,
|
|
@@ -6100,6 +6346,7 @@ __export(dist_exports2, {
|
|
|
6100
6346
|
capAllowed: () => capAllowed,
|
|
6101
6347
|
capOutput: () => capOutput,
|
|
6102
6348
|
classifyHealth: () => classifyHealth,
|
|
6349
|
+
closeWorkItem: () => closeWorkItem,
|
|
6103
6350
|
computeTemplateHash: () => computeTemplateHash,
|
|
6104
6351
|
createAttach: () => createAttach,
|
|
6105
6352
|
createCrewPaneReader: () => createCrewPaneReader,
|
|
@@ -6117,6 +6364,8 @@ __export(dist_exports2, {
|
|
|
6117
6364
|
createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
|
|
6118
6365
|
createTelegramBridge: () => createTelegramBridge,
|
|
6119
6366
|
createTelegramClient: () => createTelegramClient,
|
|
6367
|
+
createWorkItem: () => createWorkItem,
|
|
6368
|
+
createWorkStore: () => createWorkStore,
|
|
6120
6369
|
crewPaneTitle: () => crewPaneTitle,
|
|
6121
6370
|
crewTag: () => crewTag,
|
|
6122
6371
|
daemonEntryPath: () => daemonEntryPath,
|
|
@@ -6124,6 +6373,7 @@ __export(dist_exports2, {
|
|
|
6124
6373
|
decodeFrames: () => decodeFrames,
|
|
6125
6374
|
defaultIsPidAlive: () => defaultIsPidAlive,
|
|
6126
6375
|
defaultListenError: () => defaultListenError,
|
|
6376
|
+
defaultWorkRoot: () => defaultWorkRoot,
|
|
6127
6377
|
deliverStartupPrompt: () => deliverStartupPrompt,
|
|
6128
6378
|
deliverable: () => deliverable,
|
|
6129
6379
|
deriveCaptainState: () => deriveCaptainState,
|
|
@@ -6135,7 +6385,9 @@ __export(dist_exports2, {
|
|
|
6135
6385
|
encodeMsg: () => encodeMsg,
|
|
6136
6386
|
ensureDaemon: () => ensureDaemon,
|
|
6137
6387
|
evaluateStall: () => evaluateStall,
|
|
6388
|
+
findOpenChildren: () => findOpenChildren,
|
|
6138
6389
|
findProjectByThread: () => findProjectByThread,
|
|
6390
|
+
findWorkItemById: () => findWorkItemById,
|
|
6139
6391
|
formatInbound: () => formatInbound,
|
|
6140
6392
|
formatLifecycle: () => formatLifecycle,
|
|
6141
6393
|
healCmdFor: () => healCmdFor,
|
|
@@ -6147,7 +6399,9 @@ __export(dist_exports2, {
|
|
|
6147
6399
|
isCrewTitle: () => isCrewTitle,
|
|
6148
6400
|
isDaemonSocketLive: () => isDaemonSocketLive,
|
|
6149
6401
|
isNotifyActive: () => isNotifyActive,
|
|
6402
|
+
isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
|
|
6150
6403
|
isSideTitle: () => isSideTitle,
|
|
6404
|
+
isStickyAttention: () => isStickyAttention,
|
|
6151
6405
|
isTurnAccepted: () => isTurnAccepted,
|
|
6152
6406
|
kickstartArgv: () => kickstartArgv,
|
|
6153
6407
|
launchOneWorkspace: () => launchOneWorkspace,
|
|
@@ -6165,6 +6419,7 @@ __export(dist_exports2, {
|
|
|
6165
6419
|
plistPath: () => plistPath,
|
|
6166
6420
|
programArgsBlock: () => programArgsBlock,
|
|
6167
6421
|
projectHealth: () => projectHealth,
|
|
6422
|
+
purgeExpiredWorkItems: () => purgeExpiredWorkItems,
|
|
6168
6423
|
readCursor: () => readCursor,
|
|
6169
6424
|
readFromCursor: () => readFromCursor,
|
|
6170
6425
|
reapCrewChildren: () => reapCrewChildren,
|
|
@@ -6176,6 +6431,7 @@ __export(dist_exports2, {
|
|
|
6176
6431
|
reduceLifecycle: () => reduceLifecycle,
|
|
6177
6432
|
releaseDaemonLock: () => releaseDaemonLock,
|
|
6178
6433
|
renderPlist: () => renderPlist,
|
|
6434
|
+
reregisterDaemon: () => reregisterDaemon,
|
|
6179
6435
|
resolveAgentBinDirs: () => resolveAgentBinDirs,
|
|
6180
6436
|
resolveCrewRoute: () => resolveCrewRoute,
|
|
6181
6437
|
resolveCurrentProject: () => resolveCurrentProject,
|
|
@@ -6240,6 +6496,7 @@ var init_dist2 = __esm({
|
|
|
6240
6496
|
init_liveness2();
|
|
6241
6497
|
init_watchdog();
|
|
6242
6498
|
init_store();
|
|
6499
|
+
init_work_store();
|
|
6243
6500
|
init_snapshot();
|
|
6244
6501
|
init_launchd();
|
|
6245
6502
|
init_crew_pane_reader();
|
|
@@ -6279,7 +6536,7 @@ function cmuxLocal(args) {
|
|
|
6279
6536
|
}).trim();
|
|
6280
6537
|
}
|
|
6281
6538
|
function cmux(args) {
|
|
6282
|
-
return new Promise((
|
|
6539
|
+
return new Promise((resolve4, reject) => {
|
|
6283
6540
|
execFile2(
|
|
6284
6541
|
resolveCmuxBin(),
|
|
6285
6542
|
args,
|
|
@@ -6293,19 +6550,19 @@ function cmux(args) {
|
|
|
6293
6550
|
reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
|
|
6294
6551
|
return;
|
|
6295
6552
|
}
|
|
6296
|
-
|
|
6553
|
+
resolve4(stdout.trim());
|
|
6297
6554
|
}
|
|
6298
6555
|
);
|
|
6299
6556
|
});
|
|
6300
6557
|
}
|
|
6301
6558
|
function cmuxStdin(args, input) {
|
|
6302
|
-
return new Promise((
|
|
6559
|
+
return new Promise((resolve4, reject) => {
|
|
6303
6560
|
const child = execFile2(resolveCmuxBin(), args, { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } }, (err, stdout) => {
|
|
6304
6561
|
if (err) {
|
|
6305
6562
|
reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
|
|
6306
6563
|
return;
|
|
6307
6564
|
}
|
|
6308
|
-
|
|
6565
|
+
resolve4(stdout.trim());
|
|
6309
6566
|
});
|
|
6310
6567
|
child.stdin.end(input);
|
|
6311
6568
|
});
|
|
@@ -6666,9 +6923,9 @@ function createCmuxDriver() {
|
|
|
6666
6923
|
}
|
|
6667
6924
|
const draft = parseDraftFromScreen(screen);
|
|
6668
6925
|
if (draft === null)
|
|
6669
|
-
throw new DeferDelivery(null);
|
|
6926
|
+
throw new DeferDelivery(null, "no-box");
|
|
6670
6927
|
if (hasModalOptionList(screen))
|
|
6671
|
-
throw new DeferDelivery(null);
|
|
6928
|
+
throw new DeferDelivery(null, "modal");
|
|
6672
6929
|
if (draft === "") {
|
|
6673
6930
|
await deliver();
|
|
6674
6931
|
return;
|
|
@@ -6912,7 +7169,7 @@ var init_notifiers = __esm({
|
|
|
6912
7169
|
|
|
6913
7170
|
// packages/workspaces/dist/workspaces/obsidian.js
|
|
6914
7171
|
import fs15 from "fs/promises";
|
|
6915
|
-
import { existsSync as
|
|
7172
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6916
7173
|
import path12 from "path";
|
|
6917
7174
|
function resolveInRoot(root, relative) {
|
|
6918
7175
|
const joined = path12.resolve(root, relative);
|
|
@@ -6932,7 +7189,7 @@ function createObsidianDriver(scope) {
|
|
|
6932
7189
|
async probe() {
|
|
6933
7190
|
return {
|
|
6934
7191
|
installed: true,
|
|
6935
|
-
rootExists:
|
|
7192
|
+
rootExists: existsSync10(root)
|
|
6936
7193
|
};
|
|
6937
7194
|
},
|
|
6938
7195
|
async read(rel) {
|
|
@@ -7087,12 +7344,12 @@ var init_events_bridge = __esm({
|
|
|
7087
7344
|
continue;
|
|
7088
7345
|
}
|
|
7089
7346
|
this.child = child;
|
|
7090
|
-
await new Promise((
|
|
7347
|
+
await new Promise((resolve4) => {
|
|
7091
7348
|
let settled = false;
|
|
7092
7349
|
const done = () => {
|
|
7093
7350
|
if (!settled) {
|
|
7094
7351
|
settled = true;
|
|
7095
|
-
|
|
7352
|
+
resolve4();
|
|
7096
7353
|
}
|
|
7097
7354
|
};
|
|
7098
7355
|
child.stdout?.on("data", (b) => this.onData(b));
|
|
@@ -7223,9 +7480,9 @@ var init_store_fingerprint = __esm({
|
|
|
7223
7480
|
});
|
|
7224
7481
|
|
|
7225
7482
|
// packages/workspaces/dist/cmux-daemon/daemon-cmux.js
|
|
7226
|
-
import { readdirSync as
|
|
7227
|
-
import { join as
|
|
7228
|
-
import { homedir as
|
|
7483
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
|
|
7484
|
+
import { join as join14 } from "path";
|
|
7485
|
+
import { homedir as homedir9 } from "os";
|
|
7229
7486
|
var DaemonCmux;
|
|
7230
7487
|
var init_daemon_cmux = __esm({
|
|
7231
7488
|
"packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
|
|
@@ -7288,24 +7545,24 @@ var init_daemon_cmux = __esm({
|
|
|
7288
7545
|
* file failed to read/parse — see the class doc above.
|
|
7289
7546
|
*/
|
|
7290
7547
|
async liveness() {
|
|
7291
|
-
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ??
|
|
7548
|
+
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir9(), ".cmuxterm");
|
|
7292
7549
|
const projects = loadConfig().projects;
|
|
7293
7550
|
let files;
|
|
7294
7551
|
try {
|
|
7295
|
-
files =
|
|
7552
|
+
files = readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
|
|
7296
7553
|
} catch (e) {
|
|
7297
7554
|
throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
|
|
7298
7555
|
}
|
|
7299
|
-
return readLivenessSnapshot(files, (f) =>
|
|
7556
|
+
return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
|
|
7300
7557
|
}
|
|
7301
7558
|
};
|
|
7302
7559
|
}
|
|
7303
7560
|
});
|
|
7304
7561
|
|
|
7305
7562
|
// packages/workspaces/dist/cmux-daemon/cmux-store-source.js
|
|
7306
|
-
import { join as
|
|
7307
|
-
import { homedir as
|
|
7308
|
-
import { watch, readdirSync as
|
|
7563
|
+
import { join as join15 } from "path";
|
|
7564
|
+
import { homedir as homedir10 } from "os";
|
|
7565
|
+
import { watch, readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
|
|
7309
7566
|
function parseLifecycleState(s) {
|
|
7310
7567
|
if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
|
|
7311
7568
|
return s;
|
|
@@ -7322,14 +7579,14 @@ function defaultIsPidAlive2(pid) {
|
|
|
7322
7579
|
}
|
|
7323
7580
|
function defaultListFiles(dir) {
|
|
7324
7581
|
try {
|
|
7325
|
-
return
|
|
7582
|
+
return readdirSync5(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
|
|
7326
7583
|
} catch {
|
|
7327
7584
|
return [];
|
|
7328
7585
|
}
|
|
7329
7586
|
}
|
|
7330
|
-
function defaultReadFile(
|
|
7587
|
+
function defaultReadFile(path34) {
|
|
7331
7588
|
try {
|
|
7332
|
-
return
|
|
7589
|
+
return readFileSync11(path34, "utf-8");
|
|
7333
7590
|
} catch {
|
|
7334
7591
|
return void 0;
|
|
7335
7592
|
}
|
|
@@ -7365,12 +7622,12 @@ var init_cmux_store_source = __esm({
|
|
|
7365
7622
|
active = false;
|
|
7366
7623
|
lastError = null;
|
|
7367
7624
|
constructor(opts = {}) {
|
|
7368
|
-
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ??
|
|
7625
|
+
this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
|
|
7369
7626
|
this.debounceMs = opts.debounceMs ?? 50;
|
|
7370
7627
|
this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
|
|
7371
7628
|
this.listFiles = opts.listFiles ?? defaultListFiles;
|
|
7372
7629
|
this.readFile = opts.readFile ?? defaultReadFile;
|
|
7373
|
-
this.fileExists = opts.fileExists ??
|
|
7630
|
+
this.fileExists = opts.fileExists ?? existsSync11;
|
|
7374
7631
|
this.watchDir = opts.watchDir ?? defaultWatchDir;
|
|
7375
7632
|
this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
|
|
7376
7633
|
this.cancelTimer = opts.cancelTimer ?? clearTimeout;
|
|
@@ -7427,7 +7684,7 @@ var init_cmux_store_source = __esm({
|
|
|
7427
7684
|
}
|
|
7428
7685
|
scanFile(filename) {
|
|
7429
7686
|
const deps = this.deps;
|
|
7430
|
-
const filePath =
|
|
7687
|
+
const filePath = join15(this.stateDir, filename);
|
|
7431
7688
|
const lockPath = `${filePath}.lock`;
|
|
7432
7689
|
if (this.fileExists(lockPath)) {
|
|
7433
7690
|
this.log(`cmux-store: skipping ${filename} (locked)`);
|
|
@@ -7481,11 +7738,11 @@ var init_cmux_store_source = __esm({
|
|
|
7481
7738
|
});
|
|
7482
7739
|
|
|
7483
7740
|
// packages/workspaces/dist/native-hooks/native-hook-source.js
|
|
7484
|
-
import { join as
|
|
7485
|
-
import { homedir as
|
|
7486
|
-
import { mkdirSync as
|
|
7741
|
+
import { join as join16 } from "path";
|
|
7742
|
+
import { homedir as homedir11 } from "os";
|
|
7743
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
7487
7744
|
function installClaudeHooks(opts = {}) {
|
|
7488
|
-
const settingsPath = opts.settingsPath ??
|
|
7745
|
+
const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
|
|
7489
7746
|
const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
|
|
7490
7747
|
const readFile6 = opts.readFile ?? defaultReadFile2;
|
|
7491
7748
|
const writeFile5 = opts.writeFile ?? defaultWriteFile;
|
|
@@ -7493,6 +7750,7 @@ function installClaudeHooks(opts = {}) {
|
|
|
7493
7750
|
});
|
|
7494
7751
|
let settings = {};
|
|
7495
7752
|
const raw = readFile6(settingsPath);
|
|
7753
|
+
const hadExistingSettings = raw !== void 0;
|
|
7496
7754
|
if (raw) {
|
|
7497
7755
|
try {
|
|
7498
7756
|
settings = JSON.parse(raw);
|
|
@@ -7505,6 +7763,7 @@ function installClaudeHooks(opts = {}) {
|
|
|
7505
7763
|
}
|
|
7506
7764
|
const hooks = settings.hooks;
|
|
7507
7765
|
let changed = false;
|
|
7766
|
+
const repaired = [];
|
|
7508
7767
|
for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
|
|
7509
7768
|
if (!Array.isArray(hooks[eventName])) {
|
|
7510
7769
|
hooks[eventName] = [];
|
|
@@ -7516,6 +7775,26 @@ function installClaudeHooks(opts = {}) {
|
|
|
7516
7775
|
if (!alreadyPresent) {
|
|
7517
7776
|
entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
|
|
7518
7777
|
changed = true;
|
|
7778
|
+
repaired.push(`${eventName}/${sub}`);
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
if (repaired.length > 0 && hadExistingSettings) {
|
|
7782
|
+
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`);
|
|
7783
|
+
}
|
|
7784
|
+
if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
|
|
7785
|
+
if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
|
|
7786
|
+
settings.env = {};
|
|
7787
|
+
}
|
|
7788
|
+
const env = settings.env;
|
|
7789
|
+
for (const [key, value] of Object.entries(opts.claudeEnv)) {
|
|
7790
|
+
if (key in env) {
|
|
7791
|
+
if (env[key] !== value) {
|
|
7792
|
+
log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
|
|
7793
|
+
}
|
|
7794
|
+
continue;
|
|
7795
|
+
}
|
|
7796
|
+
env[key] = value;
|
|
7797
|
+
changed = true;
|
|
7519
7798
|
}
|
|
7520
7799
|
}
|
|
7521
7800
|
if (changed) {
|
|
@@ -7557,16 +7836,16 @@ function extractDetail(sub, payload) {
|
|
|
7557
7836
|
}
|
|
7558
7837
|
return void 0;
|
|
7559
7838
|
}
|
|
7560
|
-
function defaultReadFile2(
|
|
7839
|
+
function defaultReadFile2(path34) {
|
|
7561
7840
|
try {
|
|
7562
|
-
return
|
|
7841
|
+
return readFileSync12(path34, "utf-8");
|
|
7563
7842
|
} catch {
|
|
7564
7843
|
return void 0;
|
|
7565
7844
|
}
|
|
7566
7845
|
}
|
|
7567
|
-
function defaultWriteFile(
|
|
7568
|
-
|
|
7569
|
-
|
|
7846
|
+
function defaultWriteFile(path34, content) {
|
|
7847
|
+
mkdirSync7(path34.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
7848
|
+
writeFileSync9(path34, content, "utf-8");
|
|
7570
7849
|
}
|
|
7571
7850
|
var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
|
|
7572
7851
|
var init_native_hook_source = __esm({
|
|
@@ -7590,9 +7869,9 @@ var init_native_hook_source = __esm({
|
|
|
7590
7869
|
cache = /* @__PURE__ */ new Map();
|
|
7591
7870
|
active = false;
|
|
7592
7871
|
constructor(opts = {}) {
|
|
7593
|
-
this.hookInstall = opts.hookInstall ?? {};
|
|
7594
7872
|
this.log = opts.log ?? (() => {
|
|
7595
7873
|
});
|
|
7874
|
+
this.hookInstall = { log: this.log, ...opts.hookInstall };
|
|
7596
7875
|
}
|
|
7597
7876
|
start(deps) {
|
|
7598
7877
|
this.deps = deps;
|
|
@@ -7676,13 +7955,13 @@ async function settleInputBox(runtime, pane) {
|
|
|
7676
7955
|
return sawContent;
|
|
7677
7956
|
}
|
|
7678
7957
|
function getFreePort() {
|
|
7679
|
-
return new Promise((
|
|
7958
|
+
return new Promise((resolve4, reject) => {
|
|
7680
7959
|
const srv = net.createServer();
|
|
7681
7960
|
srv.once("error", reject);
|
|
7682
7961
|
srv.listen(0, "127.0.0.1", () => {
|
|
7683
7962
|
const addr = srv.address();
|
|
7684
7963
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
7685
|
-
srv.close(() => port ?
|
|
7964
|
+
srv.close(() => port ? resolve4(port) : reject(new Error("no free port assigned")));
|
|
7686
7965
|
});
|
|
7687
7966
|
});
|
|
7688
7967
|
}
|
|
@@ -7925,7 +8204,8 @@ function createClaudeDriver() {
|
|
|
7925
8204
|
if (opts.settingsPath) {
|
|
7926
8205
|
cmd += ` --settings ${opts.settingsPath}`;
|
|
7927
8206
|
}
|
|
7928
|
-
const
|
|
8207
|
+
const pluginSubdir = opts.role === "crew" ? "plugin-crew" : "plugin";
|
|
8208
|
+
const pluginDir = `${process.env.HOME}/.config/squadrant/${pluginSubdir}`;
|
|
7929
8209
|
cmd += ` --plugin-dir ${pluginDir}`;
|
|
7930
8210
|
if (!opts.interactive) {
|
|
7931
8211
|
cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
|
|
@@ -8347,8 +8627,8 @@ ${MARKER_END}
|
|
|
8347
8627
|
const startIdx = existing.indexOf(MARKER_START);
|
|
8348
8628
|
const endIdx = existing.indexOf(MARKER_END);
|
|
8349
8629
|
if (startIdx === -1 && endIdx === -1) {
|
|
8350
|
-
const
|
|
8351
|
-
return `${existing}${
|
|
8630
|
+
const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
8631
|
+
return `${existing}${sep3}${block}`;
|
|
8352
8632
|
}
|
|
8353
8633
|
if (startIdx === -1 || endIdx === -1) {
|
|
8354
8634
|
throw new Error(`Corrupted squadrant markers \u2014 found only ${startIdx === -1 ? "end" : "start"} marker. Remove the stray marker or delete the file and re-run projection emit.`);
|
|
@@ -8679,8 +8959,8 @@ var init_app_server_client = __esm({
|
|
|
8679
8959
|
const info = this.opts.clientInfo ?? { name: "squadrant", version: "0" };
|
|
8680
8960
|
const id = this.nextId++;
|
|
8681
8961
|
const env = { jsonrpc: "2.0", id, method: "initialize", params: { clientInfo: info } };
|
|
8682
|
-
const res = await new Promise((
|
|
8683
|
-
this.pending.set(id, { resolve:
|
|
8962
|
+
const res = await new Promise((resolve4, reject) => {
|
|
8963
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
8684
8964
|
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
8685
8965
|
});
|
|
8686
8966
|
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
|
|
@@ -8713,13 +8993,13 @@ var init_app_server_client = __esm({
|
|
|
8713
8993
|
const turnId = ack?.turn?.id;
|
|
8714
8994
|
if (typeof turnId !== "string")
|
|
8715
8995
|
throw new Error(`turn/start: unexpected ack shape (no turn.id): ${JSON.stringify(ack).slice(0, 200)}`);
|
|
8716
|
-
return new Promise((
|
|
8996
|
+
return new Promise((resolve4, reject) => {
|
|
8717
8997
|
const onNote = (n) => {
|
|
8718
8998
|
if (n.params?.turn?.id !== turnId)
|
|
8719
8999
|
return;
|
|
8720
9000
|
if (n.method === "turn/completed") {
|
|
8721
9001
|
cleanup();
|
|
8722
|
-
|
|
9002
|
+
resolve4({ turnId });
|
|
8723
9003
|
}
|
|
8724
9004
|
if (n.method === "turn/failed") {
|
|
8725
9005
|
cleanup();
|
|
@@ -8770,8 +9050,8 @@ var init_app_server_client = __esm({
|
|
|
8770
9050
|
throw new Error("AppServerClient not started");
|
|
8771
9051
|
const id = this.nextId++;
|
|
8772
9052
|
const env = { jsonrpc: "2.0", id, method, params: params ?? {} };
|
|
8773
|
-
return new Promise((
|
|
8774
|
-
this.pending.set(id, { resolve:
|
|
9053
|
+
return new Promise((resolve4, reject) => {
|
|
9054
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
8775
9055
|
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
8776
9056
|
});
|
|
8777
9057
|
}
|
|
@@ -8903,11 +9183,11 @@ var init_codex_app_server_source = __esm({
|
|
|
8903
9183
|
|
|
8904
9184
|
// packages/agents/dist/codex/config.js
|
|
8905
9185
|
import { readFile as readFile5 } from "fs/promises";
|
|
8906
|
-
import { homedir as
|
|
8907
|
-
import { join as
|
|
9186
|
+
import { homedir as homedir13 } from "os";
|
|
9187
|
+
import { join as join18 } from "path";
|
|
8908
9188
|
async function resolveCodexModel() {
|
|
8909
|
-
const home = process.env["CODEX_HOME"] ??
|
|
8910
|
-
const configPath =
|
|
9189
|
+
const home = process.env["CODEX_HOME"] ?? join18(homedir13(), ".codex");
|
|
9190
|
+
const configPath = join18(home, "config.toml");
|
|
8911
9191
|
let text;
|
|
8912
9192
|
try {
|
|
8913
9193
|
text = await readFile5(configPath, "utf8");
|
|
@@ -9022,11 +9302,11 @@ function buildCodexDeveloperInstructions(rec) {
|
|
|
9022
9302
|
${directive}` : directive;
|
|
9023
9303
|
}
|
|
9024
9304
|
function withTimeout(p, ms, msg) {
|
|
9025
|
-
return new Promise((
|
|
9305
|
+
return new Promise((resolve4, reject) => {
|
|
9026
9306
|
const t = setTimeout(() => reject(new Error(msg)), ms);
|
|
9027
9307
|
p.then((v) => {
|
|
9028
9308
|
clearTimeout(t);
|
|
9029
|
-
|
|
9309
|
+
resolve4(v);
|
|
9030
9310
|
}, (e) => {
|
|
9031
9311
|
clearTimeout(t);
|
|
9032
9312
|
reject(e);
|
|
@@ -9428,9 +9708,9 @@ var init_sse_bridge = __esm({
|
|
|
9428
9708
|
|
|
9429
9709
|
// packages/agents/dist/interactive/claude.js
|
|
9430
9710
|
import { execSync as execSync7 } from "child_process";
|
|
9431
|
-
import { readFileSync as
|
|
9432
|
-
import { homedir as
|
|
9433
|
-
import { join as
|
|
9711
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
9712
|
+
import { homedir as homedir14 } from "os";
|
|
9713
|
+
import { join as join19 } from "path";
|
|
9434
9714
|
function probeClaudeSettingsFlag() {
|
|
9435
9715
|
try {
|
|
9436
9716
|
const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9488,11 +9768,11 @@ function deriveTranscriptPath(sessionId, cwd) {
|
|
|
9488
9768
|
if (!sessionId || !cwd)
|
|
9489
9769
|
return null;
|
|
9490
9770
|
const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
9491
|
-
return
|
|
9771
|
+
return join19(homedir14(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
|
|
9492
9772
|
}
|
|
9493
9773
|
function readLastAssistantText(transcriptPath) {
|
|
9494
9774
|
try {
|
|
9495
|
-
const raw =
|
|
9775
|
+
const raw = readFileSync13(transcriptPath, "utf-8");
|
|
9496
9776
|
const lines = raw.split(/\r?\n/);
|
|
9497
9777
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
9498
9778
|
const line = lines[i].trim();
|
|
@@ -9534,8 +9814,8 @@ function resolveLastAssistantText(payload) {
|
|
|
9534
9814
|
const derived = deriveTranscriptPath(p?.session_id, cwd);
|
|
9535
9815
|
if (derived)
|
|
9536
9816
|
candidates.push(derived);
|
|
9537
|
-
for (const
|
|
9538
|
-
const text = readLastAssistantText(
|
|
9817
|
+
for (const path34 of candidates) {
|
|
9818
|
+
const text = readLastAssistantText(path34);
|
|
9539
9819
|
if (text != null)
|
|
9540
9820
|
return text;
|
|
9541
9821
|
}
|
|
@@ -9885,14 +10165,14 @@ function runHeadless(opts) {
|
|
|
9885
10165
|
if (err.length > ERR_CAP)
|
|
9886
10166
|
err = err.slice(err.length - ERR_CAP);
|
|
9887
10167
|
});
|
|
9888
|
-
const result = new Promise((
|
|
10168
|
+
const result = new Promise((resolve4) => {
|
|
9889
10169
|
child.once("error", (e) => {
|
|
9890
10170
|
if (debounceTimer) {
|
|
9891
10171
|
clearTimeout(debounceTimer);
|
|
9892
10172
|
debounceTimer = null;
|
|
9893
10173
|
}
|
|
9894
10174
|
opts.emit({ type: "task.failed", id: opts.id, error: `spawn error: ${e.message}`, exitCode: void 0 });
|
|
9895
|
-
|
|
10175
|
+
resolve4();
|
|
9896
10176
|
});
|
|
9897
10177
|
child.on("close", (code) => {
|
|
9898
10178
|
if (chunksSinceProgress > 0)
|
|
@@ -9909,7 +10189,7 @@ function runHeadless(opts) {
|
|
|
9909
10189
|
const ref = opts.writeResult ? opts.writeResult(opts.id, res.payload ?? "") : "";
|
|
9910
10190
|
opts.emit({ type: "task.done", id: opts.id, resultRef: ref, parseWarning: res.parseWarning });
|
|
9911
10191
|
}
|
|
9912
|
-
|
|
10192
|
+
resolve4();
|
|
9913
10193
|
});
|
|
9914
10194
|
});
|
|
9915
10195
|
return { result, kill: () => child.kill("SIGTERM") };
|
|
@@ -9989,8 +10269,8 @@ var require_daemon_exports = {};
|
|
|
9989
10269
|
__export(require_daemon_exports, {
|
|
9990
10270
|
requireDaemon: () => requireDaemon
|
|
9991
10271
|
});
|
|
9992
|
-
import { join as
|
|
9993
|
-
import { homedir as
|
|
10272
|
+
import { join as join25 } from "path";
|
|
10273
|
+
import { homedir as homedir20 } from "os";
|
|
9994
10274
|
async function requireDaemon(sockPath = DEFAULT_SOCK_PATH3) {
|
|
9995
10275
|
const isLive = await isDaemonSocketLive(sockPath);
|
|
9996
10276
|
if (!isLive) {
|
|
@@ -10001,18 +10281,18 @@ var DEFAULT_SOCK_PATH3;
|
|
|
10001
10281
|
var init_require_daemon = __esm({
|
|
10002
10282
|
"packages/cli/src/lib/require-daemon.ts"() {
|
|
10003
10283
|
init_dist2();
|
|
10004
|
-
DEFAULT_SOCK_PATH3 =
|
|
10284
|
+
DEFAULT_SOCK_PATH3 = join25(homedir20(), ".config", "squadrant", "squadrant.sock");
|
|
10005
10285
|
}
|
|
10006
10286
|
});
|
|
10007
10287
|
|
|
10008
10288
|
// packages/cli/src/index.ts
|
|
10009
10289
|
init_dist();
|
|
10010
10290
|
init_dist2();
|
|
10011
|
-
import { Command as
|
|
10012
|
-
import { readFileSync as
|
|
10291
|
+
import { Command as Command35 } from "commander";
|
|
10292
|
+
import { readFileSync as readFileSync16, existsSync as existsSync13, writeFileSync as writeFileSync12 } from "fs";
|
|
10013
10293
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
10014
|
-
import { dirname as dirname9, join as
|
|
10015
|
-
import { homedir as
|
|
10294
|
+
import { dirname as dirname9, join as join30 } from "path";
|
|
10295
|
+
import { homedir as homedir22 } from "os";
|
|
10016
10296
|
|
|
10017
10297
|
// packages/cli/src/commands/doctor.ts
|
|
10018
10298
|
init_dist();
|
|
@@ -10029,10 +10309,10 @@ import chalk3 from "chalk";
|
|
|
10029
10309
|
// packages/cli/src/commands/health-view.ts
|
|
10030
10310
|
init_dist2();
|
|
10031
10311
|
init_dist2();
|
|
10032
|
-
import { homedir as
|
|
10033
|
-
import { join as
|
|
10312
|
+
import { homedir as homedir12 } from "os";
|
|
10313
|
+
import { join as join17 } from "path";
|
|
10034
10314
|
import chalk2 from "chalk";
|
|
10035
|
-
var SOCK =
|
|
10315
|
+
var SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
10036
10316
|
async function queryHealth(project) {
|
|
10037
10317
|
try {
|
|
10038
10318
|
const reply = await sendRequest(SOCK, { kind: "health", project });
|
|
@@ -10371,9 +10651,9 @@ import chalk4 from "chalk";
|
|
|
10371
10651
|
|
|
10372
10652
|
// packages/cli/src/lib/per-crew-settings.ts
|
|
10373
10653
|
init_dist4();
|
|
10374
|
-
import { mkdirSync as
|
|
10375
|
-
import { dirname as dirname5, join as
|
|
10376
|
-
import { homedir as
|
|
10654
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
10655
|
+
import { dirname as dirname5, join as join20 } from "path";
|
|
10656
|
+
import { homedir as homedir15 } from "os";
|
|
10377
10657
|
var CREW_PERMISSION_ALLOWLIST = [
|
|
10378
10658
|
// git — read + safe mutations (reset/clean/config intentionally excluded)
|
|
10379
10659
|
"Bash(git status:*)",
|
|
@@ -10464,29 +10744,29 @@ function mergeCrewPermissions(settings) {
|
|
|
10464
10744
|
return next;
|
|
10465
10745
|
}
|
|
10466
10746
|
function writePerCrewSettingsLocal(o) {
|
|
10467
|
-
const dir =
|
|
10468
|
-
|
|
10469
|
-
const file =
|
|
10747
|
+
const dir = join20(o.projectCwd, ".claude");
|
|
10748
|
+
mkdirSync8(dir, { recursive: true });
|
|
10749
|
+
const file = join20(dir, "settings.local.json");
|
|
10470
10750
|
let existing = {};
|
|
10471
10751
|
try {
|
|
10472
|
-
const raw = healStaleCockpitRefs(
|
|
10752
|
+
const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
|
|
10473
10753
|
existing = JSON.parse(raw);
|
|
10474
10754
|
} catch {
|
|
10475
10755
|
}
|
|
10476
10756
|
const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
|
|
10477
10757
|
const merged = mergeCrewPermissions(withHooks);
|
|
10478
|
-
|
|
10758
|
+
writeFileSync10(file, JSON.stringify(merged, null, 2));
|
|
10479
10759
|
return file;
|
|
10480
10760
|
}
|
|
10481
|
-
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH =
|
|
10761
|
+
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join20(homedir15(), ".config", "opencode", "opencode.json");
|
|
10482
10762
|
function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
|
|
10483
|
-
|
|
10763
|
+
mkdirSync8(dirname5(configPath), { recursive: true });
|
|
10484
10764
|
const defaultConfig = {
|
|
10485
10765
|
$schema: "https://opencode.ai/config.json",
|
|
10486
10766
|
model: "anthropic/claude-sonnet-4-5"
|
|
10487
10767
|
};
|
|
10488
10768
|
try {
|
|
10489
|
-
|
|
10769
|
+
writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
|
|
10490
10770
|
return configPath;
|
|
10491
10771
|
} catch (err) {
|
|
10492
10772
|
if (err.code === "EEXIST") return null;
|
|
@@ -10494,9 +10774,9 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
|
|
|
10494
10774
|
}
|
|
10495
10775
|
}
|
|
10496
10776
|
function writePerCrewOpencodeConfig(o) {
|
|
10497
|
-
const dir =
|
|
10498
|
-
|
|
10499
|
-
const file =
|
|
10777
|
+
const dir = join20(o.stateRoot, o.project, o.taskId);
|
|
10778
|
+
mkdirSync8(dir, { recursive: true });
|
|
10779
|
+
const file = join20(dir, "opencode.json");
|
|
10500
10780
|
const config = {
|
|
10501
10781
|
permission: {
|
|
10502
10782
|
read: "allow",
|
|
@@ -10511,7 +10791,7 @@ function writePerCrewOpencodeConfig(o) {
|
|
|
10511
10791
|
external_directory: { "**": "allow" }
|
|
10512
10792
|
}
|
|
10513
10793
|
};
|
|
10514
|
-
|
|
10794
|
+
writeFileSync10(file, JSON.stringify(config, null, 2));
|
|
10515
10795
|
return file;
|
|
10516
10796
|
}
|
|
10517
10797
|
|
|
@@ -10542,11 +10822,11 @@ function stepHeader(n, total, label) {
|
|
|
10542
10822
|
${n}/${total} ${label}`));
|
|
10543
10823
|
}
|
|
10544
10824
|
function promptLine(question) {
|
|
10545
|
-
return new Promise((
|
|
10825
|
+
return new Promise((resolve4) => {
|
|
10546
10826
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10547
10827
|
rl.question(question, (answer) => {
|
|
10548
10828
|
rl.close();
|
|
10549
|
-
|
|
10829
|
+
resolve4(answer.trim());
|
|
10550
10830
|
});
|
|
10551
10831
|
});
|
|
10552
10832
|
}
|
|
@@ -10874,48 +11154,22 @@ var projectsCommand = new Command3("projects").description("Manage registered pr
|
|
|
10874
11154
|
|
|
10875
11155
|
// packages/cli/src/commands/status.ts
|
|
10876
11156
|
init_dist();
|
|
10877
|
-
init_dist3();
|
|
10878
11157
|
import { Command as Command4 } from "commander";
|
|
10879
11158
|
import chalk6 from "chalk";
|
|
10880
|
-
import matter2 from "gray-matter";
|
|
10881
|
-
function timeAgo(dateStr) {
|
|
10882
|
-
if (!dateStr) return chalk6.dim("\u2014");
|
|
10883
|
-
const date = new Date(dateStr);
|
|
10884
|
-
if (isNaN(date.getTime())) return chalk6.dim("\u2014");
|
|
10885
|
-
const diff = Date.now() - date.getTime();
|
|
10886
|
-
const mins = Math.floor(diff / 6e4);
|
|
10887
|
-
const hours = Math.floor(diff / 36e5);
|
|
10888
|
-
const days = Math.floor(diff / 864e5);
|
|
10889
|
-
if (mins < 1) return "just now";
|
|
10890
|
-
if (mins < 60) return `${mins}m ago`;
|
|
10891
|
-
if (hours < 24) return `${hours}h ago`;
|
|
10892
|
-
return `${days}d ago`;
|
|
10893
|
-
}
|
|
10894
|
-
function progressBar(completed, total) {
|
|
10895
|
-
if (total === 0) return chalk6.dim("no tasks");
|
|
10896
|
-
const pct = Math.round(completed / total * 100);
|
|
10897
|
-
const filled = Math.round(pct / 10);
|
|
10898
|
-
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
10899
|
-
return `${bar} ${pct}%`;
|
|
10900
|
-
}
|
|
10901
11159
|
function captainIndicator(state) {
|
|
10902
11160
|
if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
|
|
10903
11161
|
if (state === "stopped") return chalk6.magenta("\u23FB");
|
|
10904
11162
|
if (state === void 0 || state === "unknown") return chalk6.dim("?");
|
|
10905
11163
|
return chalk6.dim("\u25CB");
|
|
10906
11164
|
}
|
|
10907
|
-
function formatProjectRow(name, captainName,
|
|
11165
|
+
function formatProjectRow(name, captainName, captainState) {
|
|
10908
11166
|
const sessionIndicator = captainIndicator(captainState);
|
|
10909
11167
|
const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
|
|
10910
|
-
|
|
10911
|
-
const progress = statusMdState === "ok" ? progressBar(fm.tasks_completed ?? 0, fm.tasks_total ?? 0).padEnd(25) : statusMdState === "unreadable" ? chalk6.red("status.md unreadable").padEnd(25) : chalk6.dim("no notes").padEnd(25);
|
|
10912
|
-
const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
|
|
10913
|
-
return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
|
|
11168
|
+
return ` ${name.padEnd(18)} ${captainDisplay}`;
|
|
10914
11169
|
}
|
|
10915
|
-
var statusCommand = new Command4("status").description("Show
|
|
11170
|
+
var statusCommand = new Command4("status").description("Show captain liveness for all projects (task/crew counts have no data source \u2014 #630)").option("--detailed", "also show live per-component service health from the daemon (#77)").action(async (opts) => {
|
|
10916
11171
|
const config = loadConfig();
|
|
10917
11172
|
const projects = Object.entries(config.projects);
|
|
10918
|
-
const registry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
|
|
10919
11173
|
if (projects.length === 0) {
|
|
10920
11174
|
console.log(chalk6.yellow("\nNo projects registered. Use: squadrant projects add <name> <path>\n"));
|
|
10921
11175
|
return;
|
|
@@ -10928,30 +11182,12 @@ var statusCommand = new Command4("status").description("Show status of all proje
|
|
|
10928
11182
|
}
|
|
10929
11183
|
}
|
|
10930
11184
|
console.log(chalk6.bold("\nProject Status\n"));
|
|
10931
|
-
console.log(
|
|
10932
|
-
|
|
10933
|
-
` ${"PROJECT".padEnd(18)} ${"CAPTAIN".padEnd(12)} ${"CREW".padEnd(6)} ${"PROGRESS".padEnd(25)} LAST UPDATE`
|
|
10934
|
-
)
|
|
10935
|
-
);
|
|
10936
|
-
console.log(chalk6.dim(" " + "\u2500".repeat(85)));
|
|
11185
|
+
console.log(chalk6.dim(` ${"PROJECT".padEnd(18)} CAPTAIN`));
|
|
11186
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(35)));
|
|
10937
11187
|
for (const [name, project] of projects) {
|
|
10938
|
-
|
|
10939
|
-
let fm = {};
|
|
10940
|
-
let statusMdState = "missing";
|
|
10941
|
-
if (await workspace.exists("status.md")) {
|
|
10942
|
-
try {
|
|
10943
|
-
const raw = await workspace.read("status.md");
|
|
10944
|
-
fm = matter2(raw).data;
|
|
10945
|
-
statusMdState = "ok";
|
|
10946
|
-
} catch {
|
|
10947
|
-
statusMdState = "unreadable";
|
|
10948
|
-
}
|
|
10949
|
-
}
|
|
10950
|
-
console.log(
|
|
10951
|
-
formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
|
|
10952
|
-
);
|
|
11188
|
+
console.log(formatProjectRow(name, project.captainName, captainStateByProject.get(name)));
|
|
10953
11189
|
}
|
|
10954
|
-
console.log("");
|
|
11190
|
+
console.log(chalk6.dim("\n Task/crew counts: no data source yet \u2014 see squadrant/squadrant#630\n"));
|
|
10955
11191
|
if (opts.detailed) {
|
|
10956
11192
|
printServiceHealth(health);
|
|
10957
11193
|
}
|
|
@@ -11040,9 +11276,9 @@ import { Command as Command8 } from "commander";
|
|
|
11040
11276
|
import { createConnection as createConnection3 } from "net";
|
|
11041
11277
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
11042
11278
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
11043
|
-
import { homedir as
|
|
11044
|
-
import { join as
|
|
11045
|
-
import { mkdirSync as
|
|
11279
|
+
import { homedir as homedir17 } from "os";
|
|
11280
|
+
import { join as join22 } from "path";
|
|
11281
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
11046
11282
|
|
|
11047
11283
|
// packages/cli/src/commands/crew-output.ts
|
|
11048
11284
|
function tailLines(text, maxLines = 40, maxBytes = 4096) {
|
|
@@ -11100,11 +11336,11 @@ init_dist2();
|
|
|
11100
11336
|
import { Command as Command6 } from "commander";
|
|
11101
11337
|
import chalk8 from "chalk";
|
|
11102
11338
|
import { createConnection as createConnection2 } from "net";
|
|
11103
|
-
import { homedir as
|
|
11104
|
-
import { join as
|
|
11339
|
+
import { homedir as homedir16 } from "os";
|
|
11340
|
+
import { join as join21 } from "path";
|
|
11105
11341
|
import { createInterface } from "readline";
|
|
11106
11342
|
function socketPath() {
|
|
11107
|
-
return process.env.SQUADRANTD_SOCK ??
|
|
11343
|
+
return process.env.SQUADRANTD_SOCK ?? join21(homedir16(), ".config", "squadrant", "squadrant.sock");
|
|
11108
11344
|
}
|
|
11109
11345
|
function rule(width, ch = "\u2500") {
|
|
11110
11346
|
return ch.repeat(Math.max(0, width));
|
|
@@ -11340,11 +11576,11 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
|
|
|
11340
11576
|
});
|
|
11341
11577
|
|
|
11342
11578
|
// packages/cli/src/commands/crew-control.ts
|
|
11343
|
-
var SOCK2 =
|
|
11579
|
+
var SOCK2 = join22(homedir17(), ".config", "squadrant", "squadrant.sock");
|
|
11344
11580
|
var CODEX_FIRST_TURN_DELAY_MS = 1500;
|
|
11345
11581
|
async function sendCodexFirstTurn(taskId, text) {
|
|
11346
11582
|
await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
|
|
11347
|
-
await new Promise((
|
|
11583
|
+
await new Promise((resolve4, reject) => {
|
|
11348
11584
|
const conn = createConnection3(SOCK2);
|
|
11349
11585
|
conn.setEncoding("utf-8");
|
|
11350
11586
|
conn.on("data", () => {
|
|
@@ -11360,7 +11596,7 @@ async function sendCodexFirstTurn(taskId, text) {
|
|
|
11360
11596
|
}
|
|
11361
11597
|
setTimeout(() => {
|
|
11362
11598
|
conn.end();
|
|
11363
|
-
|
|
11599
|
+
resolve4();
|
|
11364
11600
|
}, 100);
|
|
11365
11601
|
});
|
|
11366
11602
|
});
|
|
@@ -11447,10 +11683,10 @@ function buildSignalRequest(signal, o) {
|
|
|
11447
11683
|
return { kind: "event", project, event };
|
|
11448
11684
|
}
|
|
11449
11685
|
function defaultWriteResult(id, payload) {
|
|
11450
|
-
const dir =
|
|
11451
|
-
|
|
11452
|
-
const file =
|
|
11453
|
-
|
|
11686
|
+
const dir = join22(homedir17(), ".config", "squadrant", "state", "_results");
|
|
11687
|
+
mkdirSync9(dir, { recursive: true });
|
|
11688
|
+
const file = join22(dir, `${id}.txt`);
|
|
11689
|
+
writeFileSync11(file, payload);
|
|
11454
11690
|
return file;
|
|
11455
11691
|
}
|
|
11456
11692
|
async function runCrewSignal(signal, o, deps) {
|
|
@@ -11484,6 +11720,16 @@ function defaultCreatePr(cwd, o) {
|
|
|
11484
11720
|
{ cwd, encoding: "utf-8" }
|
|
11485
11721
|
).trim();
|
|
11486
11722
|
}
|
|
11723
|
+
function defaultGetCommitSubject(cwd) {
|
|
11724
|
+
try {
|
|
11725
|
+
return execFileSync6("git", ["-C", cwd, "log", "-1", "--format=%s"], {
|
|
11726
|
+
encoding: "utf-8",
|
|
11727
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
11728
|
+
}).trim();
|
|
11729
|
+
} catch {
|
|
11730
|
+
return "";
|
|
11731
|
+
}
|
|
11732
|
+
}
|
|
11487
11733
|
async function runCrewApprove(project, crew, deps) {
|
|
11488
11734
|
const config = loadConfig();
|
|
11489
11735
|
const proj = config.projects[project];
|
|
@@ -11499,8 +11745,12 @@ async function runCrewApprove(project, crew, deps) {
|
|
|
11499
11745
|
const cwd = task.cwd ?? proj.path;
|
|
11500
11746
|
const base = resolveWorktreeBase(proj.path);
|
|
11501
11747
|
const branch = crewBranch(crew);
|
|
11502
|
-
const title = (task.task ?? branch).split(/\r?\n/)[0].trim().slice(0, 100);
|
|
11503
11748
|
const body = (task.reviewNote ?? task.task ?? "").trim();
|
|
11749
|
+
const getCommitSubject = deps.getCommitSubject ?? defaultGetCommitSubject;
|
|
11750
|
+
const commitSubject = getCommitSubject(cwd).trim();
|
|
11751
|
+
const reviewNoteFirstLine = (task.reviewNote ?? "").split(/\r?\n/)[0].trim();
|
|
11752
|
+
const taskFirstLine = (task.task ?? branch).split(/\r?\n/)[0].trim();
|
|
11753
|
+
const title = (commitSubject || reviewNoteFirstLine || taskFirstLine).slice(0, 100);
|
|
11504
11754
|
const pushBranch = deps.pushBranch ?? defaultPushBranch;
|
|
11505
11755
|
const createPr = deps.createPr ?? defaultCreatePr;
|
|
11506
11756
|
pushBranch(cwd, branch);
|
|
@@ -11838,11 +12088,11 @@ function parseCrewPick(raw, stats) {
|
|
|
11838
12088
|
throw new Error(`Invalid selection '${raw}'. Enter a number 1-${stats.length} or a crew name.`);
|
|
11839
12089
|
}
|
|
11840
12090
|
function promptLine2(question) {
|
|
11841
|
-
return new Promise((
|
|
12091
|
+
return new Promise((resolve4) => {
|
|
11842
12092
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
11843
12093
|
rl.question(question, (answer) => {
|
|
11844
12094
|
rl.close();
|
|
11845
|
-
|
|
12095
|
+
resolve4(answer);
|
|
11846
12096
|
});
|
|
11847
12097
|
});
|
|
11848
12098
|
}
|
|
@@ -12116,8 +12366,8 @@ init_dist();
|
|
|
12116
12366
|
init_dist3();
|
|
12117
12367
|
import { Command as Command12 } from "commander";
|
|
12118
12368
|
import { execSync as execSync10 } from "child_process";
|
|
12119
|
-
import { homedir as
|
|
12120
|
-
import { join as
|
|
12369
|
+
import { homedir as homedir19 } from "os";
|
|
12370
|
+
import { join as join24 } from "path";
|
|
12121
12371
|
import chalk13 from "chalk";
|
|
12122
12372
|
|
|
12123
12373
|
// packages/web/dist/read-status.js
|
|
@@ -12330,9 +12580,9 @@ function mergeSnapshot(daemon, external, now) {
|
|
|
12330
12580
|
// packages/web/dist/probes.js
|
|
12331
12581
|
init_dist();
|
|
12332
12582
|
init_dist();
|
|
12333
|
-
import { join as
|
|
12334
|
-
import { homedir as
|
|
12335
|
-
import { existsSync as
|
|
12583
|
+
import { join as join23 } from "path";
|
|
12584
|
+
import { homedir as homedir18 } from "os";
|
|
12585
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
12336
12586
|
import { execFile as execFile4 } from "child_process";
|
|
12337
12587
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
12338
12588
|
var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
|
|
@@ -12368,7 +12618,7 @@ function vaultProbe(run, dir) {
|
|
|
12368
12618
|
return { state: "unknown", detail: "no vault configured" };
|
|
12369
12619
|
if (!run.pathExists(dir))
|
|
12370
12620
|
return { state: "gone", detail: "vault directory missing" };
|
|
12371
|
-
if (!run.pathExists(
|
|
12621
|
+
if (!run.pathExists(join23(dir, ".obsidian")))
|
|
12372
12622
|
return { state: "gone", detail: "no .obsidian/ (not a vault)" };
|
|
12373
12623
|
return { state: "alive" };
|
|
12374
12624
|
} catch {
|
|
@@ -12436,27 +12686,27 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
12436
12686
|
const sessions = probeSessions(run);
|
|
12437
12687
|
return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
|
|
12438
12688
|
}
|
|
12439
|
-
var SESSIONS_PATH =
|
|
12689
|
+
var SESSIONS_PATH = join23(homedir18(), ".config", "squadrant", "sessions.json");
|
|
12440
12690
|
function onPath(cli) {
|
|
12441
12691
|
const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
12442
|
-
return dirs.some((d) =>
|
|
12692
|
+
return dirs.some((d) => existsSync12(join23(d, cli)));
|
|
12443
12693
|
}
|
|
12444
12694
|
function readSessionsHashes() {
|
|
12445
|
-
const raw = JSON.parse(
|
|
12695
|
+
const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
|
|
12446
12696
|
const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
|
|
12447
12697
|
return [...new Set(hashes)];
|
|
12448
12698
|
}
|
|
12449
12699
|
function defaultProbeRunners() {
|
|
12450
12700
|
return {
|
|
12451
|
-
probeCmuxBin: () => new Promise((
|
|
12701
|
+
probeCmuxBin: () => new Promise((resolve4) => {
|
|
12452
12702
|
try {
|
|
12453
|
-
execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) =>
|
|
12703
|
+
execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve4(!err));
|
|
12454
12704
|
} catch {
|
|
12455
|
-
|
|
12705
|
+
resolve4(false);
|
|
12456
12706
|
}
|
|
12457
12707
|
}),
|
|
12458
12708
|
probeOnPath: async (cli) => onPath(cli),
|
|
12459
|
-
pathExists: (p) =>
|
|
12709
|
+
pathExists: (p) => existsSync12(p),
|
|
12460
12710
|
loadConfig: () => loadConfig(),
|
|
12461
12711
|
loadSessionsHashes: () => readSessionsHashes()
|
|
12462
12712
|
};
|
|
@@ -13317,9 +13567,9 @@ async function startWebServer(opts) {
|
|
|
13317
13567
|
}
|
|
13318
13568
|
res.writeHead(404).end();
|
|
13319
13569
|
});
|
|
13320
|
-
await new Promise((
|
|
13570
|
+
await new Promise((resolve4, reject) => {
|
|
13321
13571
|
server.once("error", reject);
|
|
13322
|
-
server.listen(opts.port, host,
|
|
13572
|
+
server.listen(opts.port, host, resolve4);
|
|
13323
13573
|
});
|
|
13324
13574
|
const addr = server.address();
|
|
13325
13575
|
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
@@ -13331,7 +13581,7 @@ async function startWebServer(opts) {
|
|
|
13331
13581
|
timer.unref?.();
|
|
13332
13582
|
return {
|
|
13333
13583
|
port: boundPort,
|
|
13334
|
-
close: () => new Promise((
|
|
13584
|
+
close: () => new Promise((resolve4) => {
|
|
13335
13585
|
clearInterval(timer);
|
|
13336
13586
|
for (const res of clients) {
|
|
13337
13587
|
try {
|
|
@@ -13340,14 +13590,14 @@ async function startWebServer(opts) {
|
|
|
13340
13590
|
}
|
|
13341
13591
|
}
|
|
13342
13592
|
clients.clear();
|
|
13343
|
-
server.close(() =>
|
|
13593
|
+
server.close(() => resolve4());
|
|
13344
13594
|
})
|
|
13345
13595
|
};
|
|
13346
13596
|
}
|
|
13347
13597
|
|
|
13348
13598
|
// packages/cli/src/commands/dashboard.ts
|
|
13349
13599
|
init_dist();
|
|
13350
|
-
var SOCK3 =
|
|
13600
|
+
var SOCK3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
|
|
13351
13601
|
function detectCurrentWorkspace2() {
|
|
13352
13602
|
const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
|
|
13353
13603
|
const match = out.match(/workspace:\d+/);
|
|
@@ -13866,29 +14116,16 @@ init_dist();
|
|
|
13866
14116
|
init_dist();
|
|
13867
14117
|
init_dist3();
|
|
13868
14118
|
import { Command as Command16 } from "commander";
|
|
13869
|
-
import fs24 from "fs";
|
|
13870
|
-
import path26 from "path";
|
|
13871
14119
|
import chalk17 from "chalk";
|
|
13872
|
-
import matter3 from "gray-matter";
|
|
13873
14120
|
function getDateStr(yesterday) {
|
|
13874
14121
|
return iso(daysAgo(yesterday ? 1 : 0));
|
|
13875
14122
|
}
|
|
13876
14123
|
async function getProjectStandup(name, project, dateStr, registry, config) {
|
|
13877
14124
|
const workspace = registry.forProject(name, config);
|
|
13878
|
-
const spokeVault = resolveHome(project.spokeVault);
|
|
13879
|
-
const statusFile = path26.join(spokeVault, "status.md");
|
|
13880
|
-
let status = {};
|
|
13881
|
-
if (fs24.existsSync(statusFile)) {
|
|
13882
|
-
try {
|
|
13883
|
-
status = matter3(fs24.readFileSync(statusFile, "utf-8")).data;
|
|
13884
|
-
} catch {
|
|
13885
|
-
}
|
|
13886
|
-
}
|
|
13887
14125
|
const log = await readDailyLog(workspace, dateStr);
|
|
13888
14126
|
const gitCommits = getGitCommits(project.path, dateStr);
|
|
13889
14127
|
return {
|
|
13890
14128
|
name,
|
|
13891
|
-
status,
|
|
13892
14129
|
dailyLog: log?.content ?? null,
|
|
13893
14130
|
gitCommits,
|
|
13894
14131
|
blockers: log?.blockers ?? []
|
|
@@ -13907,26 +14144,16 @@ ${header}
|
|
|
13907
14144
|
}
|
|
13908
14145
|
let hasBlockers = false;
|
|
13909
14146
|
for (const s of standups) {
|
|
13910
|
-
const tasksDone = s.status.tasks_completed ?? 0;
|
|
13911
|
-
const tasksTotal = s.status.tasks_total ?? 0;
|
|
13912
|
-
const tasksInProgress = s.status.tasks_in_progress ?? 0;
|
|
13913
14147
|
if (!raw) {
|
|
13914
14148
|
lines.push(chalk17.cyan.bold(`## ${s.name}`));
|
|
13915
14149
|
} else {
|
|
13916
14150
|
lines.push(`## ${s.name}`);
|
|
13917
14151
|
}
|
|
13918
|
-
if (s.gitCommits.length > 0
|
|
14152
|
+
if (s.gitCommits.length > 0) {
|
|
13919
14153
|
lines.push(!raw ? chalk17.green("Done:") : "**Done:**");
|
|
13920
14154
|
for (const commit of s.gitCommits) {
|
|
13921
14155
|
lines.push(` - ${commit}`);
|
|
13922
14156
|
}
|
|
13923
|
-
if (tasksDone > 0 && s.gitCommits.length === 0) {
|
|
13924
|
-
lines.push(` - ${tasksDone}/${tasksTotal} tasks completed`);
|
|
13925
|
-
}
|
|
13926
|
-
}
|
|
13927
|
-
if (tasksInProgress > 0) {
|
|
13928
|
-
lines.push(!raw ? chalk17.yellow("In Progress:") : "**In Progress:**");
|
|
13929
|
-
lines.push(` - ${tasksInProgress} task(s) active`);
|
|
13930
14157
|
}
|
|
13931
14158
|
if (s.dailyLog) {
|
|
13932
14159
|
const sections = ["Completed", "In Progress", "Tomorrow"];
|
|
@@ -13948,19 +14175,18 @@ ${header}
|
|
|
13948
14175
|
lines.push(` - ${b}`);
|
|
13949
14176
|
}
|
|
13950
14177
|
}
|
|
13951
|
-
if (s.gitCommits.length === 0 &&
|
|
14178
|
+
if (s.gitCommits.length === 0 && !s.dailyLog) {
|
|
13952
14179
|
lines.push(!raw ? chalk17.dim(" (no activity)") : " (no activity)");
|
|
13953
14180
|
}
|
|
13954
14181
|
lines.push("");
|
|
13955
14182
|
}
|
|
13956
14183
|
const totalCommits = standups.reduce((sum, s) => sum + s.gitCommits.length, 0);
|
|
13957
|
-
const totalDone = standups.reduce((sum, s) => sum + (s.status.tasks_completed ?? 0), 0);
|
|
13958
14184
|
if (!raw) {
|
|
13959
|
-
lines.push(chalk17.dim(`--- ${totalCommits} commits
|
|
14185
|
+
lines.push(chalk17.dim(`--- ${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630) ---
|
|
13960
14186
|
`));
|
|
13961
14187
|
} else {
|
|
13962
14188
|
lines.push(`---
|
|
13963
|
-
*${totalCommits} commits
|
|
14189
|
+
*${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630)*
|
|
13964
14190
|
`);
|
|
13965
14191
|
}
|
|
13966
14192
|
return lines.join("\n");
|
|
@@ -13998,19 +14224,7 @@ init_dist();
|
|
|
13998
14224
|
init_dist();
|
|
13999
14225
|
init_dist3();
|
|
14000
14226
|
import { Command as Command17 } from "commander";
|
|
14001
|
-
import fs25 from "fs";
|
|
14002
|
-
import path27 from "path";
|
|
14003
14227
|
import chalk18 from "chalk";
|
|
14004
|
-
import matter4 from "gray-matter";
|
|
14005
|
-
function readStatus(spokeVault) {
|
|
14006
|
-
const statusFile = path27.join(spokeVault, "status.md");
|
|
14007
|
-
if (!fs25.existsSync(statusFile)) return {};
|
|
14008
|
-
try {
|
|
14009
|
-
return matter4(fs25.readFileSync(statusFile, "utf-8")).data;
|
|
14010
|
-
} catch {
|
|
14011
|
-
return {};
|
|
14012
|
-
}
|
|
14013
|
-
}
|
|
14014
14228
|
function dedupe(items) {
|
|
14015
14229
|
const seen = /* @__PURE__ */ new Set();
|
|
14016
14230
|
const out = [];
|
|
@@ -14025,8 +14239,6 @@ function dedupe(items) {
|
|
|
14025
14239
|
}
|
|
14026
14240
|
async function getProjectRetro(name, project, fromStr, toStr, registry, config) {
|
|
14027
14241
|
const workspace = registry.forProject(name, config);
|
|
14028
|
-
const spokeVault = resolveHome(project.spokeVault);
|
|
14029
|
-
const status = readStatus(spokeVault);
|
|
14030
14242
|
const shipped = [];
|
|
14031
14243
|
const inProgress = [];
|
|
14032
14244
|
const blocked = [];
|
|
@@ -14056,9 +14268,7 @@ async function getProjectRetro(name, project, fromStr, toStr, registry, config)
|
|
|
14056
14268
|
blocked: dedupe(blocked),
|
|
14057
14269
|
decisions: dedupe(decisions),
|
|
14058
14270
|
commits,
|
|
14059
|
-
mergedPRs
|
|
14060
|
-
tasksCompletedNow: status.tasks_completed ?? 0,
|
|
14061
|
-
tasksInProgressNow: status.tasks_in_progress ?? 0
|
|
14271
|
+
mergedPRs
|
|
14062
14272
|
};
|
|
14063
14273
|
}
|
|
14064
14274
|
function renderList(lines, items, raw, label, color) {
|
|
@@ -14214,9 +14424,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
|
|
|
14214
14424
|
const resolved = resolveTarget(registry, config, target, !!opts.command);
|
|
14215
14425
|
await needRef(resolved);
|
|
14216
14426
|
const finalProject = opts.command ? config.commandName : target;
|
|
14217
|
-
const { join:
|
|
14427
|
+
const { join: join31, dirname: dirname10 } = await import("path");
|
|
14218
14428
|
const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
14219
|
-
const stateRoot =
|
|
14429
|
+
const stateRoot = join31(dirname10(DEFAULT_CONFIG_PATH2), "state");
|
|
14220
14430
|
const seq = await appendCaptainMessage2({
|
|
14221
14431
|
stateRoot,
|
|
14222
14432
|
project: finalProject,
|
|
@@ -14329,9 +14539,9 @@ workspaceCommand.command("read").description("Print the contents of a scope-rela
|
|
|
14329
14539
|
const config = loadConfig();
|
|
14330
14540
|
const registry = buildRegistry2();
|
|
14331
14541
|
try {
|
|
14332
|
-
const { projectTarget, path:
|
|
14542
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14333
14543
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14334
|
-
const content = await driver.read(
|
|
14544
|
+
const content = await driver.read(path34);
|
|
14335
14545
|
process.stdout.write(content);
|
|
14336
14546
|
} catch (err) {
|
|
14337
14547
|
console.error(chalk20.red(err.message));
|
|
@@ -14343,26 +14553,26 @@ workspaceCommand.command("write").description("Write content to a scope-relative
|
|
|
14343
14553
|
const registry = buildRegistry2();
|
|
14344
14554
|
try {
|
|
14345
14555
|
let projectTarget;
|
|
14346
|
-
let
|
|
14556
|
+
let path34;
|
|
14347
14557
|
let rawContent;
|
|
14348
14558
|
if (opts.hub) {
|
|
14349
14559
|
if (arg3 !== void 0) {
|
|
14350
14560
|
throw new Error("With --hub, pass only the path and content");
|
|
14351
14561
|
}
|
|
14352
14562
|
projectTarget = void 0;
|
|
14353
|
-
|
|
14563
|
+
path34 = arg1;
|
|
14354
14564
|
rawContent = arg2;
|
|
14355
14565
|
} else {
|
|
14356
14566
|
if (arg3 === void 0) {
|
|
14357
14567
|
throw new Error("Missing content \u2014 usage: <project> <path> <content>");
|
|
14358
14568
|
}
|
|
14359
14569
|
projectTarget = arg1;
|
|
14360
|
-
|
|
14570
|
+
path34 = arg2;
|
|
14361
14571
|
rawContent = arg3;
|
|
14362
14572
|
}
|
|
14363
14573
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14364
14574
|
const payload = rawContent === "-" ? await readStdin() : rawContent;
|
|
14365
|
-
await driver.write(
|
|
14575
|
+
await driver.write(path34, payload);
|
|
14366
14576
|
} catch (err) {
|
|
14367
14577
|
console.error(chalk20.red(err.message));
|
|
14368
14578
|
process.exit(1);
|
|
@@ -14372,9 +14582,9 @@ workspaceCommand.command("list").description("List entries in a scope-relative d
|
|
|
14372
14582
|
const config = loadConfig();
|
|
14373
14583
|
const registry = buildRegistry2();
|
|
14374
14584
|
try {
|
|
14375
|
-
const { projectTarget, path:
|
|
14585
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14376
14586
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14377
|
-
const entries = await driver.list(
|
|
14587
|
+
const entries = await driver.list(path34);
|
|
14378
14588
|
for (const entry of entries) console.log(entry);
|
|
14379
14589
|
} catch (err) {
|
|
14380
14590
|
console.error(chalk20.red(err.message));
|
|
@@ -14385,9 +14595,9 @@ workspaceCommand.command("exists").description("Exit 0 if path exists, 1 if not"
|
|
|
14385
14595
|
const config = loadConfig();
|
|
14386
14596
|
const registry = buildRegistry2();
|
|
14387
14597
|
try {
|
|
14388
|
-
const { projectTarget, path:
|
|
14598
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14389
14599
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14390
|
-
const ok2 = await driver.exists(
|
|
14600
|
+
const ok2 = await driver.exists(path34);
|
|
14391
14601
|
process.exit(ok2 ? 0 : 1);
|
|
14392
14602
|
} catch (err) {
|
|
14393
14603
|
console.error(chalk20.red(err.message));
|
|
@@ -14398,9 +14608,9 @@ workspaceCommand.command("mkdir").description("Recursively create a scope-relati
|
|
|
14398
14608
|
const config = loadConfig();
|
|
14399
14609
|
const registry = buildRegistry2();
|
|
14400
14610
|
try {
|
|
14401
|
-
const { projectTarget, path:
|
|
14611
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14402
14612
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14403
|
-
await driver.mkdir(
|
|
14613
|
+
await driver.mkdir(path34);
|
|
14404
14614
|
} catch (err) {
|
|
14405
14615
|
console.error(chalk20.red(err.message));
|
|
14406
14616
|
process.exit(1);
|
|
@@ -14439,8 +14649,8 @@ init_dist3();
|
|
|
14439
14649
|
init_dist();
|
|
14440
14650
|
import { Command as Command21 } from "commander";
|
|
14441
14651
|
import chalk22 from "chalk";
|
|
14442
|
-
import
|
|
14443
|
-
import
|
|
14652
|
+
import fs24 from "fs";
|
|
14653
|
+
import path26 from "path";
|
|
14444
14654
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
14445
14655
|
function parseScope(v) {
|
|
14446
14656
|
if (v !== "user" && v !== "project") {
|
|
@@ -14449,10 +14659,10 @@ function parseScope(v) {
|
|
|
14449
14659
|
return v;
|
|
14450
14660
|
}
|
|
14451
14661
|
function findPackageRoot3() {
|
|
14452
|
-
let dir =
|
|
14662
|
+
let dir = path26.dirname(fileURLToPath4(import.meta.url));
|
|
14453
14663
|
while (dir !== "/" && dir !== "") {
|
|
14454
|
-
if (
|
|
14455
|
-
dir =
|
|
14664
|
+
if (fs24.existsSync(path26.join(dir, "package.json"))) return dir;
|
|
14665
|
+
dir = path26.dirname(dir);
|
|
14456
14666
|
}
|
|
14457
14667
|
return process.cwd();
|
|
14458
14668
|
}
|
|
@@ -14576,7 +14786,7 @@ projectionCommand.command("list").description("List registered projection target
|
|
|
14576
14786
|
// packages/cli/src/commands/codex-chat-smoke.ts
|
|
14577
14787
|
init_dist4();
|
|
14578
14788
|
import { Command as Command22 } from "commander";
|
|
14579
|
-
import { resolve as
|
|
14789
|
+
import { resolve as resolve3 } from "path";
|
|
14580
14790
|
var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase 1 gate: prove the codex app-server JSON-RPC path works end-to-end.").option("--cwd <dir>", "working dir for the codex thread", process.cwd()).option("--model <m>", "model id (optional)").option(
|
|
14581
14791
|
"--approval",
|
|
14582
14792
|
"include the approval round-trip (Phase 1 PASS requires this)",
|
|
@@ -14593,7 +14803,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
|
|
|
14593
14803
|
c.start();
|
|
14594
14804
|
await c.initialize();
|
|
14595
14805
|
const { threadId } = await c.startThread({
|
|
14596
|
-
cwd:
|
|
14806
|
+
cwd: resolve3(opts.cwd),
|
|
14597
14807
|
model: opts.model,
|
|
14598
14808
|
sandbox: "workspace-write",
|
|
14599
14809
|
// With --approval, force untrusted policy so codex requests approval
|
|
@@ -14614,7 +14824,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
|
|
|
14614
14824
|
pendingApprovals.push({ id: r.id, method: r.method });
|
|
14615
14825
|
c.respondToServerRequest(r.id, { decision: "approve" });
|
|
14616
14826
|
});
|
|
14617
|
-
await c.sendTurn(threadId, `Write the text "approval-ok" to a file at ${
|
|
14827
|
+
await c.sendTurn(threadId, `Write the text "approval-ok" to a file at ${resolve3(opts.cwd)}/.squadrant-smoke.txt`);
|
|
14618
14828
|
if (pendingApprovals.length === 0) {
|
|
14619
14829
|
throw new Error("approval gate: expected at least one server-request (approval/input) during the turn");
|
|
14620
14830
|
}
|
|
@@ -14647,12 +14857,12 @@ init_dist();
|
|
|
14647
14857
|
init_dist();
|
|
14648
14858
|
init_dist2();
|
|
14649
14859
|
import { Command as Command23 } from "commander";
|
|
14650
|
-
import
|
|
14860
|
+
import fs25 from "fs";
|
|
14651
14861
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
14652
|
-
import { dirname as dirname6, join as
|
|
14862
|
+
import { dirname as dirname6, join as join26 } from "path";
|
|
14653
14863
|
import chalk23 from "chalk";
|
|
14654
14864
|
function runConfigCheck(opts) {
|
|
14655
|
-
const raw = JSON.parse(
|
|
14865
|
+
const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
|
|
14656
14866
|
const def = getDefaultConfig();
|
|
14657
14867
|
const items = detectDrift(raw, def);
|
|
14658
14868
|
let working = raw;
|
|
@@ -14669,7 +14879,7 @@ function runConfigCheck(opts) {
|
|
|
14669
14879
|
stamped = true;
|
|
14670
14880
|
}
|
|
14671
14881
|
if (opts.fix || opts.accept || stamped) {
|
|
14672
|
-
|
|
14882
|
+
fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
|
|
14673
14883
|
}
|
|
14674
14884
|
return { items, applied, remaining, stamped };
|
|
14675
14885
|
}
|
|
@@ -14740,7 +14950,7 @@ function printItems(items) {
|
|
|
14740
14950
|
var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
|
|
14741
14951
|
configCommand.command("check").description("Detect config drift vs the current default schema").option("--fix", "Apply the safe tier (add missing, remove deprecated)", false).option("--accept", "Stamp the current version without changing config (dismiss advisories)", false).option("--json", "Output drift items as JSON", false).action((opts) => {
|
|
14742
14952
|
const pkgVersion = readPkgVersion2();
|
|
14743
|
-
if (!
|
|
14953
|
+
if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
|
|
14744
14954
|
console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
|
|
14745
14955
|
return;
|
|
14746
14956
|
}
|
|
@@ -14785,8 +14995,8 @@ configCommand.command("set").description("Write a config value by dotted key (e.
|
|
|
14785
14995
|
}
|
|
14786
14996
|
});
|
|
14787
14997
|
function readPkgVersion2() {
|
|
14788
|
-
const pkgPath =
|
|
14789
|
-
return JSON.parse(
|
|
14998
|
+
const pkgPath = join26(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
|
|
14999
|
+
return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
|
|
14790
15000
|
}
|
|
14791
15001
|
|
|
14792
15002
|
// packages/cli/src/commands/heal.ts
|
|
@@ -14871,9 +15081,9 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
|
|
|
14871
15081
|
process.exit(code);
|
|
14872
15082
|
})
|
|
14873
15083
|
).addCommand(
|
|
14874
|
-
new Command24("daemon").description("
|
|
15084
|
+
new Command24("daemon").description("Explicitly reconcile + restart squadrantd (#636 operator opt-in \u2014 reads current PATH/entry drift and applies it, regardless of role)").action(async () => {
|
|
14875
15085
|
const code = await runHealDaemon({
|
|
14876
|
-
ensureDaemon: () =>
|
|
15086
|
+
ensureDaemon: () => reregisterDaemon(),
|
|
14877
15087
|
stdout: process.stdout,
|
|
14878
15088
|
stderr: process.stderr
|
|
14879
15089
|
});
|
|
@@ -14939,7 +15149,7 @@ var groupCommand = new Command26("group").description("Cross-project intra-group
|
|
|
14939
15149
|
// packages/cli/src/commands/ping.ts
|
|
14940
15150
|
init_dist();
|
|
14941
15151
|
init_dist2();
|
|
14942
|
-
import { join as
|
|
15152
|
+
import { join as join27, dirname as dirname7 } from "path";
|
|
14943
15153
|
import { Command as Command27 } from "commander";
|
|
14944
15154
|
import chalk27 from "chalk";
|
|
14945
15155
|
init_require_daemon();
|
|
@@ -14949,7 +15159,7 @@ async function runPing(project, message) {
|
|
|
14949
15159
|
const resolved = resolveTarget(registry, config, project, false);
|
|
14950
15160
|
await requireDaemon();
|
|
14951
15161
|
await needRef(resolved);
|
|
14952
|
-
const stateRoot =
|
|
15162
|
+
const stateRoot = join27(dirname7(DEFAULT_CONFIG_PATH), "state");
|
|
14953
15163
|
await appendCaptainMessage({
|
|
14954
15164
|
stateRoot,
|
|
14955
15165
|
project,
|
|
@@ -15026,8 +15236,8 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
|
|
|
15026
15236
|
// packages/cli/src/commands/effort.ts
|
|
15027
15237
|
init_dist();
|
|
15028
15238
|
init_dist2();
|
|
15029
|
-
import
|
|
15030
|
-
import
|
|
15239
|
+
import fs26 from "fs";
|
|
15240
|
+
import path27 from "path";
|
|
15031
15241
|
import { Command as Command29 } from "commander";
|
|
15032
15242
|
import chalk29 from "chalk";
|
|
15033
15243
|
var VALID_EFFORTS = ["max", "balance", "low"];
|
|
@@ -15070,9 +15280,9 @@ function effortScopeLabel(projectName) {
|
|
|
15070
15280
|
}
|
|
15071
15281
|
function canonical(p) {
|
|
15072
15282
|
try {
|
|
15073
|
-
return
|
|
15283
|
+
return fs26.realpathSync(p);
|
|
15074
15284
|
} catch {
|
|
15075
|
-
return
|
|
15285
|
+
return path27.resolve(p);
|
|
15076
15286
|
}
|
|
15077
15287
|
}
|
|
15078
15288
|
async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
|
|
@@ -15122,7 +15332,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
|
|
|
15122
15332
|
const config = loadConfig();
|
|
15123
15333
|
const registry = new RuntimeRegistry2({ cmux: createCmuxDriver2() });
|
|
15124
15334
|
const driver = registry.global(config);
|
|
15125
|
-
const stateRoot =
|
|
15335
|
+
const stateRoot = path27.join(path27.dirname(DEFAULT_CONFIG_PATH), "state");
|
|
15126
15336
|
const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
|
|
15127
15337
|
await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
|
|
15128
15338
|
} catch {
|
|
@@ -15130,18 +15340,308 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
|
|
|
15130
15340
|
}
|
|
15131
15341
|
});
|
|
15132
15342
|
|
|
15343
|
+
// packages/cli/src/commands/tokens.ts
|
|
15344
|
+
init_dist();
|
|
15345
|
+
import fs27 from "fs";
|
|
15346
|
+
import path28 from "path";
|
|
15347
|
+
import os15 from "os";
|
|
15348
|
+
import readline3 from "readline";
|
|
15349
|
+
import { Command as Command30 } from "commander";
|
|
15350
|
+
import chalk30 from "chalk";
|
|
15351
|
+
var CLAUDE_PROJECTS_DIR = path28.join(os15.homedir(), ".claude", "projects");
|
|
15352
|
+
function parseTranscriptLine(rawLine) {
|
|
15353
|
+
const line = rawLine.trim();
|
|
15354
|
+
if (!line) return { timestamp: null, usage: null };
|
|
15355
|
+
let obj;
|
|
15356
|
+
try {
|
|
15357
|
+
obj = JSON.parse(line);
|
|
15358
|
+
} catch {
|
|
15359
|
+
return { timestamp: null, usage: null };
|
|
15360
|
+
}
|
|
15361
|
+
const entry = obj;
|
|
15362
|
+
const timestamp = typeof entry?.timestamp === "string" ? entry.timestamp : null;
|
|
15363
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant") return { timestamp, usage: null };
|
|
15364
|
+
const usage2 = entry.message?.usage;
|
|
15365
|
+
if (!usage2) return { timestamp, usage: null };
|
|
15366
|
+
return {
|
|
15367
|
+
timestamp,
|
|
15368
|
+
usage: {
|
|
15369
|
+
input: usage2.input_tokens ?? 0,
|
|
15370
|
+
output: usage2.output_tokens ?? 0,
|
|
15371
|
+
cacheRead: usage2.cache_read_input_tokens ?? 0,
|
|
15372
|
+
cacheWrite: usage2.cache_creation_input_tokens ?? 0
|
|
15373
|
+
}
|
|
15374
|
+
};
|
|
15375
|
+
}
|
|
15376
|
+
function emptySessionAggregate() {
|
|
15377
|
+
return { calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: [], earliest: null, latest: null };
|
|
15378
|
+
}
|
|
15379
|
+
function extendRange(range, timestamp) {
|
|
15380
|
+
if (!timestamp) return;
|
|
15381
|
+
if (range.earliest === null || timestamp < range.earliest) range.earliest = timestamp;
|
|
15382
|
+
if (range.latest === null || timestamp > range.latest) range.latest = timestamp;
|
|
15383
|
+
}
|
|
15384
|
+
function foldTranscriptLine(agg, rawLine, state) {
|
|
15385
|
+
const { timestamp, usage: usage2 } = parseTranscriptLine(rawLine);
|
|
15386
|
+
extendRange(agg, timestamp);
|
|
15387
|
+
if (!usage2) return;
|
|
15388
|
+
agg.calls++;
|
|
15389
|
+
agg.input += usage2.input;
|
|
15390
|
+
agg.output += usage2.output;
|
|
15391
|
+
agg.cacheRead += usage2.cacheRead;
|
|
15392
|
+
agg.cacheWrite += usage2.cacheWrite;
|
|
15393
|
+
if (usage2.cacheRead !== state.lastCacheRead) {
|
|
15394
|
+
state.lastCacheRead = usage2.cacheRead;
|
|
15395
|
+
agg.turns.push({ total: usage2.input + usage2.cacheWrite + usage2.cacheRead, cacheRead: usage2.cacheRead });
|
|
15396
|
+
}
|
|
15397
|
+
}
|
|
15398
|
+
async function aggregateTranscriptFile(filePath) {
|
|
15399
|
+
const agg = emptySessionAggregate();
|
|
15400
|
+
const state = { lastCacheRead: null };
|
|
15401
|
+
const rl = readline3.createInterface({ input: fs27.createReadStream(filePath), crlfDelay: Infinity });
|
|
15402
|
+
for await (const line of rl) {
|
|
15403
|
+
foldTranscriptLine(agg, line, state);
|
|
15404
|
+
}
|
|
15405
|
+
return agg;
|
|
15406
|
+
}
|
|
15407
|
+
function escapeClaudeProjectPath(cwd) {
|
|
15408
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
15409
|
+
}
|
|
15410
|
+
function isCrewDirName(dirName, captainSlug) {
|
|
15411
|
+
return dirName !== captainSlug && dirName.startsWith(`${captainSlug}-`);
|
|
15412
|
+
}
|
|
15413
|
+
function mergeRanges(ranges) {
|
|
15414
|
+
const merged = { earliest: null, latest: null };
|
|
15415
|
+
for (const r of ranges) {
|
|
15416
|
+
extendRange(merged, r.earliest);
|
|
15417
|
+
extendRange(merged, r.latest);
|
|
15418
|
+
}
|
|
15419
|
+
return merged;
|
|
15420
|
+
}
|
|
15421
|
+
function buildRoleReport(role, sessions) {
|
|
15422
|
+
let calls = 0;
|
|
15423
|
+
let input = 0;
|
|
15424
|
+
let output = 0;
|
|
15425
|
+
let cacheRead = 0;
|
|
15426
|
+
let cacheWrite = 0;
|
|
15427
|
+
const boots = [];
|
|
15428
|
+
let bootConfirmedSessions = 0;
|
|
15429
|
+
for (const s of sessions) {
|
|
15430
|
+
calls += s.calls;
|
|
15431
|
+
input += s.input;
|
|
15432
|
+
output += s.output;
|
|
15433
|
+
cacheRead += s.cacheRead;
|
|
15434
|
+
cacheWrite += s.cacheWrite;
|
|
15435
|
+
const [first, second] = s.turns;
|
|
15436
|
+
if (first) {
|
|
15437
|
+
boots.push(first.total);
|
|
15438
|
+
if (second && first.total > 0) {
|
|
15439
|
+
const rel = Math.abs(second.cacheRead - first.total) / first.total;
|
|
15440
|
+
if (rel < 0.02) bootConfirmedSessions++;
|
|
15441
|
+
}
|
|
15442
|
+
}
|
|
15443
|
+
}
|
|
15444
|
+
const meanCacheReadPerCall = calls > 0 ? cacheRead / calls : null;
|
|
15445
|
+
const meanBoot = boots.length > 0 ? boots.reduce((a, b) => a + b, 0) / boots.length : null;
|
|
15446
|
+
const accumulated = meanCacheReadPerCall !== null && meanBoot !== null ? meanCacheReadPerCall - meanBoot : null;
|
|
15447
|
+
const accumulatedPct = accumulated !== null && meanCacheReadPerCall ? accumulated / meanCacheReadPerCall : null;
|
|
15448
|
+
const { earliest, latest } = mergeRanges(sessions);
|
|
15449
|
+
return {
|
|
15450
|
+
role,
|
|
15451
|
+
sessionFiles: sessions.length,
|
|
15452
|
+
calls,
|
|
15453
|
+
input,
|
|
15454
|
+
output,
|
|
15455
|
+
cacheRead,
|
|
15456
|
+
cacheWrite,
|
|
15457
|
+
meanCacheReadPerCall,
|
|
15458
|
+
meanBoot,
|
|
15459
|
+
accumulated,
|
|
15460
|
+
accumulatedPct,
|
|
15461
|
+
bootConfirmedSessions,
|
|
15462
|
+
bootSampledSessions: boots.length,
|
|
15463
|
+
earliest,
|
|
15464
|
+
latest
|
|
15465
|
+
};
|
|
15466
|
+
}
|
|
15467
|
+
async function readdirSafe(dir) {
|
|
15468
|
+
try {
|
|
15469
|
+
return await fs27.promises.readdir(dir);
|
|
15470
|
+
} catch {
|
|
15471
|
+
return [];
|
|
15472
|
+
}
|
|
15473
|
+
}
|
|
15474
|
+
async function listJsonlFiles(dir) {
|
|
15475
|
+
const entries = await readdirSafe(dir);
|
|
15476
|
+
return entries.filter((e) => e.endsWith(".jsonl")).map((e) => path28.join(dir, e));
|
|
15477
|
+
}
|
|
15478
|
+
async function findTranscriptDirs(claudeProjectsDir, captainSlug) {
|
|
15479
|
+
const entries = await readdirSafe(claudeProjectsDir);
|
|
15480
|
+
const captainDirs = [];
|
|
15481
|
+
const crewDirs = [];
|
|
15482
|
+
for (const entry of entries) {
|
|
15483
|
+
if (entry === captainSlug) captainDirs.push(path28.join(claudeProjectsDir, entry));
|
|
15484
|
+
else if (isCrewDirName(entry, captainSlug)) crewDirs.push(path28.join(claudeProjectsDir, entry));
|
|
15485
|
+
}
|
|
15486
|
+
return { captainDirs, crewDirs };
|
|
15487
|
+
}
|
|
15488
|
+
async function aggregateFiles(files) {
|
|
15489
|
+
const sessions = [];
|
|
15490
|
+
for (const file of files) {
|
|
15491
|
+
sessions.push(await aggregateTranscriptFile(file));
|
|
15492
|
+
}
|
|
15493
|
+
return sessions;
|
|
15494
|
+
}
|
|
15495
|
+
async function collectProjectTokenReport(name, projectPath, claudeProjectsDir = CLAUDE_PROJECTS_DIR) {
|
|
15496
|
+
const captainSlug = escapeClaudeProjectPath(projectPath);
|
|
15497
|
+
const { captainDirs, crewDirs } = await findTranscriptDirs(claudeProjectsDir, captainSlug);
|
|
15498
|
+
const captainFiles = (await Promise.all(captainDirs.map(listJsonlFiles))).flat();
|
|
15499
|
+
const crewFiles = (await Promise.all(crewDirs.map(listJsonlFiles))).flat();
|
|
15500
|
+
const [captainSessions, crewSessions] = await Promise.all([
|
|
15501
|
+
aggregateFiles(captainFiles),
|
|
15502
|
+
aggregateFiles(crewFiles)
|
|
15503
|
+
]);
|
|
15504
|
+
return {
|
|
15505
|
+
project: name,
|
|
15506
|
+
path: projectPath,
|
|
15507
|
+
captain: buildRoleReport("captain", captainSessions),
|
|
15508
|
+
crews: buildRoleReport("crews", crewSessions)
|
|
15509
|
+
};
|
|
15510
|
+
}
|
|
15511
|
+
function formatTokens(n) {
|
|
15512
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
15513
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
15514
|
+
return String(Math.round(n));
|
|
15515
|
+
}
|
|
15516
|
+
function formatPct(n) {
|
|
15517
|
+
return n === null ? "n/a" : `${Math.round(n * 100)}%`;
|
|
15518
|
+
}
|
|
15519
|
+
function printRoleRow(label, r) {
|
|
15520
|
+
const totalVolume = r.input + r.output + r.cacheRead + r.cacheWrite;
|
|
15521
|
+
console.log(
|
|
15522
|
+
` ${label.padEnd(10)} ${String(r.sessionFiles).padStart(6)} ${String(r.calls).padStart(8)} ${formatTokens(r.input).padStart(8)} ${formatTokens(r.output).padStart(8)} ${formatTokens(r.cacheRead).padStart(10)} ${formatTokens(r.cacheWrite).padStart(10)} ${formatTokens(totalVolume).padStart(10)} ` + chalk30.dim(formatRange(r))
|
|
15523
|
+
);
|
|
15524
|
+
}
|
|
15525
|
+
function printBootLine(label, r) {
|
|
15526
|
+
if (r.meanBoot === null || r.meanCacheReadPerCall === null) {
|
|
15527
|
+
console.log(chalk30.dim(` ${label.padEnd(10)} no sessions with turn data`));
|
|
15528
|
+
return;
|
|
15529
|
+
}
|
|
15530
|
+
const bootPct = r.meanCacheReadPerCall > 0 ? r.meanBoot / r.meanCacheReadPerCall : null;
|
|
15531
|
+
console.log(
|
|
15532
|
+
` ${label.padEnd(10)} mean ctx/call ${formatTokens(r.meanCacheReadPerCall).padStart(8)} boot ${formatTokens(r.meanBoot).padStart(8)} (${formatPct(bootPct)}) accumulated ${formatTokens(r.accumulated ?? 0).padStart(8)} (${formatPct(r.accumulatedPct)})` + chalk30.dim(` [boot confirmed ${r.bootConfirmedSessions}/${r.bootSampledSessions} sessions]`)
|
|
15533
|
+
);
|
|
15534
|
+
}
|
|
15535
|
+
function sumRoleReports(role, reports) {
|
|
15536
|
+
const calls = reports.reduce((a, r) => a + r.calls, 0);
|
|
15537
|
+
const cacheRead = reports.reduce((a, r) => a + r.cacheRead, 0);
|
|
15538
|
+
const bootWeighted = reports.reduce(
|
|
15539
|
+
(a, r) => a + (r.meanBoot !== null ? r.meanBoot * r.bootSampledSessions : 0),
|
|
15540
|
+
0
|
|
15541
|
+
);
|
|
15542
|
+
const bootSampledSessions = reports.reduce((a, r) => a + r.bootSampledSessions, 0);
|
|
15543
|
+
const meanBoot = bootSampledSessions > 0 ? bootWeighted / bootSampledSessions : null;
|
|
15544
|
+
const meanCacheReadPerCall = calls > 0 ? cacheRead / calls : null;
|
|
15545
|
+
const accumulated = meanCacheReadPerCall !== null && meanBoot !== null ? meanCacheReadPerCall - meanBoot : null;
|
|
15546
|
+
const accumulatedPct = accumulated !== null && meanCacheReadPerCall ? accumulated / meanCacheReadPerCall : null;
|
|
15547
|
+
const { earliest, latest } = mergeRanges(reports);
|
|
15548
|
+
return {
|
|
15549
|
+
role,
|
|
15550
|
+
sessionFiles: reports.reduce((a, r) => a + r.sessionFiles, 0),
|
|
15551
|
+
calls,
|
|
15552
|
+
input: reports.reduce((a, r) => a + r.input, 0),
|
|
15553
|
+
output: reports.reduce((a, r) => a + r.output, 0),
|
|
15554
|
+
cacheRead,
|
|
15555
|
+
cacheWrite: reports.reduce((a, r) => a + r.cacheWrite, 0),
|
|
15556
|
+
meanCacheReadPerCall,
|
|
15557
|
+
meanBoot,
|
|
15558
|
+
accumulated,
|
|
15559
|
+
accumulatedPct,
|
|
15560
|
+
bootConfirmedSessions: reports.reduce((a, r) => a + r.bootConfirmedSessions, 0),
|
|
15561
|
+
bootSampledSessions,
|
|
15562
|
+
earliest,
|
|
15563
|
+
latest
|
|
15564
|
+
};
|
|
15565
|
+
}
|
|
15566
|
+
var ROLLING_WINDOW_NOTE = "Rolling window, not all-time: Claude Code prunes transcripts older than `cleanupPeriodDays` (default 30 days). Totals shrink over time purely from retention as old sessions age out \u2014 that is NOT the same as spend going down.";
|
|
15567
|
+
function formatDate(iso2) {
|
|
15568
|
+
return iso2 ? iso2.slice(0, 10) : "?";
|
|
15569
|
+
}
|
|
15570
|
+
function formatRange(r) {
|
|
15571
|
+
if (!r.earliest && !r.latest) return "no dated turns";
|
|
15572
|
+
return `${formatDate(r.earliest)} \u2192 ${formatDate(r.latest)}`;
|
|
15573
|
+
}
|
|
15574
|
+
var tokensCommand = new Command30("tokens").description(
|
|
15575
|
+
"Attribute token spend across captain vs crews and boot prefix vs accumulated conversation (Claude Code transcripts only)"
|
|
15576
|
+
).option("--project <name>", "scope to a single registered project").option("--json", "print machine-readable JSON instead of a table").action(async (opts) => {
|
|
15577
|
+
const config = loadConfig();
|
|
15578
|
+
let entries = Object.entries(config.projects);
|
|
15579
|
+
if (opts.project) {
|
|
15580
|
+
if (!(opts.project in config.projects)) {
|
|
15581
|
+
const known = Object.keys(config.projects).sort().join(", ") || "(no projects registered)";
|
|
15582
|
+
console.error(chalk30.red(`Unknown project '${opts.project}'. Known projects: ${known}`));
|
|
15583
|
+
process.exit(1);
|
|
15584
|
+
}
|
|
15585
|
+
entries = entries.filter(([name]) => name === opts.project);
|
|
15586
|
+
}
|
|
15587
|
+
const reports = [];
|
|
15588
|
+
for (const [name, project] of entries) {
|
|
15589
|
+
reports.push(await collectProjectTokenReport(name, resolveHome(project.path)));
|
|
15590
|
+
}
|
|
15591
|
+
const active = reports.filter((r) => r.captain.calls > 0 || r.crews.calls > 0);
|
|
15592
|
+
const skipped = reports.length - active.length;
|
|
15593
|
+
const dataWindow = mergeRanges(active.flatMap((r) => [r.captain, r.crews]));
|
|
15594
|
+
if (opts.json) {
|
|
15595
|
+
console.log(JSON.stringify({ dataWindow, rollingWindowNote: ROLLING_WINDOW_NOTE, projects: active }, null, 2));
|
|
15596
|
+
return;
|
|
15597
|
+
}
|
|
15598
|
+
if (active.length === 0) {
|
|
15599
|
+
console.log(chalk30.yellow("\nNo Claude Code transcripts found for any registered project.\n"));
|
|
15600
|
+
return;
|
|
15601
|
+
}
|
|
15602
|
+
console.log(chalk30.bold("\nToken spend by project (Claude Code transcripts only)\n"));
|
|
15603
|
+
console.log(chalk30.yellow(` Data window: ${formatRange(dataWindow)}`));
|
|
15604
|
+
console.log(chalk30.dim(` ${ROLLING_WINDOW_NOTE}
|
|
15605
|
+
`));
|
|
15606
|
+
console.log(chalk30.dim(` ${"PROJECT/ROLE".padEnd(10)} ${"FILES".padStart(6)} ${"CALLS".padStart(8)} ${"INPUT".padStart(8)} ${"OUTPUT".padStart(8)} ${"CACHE_READ".padStart(10)} ${"CACHE_WRITE".padStart(10)} ${"TOTAL".padStart(10)} WINDOW`));
|
|
15607
|
+
console.log(chalk30.dim(" " + "\u2500".repeat(78)));
|
|
15608
|
+
for (const r of active) {
|
|
15609
|
+
console.log(chalk30.bold(` ${r.project}`));
|
|
15610
|
+
if (r.captain.calls > 0) printRoleRow("captain", r.captain);
|
|
15611
|
+
if (r.crews.calls > 0) printRoleRow("crews", r.crews);
|
|
15612
|
+
}
|
|
15613
|
+
const totalCaptain = sumRoleReports("captain", active.map((r) => r.captain));
|
|
15614
|
+
const totalCrews = sumRoleReports("crews", active.map((r) => r.crews));
|
|
15615
|
+
console.log(chalk30.dim(" " + "\u2500".repeat(78)));
|
|
15616
|
+
console.log(chalk30.bold(" TOTAL"));
|
|
15617
|
+
printRoleRow("captain", totalCaptain);
|
|
15618
|
+
printRoleRow("crews", totalCrews);
|
|
15619
|
+
console.log(chalk30.bold("\nBoot prefix vs accumulated conversation\n"));
|
|
15620
|
+
printBootLine("captain", totalCaptain);
|
|
15621
|
+
printBootLine("crews", totalCrews);
|
|
15622
|
+
console.log(
|
|
15623
|
+
chalk30.dim(
|
|
15624
|
+
"\n cache_read is ~1/10 the price of fresh input \u2014 do not read the TOTAL column as spend.\n claude-only reader today; squadrant is multi-agent but no other driver writes an equivalent transcript yet.\n"
|
|
15625
|
+
)
|
|
15626
|
+
);
|
|
15627
|
+
if (skipped > 0) {
|
|
15628
|
+
console.log(chalk30.dim(` ${skipped} project(s) with no local Claude transcripts omitted.
|
|
15629
|
+
`));
|
|
15630
|
+
}
|
|
15631
|
+
});
|
|
15632
|
+
|
|
15133
15633
|
// packages/cli/src/commands/telegram.ts
|
|
15134
15634
|
init_dist();
|
|
15135
15635
|
init_dist2();
|
|
15136
|
-
import { join as
|
|
15636
|
+
import { join as join28, dirname as dirname8 } from "path";
|
|
15137
15637
|
import { emitKeypressEvents } from "readline";
|
|
15138
|
-
import { Command as
|
|
15139
|
-
import
|
|
15638
|
+
import { Command as Command31 } from "commander";
|
|
15639
|
+
import chalk31 from "chalk";
|
|
15140
15640
|
function defaultStateRoot() {
|
|
15141
|
-
return
|
|
15641
|
+
return join28(dirname8(DEFAULT_CONFIG_PATH), "state");
|
|
15142
15642
|
}
|
|
15143
15643
|
async function questionMasked() {
|
|
15144
|
-
return new Promise((
|
|
15644
|
+
return new Promise((resolve4) => {
|
|
15145
15645
|
emitKeypressEvents(process.stdin);
|
|
15146
15646
|
process.stdin.setRawMode(true);
|
|
15147
15647
|
process.stdin.resume();
|
|
@@ -15158,7 +15658,7 @@ async function questionMasked() {
|
|
|
15158
15658
|
process.stdin.setRawMode(false);
|
|
15159
15659
|
process.stdin.pause();
|
|
15160
15660
|
process.stdout.write("\n");
|
|
15161
|
-
|
|
15661
|
+
resolve4(answer);
|
|
15162
15662
|
} else if (key.name === "backspace") {
|
|
15163
15663
|
if (answer.length > 0) {
|
|
15164
15664
|
answer = answer.slice(0, -1);
|
|
@@ -15175,19 +15675,19 @@ async function questionMasked() {
|
|
|
15175
15675
|
async function questionYesNo(prompt) {
|
|
15176
15676
|
const { createInterface: createInterface2 } = await import("readline");
|
|
15177
15677
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
15178
|
-
return new Promise((
|
|
15678
|
+
return new Promise((resolve4) => {
|
|
15179
15679
|
rl.question(prompt, (ans) => {
|
|
15180
15680
|
rl.close();
|
|
15181
15681
|
process.stdin.pause();
|
|
15182
|
-
|
|
15682
|
+
resolve4(/^y(es)?$/i.test(ans.trim()));
|
|
15183
15683
|
});
|
|
15184
15684
|
});
|
|
15185
15685
|
}
|
|
15186
|
-
var telegramCommand = new
|
|
15686
|
+
var telegramCommand = new Command31("telegram").description("Two-way Telegram integration: push crew events to a topic and reply into the captain pane");
|
|
15187
15687
|
telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
|
|
15188
15688
|
const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
|
|
15189
|
-
console.log(`token: ${tokenSet ?
|
|
15190
|
-
console.log(`supergroup: ${supergroupId ??
|
|
15689
|
+
console.log(`token: ${tokenSet ? chalk31.green("set") : chalk31.yellow("unset")}`);
|
|
15690
|
+
console.log(`supergroup: ${supergroupId ?? chalk31.yellow("unset")}`);
|
|
15191
15691
|
if (links.length === 0) {
|
|
15192
15692
|
console.log("no projects linked");
|
|
15193
15693
|
return;
|
|
@@ -15197,32 +15697,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
|
|
|
15197
15697
|
telegramCommand.command("link").argument("<project>", "project to bind to a Telegram topic").description("Create (or reuse) a forum topic for a project and bind it").action(async (project) => {
|
|
15198
15698
|
const cfg = loadConfig().telegram;
|
|
15199
15699
|
if (!cfg) {
|
|
15200
|
-
console.error(
|
|
15700
|
+
console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
|
|
15201
15701
|
process.exit(1);
|
|
15202
15702
|
}
|
|
15203
15703
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15204
15704
|
if (!token) {
|
|
15205
|
-
console.error(
|
|
15705
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15206
15706
|
process.exit(1);
|
|
15207
15707
|
}
|
|
15208
15708
|
const client = createTelegramClient({ token });
|
|
15209
15709
|
const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
|
|
15210
|
-
console.log(
|
|
15710
|
+
console.log(chalk31.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
|
|
15211
15711
|
});
|
|
15212
15712
|
telegramCommand.command("setup").description("Interactive wizard \u2014 bot token, validate, auto-detect supergroup, write config").option("--reset-token", "force re-entry of the bot token even if one already exists").option("--redetect", "force group re-detection even when a supergroup is already configured").option("--user-id <id>", "allowlist user-id \u2014 enables remote control on a re-run without getUpdates detection", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
15213
15713
|
if (!process.stdin.isTTY) {
|
|
15214
|
-
console.error(
|
|
15714
|
+
console.error(chalk31.red("setup requires a TTY \u2014 pipe input is not supported"));
|
|
15215
15715
|
process.exit(1);
|
|
15216
15716
|
}
|
|
15217
15717
|
console.log();
|
|
15218
|
-
console.log(
|
|
15718
|
+
console.log(chalk31.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
|
|
15219
15719
|
console.log();
|
|
15220
15720
|
console.log("Before you start you need:");
|
|
15221
15721
|
console.log(" 1. A bot token from @BotFather (send /newbot)");
|
|
15222
15722
|
console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
|
|
15223
15723
|
console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
|
|
15224
15724
|
console.log();
|
|
15225
|
-
console.log(
|
|
15725
|
+
console.log(chalk31.bold("Step 1/3 \u2014 Bot token"));
|
|
15226
15726
|
const existingCfg = loadConfig().telegram;
|
|
15227
15727
|
const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15228
15728
|
const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
|
|
@@ -15234,67 +15734,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15234
15734
|
try {
|
|
15235
15735
|
botUser = await client.getMe();
|
|
15236
15736
|
token = existingToken;
|
|
15237
|
-
console.log(
|
|
15737
|
+
console.log(chalk31.green(`Using existing bot token (@${botUser.username})`));
|
|
15238
15738
|
console.log();
|
|
15239
15739
|
} catch {
|
|
15240
|
-
console.log(
|
|
15740
|
+
console.log(chalk31.yellow("Existing token is invalid \u2014 please enter a new one."));
|
|
15241
15741
|
console.log("Paste your bot token then press Enter (input is hidden):");
|
|
15242
15742
|
token = await questionMasked();
|
|
15243
15743
|
if (!token) {
|
|
15244
|
-
console.error(
|
|
15744
|
+
console.error(chalk31.red("token required"));
|
|
15245
15745
|
process.exit(1);
|
|
15246
15746
|
}
|
|
15247
15747
|
client = createTelegramClient({ token });
|
|
15248
15748
|
try {
|
|
15249
15749
|
botUser = await client.getMe();
|
|
15250
15750
|
} catch (e) {
|
|
15251
|
-
console.error(
|
|
15751
|
+
console.error(chalk31.red(`token rejected: ${e.message}`));
|
|
15252
15752
|
process.exit(1);
|
|
15253
15753
|
}
|
|
15254
|
-
console.log(
|
|
15754
|
+
console.log(chalk31.green(`Connected as @${botUser.username}`));
|
|
15255
15755
|
console.log();
|
|
15256
15756
|
}
|
|
15257
15757
|
} else {
|
|
15258
15758
|
console.log("Paste your bot token then press Enter (input is hidden):");
|
|
15259
15759
|
token = await questionMasked();
|
|
15260
15760
|
if (!token) {
|
|
15261
|
-
console.error(
|
|
15761
|
+
console.error(chalk31.red("token required"));
|
|
15262
15762
|
process.exit(1);
|
|
15263
15763
|
}
|
|
15264
15764
|
client = createTelegramClient({ token });
|
|
15265
15765
|
try {
|
|
15266
15766
|
botUser = await client.getMe();
|
|
15267
15767
|
} catch (e) {
|
|
15268
|
-
console.error(
|
|
15768
|
+
console.error(chalk31.red(`token rejected: ${e.message}`));
|
|
15269
15769
|
process.exit(1);
|
|
15270
15770
|
}
|
|
15271
|
-
console.log(
|
|
15771
|
+
console.log(chalk31.green(`Connected as @${botUser.username}`));
|
|
15272
15772
|
console.log();
|
|
15273
15773
|
}
|
|
15274
|
-
console.log(
|
|
15774
|
+
console.log(chalk31.bold("Step 2/3 \u2014 Supergroup"));
|
|
15275
15775
|
const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
|
|
15276
15776
|
let supergroupId;
|
|
15277
15777
|
let detectedUserId;
|
|
15278
15778
|
if (groupDecision === "reuse") {
|
|
15279
15779
|
supergroupId = existingCfg.supergroupId;
|
|
15280
|
-
console.log(
|
|
15780
|
+
console.log(chalk31.green(`Using existing group: ${supergroupId}`));
|
|
15281
15781
|
console.log();
|
|
15282
15782
|
} else {
|
|
15283
15783
|
console.log("Add the bot to your forum supergroup, then send any message in it.");
|
|
15284
|
-
console.log(
|
|
15784
|
+
console.log(chalk31.dim("Waiting for a message (up to 60s)\u2026"));
|
|
15285
15785
|
try {
|
|
15286
15786
|
({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
|
|
15287
15787
|
} catch {
|
|
15288
|
-
console.error(
|
|
15289
|
-
console.error(
|
|
15788
|
+
console.error(chalk31.red("Timed out \u2014 no supergroup message received within 60s."));
|
|
15789
|
+
console.error(chalk31.yellow("Check: bot is an admin in the group \xB7 privacy mode is OFF \xB7 Topics enabled"));
|
|
15290
15790
|
process.exit(1);
|
|
15291
15791
|
}
|
|
15292
|
-
console.log(
|
|
15792
|
+
console.log(chalk31.green(`Found group: ${supergroupId}`));
|
|
15293
15793
|
console.log();
|
|
15294
15794
|
}
|
|
15295
|
-
console.log(
|
|
15296
|
-
console.log(
|
|
15297
|
-
console.log(
|
|
15795
|
+
console.log(chalk31.bold("Step 3/3 \u2014 Remote control + Save"));
|
|
15796
|
+
console.log(chalk31.dim("Remote control enables auto-launching captains and the General command channel"));
|
|
15797
|
+
console.log(chalk31.dim("from your phone \u2014 gated to your Telegram user-id only (fail-closed)."));
|
|
15298
15798
|
const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
|
|
15299
15799
|
let users;
|
|
15300
15800
|
let remoteControl;
|
|
@@ -15308,32 +15808,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15308
15808
|
remoteControl = true;
|
|
15309
15809
|
}
|
|
15310
15810
|
} else if (groupDecision === "detect") {
|
|
15311
|
-
console.log(
|
|
15312
|
-
console.log(
|
|
15811
|
+
console.log(chalk31.yellow("Could not read your user-id from that message \u2014 skipping remote control."));
|
|
15812
|
+
console.log(chalk31.yellow("Re-run with --user-id <id> to enable, or edit telegram.users in config manually."));
|
|
15313
15813
|
printedRemoteControlState = true;
|
|
15314
15814
|
} else {
|
|
15315
15815
|
const existingUsers = existingCfg?.users;
|
|
15316
15816
|
if (existingUsers && existingUsers.length > 0) {
|
|
15317
|
-
console.log(
|
|
15817
|
+
console.log(chalk31.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
|
|
15318
15818
|
} else {
|
|
15319
|
-
console.log(
|
|
15819
|
+
console.log(chalk31.dim("Remote control: off. Re-run with --user-id <id> to enable."));
|
|
15320
15820
|
}
|
|
15321
15821
|
printedRemoteControlState = true;
|
|
15322
15822
|
}
|
|
15323
15823
|
writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
|
|
15324
|
-
console.log(
|
|
15824
|
+
console.log(chalk31.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
|
|
15325
15825
|
if (!printedRemoteControlState) {
|
|
15326
15826
|
if (remoteControl) {
|
|
15327
|
-
console.log(
|
|
15827
|
+
console.log(chalk31.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
|
|
15328
15828
|
} else {
|
|
15329
|
-
console.log(
|
|
15829
|
+
console.log(chalk31.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
|
|
15330
15830
|
}
|
|
15331
15831
|
}
|
|
15332
15832
|
try {
|
|
15333
15833
|
await runRegisterCommands({ client });
|
|
15334
|
-
console.log(
|
|
15834
|
+
console.log(chalk31.dim("Registered the /command menu."));
|
|
15335
15835
|
} catch (e) {
|
|
15336
|
-
console.log(
|
|
15836
|
+
console.log(chalk31.yellow(`command-menu registration skipped: ${e.message}`));
|
|
15337
15837
|
}
|
|
15338
15838
|
const topics = loadState(defaultStateRoot()).topics;
|
|
15339
15839
|
const topicEntries = Object.entries(topics);
|
|
@@ -15342,28 +15842,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15342
15842
|
const project = key.slice(0, key.indexOf("::"));
|
|
15343
15843
|
return `${project}\u2192${id}`;
|
|
15344
15844
|
}).join(", ");
|
|
15345
|
-
console.log(
|
|
15845
|
+
console.log(chalk31.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
|
|
15346
15846
|
} else {
|
|
15347
|
-
console.log(
|
|
15847
|
+
console.log(chalk31.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
|
|
15348
15848
|
}
|
|
15349
15849
|
runTelegramPostSetup({});
|
|
15350
15850
|
console.log();
|
|
15351
|
-
console.log(`Next: ${
|
|
15851
|
+
console.log(`Next: ${chalk31.cyan("squadrant telegram link <project>")}`);
|
|
15352
15852
|
});
|
|
15353
15853
|
telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
|
|
15354
15854
|
const cfg = loadConfig().telegram;
|
|
15355
15855
|
if (!cfg) {
|
|
15356
|
-
console.error(
|
|
15856
|
+
console.error(chalk31.red("telegram config absent \u2014 run: squadrant telegram setup"));
|
|
15357
15857
|
process.exit(1);
|
|
15358
15858
|
}
|
|
15359
15859
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15360
15860
|
if (!token) {
|
|
15361
|
-
console.error(
|
|
15861
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15362
15862
|
process.exit(1);
|
|
15363
15863
|
}
|
|
15364
15864
|
const client = createTelegramClient({ token });
|
|
15365
15865
|
await runRegisterCommands({ client });
|
|
15366
|
-
console.log(
|
|
15866
|
+
console.log(chalk31.green(`registered ${BOT_COMMANDS.length} bot commands`));
|
|
15367
15867
|
});
|
|
15368
15868
|
telegramCommand.command("notify").argument("[project]", "project to toggle").argument("[state]", "on | off | crew | cap").argument("[value]", "tier for crew (all|alert_only|done_only|none) or on|off for cap").option("--status", "list notification state for all projects").description("Live on|off (state), or crew <tier> / cap <on|off> preference (per-project config)").action(async (project, state, value, opts) => {
|
|
15369
15869
|
const stateRoot = defaultStateRoot();
|
|
@@ -15374,7 +15874,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
|
|
|
15374
15874
|
return;
|
|
15375
15875
|
}
|
|
15376
15876
|
for (const r of rows) {
|
|
15377
|
-
console.log(` ${r.project}: ${r.active ?
|
|
15877
|
+
console.log(` ${r.project}: ${r.active ? chalk31.green("on") : chalk31.dim("off (muted)")}`);
|
|
15378
15878
|
}
|
|
15379
15879
|
return;
|
|
15380
15880
|
}
|
|
@@ -15383,53 +15883,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
|
|
|
15383
15883
|
const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15384
15884
|
if (state === "crew" || state === "cap") {
|
|
15385
15885
|
if (value === void 0) {
|
|
15386
|
-
console.error(
|
|
15886
|
+
console.error(chalk31.red(`usage: squadrant telegram notify <project> ${state} <value>`));
|
|
15387
15887
|
process.exit(1);
|
|
15388
15888
|
}
|
|
15389
15889
|
const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
|
|
15390
15890
|
const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
|
|
15391
15891
|
const res = runTelegramNotifyPref({ project, dimension: state, value });
|
|
15392
15892
|
if (!res.ok) {
|
|
15393
|
-
console.error(
|
|
15893
|
+
console.error(chalk31.red(res.message));
|
|
15394
15894
|
process.exit(1);
|
|
15395
15895
|
}
|
|
15396
|
-
console.log(
|
|
15896
|
+
console.log(chalk31.green(`${project} ${state} = ${value}`));
|
|
15397
15897
|
const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
|
|
15398
15898
|
if (tgCfg && token) {
|
|
15399
15899
|
const client = createTelegramClient({ token });
|
|
15400
15900
|
const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
|
|
15401
|
-
if (sent) console.log(
|
|
15901
|
+
if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
|
|
15402
15902
|
}
|
|
15403
15903
|
return;
|
|
15404
15904
|
}
|
|
15405
15905
|
if (state !== "on" && state !== "off") {
|
|
15406
|
-
console.error(
|
|
15906
|
+
console.error(chalk31.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
|
|
15407
15907
|
process.exit(1);
|
|
15408
15908
|
}
|
|
15409
15909
|
const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
|
|
15410
15910
|
const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
|
|
15411
15911
|
const after = { ...before, active: state === "on" };
|
|
15412
15912
|
runTelegramNotifySet({ project, active: state === "on", stateRoot });
|
|
15413
|
-
console.log(
|
|
15913
|
+
console.log(chalk31.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
|
|
15414
15914
|
if (tgCfg && token) {
|
|
15415
15915
|
const client = createTelegramClient({ token });
|
|
15416
15916
|
const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
|
|
15417
|
-
if (sent) console.log(
|
|
15917
|
+
if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
|
|
15418
15918
|
}
|
|
15419
15919
|
});
|
|
15420
15920
|
telegramCommand.command("send").argument("<project>", "project whose topic receives the message").argument("[message...]", "message text (omit to read from stdin)").description("Send a message to a project's linked Telegram topic").action(async (project, messageParts) => {
|
|
15421
15921
|
const cfg = loadConfig().telegram;
|
|
15422
15922
|
if (!cfg) {
|
|
15423
|
-
console.error(
|
|
15923
|
+
console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
|
|
15424
15924
|
process.exit(1);
|
|
15425
15925
|
}
|
|
15426
15926
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15427
15927
|
if (!token) {
|
|
15428
|
-
console.error(
|
|
15928
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15429
15929
|
process.exit(1);
|
|
15430
15930
|
}
|
|
15431
15931
|
if (!capAllowed(project, cfg.notify)) {
|
|
15432
|
-
console.log(
|
|
15932
|
+
console.log(chalk31.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
|
|
15433
15933
|
return;
|
|
15434
15934
|
}
|
|
15435
15935
|
let message;
|
|
@@ -15442,19 +15942,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
|
|
|
15442
15942
|
for await (const line of rl) lines.push(line);
|
|
15443
15943
|
message = lines.join("\n").trimEnd();
|
|
15444
15944
|
if (!message) {
|
|
15445
|
-
console.error(
|
|
15945
|
+
console.error(chalk31.red("no message provided (stdin was empty)"));
|
|
15446
15946
|
process.exit(1);
|
|
15447
15947
|
}
|
|
15448
15948
|
} else {
|
|
15449
|
-
console.error(
|
|
15949
|
+
console.error(chalk31.red("message required \u2014 pass as argument or pipe via stdin"));
|
|
15450
15950
|
process.exit(1);
|
|
15451
15951
|
}
|
|
15452
15952
|
const client = createTelegramClient({ token });
|
|
15453
15953
|
try {
|
|
15454
15954
|
const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
|
|
15455
|
-
console.log(
|
|
15955
|
+
console.log(chalk31.green(`sent to group ${chatId} topic ${topicId}`));
|
|
15456
15956
|
} catch (e) {
|
|
15457
|
-
console.error(
|
|
15957
|
+
console.error(chalk31.red(e.message));
|
|
15458
15958
|
process.exit(1);
|
|
15459
15959
|
}
|
|
15460
15960
|
});
|
|
@@ -15462,10 +15962,92 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
|
|
|
15462
15962
|
// packages/cli/src/commands/hooks.ts
|
|
15463
15963
|
init_dist2();
|
|
15464
15964
|
init_dist4();
|
|
15465
|
-
|
|
15466
|
-
import {
|
|
15467
|
-
import {
|
|
15468
|
-
|
|
15965
|
+
init_dist();
|
|
15966
|
+
import { Command as Command32 } from "commander";
|
|
15967
|
+
import { join as join29 } from "path";
|
|
15968
|
+
import { homedir as homedir21 } from "os";
|
|
15969
|
+
|
|
15970
|
+
// packages/cli/src/lib/captain-session-registry.ts
|
|
15971
|
+
import fs28 from "fs";
|
|
15972
|
+
import path29 from "path";
|
|
15973
|
+
|
|
15974
|
+
// packages/cli/src/lib/handoff-facts.ts
|
|
15975
|
+
var STALE_FETCH_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
15976
|
+
var SESSION_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15977
|
+
function staleWarning(live) {
|
|
15978
|
+
if (live.aheadOfBaseSource !== "local-git") return null;
|
|
15979
|
+
if (live.fetchAgeMs === null) {
|
|
15980
|
+
return "aheadOfBase came from local git with no known last-fetch time \u2014 treat as possibly stale";
|
|
15981
|
+
}
|
|
15982
|
+
if (live.fetchAgeMs > STALE_FETCH_WARNING_MS) {
|
|
15983
|
+
const hours = Math.round(live.fetchAgeMs / 36e5);
|
|
15984
|
+
return `aheadOfBase came from local git, last fetched ${hours}h ago \u2014 may be stale`;
|
|
15985
|
+
}
|
|
15986
|
+
return null;
|
|
15987
|
+
}
|
|
15988
|
+
function sourceAvailability(live, claudeMem, checkpoint, gapSessions) {
|
|
15989
|
+
const available = [];
|
|
15990
|
+
const missing = [];
|
|
15991
|
+
const liveHasData = live.openPRs.length > 0 || live.liveCrews.length > 0 || live.aheadOfBaseSource !== "unknown" || live.recentCommits.length > 0;
|
|
15992
|
+
(liveHasData ? available : missing).push("liveRepo");
|
|
15993
|
+
const claudeMemHasData = !!claudeMem && (claudeMem.latestSessionSummary !== null || claudeMem.recentDecisions.length > 0);
|
|
15994
|
+
(claudeMemHasData ? available : missing).push("claudeMem");
|
|
15995
|
+
(checkpoint ? available : missing).push("checkpoint");
|
|
15996
|
+
(gapSessions.length > 0 ? available : missing).push("gapSessions");
|
|
15997
|
+
return { available, missing };
|
|
15998
|
+
}
|
|
15999
|
+
function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, extras = {}) {
|
|
16000
|
+
const sortedGap = [...gapSessions].sort((a, b) => Date.parse(b.session.startedAt) - Date.parse(a.session.startedAt));
|
|
16001
|
+
const { available, missing } = sourceAvailability(live, claudeMem, checkpoint, sortedGap);
|
|
16002
|
+
return {
|
|
16003
|
+
meta: {
|
|
16004
|
+
generatedAt: now,
|
|
16005
|
+
checkpointFilename: checkpoint?.filename ?? null,
|
|
16006
|
+
usedFallbackWindow: extras.usedFallbackWindow ?? false,
|
|
16007
|
+
fallbackWindowMs: extras.usedFallbackWindow ? extras.fallbackWindowMs ?? SESSION_WINDOW_MS : null,
|
|
16008
|
+
gapSessionIds: sortedGap.map((s) => s.session.sessionId),
|
|
16009
|
+
sourcesAvailable: available,
|
|
16010
|
+
sourcesMissing: missing,
|
|
16011
|
+
registryNote: extras.registryNote ?? null
|
|
16012
|
+
},
|
|
16013
|
+
liveRepo: { ...live, staleWarning: staleWarning(live) },
|
|
16014
|
+
claudeMem,
|
|
16015
|
+
checkpoint,
|
|
16016
|
+
gapSessions: sortedGap
|
|
16017
|
+
};
|
|
16018
|
+
}
|
|
16019
|
+
|
|
16020
|
+
// packages/cli/src/lib/captain-session-registry.ts
|
|
16021
|
+
var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
|
|
16022
|
+
function appendCaptainSession(spokeVault, record) {
|
|
16023
|
+
fs28.mkdirSync(spokeVault, { recursive: true });
|
|
16024
|
+
const file = path29.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
|
|
16025
|
+
fs28.appendFileSync(file, JSON.stringify(record) + "\n");
|
|
16026
|
+
}
|
|
16027
|
+
function readCaptainSessionRegistry(spokeVault) {
|
|
16028
|
+
const file = path29.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
|
|
16029
|
+
if (!fs28.existsSync(file)) return [];
|
|
16030
|
+
const records = [];
|
|
16031
|
+
for (const line of fs28.readFileSync(file, "utf-8").split("\n")) {
|
|
16032
|
+
if (!line.trim()) continue;
|
|
16033
|
+
try {
|
|
16034
|
+
records.push(JSON.parse(line));
|
|
16035
|
+
} catch {
|
|
16036
|
+
}
|
|
16037
|
+
}
|
|
16038
|
+
return records;
|
|
16039
|
+
}
|
|
16040
|
+
function selectGapSessions(records, currentSessionId, checkpoint, now, fallbackWindowMs = SESSION_WINDOW_MS) {
|
|
16041
|
+
const excludingCurrent = records.filter((r) => r.sessionId !== currentSessionId);
|
|
16042
|
+
const filtered = checkpoint ? excludingCurrent.filter((r) => Date.parse(r.startedAt) > now - checkpoint.ageMs) : excludingCurrent.filter((r) => now - Date.parse(r.startedAt) <= fallbackWindowMs);
|
|
16043
|
+
return {
|
|
16044
|
+
gapSessions: filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)),
|
|
16045
|
+
usedFallbackWindow: !checkpoint
|
|
16046
|
+
};
|
|
16047
|
+
}
|
|
16048
|
+
|
|
16049
|
+
// packages/cli/src/commands/hooks.ts
|
|
16050
|
+
var SOCK4 = join29(homedir21(), ".config", "squadrant", "squadrant.sock");
|
|
15469
16051
|
async function sendToSock(req) {
|
|
15470
16052
|
await sendRequest(SOCK4, req);
|
|
15471
16053
|
}
|
|
@@ -15488,14 +16070,31 @@ function mapHookSub(sub, payload, taskId) {
|
|
|
15488
16070
|
return null;
|
|
15489
16071
|
}
|
|
15490
16072
|
}
|
|
16073
|
+
function buildCaptainSessionRecord(payload, project, fallbackCwd, now) {
|
|
16074
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
16075
|
+
const p = payload;
|
|
16076
|
+
const sessionId = typeof p.session_id === "string" && p.session_id ? p.session_id : null;
|
|
16077
|
+
if (!sessionId) return null;
|
|
16078
|
+
const cwd = typeof p.cwd === "string" && p.cwd ? p.cwd : fallbackCwd;
|
|
16079
|
+
const transcriptPath = (typeof p.transcript_path === "string" && p.transcript_path ? p.transcript_path : null) ?? deriveTranscriptPath(sessionId, cwd) ?? "";
|
|
16080
|
+
return { sessionId, project, agent: "claude", startedAt: now, cwd, transcriptPath };
|
|
16081
|
+
}
|
|
16082
|
+
function recordCaptainSessionStart(payload) {
|
|
16083
|
+
try {
|
|
16084
|
+
const config = loadConfig();
|
|
16085
|
+
const project = resolveCurrentProject(config);
|
|
16086
|
+
if (!project) return;
|
|
16087
|
+
const proj = config.projects[project];
|
|
16088
|
+
if (!proj) return;
|
|
16089
|
+
const record = buildCaptainSessionRecord(payload, project, process.cwd(), (/* @__PURE__ */ new Date()).toISOString());
|
|
16090
|
+
if (!record) return;
|
|
16091
|
+
appendCaptainSession(proj.spokeVault, record);
|
|
16092
|
+
} catch {
|
|
16093
|
+
}
|
|
16094
|
+
}
|
|
15491
16095
|
function hooksCommand() {
|
|
15492
|
-
const hooks = new
|
|
16096
|
+
const hooks = new Command32("hooks").description("(internal) receive lifecycle hook events from agent processes");
|
|
15493
16097
|
hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
|
|
15494
|
-
const taskId = process.env.SQUADRANT_CREW_TASK_ID;
|
|
15495
|
-
const project = process.env.SQUADRANT_CREW_PROJECT;
|
|
15496
|
-
if (!taskId || !project) {
|
|
15497
|
-
process.exit(0);
|
|
15498
|
-
}
|
|
15499
16098
|
let stdin = "";
|
|
15500
16099
|
try {
|
|
15501
16100
|
for await (const chunk of process.stdin) stdin += chunk;
|
|
@@ -15508,6 +16107,14 @@ function hooksCommand() {
|
|
|
15508
16107
|
} catch {
|
|
15509
16108
|
}
|
|
15510
16109
|
}
|
|
16110
|
+
if (sub === "session-start" && process.env.SQUADRANT_ROLE === "captain") {
|
|
16111
|
+
recordCaptainSessionStart(payload);
|
|
16112
|
+
}
|
|
16113
|
+
const taskId = process.env.SQUADRANT_CREW_TASK_ID;
|
|
16114
|
+
const project = process.env.SQUADRANT_CREW_PROJECT;
|
|
16115
|
+
if (!taskId || !project) {
|
|
16116
|
+
process.exit(0);
|
|
16117
|
+
}
|
|
15511
16118
|
const ev = mapHookSub(sub, payload, taskId);
|
|
15512
16119
|
if (!ev) {
|
|
15513
16120
|
process.exit(0);
|
|
@@ -15521,26 +16128,553 @@ function hooksCommand() {
|
|
|
15521
16128
|
return hooks;
|
|
15522
16129
|
}
|
|
15523
16130
|
|
|
16131
|
+
// packages/cli/src/commands/work.ts
|
|
16132
|
+
init_dist();
|
|
16133
|
+
init_dist2();
|
|
16134
|
+
import path30 from "path";
|
|
16135
|
+
import { Command as Command33 } from "commander";
|
|
16136
|
+
import chalk32 from "chalk";
|
|
16137
|
+
function detectCurrentProject(config, cwd = process.cwd()) {
|
|
16138
|
+
for (const [name, proj] of Object.entries(config.projects)) {
|
|
16139
|
+
const projPath = resolveHome(proj.path);
|
|
16140
|
+
if (cwd === projPath || cwd.startsWith(projPath + path30.sep)) return name;
|
|
16141
|
+
}
|
|
16142
|
+
return void 0;
|
|
16143
|
+
}
|
|
16144
|
+
function groupByParent(items) {
|
|
16145
|
+
const byId = new Map(items.map((i) => [i.id, i]));
|
|
16146
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
16147
|
+
const roots = [];
|
|
16148
|
+
for (const item of items) {
|
|
16149
|
+
if (item.parent && byId.has(item.parent)) {
|
|
16150
|
+
const list = childrenOf.get(item.parent) ?? [];
|
|
16151
|
+
list.push(item);
|
|
16152
|
+
childrenOf.set(item.parent, list);
|
|
16153
|
+
} else {
|
|
16154
|
+
roots.push(item);
|
|
16155
|
+
}
|
|
16156
|
+
}
|
|
16157
|
+
return { roots, childrenOf };
|
|
16158
|
+
}
|
|
16159
|
+
function visibleItems(items, includeDone) {
|
|
16160
|
+
if (includeDone) return items;
|
|
16161
|
+
const { childrenOf } = groupByParent(items);
|
|
16162
|
+
const memo = /* @__PURE__ */ new Map();
|
|
16163
|
+
const keep = (item) => {
|
|
16164
|
+
const cached = memo.get(item.id);
|
|
16165
|
+
if (cached !== void 0) return cached;
|
|
16166
|
+
memo.set(item.id, false);
|
|
16167
|
+
const result = !TERMINAL_WORK_STATES.has(item.state) || (childrenOf.get(item.id) ?? []).some(keep);
|
|
16168
|
+
memo.set(item.id, result);
|
|
16169
|
+
return result;
|
|
16170
|
+
};
|
|
16171
|
+
return items.filter(keep);
|
|
16172
|
+
}
|
|
16173
|
+
function stateColor(state) {
|
|
16174
|
+
switch (state) {
|
|
16175
|
+
case "done":
|
|
16176
|
+
return chalk32.green;
|
|
16177
|
+
case "cancelled":
|
|
16178
|
+
return chalk32.dim;
|
|
16179
|
+
case "blocked":
|
|
16180
|
+
return chalk32.red;
|
|
16181
|
+
case "paused":
|
|
16182
|
+
return chalk32.yellow;
|
|
16183
|
+
default:
|
|
16184
|
+
return chalk32.cyan;
|
|
16185
|
+
}
|
|
16186
|
+
}
|
|
16187
|
+
function printItem(item, indent) {
|
|
16188
|
+
const color = stateColor(item.state);
|
|
16189
|
+
const line = " ".repeat(indent) + `${chalk32.dim(item.id)} ${item.title} ${color(`[${item.state}]`)}` + (indent === 0 ? chalk32.dim(` (${item.project})`) : "");
|
|
16190
|
+
console.log(TERMINAL_WORK_STATES.has(item.state) ? chalk32.dim(line) : line);
|
|
16191
|
+
}
|
|
16192
|
+
function printTree(items) {
|
|
16193
|
+
const { roots, childrenOf } = groupByParent(items);
|
|
16194
|
+
const walk = (item, depth) => {
|
|
16195
|
+
printItem(item, depth);
|
|
16196
|
+
for (const child of childrenOf.get(item.id) ?? []) walk(child, depth + 1);
|
|
16197
|
+
};
|
|
16198
|
+
for (const root of roots) walk(root, 0);
|
|
16199
|
+
}
|
|
16200
|
+
function printFlat(items) {
|
|
16201
|
+
for (const item of items) printItem(item, 0);
|
|
16202
|
+
}
|
|
16203
|
+
var startCmd = new Command33("start").description("Start a new work item").argument("<title>", "what you're doing").option("--project <name>", "project this work belongs to (defaults to the current registered project)").option("--parent <id>", "id of the wave/parent item this nests under").option("--tag <tag>", "attach a tag (repeatable)", (v, prev) => [...prev, v], []).action((title, opts) => {
|
|
16204
|
+
const config = loadConfig();
|
|
16205
|
+
const store = createWorkStore();
|
|
16206
|
+
purgeExpiredWorkItems(store);
|
|
16207
|
+
const project = opts.project ?? detectCurrentProject(config);
|
|
16208
|
+
if (!project) {
|
|
16209
|
+
console.error(chalk32.red("No --project given and cwd is not inside a registered project."));
|
|
16210
|
+
process.exit(1);
|
|
16211
|
+
}
|
|
16212
|
+
if (opts.parent && !findWorkItemById(store, opts.parent)) {
|
|
16213
|
+
console.error(chalk32.red(`Parent work item '${opts.parent}' not found.`));
|
|
16214
|
+
process.exit(1);
|
|
16215
|
+
}
|
|
16216
|
+
const item = createWorkItem(store, { project, title, parent: opts.parent ?? null, tags: opts.tag });
|
|
16217
|
+
console.log(chalk32.green(`\u2713 ${item.id}`) + ` ${item.title}` + chalk32.dim(` (${item.project})`));
|
|
16218
|
+
});
|
|
16219
|
+
var listCmd2 = new Command33("list").description("List work items").option("--project <name>", "scope to one project").option("--all", "list across every project").option("--tree", "render parent/child nesting").option("--include-done", "include done/cancelled items").action((opts) => {
|
|
16220
|
+
const config = loadConfig();
|
|
16221
|
+
const store = createWorkStore();
|
|
16222
|
+
purgeExpiredWorkItems(store);
|
|
16223
|
+
let items;
|
|
16224
|
+
if (opts.project) {
|
|
16225
|
+
items = store.list(opts.project);
|
|
16226
|
+
} else if (opts.all) {
|
|
16227
|
+
items = store.listAll();
|
|
16228
|
+
} else {
|
|
16229
|
+
const project = detectCurrentProject(config);
|
|
16230
|
+
if (!project) {
|
|
16231
|
+
console.error(chalk32.red("cwd is not inside a registered project \u2014 pass --project or --all."));
|
|
16232
|
+
process.exit(1);
|
|
16233
|
+
}
|
|
16234
|
+
items = store.list(project);
|
|
16235
|
+
}
|
|
16236
|
+
items = visibleItems(items, opts.includeDone ?? false);
|
|
16237
|
+
if (items.length === 0) {
|
|
16238
|
+
console.log(chalk32.dim("\nNo work items.\n"));
|
|
16239
|
+
return;
|
|
16240
|
+
}
|
|
16241
|
+
console.log();
|
|
16242
|
+
if (opts.tree) printTree(items);
|
|
16243
|
+
else printFlat(items);
|
|
16244
|
+
console.log();
|
|
16245
|
+
});
|
|
16246
|
+
function closeCommand(name, state, flag, key, desc) {
|
|
16247
|
+
return new Command33(name).description(`Mark a work item ${state}`).argument("<id>", "work item id").option(flag, desc).action((id, opts) => {
|
|
16248
|
+
const store = createWorkStore();
|
|
16249
|
+
purgeExpiredWorkItems(store);
|
|
16250
|
+
const item = closeWorkItem(store, id, state, { note: opts[key] });
|
|
16251
|
+
if (!item) {
|
|
16252
|
+
console.error(chalk32.red(`Work item '${id}' not found.`));
|
|
16253
|
+
process.exit(1);
|
|
16254
|
+
}
|
|
16255
|
+
console.log(chalk32.green(`\u2713 ${item.id}`) + ` ${item.title} ${chalk32.dim(`[${item.state}]`)}`);
|
|
16256
|
+
if (state === "done") {
|
|
16257
|
+
const openChildren = findOpenChildren(store, item.id);
|
|
16258
|
+
if (openChildren.length > 0) {
|
|
16259
|
+
const names = openChildren.map((c) => `${c.id} [${c.state}]`).join(", ");
|
|
16260
|
+
console.log(chalk32.yellow(`\u26A0 still has ${openChildren.length} unfinished child item(s): ${names}`));
|
|
16261
|
+
}
|
|
16262
|
+
}
|
|
16263
|
+
});
|
|
16264
|
+
}
|
|
16265
|
+
var doneCmd = closeCommand("done", "done", "--note <text>", "note", "closing note");
|
|
16266
|
+
var cancelCmd = closeCommand("cancel", "cancelled", "--why <text>", "why", "reason");
|
|
16267
|
+
var workCommand = new Command33("work").description("Track your own in-flight work \u2014 persisted, cross-project, cross-session").addCommand(startCmd).addCommand(listCmd2).addCommand(doneCmd).addCommand(cancelCmd);
|
|
16268
|
+
|
|
16269
|
+
// packages/cli/src/commands/handoff.ts
|
|
16270
|
+
init_dist();
|
|
16271
|
+
import { Command as Command34 } from "commander";
|
|
16272
|
+
import path33 from "path";
|
|
16273
|
+
import os16 from "os";
|
|
16274
|
+
|
|
16275
|
+
// packages/cli/src/lib/handoff-live-repo.ts
|
|
16276
|
+
init_dist();
|
|
16277
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
16278
|
+
import fs29 from "fs";
|
|
16279
|
+
import path31 from "path";
|
|
16280
|
+
|
|
16281
|
+
// packages/cli/src/lib/handoff-branch-state.ts
|
|
16282
|
+
function tryRun(runner, cmd, args, cwd) {
|
|
16283
|
+
try {
|
|
16284
|
+
return runner.run(cmd, args, cwd);
|
|
16285
|
+
} catch {
|
|
16286
|
+
return null;
|
|
16287
|
+
}
|
|
16288
|
+
}
|
|
16289
|
+
function parseUpstreamTrack(raw) {
|
|
16290
|
+
if (raw === null) return { upstreamStatus: "unknown", aheadOfUpstream: null, behindUpstream: null };
|
|
16291
|
+
const [upstreamShort = "", track = ""] = raw.trim().split("|");
|
|
16292
|
+
if (!upstreamShort.trim()) return { upstreamStatus: "no-upstream", aheadOfUpstream: null, behindUpstream: null };
|
|
16293
|
+
if (track.includes("[gone]")) return { upstreamStatus: "upstream-gone", aheadOfUpstream: null, behindUpstream: null };
|
|
16294
|
+
const aheadMatch = track.match(/ahead (\d+)/);
|
|
16295
|
+
const behindMatch = track.match(/behind (\d+)/);
|
|
16296
|
+
const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
16297
|
+
const behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
16298
|
+
if (ahead > 0 && behind > 0) return { upstreamStatus: "diverged", aheadOfUpstream: ahead, behindUpstream: behind };
|
|
16299
|
+
if (ahead > 0) return { upstreamStatus: "ahead", aheadOfUpstream: ahead, behindUpstream: 0 };
|
|
16300
|
+
if (behind > 0) return { upstreamStatus: "behind", aheadOfUpstream: 0, behindUpstream: behind };
|
|
16301
|
+
return { upstreamStatus: "up-to-date", aheadOfUpstream: 0, behindUpstream: 0 };
|
|
16302
|
+
}
|
|
16303
|
+
function gatherMergedIntoBase(runner, projectPath, branch, baseBranch) {
|
|
16304
|
+
if (branch === baseBranch) return null;
|
|
16305
|
+
const originBase = `origin/${baseBranch}`;
|
|
16306
|
+
const originResolved = tryRun(runner, "git", ["-C", projectPath, "rev-parse", "--verify", originBase], projectPath) !== null;
|
|
16307
|
+
const target = originResolved ? originBase : baseBranch;
|
|
16308
|
+
const branchSha = tryRun(runner, "git", ["-C", projectPath, "rev-parse", branch], projectPath);
|
|
16309
|
+
const mergeBaseSha = tryRun(runner, "git", ["-C", projectPath, "merge-base", branch, target], projectPath);
|
|
16310
|
+
if (branchSha === null || mergeBaseSha === null) return null;
|
|
16311
|
+
return branchSha.trim() === mergeBaseSha.trim();
|
|
16312
|
+
}
|
|
16313
|
+
function gatherDirty(runner, projectPath) {
|
|
16314
|
+
const status = tryRun(runner, "git", ["-C", projectPath, "status", "--porcelain"], projectPath);
|
|
16315
|
+
if (status === null) return null;
|
|
16316
|
+
return status.trim().length > 0;
|
|
16317
|
+
}
|
|
16318
|
+
function gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2) {
|
|
16319
|
+
const fetchPerformed = fetch2 && tryRun(runner, "git", ["-C", projectPath, "fetch", "origin"], projectPath) !== null;
|
|
16320
|
+
const dirtyWorkingTree = gatherDirty(runner, projectPath);
|
|
16321
|
+
if (detached) {
|
|
16322
|
+
return {
|
|
16323
|
+
upstreamStatus: "unknown",
|
|
16324
|
+
aheadOfUpstream: null,
|
|
16325
|
+
behindUpstream: null,
|
|
16326
|
+
mergedIntoBase: null,
|
|
16327
|
+
dirtyWorkingTree,
|
|
16328
|
+
onUnexpectedBranch: false,
|
|
16329
|
+
fetchPerformed
|
|
16330
|
+
};
|
|
16331
|
+
}
|
|
16332
|
+
const trackRaw = tryRun(
|
|
16333
|
+
runner,
|
|
16334
|
+
"git",
|
|
16335
|
+
["-C", projectPath, "for-each-ref", "--format=%(upstream:short)|%(upstream:track)", `refs/heads/${branch}`],
|
|
16336
|
+
projectPath
|
|
16337
|
+
);
|
|
16338
|
+
const { upstreamStatus, aheadOfUpstream, behindUpstream } = parseUpstreamTrack(trackRaw);
|
|
16339
|
+
return {
|
|
16340
|
+
upstreamStatus,
|
|
16341
|
+
aheadOfUpstream,
|
|
16342
|
+
behindUpstream,
|
|
16343
|
+
mergedIntoBase: gatherMergedIntoBase(runner, projectPath, branch, baseBranch),
|
|
16344
|
+
dirtyWorkingTree,
|
|
16345
|
+
onUnexpectedBranch: branch.startsWith("crew/"),
|
|
16346
|
+
fetchPerformed
|
|
16347
|
+
};
|
|
16348
|
+
}
|
|
16349
|
+
|
|
16350
|
+
// packages/cli/src/lib/handoff-live-repo.ts
|
|
16351
|
+
var RECENT_COMMITS_LIMIT = 15;
|
|
16352
|
+
var OPEN_PR_LIMIT = 20;
|
|
16353
|
+
var RELEASE_BRANCH = "main";
|
|
16354
|
+
var defaultCommandRunner = {
|
|
16355
|
+
run(cmd, args, cwd) {
|
|
16356
|
+
return execFileSync8(cmd, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
16357
|
+
}
|
|
16358
|
+
};
|
|
16359
|
+
function tryRun2(runner, cmd, args, cwd) {
|
|
16360
|
+
try {
|
|
16361
|
+
return runner.run(cmd, args, cwd);
|
|
16362
|
+
} catch {
|
|
16363
|
+
return null;
|
|
16364
|
+
}
|
|
16365
|
+
}
|
|
16366
|
+
function tryInt(raw) {
|
|
16367
|
+
if (raw === null) return null;
|
|
16368
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
16369
|
+
return Number.isFinite(n) ? n : null;
|
|
16370
|
+
}
|
|
16371
|
+
function gatherGhRepoInfo(runner, projectPath) {
|
|
16372
|
+
const out = tryRun2(runner, "gh", ["repo", "view", "--json", "nameWithOwner,defaultBranchRef"], projectPath);
|
|
16373
|
+
if (!out) return null;
|
|
16374
|
+
try {
|
|
16375
|
+
const parsed = JSON.parse(out);
|
|
16376
|
+
if (!parsed.defaultBranchRef) return null;
|
|
16377
|
+
return { nameWithOwner: parsed.nameWithOwner, defaultBranch: parsed.defaultBranchRef.name };
|
|
16378
|
+
} catch {
|
|
16379
|
+
return null;
|
|
16380
|
+
}
|
|
16381
|
+
}
|
|
16382
|
+
function gatherOpenPRs(runner, projectPath) {
|
|
16383
|
+
const out = tryRun2(
|
|
16384
|
+
runner,
|
|
16385
|
+
"gh",
|
|
16386
|
+
["pr", "list", "--json", "number,title,headRefName", "--limit", String(OPEN_PR_LIMIT)],
|
|
16387
|
+
projectPath
|
|
16388
|
+
);
|
|
16389
|
+
if (!out) return [];
|
|
16390
|
+
try {
|
|
16391
|
+
const parsed = JSON.parse(out);
|
|
16392
|
+
return parsed.map((pr) => ({ number: pr.number, title: pr.title, headRefName: pr.headRefName }));
|
|
16393
|
+
} catch {
|
|
16394
|
+
return [];
|
|
16395
|
+
}
|
|
16396
|
+
}
|
|
16397
|
+
function gatherLiveCrews(tasks) {
|
|
16398
|
+
return tasks.filter((t) => !TERMINAL_STATES.has(t.state)).map((t) => ({ name: t.name ?? t.id, state: t.state, task: t.task, question: t.question }));
|
|
16399
|
+
}
|
|
16400
|
+
function ghAheadOfBase(runner, projectPath, nameWithOwner, base, branch) {
|
|
16401
|
+
return tryInt(
|
|
16402
|
+
tryRun2(runner, "gh", ["api", `repos/${nameWithOwner}/compare/${base}...${branch}`, "--jq", ".ahead_by"], projectPath)
|
|
16403
|
+
);
|
|
16404
|
+
}
|
|
16405
|
+
function ghBaseSha(runner, projectPath, nameWithOwner, base) {
|
|
16406
|
+
const out = tryRun2(runner, "gh", ["api", `repos/${nameWithOwner}/commits/${base}`, "--jq", ".sha"], projectPath);
|
|
16407
|
+
return out ? out.trim() : null;
|
|
16408
|
+
}
|
|
16409
|
+
function localBaseSha(runner, projectPath, base) {
|
|
16410
|
+
const out = tryRun2(runner, "git", ["-C", projectPath, "rev-parse", `origin/${base}`], projectPath);
|
|
16411
|
+
return out ? out.trim() : null;
|
|
16412
|
+
}
|
|
16413
|
+
function localAheadOfBase(runner, projectPath, base) {
|
|
16414
|
+
return tryInt(tryRun2(runner, "git", ["-C", projectPath, "rev-list", "--count", `origin/${base}..HEAD`], projectPath));
|
|
16415
|
+
}
|
|
16416
|
+
function readFetchAgeMs(projectPath, now) {
|
|
16417
|
+
try {
|
|
16418
|
+
const stat2 = fs29.statSync(path31.join(projectPath, ".git", "FETCH_HEAD"));
|
|
16419
|
+
return now - stat2.mtime.getTime();
|
|
16420
|
+
} catch {
|
|
16421
|
+
return null;
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = defaultCommandRunner, now = Date.now(), fetch2 = false) {
|
|
16425
|
+
const branch = (tryRun2(runner, "git", ["-C", projectPath, "rev-parse", "--abbrev-ref", "HEAD"], projectPath) ?? "").trim();
|
|
16426
|
+
const detached = branch === "HEAD";
|
|
16427
|
+
const log = tryRun2(runner, "git", ["-C", projectPath, "log", `-${RECENT_COMMITS_LIMIT}`, "--oneline"], projectPath) ?? "";
|
|
16428
|
+
const recentCommits = log.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
16429
|
+
const ghInfo = gatherGhRepoInfo(runner, projectPath);
|
|
16430
|
+
const baseBranch = ghInfo?.defaultBranch ?? fallbackBaseBranch;
|
|
16431
|
+
const baseBranchSource = ghInfo ? "gh-api" : "local-fallback";
|
|
16432
|
+
const fetchAgeMs = readFetchAgeMs(projectPath, now);
|
|
16433
|
+
let aheadOfBase = 0;
|
|
16434
|
+
let aheadOfBaseSource = "unknown";
|
|
16435
|
+
if (branch === baseBranch) {
|
|
16436
|
+
aheadOfBase = null;
|
|
16437
|
+
aheadOfBaseSource = "n-a";
|
|
16438
|
+
} else {
|
|
16439
|
+
if (ghInfo && !detached) {
|
|
16440
|
+
const ghAhead = ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, baseBranch, branch);
|
|
16441
|
+
if (ghAhead !== null) {
|
|
16442
|
+
aheadOfBase = ghAhead;
|
|
16443
|
+
aheadOfBaseSource = "gh-api";
|
|
16444
|
+
}
|
|
16445
|
+
}
|
|
16446
|
+
if (aheadOfBaseSource === "unknown") {
|
|
16447
|
+
const localAhead = localAheadOfBase(runner, projectPath, baseBranch);
|
|
16448
|
+
if (localAhead !== null) {
|
|
16449
|
+
aheadOfBase = localAhead;
|
|
16450
|
+
aheadOfBaseSource = "local-git";
|
|
16451
|
+
}
|
|
16452
|
+
}
|
|
16453
|
+
}
|
|
16454
|
+
const unreleasedAheadOfReleaseBranch = ghInfo && baseBranch !== RELEASE_BRANCH ? ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, RELEASE_BRANCH, baseBranch) : null;
|
|
16455
|
+
const conflicts = [];
|
|
16456
|
+
if (ghInfo) {
|
|
16457
|
+
const ghSha = ghBaseSha(runner, projectPath, ghInfo.nameWithOwner, baseBranch);
|
|
16458
|
+
const localSha = localBaseSha(runner, projectPath, baseBranch);
|
|
16459
|
+
if (ghSha && localSha && ghSha !== localSha) {
|
|
16460
|
+
const ageNote = fetchAgeMs !== null ? `fetched ${Math.round(fetchAgeMs / 36e5)}h ago` : "fetch age unknown";
|
|
16461
|
+
conflicts.push({
|
|
16462
|
+
field: "baseBranch",
|
|
16463
|
+
claim: `local git's last-known ${baseBranch} is ${localSha} (${ageNote})`,
|
|
16464
|
+
fact: `GitHub's live ${baseBranch} is ${ghSha}`,
|
|
16465
|
+
resolution: "GitHub API wins \u2014 local git may be stale"
|
|
16466
|
+
});
|
|
16467
|
+
}
|
|
16468
|
+
}
|
|
16469
|
+
return {
|
|
16470
|
+
branch,
|
|
16471
|
+
detached,
|
|
16472
|
+
baseBranch,
|
|
16473
|
+
baseBranchSource,
|
|
16474
|
+
recentCommits,
|
|
16475
|
+
aheadOfBase,
|
|
16476
|
+
aheadOfBaseSource,
|
|
16477
|
+
fetchAgeMs,
|
|
16478
|
+
openPRs: gatherOpenPRs(runner, projectPath),
|
|
16479
|
+
liveCrews: gatherLiveCrews(tasks),
|
|
16480
|
+
conflicts,
|
|
16481
|
+
branchState: gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2),
|
|
16482
|
+
unreleasedAheadOfReleaseBranch
|
|
16483
|
+
};
|
|
16484
|
+
}
|
|
16485
|
+
|
|
16486
|
+
// packages/cli/src/lib/handoff-claude-mem.ts
|
|
16487
|
+
import { createRequire } from "module";
|
|
16488
|
+
import fs30 from "fs";
|
|
16489
|
+
var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
16490
|
+
var CLAUDE_MEM_RECENCY_LIMIT = 20;
|
|
16491
|
+
function decisionText(row) {
|
|
16492
|
+
if (row.facts) {
|
|
16493
|
+
try {
|
|
16494
|
+
const parsed = JSON.parse(row.facts);
|
|
16495
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed.join("; ");
|
|
16496
|
+
} catch {
|
|
16497
|
+
}
|
|
16498
|
+
}
|
|
16499
|
+
return row.narrative ?? "";
|
|
16500
|
+
}
|
|
16501
|
+
function queryClaudeMem(dbPath, project) {
|
|
16502
|
+
if (!fs30.existsSync(dbPath)) return null;
|
|
16503
|
+
let db;
|
|
16504
|
+
try {
|
|
16505
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
16506
|
+
} catch {
|
|
16507
|
+
return null;
|
|
16508
|
+
}
|
|
16509
|
+
try {
|
|
16510
|
+
const summaryRow = db.prepare(
|
|
16511
|
+
`SELECT request, completed, next_steps, created_at FROM session_summaries
|
|
16512
|
+
WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1`
|
|
16513
|
+
).get(project);
|
|
16514
|
+
const decisionRows = db.prepare(
|
|
16515
|
+
`SELECT title, narrative, facts, created_at FROM observations
|
|
16516
|
+
WHERE project = ? AND type = 'decision' ORDER BY created_at_epoch DESC LIMIT ?`
|
|
16517
|
+
).all(project, CLAUDE_MEM_RECENCY_LIMIT);
|
|
16518
|
+
const recentDecisions = decisionRows.map((r) => ({
|
|
16519
|
+
title: r.title,
|
|
16520
|
+
text: decisionText(r),
|
|
16521
|
+
createdAt: r.created_at
|
|
16522
|
+
}));
|
|
16523
|
+
const candidates = [summaryRow?.created_at, ...decisionRows.map((r) => r.created_at)].filter(
|
|
16524
|
+
(v) => !!v
|
|
16525
|
+
);
|
|
16526
|
+
const oldestCreatedAt = candidates.length > 0 ? candidates.reduce((a, b) => a < b ? a : b) : null;
|
|
16527
|
+
return {
|
|
16528
|
+
latestSessionSummary: summaryRow ? {
|
|
16529
|
+
request: summaryRow.request,
|
|
16530
|
+
completed: summaryRow.completed,
|
|
16531
|
+
nextSteps: summaryRow.next_steps,
|
|
16532
|
+
createdAt: summaryRow.created_at
|
|
16533
|
+
} : null,
|
|
16534
|
+
recentDecisions,
|
|
16535
|
+
oldestCreatedAt
|
|
16536
|
+
};
|
|
16537
|
+
} catch {
|
|
16538
|
+
return null;
|
|
16539
|
+
} finally {
|
|
16540
|
+
db.close();
|
|
16541
|
+
}
|
|
16542
|
+
}
|
|
16543
|
+
|
|
16544
|
+
// packages/cli/src/lib/handoff-transcript.ts
|
|
16545
|
+
import fs31 from "fs";
|
|
16546
|
+
var TRANSCRIPT_BYTE_CAP = 2e5;
|
|
16547
|
+
function tailOf(content, byteCap) {
|
|
16548
|
+
const buf = Buffer.from(content, "utf-8");
|
|
16549
|
+
if (buf.length <= byteCap) return content;
|
|
16550
|
+
const text = buf.subarray(buf.length - byteCap).toString("utf-8");
|
|
16551
|
+
return text.split("\n").slice(1).join("\n");
|
|
16552
|
+
}
|
|
16553
|
+
function extractMessages(tailText) {
|
|
16554
|
+
let lastUserMessage = null;
|
|
16555
|
+
let lastAssistantText = null;
|
|
16556
|
+
for (const line of tailText.split("\n")) {
|
|
16557
|
+
if (!line.trim()) continue;
|
|
16558
|
+
let obj;
|
|
16559
|
+
try {
|
|
16560
|
+
obj = JSON.parse(line);
|
|
16561
|
+
} catch {
|
|
16562
|
+
continue;
|
|
16563
|
+
}
|
|
16564
|
+
if (obj.type === "user" && typeof obj.message?.content === "string") {
|
|
16565
|
+
lastUserMessage = obj.message.content;
|
|
16566
|
+
} else if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
16567
|
+
const texts = obj.message.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
|
|
16568
|
+
if (texts.length > 0) lastAssistantText = texts.join("\n");
|
|
16569
|
+
}
|
|
16570
|
+
}
|
|
16571
|
+
return { lastUserMessage, lastAssistantText };
|
|
16572
|
+
}
|
|
16573
|
+
function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
|
|
16574
|
+
if (!fs31.existsSync(transcriptPath)) return null;
|
|
16575
|
+
const content = fs31.readFileSync(transcriptPath, "utf-8");
|
|
16576
|
+
const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
|
|
16577
|
+
const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
|
|
16578
|
+
return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
|
|
16579
|
+
}
|
|
16580
|
+
|
|
16581
|
+
// packages/cli/src/lib/handoff-archive.ts
|
|
16582
|
+
import fs32 from "fs";
|
|
16583
|
+
import path32 from "path";
|
|
16584
|
+
function readNewestArchivedHandoff(spokeVault, now) {
|
|
16585
|
+
const dir = path32.join(spokeVault, "handoffs");
|
|
16586
|
+
if (!fs32.existsSync(dir)) return null;
|
|
16587
|
+
const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
|
|
16588
|
+
const full = path32.join(dir, e.name);
|
|
16589
|
+
return { name: e.name, full, mtime: fs32.statSync(full).mtime };
|
|
16590
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
16591
|
+
for (const candidate of candidates) {
|
|
16592
|
+
let content;
|
|
16593
|
+
try {
|
|
16594
|
+
content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
|
|
16595
|
+
} catch {
|
|
16596
|
+
continue;
|
|
16597
|
+
}
|
|
16598
|
+
return { filename: candidate.name, path: candidate.full, ageMs: now - candidate.mtime.getTime(), content };
|
|
16599
|
+
}
|
|
16600
|
+
return null;
|
|
16601
|
+
}
|
|
16602
|
+
|
|
16603
|
+
// packages/cli/src/commands/handoff.ts
|
|
16604
|
+
var CLAUDE_MEM_DB_PATH = path33.join(os16.homedir(), ".claude-mem", "claude-mem.db");
|
|
16605
|
+
async function defaultFetchTasks(project) {
|
|
16606
|
+
return await squadrantdCall({ kind: "list", project });
|
|
16607
|
+
}
|
|
16608
|
+
async function runHandoffFacts(project, deps = {}) {
|
|
16609
|
+
const config = loadConfig();
|
|
16610
|
+
const proj = config.projects[project];
|
|
16611
|
+
if (!proj) {
|
|
16612
|
+
throw new Error(`Project '${project}' not found. Run 'squadrant projects list'.`);
|
|
16613
|
+
}
|
|
16614
|
+
const fallbackBaseBranch = resolveWorktreeBase(proj.path);
|
|
16615
|
+
let tasks;
|
|
16616
|
+
try {
|
|
16617
|
+
tasks = await (deps.fetchTasks ?? defaultFetchTasks)(project);
|
|
16618
|
+
} catch {
|
|
16619
|
+
tasks = [];
|
|
16620
|
+
}
|
|
16621
|
+
const nowIso = deps.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16622
|
+
const nowMs = Date.parse(nowIso);
|
|
16623
|
+
const fallbackWindowMs = deps.windowMs ?? SESSION_WINDOW_MS;
|
|
16624
|
+
const live = gatherLiveRepoState(proj.path, fallbackBaseBranch, tasks, deps.runner ?? defaultCommandRunner, nowMs, deps.fetch ?? false);
|
|
16625
|
+
const claudeMem = queryClaudeMem(deps.claudeMemDbPath ?? CLAUDE_MEM_DB_PATH, project);
|
|
16626
|
+
const checkpoint = readNewestArchivedHandoff(proj.spokeVault, nowMs);
|
|
16627
|
+
const currentSessionId = deps.currentSessionId !== void 0 ? deps.currentSessionId : process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
16628
|
+
let registryNote = null;
|
|
16629
|
+
let gapSessions = [];
|
|
16630
|
+
let usedFallbackWindow = false;
|
|
16631
|
+
if (currentSessionId === null) {
|
|
16632
|
+
registryNote = "current session id unknown (CLAUDE_CODE_SESSION_ID unset) \u2014 cannot safely exclude the running session, so the gap was skipped";
|
|
16633
|
+
} else {
|
|
16634
|
+
const allRecords = readCaptainSessionRegistry(proj.spokeVault);
|
|
16635
|
+
if (allRecords.length === 0) {
|
|
16636
|
+
registryNote = "no session registry found yet for this project (#651's SessionStart hook may not have fired before now)";
|
|
16637
|
+
}
|
|
16638
|
+
const selection = selectGapSessions(allRecords, currentSessionId, checkpoint, nowMs, fallbackWindowMs);
|
|
16639
|
+
usedFallbackWindow = selection.usedFallbackWindow;
|
|
16640
|
+
gapSessions = selection.gapSessions.map((session) => ({ session, transcript: extractTranscriptTail(session.transcriptPath) }));
|
|
16641
|
+
}
|
|
16642
|
+
return assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, nowIso, {
|
|
16643
|
+
registryNote,
|
|
16644
|
+
usedFallbackWindow,
|
|
16645
|
+
fallbackWindowMs
|
|
16646
|
+
});
|
|
16647
|
+
}
|
|
16648
|
+
var handoffCommand = new Command34("handoff").description(
|
|
16649
|
+
"Handoff continuity \u2014 gather verified facts for the captain to synthesize a handoff from (#650/#651)"
|
|
16650
|
+
);
|
|
16651
|
+
handoffCommand.command("facts <project>").description(
|
|
16652
|
+
"Gather structured facts (gh API > local git > claude-mem > registry-attributed session window) \u2014 NOT a handoff. Read-only, pre-rendered JSON on stdout; the caller synthesizes."
|
|
16653
|
+
).option("--fetch", "update remote-tracking refs (git fetch origin) before computing branch state \u2014 the only opt-in exception to this command's read-only contract", false).action(async (project, opts) => {
|
|
16654
|
+
const out = await runHandoffFacts(project, { fetch: opts.fetch });
|
|
16655
|
+
console.log(JSON.stringify(out, null, 2));
|
|
16656
|
+
});
|
|
16657
|
+
|
|
15524
16658
|
// packages/cli/src/index.ts
|
|
15525
16659
|
init_dist();
|
|
15526
16660
|
init_dist();
|
|
15527
16661
|
init_dist();
|
|
15528
16662
|
init_dist();
|
|
15529
16663
|
var __dirname = dirname9(fileURLToPath6(import.meta.url));
|
|
15530
|
-
var pkg = JSON.parse(
|
|
16664
|
+
var pkg = JSON.parse(readFileSync16(join30(__dirname, "..", "package.json"), "utf-8"));
|
|
15531
16665
|
ensureRuntimeSynced({
|
|
15532
|
-
sourceRoot:
|
|
15533
|
-
runtimeRoot:
|
|
16666
|
+
sourceRoot: join30(__dirname, ".."),
|
|
16667
|
+
runtimeRoot: join30(homedir22(), ".config", "squadrant")
|
|
15534
16668
|
});
|
|
15535
16669
|
if (process.argv[2] !== "config") {
|
|
15536
16670
|
try {
|
|
15537
|
-
const cfgPath =
|
|
15538
|
-
if (
|
|
15539
|
-
const cfg = JSON.parse(
|
|
16671
|
+
const cfgPath = join30(homedir22(), ".config", "squadrant", "config.json");
|
|
16672
|
+
if (existsSync13(cfgPath)) {
|
|
16673
|
+
const cfg = JSON.parse(readFileSync16(cfgPath, "utf-8"));
|
|
15540
16674
|
if (needsCheck(cfg, pkg.version)) {
|
|
15541
16675
|
const items = detectDrift(cfg, getDefaultConfig());
|
|
15542
16676
|
if (items.length === 0) {
|
|
15543
|
-
|
|
16677
|
+
writeFileSync12(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
|
|
15544
16678
|
} else {
|
|
15545
16679
|
const from = cfg._squadrantVersion ?? "an earlier version";
|
|
15546
16680
|
process.stderr.write(
|
|
@@ -15558,9 +16692,9 @@ if (process.argv[2] !== "config") {
|
|
|
15558
16692
|
}
|
|
15559
16693
|
}
|
|
15560
16694
|
if (!process.env.SQUADRANT_DAEMON_SKIP) {
|
|
15561
|
-
ensureDaemon();
|
|
16695
|
+
ensureDaemon(void 0, { operatorInitiated: isOperatorInitiatedCommand(process.argv[2]) });
|
|
15562
16696
|
}
|
|
15563
|
-
var program = new
|
|
16697
|
+
var program = new Command35();
|
|
15564
16698
|
program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
|
|
15565
16699
|
program.addCommand(doctorCommand);
|
|
15566
16700
|
program.addCommand(initCommand);
|
|
@@ -15589,8 +16723,11 @@ program.addCommand(pingCommand);
|
|
|
15589
16723
|
program.addCommand(dispatchCommand);
|
|
15590
16724
|
program.addCommand(cmuxCommand);
|
|
15591
16725
|
program.addCommand(effortCommand);
|
|
16726
|
+
program.addCommand(tokensCommand);
|
|
15592
16727
|
program.addCommand(telegramCommand);
|
|
15593
16728
|
program.addCommand(hooksCommand());
|
|
16729
|
+
program.addCommand(workCommand);
|
|
16730
|
+
program.addCommand(handoffCommand);
|
|
15594
16731
|
program.parseAsync().catch((e) => {
|
|
15595
16732
|
process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
|
|
15596
16733
|
`);
|