squadrant 0.16.6 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1512 -448
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +111 -104
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +50 -45
- package/plugin/skills/command-ops/SKILL.md +12 -20
- package/plugin/skills/karpathy-principles/SKILL.md +1 -1
- package/scripts/read-handoff.sh +16 -4
- package/templates/captain.claude.md +9 -5
- package/templates/captain.generic.md +8 -3
- package/templates/command.claude.md +2 -2
- package/templates/crew.generic.md +1 -1
- package/templates/crew.opencode.md +1 -1
- package/templates/learnings.claude.md +1 -1
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();
|
|
@@ -2266,9 +2297,9 @@ function startServer(sockPath, handlerOrCallbacks, onListenError = defaultListen
|
|
|
2266
2297
|
return server;
|
|
2267
2298
|
}
|
|
2268
2299
|
function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
2269
|
-
return new Promise((
|
|
2300
|
+
return new Promise((resolve4) => {
|
|
2270
2301
|
if (!existsSync5(sockPath)) {
|
|
2271
|
-
|
|
2302
|
+
resolve4(false);
|
|
2272
2303
|
return;
|
|
2273
2304
|
}
|
|
2274
2305
|
const conn = createConnection(sockPath);
|
|
@@ -2277,7 +2308,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
|
2277
2308
|
conn.destroy();
|
|
2278
2309
|
} catch {
|
|
2279
2310
|
}
|
|
2280
|
-
|
|
2311
|
+
resolve4(v);
|
|
2281
2312
|
};
|
|
2282
2313
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2283
2314
|
conn.on("connect", () => {
|
|
@@ -2291,7 +2322,7 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
|
2291
2322
|
});
|
|
2292
2323
|
}
|
|
2293
2324
|
function sendRequest(sockPath, msg, timeoutMs = 5e3) {
|
|
2294
|
-
return new Promise((
|
|
2325
|
+
return new Promise((resolve4, reject) => {
|
|
2295
2326
|
const conn = createConnection(sockPath);
|
|
2296
2327
|
const dec = createDecoder();
|
|
2297
2328
|
const timer = setTimeout(() => {
|
|
@@ -2307,7 +2338,7 @@ function sendRequest(sockPath, msg, timeoutMs = 5e3) {
|
|
|
2307
2338
|
if (m._v !== void 0 && m._v !== PROTOCOL_VERSION) {
|
|
2308
2339
|
reject(new Error(`squadrantd protocol v${m._v}, this client expects v${PROTOCOL_VERSION} \u2014 upgrade squadrantd or this CLI`));
|
|
2309
2340
|
} else if (m.ok) {
|
|
2310
|
-
|
|
2341
|
+
resolve4(m.reply);
|
|
2311
2342
|
} else {
|
|
2312
2343
|
reject(new Error(m.error));
|
|
2313
2344
|
}
|
|
@@ -2527,6 +2558,151 @@ var init_store = __esm({
|
|
|
2527
2558
|
}
|
|
2528
2559
|
});
|
|
2529
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
|
+
|
|
2530
2706
|
// packages/core/dist/snapshot.js
|
|
2531
2707
|
var snapshot_exports = {};
|
|
2532
2708
|
__export(snapshot_exports, {
|
|
@@ -2580,16 +2756,16 @@ var init_snapshot = __esm({
|
|
|
2580
2756
|
|
|
2581
2757
|
// packages/core/dist/launchd.js
|
|
2582
2758
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2583
|
-
import { mkdirSync as
|
|
2584
|
-
import { homedir as
|
|
2585
|
-
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";
|
|
2586
2762
|
import { fileURLToPath } from "url";
|
|
2587
2763
|
function plistPath() {
|
|
2588
|
-
return
|
|
2764
|
+
return join8(homedir5(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
2589
2765
|
}
|
|
2590
2766
|
function daemonEntryPath() {
|
|
2591
|
-
const p =
|
|
2592
|
-
if (!
|
|
2767
|
+
const p = join8(dirname3(fileURLToPath(import.meta.url)), "squadrantd.js");
|
|
2768
|
+
if (!existsSync8(p)) {
|
|
2593
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)`);
|
|
2594
2770
|
}
|
|
2595
2771
|
return p;
|
|
@@ -2597,10 +2773,10 @@ function daemonEntryPath() {
|
|
|
2597
2773
|
function xmlEscape(s) {
|
|
2598
2774
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2599
2775
|
}
|
|
2600
|
-
function sanitizePathForPlist(
|
|
2776
|
+
function sanitizePathForPlist(path34) {
|
|
2601
2777
|
const seen = /* @__PURE__ */ new Set();
|
|
2602
2778
|
const stable = [];
|
|
2603
|
-
for (const p of
|
|
2779
|
+
for (const p of path34.split(":")) {
|
|
2604
2780
|
if (!p)
|
|
2605
2781
|
continue;
|
|
2606
2782
|
if (p.includes("/.claude/plugins/"))
|
|
@@ -2646,7 +2822,7 @@ function buildDaemonPath(shellPath) {
|
|
|
2646
2822
|
}).join(":");
|
|
2647
2823
|
}
|
|
2648
2824
|
function renderPlist(nodeBin, daemonEntry, pathEnv = "") {
|
|
2649
|
-
const logPath2 =
|
|
2825
|
+
const logPath2 = join8(homedir5(), ".config", "squadrant", "squadrantd.log");
|
|
2650
2826
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2651
2827
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2652
2828
|
<plist version="1.0">
|
|
@@ -2675,13 +2851,13 @@ function _resetRestartInFlightForTest() {
|
|
|
2675
2851
|
restartInFlight = false;
|
|
2676
2852
|
}
|
|
2677
2853
|
function daemonLockPath() {
|
|
2678
|
-
return
|
|
2854
|
+
return join8(homedir5(), ".config", "squadrant", "daemon.lock");
|
|
2679
2855
|
}
|
|
2680
2856
|
function tryAcquireDaemonLock() {
|
|
2681
2857
|
const lp = daemonLockPath();
|
|
2682
|
-
if (
|
|
2858
|
+
if (existsSync8(lp)) {
|
|
2683
2859
|
try {
|
|
2684
|
-
const pid = parseInt(
|
|
2860
|
+
const pid = parseInt(readFileSync7(lp, "utf-8").trim(), 10);
|
|
2685
2861
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2686
2862
|
unlinkSync2(lp);
|
|
2687
2863
|
} else {
|
|
@@ -2714,37 +2890,57 @@ function releaseDaemonLock() {
|
|
|
2714
2890
|
} catch {
|
|
2715
2891
|
}
|
|
2716
2892
|
}
|
|
2717
|
-
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 = {}) {
|
|
2718
2926
|
if (restartInFlight)
|
|
2719
2927
|
return;
|
|
2720
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
|
+
}
|
|
2721
2939
|
if (!tryAcquireDaemonLock()) {
|
|
2722
2940
|
return;
|
|
2723
2941
|
}
|
|
2724
2942
|
try {
|
|
2725
|
-
|
|
2726
|
-
const entry = daemonEntryPath();
|
|
2727
|
-
const desired = renderPlist(nodeBin, entry, buildDaemonPath(process.env.PATH ?? ""));
|
|
2728
|
-
const current = existsSync7(p) ? readFileSync6(p, "utf-8") : null;
|
|
2729
|
-
const uid = process.getuid?.() ?? 0;
|
|
2730
|
-
const target = `gui/${uid}/${LABEL}`;
|
|
2731
|
-
const changed = current !== desired;
|
|
2732
|
-
const programChanged = current !== null && changed && !current.includes(programArgsBlock(nodeBin, entry));
|
|
2733
|
-
if (changed) {
|
|
2734
|
-
mkdirSync4(dirname3(p), { recursive: true });
|
|
2735
|
-
writeFileSync5(p, desired);
|
|
2736
|
-
}
|
|
2737
|
-
if (programChanged) {
|
|
2738
|
-
try {
|
|
2739
|
-
execFileSync3("launchctl", ["bootout", target], { stdio: "ignore" });
|
|
2740
|
-
} catch {
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
try {
|
|
2744
|
-
execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, p], { stdio: "ignore" });
|
|
2745
|
-
} catch {
|
|
2746
|
-
}
|
|
2747
|
-
execFileSync3("launchctl", ["kickstart", target], { stdio: "ignore" });
|
|
2943
|
+
applyDaemonDrift(computeDaemonDrift(nodeBin));
|
|
2748
2944
|
} catch (e) {
|
|
2749
2945
|
process.stderr.write(`[squadrant] warn: ensureDaemon failed (${e instanceof Error ? e.message : e})
|
|
2750
2946
|
`);
|
|
@@ -2752,12 +2948,22 @@ function ensureDaemon(nodeBin = process.execPath) {
|
|
|
2752
2948
|
releaseDaemonLock();
|
|
2753
2949
|
}
|
|
2754
2950
|
}
|
|
2755
|
-
|
|
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;
|
|
2756
2961
|
var init_launchd = __esm({
|
|
2757
2962
|
"packages/core/dist/launchd.js"() {
|
|
2758
2963
|
LABEL = "com.squadrant.daemon";
|
|
2759
2964
|
AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
|
|
2760
2965
|
restartInFlight = false;
|
|
2966
|
+
OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
|
|
2761
2967
|
}
|
|
2762
2968
|
});
|
|
2763
2969
|
|
|
@@ -2905,7 +3111,7 @@ var init_gate = __esm({
|
|
|
2905
3111
|
});
|
|
2906
3112
|
|
|
2907
3113
|
// packages/core/dist/daemon/liveness-registry.js
|
|
2908
|
-
import { writeFileSync as
|
|
3114
|
+
import { writeFileSync as writeFileSync7, readFileSync as readFileSync8, renameSync as renameSync3 } from "fs";
|
|
2909
3115
|
var LivenessRegistry;
|
|
2910
3116
|
var init_liveness_registry = __esm({
|
|
2911
3117
|
"packages/core/dist/daemon/liveness-registry.js"() {
|
|
@@ -2919,14 +3125,14 @@ var init_liveness_registry = __esm({
|
|
|
2919
3125
|
this.path = opts.path;
|
|
2920
3126
|
this.readFile = opts.readFile ?? ((p) => {
|
|
2921
3127
|
try {
|
|
2922
|
-
return
|
|
3128
|
+
return readFileSync8(p, "utf-8");
|
|
2923
3129
|
} catch {
|
|
2924
3130
|
return void 0;
|
|
2925
3131
|
}
|
|
2926
3132
|
});
|
|
2927
3133
|
this.writeFile = opts.writeFile ?? ((p, c) => {
|
|
2928
|
-
|
|
2929
|
-
|
|
3134
|
+
writeFileSync7(`${p}.tmp`, c);
|
|
3135
|
+
renameSync3(`${p}.tmp`, p);
|
|
2930
3136
|
});
|
|
2931
3137
|
}
|
|
2932
3138
|
load() {
|
|
@@ -2975,10 +3181,10 @@ var init_liveness_registry = __esm({
|
|
|
2975
3181
|
});
|
|
2976
3182
|
|
|
2977
3183
|
// packages/core/dist/daemon/context.js
|
|
2978
|
-
import { homedir as
|
|
2979
|
-
import { join as
|
|
3184
|
+
import { homedir as homedir6 } from "os";
|
|
3185
|
+
import { join as join9 } from "path";
|
|
2980
3186
|
import { spawn as realSpawn } from "child_process";
|
|
2981
|
-
import { writeFileSync as
|
|
3187
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
2982
3188
|
function defaultIsPidAlive(pid) {
|
|
2983
3189
|
try {
|
|
2984
3190
|
process.kill(pid, 0);
|
|
@@ -2988,18 +3194,18 @@ function defaultIsPidAlive(pid) {
|
|
|
2988
3194
|
}
|
|
2989
3195
|
}
|
|
2990
3196
|
function buildContext(opts) {
|
|
2991
|
-
const stateRoot = opts.stateRoot ??
|
|
2992
|
-
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");
|
|
2993
3199
|
const store = createStore(stateRoot);
|
|
2994
3200
|
const bootedAt = Date.now();
|
|
2995
3201
|
const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
|
|
2996
3202
|
const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
|
|
2997
3203
|
const spawn2 = opts.spawn ?? realSpawn;
|
|
2998
|
-
const resultsDir =
|
|
2999
|
-
|
|
3204
|
+
const resultsDir = join9(stateRoot, "_results");
|
|
3205
|
+
mkdirSync6(resultsDir, { recursive: true });
|
|
3000
3206
|
const writeResult = (id, payload) => {
|
|
3001
|
-
const p =
|
|
3002
|
-
|
|
3207
|
+
const p = join9(resultsDir, `${id}.txt`);
|
|
3208
|
+
writeFileSync8(p, payload);
|
|
3003
3209
|
return p;
|
|
3004
3210
|
};
|
|
3005
3211
|
const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
|
|
@@ -3021,7 +3227,7 @@ function buildContext(opts) {
|
|
|
3021
3227
|
inFlightHeadlessIds: /* @__PURE__ */ new Set(),
|
|
3022
3228
|
activeHeadlessKills: /* @__PURE__ */ new Set(),
|
|
3023
3229
|
livenessRegistry: (() => {
|
|
3024
|
-
const r = new LivenessRegistry({ path:
|
|
3230
|
+
const r = new LivenessRegistry({ path: join9(stateRoot, "liveness.json") });
|
|
3025
3231
|
r.load();
|
|
3026
3232
|
return r;
|
|
3027
3233
|
})(),
|
|
@@ -3743,19 +3949,19 @@ var init_server = __esm({
|
|
|
3743
3949
|
|
|
3744
3950
|
// packages/core/dist/daemon/snapshot-gather.js
|
|
3745
3951
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3746
|
-
import { join as
|
|
3747
|
-
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";
|
|
3748
3954
|
function distBuiltAt() {
|
|
3749
3955
|
try {
|
|
3750
|
-
return
|
|
3956
|
+
return statSync3(SELF_PATH).mtimeMs;
|
|
3751
3957
|
} catch {
|
|
3752
3958
|
return 0;
|
|
3753
3959
|
}
|
|
3754
3960
|
}
|
|
3755
|
-
function gatherLogStats(
|
|
3961
|
+
function gatherLogStats(path34, now, windowMs) {
|
|
3756
3962
|
let sizeBytes = 0;
|
|
3757
3963
|
try {
|
|
3758
|
-
sizeBytes =
|
|
3964
|
+
sizeBytes = statSync3(path34).size;
|
|
3759
3965
|
} catch {
|
|
3760
3966
|
return { errorCount: 0, sizeBytes: 0, windowMs };
|
|
3761
3967
|
}
|
|
@@ -3766,7 +3972,7 @@ function gatherLogStats(path30, now, windowMs) {
|
|
|
3766
3972
|
const len = sizeBytes - start;
|
|
3767
3973
|
let text = "";
|
|
3768
3974
|
try {
|
|
3769
|
-
const fd = openSync2(
|
|
3975
|
+
const fd = openSync2(path34, "r");
|
|
3770
3976
|
try {
|
|
3771
3977
|
const buf = Buffer.alloc(len);
|
|
3772
3978
|
readSync(fd, buf, 0, len, start);
|
|
@@ -3797,9 +4003,9 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
3797
4003
|
for (const r of store.list(project))
|
|
3798
4004
|
byState[r.state] = (byState[r.state] ?? 0) + 1;
|
|
3799
4005
|
let corruptCount = 0;
|
|
3800
|
-
const dir =
|
|
4006
|
+
const dir = join10(stateRoot, project);
|
|
3801
4007
|
try {
|
|
3802
|
-
for (const n of
|
|
4008
|
+
for (const n of readdirSync3(dir)) {
|
|
3803
4009
|
if (n.includes(".corrupt.")) {
|
|
3804
4010
|
corruptCount++;
|
|
3805
4011
|
continue;
|
|
@@ -3807,7 +4013,7 @@ function gatherStoreStats(store, stateRoot, project) {
|
|
|
3807
4013
|
if (!n.endsWith(".json"))
|
|
3808
4014
|
continue;
|
|
3809
4015
|
try {
|
|
3810
|
-
JSON.parse(
|
|
4016
|
+
JSON.parse(readFileSync9(join10(dir, n), "utf-8"));
|
|
3811
4017
|
} catch {
|
|
3812
4018
|
corruptCount++;
|
|
3813
4019
|
}
|
|
@@ -3820,9 +4026,9 @@ function gatherResults(resultsDir) {
|
|
|
3820
4026
|
let fileCount = 0;
|
|
3821
4027
|
let totalBytes = 0;
|
|
3822
4028
|
try {
|
|
3823
|
-
for (const n of
|
|
4029
|
+
for (const n of readdirSync3(resultsDir)) {
|
|
3824
4030
|
try {
|
|
3825
|
-
const s =
|
|
4031
|
+
const s = statSync3(join10(resultsDir, n));
|
|
3826
4032
|
if (s.isFile()) {
|
|
3827
4033
|
fileCount++;
|
|
3828
4034
|
totalBytes += s.size;
|
|
@@ -3842,7 +4048,7 @@ var init_snapshot_gather = __esm({
|
|
|
3842
4048
|
});
|
|
3843
4049
|
|
|
3844
4050
|
// packages/core/dist/daemon/start.js
|
|
3845
|
-
import { join as
|
|
4051
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
3846
4052
|
import { readdir } from "fs/promises";
|
|
3847
4053
|
function startDaemon(ctx, opts, pkgVersion) {
|
|
3848
4054
|
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
@@ -3917,7 +4123,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
3917
4123
|
return out;
|
|
3918
4124
|
}
|
|
3919
4125
|
async function gatherSnapshotInputs(now) {
|
|
3920
|
-
const logPath2 =
|
|
4126
|
+
const logPath2 = join11(dirname4(stateRoot), "squadrantd.log");
|
|
3921
4127
|
const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
|
|
3922
4128
|
const projects = await Promise.all(tier2Projects.map(async (project) => {
|
|
3923
4129
|
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
|
|
@@ -4051,7 +4257,7 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
4051
4257
|
};
|
|
4052
4258
|
let rotationTimer;
|
|
4053
4259
|
if (rotationInterval > 0) {
|
|
4054
|
-
const inboxPath =
|
|
4260
|
+
const inboxPath = join11(stateRoot, "inbox");
|
|
4055
4261
|
rotationTimer = setInterval(async () => {
|
|
4056
4262
|
try {
|
|
4057
4263
|
let entries;
|
|
@@ -4094,9 +4300,9 @@ function startDaemon(ctx, opts, pkgVersion) {
|
|
|
4094
4300
|
}
|
|
4095
4301
|
for (const kill of ctx.activeHeadlessKills)
|
|
4096
4302
|
kill();
|
|
4097
|
-
return new Promise((
|
|
4303
|
+
return new Promise((resolve4) => server.close(() => {
|
|
4098
4304
|
log(`exit-complete pid=${process.pid}`);
|
|
4099
|
-
|
|
4305
|
+
resolve4();
|
|
4100
4306
|
}));
|
|
4101
4307
|
},
|
|
4102
4308
|
tickDelivery: deliveryTick,
|
|
@@ -4247,8 +4453,8 @@ import { exec as nodeExec } from "child_process";
|
|
|
4247
4453
|
async function reapCrewChildren(taskId, graceMs = 2e3, execFn = nodeExec) {
|
|
4248
4454
|
const marker = `SQUADRANT_CREW_TASK_ID=${taskId}`;
|
|
4249
4455
|
try {
|
|
4250
|
-
const stdout = await new Promise((
|
|
4251
|
-
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));
|
|
4252
4458
|
});
|
|
4253
4459
|
const pids = [];
|
|
4254
4460
|
for (const line of stdout.split("\n").slice(1)) {
|
|
@@ -4464,7 +4670,7 @@ function createIsCaptainAlive(sock) {
|
|
|
4464
4670
|
};
|
|
4465
4671
|
}
|
|
4466
4672
|
function createLaunch(cliBin, log) {
|
|
4467
|
-
return (project) => new Promise((
|
|
4673
|
+
return (project) => new Promise((resolve4, reject) => {
|
|
4468
4674
|
execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
|
|
4469
4675
|
const output = capOutput(stdout ?? "", stderr ?? "");
|
|
4470
4676
|
if (err) {
|
|
@@ -4474,7 +4680,7 @@ function createLaunch(cliBin, log) {
|
|
|
4474
4680
|
}
|
|
4475
4681
|
if (output !== "(no output)")
|
|
4476
4682
|
log?.(`launch ${project}: ${output}`);
|
|
4477
|
-
|
|
4683
|
+
resolve4();
|
|
4478
4684
|
});
|
|
4479
4685
|
});
|
|
4480
4686
|
}
|
|
@@ -4622,10 +4828,10 @@ function findProjectByThread(stateRoot, threadId) {
|
|
|
4622
4828
|
for (const [key, id] of Object.entries(s.topics)) {
|
|
4623
4829
|
if (id !== threadId)
|
|
4624
4830
|
continue;
|
|
4625
|
-
const
|
|
4626
|
-
if (
|
|
4831
|
+
const sep3 = key.indexOf("::");
|
|
4832
|
+
if (sep3 === -1)
|
|
4627
4833
|
continue;
|
|
4628
|
-
return { project: key.slice(0,
|
|
4834
|
+
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2) };
|
|
4629
4835
|
}
|
|
4630
4836
|
return null;
|
|
4631
4837
|
}
|
|
@@ -5176,11 +5382,11 @@ var init_bridge = __esm({
|
|
|
5176
5382
|
|
|
5177
5383
|
// packages/core/dist/restart-daemon.js
|
|
5178
5384
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
5179
|
-
import { existsSync as
|
|
5180
|
-
import { homedir as
|
|
5181
|
-
import { join as
|
|
5385
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5386
|
+
import { homedir as homedir7 } from "os";
|
|
5387
|
+
import { join as join12 } from "path";
|
|
5182
5388
|
function defaultIsRunning() {
|
|
5183
|
-
return
|
|
5389
|
+
return existsSync9(DEFAULT_SOCK_PATH);
|
|
5184
5390
|
}
|
|
5185
5391
|
function defaultRunKickstart() {
|
|
5186
5392
|
const uid = process.getuid?.() ?? 0;
|
|
@@ -5210,7 +5416,7 @@ var DEFAULT_SOCK_PATH;
|
|
|
5210
5416
|
var init_restart_daemon = __esm({
|
|
5211
5417
|
"packages/core/dist/restart-daemon.js"() {
|
|
5212
5418
|
init_launchd();
|
|
5213
|
-
DEFAULT_SOCK_PATH =
|
|
5419
|
+
DEFAULT_SOCK_PATH = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
|
|
5214
5420
|
}
|
|
5215
5421
|
});
|
|
5216
5422
|
|
|
@@ -5310,8 +5516,8 @@ function runTelegramStatus(opts) {
|
|
|
5310
5516
|
const env = opts.env ?? process.env;
|
|
5311
5517
|
const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
|
|
5312
5518
|
const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
|
|
5313
|
-
const
|
|
5314
|
-
return { project: key.slice(0,
|
|
5519
|
+
const sep3 = key.indexOf("::");
|
|
5520
|
+
return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
|
|
5315
5521
|
});
|
|
5316
5522
|
return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
|
|
5317
5523
|
}
|
|
@@ -5335,8 +5541,8 @@ function runTelegramNotifyStatus(opts) {
|
|
|
5335
5541
|
const s = loadState(opts.stateRoot);
|
|
5336
5542
|
const projects = /* @__PURE__ */ new Set();
|
|
5337
5543
|
for (const key of Object.keys(s.topics)) {
|
|
5338
|
-
const
|
|
5339
|
-
projects.add(
|
|
5544
|
+
const sep3 = key.indexOf("::");
|
|
5545
|
+
projects.add(sep3 === -1 ? key : key.slice(0, sep3));
|
|
5340
5546
|
}
|
|
5341
5547
|
for (const p of Object.keys(s.notify))
|
|
5342
5548
|
projects.add(p);
|
|
@@ -5434,8 +5640,8 @@ var init_crew_routing = __esm({
|
|
|
5434
5640
|
|
|
5435
5641
|
// packages/core/dist/group-dispatch.js
|
|
5436
5642
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5437
|
-
import { homedir as
|
|
5438
|
-
import { join as
|
|
5643
|
+
import { homedir as homedir8 } from "os";
|
|
5644
|
+
import { join as join13 } from "path";
|
|
5439
5645
|
function resolveCurrentProject(config) {
|
|
5440
5646
|
const cwd = process.cwd();
|
|
5441
5647
|
for (const [name, proj] of Object.entries(config.projects)) {
|
|
@@ -5512,7 +5718,7 @@ var init_group_dispatch = __esm({
|
|
|
5512
5718
|
"packages/core/dist/group-dispatch.js"() {
|
|
5513
5719
|
init_dist();
|
|
5514
5720
|
init_protocol();
|
|
5515
|
-
DEFAULT_SOCK_PATH2 =
|
|
5721
|
+
DEFAULT_SOCK_PATH2 = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
|
|
5516
5722
|
GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
|
|
5517
5723
|
GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
|
|
5518
5724
|
}
|
|
@@ -5593,7 +5799,8 @@ async function launchOneWorkspace(opts) {
|
|
|
5593
5799
|
forceFresh = true;
|
|
5594
5800
|
}
|
|
5595
5801
|
}
|
|
5596
|
-
const
|
|
5802
|
+
const builtCmd = opts.agentCmdFactory(forceFresh);
|
|
5803
|
+
const agentCmd = opts.role === "captain" ? `SQUADRANT_ROLE=captain ${builtCmd}` : builtCmd;
|
|
5597
5804
|
recordSession(opts.workspaceName, opts.role, {
|
|
5598
5805
|
sessionsPath: opts.sessionsPath,
|
|
5599
5806
|
templatesDir: opts.templatesDir
|
|
@@ -6116,12 +6323,14 @@ __export(dist_exports2, {
|
|
|
6116
6323
|
IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
|
|
6117
6324
|
LABEL: () => LABEL,
|
|
6118
6325
|
MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
|
|
6326
|
+
OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
|
|
6119
6327
|
PROBE_QUIET_MS: () => PROBE_QUIET_MS,
|
|
6120
6328
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
6121
6329
|
STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
|
|
6122
6330
|
TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
|
|
6123
6331
|
TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
|
|
6124
6332
|
TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
|
|
6333
|
+
WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
|
|
6125
6334
|
WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
|
|
6126
6335
|
_resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
|
|
6127
6336
|
ageText: () => ageText,
|
|
@@ -6137,6 +6346,7 @@ __export(dist_exports2, {
|
|
|
6137
6346
|
capAllowed: () => capAllowed,
|
|
6138
6347
|
capOutput: () => capOutput,
|
|
6139
6348
|
classifyHealth: () => classifyHealth,
|
|
6349
|
+
closeWorkItem: () => closeWorkItem,
|
|
6140
6350
|
computeTemplateHash: () => computeTemplateHash,
|
|
6141
6351
|
createAttach: () => createAttach,
|
|
6142
6352
|
createCrewPaneReader: () => createCrewPaneReader,
|
|
@@ -6154,6 +6364,8 @@ __export(dist_exports2, {
|
|
|
6154
6364
|
createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
|
|
6155
6365
|
createTelegramBridge: () => createTelegramBridge,
|
|
6156
6366
|
createTelegramClient: () => createTelegramClient,
|
|
6367
|
+
createWorkItem: () => createWorkItem,
|
|
6368
|
+
createWorkStore: () => createWorkStore,
|
|
6157
6369
|
crewPaneTitle: () => crewPaneTitle,
|
|
6158
6370
|
crewTag: () => crewTag,
|
|
6159
6371
|
daemonEntryPath: () => daemonEntryPath,
|
|
@@ -6161,6 +6373,7 @@ __export(dist_exports2, {
|
|
|
6161
6373
|
decodeFrames: () => decodeFrames,
|
|
6162
6374
|
defaultIsPidAlive: () => defaultIsPidAlive,
|
|
6163
6375
|
defaultListenError: () => defaultListenError,
|
|
6376
|
+
defaultWorkRoot: () => defaultWorkRoot,
|
|
6164
6377
|
deliverStartupPrompt: () => deliverStartupPrompt,
|
|
6165
6378
|
deliverable: () => deliverable,
|
|
6166
6379
|
deriveCaptainState: () => deriveCaptainState,
|
|
@@ -6172,7 +6385,9 @@ __export(dist_exports2, {
|
|
|
6172
6385
|
encodeMsg: () => encodeMsg,
|
|
6173
6386
|
ensureDaemon: () => ensureDaemon,
|
|
6174
6387
|
evaluateStall: () => evaluateStall,
|
|
6388
|
+
findOpenChildren: () => findOpenChildren,
|
|
6175
6389
|
findProjectByThread: () => findProjectByThread,
|
|
6390
|
+
findWorkItemById: () => findWorkItemById,
|
|
6176
6391
|
formatInbound: () => formatInbound,
|
|
6177
6392
|
formatLifecycle: () => formatLifecycle,
|
|
6178
6393
|
healCmdFor: () => healCmdFor,
|
|
@@ -6184,6 +6399,7 @@ __export(dist_exports2, {
|
|
|
6184
6399
|
isCrewTitle: () => isCrewTitle,
|
|
6185
6400
|
isDaemonSocketLive: () => isDaemonSocketLive,
|
|
6186
6401
|
isNotifyActive: () => isNotifyActive,
|
|
6402
|
+
isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
|
|
6187
6403
|
isSideTitle: () => isSideTitle,
|
|
6188
6404
|
isStickyAttention: () => isStickyAttention,
|
|
6189
6405
|
isTurnAccepted: () => isTurnAccepted,
|
|
@@ -6203,6 +6419,7 @@ __export(dist_exports2, {
|
|
|
6203
6419
|
plistPath: () => plistPath,
|
|
6204
6420
|
programArgsBlock: () => programArgsBlock,
|
|
6205
6421
|
projectHealth: () => projectHealth,
|
|
6422
|
+
purgeExpiredWorkItems: () => purgeExpiredWorkItems,
|
|
6206
6423
|
readCursor: () => readCursor,
|
|
6207
6424
|
readFromCursor: () => readFromCursor,
|
|
6208
6425
|
reapCrewChildren: () => reapCrewChildren,
|
|
@@ -6214,6 +6431,7 @@ __export(dist_exports2, {
|
|
|
6214
6431
|
reduceLifecycle: () => reduceLifecycle,
|
|
6215
6432
|
releaseDaemonLock: () => releaseDaemonLock,
|
|
6216
6433
|
renderPlist: () => renderPlist,
|
|
6434
|
+
reregisterDaemon: () => reregisterDaemon,
|
|
6217
6435
|
resolveAgentBinDirs: () => resolveAgentBinDirs,
|
|
6218
6436
|
resolveCrewRoute: () => resolveCrewRoute,
|
|
6219
6437
|
resolveCurrentProject: () => resolveCurrentProject,
|
|
@@ -6278,6 +6496,7 @@ var init_dist2 = __esm({
|
|
|
6278
6496
|
init_liveness2();
|
|
6279
6497
|
init_watchdog();
|
|
6280
6498
|
init_store();
|
|
6499
|
+
init_work_store();
|
|
6281
6500
|
init_snapshot();
|
|
6282
6501
|
init_launchd();
|
|
6283
6502
|
init_crew_pane_reader();
|
|
@@ -6317,7 +6536,7 @@ function cmuxLocal(args) {
|
|
|
6317
6536
|
}).trim();
|
|
6318
6537
|
}
|
|
6319
6538
|
function cmux(args) {
|
|
6320
|
-
return new Promise((
|
|
6539
|
+
return new Promise((resolve4, reject) => {
|
|
6321
6540
|
execFile2(
|
|
6322
6541
|
resolveCmuxBin(),
|
|
6323
6542
|
args,
|
|
@@ -6331,19 +6550,19 @@ function cmux(args) {
|
|
|
6331
6550
|
reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
|
|
6332
6551
|
return;
|
|
6333
6552
|
}
|
|
6334
|
-
|
|
6553
|
+
resolve4(stdout.trim());
|
|
6335
6554
|
}
|
|
6336
6555
|
);
|
|
6337
6556
|
});
|
|
6338
6557
|
}
|
|
6339
6558
|
function cmuxStdin(args, input) {
|
|
6340
|
-
return new Promise((
|
|
6559
|
+
return new Promise((resolve4, reject) => {
|
|
6341
6560
|
const child = execFile2(resolveCmuxBin(), args, { encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } }, (err, stdout) => {
|
|
6342
6561
|
if (err) {
|
|
6343
6562
|
reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
|
|
6344
6563
|
return;
|
|
6345
6564
|
}
|
|
6346
|
-
|
|
6565
|
+
resolve4(stdout.trim());
|
|
6347
6566
|
});
|
|
6348
6567
|
child.stdin.end(input);
|
|
6349
6568
|
});
|
|
@@ -6950,7 +7169,7 @@ var init_notifiers = __esm({
|
|
|
6950
7169
|
|
|
6951
7170
|
// packages/workspaces/dist/workspaces/obsidian.js
|
|
6952
7171
|
import fs15 from "fs/promises";
|
|
6953
|
-
import { existsSync as
|
|
7172
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6954
7173
|
import path12 from "path";
|
|
6955
7174
|
function resolveInRoot(root, relative) {
|
|
6956
7175
|
const joined = path12.resolve(root, relative);
|
|
@@ -6970,7 +7189,7 @@ function createObsidianDriver(scope) {
|
|
|
6970
7189
|
async probe() {
|
|
6971
7190
|
return {
|
|
6972
7191
|
installed: true,
|
|
6973
|
-
rootExists:
|
|
7192
|
+
rootExists: existsSync10(root)
|
|
6974
7193
|
};
|
|
6975
7194
|
},
|
|
6976
7195
|
async read(rel) {
|
|
@@ -7125,12 +7344,12 @@ var init_events_bridge = __esm({
|
|
|
7125
7344
|
continue;
|
|
7126
7345
|
}
|
|
7127
7346
|
this.child = child;
|
|
7128
|
-
await new Promise((
|
|
7347
|
+
await new Promise((resolve4) => {
|
|
7129
7348
|
let settled = false;
|
|
7130
7349
|
const done = () => {
|
|
7131
7350
|
if (!settled) {
|
|
7132
7351
|
settled = true;
|
|
7133
|
-
|
|
7352
|
+
resolve4();
|
|
7134
7353
|
}
|
|
7135
7354
|
};
|
|
7136
7355
|
child.stdout?.on("data", (b) => this.onData(b));
|
|
@@ -7261,9 +7480,9 @@ var init_store_fingerprint = __esm({
|
|
|
7261
7480
|
});
|
|
7262
7481
|
|
|
7263
7482
|
// packages/workspaces/dist/cmux-daemon/daemon-cmux.js
|
|
7264
|
-
import { readdirSync as
|
|
7265
|
-
import { join as
|
|
7266
|
-
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";
|
|
7267
7486
|
var DaemonCmux;
|
|
7268
7487
|
var init_daemon_cmux = __esm({
|
|
7269
7488
|
"packages/workspaces/dist/cmux-daemon/daemon-cmux.js"() {
|
|
@@ -7326,24 +7545,24 @@ var init_daemon_cmux = __esm({
|
|
|
7326
7545
|
* file failed to read/parse — see the class doc above.
|
|
7327
7546
|
*/
|
|
7328
7547
|
async liveness() {
|
|
7329
|
-
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ??
|
|
7548
|
+
const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir9(), ".cmuxterm");
|
|
7330
7549
|
const projects = loadConfig().projects;
|
|
7331
7550
|
let files;
|
|
7332
7551
|
try {
|
|
7333
|
-
files =
|
|
7552
|
+
files = readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
|
|
7334
7553
|
} catch (e) {
|
|
7335
7554
|
throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
|
|
7336
7555
|
}
|
|
7337
|
-
return readLivenessSnapshot(files, (f) =>
|
|
7556
|
+
return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
|
|
7338
7557
|
}
|
|
7339
7558
|
};
|
|
7340
7559
|
}
|
|
7341
7560
|
});
|
|
7342
7561
|
|
|
7343
7562
|
// packages/workspaces/dist/cmux-daemon/cmux-store-source.js
|
|
7344
|
-
import { join as
|
|
7345
|
-
import { homedir as
|
|
7346
|
-
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";
|
|
7347
7566
|
function parseLifecycleState(s) {
|
|
7348
7567
|
if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
|
|
7349
7568
|
return s;
|
|
@@ -7360,14 +7579,14 @@ function defaultIsPidAlive2(pid) {
|
|
|
7360
7579
|
}
|
|
7361
7580
|
function defaultListFiles(dir) {
|
|
7362
7581
|
try {
|
|
7363
|
-
return
|
|
7582
|
+
return readdirSync5(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
|
|
7364
7583
|
} catch {
|
|
7365
7584
|
return [];
|
|
7366
7585
|
}
|
|
7367
7586
|
}
|
|
7368
|
-
function defaultReadFile(
|
|
7587
|
+
function defaultReadFile(path34) {
|
|
7369
7588
|
try {
|
|
7370
|
-
return
|
|
7589
|
+
return readFileSync11(path34, "utf-8");
|
|
7371
7590
|
} catch {
|
|
7372
7591
|
return void 0;
|
|
7373
7592
|
}
|
|
@@ -7403,12 +7622,12 @@ var init_cmux_store_source = __esm({
|
|
|
7403
7622
|
active = false;
|
|
7404
7623
|
lastError = null;
|
|
7405
7624
|
constructor(opts = {}) {
|
|
7406
|
-
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");
|
|
7407
7626
|
this.debounceMs = opts.debounceMs ?? 50;
|
|
7408
7627
|
this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
|
|
7409
7628
|
this.listFiles = opts.listFiles ?? defaultListFiles;
|
|
7410
7629
|
this.readFile = opts.readFile ?? defaultReadFile;
|
|
7411
|
-
this.fileExists = opts.fileExists ??
|
|
7630
|
+
this.fileExists = opts.fileExists ?? existsSync11;
|
|
7412
7631
|
this.watchDir = opts.watchDir ?? defaultWatchDir;
|
|
7413
7632
|
this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
|
|
7414
7633
|
this.cancelTimer = opts.cancelTimer ?? clearTimeout;
|
|
@@ -7465,7 +7684,7 @@ var init_cmux_store_source = __esm({
|
|
|
7465
7684
|
}
|
|
7466
7685
|
scanFile(filename) {
|
|
7467
7686
|
const deps = this.deps;
|
|
7468
|
-
const filePath =
|
|
7687
|
+
const filePath = join15(this.stateDir, filename);
|
|
7469
7688
|
const lockPath = `${filePath}.lock`;
|
|
7470
7689
|
if (this.fileExists(lockPath)) {
|
|
7471
7690
|
this.log(`cmux-store: skipping ${filename} (locked)`);
|
|
@@ -7519,11 +7738,11 @@ var init_cmux_store_source = __esm({
|
|
|
7519
7738
|
});
|
|
7520
7739
|
|
|
7521
7740
|
// packages/workspaces/dist/native-hooks/native-hook-source.js
|
|
7522
|
-
import { join as
|
|
7523
|
-
import { homedir as
|
|
7524
|
-
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";
|
|
7525
7744
|
function installClaudeHooks(opts = {}) {
|
|
7526
|
-
const settingsPath = opts.settingsPath ??
|
|
7745
|
+
const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
|
|
7527
7746
|
const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
|
|
7528
7747
|
const readFile6 = opts.readFile ?? defaultReadFile2;
|
|
7529
7748
|
const writeFile5 = opts.writeFile ?? defaultWriteFile;
|
|
@@ -7617,16 +7836,16 @@ function extractDetail(sub, payload) {
|
|
|
7617
7836
|
}
|
|
7618
7837
|
return void 0;
|
|
7619
7838
|
}
|
|
7620
|
-
function defaultReadFile2(
|
|
7839
|
+
function defaultReadFile2(path34) {
|
|
7621
7840
|
try {
|
|
7622
|
-
return
|
|
7841
|
+
return readFileSync12(path34, "utf-8");
|
|
7623
7842
|
} catch {
|
|
7624
7843
|
return void 0;
|
|
7625
7844
|
}
|
|
7626
7845
|
}
|
|
7627
|
-
function defaultWriteFile(
|
|
7628
|
-
|
|
7629
|
-
|
|
7846
|
+
function defaultWriteFile(path34, content) {
|
|
7847
|
+
mkdirSync7(path34.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
7848
|
+
writeFileSync9(path34, content, "utf-8");
|
|
7630
7849
|
}
|
|
7631
7850
|
var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
|
|
7632
7851
|
var init_native_hook_source = __esm({
|
|
@@ -7736,13 +7955,13 @@ async function settleInputBox(runtime, pane) {
|
|
|
7736
7955
|
return sawContent;
|
|
7737
7956
|
}
|
|
7738
7957
|
function getFreePort() {
|
|
7739
|
-
return new Promise((
|
|
7958
|
+
return new Promise((resolve4, reject) => {
|
|
7740
7959
|
const srv = net.createServer();
|
|
7741
7960
|
srv.once("error", reject);
|
|
7742
7961
|
srv.listen(0, "127.0.0.1", () => {
|
|
7743
7962
|
const addr = srv.address();
|
|
7744
7963
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
7745
|
-
srv.close(() => port ?
|
|
7964
|
+
srv.close(() => port ? resolve4(port) : reject(new Error("no free port assigned")));
|
|
7746
7965
|
});
|
|
7747
7966
|
});
|
|
7748
7967
|
}
|
|
@@ -7985,7 +8204,8 @@ function createClaudeDriver() {
|
|
|
7985
8204
|
if (opts.settingsPath) {
|
|
7986
8205
|
cmd += ` --settings ${opts.settingsPath}`;
|
|
7987
8206
|
}
|
|
7988
|
-
const
|
|
8207
|
+
const pluginSubdir = opts.role === "crew" ? "plugin-crew" : "plugin";
|
|
8208
|
+
const pluginDir = `${process.env.HOME}/.config/squadrant/${pluginSubdir}`;
|
|
7989
8209
|
cmd += ` --plugin-dir ${pluginDir}`;
|
|
7990
8210
|
if (!opts.interactive) {
|
|
7991
8211
|
cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
|
|
@@ -8407,8 +8627,8 @@ ${MARKER_END}
|
|
|
8407
8627
|
const startIdx = existing.indexOf(MARKER_START);
|
|
8408
8628
|
const endIdx = existing.indexOf(MARKER_END);
|
|
8409
8629
|
if (startIdx === -1 && endIdx === -1) {
|
|
8410
|
-
const
|
|
8411
|
-
return `${existing}${
|
|
8630
|
+
const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
8631
|
+
return `${existing}${sep3}${block}`;
|
|
8412
8632
|
}
|
|
8413
8633
|
if (startIdx === -1 || endIdx === -1) {
|
|
8414
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.`);
|
|
@@ -8739,8 +8959,8 @@ var init_app_server_client = __esm({
|
|
|
8739
8959
|
const info = this.opts.clientInfo ?? { name: "squadrant", version: "0" };
|
|
8740
8960
|
const id = this.nextId++;
|
|
8741
8961
|
const env = { jsonrpc: "2.0", id, method: "initialize", params: { clientInfo: info } };
|
|
8742
|
-
const res = await new Promise((
|
|
8743
|
-
this.pending.set(id, { resolve:
|
|
8962
|
+
const res = await new Promise((resolve4, reject) => {
|
|
8963
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
8744
8964
|
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
8745
8965
|
});
|
|
8746
8966
|
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
|
|
@@ -8773,13 +8993,13 @@ var init_app_server_client = __esm({
|
|
|
8773
8993
|
const turnId = ack?.turn?.id;
|
|
8774
8994
|
if (typeof turnId !== "string")
|
|
8775
8995
|
throw new Error(`turn/start: unexpected ack shape (no turn.id): ${JSON.stringify(ack).slice(0, 200)}`);
|
|
8776
|
-
return new Promise((
|
|
8996
|
+
return new Promise((resolve4, reject) => {
|
|
8777
8997
|
const onNote = (n) => {
|
|
8778
8998
|
if (n.params?.turn?.id !== turnId)
|
|
8779
8999
|
return;
|
|
8780
9000
|
if (n.method === "turn/completed") {
|
|
8781
9001
|
cleanup();
|
|
8782
|
-
|
|
9002
|
+
resolve4({ turnId });
|
|
8783
9003
|
}
|
|
8784
9004
|
if (n.method === "turn/failed") {
|
|
8785
9005
|
cleanup();
|
|
@@ -8830,8 +9050,8 @@ var init_app_server_client = __esm({
|
|
|
8830
9050
|
throw new Error("AppServerClient not started");
|
|
8831
9051
|
const id = this.nextId++;
|
|
8832
9052
|
const env = { jsonrpc: "2.0", id, method, params: params ?? {} };
|
|
8833
|
-
return new Promise((
|
|
8834
|
-
this.pending.set(id, { resolve:
|
|
9053
|
+
return new Promise((resolve4, reject) => {
|
|
9054
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
8835
9055
|
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
8836
9056
|
});
|
|
8837
9057
|
}
|
|
@@ -8963,11 +9183,11 @@ var init_codex_app_server_source = __esm({
|
|
|
8963
9183
|
|
|
8964
9184
|
// packages/agents/dist/codex/config.js
|
|
8965
9185
|
import { readFile as readFile5 } from "fs/promises";
|
|
8966
|
-
import { homedir as
|
|
8967
|
-
import { join as
|
|
9186
|
+
import { homedir as homedir13 } from "os";
|
|
9187
|
+
import { join as join18 } from "path";
|
|
8968
9188
|
async function resolveCodexModel() {
|
|
8969
|
-
const home = process.env["CODEX_HOME"] ??
|
|
8970
|
-
const configPath =
|
|
9189
|
+
const home = process.env["CODEX_HOME"] ?? join18(homedir13(), ".codex");
|
|
9190
|
+
const configPath = join18(home, "config.toml");
|
|
8971
9191
|
let text;
|
|
8972
9192
|
try {
|
|
8973
9193
|
text = await readFile5(configPath, "utf8");
|
|
@@ -9082,11 +9302,11 @@ function buildCodexDeveloperInstructions(rec) {
|
|
|
9082
9302
|
${directive}` : directive;
|
|
9083
9303
|
}
|
|
9084
9304
|
function withTimeout(p, ms, msg) {
|
|
9085
|
-
return new Promise((
|
|
9305
|
+
return new Promise((resolve4, reject) => {
|
|
9086
9306
|
const t = setTimeout(() => reject(new Error(msg)), ms);
|
|
9087
9307
|
p.then((v) => {
|
|
9088
9308
|
clearTimeout(t);
|
|
9089
|
-
|
|
9309
|
+
resolve4(v);
|
|
9090
9310
|
}, (e) => {
|
|
9091
9311
|
clearTimeout(t);
|
|
9092
9312
|
reject(e);
|
|
@@ -9488,9 +9708,9 @@ var init_sse_bridge = __esm({
|
|
|
9488
9708
|
|
|
9489
9709
|
// packages/agents/dist/interactive/claude.js
|
|
9490
9710
|
import { execSync as execSync7 } from "child_process";
|
|
9491
|
-
import { readFileSync as
|
|
9492
|
-
import { homedir as
|
|
9493
|
-
import { join as
|
|
9711
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
9712
|
+
import { homedir as homedir14 } from "os";
|
|
9713
|
+
import { join as join19 } from "path";
|
|
9494
9714
|
function probeClaudeSettingsFlag() {
|
|
9495
9715
|
try {
|
|
9496
9716
|
const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9548,11 +9768,11 @@ function deriveTranscriptPath(sessionId, cwd) {
|
|
|
9548
9768
|
if (!sessionId || !cwd)
|
|
9549
9769
|
return null;
|
|
9550
9770
|
const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
9551
|
-
return
|
|
9771
|
+
return join19(homedir14(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
|
|
9552
9772
|
}
|
|
9553
9773
|
function readLastAssistantText(transcriptPath) {
|
|
9554
9774
|
try {
|
|
9555
|
-
const raw =
|
|
9775
|
+
const raw = readFileSync13(transcriptPath, "utf-8");
|
|
9556
9776
|
const lines = raw.split(/\r?\n/);
|
|
9557
9777
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
9558
9778
|
const line = lines[i].trim();
|
|
@@ -9594,8 +9814,8 @@ function resolveLastAssistantText(payload) {
|
|
|
9594
9814
|
const derived = deriveTranscriptPath(p?.session_id, cwd);
|
|
9595
9815
|
if (derived)
|
|
9596
9816
|
candidates.push(derived);
|
|
9597
|
-
for (const
|
|
9598
|
-
const text = readLastAssistantText(
|
|
9817
|
+
for (const path34 of candidates) {
|
|
9818
|
+
const text = readLastAssistantText(path34);
|
|
9599
9819
|
if (text != null)
|
|
9600
9820
|
return text;
|
|
9601
9821
|
}
|
|
@@ -9945,14 +10165,14 @@ function runHeadless(opts) {
|
|
|
9945
10165
|
if (err.length > ERR_CAP)
|
|
9946
10166
|
err = err.slice(err.length - ERR_CAP);
|
|
9947
10167
|
});
|
|
9948
|
-
const result = new Promise((
|
|
10168
|
+
const result = new Promise((resolve4) => {
|
|
9949
10169
|
child.once("error", (e) => {
|
|
9950
10170
|
if (debounceTimer) {
|
|
9951
10171
|
clearTimeout(debounceTimer);
|
|
9952
10172
|
debounceTimer = null;
|
|
9953
10173
|
}
|
|
9954
10174
|
opts.emit({ type: "task.failed", id: opts.id, error: `spawn error: ${e.message}`, exitCode: void 0 });
|
|
9955
|
-
|
|
10175
|
+
resolve4();
|
|
9956
10176
|
});
|
|
9957
10177
|
child.on("close", (code) => {
|
|
9958
10178
|
if (chunksSinceProgress > 0)
|
|
@@ -9969,7 +10189,7 @@ function runHeadless(opts) {
|
|
|
9969
10189
|
const ref = opts.writeResult ? opts.writeResult(opts.id, res.payload ?? "") : "";
|
|
9970
10190
|
opts.emit({ type: "task.done", id: opts.id, resultRef: ref, parseWarning: res.parseWarning });
|
|
9971
10191
|
}
|
|
9972
|
-
|
|
10192
|
+
resolve4();
|
|
9973
10193
|
});
|
|
9974
10194
|
});
|
|
9975
10195
|
return { result, kill: () => child.kill("SIGTERM") };
|
|
@@ -10049,8 +10269,8 @@ var require_daemon_exports = {};
|
|
|
10049
10269
|
__export(require_daemon_exports, {
|
|
10050
10270
|
requireDaemon: () => requireDaemon
|
|
10051
10271
|
});
|
|
10052
|
-
import { join as
|
|
10053
|
-
import { homedir as
|
|
10272
|
+
import { join as join25 } from "path";
|
|
10273
|
+
import { homedir as homedir20 } from "os";
|
|
10054
10274
|
async function requireDaemon(sockPath = DEFAULT_SOCK_PATH3) {
|
|
10055
10275
|
const isLive = await isDaemonSocketLive(sockPath);
|
|
10056
10276
|
if (!isLive) {
|
|
@@ -10061,18 +10281,18 @@ var DEFAULT_SOCK_PATH3;
|
|
|
10061
10281
|
var init_require_daemon = __esm({
|
|
10062
10282
|
"packages/cli/src/lib/require-daemon.ts"() {
|
|
10063
10283
|
init_dist2();
|
|
10064
|
-
DEFAULT_SOCK_PATH3 =
|
|
10284
|
+
DEFAULT_SOCK_PATH3 = join25(homedir20(), ".config", "squadrant", "squadrant.sock");
|
|
10065
10285
|
}
|
|
10066
10286
|
});
|
|
10067
10287
|
|
|
10068
10288
|
// packages/cli/src/index.ts
|
|
10069
10289
|
init_dist();
|
|
10070
10290
|
init_dist2();
|
|
10071
|
-
import { Command as
|
|
10072
|
-
import { readFileSync as
|
|
10291
|
+
import { Command as Command35 } from "commander";
|
|
10292
|
+
import { readFileSync as readFileSync16, existsSync as existsSync13, writeFileSync as writeFileSync12 } from "fs";
|
|
10073
10293
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
10074
|
-
import { dirname as dirname9, join as
|
|
10075
|
-
import { homedir as
|
|
10294
|
+
import { dirname as dirname9, join as join30 } from "path";
|
|
10295
|
+
import { homedir as homedir22 } from "os";
|
|
10076
10296
|
|
|
10077
10297
|
// packages/cli/src/commands/doctor.ts
|
|
10078
10298
|
init_dist();
|
|
@@ -10089,10 +10309,10 @@ import chalk3 from "chalk";
|
|
|
10089
10309
|
// packages/cli/src/commands/health-view.ts
|
|
10090
10310
|
init_dist2();
|
|
10091
10311
|
init_dist2();
|
|
10092
|
-
import { homedir as
|
|
10093
|
-
import { join as
|
|
10312
|
+
import { homedir as homedir12 } from "os";
|
|
10313
|
+
import { join as join17 } from "path";
|
|
10094
10314
|
import chalk2 from "chalk";
|
|
10095
|
-
var SOCK =
|
|
10315
|
+
var SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
|
|
10096
10316
|
async function queryHealth(project) {
|
|
10097
10317
|
try {
|
|
10098
10318
|
const reply = await sendRequest(SOCK, { kind: "health", project });
|
|
@@ -10431,9 +10651,9 @@ import chalk4 from "chalk";
|
|
|
10431
10651
|
|
|
10432
10652
|
// packages/cli/src/lib/per-crew-settings.ts
|
|
10433
10653
|
init_dist4();
|
|
10434
|
-
import { mkdirSync as
|
|
10435
|
-
import { dirname as dirname5, join as
|
|
10436
|
-
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";
|
|
10437
10657
|
var CREW_PERMISSION_ALLOWLIST = [
|
|
10438
10658
|
// git — read + safe mutations (reset/clean/config intentionally excluded)
|
|
10439
10659
|
"Bash(git status:*)",
|
|
@@ -10524,29 +10744,29 @@ function mergeCrewPermissions(settings) {
|
|
|
10524
10744
|
return next;
|
|
10525
10745
|
}
|
|
10526
10746
|
function writePerCrewSettingsLocal(o) {
|
|
10527
|
-
const dir =
|
|
10528
|
-
|
|
10529
|
-
const file =
|
|
10747
|
+
const dir = join20(o.projectCwd, ".claude");
|
|
10748
|
+
mkdirSync8(dir, { recursive: true });
|
|
10749
|
+
const file = join20(dir, "settings.local.json");
|
|
10530
10750
|
let existing = {};
|
|
10531
10751
|
try {
|
|
10532
|
-
const raw = healStaleCockpitRefs(
|
|
10752
|
+
const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
|
|
10533
10753
|
existing = JSON.parse(raw);
|
|
10534
10754
|
} catch {
|
|
10535
10755
|
}
|
|
10536
10756
|
const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
|
|
10537
10757
|
const merged = mergeCrewPermissions(withHooks);
|
|
10538
|
-
|
|
10758
|
+
writeFileSync10(file, JSON.stringify(merged, null, 2));
|
|
10539
10759
|
return file;
|
|
10540
10760
|
}
|
|
10541
|
-
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH =
|
|
10761
|
+
var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join20(homedir15(), ".config", "opencode", "opencode.json");
|
|
10542
10762
|
function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
|
|
10543
|
-
|
|
10763
|
+
mkdirSync8(dirname5(configPath), { recursive: true });
|
|
10544
10764
|
const defaultConfig = {
|
|
10545
10765
|
$schema: "https://opencode.ai/config.json",
|
|
10546
10766
|
model: "anthropic/claude-sonnet-4-5"
|
|
10547
10767
|
};
|
|
10548
10768
|
try {
|
|
10549
|
-
|
|
10769
|
+
writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
|
|
10550
10770
|
return configPath;
|
|
10551
10771
|
} catch (err) {
|
|
10552
10772
|
if (err.code === "EEXIST") return null;
|
|
@@ -10554,9 +10774,9 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
|
|
|
10554
10774
|
}
|
|
10555
10775
|
}
|
|
10556
10776
|
function writePerCrewOpencodeConfig(o) {
|
|
10557
|
-
const dir =
|
|
10558
|
-
|
|
10559
|
-
const file =
|
|
10777
|
+
const dir = join20(o.stateRoot, o.project, o.taskId);
|
|
10778
|
+
mkdirSync8(dir, { recursive: true });
|
|
10779
|
+
const file = join20(dir, "opencode.json");
|
|
10560
10780
|
const config = {
|
|
10561
10781
|
permission: {
|
|
10562
10782
|
read: "allow",
|
|
@@ -10571,7 +10791,7 @@ function writePerCrewOpencodeConfig(o) {
|
|
|
10571
10791
|
external_directory: { "**": "allow" }
|
|
10572
10792
|
}
|
|
10573
10793
|
};
|
|
10574
|
-
|
|
10794
|
+
writeFileSync10(file, JSON.stringify(config, null, 2));
|
|
10575
10795
|
return file;
|
|
10576
10796
|
}
|
|
10577
10797
|
|
|
@@ -10602,11 +10822,11 @@ function stepHeader(n, total, label) {
|
|
|
10602
10822
|
${n}/${total} ${label}`));
|
|
10603
10823
|
}
|
|
10604
10824
|
function promptLine(question) {
|
|
10605
|
-
return new Promise((
|
|
10825
|
+
return new Promise((resolve4) => {
|
|
10606
10826
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10607
10827
|
rl.question(question, (answer) => {
|
|
10608
10828
|
rl.close();
|
|
10609
|
-
|
|
10829
|
+
resolve4(answer.trim());
|
|
10610
10830
|
});
|
|
10611
10831
|
});
|
|
10612
10832
|
}
|
|
@@ -10934,48 +11154,22 @@ var projectsCommand = new Command3("projects").description("Manage registered pr
|
|
|
10934
11154
|
|
|
10935
11155
|
// packages/cli/src/commands/status.ts
|
|
10936
11156
|
init_dist();
|
|
10937
|
-
init_dist3();
|
|
10938
11157
|
import { Command as Command4 } from "commander";
|
|
10939
11158
|
import chalk6 from "chalk";
|
|
10940
|
-
import matter2 from "gray-matter";
|
|
10941
|
-
function timeAgo(dateStr) {
|
|
10942
|
-
if (!dateStr) return chalk6.dim("\u2014");
|
|
10943
|
-
const date = new Date(dateStr);
|
|
10944
|
-
if (isNaN(date.getTime())) return chalk6.dim("\u2014");
|
|
10945
|
-
const diff = Date.now() - date.getTime();
|
|
10946
|
-
const mins = Math.floor(diff / 6e4);
|
|
10947
|
-
const hours = Math.floor(diff / 36e5);
|
|
10948
|
-
const days = Math.floor(diff / 864e5);
|
|
10949
|
-
if (mins < 1) return "just now";
|
|
10950
|
-
if (mins < 60) return `${mins}m ago`;
|
|
10951
|
-
if (hours < 24) return `${hours}h ago`;
|
|
10952
|
-
return `${days}d ago`;
|
|
10953
|
-
}
|
|
10954
|
-
function progressBar(completed, total) {
|
|
10955
|
-
if (total === 0) return chalk6.dim("no tasks");
|
|
10956
|
-
const pct = Math.round(completed / total * 100);
|
|
10957
|
-
const filled = Math.round(pct / 10);
|
|
10958
|
-
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
10959
|
-
return `${bar} ${pct}%`;
|
|
10960
|
-
}
|
|
10961
11159
|
function captainIndicator(state) {
|
|
10962
11160
|
if (state === "alive" || state === "stale") return chalk6.green("\u25CF");
|
|
10963
11161
|
if (state === "stopped") return chalk6.magenta("\u23FB");
|
|
10964
11162
|
if (state === void 0 || state === "unknown") return chalk6.dim("?");
|
|
10965
11163
|
return chalk6.dim("\u25CB");
|
|
10966
11164
|
}
|
|
10967
|
-
function formatProjectRow(name, captainName,
|
|
11165
|
+
function formatProjectRow(name, captainName, captainState) {
|
|
10968
11166
|
const sessionIndicator = captainIndicator(captainState);
|
|
10969
11167
|
const captainDisplay = `${captainName.padEnd(11)} ${sessionIndicator}`;
|
|
10970
|
-
|
|
10971
|
-
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);
|
|
10972
|
-
const updated = statusMdState === "ok" ? timeAgo(fm.last_updated) : chalk6.dim("\u2014");
|
|
10973
|
-
return ` ${name.padEnd(18)} ${captainDisplay} ${crew} ${progress} ${updated}`;
|
|
11168
|
+
return ` ${name.padEnd(18)} ${captainDisplay}`;
|
|
10974
11169
|
}
|
|
10975
|
-
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) => {
|
|
10976
11171
|
const config = loadConfig();
|
|
10977
11172
|
const projects = Object.entries(config.projects);
|
|
10978
|
-
const registry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
|
|
10979
11173
|
if (projects.length === 0) {
|
|
10980
11174
|
console.log(chalk6.yellow("\nNo projects registered. Use: squadrant projects add <name> <path>\n"));
|
|
10981
11175
|
return;
|
|
@@ -10988,30 +11182,12 @@ var statusCommand = new Command4("status").description("Show status of all proje
|
|
|
10988
11182
|
}
|
|
10989
11183
|
}
|
|
10990
11184
|
console.log(chalk6.bold("\nProject Status\n"));
|
|
10991
|
-
console.log(
|
|
10992
|
-
|
|
10993
|
-
` ${"PROJECT".padEnd(18)} ${"CAPTAIN".padEnd(12)} ${"CREW".padEnd(6)} ${"PROGRESS".padEnd(25)} LAST UPDATE`
|
|
10994
|
-
)
|
|
10995
|
-
);
|
|
10996
|
-
console.log(chalk6.dim(" " + "\u2500".repeat(85)));
|
|
11185
|
+
console.log(chalk6.dim(` ${"PROJECT".padEnd(18)} CAPTAIN`));
|
|
11186
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(35)));
|
|
10997
11187
|
for (const [name, project] of projects) {
|
|
10998
|
-
|
|
10999
|
-
let fm = {};
|
|
11000
|
-
let statusMdState = "missing";
|
|
11001
|
-
if (await workspace.exists("status.md")) {
|
|
11002
|
-
try {
|
|
11003
|
-
const raw = await workspace.read("status.md");
|
|
11004
|
-
fm = matter2(raw).data;
|
|
11005
|
-
statusMdState = "ok";
|
|
11006
|
-
} catch {
|
|
11007
|
-
statusMdState = "unreadable";
|
|
11008
|
-
}
|
|
11009
|
-
}
|
|
11010
|
-
console.log(
|
|
11011
|
-
formatProjectRow(name, project.captainName, fm, statusMdState, captainStateByProject.get(name))
|
|
11012
|
-
);
|
|
11188
|
+
console.log(formatProjectRow(name, project.captainName, captainStateByProject.get(name)));
|
|
11013
11189
|
}
|
|
11014
|
-
console.log("");
|
|
11190
|
+
console.log(chalk6.dim("\n Task/crew counts: no data source yet \u2014 see squadrant/squadrant#630\n"));
|
|
11015
11191
|
if (opts.detailed) {
|
|
11016
11192
|
printServiceHealth(health);
|
|
11017
11193
|
}
|
|
@@ -11100,9 +11276,9 @@ import { Command as Command8 } from "commander";
|
|
|
11100
11276
|
import { createConnection as createConnection3 } from "net";
|
|
11101
11277
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
11102
11278
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
11103
|
-
import { homedir as
|
|
11104
|
-
import { join as
|
|
11105
|
-
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";
|
|
11106
11282
|
|
|
11107
11283
|
// packages/cli/src/commands/crew-output.ts
|
|
11108
11284
|
function tailLines(text, maxLines = 40, maxBytes = 4096) {
|
|
@@ -11160,11 +11336,11 @@ init_dist2();
|
|
|
11160
11336
|
import { Command as Command6 } from "commander";
|
|
11161
11337
|
import chalk8 from "chalk";
|
|
11162
11338
|
import { createConnection as createConnection2 } from "net";
|
|
11163
|
-
import { homedir as
|
|
11164
|
-
import { join as
|
|
11339
|
+
import { homedir as homedir16 } from "os";
|
|
11340
|
+
import { join as join21 } from "path";
|
|
11165
11341
|
import { createInterface } from "readline";
|
|
11166
11342
|
function socketPath() {
|
|
11167
|
-
return process.env.SQUADRANTD_SOCK ??
|
|
11343
|
+
return process.env.SQUADRANTD_SOCK ?? join21(homedir16(), ".config", "squadrant", "squadrant.sock");
|
|
11168
11344
|
}
|
|
11169
11345
|
function rule(width, ch = "\u2500") {
|
|
11170
11346
|
return ch.repeat(Math.max(0, width));
|
|
@@ -11400,11 +11576,11 @@ var crewChatCommand = new Command7("chat").description("[DEPRECATED] alias for `
|
|
|
11400
11576
|
});
|
|
11401
11577
|
|
|
11402
11578
|
// packages/cli/src/commands/crew-control.ts
|
|
11403
|
-
var SOCK2 =
|
|
11579
|
+
var SOCK2 = join22(homedir17(), ".config", "squadrant", "squadrant.sock");
|
|
11404
11580
|
var CODEX_FIRST_TURN_DELAY_MS = 1500;
|
|
11405
11581
|
async function sendCodexFirstTurn(taskId, text) {
|
|
11406
11582
|
await new Promise((r) => setTimeout(r, CODEX_FIRST_TURN_DELAY_MS));
|
|
11407
|
-
await new Promise((
|
|
11583
|
+
await new Promise((resolve4, reject) => {
|
|
11408
11584
|
const conn = createConnection3(SOCK2);
|
|
11409
11585
|
conn.setEncoding("utf-8");
|
|
11410
11586
|
conn.on("data", () => {
|
|
@@ -11420,7 +11596,7 @@ async function sendCodexFirstTurn(taskId, text) {
|
|
|
11420
11596
|
}
|
|
11421
11597
|
setTimeout(() => {
|
|
11422
11598
|
conn.end();
|
|
11423
|
-
|
|
11599
|
+
resolve4();
|
|
11424
11600
|
}, 100);
|
|
11425
11601
|
});
|
|
11426
11602
|
});
|
|
@@ -11507,10 +11683,10 @@ function buildSignalRequest(signal, o) {
|
|
|
11507
11683
|
return { kind: "event", project, event };
|
|
11508
11684
|
}
|
|
11509
11685
|
function defaultWriteResult(id, payload) {
|
|
11510
|
-
const dir =
|
|
11511
|
-
|
|
11512
|
-
const file =
|
|
11513
|
-
|
|
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);
|
|
11514
11690
|
return file;
|
|
11515
11691
|
}
|
|
11516
11692
|
async function runCrewSignal(signal, o, deps) {
|
|
@@ -11912,11 +12088,11 @@ function parseCrewPick(raw, stats) {
|
|
|
11912
12088
|
throw new Error(`Invalid selection '${raw}'. Enter a number 1-${stats.length} or a crew name.`);
|
|
11913
12089
|
}
|
|
11914
12090
|
function promptLine2(question) {
|
|
11915
|
-
return new Promise((
|
|
12091
|
+
return new Promise((resolve4) => {
|
|
11916
12092
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
11917
12093
|
rl.question(question, (answer) => {
|
|
11918
12094
|
rl.close();
|
|
11919
|
-
|
|
12095
|
+
resolve4(answer);
|
|
11920
12096
|
});
|
|
11921
12097
|
});
|
|
11922
12098
|
}
|
|
@@ -12190,8 +12366,8 @@ init_dist();
|
|
|
12190
12366
|
init_dist3();
|
|
12191
12367
|
import { Command as Command12 } from "commander";
|
|
12192
12368
|
import { execSync as execSync10 } from "child_process";
|
|
12193
|
-
import { homedir as
|
|
12194
|
-
import { join as
|
|
12369
|
+
import { homedir as homedir19 } from "os";
|
|
12370
|
+
import { join as join24 } from "path";
|
|
12195
12371
|
import chalk13 from "chalk";
|
|
12196
12372
|
|
|
12197
12373
|
// packages/web/dist/read-status.js
|
|
@@ -12404,9 +12580,9 @@ function mergeSnapshot(daemon, external, now) {
|
|
|
12404
12580
|
// packages/web/dist/probes.js
|
|
12405
12581
|
init_dist();
|
|
12406
12582
|
init_dist();
|
|
12407
|
-
import { join as
|
|
12408
|
-
import { homedir as
|
|
12409
|
-
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";
|
|
12410
12586
|
import { execFile as execFile4 } from "child_process";
|
|
12411
12587
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
12412
12588
|
var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
|
|
@@ -12442,7 +12618,7 @@ function vaultProbe(run, dir) {
|
|
|
12442
12618
|
return { state: "unknown", detail: "no vault configured" };
|
|
12443
12619
|
if (!run.pathExists(dir))
|
|
12444
12620
|
return { state: "gone", detail: "vault directory missing" };
|
|
12445
|
-
if (!run.pathExists(
|
|
12621
|
+
if (!run.pathExists(join23(dir, ".obsidian")))
|
|
12446
12622
|
return { state: "gone", detail: "no .obsidian/ (not a vault)" };
|
|
12447
12623
|
return { state: "alive" };
|
|
12448
12624
|
} catch {
|
|
@@ -12510,27 +12686,27 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
12510
12686
|
const sessions = probeSessions(run);
|
|
12511
12687
|
return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
|
|
12512
12688
|
}
|
|
12513
|
-
var SESSIONS_PATH =
|
|
12689
|
+
var SESSIONS_PATH = join23(homedir18(), ".config", "squadrant", "sessions.json");
|
|
12514
12690
|
function onPath(cli) {
|
|
12515
12691
|
const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
12516
|
-
return dirs.some((d) =>
|
|
12692
|
+
return dirs.some((d) => existsSync12(join23(d, cli)));
|
|
12517
12693
|
}
|
|
12518
12694
|
function readSessionsHashes() {
|
|
12519
|
-
const raw = JSON.parse(
|
|
12695
|
+
const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
|
|
12520
12696
|
const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
|
|
12521
12697
|
return [...new Set(hashes)];
|
|
12522
12698
|
}
|
|
12523
12699
|
function defaultProbeRunners() {
|
|
12524
12700
|
return {
|
|
12525
|
-
probeCmuxBin: () => new Promise((
|
|
12701
|
+
probeCmuxBin: () => new Promise((resolve4) => {
|
|
12526
12702
|
try {
|
|
12527
|
-
execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) =>
|
|
12703
|
+
execFile4(resolveCmuxBin(), ["--version"], { timeout: 1500 }, (err) => resolve4(!err));
|
|
12528
12704
|
} catch {
|
|
12529
|
-
|
|
12705
|
+
resolve4(false);
|
|
12530
12706
|
}
|
|
12531
12707
|
}),
|
|
12532
12708
|
probeOnPath: async (cli) => onPath(cli),
|
|
12533
|
-
pathExists: (p) =>
|
|
12709
|
+
pathExists: (p) => existsSync12(p),
|
|
12534
12710
|
loadConfig: () => loadConfig(),
|
|
12535
12711
|
loadSessionsHashes: () => readSessionsHashes()
|
|
12536
12712
|
};
|
|
@@ -13391,9 +13567,9 @@ async function startWebServer(opts) {
|
|
|
13391
13567
|
}
|
|
13392
13568
|
res.writeHead(404).end();
|
|
13393
13569
|
});
|
|
13394
|
-
await new Promise((
|
|
13570
|
+
await new Promise((resolve4, reject) => {
|
|
13395
13571
|
server.once("error", reject);
|
|
13396
|
-
server.listen(opts.port, host,
|
|
13572
|
+
server.listen(opts.port, host, resolve4);
|
|
13397
13573
|
});
|
|
13398
13574
|
const addr = server.address();
|
|
13399
13575
|
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
@@ -13405,7 +13581,7 @@ async function startWebServer(opts) {
|
|
|
13405
13581
|
timer.unref?.();
|
|
13406
13582
|
return {
|
|
13407
13583
|
port: boundPort,
|
|
13408
|
-
close: () => new Promise((
|
|
13584
|
+
close: () => new Promise((resolve4) => {
|
|
13409
13585
|
clearInterval(timer);
|
|
13410
13586
|
for (const res of clients) {
|
|
13411
13587
|
try {
|
|
@@ -13414,14 +13590,14 @@ async function startWebServer(opts) {
|
|
|
13414
13590
|
}
|
|
13415
13591
|
}
|
|
13416
13592
|
clients.clear();
|
|
13417
|
-
server.close(() =>
|
|
13593
|
+
server.close(() => resolve4());
|
|
13418
13594
|
})
|
|
13419
13595
|
};
|
|
13420
13596
|
}
|
|
13421
13597
|
|
|
13422
13598
|
// packages/cli/src/commands/dashboard.ts
|
|
13423
13599
|
init_dist();
|
|
13424
|
-
var SOCK3 =
|
|
13600
|
+
var SOCK3 = join24(homedir19(), ".config", "squadrant", "squadrant.sock");
|
|
13425
13601
|
function detectCurrentWorkspace2() {
|
|
13426
13602
|
const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
|
|
13427
13603
|
const match = out.match(/workspace:\d+/);
|
|
@@ -13940,29 +14116,16 @@ init_dist();
|
|
|
13940
14116
|
init_dist();
|
|
13941
14117
|
init_dist3();
|
|
13942
14118
|
import { Command as Command16 } from "commander";
|
|
13943
|
-
import fs24 from "fs";
|
|
13944
|
-
import path26 from "path";
|
|
13945
14119
|
import chalk17 from "chalk";
|
|
13946
|
-
import matter3 from "gray-matter";
|
|
13947
14120
|
function getDateStr(yesterday) {
|
|
13948
14121
|
return iso(daysAgo(yesterday ? 1 : 0));
|
|
13949
14122
|
}
|
|
13950
14123
|
async function getProjectStandup(name, project, dateStr, registry, config) {
|
|
13951
14124
|
const workspace = registry.forProject(name, config);
|
|
13952
|
-
const spokeVault = resolveHome(project.spokeVault);
|
|
13953
|
-
const statusFile = path26.join(spokeVault, "status.md");
|
|
13954
|
-
let status = {};
|
|
13955
|
-
if (fs24.existsSync(statusFile)) {
|
|
13956
|
-
try {
|
|
13957
|
-
status = matter3(fs24.readFileSync(statusFile, "utf-8")).data;
|
|
13958
|
-
} catch {
|
|
13959
|
-
}
|
|
13960
|
-
}
|
|
13961
14125
|
const log = await readDailyLog(workspace, dateStr);
|
|
13962
14126
|
const gitCommits = getGitCommits(project.path, dateStr);
|
|
13963
14127
|
return {
|
|
13964
14128
|
name,
|
|
13965
|
-
status,
|
|
13966
14129
|
dailyLog: log?.content ?? null,
|
|
13967
14130
|
gitCommits,
|
|
13968
14131
|
blockers: log?.blockers ?? []
|
|
@@ -13981,26 +14144,16 @@ ${header}
|
|
|
13981
14144
|
}
|
|
13982
14145
|
let hasBlockers = false;
|
|
13983
14146
|
for (const s of standups) {
|
|
13984
|
-
const tasksDone = s.status.tasks_completed ?? 0;
|
|
13985
|
-
const tasksTotal = s.status.tasks_total ?? 0;
|
|
13986
|
-
const tasksInProgress = s.status.tasks_in_progress ?? 0;
|
|
13987
14147
|
if (!raw) {
|
|
13988
14148
|
lines.push(chalk17.cyan.bold(`## ${s.name}`));
|
|
13989
14149
|
} else {
|
|
13990
14150
|
lines.push(`## ${s.name}`);
|
|
13991
14151
|
}
|
|
13992
|
-
if (s.gitCommits.length > 0
|
|
14152
|
+
if (s.gitCommits.length > 0) {
|
|
13993
14153
|
lines.push(!raw ? chalk17.green("Done:") : "**Done:**");
|
|
13994
14154
|
for (const commit of s.gitCommits) {
|
|
13995
14155
|
lines.push(` - ${commit}`);
|
|
13996
14156
|
}
|
|
13997
|
-
if (tasksDone > 0 && s.gitCommits.length === 0) {
|
|
13998
|
-
lines.push(` - ${tasksDone}/${tasksTotal} tasks completed`);
|
|
13999
|
-
}
|
|
14000
|
-
}
|
|
14001
|
-
if (tasksInProgress > 0) {
|
|
14002
|
-
lines.push(!raw ? chalk17.yellow("In Progress:") : "**In Progress:**");
|
|
14003
|
-
lines.push(` - ${tasksInProgress} task(s) active`);
|
|
14004
14157
|
}
|
|
14005
14158
|
if (s.dailyLog) {
|
|
14006
14159
|
const sections = ["Completed", "In Progress", "Tomorrow"];
|
|
@@ -14022,19 +14175,18 @@ ${header}
|
|
|
14022
14175
|
lines.push(` - ${b}`);
|
|
14023
14176
|
}
|
|
14024
14177
|
}
|
|
14025
|
-
if (s.gitCommits.length === 0 &&
|
|
14178
|
+
if (s.gitCommits.length === 0 && !s.dailyLog) {
|
|
14026
14179
|
lines.push(!raw ? chalk17.dim(" (no activity)") : " (no activity)");
|
|
14027
14180
|
}
|
|
14028
14181
|
lines.push("");
|
|
14029
14182
|
}
|
|
14030
14183
|
const totalCommits = standups.reduce((sum, s) => sum + s.gitCommits.length, 0);
|
|
14031
|
-
const totalDone = standups.reduce((sum, s) => sum + (s.status.tasks_completed ?? 0), 0);
|
|
14032
14184
|
if (!raw) {
|
|
14033
|
-
lines.push(chalk17.dim(`--- ${totalCommits} commits
|
|
14185
|
+
lines.push(chalk17.dim(`--- ${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630) ---
|
|
14034
14186
|
`));
|
|
14035
14187
|
} else {
|
|
14036
14188
|
lines.push(`---
|
|
14037
|
-
*${totalCommits} commits
|
|
14189
|
+
*${totalCommits} commits${hasBlockers ? ", HAS BLOCKERS" : ""} (task tracking: no data source \u2014 #630)*
|
|
14038
14190
|
`);
|
|
14039
14191
|
}
|
|
14040
14192
|
return lines.join("\n");
|
|
@@ -14072,19 +14224,7 @@ init_dist();
|
|
|
14072
14224
|
init_dist();
|
|
14073
14225
|
init_dist3();
|
|
14074
14226
|
import { Command as Command17 } from "commander";
|
|
14075
|
-
import fs25 from "fs";
|
|
14076
|
-
import path27 from "path";
|
|
14077
14227
|
import chalk18 from "chalk";
|
|
14078
|
-
import matter4 from "gray-matter";
|
|
14079
|
-
function readStatus(spokeVault) {
|
|
14080
|
-
const statusFile = path27.join(spokeVault, "status.md");
|
|
14081
|
-
if (!fs25.existsSync(statusFile)) return {};
|
|
14082
|
-
try {
|
|
14083
|
-
return matter4(fs25.readFileSync(statusFile, "utf-8")).data;
|
|
14084
|
-
} catch {
|
|
14085
|
-
return {};
|
|
14086
|
-
}
|
|
14087
|
-
}
|
|
14088
14228
|
function dedupe(items) {
|
|
14089
14229
|
const seen = /* @__PURE__ */ new Set();
|
|
14090
14230
|
const out = [];
|
|
@@ -14099,8 +14239,6 @@ function dedupe(items) {
|
|
|
14099
14239
|
}
|
|
14100
14240
|
async function getProjectRetro(name, project, fromStr, toStr, registry, config) {
|
|
14101
14241
|
const workspace = registry.forProject(name, config);
|
|
14102
|
-
const spokeVault = resolveHome(project.spokeVault);
|
|
14103
|
-
const status = readStatus(spokeVault);
|
|
14104
14242
|
const shipped = [];
|
|
14105
14243
|
const inProgress = [];
|
|
14106
14244
|
const blocked = [];
|
|
@@ -14130,9 +14268,7 @@ async function getProjectRetro(name, project, fromStr, toStr, registry, config)
|
|
|
14130
14268
|
blocked: dedupe(blocked),
|
|
14131
14269
|
decisions: dedupe(decisions),
|
|
14132
14270
|
commits,
|
|
14133
|
-
mergedPRs
|
|
14134
|
-
tasksCompletedNow: status.tasks_completed ?? 0,
|
|
14135
|
-
tasksInProgressNow: status.tasks_in_progress ?? 0
|
|
14271
|
+
mergedPRs
|
|
14136
14272
|
};
|
|
14137
14273
|
}
|
|
14138
14274
|
function renderList(lines, items, raw, label, color) {
|
|
@@ -14288,9 +14424,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
|
|
|
14288
14424
|
const resolved = resolveTarget(registry, config, target, !!opts.command);
|
|
14289
14425
|
await needRef(resolved);
|
|
14290
14426
|
const finalProject = opts.command ? config.commandName : target;
|
|
14291
|
-
const { join:
|
|
14427
|
+
const { join: join31, dirname: dirname10 } = await import("path");
|
|
14292
14428
|
const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
14293
|
-
const stateRoot =
|
|
14429
|
+
const stateRoot = join31(dirname10(DEFAULT_CONFIG_PATH2), "state");
|
|
14294
14430
|
const seq = await appendCaptainMessage2({
|
|
14295
14431
|
stateRoot,
|
|
14296
14432
|
project: finalProject,
|
|
@@ -14403,9 +14539,9 @@ workspaceCommand.command("read").description("Print the contents of a scope-rela
|
|
|
14403
14539
|
const config = loadConfig();
|
|
14404
14540
|
const registry = buildRegistry2();
|
|
14405
14541
|
try {
|
|
14406
|
-
const { projectTarget, path:
|
|
14542
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14407
14543
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14408
|
-
const content = await driver.read(
|
|
14544
|
+
const content = await driver.read(path34);
|
|
14409
14545
|
process.stdout.write(content);
|
|
14410
14546
|
} catch (err) {
|
|
14411
14547
|
console.error(chalk20.red(err.message));
|
|
@@ -14417,26 +14553,26 @@ workspaceCommand.command("write").description("Write content to a scope-relative
|
|
|
14417
14553
|
const registry = buildRegistry2();
|
|
14418
14554
|
try {
|
|
14419
14555
|
let projectTarget;
|
|
14420
|
-
let
|
|
14556
|
+
let path34;
|
|
14421
14557
|
let rawContent;
|
|
14422
14558
|
if (opts.hub) {
|
|
14423
14559
|
if (arg3 !== void 0) {
|
|
14424
14560
|
throw new Error("With --hub, pass only the path and content");
|
|
14425
14561
|
}
|
|
14426
14562
|
projectTarget = void 0;
|
|
14427
|
-
|
|
14563
|
+
path34 = arg1;
|
|
14428
14564
|
rawContent = arg2;
|
|
14429
14565
|
} else {
|
|
14430
14566
|
if (arg3 === void 0) {
|
|
14431
14567
|
throw new Error("Missing content \u2014 usage: <project> <path> <content>");
|
|
14432
14568
|
}
|
|
14433
14569
|
projectTarget = arg1;
|
|
14434
|
-
|
|
14570
|
+
path34 = arg2;
|
|
14435
14571
|
rawContent = arg3;
|
|
14436
14572
|
}
|
|
14437
14573
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14438
14574
|
const payload = rawContent === "-" ? await readStdin() : rawContent;
|
|
14439
|
-
await driver.write(
|
|
14575
|
+
await driver.write(path34, payload);
|
|
14440
14576
|
} catch (err) {
|
|
14441
14577
|
console.error(chalk20.red(err.message));
|
|
14442
14578
|
process.exit(1);
|
|
@@ -14446,9 +14582,9 @@ workspaceCommand.command("list").description("List entries in a scope-relative d
|
|
|
14446
14582
|
const config = loadConfig();
|
|
14447
14583
|
const registry = buildRegistry2();
|
|
14448
14584
|
try {
|
|
14449
|
-
const { projectTarget, path:
|
|
14585
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14450
14586
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14451
|
-
const entries = await driver.list(
|
|
14587
|
+
const entries = await driver.list(path34);
|
|
14452
14588
|
for (const entry of entries) console.log(entry);
|
|
14453
14589
|
} catch (err) {
|
|
14454
14590
|
console.error(chalk20.red(err.message));
|
|
@@ -14459,9 +14595,9 @@ workspaceCommand.command("exists").description("Exit 0 if path exists, 1 if not"
|
|
|
14459
14595
|
const config = loadConfig();
|
|
14460
14596
|
const registry = buildRegistry2();
|
|
14461
14597
|
try {
|
|
14462
|
-
const { projectTarget, path:
|
|
14598
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14463
14599
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14464
|
-
const ok2 = await driver.exists(
|
|
14600
|
+
const ok2 = await driver.exists(path34);
|
|
14465
14601
|
process.exit(ok2 ? 0 : 1);
|
|
14466
14602
|
} catch (err) {
|
|
14467
14603
|
console.error(chalk20.red(err.message));
|
|
@@ -14472,9 +14608,9 @@ workspaceCommand.command("mkdir").description("Recursively create a scope-relati
|
|
|
14472
14608
|
const config = loadConfig();
|
|
14473
14609
|
const registry = buildRegistry2();
|
|
14474
14610
|
try {
|
|
14475
|
-
const { projectTarget, path:
|
|
14611
|
+
const { projectTarget, path: path34 } = resolveTargetAndPath(arg1, arg2, !!opts.hub);
|
|
14476
14612
|
const driver = resolveDriver(registry, config, projectTarget, !!opts.hub);
|
|
14477
|
-
await driver.mkdir(
|
|
14613
|
+
await driver.mkdir(path34);
|
|
14478
14614
|
} catch (err) {
|
|
14479
14615
|
console.error(chalk20.red(err.message));
|
|
14480
14616
|
process.exit(1);
|
|
@@ -14513,8 +14649,8 @@ init_dist3();
|
|
|
14513
14649
|
init_dist();
|
|
14514
14650
|
import { Command as Command21 } from "commander";
|
|
14515
14651
|
import chalk22 from "chalk";
|
|
14516
|
-
import
|
|
14517
|
-
import
|
|
14652
|
+
import fs24 from "fs";
|
|
14653
|
+
import path26 from "path";
|
|
14518
14654
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
14519
14655
|
function parseScope(v) {
|
|
14520
14656
|
if (v !== "user" && v !== "project") {
|
|
@@ -14523,10 +14659,10 @@ function parseScope(v) {
|
|
|
14523
14659
|
return v;
|
|
14524
14660
|
}
|
|
14525
14661
|
function findPackageRoot3() {
|
|
14526
|
-
let dir =
|
|
14662
|
+
let dir = path26.dirname(fileURLToPath4(import.meta.url));
|
|
14527
14663
|
while (dir !== "/" && dir !== "") {
|
|
14528
|
-
if (
|
|
14529
|
-
dir =
|
|
14664
|
+
if (fs24.existsSync(path26.join(dir, "package.json"))) return dir;
|
|
14665
|
+
dir = path26.dirname(dir);
|
|
14530
14666
|
}
|
|
14531
14667
|
return process.cwd();
|
|
14532
14668
|
}
|
|
@@ -14650,7 +14786,7 @@ projectionCommand.command("list").description("List registered projection target
|
|
|
14650
14786
|
// packages/cli/src/commands/codex-chat-smoke.ts
|
|
14651
14787
|
init_dist4();
|
|
14652
14788
|
import { Command as Command22 } from "commander";
|
|
14653
|
-
import { resolve as
|
|
14789
|
+
import { resolve as resolve3 } from "path";
|
|
14654
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(
|
|
14655
14791
|
"--approval",
|
|
14656
14792
|
"include the approval round-trip (Phase 1 PASS requires this)",
|
|
@@ -14667,7 +14803,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
|
|
|
14667
14803
|
c.start();
|
|
14668
14804
|
await c.initialize();
|
|
14669
14805
|
const { threadId } = await c.startThread({
|
|
14670
|
-
cwd:
|
|
14806
|
+
cwd: resolve3(opts.cwd),
|
|
14671
14807
|
model: opts.model,
|
|
14672
14808
|
sandbox: "workspace-write",
|
|
14673
14809
|
// With --approval, force untrusted policy so codex requests approval
|
|
@@ -14688,7 +14824,7 @@ var codexChatSmokeCommand = new Command22("codex-chat-smoke").description("Phase
|
|
|
14688
14824
|
pendingApprovals.push({ id: r.id, method: r.method });
|
|
14689
14825
|
c.respondToServerRequest(r.id, { decision: "approve" });
|
|
14690
14826
|
});
|
|
14691
|
-
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`);
|
|
14692
14828
|
if (pendingApprovals.length === 0) {
|
|
14693
14829
|
throw new Error("approval gate: expected at least one server-request (approval/input) during the turn");
|
|
14694
14830
|
}
|
|
@@ -14721,12 +14857,12 @@ init_dist();
|
|
|
14721
14857
|
init_dist();
|
|
14722
14858
|
init_dist2();
|
|
14723
14859
|
import { Command as Command23 } from "commander";
|
|
14724
|
-
import
|
|
14860
|
+
import fs25 from "fs";
|
|
14725
14861
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
14726
|
-
import { dirname as dirname6, join as
|
|
14862
|
+
import { dirname as dirname6, join as join26 } from "path";
|
|
14727
14863
|
import chalk23 from "chalk";
|
|
14728
14864
|
function runConfigCheck(opts) {
|
|
14729
|
-
const raw = JSON.parse(
|
|
14865
|
+
const raw = JSON.parse(fs25.readFileSync(opts.configPath, "utf-8"));
|
|
14730
14866
|
const def = getDefaultConfig();
|
|
14731
14867
|
const items = detectDrift(raw, def);
|
|
14732
14868
|
let working = raw;
|
|
@@ -14743,7 +14879,7 @@ function runConfigCheck(opts) {
|
|
|
14743
14879
|
stamped = true;
|
|
14744
14880
|
}
|
|
14745
14881
|
if (opts.fix || opts.accept || stamped) {
|
|
14746
|
-
|
|
14882
|
+
fs25.writeFileSync(opts.configPath, JSON.stringify(working, null, 2) + "\n");
|
|
14747
14883
|
}
|
|
14748
14884
|
return { items, applied, remaining, stamped };
|
|
14749
14885
|
}
|
|
@@ -14814,7 +14950,7 @@ function printItems(items) {
|
|
|
14814
14950
|
var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
|
|
14815
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) => {
|
|
14816
14952
|
const pkgVersion = readPkgVersion2();
|
|
14817
|
-
if (!
|
|
14953
|
+
if (!fs25.existsSync(DEFAULT_CONFIG_PATH)) {
|
|
14818
14954
|
console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
|
|
14819
14955
|
return;
|
|
14820
14956
|
}
|
|
@@ -14859,8 +14995,8 @@ configCommand.command("set").description("Write a config value by dotted key (e.
|
|
|
14859
14995
|
}
|
|
14860
14996
|
});
|
|
14861
14997
|
function readPkgVersion2() {
|
|
14862
|
-
const pkgPath =
|
|
14863
|
-
return JSON.parse(
|
|
14998
|
+
const pkgPath = join26(dirname6(fileURLToPath5(import.meta.url)), "..", "package.json");
|
|
14999
|
+
return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version;
|
|
14864
15000
|
}
|
|
14865
15001
|
|
|
14866
15002
|
// packages/cli/src/commands/heal.ts
|
|
@@ -14945,9 +15081,9 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
|
|
|
14945
15081
|
process.exit(code);
|
|
14946
15082
|
})
|
|
14947
15083
|
).addCommand(
|
|
14948
|
-
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 () => {
|
|
14949
15085
|
const code = await runHealDaemon({
|
|
14950
|
-
ensureDaemon: () =>
|
|
15086
|
+
ensureDaemon: () => reregisterDaemon(),
|
|
14951
15087
|
stdout: process.stdout,
|
|
14952
15088
|
stderr: process.stderr
|
|
14953
15089
|
});
|
|
@@ -15013,7 +15149,7 @@ var groupCommand = new Command26("group").description("Cross-project intra-group
|
|
|
15013
15149
|
// packages/cli/src/commands/ping.ts
|
|
15014
15150
|
init_dist();
|
|
15015
15151
|
init_dist2();
|
|
15016
|
-
import { join as
|
|
15152
|
+
import { join as join27, dirname as dirname7 } from "path";
|
|
15017
15153
|
import { Command as Command27 } from "commander";
|
|
15018
15154
|
import chalk27 from "chalk";
|
|
15019
15155
|
init_require_daemon();
|
|
@@ -15023,7 +15159,7 @@ async function runPing(project, message) {
|
|
|
15023
15159
|
const resolved = resolveTarget(registry, config, project, false);
|
|
15024
15160
|
await requireDaemon();
|
|
15025
15161
|
await needRef(resolved);
|
|
15026
|
-
const stateRoot =
|
|
15162
|
+
const stateRoot = join27(dirname7(DEFAULT_CONFIG_PATH), "state");
|
|
15027
15163
|
await appendCaptainMessage({
|
|
15028
15164
|
stateRoot,
|
|
15029
15165
|
project,
|
|
@@ -15100,8 +15236,8 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
|
|
|
15100
15236
|
// packages/cli/src/commands/effort.ts
|
|
15101
15237
|
init_dist();
|
|
15102
15238
|
init_dist2();
|
|
15103
|
-
import
|
|
15104
|
-
import
|
|
15239
|
+
import fs26 from "fs";
|
|
15240
|
+
import path27 from "path";
|
|
15105
15241
|
import { Command as Command29 } from "commander";
|
|
15106
15242
|
import chalk29 from "chalk";
|
|
15107
15243
|
var VALID_EFFORTS = ["max", "balance", "low"];
|
|
@@ -15144,9 +15280,9 @@ function effortScopeLabel(projectName) {
|
|
|
15144
15280
|
}
|
|
15145
15281
|
function canonical(p) {
|
|
15146
15282
|
try {
|
|
15147
|
-
return
|
|
15283
|
+
return fs26.realpathSync(p);
|
|
15148
15284
|
} catch {
|
|
15149
|
-
return
|
|
15285
|
+
return path27.resolve(p);
|
|
15150
15286
|
}
|
|
15151
15287
|
}
|
|
15152
15288
|
async function notifyCaptainsOfEffort(effort, config, driver, cwd = process.cwd(), append, scopeProject, projectConfigRoot) {
|
|
@@ -15196,7 +15332,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
|
|
|
15196
15332
|
const config = loadConfig();
|
|
15197
15333
|
const registry = new RuntimeRegistry2({ cmux: createCmuxDriver2() });
|
|
15198
15334
|
const driver = registry.global(config);
|
|
15199
|
-
const stateRoot =
|
|
15335
|
+
const stateRoot = path27.join(path27.dirname(DEFAULT_CONFIG_PATH), "state");
|
|
15200
15336
|
const append = (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
|
|
15201
15337
|
await notifyCaptainsOfEffort(effort, config, driver, process.cwd(), append, options.project);
|
|
15202
15338
|
} catch {
|
|
@@ -15204,18 +15340,308 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
|
|
|
15204
15340
|
}
|
|
15205
15341
|
});
|
|
15206
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
|
+
|
|
15207
15633
|
// packages/cli/src/commands/telegram.ts
|
|
15208
15634
|
init_dist();
|
|
15209
15635
|
init_dist2();
|
|
15210
|
-
import { join as
|
|
15636
|
+
import { join as join28, dirname as dirname8 } from "path";
|
|
15211
15637
|
import { emitKeypressEvents } from "readline";
|
|
15212
|
-
import { Command as
|
|
15213
|
-
import
|
|
15638
|
+
import { Command as Command31 } from "commander";
|
|
15639
|
+
import chalk31 from "chalk";
|
|
15214
15640
|
function defaultStateRoot() {
|
|
15215
|
-
return
|
|
15641
|
+
return join28(dirname8(DEFAULT_CONFIG_PATH), "state");
|
|
15216
15642
|
}
|
|
15217
15643
|
async function questionMasked() {
|
|
15218
|
-
return new Promise((
|
|
15644
|
+
return new Promise((resolve4) => {
|
|
15219
15645
|
emitKeypressEvents(process.stdin);
|
|
15220
15646
|
process.stdin.setRawMode(true);
|
|
15221
15647
|
process.stdin.resume();
|
|
@@ -15232,7 +15658,7 @@ async function questionMasked() {
|
|
|
15232
15658
|
process.stdin.setRawMode(false);
|
|
15233
15659
|
process.stdin.pause();
|
|
15234
15660
|
process.stdout.write("\n");
|
|
15235
|
-
|
|
15661
|
+
resolve4(answer);
|
|
15236
15662
|
} else if (key.name === "backspace") {
|
|
15237
15663
|
if (answer.length > 0) {
|
|
15238
15664
|
answer = answer.slice(0, -1);
|
|
@@ -15249,19 +15675,19 @@ async function questionMasked() {
|
|
|
15249
15675
|
async function questionYesNo(prompt) {
|
|
15250
15676
|
const { createInterface: createInterface2 } = await import("readline");
|
|
15251
15677
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
15252
|
-
return new Promise((
|
|
15678
|
+
return new Promise((resolve4) => {
|
|
15253
15679
|
rl.question(prompt, (ans) => {
|
|
15254
15680
|
rl.close();
|
|
15255
15681
|
process.stdin.pause();
|
|
15256
|
-
|
|
15682
|
+
resolve4(/^y(es)?$/i.test(ans.trim()));
|
|
15257
15683
|
});
|
|
15258
15684
|
});
|
|
15259
15685
|
}
|
|
15260
|
-
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");
|
|
15261
15687
|
telegramCommand.command("status").description("Show Telegram config and linked projects").action(() => {
|
|
15262
15688
|
const { tokenSet, supergroupId, links } = runTelegramStatus({ config: loadConfig(), stateRoot: defaultStateRoot() });
|
|
15263
|
-
console.log(`token: ${tokenSet ?
|
|
15264
|
-
console.log(`supergroup: ${supergroupId ??
|
|
15689
|
+
console.log(`token: ${tokenSet ? chalk31.green("set") : chalk31.yellow("unset")}`);
|
|
15690
|
+
console.log(`supergroup: ${supergroupId ?? chalk31.yellow("unset")}`);
|
|
15265
15691
|
if (links.length === 0) {
|
|
15266
15692
|
console.log("no projects linked");
|
|
15267
15693
|
return;
|
|
@@ -15271,32 +15697,32 @@ telegramCommand.command("status").description("Show Telegram config and linked p
|
|
|
15271
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) => {
|
|
15272
15698
|
const cfg = loadConfig().telegram;
|
|
15273
15699
|
if (!cfg) {
|
|
15274
|
-
console.error(
|
|
15700
|
+
console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
|
|
15275
15701
|
process.exit(1);
|
|
15276
15702
|
}
|
|
15277
15703
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15278
15704
|
if (!token) {
|
|
15279
|
-
console.error(
|
|
15705
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15280
15706
|
process.exit(1);
|
|
15281
15707
|
}
|
|
15282
15708
|
const client = createTelegramClient({ token });
|
|
15283
15709
|
const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
|
|
15284
|
-
console.log(
|
|
15710
|
+
console.log(chalk31.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
|
|
15285
15711
|
});
|
|
15286
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) => {
|
|
15287
15713
|
if (!process.stdin.isTTY) {
|
|
15288
|
-
console.error(
|
|
15714
|
+
console.error(chalk31.red("setup requires a TTY \u2014 pipe input is not supported"));
|
|
15289
15715
|
process.exit(1);
|
|
15290
15716
|
}
|
|
15291
15717
|
console.log();
|
|
15292
|
-
console.log(
|
|
15718
|
+
console.log(chalk31.bold("Telegram setup") + " \u2014 connect squadrant to a Telegram bot for notifications + remote control");
|
|
15293
15719
|
console.log();
|
|
15294
15720
|
console.log("Before you start you need:");
|
|
15295
15721
|
console.log(" 1. A bot token from @BotFather (send /newbot)");
|
|
15296
15722
|
console.log(" 2. A forum supergroup with the bot added as an admin (Topics enabled)");
|
|
15297
15723
|
console.log(" 3. Bot privacy mode set to OFF (@BotFather \u2192 /setprivacy \u2192 Disable)");
|
|
15298
15724
|
console.log();
|
|
15299
|
-
console.log(
|
|
15725
|
+
console.log(chalk31.bold("Step 1/3 \u2014 Bot token"));
|
|
15300
15726
|
const existingCfg = loadConfig().telegram;
|
|
15301
15727
|
const existingToken = existingCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15302
15728
|
const decision = resolveSetupToken(existingToken, { resetToken: opts.resetToken ?? false });
|
|
@@ -15308,67 +15734,67 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15308
15734
|
try {
|
|
15309
15735
|
botUser = await client.getMe();
|
|
15310
15736
|
token = existingToken;
|
|
15311
|
-
console.log(
|
|
15737
|
+
console.log(chalk31.green(`Using existing bot token (@${botUser.username})`));
|
|
15312
15738
|
console.log();
|
|
15313
15739
|
} catch {
|
|
15314
|
-
console.log(
|
|
15740
|
+
console.log(chalk31.yellow("Existing token is invalid \u2014 please enter a new one."));
|
|
15315
15741
|
console.log("Paste your bot token then press Enter (input is hidden):");
|
|
15316
15742
|
token = await questionMasked();
|
|
15317
15743
|
if (!token) {
|
|
15318
|
-
console.error(
|
|
15744
|
+
console.error(chalk31.red("token required"));
|
|
15319
15745
|
process.exit(1);
|
|
15320
15746
|
}
|
|
15321
15747
|
client = createTelegramClient({ token });
|
|
15322
15748
|
try {
|
|
15323
15749
|
botUser = await client.getMe();
|
|
15324
15750
|
} catch (e) {
|
|
15325
|
-
console.error(
|
|
15751
|
+
console.error(chalk31.red(`token rejected: ${e.message}`));
|
|
15326
15752
|
process.exit(1);
|
|
15327
15753
|
}
|
|
15328
|
-
console.log(
|
|
15754
|
+
console.log(chalk31.green(`Connected as @${botUser.username}`));
|
|
15329
15755
|
console.log();
|
|
15330
15756
|
}
|
|
15331
15757
|
} else {
|
|
15332
15758
|
console.log("Paste your bot token then press Enter (input is hidden):");
|
|
15333
15759
|
token = await questionMasked();
|
|
15334
15760
|
if (!token) {
|
|
15335
|
-
console.error(
|
|
15761
|
+
console.error(chalk31.red("token required"));
|
|
15336
15762
|
process.exit(1);
|
|
15337
15763
|
}
|
|
15338
15764
|
client = createTelegramClient({ token });
|
|
15339
15765
|
try {
|
|
15340
15766
|
botUser = await client.getMe();
|
|
15341
15767
|
} catch (e) {
|
|
15342
|
-
console.error(
|
|
15768
|
+
console.error(chalk31.red(`token rejected: ${e.message}`));
|
|
15343
15769
|
process.exit(1);
|
|
15344
15770
|
}
|
|
15345
|
-
console.log(
|
|
15771
|
+
console.log(chalk31.green(`Connected as @${botUser.username}`));
|
|
15346
15772
|
console.log();
|
|
15347
15773
|
}
|
|
15348
|
-
console.log(
|
|
15774
|
+
console.log(chalk31.bold("Step 2/3 \u2014 Supergroup"));
|
|
15349
15775
|
const groupDecision = resolveSetupGroup(existingCfg?.supergroupId, { redetect: opts.redetect ?? false });
|
|
15350
15776
|
let supergroupId;
|
|
15351
15777
|
let detectedUserId;
|
|
15352
15778
|
if (groupDecision === "reuse") {
|
|
15353
15779
|
supergroupId = existingCfg.supergroupId;
|
|
15354
|
-
console.log(
|
|
15780
|
+
console.log(chalk31.green(`Using existing group: ${supergroupId}`));
|
|
15355
15781
|
console.log();
|
|
15356
15782
|
} else {
|
|
15357
15783
|
console.log("Add the bot to your forum supergroup, then send any message in it.");
|
|
15358
|
-
console.log(
|
|
15784
|
+
console.log(chalk31.dim("Waiting for a message (up to 60s)\u2026"));
|
|
15359
15785
|
try {
|
|
15360
15786
|
({ supergroupId, userId: detectedUserId } = await detectGroupAndUser(client, { timeoutMs: 6e4 }));
|
|
15361
15787
|
} catch {
|
|
15362
|
-
console.error(
|
|
15363
|
-
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"));
|
|
15364
15790
|
process.exit(1);
|
|
15365
15791
|
}
|
|
15366
|
-
console.log(
|
|
15792
|
+
console.log(chalk31.green(`Found group: ${supergroupId}`));
|
|
15367
15793
|
console.log();
|
|
15368
15794
|
}
|
|
15369
|
-
console.log(
|
|
15370
|
-
console.log(
|
|
15371
|
-
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)."));
|
|
15372
15798
|
const finalUserId = resolveSetupUserId(opts.userId, detectedUserId, defaultStateRoot());
|
|
15373
15799
|
let users;
|
|
15374
15800
|
let remoteControl;
|
|
@@ -15382,32 +15808,32 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15382
15808
|
remoteControl = true;
|
|
15383
15809
|
}
|
|
15384
15810
|
} else if (groupDecision === "detect") {
|
|
15385
|
-
console.log(
|
|
15386
|
-
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."));
|
|
15387
15813
|
printedRemoteControlState = true;
|
|
15388
15814
|
} else {
|
|
15389
15815
|
const existingUsers = existingCfg?.users;
|
|
15390
15816
|
if (existingUsers && existingUsers.length > 0) {
|
|
15391
|
-
console.log(
|
|
15817
|
+
console.log(chalk31.dim(`Remote control: already configured (user-id ${existingUsers[0]}). Use --user-id to update.`));
|
|
15392
15818
|
} else {
|
|
15393
|
-
console.log(
|
|
15819
|
+
console.log(chalk31.dim("Remote control: off. Re-run with --user-id <id> to enable."));
|
|
15394
15820
|
}
|
|
15395
15821
|
printedRemoteControlState = true;
|
|
15396
15822
|
}
|
|
15397
15823
|
writeTelegramConfig(DEFAULT_CONFIG_PATH, { token, supergroupId, users, remoteControl });
|
|
15398
|
-
console.log(
|
|
15824
|
+
console.log(chalk31.green(`Wrote telegram config \u2014 token: ${maskToken(token)} group: ${supergroupId}`));
|
|
15399
15825
|
if (!printedRemoteControlState) {
|
|
15400
15826
|
if (remoteControl) {
|
|
15401
|
-
console.log(
|
|
15827
|
+
console.log(chalk31.green(`Remote control: ON (allowlisted user-id ${users[0]})`));
|
|
15402
15828
|
} else {
|
|
15403
|
-
console.log(
|
|
15829
|
+
console.log(chalk31.dim("Remote control: off (default). Re-run with --user-id <id> to enable."));
|
|
15404
15830
|
}
|
|
15405
15831
|
}
|
|
15406
15832
|
try {
|
|
15407
15833
|
await runRegisterCommands({ client });
|
|
15408
|
-
console.log(
|
|
15834
|
+
console.log(chalk31.dim("Registered the /command menu."));
|
|
15409
15835
|
} catch (e) {
|
|
15410
|
-
console.log(
|
|
15836
|
+
console.log(chalk31.yellow(`command-menu registration skipped: ${e.message}`));
|
|
15411
15837
|
}
|
|
15412
15838
|
const topics = loadState(defaultStateRoot()).topics;
|
|
15413
15839
|
const topicEntries = Object.entries(topics);
|
|
@@ -15416,28 +15842,28 @@ telegramCommand.command("setup").description("Interactive wizard \u2014 bot toke
|
|
|
15416
15842
|
const project = key.slice(0, key.indexOf("::"));
|
|
15417
15843
|
return `${project}\u2192${id}`;
|
|
15418
15844
|
}).join(", ");
|
|
15419
|
-
console.log(
|
|
15845
|
+
console.log(chalk31.dim(`Existing topics: ${summary} (already created \u2014 not recreated)`));
|
|
15420
15846
|
} else {
|
|
15421
|
-
console.log(
|
|
15847
|
+
console.log(chalk31.dim("No project topics yet \u2014 they're created on first delivery or via: squadrant telegram link <project>"));
|
|
15422
15848
|
}
|
|
15423
15849
|
runTelegramPostSetup({});
|
|
15424
15850
|
console.log();
|
|
15425
|
-
console.log(`Next: ${
|
|
15851
|
+
console.log(`Next: ${chalk31.cyan("squadrant telegram link <project>")}`);
|
|
15426
15852
|
});
|
|
15427
15853
|
telegramCommand.command("register-commands").description("Register (or re-register) the bot's / command menu with Telegram").action(async () => {
|
|
15428
15854
|
const cfg = loadConfig().telegram;
|
|
15429
15855
|
if (!cfg) {
|
|
15430
|
-
console.error(
|
|
15856
|
+
console.error(chalk31.red("telegram config absent \u2014 run: squadrant telegram setup"));
|
|
15431
15857
|
process.exit(1);
|
|
15432
15858
|
}
|
|
15433
15859
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15434
15860
|
if (!token) {
|
|
15435
|
-
console.error(
|
|
15861
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15436
15862
|
process.exit(1);
|
|
15437
15863
|
}
|
|
15438
15864
|
const client = createTelegramClient({ token });
|
|
15439
15865
|
await runRegisterCommands({ client });
|
|
15440
|
-
console.log(
|
|
15866
|
+
console.log(chalk31.green(`registered ${BOT_COMMANDS.length} bot commands`));
|
|
15441
15867
|
});
|
|
15442
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) => {
|
|
15443
15869
|
const stateRoot = defaultStateRoot();
|
|
@@ -15448,7 +15874,7 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
|
|
|
15448
15874
|
return;
|
|
15449
15875
|
}
|
|
15450
15876
|
for (const r of rows) {
|
|
15451
|
-
console.log(` ${r.project}: ${r.active ?
|
|
15877
|
+
console.log(` ${r.project}: ${r.active ? chalk31.green("on") : chalk31.dim("off (muted)")}`);
|
|
15452
15878
|
}
|
|
15453
15879
|
return;
|
|
15454
15880
|
}
|
|
@@ -15457,53 +15883,53 @@ telegramCommand.command("notify").argument("[project]", "project to toggle").arg
|
|
|
15457
15883
|
const token = tgCfg?.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15458
15884
|
if (state === "crew" || state === "cap") {
|
|
15459
15885
|
if (value === void 0) {
|
|
15460
|
-
console.error(
|
|
15886
|
+
console.error(chalk31.red(`usage: squadrant telegram notify <project> ${state} <value>`));
|
|
15461
15887
|
process.exit(1);
|
|
15462
15888
|
}
|
|
15463
15889
|
const resolved2 = resolveNotify(globalNotify, loadProjectOverride(project));
|
|
15464
15890
|
const before2 = { ...resolved2, active: isNotifyActive(stateRoot, project) };
|
|
15465
15891
|
const res = runTelegramNotifyPref({ project, dimension: state, value });
|
|
15466
15892
|
if (!res.ok) {
|
|
15467
|
-
console.error(
|
|
15893
|
+
console.error(chalk31.red(res.message));
|
|
15468
15894
|
process.exit(1);
|
|
15469
15895
|
}
|
|
15470
|
-
console.log(
|
|
15896
|
+
console.log(chalk31.green(`${project} ${state} = ${value}`));
|
|
15471
15897
|
const after2 = state === "crew" ? { ...before2, crew: value } : { ...before2, cap: value === "on" };
|
|
15472
15898
|
if (tgCfg && token) {
|
|
15473
15899
|
const client = createTelegramClient({ token });
|
|
15474
15900
|
const sent = await runNotifyConfirmation({ project, before: before2, after: after2, cfg: tgCfg, client, stateRoot });
|
|
15475
|
-
if (sent) console.log(
|
|
15901
|
+
if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
|
|
15476
15902
|
}
|
|
15477
15903
|
return;
|
|
15478
15904
|
}
|
|
15479
15905
|
if (state !== "on" && state !== "off") {
|
|
15480
|
-
console.error(
|
|
15906
|
+
console.error(chalk31.red("usage: squadrant telegram notify <project> <on|off|crew <tier>|cap <on|off>>"));
|
|
15481
15907
|
process.exit(1);
|
|
15482
15908
|
}
|
|
15483
15909
|
const resolved = resolveNotify(globalNotify, loadProjectOverride(project));
|
|
15484
15910
|
const before = { ...resolved, active: isNotifyActive(stateRoot, project) };
|
|
15485
15911
|
const after = { ...before, active: state === "on" };
|
|
15486
15912
|
runTelegramNotifySet({ project, active: state === "on", stateRoot });
|
|
15487
|
-
console.log(
|
|
15913
|
+
console.log(chalk31.green(`${project} notifications ${state === "on" ? "ON" : "OFF"}`));
|
|
15488
15914
|
if (tgCfg && token) {
|
|
15489
15915
|
const client = createTelegramClient({ token });
|
|
15490
15916
|
const sent = await runNotifyConfirmation({ project, before, after, cfg: tgCfg, client, stateRoot });
|
|
15491
|
-
if (sent) console.log(
|
|
15917
|
+
if (sent) console.log(chalk31.dim(`\u2192 notified ${project} topic`));
|
|
15492
15918
|
}
|
|
15493
15919
|
});
|
|
15494
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) => {
|
|
15495
15921
|
const cfg = loadConfig().telegram;
|
|
15496
15922
|
if (!cfg) {
|
|
15497
|
-
console.error(
|
|
15923
|
+
console.error(chalk31.red("telegram config absent \u2014 add a `telegram` block to ~/.config/squadrant/config.json"));
|
|
15498
15924
|
process.exit(1);
|
|
15499
15925
|
}
|
|
15500
15926
|
const token = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
15501
15927
|
if (!token) {
|
|
15502
|
-
console.error(
|
|
15928
|
+
console.error(chalk31.red("no botToken in config and TELEGRAM_BOT_TOKEN is unset"));
|
|
15503
15929
|
process.exit(1);
|
|
15504
15930
|
}
|
|
15505
15931
|
if (!capAllowed(project, cfg.notify)) {
|
|
15506
|
-
console.log(
|
|
15932
|
+
console.log(chalk31.dim(`${project}: captain messages muted (cap=off) \u2014 not sent`));
|
|
15507
15933
|
return;
|
|
15508
15934
|
}
|
|
15509
15935
|
let message;
|
|
@@ -15516,19 +15942,19 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
|
|
|
15516
15942
|
for await (const line of rl) lines.push(line);
|
|
15517
15943
|
message = lines.join("\n").trimEnd();
|
|
15518
15944
|
if (!message) {
|
|
15519
|
-
console.error(
|
|
15945
|
+
console.error(chalk31.red("no message provided (stdin was empty)"));
|
|
15520
15946
|
process.exit(1);
|
|
15521
15947
|
}
|
|
15522
15948
|
} else {
|
|
15523
|
-
console.error(
|
|
15949
|
+
console.error(chalk31.red("message required \u2014 pass as argument or pipe via stdin"));
|
|
15524
15950
|
process.exit(1);
|
|
15525
15951
|
}
|
|
15526
15952
|
const client = createTelegramClient({ token });
|
|
15527
15953
|
try {
|
|
15528
15954
|
const { chatId, topicId } = await runTelegramSend({ project, message, cfg, client, stateRoot: defaultStateRoot() });
|
|
15529
|
-
console.log(
|
|
15955
|
+
console.log(chalk31.green(`sent to group ${chatId} topic ${topicId}`));
|
|
15530
15956
|
} catch (e) {
|
|
15531
|
-
console.error(
|
|
15957
|
+
console.error(chalk31.red(e.message));
|
|
15532
15958
|
process.exit(1);
|
|
15533
15959
|
}
|
|
15534
15960
|
});
|
|
@@ -15536,10 +15962,92 @@ telegramCommand.command("send").argument("<project>", "project whose topic recei
|
|
|
15536
15962
|
// packages/cli/src/commands/hooks.ts
|
|
15537
15963
|
init_dist2();
|
|
15538
15964
|
init_dist4();
|
|
15539
|
-
|
|
15540
|
-
import {
|
|
15541
|
-
import {
|
|
15542
|
-
|
|
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");
|
|
15543
16051
|
async function sendToSock(req) {
|
|
15544
16052
|
await sendRequest(SOCK4, req);
|
|
15545
16053
|
}
|
|
@@ -15562,14 +16070,31 @@ function mapHookSub(sub, payload, taskId) {
|
|
|
15562
16070
|
return null;
|
|
15563
16071
|
}
|
|
15564
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
|
+
}
|
|
15565
16095
|
function hooksCommand() {
|
|
15566
|
-
const hooks = new
|
|
16096
|
+
const hooks = new Command32("hooks").description("(internal) receive lifecycle hook events from agent processes");
|
|
15567
16097
|
hooks.command("claude <sub>", { hidden: true }).description("internal: bridge a NativeHookSource claude hook to squadrantd").action(async (sub) => {
|
|
15568
|
-
const taskId = process.env.SQUADRANT_CREW_TASK_ID;
|
|
15569
|
-
const project = process.env.SQUADRANT_CREW_PROJECT;
|
|
15570
|
-
if (!taskId || !project) {
|
|
15571
|
-
process.exit(0);
|
|
15572
|
-
}
|
|
15573
16098
|
let stdin = "";
|
|
15574
16099
|
try {
|
|
15575
16100
|
for await (const chunk of process.stdin) stdin += chunk;
|
|
@@ -15582,6 +16107,14 @@ function hooksCommand() {
|
|
|
15582
16107
|
} catch {
|
|
15583
16108
|
}
|
|
15584
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
|
+
}
|
|
15585
16118
|
const ev = mapHookSub(sub, payload, taskId);
|
|
15586
16119
|
if (!ev) {
|
|
15587
16120
|
process.exit(0);
|
|
@@ -15595,26 +16128,554 @@ function hooksCommand() {
|
|
|
15595
16128
|
return hooks;
|
|
15596
16129
|
}
|
|
15597
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 Math.max(0, 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 branchState = gatherBranchState(runner, projectPath, branch, baseBranch, detached, fetch2);
|
|
16433
|
+
const fetchAgeMs = readFetchAgeMs(projectPath, now);
|
|
16434
|
+
let aheadOfBase = 0;
|
|
16435
|
+
let aheadOfBaseSource = "unknown";
|
|
16436
|
+
if (branch === baseBranch) {
|
|
16437
|
+
aheadOfBase = null;
|
|
16438
|
+
aheadOfBaseSource = "n-a";
|
|
16439
|
+
} else {
|
|
16440
|
+
if (ghInfo && !detached) {
|
|
16441
|
+
const ghAhead = ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, baseBranch, branch);
|
|
16442
|
+
if (ghAhead !== null) {
|
|
16443
|
+
aheadOfBase = ghAhead;
|
|
16444
|
+
aheadOfBaseSource = "gh-api";
|
|
16445
|
+
}
|
|
16446
|
+
}
|
|
16447
|
+
if (aheadOfBaseSource === "unknown") {
|
|
16448
|
+
const localAhead = localAheadOfBase(runner, projectPath, baseBranch);
|
|
16449
|
+
if (localAhead !== null) {
|
|
16450
|
+
aheadOfBase = localAhead;
|
|
16451
|
+
aheadOfBaseSource = "local-git";
|
|
16452
|
+
}
|
|
16453
|
+
}
|
|
16454
|
+
}
|
|
16455
|
+
const unreleasedAheadOfReleaseBranch = ghInfo && baseBranch !== RELEASE_BRANCH ? ghAheadOfBase(runner, projectPath, ghInfo.nameWithOwner, RELEASE_BRANCH, baseBranch) : null;
|
|
16456
|
+
const conflicts = [];
|
|
16457
|
+
if (ghInfo) {
|
|
16458
|
+
const ghSha = ghBaseSha(runner, projectPath, ghInfo.nameWithOwner, baseBranch);
|
|
16459
|
+
const localSha = localBaseSha(runner, projectPath, baseBranch);
|
|
16460
|
+
if (ghSha && localSha && ghSha !== localSha) {
|
|
16461
|
+
const ageNote = fetchAgeMs !== null ? `fetched ${Math.round(fetchAgeMs / 36e5)}h ago` : "fetch age unknown";
|
|
16462
|
+
conflicts.push({
|
|
16463
|
+
field: "baseBranch",
|
|
16464
|
+
claim: `local git's last-known ${baseBranch} is ${localSha} (${ageNote})`,
|
|
16465
|
+
fact: `GitHub's live ${baseBranch} is ${ghSha}`,
|
|
16466
|
+
resolution: "GitHub API wins \u2014 local git may be stale"
|
|
16467
|
+
});
|
|
16468
|
+
}
|
|
16469
|
+
}
|
|
16470
|
+
return {
|
|
16471
|
+
branch,
|
|
16472
|
+
detached,
|
|
16473
|
+
baseBranch,
|
|
16474
|
+
baseBranchSource,
|
|
16475
|
+
recentCommits,
|
|
16476
|
+
aheadOfBase,
|
|
16477
|
+
aheadOfBaseSource,
|
|
16478
|
+
fetchAgeMs,
|
|
16479
|
+
openPRs: gatherOpenPRs(runner, projectPath),
|
|
16480
|
+
liveCrews: gatherLiveCrews(tasks),
|
|
16481
|
+
conflicts,
|
|
16482
|
+
branchState,
|
|
16483
|
+
unreleasedAheadOfReleaseBranch
|
|
16484
|
+
};
|
|
16485
|
+
}
|
|
16486
|
+
|
|
16487
|
+
// packages/cli/src/lib/handoff-claude-mem.ts
|
|
16488
|
+
import { createRequire } from "module";
|
|
16489
|
+
import fs30 from "fs";
|
|
16490
|
+
var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
16491
|
+
var CLAUDE_MEM_RECENCY_LIMIT = 20;
|
|
16492
|
+
function decisionText(row) {
|
|
16493
|
+
if (row.facts) {
|
|
16494
|
+
try {
|
|
16495
|
+
const parsed = JSON.parse(row.facts);
|
|
16496
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed.join("; ");
|
|
16497
|
+
} catch {
|
|
16498
|
+
}
|
|
16499
|
+
}
|
|
16500
|
+
return row.narrative ?? "";
|
|
16501
|
+
}
|
|
16502
|
+
function queryClaudeMem(dbPath, project) {
|
|
16503
|
+
if (!fs30.existsSync(dbPath)) return null;
|
|
16504
|
+
let db;
|
|
16505
|
+
try {
|
|
16506
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
16507
|
+
} catch {
|
|
16508
|
+
return null;
|
|
16509
|
+
}
|
|
16510
|
+
try {
|
|
16511
|
+
const summaryRow = db.prepare(
|
|
16512
|
+
`SELECT request, completed, next_steps, created_at FROM session_summaries
|
|
16513
|
+
WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1`
|
|
16514
|
+
).get(project);
|
|
16515
|
+
const decisionRows = db.prepare(
|
|
16516
|
+
`SELECT title, narrative, facts, created_at FROM observations
|
|
16517
|
+
WHERE project = ? AND type = 'decision' ORDER BY created_at_epoch DESC LIMIT ?`
|
|
16518
|
+
).all(project, CLAUDE_MEM_RECENCY_LIMIT);
|
|
16519
|
+
const recentDecisions = decisionRows.map((r) => ({
|
|
16520
|
+
title: r.title,
|
|
16521
|
+
text: decisionText(r),
|
|
16522
|
+
createdAt: r.created_at
|
|
16523
|
+
}));
|
|
16524
|
+
const candidates = [summaryRow?.created_at, ...decisionRows.map((r) => r.created_at)].filter(
|
|
16525
|
+
(v) => !!v
|
|
16526
|
+
);
|
|
16527
|
+
const oldestCreatedAt = candidates.length > 0 ? candidates.reduce((a, b) => a < b ? a : b) : null;
|
|
16528
|
+
return {
|
|
16529
|
+
latestSessionSummary: summaryRow ? {
|
|
16530
|
+
request: summaryRow.request,
|
|
16531
|
+
completed: summaryRow.completed,
|
|
16532
|
+
nextSteps: summaryRow.next_steps,
|
|
16533
|
+
createdAt: summaryRow.created_at
|
|
16534
|
+
} : null,
|
|
16535
|
+
recentDecisions,
|
|
16536
|
+
oldestCreatedAt
|
|
16537
|
+
};
|
|
16538
|
+
} catch {
|
|
16539
|
+
return null;
|
|
16540
|
+
} finally {
|
|
16541
|
+
db.close();
|
|
16542
|
+
}
|
|
16543
|
+
}
|
|
16544
|
+
|
|
16545
|
+
// packages/cli/src/lib/handoff-transcript.ts
|
|
16546
|
+
import fs31 from "fs";
|
|
16547
|
+
var TRANSCRIPT_BYTE_CAP = 2e5;
|
|
16548
|
+
function tailOf(content, byteCap) {
|
|
16549
|
+
const buf = Buffer.from(content, "utf-8");
|
|
16550
|
+
if (buf.length <= byteCap) return content;
|
|
16551
|
+
const text = buf.subarray(buf.length - byteCap).toString("utf-8");
|
|
16552
|
+
return text.split("\n").slice(1).join("\n");
|
|
16553
|
+
}
|
|
16554
|
+
function extractMessages(tailText) {
|
|
16555
|
+
let lastUserMessage = null;
|
|
16556
|
+
let lastAssistantText = null;
|
|
16557
|
+
for (const line of tailText.split("\n")) {
|
|
16558
|
+
if (!line.trim()) continue;
|
|
16559
|
+
let obj;
|
|
16560
|
+
try {
|
|
16561
|
+
obj = JSON.parse(line);
|
|
16562
|
+
} catch {
|
|
16563
|
+
continue;
|
|
16564
|
+
}
|
|
16565
|
+
if (obj.type === "user" && typeof obj.message?.content === "string") {
|
|
16566
|
+
lastUserMessage = obj.message.content;
|
|
16567
|
+
} else if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
16568
|
+
const texts = obj.message.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
|
|
16569
|
+
if (texts.length > 0) lastAssistantText = texts.join("\n");
|
|
16570
|
+
}
|
|
16571
|
+
}
|
|
16572
|
+
return { lastUserMessage, lastAssistantText };
|
|
16573
|
+
}
|
|
16574
|
+
function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
|
|
16575
|
+
if (!fs31.existsSync(transcriptPath)) return null;
|
|
16576
|
+
const content = fs31.readFileSync(transcriptPath, "utf-8");
|
|
16577
|
+
const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
|
|
16578
|
+
const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
|
|
16579
|
+
return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
|
|
16580
|
+
}
|
|
16581
|
+
|
|
16582
|
+
// packages/cli/src/lib/handoff-archive.ts
|
|
16583
|
+
import fs32 from "fs";
|
|
16584
|
+
import path32 from "path";
|
|
16585
|
+
function readNewestArchivedHandoff(spokeVault, now) {
|
|
16586
|
+
const dir = path32.join(spokeVault, "handoffs");
|
|
16587
|
+
if (!fs32.existsSync(dir)) return null;
|
|
16588
|
+
const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
|
|
16589
|
+
const full = path32.join(dir, e.name);
|
|
16590
|
+
return { name: e.name, full, mtime: fs32.statSync(full).mtime };
|
|
16591
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
16592
|
+
for (const candidate of candidates) {
|
|
16593
|
+
let content;
|
|
16594
|
+
try {
|
|
16595
|
+
content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
|
|
16596
|
+
} catch {
|
|
16597
|
+
continue;
|
|
16598
|
+
}
|
|
16599
|
+
return { filename: candidate.name, path: candidate.full, ageMs: now - candidate.mtime.getTime(), content };
|
|
16600
|
+
}
|
|
16601
|
+
return null;
|
|
16602
|
+
}
|
|
16603
|
+
|
|
16604
|
+
// packages/cli/src/commands/handoff.ts
|
|
16605
|
+
var CLAUDE_MEM_DB_PATH = path33.join(os16.homedir(), ".claude-mem", "claude-mem.db");
|
|
16606
|
+
async function defaultFetchTasks(project) {
|
|
16607
|
+
return await squadrantdCall({ kind: "list", project });
|
|
16608
|
+
}
|
|
16609
|
+
async function runHandoffFacts(project, deps = {}) {
|
|
16610
|
+
const config = loadConfig();
|
|
16611
|
+
const proj = config.projects[project];
|
|
16612
|
+
if (!proj) {
|
|
16613
|
+
throw new Error(`Project '${project}' not found. Run 'squadrant projects list'.`);
|
|
16614
|
+
}
|
|
16615
|
+
const fallbackBaseBranch = resolveWorktreeBase(proj.path);
|
|
16616
|
+
let tasks;
|
|
16617
|
+
try {
|
|
16618
|
+
tasks = await (deps.fetchTasks ?? defaultFetchTasks)(project);
|
|
16619
|
+
} catch {
|
|
16620
|
+
tasks = [];
|
|
16621
|
+
}
|
|
16622
|
+
const nowIso = deps.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16623
|
+
const nowMs = Date.parse(nowIso);
|
|
16624
|
+
const fallbackWindowMs = deps.windowMs ?? SESSION_WINDOW_MS;
|
|
16625
|
+
const live = gatherLiveRepoState(proj.path, fallbackBaseBranch, tasks, deps.runner ?? defaultCommandRunner, nowMs, deps.fetch ?? false);
|
|
16626
|
+
const claudeMem = queryClaudeMem(deps.claudeMemDbPath ?? CLAUDE_MEM_DB_PATH, project);
|
|
16627
|
+
const checkpoint = readNewestArchivedHandoff(proj.spokeVault, nowMs);
|
|
16628
|
+
const currentSessionId = deps.currentSessionId !== void 0 ? deps.currentSessionId : process.env.CLAUDE_CODE_SESSION_ID ?? null;
|
|
16629
|
+
let registryNote = null;
|
|
16630
|
+
let gapSessions = [];
|
|
16631
|
+
let usedFallbackWindow = false;
|
|
16632
|
+
if (currentSessionId === null) {
|
|
16633
|
+
registryNote = "current session id unknown (CLAUDE_CODE_SESSION_ID unset) \u2014 cannot safely exclude the running session, so the gap was skipped";
|
|
16634
|
+
} else {
|
|
16635
|
+
const allRecords = readCaptainSessionRegistry(proj.spokeVault);
|
|
16636
|
+
if (allRecords.length === 0) {
|
|
16637
|
+
registryNote = "no session registry found yet for this project (#651's SessionStart hook may not have fired before now)";
|
|
16638
|
+
}
|
|
16639
|
+
const selection = selectGapSessions(allRecords, currentSessionId, checkpoint, nowMs, fallbackWindowMs);
|
|
16640
|
+
usedFallbackWindow = selection.usedFallbackWindow;
|
|
16641
|
+
gapSessions = selection.gapSessions.map((session) => ({ session, transcript: extractTranscriptTail(session.transcriptPath) }));
|
|
16642
|
+
}
|
|
16643
|
+
return assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, nowIso, {
|
|
16644
|
+
registryNote,
|
|
16645
|
+
usedFallbackWindow,
|
|
16646
|
+
fallbackWindowMs
|
|
16647
|
+
});
|
|
16648
|
+
}
|
|
16649
|
+
var handoffCommand = new Command34("handoff").description(
|
|
16650
|
+
"Handoff continuity \u2014 gather verified facts for the captain to synthesize a handoff from (#650/#651)"
|
|
16651
|
+
);
|
|
16652
|
+
handoffCommand.command("facts <project>").description(
|
|
16653
|
+
"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."
|
|
16654
|
+
).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) => {
|
|
16655
|
+
const out = await runHandoffFacts(project, { fetch: opts.fetch });
|
|
16656
|
+
console.log(JSON.stringify(out, null, 2));
|
|
16657
|
+
});
|
|
16658
|
+
|
|
15598
16659
|
// packages/cli/src/index.ts
|
|
15599
16660
|
init_dist();
|
|
15600
16661
|
init_dist();
|
|
15601
16662
|
init_dist();
|
|
15602
16663
|
init_dist();
|
|
15603
16664
|
var __dirname = dirname9(fileURLToPath6(import.meta.url));
|
|
15604
|
-
var pkg = JSON.parse(
|
|
16665
|
+
var pkg = JSON.parse(readFileSync16(join30(__dirname, "..", "package.json"), "utf-8"));
|
|
15605
16666
|
ensureRuntimeSynced({
|
|
15606
|
-
sourceRoot:
|
|
15607
|
-
runtimeRoot:
|
|
16667
|
+
sourceRoot: join30(__dirname, ".."),
|
|
16668
|
+
runtimeRoot: join30(homedir22(), ".config", "squadrant")
|
|
15608
16669
|
});
|
|
15609
16670
|
if (process.argv[2] !== "config") {
|
|
15610
16671
|
try {
|
|
15611
|
-
const cfgPath =
|
|
15612
|
-
if (
|
|
15613
|
-
const cfg = JSON.parse(
|
|
16672
|
+
const cfgPath = join30(homedir22(), ".config", "squadrant", "config.json");
|
|
16673
|
+
if (existsSync13(cfgPath)) {
|
|
16674
|
+
const cfg = JSON.parse(readFileSync16(cfgPath, "utf-8"));
|
|
15614
16675
|
if (needsCheck(cfg, pkg.version)) {
|
|
15615
16676
|
const items = detectDrift(cfg, getDefaultConfig());
|
|
15616
16677
|
if (items.length === 0) {
|
|
15617
|
-
|
|
16678
|
+
writeFileSync12(cfgPath, JSON.stringify(withStamp(cfg, pkg.version), null, 2) + "\n");
|
|
15618
16679
|
} else {
|
|
15619
16680
|
const from = cfg._squadrantVersion ?? "an earlier version";
|
|
15620
16681
|
process.stderr.write(
|
|
@@ -15632,9 +16693,9 @@ if (process.argv[2] !== "config") {
|
|
|
15632
16693
|
}
|
|
15633
16694
|
}
|
|
15634
16695
|
if (!process.env.SQUADRANT_DAEMON_SKIP) {
|
|
15635
|
-
ensureDaemon();
|
|
16696
|
+
ensureDaemon(void 0, { operatorInitiated: isOperatorInitiatedCommand(process.argv[2]) });
|
|
15636
16697
|
}
|
|
15637
|
-
var program = new
|
|
16698
|
+
var program = new Command35();
|
|
15638
16699
|
program.name("squadrant").description("Multi-project orchestration for your coding agents (Claude, Codex, opencode, Gemini)").version(pkg.version);
|
|
15639
16700
|
program.addCommand(doctorCommand);
|
|
15640
16701
|
program.addCommand(initCommand);
|
|
@@ -15663,8 +16724,11 @@ program.addCommand(pingCommand);
|
|
|
15663
16724
|
program.addCommand(dispatchCommand);
|
|
15664
16725
|
program.addCommand(cmuxCommand);
|
|
15665
16726
|
program.addCommand(effortCommand);
|
|
16727
|
+
program.addCommand(tokensCommand);
|
|
15666
16728
|
program.addCommand(telegramCommand);
|
|
15667
16729
|
program.addCommand(hooksCommand());
|
|
16730
|
+
program.addCommand(workCommand);
|
|
16731
|
+
program.addCommand(handoffCommand);
|
|
15668
16732
|
program.parseAsync().catch((e) => {
|
|
15669
16733
|
process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
|
|
15670
16734
|
`);
|