switchroom 0.13.3 → 0.13.4
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/cli/switchroom.js
CHANGED
|
@@ -23872,7 +23872,7 @@ import {
|
|
|
23872
23872
|
existsSync as existsSync17,
|
|
23873
23873
|
mkdirSync as mkdirSync12,
|
|
23874
23874
|
readFileSync as readFileSync17,
|
|
23875
|
-
readdirSync as
|
|
23875
|
+
readdirSync as readdirSync8,
|
|
23876
23876
|
renameSync as renameSync5,
|
|
23877
23877
|
rmSync as rmSync4,
|
|
23878
23878
|
statSync as statSync11,
|
|
@@ -23943,7 +23943,7 @@ function listSlots(agentDir) {
|
|
|
23943
23943
|
if (!existsSync17(dir))
|
|
23944
23944
|
return [];
|
|
23945
23945
|
try {
|
|
23946
|
-
return
|
|
23946
|
+
return readdirSync8(dir).filter((name) => {
|
|
23947
23947
|
try {
|
|
23948
23948
|
return statSync11(join13(dir, name)).isDirectory();
|
|
23949
23949
|
} catch {
|
|
@@ -24115,7 +24115,7 @@ var init_accounts = __esm(() => {
|
|
|
24115
24115
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
24116
24116
|
import {
|
|
24117
24117
|
readFileSync as readFileSync18,
|
|
24118
|
-
readdirSync as
|
|
24118
|
+
readdirSync as readdirSync9,
|
|
24119
24119
|
existsSync as existsSync18,
|
|
24120
24120
|
writeFileSync as writeFileSync10,
|
|
24121
24121
|
mkdirSync as mkdirSync13,
|
|
@@ -24272,7 +24272,7 @@ function cleanupAuthTempDirs(agentDir) {
|
|
|
24272
24272
|
if (!existsSync18(dir))
|
|
24273
24273
|
return;
|
|
24274
24274
|
try {
|
|
24275
|
-
for (const entry of
|
|
24275
|
+
for (const entry of readdirSync9(dir)) {
|
|
24276
24276
|
if (entry.startsWith(".setup-token-tmp-")) {
|
|
24277
24277
|
rmSync5(join14(dir, entry), { recursive: true, force: true });
|
|
24278
24278
|
}
|
|
@@ -25226,7 +25226,7 @@ class AuthBrokerClient {
|
|
|
25226
25226
|
closed = false;
|
|
25227
25227
|
constructor(opts = {}) {
|
|
25228
25228
|
this.socketPath = resolveAuthBrokerSocketPath(opts);
|
|
25229
|
-
this.timeoutMs = opts.timeoutMs ??
|
|
25229
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
25230
25230
|
}
|
|
25231
25231
|
getSocketPath() {
|
|
25232
25232
|
return this.socketPath;
|
|
@@ -25477,7 +25477,7 @@ async function withAuthBrokerClient(fn, opts) {
|
|
|
25477
25477
|
function authBrokerSocketExists(opts) {
|
|
25478
25478
|
return existsSync25(resolveAuthBrokerSocketPath(opts));
|
|
25479
25479
|
}
|
|
25480
|
-
var
|
|
25480
|
+
var DEFAULT_TIMEOUT_MS2 = 5000, AuthBrokerError, AuthBrokerUnreachableError;
|
|
25481
25481
|
var init_client2 = __esm(() => {
|
|
25482
25482
|
init_protocol2();
|
|
25483
25483
|
AuthBrokerError = class AuthBrokerError extends Error {
|
|
@@ -25537,7 +25537,7 @@ import {
|
|
|
25537
25537
|
existsSync as existsSync26,
|
|
25538
25538
|
mkdirSync as mkdirSync15,
|
|
25539
25539
|
readFileSync as readFileSync22,
|
|
25540
|
-
readdirSync as
|
|
25540
|
+
readdirSync as readdirSync13,
|
|
25541
25541
|
renameSync as renameSync6,
|
|
25542
25542
|
rmSync as rmSync10,
|
|
25543
25543
|
statSync as statSync15,
|
|
@@ -25568,7 +25568,7 @@ function listAccounts(home2 = homedir8()) {
|
|
|
25568
25568
|
if (!existsSync26(root))
|
|
25569
25569
|
return [];
|
|
25570
25570
|
try {
|
|
25571
|
-
return
|
|
25571
|
+
return readdirSync13(root).filter((name) => {
|
|
25572
25572
|
try {
|
|
25573
25573
|
return statSync15(join19(root, name)).isDirectory();
|
|
25574
25574
|
} catch {
|
|
@@ -25730,7 +25730,7 @@ __export(exports_oauth, {
|
|
|
25730
25730
|
});
|
|
25731
25731
|
import * as http from "node:http";
|
|
25732
25732
|
import * as crypto2 from "node:crypto";
|
|
25733
|
-
import { spawn as
|
|
25733
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
25734
25734
|
function detectHeadless(env2) {
|
|
25735
25735
|
const hasDisplay = Boolean(env2.DISPLAY && env2.DISPLAY.trim() !== "" || env2.WAYLAND_DISPLAY && env2.WAYLAND_DISPLAY.trim() !== "");
|
|
25736
25736
|
const inSsh = Boolean(env2.SSH_CONNECTION && env2.SSH_CONNECTION.trim() !== "" || env2.SSH_TTY && env2.SSH_TTY.trim() !== "");
|
|
@@ -25890,7 +25890,7 @@ function buildLoopbackAuthUrl(cfg, redirectUri, state) {
|
|
|
25890
25890
|
u.searchParams.set("state", state);
|
|
25891
25891
|
return u.toString();
|
|
25892
25892
|
}
|
|
25893
|
-
async function openBrowser(url, platform = process.platform, spawnImpl =
|
|
25893
|
+
async function openBrowser(url, platform = process.platform, spawnImpl = spawn2) {
|
|
25894
25894
|
let cmd;
|
|
25895
25895
|
let args;
|
|
25896
25896
|
if (platform === "darwin") {
|
|
@@ -26407,7 +26407,7 @@ async function waitForApproval(opts) {
|
|
|
26407
26407
|
const request = opts._request ?? approvalRequest;
|
|
26408
26408
|
const lookup = opts._lookup ?? approvalLookup;
|
|
26409
26409
|
const sleep3 = opts._sleep ?? defaultSleep;
|
|
26410
|
-
const timeoutMs = opts.timeout_ms ??
|
|
26410
|
+
const timeoutMs = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS3;
|
|
26411
26411
|
const initialPoll = opts.initial_poll_ms ?? DEFAULT_INITIAL_POLL_MS;
|
|
26412
26412
|
const maxPoll = opts.max_poll_ms ?? DEFAULT_MAX_POLL_MS;
|
|
26413
26413
|
const backoff = opts.backoff ?? DEFAULT_BACKOFF;
|
|
@@ -26495,7 +26495,7 @@ async function waitForApproval(opts) {
|
|
|
26495
26495
|
}
|
|
26496
26496
|
}
|
|
26497
26497
|
}
|
|
26498
|
-
var
|
|
26498
|
+
var DEFAULT_TIMEOUT_MS3 = 600000, DEFAULT_INITIAL_POLL_MS = 2000, DEFAULT_MAX_POLL_MS = 30000, DEFAULT_BACKOFF = 1.5, DENY_MODES;
|
|
26499
26499
|
var init_wait = __esm(() => {
|
|
26500
26500
|
init_client3();
|
|
26501
26501
|
DENY_MODES = new Set(["deny", "deny_perm"]);
|
|
@@ -27053,7 +27053,7 @@ async function runViaClaude(opts) {
|
|
|
27053
27053
|
if (tmuxHasSession(SESSION))
|
|
27054
27054
|
tmuxKillSession(SESSION);
|
|
27055
27055
|
}
|
|
27056
|
-
const
|
|
27056
|
+
const spawn3 = opts.spawnClaude ?? (() => {
|
|
27057
27057
|
execFileSync11("tmux", [
|
|
27058
27058
|
"new-session",
|
|
27059
27059
|
"-d",
|
|
@@ -27069,7 +27069,7 @@ async function runViaClaude(opts) {
|
|
|
27069
27069
|
], { stdio: "ignore", timeout: 5000 });
|
|
27070
27070
|
});
|
|
27071
27071
|
log(" Spawning claude in a tmux session to mint a broader-scope OAuth token\u2026");
|
|
27072
|
-
|
|
27072
|
+
spawn3();
|
|
27073
27073
|
try {
|
|
27074
27074
|
const preFired = new Set;
|
|
27075
27075
|
const phase1Deadline = Date.now() + urlTimeout;
|
|
@@ -27605,7 +27605,7 @@ var init_doctor_status = __esm(() => {
|
|
|
27605
27605
|
import {
|
|
27606
27606
|
existsSync as existsSync46,
|
|
27607
27607
|
readFileSync as readFileSync42,
|
|
27608
|
-
readdirSync as
|
|
27608
|
+
readdirSync as readdirSync17
|
|
27609
27609
|
} from "node:fs";
|
|
27610
27610
|
import { dirname as dirname11, join as join38 } from "node:path";
|
|
27611
27611
|
import { execSync as execSync2 } from "node:child_process";
|
|
@@ -27686,7 +27686,7 @@ function probePlaywrightMcpVersion() {
|
|
|
27686
27686
|
if (!existsSync46(npxCache))
|
|
27687
27687
|
return null;
|
|
27688
27688
|
try {
|
|
27689
|
-
const entries =
|
|
27689
|
+
const entries = readdirSync17(npxCache);
|
|
27690
27690
|
for (const entry of entries) {
|
|
27691
27691
|
const pkgPath = join38(npxCache, entry, "node_modules/@playwright/mcp/package.json");
|
|
27692
27692
|
if (existsSync46(pkgPath)) {
|
|
@@ -28834,7 +28834,7 @@ import { join as join42 } from "node:path";
|
|
|
28834
28834
|
function runCredentialsMigrationChecks(config, deps = {}) {
|
|
28835
28835
|
const credDir = deps.credentialsDir ?? join42(homedir23(), ".switchroom", "credentials");
|
|
28836
28836
|
const existsSync49 = deps.existsSync ?? ((p) => realExistsSync2(p));
|
|
28837
|
-
const
|
|
28837
|
+
const readdirSync19 = deps.readdirSync ?? ((p) => realReaddirSync(p));
|
|
28838
28838
|
const isDirectory = deps.isDirectory ?? ((p) => {
|
|
28839
28839
|
try {
|
|
28840
28840
|
return realStatSync(p).isDirectory();
|
|
@@ -28847,7 +28847,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
|
|
|
28847
28847
|
const agentNames = new Set(Object.keys(config.agents ?? {}));
|
|
28848
28848
|
let entries;
|
|
28849
28849
|
try {
|
|
28850
|
-
entries =
|
|
28850
|
+
entries = readdirSync19(credDir);
|
|
28851
28851
|
} catch {
|
|
28852
28852
|
return [
|
|
28853
28853
|
{
|
|
@@ -29403,7 +29403,7 @@ import {
|
|
|
29403
29403
|
lstatSync as lstatSync5,
|
|
29404
29404
|
mkdirSync as mkdirSync27,
|
|
29405
29405
|
readFileSync as readFileSync45,
|
|
29406
|
-
readdirSync as
|
|
29406
|
+
readdirSync as readdirSync19,
|
|
29407
29407
|
statSync as statSync22
|
|
29408
29408
|
} from "node:fs";
|
|
29409
29409
|
import { dirname as dirname12, join as join45, resolve as resolve29 } from "node:path";
|
|
@@ -29413,7 +29413,7 @@ function findInNvm(bin) {
|
|
|
29413
29413
|
if (!existsSync50(nvmRoot))
|
|
29414
29414
|
return null;
|
|
29415
29415
|
try {
|
|
29416
|
-
const versions =
|
|
29416
|
+
const versions = readdirSync19(nvmRoot).sort().reverse();
|
|
29417
29417
|
for (const v of versions) {
|
|
29418
29418
|
const candidate = join45(nvmRoot, v, "bin", bin);
|
|
29419
29419
|
try {
|
|
@@ -29585,7 +29585,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
|
|
|
29585
29585
|
if (!existsSync50(cacheDir))
|
|
29586
29586
|
continue;
|
|
29587
29587
|
try {
|
|
29588
|
-
const entries =
|
|
29588
|
+
const entries = readdirSync19(cacheDir).filter((e) => e.startsWith("chromium"));
|
|
29589
29589
|
for (const entry of entries) {
|
|
29590
29590
|
const candidates2 = [
|
|
29591
29591
|
join45(cacheDir, entry, "chrome-linux64", "chrome"),
|
|
@@ -29898,7 +29898,7 @@ function checkPendingRetainsQueue(dir) {
|
|
|
29898
29898
|
}
|
|
29899
29899
|
let names;
|
|
29900
29900
|
try {
|
|
29901
|
-
names =
|
|
29901
|
+
names = readdirSync19(pendingDir);
|
|
29902
29902
|
} catch (err) {
|
|
29903
29903
|
return {
|
|
29904
29904
|
name: "pending-retains queue",
|
|
@@ -30158,7 +30158,7 @@ function checkRepoHygiene(repoRoot) {
|
|
|
30158
30158
|
});
|
|
30159
30159
|
}
|
|
30160
30160
|
try {
|
|
30161
|
-
const entries =
|
|
30161
|
+
const entries = readdirSync19(repoRoot);
|
|
30162
30162
|
for (const name of entries) {
|
|
30163
30163
|
if (name === "clerk-export-with-secrets.tar.gz")
|
|
30164
30164
|
continue;
|
|
@@ -47248,8 +47248,8 @@ var {
|
|
|
47248
47248
|
} = import__.default;
|
|
47249
47249
|
|
|
47250
47250
|
// src/build-info.ts
|
|
47251
|
-
var VERSION = "0.13.
|
|
47252
|
-
var COMMIT_SHA = "
|
|
47251
|
+
var VERSION = "0.13.4";
|
|
47252
|
+
var COMMIT_SHA = "b4fd0264";
|
|
47253
47253
|
|
|
47254
47254
|
// src/cli/agent.ts
|
|
47255
47255
|
init_source();
|
|
@@ -50468,13 +50468,11 @@ import { join as join12 } from "node:path";
|
|
|
50468
50468
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
50469
50469
|
|
|
50470
50470
|
// src/agents/handoff-summarizer.ts
|
|
50471
|
-
import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as renameSync4, mkdirSync as mkdirSync11, existsSync as existsSync14, statSync as statSync9 } from "node:fs";
|
|
50471
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as renameSync4, mkdirSync as mkdirSync11, existsSync as existsSync14, statSync as statSync9, readdirSync as readdirSync7 } from "node:fs";
|
|
50472
50472
|
import { join as join11 } from "node:path";
|
|
50473
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
50474
|
-
var DEFAULT_SUMMARIZER_MODEL = "claude-haiku-4-5-20251001";
|
|
50475
50473
|
var DEFAULT_MAX_TURNS = 50;
|
|
50476
|
-
var DEFAULT_TIMEOUT_MS2 = 30000;
|
|
50477
50474
|
var TOPIC_MAX_CHARS = 117;
|
|
50475
|
+
var TURN_TEXT_MAX_CHARS = 1200;
|
|
50478
50476
|
function extractTurnsFromJsonl(path, maxTurns) {
|
|
50479
50477
|
let raw;
|
|
50480
50478
|
try {
|
|
@@ -50507,7 +50505,8 @@ function extractTurnsFromJsonl(path, maxTurns) {
|
|
|
50507
50505
|
}
|
|
50508
50506
|
if (obj.type === "user" && obj.message && typeof obj.message === "object") {
|
|
50509
50507
|
const content = obj.message.content;
|
|
50510
|
-
const
|
|
50508
|
+
const raw2 = extractTextBlocks(content);
|
|
50509
|
+
const text = raw2 ? extractChannelBody(raw2) : null;
|
|
50511
50510
|
if (text)
|
|
50512
50511
|
turns.push({ role: "user", text });
|
|
50513
50512
|
continue;
|
|
@@ -50520,9 +50519,16 @@ function extractTurnsFromJsonl(path, maxTurns) {
|
|
|
50520
50519
|
continue;
|
|
50521
50520
|
}
|
|
50522
50521
|
}
|
|
50523
|
-
|
|
50524
|
-
|
|
50525
|
-
|
|
50522
|
+
const deduped = [];
|
|
50523
|
+
for (const t of turns) {
|
|
50524
|
+
const prev = deduped[deduped.length - 1];
|
|
50525
|
+
if (prev && prev.role === t.role && prev.text === t.text)
|
|
50526
|
+
continue;
|
|
50527
|
+
deduped.push(t);
|
|
50528
|
+
}
|
|
50529
|
+
if (deduped.length <= maxTurns)
|
|
50530
|
+
return deduped;
|
|
50531
|
+
return deduped.slice(deduped.length - maxTurns);
|
|
50526
50532
|
}
|
|
50527
50533
|
function extractChannelBody(raw) {
|
|
50528
50534
|
const m = raw.match(/<channel[^>]*>([\s\S]*?)<\/channel>/);
|
|
@@ -50553,56 +50559,48 @@ function extractTextBlocks(content) {
|
|
|
50553
50559
|
`).trim();
|
|
50554
50560
|
return joined.length > 0 ? joined : null;
|
|
50555
50561
|
}
|
|
50556
|
-
function
|
|
50557
|
-
const
|
|
50558
|
-
|
|
50559
|
-
` + `Output format \u2014 EXACTLY this structure, no preamble:
|
|
50560
|
-
` + `## Topic: <one short line, max 100 chars, describing what the user and assistant were most recently focused on>
|
|
50561
|
-
|
|
50562
|
-
` + `## Summary
|
|
50563
|
-
<one paragraph, what we were working on>
|
|
50564
|
-
|
|
50565
|
-
` + `## Open threads
|
|
50566
|
-
- <bulleted list of pending/unresolved items; empty list ok>
|
|
50567
|
-
|
|
50568
|
-
` + `## Last exchange
|
|
50569
|
-
**User:** <verbatim or near-verbatim last user message, truncated to ~500 chars>
|
|
50570
|
-
**Assistant:** <last assistant response, truncated to ~500 chars>
|
|
50562
|
+
function formatTranscriptTail(turns) {
|
|
50563
|
+
const header = `# Handoff \u2014 previous session
|
|
50571
50564
|
|
|
50572
|
-
` +
|
|
50573
|
-
|
|
50574
|
-
|
|
50575
|
-
|
|
50576
|
-
|
|
50577
|
-
|
|
50578
|
-
|
|
50579
|
-
const
|
|
50580
|
-
|
|
50565
|
+
` + "You are resuming this agent's work. There is no generated summary " + "\u2014 below is the **raw tail of the previous session's transcript** " + "(oldest first, most recent last). Read it to reorient, then carry " + "on. Anything important worth keeping long-term should already be " + `in your memory files \u2014 check those too.
|
|
50566
|
+
`;
|
|
50567
|
+
if (turns.length === 0) {
|
|
50568
|
+
return header + `
|
|
50569
|
+
_(No recent turns were recoverable from the previous session.)_
|
|
50570
|
+
`;
|
|
50571
|
+
}
|
|
50572
|
+
const body = turns.map((t) => {
|
|
50573
|
+
let text = t.text;
|
|
50574
|
+
if (text.length > TURN_TEXT_MAX_CHARS) {
|
|
50575
|
+
text = text.slice(0, TURN_TEXT_MAX_CHARS) + `
|
|
50576
|
+
\u2026[truncated]`;
|
|
50577
|
+
}
|
|
50578
|
+
return `### ${t.role === "user" ? "User" : "Assistant"}
|
|
50579
|
+
${text}`;
|
|
50580
|
+
}).join(`
|
|
50581
50581
|
|
|
50582
50582
|
`);
|
|
50583
|
-
|
|
50583
|
+
return `${header}
|
|
50584
|
+
## Recent turns
|
|
50584
50585
|
|
|
50585
|
-
|
|
50586
|
-
|
|
50586
|
+
${body}
|
|
50587
|
+
`;
|
|
50587
50588
|
}
|
|
50588
|
-
function
|
|
50589
|
-
|
|
50590
|
-
|
|
50591
|
-
|
|
50592
|
-
start++;
|
|
50593
|
-
if (start >= lines.length)
|
|
50594
|
-
return null;
|
|
50595
|
-
const first = lines[start].trim();
|
|
50596
|
-
const m = first.match(/^##\s*Topic:\s*(.+)$/i);
|
|
50597
|
-
if (!m)
|
|
50598
|
-
return null;
|
|
50599
|
-
let topic = m[1].trim();
|
|
50600
|
-
if (topic.length > TOPIC_MAX_CHARS) {
|
|
50601
|
-
topic = topic.slice(0, TOPIC_MAX_CHARS) + "\u2026";
|
|
50589
|
+
function deriveTopic(turns) {
|
|
50590
|
+
for (let i = turns.length - 1;i >= 0; i--) {
|
|
50591
|
+
if (turns[i].role === "user")
|
|
50592
|
+
return clampTopic(firstLine(turns[i].text));
|
|
50602
50593
|
}
|
|
50603
|
-
|
|
50604
|
-
|
|
50605
|
-
return
|
|
50594
|
+
if (turns.length > 0)
|
|
50595
|
+
return clampTopic(firstLine(turns[turns.length - 1].text));
|
|
50596
|
+
return "previous session";
|
|
50597
|
+
}
|
|
50598
|
+
function firstLine(s) {
|
|
50599
|
+
const line = s.split(/\r?\n/).find((l) => l.trim().length > 0) ?? s;
|
|
50600
|
+
return line.trim();
|
|
50601
|
+
}
|
|
50602
|
+
function clampTopic(s) {
|
|
50603
|
+
return s.length > TOPIC_MAX_CHARS ? s.slice(0, TOPIC_MAX_CHARS) + "\u2026" : s;
|
|
50606
50604
|
}
|
|
50607
50605
|
function writeSidecarsAtomic(agentDir, briefing, topic) {
|
|
50608
50606
|
mkdirSync11(agentDir, { recursive: true });
|
|
@@ -50615,103 +50613,24 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
|
|
|
50615
50613
|
renameSync4(handoffTmp, handoffPath);
|
|
50616
50614
|
renameSync4(topicTmp, topicPath);
|
|
50617
50615
|
}
|
|
50618
|
-
async function
|
|
50619
|
-
const model = opts.model ?? DEFAULT_SUMMARIZER_MODEL;
|
|
50616
|
+
async function buildHandoff(opts) {
|
|
50620
50617
|
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
50621
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
50622
50618
|
const turns = extractTurnsFromJsonl(opts.jsonlPath, maxTurns);
|
|
50623
50619
|
if (turns.length === 0) {
|
|
50624
50620
|
return "no-turns";
|
|
50625
50621
|
}
|
|
50626
|
-
const
|
|
50627
|
-
const
|
|
50628
|
-
let raw;
|
|
50629
|
-
try {
|
|
50630
|
-
raw = await runner.run({
|
|
50631
|
-
model,
|
|
50632
|
-
system: prompt.system,
|
|
50633
|
-
user: prompt.user,
|
|
50634
|
-
timeoutMs
|
|
50635
|
-
});
|
|
50636
|
-
} catch (err) {
|
|
50637
|
-
process.stderr.write(`handoff-summarizer: claude -p call failed \u2014 ${errMsg(err)}
|
|
50638
|
-
`);
|
|
50639
|
-
return "cli-error";
|
|
50640
|
-
}
|
|
50641
|
-
raw = raw.trim();
|
|
50642
|
-
if (!raw) {
|
|
50643
|
-
return "empty-response";
|
|
50644
|
-
}
|
|
50645
|
-
const parsed = parseHandoffResponse(raw);
|
|
50646
|
-
if (!parsed) {
|
|
50647
|
-
process.stderr.write(`handoff-summarizer: response missing '## Topic:' header; skipping
|
|
50648
|
-
`);
|
|
50649
|
-
return "parse-error";
|
|
50650
|
-
}
|
|
50622
|
+
const briefing = formatTranscriptTail(turns);
|
|
50623
|
+
const topic = deriveTopic(turns);
|
|
50651
50624
|
try {
|
|
50652
|
-
writeSidecarsAtomic(opts.agentDir,
|
|
50625
|
+
writeSidecarsAtomic(opts.agentDir, briefing, topic);
|
|
50653
50626
|
} catch (err) {
|
|
50654
|
-
process.stderr.write(`handoff
|
|
50627
|
+
process.stderr.write(`handoff: sidecar write failed \u2014 ${errMsg(err)}
|
|
50655
50628
|
`);
|
|
50656
50629
|
return "write-error";
|
|
50657
50630
|
}
|
|
50658
|
-
await mirrorToHindsight(
|
|
50631
|
+
await mirrorToHindsight(briefing, opts).catch(() => {});
|
|
50659
50632
|
return "ok";
|
|
50660
50633
|
}
|
|
50661
|
-
function buildHandoffClaudeArgs(opts) {
|
|
50662
|
-
return [
|
|
50663
|
-
"-p",
|
|
50664
|
-
opts.user,
|
|
50665
|
-
"--model",
|
|
50666
|
-
opts.model,
|
|
50667
|
-
"--append-system-prompt",
|
|
50668
|
-
opts.system,
|
|
50669
|
-
"--no-session-persistence",
|
|
50670
|
-
"--strict-mcp-config"
|
|
50671
|
-
];
|
|
50672
|
-
}
|
|
50673
|
-
var defaultClaudeCliRunner = {
|
|
50674
|
-
async run({ model, system, user, timeoutMs }) {
|
|
50675
|
-
return await new Promise((resolve14, reject) => {
|
|
50676
|
-
const args = buildHandoffClaudeArgs({ model, system, user });
|
|
50677
|
-
const env2 = {
|
|
50678
|
-
...process.env,
|
|
50679
|
-
FORCE_COLOR: "0",
|
|
50680
|
-
NO_COLOR: "1"
|
|
50681
|
-
};
|
|
50682
|
-
const child = spawn2("claude", args, {
|
|
50683
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
50684
|
-
env: env2
|
|
50685
|
-
});
|
|
50686
|
-
let stdout = "";
|
|
50687
|
-
let stderr = "";
|
|
50688
|
-
const timer = setTimeout(() => {
|
|
50689
|
-
child.kill("SIGTERM");
|
|
50690
|
-
reject(new Error(`timeout after ${timeoutMs}ms`));
|
|
50691
|
-
}, timeoutMs);
|
|
50692
|
-
child.stdout.on("data", (d) => {
|
|
50693
|
-
stdout += d.toString("utf-8");
|
|
50694
|
-
});
|
|
50695
|
-
child.stderr.on("data", (d) => {
|
|
50696
|
-
stderr += d.toString("utf-8");
|
|
50697
|
-
});
|
|
50698
|
-
child.on("error", (err) => {
|
|
50699
|
-
clearTimeout(timer);
|
|
50700
|
-
reject(err);
|
|
50701
|
-
});
|
|
50702
|
-
child.on("close", (code) => {
|
|
50703
|
-
clearTimeout(timer);
|
|
50704
|
-
if (code === 0) {
|
|
50705
|
-
resolve14(stdout);
|
|
50706
|
-
} else {
|
|
50707
|
-
const tail = stderr.trim().split(`
|
|
50708
|
-
`).slice(-3).join(" | ");
|
|
50709
|
-
reject(new Error(`claude -p exited ${code}: ${tail || "(no stderr)"}`));
|
|
50710
|
-
}
|
|
50711
|
-
});
|
|
50712
|
-
});
|
|
50713
|
-
}
|
|
50714
|
-
};
|
|
50715
50634
|
function errMsg(err) {
|
|
50716
50635
|
if (err && typeof err === "object" && "message" in err) {
|
|
50717
50636
|
return String(err.message);
|
|
@@ -50742,7 +50661,7 @@ async function mirrorToHindsight(briefing, opts) {
|
|
|
50742
50661
|
body: JSON.stringify(body)
|
|
50743
50662
|
});
|
|
50744
50663
|
} catch (err) {
|
|
50745
|
-
process.stderr.write(`handoff
|
|
50664
|
+
process.stderr.write(`handoff: hindsight mirror failed \u2014 ${errMsg(err)}
|
|
50746
50665
|
`);
|
|
50747
50666
|
}
|
|
50748
50667
|
}
|
|
@@ -50754,7 +50673,7 @@ function findLatestSessionJsonl(claudeConfigDir) {
|
|
|
50754
50673
|
const walk = (dir) => {
|
|
50755
50674
|
let entries;
|
|
50756
50675
|
try {
|
|
50757
|
-
entries =
|
|
50676
|
+
entries = readdirSync7(dir);
|
|
50758
50677
|
} catch {
|
|
50759
50678
|
return;
|
|
50760
50679
|
}
|
|
@@ -51708,7 +51627,7 @@ function enforceUsername(username, agentSlug, expectedUsername, loose, warn) {
|
|
|
51708
51627
|
}
|
|
51709
51628
|
|
|
51710
51629
|
// src/setup/profile-picker.ts
|
|
51711
|
-
import { existsSync as existsSync20, readdirSync as
|
|
51630
|
+
import { existsSync as existsSync20, readdirSync as readdirSync10, statSync as statSync13 } from "node:fs";
|
|
51712
51631
|
import { resolve as resolve16 } from "node:path";
|
|
51713
51632
|
var PROFILE_GLOSSES = {
|
|
51714
51633
|
default: "minimal baseline \u2014 generic chat helper, no opinion.",
|
|
@@ -51724,7 +51643,7 @@ function defaultListProfileSkills(profileName) {
|
|
|
51724
51643
|
return [];
|
|
51725
51644
|
}
|
|
51726
51645
|
try {
|
|
51727
|
-
return
|
|
51646
|
+
return readdirSync10(skillsDir).filter((entry) => {
|
|
51728
51647
|
try {
|
|
51729
51648
|
return statSync13(resolve16(skillsDir, entry)).isDirectory();
|
|
51730
51649
|
} catch {
|
|
@@ -55390,7 +55309,7 @@ init_source();
|
|
|
55390
55309
|
init_loader();
|
|
55391
55310
|
init_vault();
|
|
55392
55311
|
init_hindsight();
|
|
55393
|
-
import { readFileSync as readFileSync26, writeFileSync as writeFileSync16, copyFileSync as copyFileSync6, existsSync as existsSync30, readdirSync as
|
|
55312
|
+
import { readFileSync as readFileSync26, writeFileSync as writeFileSync16, copyFileSync as copyFileSync6, existsSync as existsSync30, readdirSync as readdirSync14, statSync as statSync17 } from "node:fs";
|
|
55394
55313
|
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
55395
55314
|
import { join as join23 } from "node:path";
|
|
55396
55315
|
import { homedir as homedir10 } from "node:os";
|
|
@@ -55415,7 +55334,7 @@ function findClaudeTranscripts() {
|
|
|
55415
55334
|
const walk = (dir) => {
|
|
55416
55335
|
let entries;
|
|
55417
55336
|
try {
|
|
55418
|
-
entries =
|
|
55337
|
+
entries = readdirSync14(dir);
|
|
55419
55338
|
} catch {
|
|
55420
55339
|
return;
|
|
55421
55340
|
}
|
|
@@ -55774,13 +55693,13 @@ init_loader();
|
|
|
55774
55693
|
init_loader();
|
|
55775
55694
|
init_client();
|
|
55776
55695
|
import { readFileSync as readFileSync31, existsSync as existsSync34, unlinkSync as unlinkSync9 } from "node:fs";
|
|
55777
|
-
import { spawn as
|
|
55696
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
55778
55697
|
|
|
55779
55698
|
// src/vault/broker/server.ts
|
|
55780
55699
|
init_compose();
|
|
55781
55700
|
init_vault();
|
|
55782
55701
|
import * as net3 from "node:net";
|
|
55783
|
-
import { mkdirSync as mkdirSync20, chmodSync as chmodSync7, chownSync, existsSync as existsSync33, readFileSync as readFileSync29, readdirSync as
|
|
55702
|
+
import { mkdirSync as mkdirSync20, chmodSync as chmodSync7, chownSync, existsSync as existsSync33, readFileSync as readFileSync29, readdirSync as readdirSync15, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync18, renameSync as renameSync9 } from "node:fs";
|
|
55784
55703
|
import { dirname as dirname6, resolve as resolve24, basename as basename5 } from "node:path";
|
|
55785
55704
|
import * as os4 from "node:os";
|
|
55786
55705
|
import * as path3 from "node:path";
|
|
@@ -59844,7 +59763,7 @@ async function main() {
|
|
|
59844
59763
|
let perAgentTargets = [];
|
|
59845
59764
|
try {
|
|
59846
59765
|
if (existsSync33(perAgentDir)) {
|
|
59847
|
-
const entries =
|
|
59766
|
+
const entries = readdirSync15(perAgentDir, { withFileTypes: true });
|
|
59848
59767
|
const flat = [];
|
|
59849
59768
|
const subdirs = [];
|
|
59850
59769
|
for (const e of entries) {
|
|
@@ -60124,7 +60043,7 @@ function registerVaultBrokerCommand(vaultCmd, program3) {
|
|
|
60124
60043
|
const args = ["vault", "broker", "start", "--foreground"];
|
|
60125
60044
|
if (parentOpts.config)
|
|
60126
60045
|
args.unshift("--config", parentOpts.config);
|
|
60127
|
-
const child =
|
|
60046
|
+
const child = spawn3(process.execPath, [self2, ...args], {
|
|
60128
60047
|
detached: true,
|
|
60129
60048
|
stdio: "ignore"
|
|
60130
60049
|
});
|
|
@@ -60411,11 +60330,11 @@ function levelLabel(level) {
|
|
|
60411
60330
|
function printDiagnostic(d) {
|
|
60412
60331
|
const glyph = levelGlyph(d.level);
|
|
60413
60332
|
const label = levelLabel(d.level);
|
|
60414
|
-
const
|
|
60333
|
+
const firstLine2 = d.message.split(`
|
|
60415
60334
|
`)[0];
|
|
60416
60335
|
const rest = d.message.split(`
|
|
60417
60336
|
`).slice(1);
|
|
60418
|
-
console.log(` ${glyph} [${label}] ${
|
|
60337
|
+
console.log(` ${glyph} [${label}] ${firstLine2}`);
|
|
60419
60338
|
for (const line of rest) {
|
|
60420
60339
|
console.log(` ${source_default.gray(line)}`);
|
|
60421
60340
|
}
|
|
@@ -60810,7 +60729,7 @@ import {
|
|
|
60810
60729
|
fsyncSync as fsyncSync5,
|
|
60811
60730
|
mkdirSync as mkdirSync21,
|
|
60812
60731
|
openSync as openSync9,
|
|
60813
|
-
readdirSync as
|
|
60732
|
+
readdirSync as readdirSync16,
|
|
60814
60733
|
readFileSync as readFileSync33,
|
|
60815
60734
|
renameSync as renameSync10,
|
|
60816
60735
|
statSync as statSync20,
|
|
@@ -60918,7 +60837,7 @@ function listBackupFiles(dir) {
|
|
|
60918
60837
|
return [];
|
|
60919
60838
|
let entries;
|
|
60920
60839
|
try {
|
|
60921
|
-
entries =
|
|
60840
|
+
entries = readdirSync16(dir);
|
|
60922
60841
|
} catch {
|
|
60923
60842
|
return [];
|
|
60924
60843
|
}
|
|
@@ -60941,7 +60860,7 @@ function backupVault(opts) {
|
|
|
60941
60860
|
throw new Error(`vault backup refused: source is not a valid vault envelope: ${validationError}`);
|
|
60942
60861
|
}
|
|
60943
60862
|
mkdirSync21(opts.destDir, { recursive: true, mode: 448 });
|
|
60944
|
-
const dirEntries =
|
|
60863
|
+
const dirEntries = readdirSync16(opts.destDir);
|
|
60945
60864
|
const offender = findAutoUnlockSibling(dirEntries);
|
|
60946
60865
|
if (offender) {
|
|
60947
60866
|
throw new Error(`vault backup refused: destination '${opts.destDir}' contains a file ` + `that looks like an auto-unlock credential ('${offender}'). The ` + `machine-bound auto-unlock blob MUST NOT be co-located with the ` + `encrypted vault \u2014 if they're together in version control, the ` + `passphrase gate is bypassed. Move/remove that file and retry.`);
|
|
@@ -62710,7 +62629,7 @@ import {
|
|
|
62710
62629
|
} from "node:fs";
|
|
62711
62630
|
import { resolve as resolve26, extname, join as join37, relative, dirname as dirname9 } from "node:path";
|
|
62712
62631
|
import { homedir as homedir19 } from "node:os";
|
|
62713
|
-
import { spawn as
|
|
62632
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
62714
62633
|
import { timingSafeEqual as timingSafeEqual2, randomBytes as randomBytes10 } from "node:crypto";
|
|
62715
62634
|
|
|
62716
62635
|
// src/web/api.ts
|
|
@@ -68626,7 +68545,7 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
68626
68545
|
existing.kill();
|
|
68627
68546
|
ws._logProcess = null;
|
|
68628
68547
|
}
|
|
68629
|
-
const child =
|
|
68548
|
+
const child = spawn4("docker", ["logs", "-f", "--tail", "20", containerName(agentName)], { stdio: ["ignore", "pipe", "pipe"] });
|
|
68630
68549
|
child.on("error", (err) => {
|
|
68631
68550
|
try {
|
|
68632
68551
|
ws.send(JSON.stringify({
|
|
@@ -70233,7 +70152,7 @@ init_helpers();
|
|
|
70233
70152
|
init_loader();
|
|
70234
70153
|
import { resolve as resolve32 } from "node:path";
|
|
70235
70154
|
function registerHandoffCommand(program3) {
|
|
70236
|
-
program3.command("handoff <agent>", { hidden: true }).description("
|
|
70155
|
+
program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
|
|
70237
70156
|
const config = getConfig(program3);
|
|
70238
70157
|
const agentConfig = config.agents[agentName];
|
|
70239
70158
|
if (!agentConfig) {
|
|
@@ -70256,17 +70175,13 @@ function registerHandoffCommand(program3) {
|
|
|
70256
70175
|
`);
|
|
70257
70176
|
return;
|
|
70258
70177
|
}
|
|
70259
|
-
const timeoutMs = Math.max(1, parseInt(opts.timeout, 10)) * 1000;
|
|
70260
70178
|
const maxTurns = Math.max(1, parseInt(opts.maxTurns, 10));
|
|
70261
|
-
const model = continuity?.summarizer_model ?? opts.model;
|
|
70262
70179
|
const cappedMaxTurns = continuity?.max_turns_in_briefing ?? maxTurns;
|
|
70263
|
-
const status = await
|
|
70180
|
+
const status = await buildHandoff({
|
|
70264
70181
|
jsonlPath: jsonl,
|
|
70265
70182
|
agentDir,
|
|
70266
70183
|
agentName,
|
|
70267
|
-
|
|
70268
|
-
maxTurns: cappedMaxTurns,
|
|
70269
|
-
timeoutMs
|
|
70184
|
+
maxTurns: cappedMaxTurns
|
|
70270
70185
|
});
|
|
70271
70186
|
process.stderr.write(`handoff: ${status}
|
|
70272
70187
|
`);
|
|
@@ -70279,7 +70194,7 @@ import {
|
|
|
70279
70194
|
existsSync as existsSync53,
|
|
70280
70195
|
mkdirSync as mkdirSync29,
|
|
70281
70196
|
openSync as openSync11,
|
|
70282
|
-
readdirSync as
|
|
70197
|
+
readdirSync as readdirSync20,
|
|
70283
70198
|
readFileSync as readFileSync48,
|
|
70284
70199
|
renameSync as renameSync11,
|
|
70285
70200
|
statSync as statSync24,
|
|
@@ -70768,7 +70683,7 @@ var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
|
|
|
70768
70683
|
function sweepOrphanTmpFiles(stateDir) {
|
|
70769
70684
|
let entries;
|
|
70770
70685
|
try {
|
|
70771
|
-
entries =
|
|
70686
|
+
entries = readdirSync20(stateDir);
|
|
70772
70687
|
} catch {
|
|
70773
70688
|
return;
|
|
70774
70689
|
}
|
|
@@ -72341,7 +72256,7 @@ function registerSoulCommand(program3) {
|
|
|
72341
72256
|
// src/cli/debug.ts
|
|
72342
72257
|
init_helpers();
|
|
72343
72258
|
init_loader();
|
|
72344
|
-
import { existsSync as existsSync59, readFileSync as readFileSync52, readdirSync as
|
|
72259
|
+
import { existsSync as existsSync59, readFileSync as readFileSync52, readdirSync as readdirSync21, statSync as statSync25 } from "node:fs";
|
|
72345
72260
|
import { resolve as resolve37, join as join53 } from "node:path";
|
|
72346
72261
|
import { createHash as createHash12 } from "node:crypto";
|
|
72347
72262
|
init_merge();
|
|
@@ -72360,7 +72275,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
|
72360
72275
|
if (!existsSync59(projectsDir))
|
|
72361
72276
|
return;
|
|
72362
72277
|
try {
|
|
72363
|
-
const entries =
|
|
72278
|
+
const entries = readdirSync21(projectsDir, { withFileTypes: true });
|
|
72364
72279
|
let latest;
|
|
72365
72280
|
for (const entry of entries) {
|
|
72366
72281
|
if (!entry.isDirectory())
|
|
@@ -72598,7 +72513,7 @@ import {
|
|
|
72598
72513
|
mkdirSync as mkdirSync32,
|
|
72599
72514
|
writeFileSync as writeFileSync29,
|
|
72600
72515
|
readFileSync as readFileSync53,
|
|
72601
|
-
readdirSync as
|
|
72516
|
+
readdirSync as readdirSync22,
|
|
72602
72517
|
unlinkSync as unlinkSync12,
|
|
72603
72518
|
existsSync as existsSync60,
|
|
72604
72519
|
renameSync as renameSync12
|
|
@@ -72641,7 +72556,7 @@ function listRecords() {
|
|
|
72641
72556
|
ensureDir2();
|
|
72642
72557
|
const dir = registryDir();
|
|
72643
72558
|
const records = [];
|
|
72644
|
-
for (const entry of
|
|
72559
|
+
for (const entry of readdirSync22(dir)) {
|
|
72645
72560
|
if (!entry.endsWith(".json"))
|
|
72646
72561
|
continue;
|
|
72647
72562
|
const id = entry.slice(0, -5);
|
|
@@ -72988,7 +72903,7 @@ init_scaffold_integration();
|
|
|
72988
72903
|
import {
|
|
72989
72904
|
chmodSync as chmodSync9,
|
|
72990
72905
|
mkdirSync as mkdirSync34,
|
|
72991
|
-
readdirSync as
|
|
72906
|
+
readdirSync as readdirSync23,
|
|
72992
72907
|
rmSync as rmSync15,
|
|
72993
72908
|
writeFileSync as writeFileSync30
|
|
72994
72909
|
} from "node:fs";
|
|
@@ -73161,7 +73076,7 @@ function resolveCredentialsDir(env2) {
|
|
|
73161
73076
|
function writeSeedFile(dir, email, seed) {
|
|
73162
73077
|
mkdirSync34(dir, { recursive: true, mode: 448 });
|
|
73163
73078
|
chmodSync9(dir, 448);
|
|
73164
|
-
for (const name of
|
|
73079
|
+
for (const name of readdirSync23(dir)) {
|
|
73165
73080
|
rmSync15(join56(dir, name), { force: true, recursive: true });
|
|
73166
73081
|
}
|
|
73167
73082
|
const filename = encodeCredentialsFilename(email);
|
|
@@ -73281,9 +73196,9 @@ async function runDriveMcpLauncher(opts) {
|
|
|
73281
73196
|
const tier = opts.tier ?? configSecrets.tier;
|
|
73282
73197
|
const args = buildUvxArgs(tier);
|
|
73283
73198
|
const env2 = buildChildEnv(process.env, credentialsDir, brokerCreds.accountEmail);
|
|
73284
|
-
const { spawn:
|
|
73199
|
+
const { spawn: spawn5 } = await import("node:child_process");
|
|
73285
73200
|
const os5 = await import("node:os");
|
|
73286
|
-
const child =
|
|
73201
|
+
const child = spawn5("uvx", args, {
|
|
73287
73202
|
stdio: ["pipe", "pipe", "inherit"],
|
|
73288
73203
|
env: env2
|
|
73289
73204
|
});
|
|
@@ -73318,7 +73233,7 @@ function registerDriveMcpLauncherCommand(program3) {
|
|
|
73318
73233
|
|
|
73319
73234
|
// src/cli/apply.ts
|
|
73320
73235
|
init_source();
|
|
73321
|
-
import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync67, mkdirSync as mkdirSync36, readdirSync as
|
|
73236
|
+
import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync67, mkdirSync as mkdirSync36, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
|
|
73322
73237
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
73323
73238
|
import { spawnSync as childSpawnSync } from "node:child_process";
|
|
73324
73239
|
import readline from "node:readline";
|
|
@@ -73875,7 +73790,7 @@ import {
|
|
|
73875
73790
|
chownSync as chownSync2,
|
|
73876
73791
|
existsSync as existsSync66,
|
|
73877
73792
|
lstatSync as lstatSync7,
|
|
73878
|
-
readdirSync as
|
|
73793
|
+
readdirSync as readdirSync24,
|
|
73879
73794
|
realpathSync as realpathSync6,
|
|
73880
73795
|
statSync as statSync26
|
|
73881
73796
|
} from "node:fs";
|
|
@@ -73931,7 +73846,7 @@ function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
|
|
|
73931
73846
|
});
|
|
73932
73847
|
const readdir2 = deps.readdir ?? ((p) => {
|
|
73933
73848
|
try {
|
|
73934
|
-
return
|
|
73849
|
+
return readdirSync24(p);
|
|
73935
73850
|
} catch {
|
|
73936
73851
|
return [];
|
|
73937
73852
|
}
|
|
@@ -73977,7 +73892,7 @@ function resolveVaultBindMountDir(homeDir, ctx) {
|
|
|
73977
73892
|
function inspectVaultBindMountDir(vaultDir) {
|
|
73978
73893
|
if (!existsSync67(vaultDir))
|
|
73979
73894
|
return { kind: "missing" };
|
|
73980
|
-
const entries =
|
|
73895
|
+
const entries = readdirSync25(vaultDir);
|
|
73981
73896
|
const unknown = [];
|
|
73982
73897
|
for (const name of entries) {
|
|
73983
73898
|
if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
|
|
@@ -74547,7 +74462,7 @@ function runRedactStdin() {
|
|
|
74547
74462
|
}
|
|
74548
74463
|
|
|
74549
74464
|
// src/cli/status-ask.ts
|
|
74550
|
-
import { readFileSync as readFileSync55, existsSync as existsSync68, readdirSync as
|
|
74465
|
+
import { readFileSync as readFileSync55, existsSync as existsSync68, readdirSync as readdirSync26 } from "node:fs";
|
|
74551
74466
|
import { join as join61 } from "node:path";
|
|
74552
74467
|
import { homedir as homedir34 } from "node:os";
|
|
74553
74468
|
|
|
@@ -74891,7 +74806,7 @@ function resolveSources(explicitPath) {
|
|
|
74891
74806
|
const sources = [];
|
|
74892
74807
|
let entries;
|
|
74893
74808
|
try {
|
|
74894
|
-
entries =
|
|
74809
|
+
entries = readdirSync26(agentsDir);
|
|
74895
74810
|
} catch {
|
|
74896
74811
|
return [];
|
|
74897
74812
|
}
|
|
@@ -75179,7 +75094,7 @@ import {
|
|
|
75179
75094
|
fsyncSync as fsyncSync6,
|
|
75180
75095
|
mkdirSync as mkdirSync38,
|
|
75181
75096
|
openSync as openSync13,
|
|
75182
|
-
readdirSync as
|
|
75097
|
+
readdirSync as readdirSync27,
|
|
75183
75098
|
readFileSync as readFileSync57,
|
|
75184
75099
|
renameSync as renameSync14,
|
|
75185
75100
|
statSync as statSync27,
|
|
@@ -75299,7 +75214,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
|
|
|
75299
75214
|
if (!existsSync70(paths.skillsDir))
|
|
75300
75215
|
return [];
|
|
75301
75216
|
const out = [];
|
|
75302
|
-
for (const name of
|
|
75217
|
+
for (const name of readdirSync27(paths.skillsDir)) {
|
|
75303
75218
|
if (!/\.ya?ml$/i.test(name))
|
|
75304
75219
|
continue;
|
|
75305
75220
|
const full = join63(paths.skillsDir, name);
|
|
@@ -75326,7 +75241,7 @@ function listOverlayEntries(agent, opts = {}) {
|
|
|
75326
75241
|
if (!existsSync70(paths.scheduleDir))
|
|
75327
75242
|
return [];
|
|
75328
75243
|
const out = [];
|
|
75329
|
-
for (const name of
|
|
75244
|
+
for (const name of readdirSync27(paths.scheduleDir)) {
|
|
75330
75245
|
if (!/\.ya?ml$/i.test(name))
|
|
75331
75246
|
continue;
|
|
75332
75247
|
const full = join63(paths.scheduleDir, name);
|
|
@@ -75478,7 +75393,7 @@ import {
|
|
|
75478
75393
|
fsyncSync as fsyncSync7,
|
|
75479
75394
|
mkdirSync as mkdirSync39,
|
|
75480
75395
|
openSync as openSync14,
|
|
75481
|
-
readdirSync as
|
|
75396
|
+
readdirSync as readdirSync28,
|
|
75482
75397
|
readFileSync as readFileSync58,
|
|
75483
75398
|
renameSync as renameSync15,
|
|
75484
75399
|
unlinkSync as unlinkSync15,
|
|
@@ -75534,7 +75449,7 @@ function listPendingScheduleEntries(agent, opts = {}) {
|
|
|
75534
75449
|
if (!existsSync71(dir))
|
|
75535
75450
|
return [];
|
|
75536
75451
|
const out = [];
|
|
75537
|
-
for (const name of
|
|
75452
|
+
for (const name of readdirSync28(dir).sort()) {
|
|
75538
75453
|
if (!name.endsWith(".meta.json"))
|
|
75539
75454
|
continue;
|
|
75540
75455
|
const stageId = name.slice(0, -".meta.json".length);
|
|
@@ -76272,7 +76187,7 @@ init_helpers();
|
|
|
76272
76187
|
init_loader();
|
|
76273
76188
|
import {
|
|
76274
76189
|
existsSync as existsSync75,
|
|
76275
|
-
readdirSync as
|
|
76190
|
+
readdirSync as readdirSync29,
|
|
76276
76191
|
readFileSync as readFileSync61,
|
|
76277
76192
|
renameSync as renameSync16,
|
|
76278
76193
|
statSync as statSync28,
|
|
@@ -76291,7 +76206,7 @@ function planCronUnitRenames(agentsDir, agents) {
|
|
|
76291
76206
|
continue;
|
|
76292
76207
|
let entries;
|
|
76293
76208
|
try {
|
|
76294
|
-
entries =
|
|
76209
|
+
entries = readdirSync29(telegramDir);
|
|
76295
76210
|
} catch {
|
|
76296
76211
|
continue;
|
|
76297
76212
|
}
|
|
@@ -76446,7 +76361,7 @@ function registerMigrateCommand(program3) {
|
|
|
76446
76361
|
// src/cli/hostd.ts
|
|
76447
76362
|
init_source();
|
|
76448
76363
|
init_helpers();
|
|
76449
|
-
import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as
|
|
76364
|
+
import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as readdirSync30, readFileSync as readFileSync62, writeFileSync as writeFileSync34, statSync as statSync29, copyFileSync as copyFileSync12 } from "node:fs";
|
|
76450
76365
|
import { homedir as homedir36 } from "node:os";
|
|
76451
76366
|
import { join as join67 } from "node:path";
|
|
76452
76367
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
@@ -76647,7 +76562,7 @@ function doStatus() {
|
|
|
76647
76562
|
if (existsSync76(dir)) {
|
|
76648
76563
|
const entries = [];
|
|
76649
76564
|
try {
|
|
76650
|
-
for (const name of
|
|
76565
|
+
for (const name of readdirSync30(dir)) {
|
|
76651
76566
|
if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
|
|
76652
76567
|
continue;
|
|
76653
76568
|
const sockPath = join67(dir, name, "sock");
|
package/package.json
CHANGED
|
@@ -422,13 +422,13 @@ mkdir -p "$TELEGRAM_STATE_DIR" 2>/dev/null || true
|
|
|
422
422
|
|
|
423
423
|
{{#if handoffEnabled}}
|
|
424
424
|
# --- Session handoff briefing ---
|
|
425
|
-
# On a normal shutdown the Stop hook writes .handoff.md (
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
# without firing the Stop hook, we fall back to
|
|
430
|
-
#
|
|
431
|
-
# and rely on Hindsight's per-turn auto-recall.
|
|
425
|
+
# On a normal shutdown the Stop hook writes .handoff.md (a bounded
|
|
426
|
+
# raw transcript tail of the prior session — see RFC #1620) into the
|
|
427
|
+
# agent dir. Here we merge that into --append-system-prompt so the
|
|
428
|
+
# fresh session wakes up able to reorient. If the prior session
|
|
429
|
+
# crashed without firing the Stop hook, we fall back to building the
|
|
430
|
+
# tail synchronously, capped at 2s — if that doesn't finish, skip
|
|
431
|
+
# gracefully and rely on Hindsight's per-turn auto-recall.
|
|
432
432
|
#
|
|
433
433
|
# In 'handoff' mode (the default as of #362), when no .handoff.md is
|
|
434
434
|
# available from the Stop hook, we also run handoff-briefing.sh which
|
|
@@ -461,7 +461,7 @@ fi
|
|
|
461
461
|
export SWITCHROOM_HANDOFF_SHOW_LINE={{#if handoffShowLine}}true{{else}}false{{/if}}
|
|
462
462
|
APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
|
|
463
463
|
# Inject .handoff-briefing.md first (assembled from live sources), then
|
|
464
|
-
# .handoff.md (
|
|
464
|
+
# .handoff.md (raw transcript tail from the Stop hook). If both
|
|
465
465
|
# exist, separate them with a divider so the agent sees both.
|
|
466
466
|
if [ -s "$HANDOFF_BRIEFING_FILE" ]; then
|
|
467
467
|
_BRIEFING_CONTENT=$(cat "$HANDOFF_BRIEFING_FILE")
|
|
@@ -108,7 +108,7 @@ If no sub-agents are configured, do the work yourself.
|
|
|
108
108
|
|
|
109
109
|
By default, every restart starts a **fresh `claude` session** — the in-flight transcript is NOT carried over (`session_continuity.resume_mode: handoff`, the default since switchroom #362). Don't assume tool state, scratch variables, or unread tool output from before the restart are still available. What does survive:
|
|
110
110
|
|
|
111
|
-
- **Handoff briefing** — on a clean shutdown, the Stop hook writes a
|
|
111
|
+
- **Handoff briefing** — on a clean shutdown, the Stop hook writes a bounded raw transcript tail of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you can reorient — read it, and lean on your memory files for anything older. If the prior session crashed before the Stop hook fired, a live briefing is assembled from recent Telegram messages, Hindsight recall, and today's daily memory file (`.handoff-briefing.md`).
|
|
112
112
|
- **Hindsight memory** — auto-recall fires on every inbound user message and surfaces relevant memories from past sessions. Long-term facts, decisions, and mental models live here, not in the transcript.
|
|
113
113
|
- **Telegram history** — the gateway's SQLite buffer remembers every inbound/outbound message. Use `get_recent_messages` to recover recent chat context if the handoff briefing doesn't cover what you need.
|
|
114
114
|
- **`SWITCHROOM_PENDING_TURN`** — if your previous session was killed mid-turn (watchdog, SIGTERM, timeout), start.sh exports this env var plus the chat/thread/last-user-message context. Acknowledge the interruption and ask for direction rather than silently resuming.
|
|
@@ -47702,10 +47702,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
47702
47702
|
}
|
|
47703
47703
|
|
|
47704
47704
|
// ../src/build-info.ts
|
|
47705
|
-
var VERSION = "0.13.
|
|
47706
|
-
var COMMIT_SHA = "
|
|
47707
|
-
var COMMIT_DATE = "2026-05-
|
|
47708
|
-
var LATEST_PR =
|
|
47705
|
+
var VERSION = "0.13.4";
|
|
47706
|
+
var COMMIT_SHA = "b4fd0264";
|
|
47707
|
+
var COMMIT_DATE = "2026-05-21T09:34:13Z";
|
|
47708
|
+
var LATEST_PR = 1628;
|
|
47709
47709
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
47710
47710
|
|
|
47711
47711
|
// gateway/boot-version.ts
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-flap resilience scenario — regression guard for #1613 / #1616.
|
|
3
|
+
*
|
|
4
|
+
* ## The bug this guards
|
|
5
|
+
*
|
|
6
|
+
* The handoff-briefing summarizer shells out to a headless `claude -p`
|
|
7
|
+
* once per turn (handoff Stop hook). Before #1616 it ran without
|
|
8
|
+
* `--strict-mcp-config`, so it auto-discovered the agent's project
|
|
9
|
+
* `.mcp.json` and started every MCP server in it — including
|
|
10
|
+
* `switchroom-telegram`. That spun up a *second* telegram bridge
|
|
11
|
+
* process which registered against the same gateway socket as the
|
|
12
|
+
* live agent's real bridge; the two collided under the gateway's
|
|
13
|
+
* register-race close, producing an A↔B "bridge reconnect race" flap
|
|
14
|
+
* every ~2s for the ~7-9s the `claude -p` lived. The handoff hook
|
|
15
|
+
* fires every turn, so did the flap. A turn whose completion landed
|
|
16
|
+
* inside a flap burst could have its `turn_end` signal eaten — the
|
|
17
|
+
* agent looked wedged for that turn.
|
|
18
|
+
*
|
|
19
|
+
* The fix (#1616): the summarizer passes `--strict-mcp-config`, so
|
|
20
|
+
* the headless `claude -p` loads zero MCP servers and never spawns a
|
|
21
|
+
* competing bridge. The structural guard against a new offending
|
|
22
|
+
* callsite is `tests/bridge-flap-regression-guard.test.ts`; this
|
|
23
|
+
* scenario is the behavioural backstop.
|
|
24
|
+
*
|
|
25
|
+
* ## What this scenario asserts (root-cause-agnostic by design)
|
|
26
|
+
*
|
|
27
|
+
* The checks are symptom-based, so they catch a flap reintroduced by
|
|
28
|
+
* ANY future change — not only a regression of #1616:
|
|
29
|
+
*
|
|
30
|
+
* 1. Send a handful of DMs in succession — each drives a turn (and a
|
|
31
|
+
* handoff-hook fire). **Primary assertion:** every DM gets a reply
|
|
32
|
+
* within budget. Directly catches both the flap (eats turn_end)
|
|
33
|
+
* and the wedge (a zero-bridge gap strands the inbound).
|
|
34
|
+
* 2. **Forensic assertion:** inspect the agent's gateway-supervisor.log
|
|
35
|
+
* over the test window and assert the `bridge disconnected` density
|
|
36
|
+
* stays BELOW a flap threshold. One healthy persistent bridge
|
|
37
|
+
* produces only a trickle of disconnects; a sustained reconnect
|
|
38
|
+
* race produces dozens in tight ~2s bursts.
|
|
39
|
+
*
|
|
40
|
+
* ## Why the log inspection
|
|
41
|
+
*
|
|
42
|
+
* A flap is a server-side phenomenon that does not always surface as
|
|
43
|
+
* a missed reply (a burst can self-heal in ~20-30s). The `bridge
|
|
44
|
+
* disconnected` count is the transport-agnostic flap symptom. This
|
|
45
|
+
* scenario shells into the agent container via `docker exec` to read
|
|
46
|
+
* the gateway log; if docker is unavailable the log assertion is
|
|
47
|
+
* skipped with a warning and the responsiveness checks still run.
|
|
48
|
+
*
|
|
49
|
+
* ## Tolerances
|
|
50
|
+
*
|
|
51
|
+
* - `DISCONNECT_FLAP_THRESHOLD` is the max acceptable `bridge
|
|
52
|
+
* disconnected` count over the window. Post-#1616 a healthy ~4-turn
|
|
53
|
+
* run sits well under 16 (measured ~8-13, including anonymous
|
|
54
|
+
* probe-connection churn); a sustained flap is 20-40+. 16 sits
|
|
55
|
+
* comfortably in the gap.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { describe, expect, it } from "vitest";
|
|
59
|
+
import { execSync } from "node:child_process";
|
|
60
|
+
import { spinUp } from "../harness.js";
|
|
61
|
+
import type { ObservedMessage } from "../driver.js";
|
|
62
|
+
|
|
63
|
+
const AGENT = "test-harness";
|
|
64
|
+
const CONTAINER = `switchroom-${AGENT}`;
|
|
65
|
+
const GATEWAY_LOG = "/var/log/switchroom/gateway-supervisor.log";
|
|
66
|
+
|
|
67
|
+
const DM_COUNT = 4;
|
|
68
|
+
const PER_DM_TIMEOUT_MS = 30_000;
|
|
69
|
+
const OVERALL_DEADLINE_MS = 180_000;
|
|
70
|
+
|
|
71
|
+
// Post-#1616 a healthy ~4-turn run logs ~8-13 `bridge disconnected`
|
|
72
|
+
// lines (one persistent bridge + anonymous probe-connection churn).
|
|
73
|
+
// A sustained A↔B flap produces 20-40+ in tight ~2s bursts. 16 sits
|
|
74
|
+
// in the gap.
|
|
75
|
+
const DISCONNECT_FLAP_THRESHOLD = 16;
|
|
76
|
+
|
|
77
|
+
/** Total line count of the agent's gateway-supervisor.log, or null if
|
|
78
|
+
* the container/log is unreachable (CI without the container). */
|
|
79
|
+
function gatewayLogLineCount(): number | null {
|
|
80
|
+
try {
|
|
81
|
+
const out = execSync(
|
|
82
|
+
`docker exec ${CONTAINER} sh -lc 'wc -l < ${GATEWAY_LOG}'`,
|
|
83
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
|
|
84
|
+
);
|
|
85
|
+
return parseInt(out.trim(), 10);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Count `bridge disconnected` lines after `sinceLine` in the log. */
|
|
92
|
+
function disconnectCountSince(sinceLine: number): number | null {
|
|
93
|
+
try {
|
|
94
|
+
const out = execSync(
|
|
95
|
+
`docker exec ${CONTAINER} sh -lc ` +
|
|
96
|
+
`'awk "NR>${sinceLine}" ${GATEWAY_LOG} | grep -c "bridge disconnected" || true'`,
|
|
97
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
|
|
98
|
+
);
|
|
99
|
+
return parseInt(out.trim(), 10);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("uat: bridge-flap resilience — agent stays responsive, gateway does not flap", () => {
|
|
106
|
+
it(
|
|
107
|
+
"every DM gets a reply and the gateway does not flap across turns",
|
|
108
|
+
async () => {
|
|
109
|
+
const baselineLine = gatewayLogLineCount();
|
|
110
|
+
const sc = await spinUp({ agent: AGENT });
|
|
111
|
+
try {
|
|
112
|
+
const overallDeadline = Date.now() + OVERALL_DEADLINE_MS;
|
|
113
|
+
|
|
114
|
+
for (let i = 1; i <= DM_COUNT; i++) {
|
|
115
|
+
await sc.sendDM(`flap-resilience probe ${i}/${DM_COUNT}: reply with OK${i}`);
|
|
116
|
+
|
|
117
|
+
const remaining = Math.min(
|
|
118
|
+
PER_DM_TIMEOUT_MS,
|
|
119
|
+
overallDeadline - Date.now(),
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
remaining,
|
|
123
|
+
`overall deadline hit before DM ${i} — earlier turns were too slow`,
|
|
124
|
+
).toBeGreaterThan(0);
|
|
125
|
+
|
|
126
|
+
const reply = await sc.expectMessage(
|
|
127
|
+
(m: ObservedMessage) => m.fromBot && !m.edited,
|
|
128
|
+
{ from: "bot", timeout: remaining },
|
|
129
|
+
);
|
|
130
|
+
expect(
|
|
131
|
+
reply.text.length,
|
|
132
|
+
`DM ${i}/${DM_COUNT} produced an empty reply — a flap may have ` +
|
|
133
|
+
`eaten the turn_end signal`,
|
|
134
|
+
).toBeGreaterThan(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Responsiveness held for all DM_COUNT turns. Now check the
|
|
138
|
+
// server-side flap signal.
|
|
139
|
+
if (baselineLine == null) {
|
|
140
|
+
console.warn(
|
|
141
|
+
"[bridge-flap-resilience] docker exec unavailable — skipping " +
|
|
142
|
+
"the gateway-log flap assertion; responsiveness checks passed.",
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const disconnectCount = disconnectCountSince(baselineLine);
|
|
148
|
+
expect(
|
|
149
|
+
disconnectCount,
|
|
150
|
+
"could not read gateway log after the run — container went away",
|
|
151
|
+
).not.toBeNull();
|
|
152
|
+
expect(
|
|
153
|
+
disconnectCount as number,
|
|
154
|
+
`gateway logged ${disconnectCount} "bridge disconnected" lines across ` +
|
|
155
|
+
`${DM_COUNT} turns — at/above the flap threshold ` +
|
|
156
|
+
`(${DISCONNECT_FLAP_THRESHOLD}). A parasitic bridge is racing the ` +
|
|
157
|
+
`live one — check for a headless 'claude -p' spawned without ` +
|
|
158
|
+
`--strict-mcp-config (#1613/#1616).`,
|
|
159
|
+
).toBeLessThan(DISCONNECT_FLAP_THRESHOLD);
|
|
160
|
+
} finally {
|
|
161
|
+
await sc.tearDown();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
OVERALL_DEADLINE_MS + 30_000,
|
|
165
|
+
);
|
|
166
|
+
});
|