switchroom 0.21.17 → 0.21.19
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 +1884 -1512
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +6 -1
- package/telegram-plugin/connection-drop.ts +83 -0
- package/telegram-plugin/dist/bridge/bridge.js +41 -2
- package/telegram-plugin/dist/gateway/gateway.js +48 -6
- package/telegram-plugin/dist/server.js +46 -3
- package/telegram-plugin/llm-error-present.ts +25 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +6 -0
- package/telegram-plugin/session-tail.ts +38 -1
- package/telegram-plugin/tests/llm-error-present.test.ts +183 -0
- package/telegram-plugin/uat/flip/allowlist.test.ts +229 -0
- package/telegram-plugin/uat/flip/allowlist.ts +349 -0
- package/telegram-plugin/uat/flip/gate.test.ts +153 -0
- package/telegram-plugin/uat/flip/gate.ts +232 -0
- package/telegram-plugin/uat/flip/probe-scoring.test.ts +210 -0
- package/telegram-plugin/uat/flip/probe-scoring.ts +200 -0
- package/telegram-plugin/uat/flip/probe-suite.test.ts +95 -0
- package/telegram-plugin/uat/flip/probe-suite.ts +155 -0
- package/telegram-plugin/uat/flip/probes/kdogg.probes.json +36 -0
- package/telegram-plugin/uat/flip/probes/test-harness.probes.json +15 -0
- package/telegram-plugin/uat/flip/recall-log.test.ts +131 -0
- package/telegram-plugin/uat/flip/recall-log.ts +178 -0
- package/telegram-plugin/uat/flip/report.ts +95 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.test.ts +470 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.ts +697 -0
- package/telegram-plugin/uat/flip/tier2-probe-runner.ts +327 -0
- package/telegram-plugin/uat/runners/scorer.ts +1 -1
- package/vendor/hindsight-memory/scripts/prefetch.py +33 -5
- package/vendor/hindsight-memory/scripts/recall.py +131 -17
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +97 -6
- package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +70 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.21.
|
|
2123
|
+
var VERSION = "0.21.19", COMMIT_SHA = "7e7061ba", COMMIT_DATE = "2026-08-18T15:36:04Z";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -21340,6 +21340,60 @@ function renderHindsightHooksOverrides(raw, tunables) {
|
|
|
21340
21340
|
return JSON.stringify(root, null, 2) + `
|
|
21341
21341
|
`;
|
|
21342
21342
|
}
|
|
21343
|
+
function readHooksPrefetchAsyncTimeout(raw) {
|
|
21344
|
+
const absent = { present: false, async: false, timeout: null };
|
|
21345
|
+
let parsed;
|
|
21346
|
+
try {
|
|
21347
|
+
parsed = JSON.parse(raw);
|
|
21348
|
+
} catch {
|
|
21349
|
+
return absent;
|
|
21350
|
+
}
|
|
21351
|
+
const hooks = parsed?.hooks;
|
|
21352
|
+
if (hooks == null || typeof hooks !== "object")
|
|
21353
|
+
return absent;
|
|
21354
|
+
const matchers = hooks.Stop;
|
|
21355
|
+
if (!Array.isArray(matchers))
|
|
21356
|
+
return absent;
|
|
21357
|
+
for (const matcher of matchers) {
|
|
21358
|
+
const inner = matcher?.hooks;
|
|
21359
|
+
if (!Array.isArray(inner))
|
|
21360
|
+
continue;
|
|
21361
|
+
for (const hook of inner) {
|
|
21362
|
+
const h = hook;
|
|
21363
|
+
if (h == null || typeof h.command !== "string")
|
|
21364
|
+
continue;
|
|
21365
|
+
if (!h.command.includes(PREFETCH_HOOK_COMMAND_MARKER))
|
|
21366
|
+
continue;
|
|
21367
|
+
return {
|
|
21368
|
+
present: true,
|
|
21369
|
+
async: h.async === true,
|
|
21370
|
+
timeout: typeof h.timeout === "number" ? h.timeout : null
|
|
21371
|
+
};
|
|
21372
|
+
}
|
|
21373
|
+
}
|
|
21374
|
+
return absent;
|
|
21375
|
+
}
|
|
21376
|
+
function validatePrefetchAsyncTimeout(shape, prefetchRecallTimeoutSeconds) {
|
|
21377
|
+
const problems = [];
|
|
21378
|
+
if (!shape.present) {
|
|
21379
|
+
problems.push("hooks/hooks.json registers no Stop hook for prefetch.py (the async " + "recall-prefetch producer)");
|
|
21380
|
+
return problems;
|
|
21381
|
+
}
|
|
21382
|
+
if (!shape.async) {
|
|
21383
|
+
problems.push('the prefetch.py Stop hook is not marked `"async": true` \u2014 a synchronous ' + "prefetch blocks every turn's completion for up to its timeout");
|
|
21384
|
+
}
|
|
21385
|
+
if (shape.timeout === null || shape.timeout <= 0) {
|
|
21386
|
+
problems.push("the prefetch.py Stop hook has no positive `timeout` (async ceiling) \u2014 a " + "wedged producer would never be reaped");
|
|
21387
|
+
return problems;
|
|
21388
|
+
}
|
|
21389
|
+
if (shape.timeout > MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS) {
|
|
21390
|
+
problems.push(`the prefetch.py async ceiling is ${shape.timeout}s, above the ` + `${MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS}s maximum \u2014 a wedged producer holds a ` + `background slot for that long`);
|
|
21391
|
+
}
|
|
21392
|
+
if (prefetchRecallTimeoutSeconds >= shape.timeout) {
|
|
21393
|
+
problems.push(`memoryPrefetchTimeoutSeconds ${prefetchRecallTimeoutSeconds}s is >= the ` + `${shape.timeout}s async hook ceiling \u2014 the producer's own recall can outlive ` + `its hook and be SIGKILLed mid buffer-write, leaving a torn buffer`);
|
|
21394
|
+
}
|
|
21395
|
+
return problems;
|
|
21396
|
+
}
|
|
21343
21397
|
function readHooksRecallTimeout(raw) {
|
|
21344
21398
|
let parsed;
|
|
21345
21399
|
try {
|
|
@@ -21368,7 +21422,7 @@ function readHooksRecallTimeout(raw) {
|
|
|
21368
21422
|
}
|
|
21369
21423
|
return null;
|
|
21370
21424
|
}
|
|
21371
|
-
var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py";
|
|
21425
|
+
var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py", PREFETCH_HOOK_COMMAND_MARKER = "prefetch.py", MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS = 30;
|
|
21372
21426
|
var init_hindsight_recall_tunables = __esm(() => {
|
|
21373
21427
|
init_hindsight_recall_passthrough();
|
|
21374
21428
|
MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
|
|
@@ -28559,6 +28613,25 @@ class DirectiveAdmin {
|
|
|
28559
28613
|
});
|
|
28560
28614
|
return `Deactivated directive '${target.name}' (id ${target.id}) in bank ` + `'${this.opts.bankId}', tagged ${args.tag}.`;
|
|
28561
28615
|
}
|
|
28616
|
+
async deactivateAllActiveByName(args) {
|
|
28617
|
+
const all = await this.list();
|
|
28618
|
+
const named = all.filter((d) => d.name === args.name);
|
|
28619
|
+
if (named.length === 0) {
|
|
28620
|
+
const known = all.map((d) => d.name).sort().join(", ");
|
|
28621
|
+
throw new Error(`no directive named '${args.name}' in bank '${this.opts.bankId}' ` + `(the directive to deactivate). Known directives: ${known || "(none)"}`);
|
|
28622
|
+
}
|
|
28623
|
+
const active = named.filter((d) => d.is_active !== false);
|
|
28624
|
+
const alreadyInactive = named.filter((d) => d.is_active === false).map((d) => ({ id: d.id, name: d.name, priority: d.priority }));
|
|
28625
|
+
for (const d of active)
|
|
28626
|
+
this.refuseIfRulesBlock(d);
|
|
28627
|
+
const deactivated = [];
|
|
28628
|
+
for (const d of active) {
|
|
28629
|
+
if (!args.dryRun)
|
|
28630
|
+
await this.deactivateResolved(all, d);
|
|
28631
|
+
deactivated.push({ id: d.id, name: d.name, priority: d.priority });
|
|
28632
|
+
}
|
|
28633
|
+
return { deactivated, alreadyInactive, dryRun: args.dryRun ?? false };
|
|
28634
|
+
}
|
|
28562
28635
|
async reactivate(args) {
|
|
28563
28636
|
const all = await this.list();
|
|
28564
28637
|
const target = this.resolve(all, args.name, "the directive to reactivate");
|
|
@@ -63701,6 +63774,49 @@ function detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config)
|
|
|
63701
63774
|
}
|
|
63702
63775
|
return findings;
|
|
63703
63776
|
}
|
|
63777
|
+
function detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config) {
|
|
63778
|
+
if (!isHindsightEnabled(config))
|
|
63779
|
+
return [];
|
|
63780
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
63781
|
+
if (resolved.memory?.auto_recall === false)
|
|
63782
|
+
return [];
|
|
63783
|
+
const pluginDir = join66(agentDir, ".claude", "plugins", "hindsight-memory");
|
|
63784
|
+
if (!existsSync69(pluginDir))
|
|
63785
|
+
return [];
|
|
63786
|
+
const settingsPath = join66(pluginDir, "settings.json");
|
|
63787
|
+
if (!existsSync69(settingsPath))
|
|
63788
|
+
return [];
|
|
63789
|
+
let settings = null;
|
|
63790
|
+
try {
|
|
63791
|
+
settings = JSON.parse(readFileSync62(settingsPath, "utf-8"));
|
|
63792
|
+
} catch {
|
|
63793
|
+
return [];
|
|
63794
|
+
}
|
|
63795
|
+
if (settings?.memoryPrefetchEnabled !== true)
|
|
63796
|
+
return [];
|
|
63797
|
+
const hooksPath = join66(pluginDir, "hooks", "hooks.json");
|
|
63798
|
+
if (!existsSync69(hooksPath))
|
|
63799
|
+
return [];
|
|
63800
|
+
let shape;
|
|
63801
|
+
try {
|
|
63802
|
+
shape = readHooksPrefetchAsyncTimeout(readFileSync62(hooksPath, "utf-8"));
|
|
63803
|
+
} catch {
|
|
63804
|
+
return [];
|
|
63805
|
+
}
|
|
63806
|
+
const rawPrefetchTimeout = settings.memoryPrefetchTimeoutSeconds;
|
|
63807
|
+
const prefetchRecallTimeout = typeof rawPrefetchTimeout === "number" && Number.isFinite(rawPrefetchTimeout) && rawPrefetchTimeout > 0 ? rawPrefetchTimeout : 5;
|
|
63808
|
+
const problems = validatePrefetchAsyncTimeout(shape, prefetchRecallTimeout);
|
|
63809
|
+
if (problems.length === 0)
|
|
63810
|
+
return [];
|
|
63811
|
+
return [
|
|
63812
|
+
{
|
|
63813
|
+
surface: "memory-prefetch",
|
|
63814
|
+
agent: name,
|
|
63815
|
+
detail: `async recall-prefetch (memoryPrefetchEnabled) is ON but its async-timeout ` + `config is unsafe: ${problems.join("; ")}`,
|
|
63816
|
+
fix: "Re-stamp the plugin (`switchroom apply`) so hooks.json carries the " + '`prefetch.py` Stop hook with `"async": true` and a timeout above ' + "memoryPrefetchTimeoutSeconds, or set memoryPrefetchEnabled off until the " + "async ceiling is corrected \u2014 a wedged prefetch either blocks the turn or " + "is killed mid buffer-write."
|
|
63817
|
+
}
|
|
63818
|
+
];
|
|
63819
|
+
}
|
|
63704
63820
|
function writeDriftReport(agentDir, findings) {
|
|
63705
63821
|
try {
|
|
63706
63822
|
const report = {
|
|
@@ -63724,6 +63840,7 @@ function detectAgentDrift(name, agentConfigRaw, agentsDir, config, configPath, o
|
|
|
63724
63840
|
}));
|
|
63725
63841
|
findings.push(...detectSkillsDrift(name, agentDir));
|
|
63726
63842
|
findings.push(...detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config));
|
|
63843
|
+
findings.push(...detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config));
|
|
63727
63844
|
if (!opts.skipContainerProbes) {
|
|
63728
63845
|
findings.push(...detectHookScriptDrift(name, {
|
|
63729
63846
|
binDir: opts.binDir,
|
|
@@ -71736,11 +71853,11 @@ var init_apply = __esm(() => {
|
|
|
71736
71853
|
});
|
|
71737
71854
|
|
|
71738
71855
|
// src/host-control/audit-reader.ts
|
|
71739
|
-
import { closeSync as closeSync15, existsSync as
|
|
71856
|
+
import { closeSync as closeSync15, existsSync as existsSync84, openSync as openSync15, readSync as readSync5, statSync as statSync50 } from "node:fs";
|
|
71740
71857
|
import { homedir as homedir43 } from "node:os";
|
|
71741
|
-
import { join as
|
|
71858
|
+
import { join as join91 } from "node:path";
|
|
71742
71859
|
function defaultAuditLogPath2(home2 = homedir43()) {
|
|
71743
|
-
return
|
|
71860
|
+
return join91(home2, ".switchroom", "host-control-audit.log");
|
|
71744
71861
|
}
|
|
71745
71862
|
function auditRotation() {
|
|
71746
71863
|
return resolveRotationConfig({
|
|
@@ -71757,14 +71874,14 @@ function auditReadFullWindowBytes() {
|
|
|
71757
71874
|
function auditReadMaxGenerations() {
|
|
71758
71875
|
return auditRotation().maxFiles;
|
|
71759
71876
|
}
|
|
71760
|
-
function tailBytes(
|
|
71877
|
+
function tailBytes(path9, n, strict = false) {
|
|
71761
71878
|
if (n <= 0)
|
|
71762
71879
|
return "";
|
|
71763
71880
|
let fd;
|
|
71764
71881
|
let size;
|
|
71765
71882
|
try {
|
|
71766
|
-
size =
|
|
71767
|
-
fd = openSync15(
|
|
71883
|
+
size = statSync50(path9).size;
|
|
71884
|
+
fd = openSync15(path9, "r");
|
|
71768
71885
|
} catch (err) {
|
|
71769
71886
|
if (strict && err.code !== "ENOENT")
|
|
71770
71887
|
throw err;
|
|
@@ -71809,7 +71926,7 @@ function readAuditRaw(logPath, opts = {}) {
|
|
|
71809
71926
|
budget -= Buffer.byteLength(active, "utf-8");
|
|
71810
71927
|
for (let n = 1;n <= maxFiles && budget > 0; n++) {
|
|
71811
71928
|
const gen = `${logPath}.${n}`;
|
|
71812
|
-
if (!
|
|
71929
|
+
if (!existsSync84(gen))
|
|
71813
71930
|
break;
|
|
71814
71931
|
const chunk2 = tailBytes(gen, budget, strict);
|
|
71815
71932
|
if (chunk2.length === 0)
|
|
@@ -71987,16 +72104,16 @@ var init_audit_reader = __esm(() => {
|
|
|
71987
72104
|
});
|
|
71988
72105
|
|
|
71989
72106
|
// src/web/fleet-health-read.ts
|
|
71990
|
-
import { readFileSync as
|
|
71991
|
-
import { resolve as
|
|
72107
|
+
import { readFileSync as readFileSync79 } from "node:fs";
|
|
72108
|
+
import { resolve as resolve46 } from "node:path";
|
|
71992
72109
|
import { homedir as homedir46 } from "node:os";
|
|
71993
72110
|
function fleetHealthLedgerPath(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir46()) {
|
|
71994
|
-
return
|
|
72111
|
+
return resolve46(home2, ".switchroom", "fleet-health", "ledger.json");
|
|
71995
72112
|
}
|
|
71996
|
-
function readFleetHealth(
|
|
72113
|
+
function readFleetHealth(path9) {
|
|
71997
72114
|
let ledger;
|
|
71998
72115
|
try {
|
|
71999
|
-
ledger = JSON.parse(
|
|
72116
|
+
ledger = JSON.parse(readFileSync79(path9, "utf-8"));
|
|
72000
72117
|
} catch {
|
|
72001
72118
|
return {
|
|
72002
72119
|
owner_agent: null,
|
|
@@ -72022,8 +72139,8 @@ function readFleetHealth(path7) {
|
|
|
72022
72139
|
...ranked.length === 0 ? { reason: EMPTY_REASON } : {}
|
|
72023
72140
|
};
|
|
72024
72141
|
}
|
|
72025
|
-
function handleGetFleetHealth(
|
|
72026
|
-
return readFleetHealth(
|
|
72142
|
+
function handleGetFleetHealth(path9 = fleetHealthLedgerPath()) {
|
|
72143
|
+
return readFleetHealth(path9);
|
|
72027
72144
|
}
|
|
72028
72145
|
var EMPTY_REASON;
|
|
72029
72146
|
var init_fleet_health_read = __esm(() => {
|
|
@@ -110258,8 +110375,8 @@ init_loader();
|
|
|
110258
110375
|
init_hindsight();
|
|
110259
110376
|
var import_yaml16 = __toESM(require_dist(), 1);
|
|
110260
110377
|
import { spawn as spawn5 } from "node:child_process";
|
|
110261
|
-
import { existsSync as
|
|
110262
|
-
import { join as
|
|
110378
|
+
import { existsSync as existsSync83, readFileSync as readFileSync75, statSync as statSync49 } from "node:fs";
|
|
110379
|
+
import { join as join89 } from "node:path";
|
|
110263
110380
|
|
|
110264
110381
|
// src/cli/memory-rules.ts
|
|
110265
110382
|
init_source();
|
|
@@ -110495,6 +110612,37 @@ function registerMemoryDirectiveCommand(memory, program3) {
|
|
|
110495
110612
|
process.exit(1);
|
|
110496
110613
|
}
|
|
110497
110614
|
}));
|
|
110615
|
+
directive.command("deactivate <agent> <name>").description("Deactivate every ACTIVE directive named <name> in <agent>'s bank by " + "flipping is_active=false \u2014 the M3 directive-placement campaign entry " + "point for retiring a guardrail's in-bank copy once it has been " + "relocated to the agent's always-loaded CLAUDE.md. Matches by NAME " + "(mirrors reconcile's 'every ACTIVE directive named <name>' selection); " + "if multiple active copies share the name, all are deactivated. " + "REVERSIBLE \u2014 only is_active is touched, never a delete or a " + "content/priority rewrite, so it can be reactivated. Idempotent: a " + "re-run over already-inactive copies is a clean no-op, not an error. " + "REFUSES any directive carrying the rules-block marker tag (that stays " + "active until an M3-flip removes the marker) \u2014 the whole call is " + "refused if even one active copy carries it, mutating nothing.").option("--dry-run", "Print what WOULD deactivate without mutating anything").option("--json", "Machine-readable output").action(withConfigError(async (agent, name, opts) => {
|
|
110616
|
+
const admin = resolveDirectiveAdmin(program3, agent);
|
|
110617
|
+
try {
|
|
110618
|
+
const result = await admin.deactivateAllActiveByName({
|
|
110619
|
+
name,
|
|
110620
|
+
...opts.dryRun ? { dryRun: true } : {}
|
|
110621
|
+
});
|
|
110622
|
+
if (opts.json) {
|
|
110623
|
+
console.log(JSON.stringify({ ok: true, agent, name, ...result }));
|
|
110624
|
+
return;
|
|
110625
|
+
}
|
|
110626
|
+
const verb = result.dryRun ? "would deactivate" : "deactivated";
|
|
110627
|
+
if (result.deactivated.length === 0) {
|
|
110628
|
+
console.log(source_default.yellow(`\u2022 no-op: no ACTIVE directive named "${name}" in ${agent}'s bank ` + `(${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} left ` + `untouched). Nothing to do.`));
|
|
110629
|
+
return;
|
|
110630
|
+
}
|
|
110631
|
+
for (const d of result.deactivated) {
|
|
110632
|
+
console.log(source_default.green(` ${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} "${d.name}" ` + `(id ${d.id}, prior priority ${d.priority ?? "unset"})`));
|
|
110633
|
+
}
|
|
110634
|
+
const n = result.deactivated.length;
|
|
110635
|
+
console.log(source_default.green(`${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} ${n} directive` + `${n === 1 ? "" : "s"} named "${name}" in ${agent}'s bank` + (result.alreadyInactive.length > 0 ? ` (${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} skipped)` : "") + (result.dryRun ? " \u2014 nothing was mutated" : ". Reverse with reactivate.")));
|
|
110636
|
+
} catch (e) {
|
|
110637
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
110638
|
+
if (opts.json) {
|
|
110639
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
110640
|
+
} else {
|
|
110641
|
+
console.error(source_default.red(`\u2717 ${msg}`));
|
|
110642
|
+
}
|
|
110643
|
+
process.exit(1);
|
|
110644
|
+
}
|
|
110645
|
+
}));
|
|
110498
110646
|
directive.command("mark-rules-block <agent> <id>").description("Stamp the persisted rules-block marker tag on directive <id> in " + "<agent>'s bank \u2014 the SAME DirectiveAdmin.markRulesBlock write path " + "the batch triage executor uses. Once stamped, deactivate_directive " + "(and every other DirectiveAdmin deactivation path) refuses this " + "directive unconditionally until an M3-flip action removes the " + "marker. This is the real entry point the mental-model-curator " + "skill's interactive triage pass calls for every directive it " + "classifies rules-block, BEFORE presenting the card \u2014 that skill " + "has no other write path to this marker (no MCP tool exposes it), " + "so without this call the code-level refusal never actually " + "arms itself on the interactive path (PR #4760 review follow-up).").option("--json", "Machine-readable output").action(withConfigError(async (agent, id, opts) => {
|
|
110499
110647
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
110500
110648
|
if (selfAgent && selfAgent !== agent) {
|
|
@@ -110526,305 +110674,1227 @@ function registerMemoryDirectiveCommand(memory, program3) {
|
|
|
110526
110674
|
}));
|
|
110527
110675
|
}
|
|
110528
110676
|
|
|
110529
|
-
// src/cli/memory.ts
|
|
110530
|
-
|
|
110531
|
-
|
|
110532
|
-
|
|
110533
|
-
|
|
110534
|
-
|
|
110535
|
-
|
|
110536
|
-
|
|
110537
|
-
|
|
110538
|
-
|
|
110539
|
-
|
|
110540
|
-
|
|
110541
|
-
|
|
110542
|
-
|
|
110543
|
-
|
|
110544
|
-
|
|
110545
|
-
|
|
110546
|
-
|
|
110547
|
-
}
|
|
110677
|
+
// src/cli/memory-flip.ts
|
|
110678
|
+
init_source();
|
|
110679
|
+
init_helpers();
|
|
110680
|
+
init_hindsight2();
|
|
110681
|
+
init_hindsight();
|
|
110682
|
+
init_hindsight_directive_admin();
|
|
110683
|
+
|
|
110684
|
+
// src/memory/directive-triage.ts
|
|
110685
|
+
init_hindsight_directive_admin();
|
|
110686
|
+
var SUPERSEDED_BY_TAG_RE = /^superseded-by:(.+)$/;
|
|
110687
|
+
function classifyDirective(directive, override) {
|
|
110688
|
+
const isActive = directive.is_active !== false;
|
|
110689
|
+
const overrideSignal = override?.signal.trim();
|
|
110690
|
+
if (hasRulesBlockMarker(directive.tags)) {
|
|
110691
|
+
return {
|
|
110692
|
+
category: "rules-block",
|
|
110693
|
+
signal: "carries the rules-block marker tag \u2014 staged for M3, never retired",
|
|
110694
|
+
action: "stage-for-m3"
|
|
110695
|
+
};
|
|
110548
110696
|
}
|
|
110549
|
-
|
|
110550
|
-
|
|
110551
|
-
|
|
110552
|
-
|
|
110553
|
-
|
|
110554
|
-
|
|
110555
|
-
}
|
|
110556
|
-
|
|
110557
|
-
|
|
110558
|
-
|
|
110559
|
-
|
|
110560
|
-
|
|
110697
|
+
if (override && override.category === "rules-block") {
|
|
110698
|
+
return {
|
|
110699
|
+
category: "rules-block",
|
|
110700
|
+
signal: overrideSignal || "rules-block override with no signal text \u2014 staged for M3, never retired",
|
|
110701
|
+
action: "stage-for-m3"
|
|
110702
|
+
};
|
|
110703
|
+
}
|
|
110704
|
+
const supersessionTag = (directive.tags ?? []).map((t) => SUPERSEDED_BY_TAG_RE.exec(t)).find((m) => m !== null);
|
|
110705
|
+
if (supersessionTag) {
|
|
110706
|
+
const winner = supersessionTag[1];
|
|
110707
|
+
return {
|
|
110708
|
+
category: "retire",
|
|
110709
|
+
signal: isActive ? `superseded by '${winner}' (superseded-by tag)` : `superseded by '${winner}' (superseded-by tag) \u2014 already inactive, nothing to retire`,
|
|
110710
|
+
action: isActive ? "retire" : "keep",
|
|
110711
|
+
supersededBy: winner
|
|
110712
|
+
};
|
|
110713
|
+
}
|
|
110714
|
+
if (override && overrideSignal) {
|
|
110715
|
+
if (override.category === "reflect-directive") {
|
|
110716
|
+
return { category: "reflect-directive", signal: overrideSignal, action: "keep" };
|
|
110561
110717
|
}
|
|
110562
|
-
|
|
110563
|
-
|
|
110564
|
-
|
|
110565
|
-
|
|
110718
|
+
return {
|
|
110719
|
+
category: override.category,
|
|
110720
|
+
signal: isActive ? overrideSignal : `${overrideSignal} \u2014 already inactive, nothing to retire`,
|
|
110721
|
+
action: isActive ? "retire" : "keep"
|
|
110722
|
+
};
|
|
110566
110723
|
}
|
|
110567
|
-
|
|
110568
|
-
|
|
110569
|
-
|
|
110570
|
-
|
|
110571
|
-
}
|
|
110572
|
-
function hindsightSecretValues(input) {
|
|
110573
|
-
const out = [];
|
|
110574
|
-
const push = (v) => {
|
|
110575
|
-
if (typeof v === "string" && v.trim().length > 0)
|
|
110576
|
-
out.push(v.trim());
|
|
110724
|
+
return {
|
|
110725
|
+
category: "reflect-directive",
|
|
110726
|
+
signal: "no deterministic signal \u2014 defaults to KEEP",
|
|
110727
|
+
action: "keep"
|
|
110577
110728
|
};
|
|
110578
|
-
push(input.cpAccessKey);
|
|
110579
|
-
push(input.llm?.api_key);
|
|
110580
|
-
for (const op of ["retain", "reflect", "consolidation"]) {
|
|
110581
|
-
push(input.llm?.[op]?.api_key);
|
|
110582
|
-
}
|
|
110583
|
-
return out;
|
|
110584
110729
|
}
|
|
110585
|
-
function
|
|
110586
|
-
|
|
110587
|
-
|
|
110588
|
-
|
|
110589
|
-
|
|
110590
|
-
|
|
110730
|
+
function buildDirectiveTriageRows(directives, overrides) {
|
|
110731
|
+
return directives.map((d) => {
|
|
110732
|
+
const classified = classifyDirective(d, overrides?.get(d.id));
|
|
110733
|
+
return {
|
|
110734
|
+
id: d.id,
|
|
110735
|
+
name: d.name,
|
|
110736
|
+
priority: d.priority ?? 0,
|
|
110737
|
+
isActive: d.is_active !== false,
|
|
110738
|
+
...classified
|
|
110739
|
+
};
|
|
110740
|
+
});
|
|
110591
110741
|
}
|
|
110592
|
-
|
|
110593
|
-
|
|
110742
|
+
|
|
110743
|
+
// src/cli/debug.ts
|
|
110744
|
+
init_helpers();
|
|
110745
|
+
init_loader();
|
|
110746
|
+
import { existsSync as existsSync82, readFileSync as readFileSync74, readdirSync as readdirSync30, statSync as statSync48 } from "node:fs";
|
|
110747
|
+
import { resolve as resolve43, join as join88 } from "node:path";
|
|
110748
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
110749
|
+
|
|
110750
|
+
// src/agents/workspace.ts
|
|
110751
|
+
import { readFile as readFile2, stat } from "node:fs/promises";
|
|
110752
|
+
import path8 from "node:path";
|
|
110753
|
+
|
|
110754
|
+
// src/agents/bootstrap-budget.ts
|
|
110755
|
+
import path7 from "node:path";
|
|
110756
|
+
var DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
|
|
110757
|
+
var DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
|
|
110758
|
+
var DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
|
|
110759
|
+
|
|
110760
|
+
class BootstrapBudgetExceededError extends Error {
|
|
110761
|
+
analysis;
|
|
110762
|
+
constructor(analysis, message) {
|
|
110763
|
+
super(message);
|
|
110764
|
+
this.analysis = analysis;
|
|
110765
|
+
this.name = "BootstrapBudgetExceededError";
|
|
110766
|
+
}
|
|
110594
110767
|
}
|
|
110595
|
-
|
|
110596
|
-
|
|
110597
|
-
|
|
110768
|
+
function normalizePositiveLimit(value) {
|
|
110769
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
110770
|
+
return 1;
|
|
110771
|
+
}
|
|
110772
|
+
return Math.floor(value);
|
|
110598
110773
|
}
|
|
110599
|
-
function
|
|
110600
|
-
|
|
110601
|
-
|
|
110602
|
-
|
|
110603
|
-
|
|
110604
|
-
try {
|
|
110605
|
-
raw = readFileSync74(path7, "utf-8");
|
|
110606
|
-
} catch {
|
|
110774
|
+
function formatWarningCause(cause) {
|
|
110775
|
+
return cause === "per-file-limit" ? "max/file" : "max/total";
|
|
110776
|
+
}
|
|
110777
|
+
function normalizeSeenSignatures(signatures) {
|
|
110778
|
+
if (!Array.isArray(signatures) || signatures.length === 0) {
|
|
110607
110779
|
return [];
|
|
110608
110780
|
}
|
|
110609
|
-
const
|
|
110610
|
-
|
|
110611
|
-
const
|
|
110612
|
-
|
|
110613
|
-
|
|
110614
|
-
|
|
110615
|
-
|
|
110616
|
-
|
|
110781
|
+
const seen = new Set;
|
|
110782
|
+
const result = [];
|
|
110783
|
+
for (const signature of signatures) {
|
|
110784
|
+
const value = typeof signature === "string" ? signature.trim() : "";
|
|
110785
|
+
if (!value || seen.has(value)) {
|
|
110786
|
+
continue;
|
|
110787
|
+
}
|
|
110788
|
+
seen.add(value);
|
|
110789
|
+
result.push(value);
|
|
110617
110790
|
}
|
|
110618
|
-
return
|
|
110791
|
+
return result;
|
|
110619
110792
|
}
|
|
110620
|
-
function
|
|
110621
|
-
|
|
110622
|
-
|
|
110623
|
-
const total = entries.length;
|
|
110624
|
-
const hits = entries.filter((e) => e.cache_hit).length;
|
|
110625
|
-
const cappedTurns = entries.filter((e) => e.capped).length;
|
|
110626
|
-
const omittedTurns = entries.filter((e) => typeof e.directives_omitted === "number" && e.directives_omitted > 0).length;
|
|
110627
|
-
const memCounts = entries.map((e) => e.result_count).filter((n) => typeof n === "number");
|
|
110628
|
-
const avg = memCounts.length > 0 ? Math.round(memCounts.reduce((s, n) => s + n, 0) / memCounts.length * 10) / 10 : null;
|
|
110629
|
-
const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
|
|
110630
|
-
const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
|
|
110631
|
-
const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
|
|
110632
|
-
const collapseTurns = entries.filter((e) => typeof e.injected_own_bank_count === "number" && e.injected_own_bank_count === 0 && typeof e.injected_additional_bank_count === "number" && e.injected_additional_bank_count > 0).length;
|
|
110633
|
-
lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "") + (collapseTurns > 0 ? source_default.red(` own_bank_collapse_turns=${collapseTurns}`) : "")));
|
|
110634
|
-
for (const e of entries) {
|
|
110635
|
-
const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
|
|
110636
|
-
const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
|
|
110637
|
-
const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
|
|
110638
|
-
const ids = e.memory_ids && e.memory_ids.length > 0 ? source_default.dim(` ids=${e.memory_ids.slice(0, 3).join(",")}${e.memory_ids.length > 3 ? `\u2026+${e.memory_ids.length - 3}` : ""}`) : "";
|
|
110639
|
-
const own = e.injected_own_bank_count;
|
|
110640
|
-
const additional = e.injected_additional_bank_count;
|
|
110641
|
-
const composition = typeof own === "number" || typeof additional === "number" ? (() => {
|
|
110642
|
-
const text = ` own/add=${own ?? "\u2014"}/${additional ?? "\u2014"}`;
|
|
110643
|
-
return own === 0 && typeof additional === "number" && additional > 0 ? source_default.red(text) : source_default.dim(text);
|
|
110644
|
-
})() : "";
|
|
110645
|
-
const score = typeof e.injected_score_min === "number" && typeof e.injected_score_median === "number" && typeof e.injected_score_max === "number" ? source_default.dim(` score=${e.injected_score_min.toFixed(2)}/` + `${e.injected_score_median.toFixed(2)}/` + `${e.injected_score_max.toFixed(2)}`) : "";
|
|
110646
|
-
lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${composition}${ids}`);
|
|
110793
|
+
function appendSeenSignature(signatures, signature) {
|
|
110794
|
+
if (!signature.trim()) {
|
|
110795
|
+
return signatures;
|
|
110647
110796
|
}
|
|
110648
|
-
|
|
110797
|
+
if (signatures.includes(signature)) {
|
|
110798
|
+
return signatures;
|
|
110799
|
+
}
|
|
110800
|
+
const next = [...signatures, signature];
|
|
110801
|
+
if (next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX) {
|
|
110802
|
+
return next;
|
|
110803
|
+
}
|
|
110804
|
+
return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
|
|
110649
110805
|
}
|
|
110650
|
-
function
|
|
110651
|
-
const
|
|
110652
|
-
|
|
110653
|
-
|
|
110654
|
-
|
|
110655
|
-
|
|
110656
|
-
|
|
110657
|
-
process.exit(1);
|
|
110658
|
-
}
|
|
110659
|
-
const collection = getCollectionForAgent(opts.agent, config);
|
|
110660
|
-
console.log(source_default.bold(`
|
|
110661
|
-
Search: ${opts.agent} (collection: ${collection})
|
|
110662
|
-
`));
|
|
110663
|
-
console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
|
|
110664
|
-
console.log();
|
|
110665
|
-
return;
|
|
110666
|
-
}
|
|
110667
|
-
const agentNames = Object.keys(config.agents);
|
|
110668
|
-
console.log(source_default.bold(`
|
|
110669
|
-
Searching all eligible collections:
|
|
110670
|
-
`));
|
|
110671
|
-
for (const name of agentNames) {
|
|
110672
|
-
const collection = getCollectionForAgent(name, config);
|
|
110673
|
-
if (isStrictIsolation(name, config)) {
|
|
110674
|
-
console.log(source_default.gray(` ${name} (${collection}) \u2014 skipped (strict isolation)`));
|
|
110675
|
-
continue;
|
|
110676
|
-
}
|
|
110677
|
-
console.log(source_default.cyan(` ${name} (${collection}):`));
|
|
110678
|
-
console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
|
|
110679
|
-
}
|
|
110680
|
-
console.log();
|
|
110681
|
-
}));
|
|
110682
|
-
memory.command("stats").description("List agents with their collection names and isolation mode").action(withConfigError(async () => {
|
|
110683
|
-
const config = getConfig(program3);
|
|
110684
|
-
const agentNames = Object.keys(config.agents);
|
|
110685
|
-
if (agentNames.length === 0) {
|
|
110686
|
-
console.log(source_default.yellow("No agents defined in switchroom.yaml"));
|
|
110687
|
-
return;
|
|
110688
|
-
}
|
|
110689
|
-
const headers = ["Agent", "Collection", "Isolation", "Auto-recall"];
|
|
110690
|
-
const widths = [20, 20, 12, 12];
|
|
110691
|
-
const headerLine = headers.map((h, i2) => source_default.bold(h.padEnd(widths[i2]))).join(" ");
|
|
110692
|
-
console.log(`
|
|
110693
|
-
${headerLine}`);
|
|
110694
|
-
for (const name of agentNames) {
|
|
110695
|
-
const collection = getCollectionForAgent(name, config);
|
|
110696
|
-
const isolation = isStrictIsolation(name, config) ? "strict" : "default";
|
|
110697
|
-
const autoRecall = config.agents[name].memory?.auto_recall ?? true;
|
|
110698
|
-
const row = [
|
|
110699
|
-
name.padEnd(widths[0]),
|
|
110700
|
-
collection.padEnd(widths[1]),
|
|
110701
|
-
isolation.padEnd(widths[2]),
|
|
110702
|
-
(autoRecall ? "yes" : "no").padEnd(widths[3])
|
|
110703
|
-
].join(" ");
|
|
110704
|
-
console.log(` ${row}`);
|
|
110806
|
+
function buildBootstrapInjectionStats(params) {
|
|
110807
|
+
const injectedByPath = new Map;
|
|
110808
|
+
const injectedByBaseName = new Map;
|
|
110809
|
+
for (const file of params.injectedFiles) {
|
|
110810
|
+
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
|
|
110811
|
+
if (!pathValue) {
|
|
110812
|
+
continue;
|
|
110705
110813
|
}
|
|
110706
|
-
|
|
110707
|
-
|
|
110708
|
-
if (profileBanks.size > 0) {
|
|
110709
|
-
const owners = new Map;
|
|
110710
|
-
for (const [uname, u] of Object.entries(config.users ?? {})) {
|
|
110711
|
-
if (u.profile_bank) {
|
|
110712
|
-
owners.set(u.profile_bank, [
|
|
110713
|
-
...owners.get(u.profile_bank) ?? [],
|
|
110714
|
-
uname
|
|
110715
|
-
]);
|
|
110716
|
-
}
|
|
110717
|
-
}
|
|
110718
|
-
console.log(source_default.bold(` Profile banks (per-user / shared):
|
|
110719
|
-
`));
|
|
110720
|
-
for (const bank of [...profileBanks].sort()) {
|
|
110721
|
-
const who = owners.get(bank);
|
|
110722
|
-
const label = who ? `user: ${who.join(", ")}` : "shared";
|
|
110723
|
-
console.log(` ${bank.padEnd(widths[1])} ${source_default.gray(label)}`);
|
|
110724
|
-
}
|
|
110725
|
-
console.log();
|
|
110814
|
+
if (!injectedByPath.has(pathValue)) {
|
|
110815
|
+
injectedByPath.set(pathValue, file.content);
|
|
110726
110816
|
}
|
|
110727
|
-
|
|
110728
|
-
|
|
110729
|
-
|
|
110730
|
-
|
|
110731
|
-
console.log(source_default.gray(` $ ${getMemoryStats(collection)}`));
|
|
110817
|
+
const normalizedPath = pathValue.replace(/\\/g, "/");
|
|
110818
|
+
const baseName = path7.posix.basename(normalizedPath);
|
|
110819
|
+
if (!injectedByBaseName.has(baseName)) {
|
|
110820
|
+
injectedByBaseName.set(baseName, file.content);
|
|
110732
110821
|
}
|
|
110733
|
-
|
|
110734
|
-
|
|
110735
|
-
|
|
110736
|
-
const
|
|
110737
|
-
const
|
|
110738
|
-
|
|
110739
|
-
|
|
110740
|
-
|
|
110741
|
-
|
|
110742
|
-
|
|
110743
|
-
|
|
110744
|
-
|
|
110745
|
-
|
|
110822
|
+
}
|
|
110823
|
+
return params.bootstrapFiles.map((file) => {
|
|
110824
|
+
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
|
|
110825
|
+
const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
|
|
110826
|
+
const injected = (pathValue ? injectedByPath.get(pathValue) : undefined) ?? injectedByPath.get(file.name) ?? injectedByBaseName.get(file.name);
|
|
110827
|
+
const injectedChars = injected ? injected.length : 0;
|
|
110828
|
+
const truncated = !file.missing && injectedChars < rawChars;
|
|
110829
|
+
return {
|
|
110830
|
+
name: file.name,
|
|
110831
|
+
path: pathValue || file.name,
|
|
110832
|
+
missing: file.missing,
|
|
110833
|
+
rawChars,
|
|
110834
|
+
injectedChars,
|
|
110835
|
+
truncated
|
|
110836
|
+
};
|
|
110837
|
+
});
|
|
110838
|
+
}
|
|
110839
|
+
function analyzeBootstrapBudget(params) {
|
|
110840
|
+
const bootstrapMaxChars = normalizePositiveLimit(params.bootstrapMaxChars);
|
|
110841
|
+
const bootstrapTotalMaxChars = normalizePositiveLimit(params.bootstrapTotalMaxChars);
|
|
110842
|
+
const nearLimitRatio = typeof params.nearLimitRatio === "number" && Number.isFinite(params.nearLimitRatio) && params.nearLimitRatio > 0 && params.nearLimitRatio < 1 ? params.nearLimitRatio : DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO;
|
|
110843
|
+
const nonMissing = params.files.filter((file) => !file.missing);
|
|
110844
|
+
const rawChars = nonMissing.reduce((sum, file) => sum + file.rawChars, 0);
|
|
110845
|
+
const injectedChars = nonMissing.reduce((sum, file) => sum + file.injectedChars, 0);
|
|
110846
|
+
const totalNearLimit = injectedChars >= Math.ceil(bootstrapTotalMaxChars * nearLimitRatio);
|
|
110847
|
+
const totalOverLimit = injectedChars >= bootstrapTotalMaxChars;
|
|
110848
|
+
const files = params.files.map((file) => {
|
|
110849
|
+
if (file.missing) {
|
|
110850
|
+
return { ...file, nearLimit: false, causes: [] };
|
|
110746
110851
|
}
|
|
110747
|
-
|
|
110748
|
-
|
|
110749
|
-
|
|
110750
|
-
|
|
110751
|
-
|
|
110852
|
+
const perFileOverLimit = file.rawChars > bootstrapMaxChars;
|
|
110853
|
+
const nearLimit = file.rawChars >= Math.ceil(bootstrapMaxChars * nearLimitRatio);
|
|
110854
|
+
const causes = [];
|
|
110855
|
+
if (file.truncated) {
|
|
110856
|
+
if (perFileOverLimit) {
|
|
110857
|
+
causes.push("per-file-limit");
|
|
110752
110858
|
}
|
|
110753
|
-
|
|
110754
|
-
|
|
110755
|
-
console.log(source_default.bold(`
|
|
110756
|
-
Hindsight CLI commands:
|
|
110757
|
-
`));
|
|
110758
|
-
for (const cmd of commands) {
|
|
110759
|
-
console.log(source_default.gray(` $ ${cmd}`));
|
|
110859
|
+
if (totalOverLimit) {
|
|
110860
|
+
causes.push("total-limit");
|
|
110760
110861
|
}
|
|
110761
|
-
} else {
|
|
110762
|
-
console.log(source_default.yellow(`
|
|
110763
|
-
No eligible collections for reflection.`));
|
|
110764
110862
|
}
|
|
110765
|
-
|
|
110766
|
-
})
|
|
110767
|
-
|
|
110768
|
-
|
|
110769
|
-
|
|
110770
|
-
|
|
110771
|
-
|
|
110772
|
-
|
|
110773
|
-
|
|
110774
|
-
|
|
110775
|
-
|
|
110776
|
-
|
|
110777
|
-
|
|
110778
|
-
|
|
110779
|
-
|
|
110780
|
-
|
|
110781
|
-
|
|
110782
|
-
`));
|
|
110783
|
-
console.log(source_default.gray(" Run 'switchroom memory setup' to start it."));
|
|
110784
|
-
}
|
|
110785
|
-
return;
|
|
110863
|
+
return { ...file, nearLimit, causes };
|
|
110864
|
+
});
|
|
110865
|
+
const truncatedFiles = files.filter((file) => file.truncated);
|
|
110866
|
+
const nearLimitFiles = files.filter((file) => file.nearLimit);
|
|
110867
|
+
return {
|
|
110868
|
+
files,
|
|
110869
|
+
truncatedFiles,
|
|
110870
|
+
nearLimitFiles,
|
|
110871
|
+
totalNearLimit,
|
|
110872
|
+
hasTruncation: truncatedFiles.length > 0,
|
|
110873
|
+
totals: {
|
|
110874
|
+
rawChars,
|
|
110875
|
+
injectedChars,
|
|
110876
|
+
truncatedChars: Math.max(0, rawChars - injectedChars),
|
|
110877
|
+
bootstrapMaxChars,
|
|
110878
|
+
bootstrapTotalMaxChars,
|
|
110879
|
+
nearLimitRatio
|
|
110786
110880
|
}
|
|
110787
|
-
|
|
110788
|
-
|
|
110789
|
-
|
|
110790
|
-
|
|
110791
|
-
|
|
110792
|
-
|
|
110793
|
-
|
|
110794
|
-
|
|
110795
|
-
|
|
110796
|
-
|
|
110797
|
-
|
|
110798
|
-
|
|
110799
|
-
|
|
110800
|
-
|
|
110881
|
+
};
|
|
110882
|
+
}
|
|
110883
|
+
function buildBootstrapTruncationSignature(analysis) {
|
|
110884
|
+
if (!analysis.hasTruncation) {
|
|
110885
|
+
return;
|
|
110886
|
+
}
|
|
110887
|
+
const files = analysis.truncatedFiles.map((file) => ({
|
|
110888
|
+
path: file.path || file.name,
|
|
110889
|
+
rawChars: file.rawChars,
|
|
110890
|
+
injectedChars: file.injectedChars,
|
|
110891
|
+
causes: [...file.causes].sort()
|
|
110892
|
+
})).sort((a, b) => {
|
|
110893
|
+
const pathCmp = a.path.localeCompare(b.path);
|
|
110894
|
+
if (pathCmp !== 0) {
|
|
110895
|
+
return pathCmp;
|
|
110801
110896
|
}
|
|
110802
|
-
if (
|
|
110803
|
-
|
|
110804
|
-
Docker is not available.`));
|
|
110805
|
-
console.log(source_default.gray(` Install Docker: https://docs.docker.com/get-docker/
|
|
110806
|
-
`));
|
|
110807
|
-
process.exit(1);
|
|
110897
|
+
if (a.rawChars !== b.rawChars) {
|
|
110898
|
+
return a.rawChars - b.rawChars;
|
|
110808
110899
|
}
|
|
110809
|
-
|
|
110810
|
-
|
|
110811
|
-
const getResolvedLlm = () => {
|
|
110812
|
-
if (!resolvedLlmPromise) {
|
|
110813
|
-
resolvedLlmPromise = resolveHindsightLlmSecrets(getConfig(program3).hindsight?.llm);
|
|
110814
|
-
}
|
|
110815
|
-
return resolvedLlmPromise;
|
|
110816
|
-
};
|
|
110817
|
-
let releasePin;
|
|
110818
|
-
try {
|
|
110819
|
-
releasePin = getConfig(program3).release?.pin ?? undefined;
|
|
110820
|
-
} catch {
|
|
110821
|
-
releasePin = undefined;
|
|
110900
|
+
if (a.injectedChars !== b.injectedChars) {
|
|
110901
|
+
return a.injectedChars - b.injectedChars;
|
|
110822
110902
|
}
|
|
110823
|
-
|
|
110824
|
-
|
|
110825
|
-
|
|
110826
|
-
|
|
110827
|
-
|
|
110903
|
+
return a.causes.join("+").localeCompare(b.causes.join("+"));
|
|
110904
|
+
});
|
|
110905
|
+
return JSON.stringify({
|
|
110906
|
+
bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
|
|
110907
|
+
bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
|
|
110908
|
+
files
|
|
110909
|
+
});
|
|
110910
|
+
}
|
|
110911
|
+
function formatBootstrapTruncationWarningLines(params) {
|
|
110912
|
+
if (!params.analysis.hasTruncation) {
|
|
110913
|
+
return [];
|
|
110914
|
+
}
|
|
110915
|
+
const maxFiles = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
|
|
110916
|
+
const lines = [];
|
|
110917
|
+
const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
|
|
110918
|
+
acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
|
|
110919
|
+
return acc;
|
|
110920
|
+
}, new Map);
|
|
110921
|
+
const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
|
|
110922
|
+
for (const file of topFiles) {
|
|
110923
|
+
const pct = file.rawChars > 0 ? Math.round((file.rawChars - file.injectedChars) / file.rawChars * 100) : 0;
|
|
110924
|
+
const causeText = file.causes.length > 0 ? file.causes.map((cause) => formatWarningCause(cause)).join(", ") : "";
|
|
110925
|
+
const nameLabel = (duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0 ? `${file.name} (${file.path})` : file.name;
|
|
110926
|
+
lines.push(`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`);
|
|
110927
|
+
}
|
|
110928
|
+
if (params.analysis.truncatedFiles.length > topFiles.length) {
|
|
110929
|
+
lines.push(`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`);
|
|
110930
|
+
}
|
|
110931
|
+
lines.push("If unintentional, raise switchroom workspace bootstrapMaxChars and/or bootstrapTotalMaxChars config.");
|
|
110932
|
+
return lines;
|
|
110933
|
+
}
|
|
110934
|
+
function buildBootstrapPromptWarning(params) {
|
|
110935
|
+
const signature = buildBootstrapTruncationSignature(params.analysis);
|
|
110936
|
+
let seenSignatures = normalizeSeenSignatures(params.seenSignatures);
|
|
110937
|
+
if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
|
|
110938
|
+
seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
|
|
110939
|
+
}
|
|
110940
|
+
const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
|
|
110941
|
+
const warningShown = params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
|
|
110942
|
+
const warningSignaturesSeen = signature && params.mode !== "off" ? appendSeenSignature(seenSignatures, signature) : seenSignatures;
|
|
110943
|
+
return {
|
|
110944
|
+
signature,
|
|
110945
|
+
warningShown,
|
|
110946
|
+
lines: warningShown ? formatBootstrapTruncationWarningLines({
|
|
110947
|
+
analysis: params.analysis,
|
|
110948
|
+
maxFiles: params.maxFiles
|
|
110949
|
+
}) : [],
|
|
110950
|
+
warningSignaturesSeen
|
|
110951
|
+
};
|
|
110952
|
+
}
|
|
110953
|
+
|
|
110954
|
+
// src/agents/bootstrap-types.ts
|
|
110955
|
+
var DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
|
110956
|
+
var DEFAULT_SOUL_DEFAULT_FILENAME = "SOUL.default.md";
|
|
110957
|
+
var DEFAULT_SOUL_FILENAME = "SOUL.md";
|
|
110958
|
+
var DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
|
110959
|
+
var DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
110960
|
+
var DEFAULT_USER_FILENAME = "USER.md";
|
|
110961
|
+
var DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
110962
|
+
var DEFAULT_MEMORY_FILENAME = "MEMORY.md";
|
|
110963
|
+
|
|
110964
|
+
// src/agents/workspace.ts
|
|
110965
|
+
var DEFAULT_WORKSPACE_DIR_NAME = "workspace";
|
|
110966
|
+
var DEFAULT_MEMORY_SUBDIR = "memory";
|
|
110967
|
+
var STABLE_BOOTSTRAP_FILENAMES = [
|
|
110968
|
+
DEFAULT_AGENTS_FILENAME,
|
|
110969
|
+
DEFAULT_SOUL_DEFAULT_FILENAME,
|
|
110970
|
+
DEFAULT_SOUL_FILENAME,
|
|
110971
|
+
DEFAULT_IDENTITY_FILENAME,
|
|
110972
|
+
DEFAULT_USER_FILENAME,
|
|
110973
|
+
DEFAULT_TOOLS_FILENAME,
|
|
110974
|
+
DEFAULT_BOOTSTRAP_FILENAME
|
|
110975
|
+
];
|
|
110976
|
+
var DYNAMIC_BOOTSTRAP_FILENAMES = [
|
|
110977
|
+
DEFAULT_MEMORY_FILENAME
|
|
110978
|
+
];
|
|
110979
|
+
var DEFAULT_BOOTSTRAP_MAX_CHARS = 12000;
|
|
110980
|
+
var DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 64000;
|
|
110981
|
+
var DEFAULT_DYNAMIC_TOTAL_MAX_CHARS = 24000;
|
|
110982
|
+
function resolveAgentWorkspaceDir(agentDir) {
|
|
110983
|
+
return path8.join(agentDir, DEFAULT_WORKSPACE_DIR_NAME);
|
|
110984
|
+
}
|
|
110985
|
+
async function readOptionalFile(filePath) {
|
|
110986
|
+
try {
|
|
110987
|
+
const content = await readFile2(filePath, "utf8");
|
|
110988
|
+
return content;
|
|
110989
|
+
} catch (err) {
|
|
110990
|
+
if (isErrnoException(err) && (err.code === "ENOENT" || err.code === "EISDIR")) {
|
|
110991
|
+
return;
|
|
110992
|
+
}
|
|
110993
|
+
throw err;
|
|
110994
|
+
}
|
|
110995
|
+
}
|
|
110996
|
+
function isErrnoException(value) {
|
|
110997
|
+
return value instanceof Error && typeof value.code === "string";
|
|
110998
|
+
}
|
|
110999
|
+
async function loadNamedFile(workspaceDir, name, relativePath) {
|
|
111000
|
+
const filePath = path8.join(workspaceDir, relativePath ?? name);
|
|
111001
|
+
const content = await readOptionalFile(filePath);
|
|
111002
|
+
if (content === undefined) {
|
|
111003
|
+
return { name, path: filePath, missing: true };
|
|
111004
|
+
}
|
|
111005
|
+
return { name, path: filePath, content, missing: false };
|
|
111006
|
+
}
|
|
111007
|
+
async function loadStableBootstrapFiles(workspaceDir, options) {
|
|
111008
|
+
const defaultFiles = await Promise.all(STABLE_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
|
|
111009
|
+
const extras = options?.extraStableFiles ?? [];
|
|
111010
|
+
if (extras.length === 0) {
|
|
111011
|
+
return defaultFiles;
|
|
111012
|
+
}
|
|
111013
|
+
const extraFiles = await Promise.all(extras.map((name) => loadNamedFile(workspaceDir, name)));
|
|
111014
|
+
return [...defaultFiles, ...extraFiles];
|
|
111015
|
+
}
|
|
111016
|
+
async function loadDynamicBootstrapFiles(workspaceDir, options) {
|
|
111017
|
+
const now2 = options?.now ?? new Date;
|
|
111018
|
+
const includeYesterday = options?.includeYesterday ?? true;
|
|
111019
|
+
const files = await Promise.all(DYNAMIC_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
|
|
111020
|
+
const todayRelative = dailyMemoryRelativePath(now2);
|
|
111021
|
+
const todayName = DEFAULT_MEMORY_FILENAME;
|
|
111022
|
+
const today = await loadNamedFile(workspaceDir, todayName, todayRelative);
|
|
111023
|
+
today.path = path8.join(workspaceDir, todayRelative);
|
|
111024
|
+
files.push(today);
|
|
111025
|
+
if (includeYesterday) {
|
|
111026
|
+
const yesterdayRelative = dailyMemoryRelativePath(addDays(now2, -1));
|
|
111027
|
+
const yesterday = await loadNamedFile(workspaceDir, todayName, yesterdayRelative);
|
|
111028
|
+
yesterday.path = path8.join(workspaceDir, yesterdayRelative);
|
|
111029
|
+
files.push(yesterday);
|
|
111030
|
+
}
|
|
111031
|
+
return files;
|
|
111032
|
+
}
|
|
111033
|
+
function pad22(n) {
|
|
111034
|
+
return n < 10 ? `0${n}` : String(n);
|
|
111035
|
+
}
|
|
111036
|
+
function dailyMemoryRelativePath(date) {
|
|
111037
|
+
const y = date.getFullYear();
|
|
111038
|
+
const m = pad22(date.getMonth() + 1);
|
|
111039
|
+
const d = pad22(date.getDate());
|
|
111040
|
+
return path8.join(DEFAULT_MEMORY_SUBDIR, `${y}-${m}-${d}.md`);
|
|
111041
|
+
}
|
|
111042
|
+
function addDays(date, days) {
|
|
111043
|
+
const out = new Date(date.getTime());
|
|
111044
|
+
out.setDate(out.getDate() + days);
|
|
111045
|
+
return out;
|
|
111046
|
+
}
|
|
111047
|
+
function truncateContent(name, content, maxChars) {
|
|
111048
|
+
if (content.length <= maxChars) {
|
|
111049
|
+
return content;
|
|
111050
|
+
}
|
|
111051
|
+
if (maxChars < 200) {
|
|
111052
|
+
return content.slice(0, Math.max(0, maxChars));
|
|
111053
|
+
}
|
|
111054
|
+
const headRoomInitial = Math.floor(maxChars * 0.7);
|
|
111055
|
+
const sampleMarker = `
|
|
111056
|
+
\u2026(truncated ${name}: kept ${headRoomInitial}+${Math.max(0, maxChars - headRoomInitial - 96)} chars of ${content.length})\u2026
|
|
111057
|
+
`;
|
|
111058
|
+
const markerReserve = Math.max(64, sampleMarker.length + 8);
|
|
111059
|
+
const headRoom = Math.min(headRoomInitial, Math.max(0, maxChars - markerReserve - 8));
|
|
111060
|
+
const tailRoom = Math.max(0, maxChars - headRoom - markerReserve);
|
|
111061
|
+
const head = content.slice(0, Math.max(0, headRoom));
|
|
111062
|
+
const tail = tailRoom > 0 ? content.slice(content.length - tailRoom) : "";
|
|
111063
|
+
const result = `${head}
|
|
111064
|
+
\u2026(truncated ${name}: kept ${headRoom}+${tailRoom} chars of ${content.length})\u2026
|
|
111065
|
+
${tail}`;
|
|
111066
|
+
return result.length <= maxChars ? result : result.slice(0, maxChars);
|
|
111067
|
+
}
|
|
111068
|
+
function projectBootstrapFiles(params) {
|
|
111069
|
+
const {
|
|
111070
|
+
files,
|
|
111071
|
+
heading,
|
|
111072
|
+
budget,
|
|
111073
|
+
seenSignatures,
|
|
111074
|
+
warningMode = "error",
|
|
111075
|
+
warningMaxFiles = DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES
|
|
111076
|
+
} = params;
|
|
111077
|
+
const perFileCap = budget.bootstrapMaxChars;
|
|
111078
|
+
let remainingTotal = budget.bootstrapTotalMaxChars;
|
|
111079
|
+
const injectedFiles = [];
|
|
111080
|
+
const parts = [];
|
|
111081
|
+
for (const file of files) {
|
|
111082
|
+
if (file.missing || typeof file.content !== "string") {
|
|
111083
|
+
continue;
|
|
111084
|
+
}
|
|
111085
|
+
const raw = file.content.trimEnd();
|
|
111086
|
+
if (raw.length === 0) {
|
|
111087
|
+
continue;
|
|
111088
|
+
}
|
|
111089
|
+
const perFileAllowed = Math.min(perFileCap, remainingTotal);
|
|
111090
|
+
if (perFileAllowed <= 0) {
|
|
111091
|
+
break;
|
|
111092
|
+
}
|
|
111093
|
+
const injected = truncateContent(file.name, raw, perFileAllowed);
|
|
111094
|
+
const relativePath = file.path;
|
|
111095
|
+
injectedFiles.push({ path: relativePath, content: injected });
|
|
111096
|
+
parts.push(`## ${relativePath}`);
|
|
111097
|
+
parts.push(injected);
|
|
111098
|
+
remainingTotal -= injected.length;
|
|
111099
|
+
if (remainingTotal <= 0) {
|
|
111100
|
+
break;
|
|
111101
|
+
}
|
|
111102
|
+
}
|
|
111103
|
+
const concatenated = parts.length > 0 ? [heading.trim().length > 0 ? `# ${heading}` : null, ...parts].filter((p) => p !== null).join(`
|
|
111104
|
+
|
|
111105
|
+
`) : "";
|
|
111106
|
+
const stats = buildBootstrapInjectionStats({
|
|
111107
|
+
bootstrapFiles: files,
|
|
111108
|
+
injectedFiles
|
|
111109
|
+
});
|
|
111110
|
+
const analysis = analyzeBootstrapBudget({
|
|
111111
|
+
files: stats,
|
|
111112
|
+
bootstrapMaxChars: budget.bootstrapMaxChars,
|
|
111113
|
+
bootstrapTotalMaxChars: budget.bootstrapTotalMaxChars,
|
|
111114
|
+
nearLimitRatio: budget.nearLimitRatio ?? DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO
|
|
111115
|
+
});
|
|
111116
|
+
const warning = buildBootstrapPromptWarning({
|
|
111117
|
+
analysis,
|
|
111118
|
+
mode: warningMode,
|
|
111119
|
+
seenSignatures,
|
|
111120
|
+
maxFiles: warningMaxFiles
|
|
111121
|
+
});
|
|
111122
|
+
if (warningMode === "error" && analysis.hasTruncation) {
|
|
111123
|
+
const errorLines = [
|
|
111124
|
+
"Bootstrap budget exceeded. The following files exceed limits:",
|
|
111125
|
+
""
|
|
111126
|
+
];
|
|
111127
|
+
for (const file of analysis.truncatedFiles) {
|
|
111128
|
+
const excessChars = file.rawChars - file.injectedChars;
|
|
111129
|
+
const causes = file.causes.map((c) => c === "per-file-limit" ? `per-file limit (${analysis.totals.bootstrapMaxChars.toLocaleString()} chars)` : `total limit (${analysis.totals.bootstrapTotalMaxChars.toLocaleString()} chars)`);
|
|
111130
|
+
errorLines.push(` ${file.name}: ${file.rawChars.toLocaleString()} bytes (exceeds ${causes.join(", ")})`);
|
|
111131
|
+
errorLines.push(` Trim ${excessChars.toLocaleString()} bytes, or pass --warning-mode warn to proceed with truncation.`);
|
|
111132
|
+
}
|
|
111133
|
+
throw new BootstrapBudgetExceededError(analysis, errorLines.join(`
|
|
111134
|
+
`));
|
|
111135
|
+
}
|
|
111136
|
+
return { files, injectedFiles, concatenated, analysis, warning };
|
|
111137
|
+
}
|
|
111138
|
+
async function buildStableBootstrapPrompt(params) {
|
|
111139
|
+
const files = await loadStableBootstrapFiles(params.workspaceDir, {
|
|
111140
|
+
extraStableFiles: params.extraStableFiles
|
|
111141
|
+
});
|
|
111142
|
+
const budget = {
|
|
111143
|
+
bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
|
|
111144
|
+
bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
|
|
111145
|
+
nearLimitRatio: params.budget?.nearLimitRatio
|
|
111146
|
+
};
|
|
111147
|
+
return projectBootstrapFiles({
|
|
111148
|
+
files,
|
|
111149
|
+
heading: "Project Context (stable workspace files)",
|
|
111150
|
+
budget,
|
|
111151
|
+
seenSignatures: params.seenSignatures,
|
|
111152
|
+
warningMode: params.budget?.warningMode,
|
|
111153
|
+
warningMaxFiles: params.budget?.warningMaxFiles
|
|
111154
|
+
});
|
|
111155
|
+
}
|
|
111156
|
+
async function buildDynamicBootstrapPrompt(params) {
|
|
111157
|
+
const files = await loadDynamicBootstrapFiles(params.workspaceDir, {
|
|
111158
|
+
now: params.now,
|
|
111159
|
+
includeYesterday: params.includeYesterday
|
|
111160
|
+
});
|
|
111161
|
+
const budget = {
|
|
111162
|
+
bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
|
|
111163
|
+
bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_DYNAMIC_TOTAL_MAX_CHARS,
|
|
111164
|
+
nearLimitRatio: params.budget?.nearLimitRatio
|
|
111165
|
+
};
|
|
111166
|
+
return projectBootstrapFiles({
|
|
111167
|
+
files,
|
|
111168
|
+
heading: "Project Context (dynamic workspace files)",
|
|
111169
|
+
budget,
|
|
111170
|
+
seenSignatures: params.seenSignatures,
|
|
111171
|
+
warningMode: params.budget?.warningMode,
|
|
111172
|
+
warningMaxFiles: params.budget?.warningMaxFiles
|
|
111173
|
+
});
|
|
111174
|
+
}
|
|
111175
|
+
|
|
111176
|
+
// src/cli/debug.ts
|
|
111177
|
+
init_merge();
|
|
111178
|
+
init_hindsight2();
|
|
111179
|
+
function formatBytes(bytes) {
|
|
111180
|
+
return `${bytes.toLocaleString()} bytes`;
|
|
111181
|
+
}
|
|
111182
|
+
function estimateTokens(bytes) {
|
|
111183
|
+
return Math.round(bytes / 3.7);
|
|
111184
|
+
}
|
|
111185
|
+
function readMcpServerNames(agentDir) {
|
|
111186
|
+
const mcpPath = join88(agentDir, ".mcp.json");
|
|
111187
|
+
if (!existsSync82(mcpPath))
|
|
111188
|
+
return [];
|
|
111189
|
+
try {
|
|
111190
|
+
const parsed = JSON.parse(readFileSync74(mcpPath, "utf-8"));
|
|
111191
|
+
return Object.keys(parsed.mcpServers ?? {});
|
|
111192
|
+
} catch {
|
|
111193
|
+
return null;
|
|
111194
|
+
}
|
|
111195
|
+
}
|
|
111196
|
+
function sha2562(content) {
|
|
111197
|
+
return createHash15("sha256").update(content).digest("hex").slice(0, 16);
|
|
111198
|
+
}
|
|
111199
|
+
function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
111200
|
+
const projectsDir = join88(claudeConfigDir, "projects");
|
|
111201
|
+
if (!existsSync82(projectsDir))
|
|
111202
|
+
return;
|
|
111203
|
+
try {
|
|
111204
|
+
const entries = readdirSync30(projectsDir, { withFileTypes: true });
|
|
111205
|
+
let latest;
|
|
111206
|
+
for (const entry of entries) {
|
|
111207
|
+
if (!entry.isDirectory())
|
|
111208
|
+
continue;
|
|
111209
|
+
const projectPath = join88(projectsDir, entry.name);
|
|
111210
|
+
const transcriptPath = join88(projectPath, "transcript.jsonl");
|
|
111211
|
+
if (!existsSync82(transcriptPath))
|
|
111212
|
+
continue;
|
|
111213
|
+
const stat2 = statSync48(transcriptPath);
|
|
111214
|
+
if (!latest || stat2.mtimeMs > latest.mtime) {
|
|
111215
|
+
latest = { path: transcriptPath, mtime: stat2.mtimeMs };
|
|
111216
|
+
}
|
|
111217
|
+
}
|
|
111218
|
+
return latest?.path;
|
|
111219
|
+
} catch {
|
|
111220
|
+
return;
|
|
111221
|
+
}
|
|
111222
|
+
}
|
|
111223
|
+
function extractLatestUserMessage(transcriptPath) {
|
|
111224
|
+
try {
|
|
111225
|
+
const content = readFileSync74(transcriptPath, "utf-8");
|
|
111226
|
+
const lines = content.trim().split(`
|
|
111227
|
+
`).filter(Boolean);
|
|
111228
|
+
for (let i2 = lines.length - 1;i2 >= 0; i2--) {
|
|
111229
|
+
const line = lines[i2];
|
|
111230
|
+
try {
|
|
111231
|
+
const event = JSON.parse(line);
|
|
111232
|
+
if (event.type === "message" && event.role === "user" && typeof event.content === "string") {
|
|
111233
|
+
const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : "unknown";
|
|
111234
|
+
return { text: event.content, timestamp };
|
|
111235
|
+
}
|
|
111236
|
+
} catch {
|
|
111237
|
+
continue;
|
|
111238
|
+
}
|
|
111239
|
+
}
|
|
111240
|
+
} catch {
|
|
111241
|
+
return;
|
|
111242
|
+
}
|
|
111243
|
+
}
|
|
111244
|
+
function buildProgressUpdateGuidance() {
|
|
111245
|
+
return `## Progress updates (human-style check-ins)
|
|
111246
|
+
|
|
111247
|
+
You're talking to a human colleague on Telegram. Alongside the emoji status
|
|
111248
|
+
ladder, send a short \`progress_update\` at inflection points, the moments a
|
|
111249
|
+
senior colleague would ping the person who asked them to do something:
|
|
111250
|
+
|
|
111251
|
+
- **Plan formed:** "Got it. Going to do X first, then Y, then Z."
|
|
111252
|
+
- **Pivot or blocker:** "First approach didn't work because <reason>. Trying
|
|
111253
|
+
<alternative> instead."
|
|
111254
|
+
- **Chunk finished:** "Done with X. Starting Y now."
|
|
111255
|
+
|
|
111256
|
+
Keep them short (one or two sentences). Don't narrate every step, the pinned
|
|
111257
|
+
progress card shows that for free. Don't send an update on a trivial one-shot
|
|
111258
|
+
task. Send them when a colleague would genuinely want to know what's happening.
|
|
111259
|
+
|
|
111260
|
+
Final answers still go through \`reply\` as usual,
|
|
111261
|
+
\`progress_update\` is only for mid-turn check-ins.`;
|
|
111262
|
+
}
|
|
111263
|
+
function registerDebugCommand(program3) {
|
|
111264
|
+
const cmd = program3.command("debug", { hidden: true }).description("Observability tools for inspecting agent prompt layering [advanced]");
|
|
111265
|
+
cmd.command("turn <agent>").description("Dump the exact prompt layering the model saw on the most recent turn").option("--last <n>", "Show N-th most recent turn instead of latest", "1").action(withConfigError(async (agentName, opts) => {
|
|
111266
|
+
const config = getConfig(program3);
|
|
111267
|
+
const agentConfig = config.agents[agentName];
|
|
111268
|
+
if (!agentConfig) {
|
|
111269
|
+
console.error(`Agent '${agentName}' not found in switchroom.yaml`);
|
|
111270
|
+
process.exit(1);
|
|
111271
|
+
}
|
|
111272
|
+
const agentsDir = resolveAgentsDir(config);
|
|
111273
|
+
const agentDir = resolve43(agentsDir, agentName);
|
|
111274
|
+
if (!existsSync82(agentDir)) {
|
|
111275
|
+
console.error(`Agent directory not found: ${agentDir}`);
|
|
111276
|
+
process.exit(1);
|
|
111277
|
+
}
|
|
111278
|
+
const workspaceDir = resolveAgentWorkspaceDir(agentDir);
|
|
111279
|
+
const claudeConfigDir = join88(agentDir, ".claude");
|
|
111280
|
+
const claudeMdPath2 = join88(agentDir, "CLAUDE.md");
|
|
111281
|
+
const soulMdPath = join88(agentDir, "SOUL.md");
|
|
111282
|
+
const workspaceSoulMdPath = join88(workspaceDir, "SOUL.md");
|
|
111283
|
+
const handoffPath = join88(agentDir, ".handoff.md");
|
|
111284
|
+
const lastN = parseInt(opts.last, 10);
|
|
111285
|
+
if (isNaN(lastN) || lastN < 1) {
|
|
111286
|
+
console.error("--last must be a positive integer");
|
|
111287
|
+
process.exit(1);
|
|
111288
|
+
}
|
|
111289
|
+
if (lastN > 1) {
|
|
111290
|
+
console.error("Note: --last N where N > 1 not yet implemented (only latest turn supported)");
|
|
111291
|
+
process.exit(1);
|
|
111292
|
+
}
|
|
111293
|
+
console.log(`=== Debug Turn Dump: ${agentName} ===
|
|
111294
|
+
`);
|
|
111295
|
+
console.log(`=== Append System Prompt (stable) ===
|
|
111296
|
+
`);
|
|
111297
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
111298
|
+
const useHotReloadStable = resolved.channels?.telegram?.hotReloadStable === true;
|
|
111299
|
+
const stableResult = await buildStableBootstrapPrompt({
|
|
111300
|
+
workspaceDir,
|
|
111301
|
+
budget: { warningMode: "off" }
|
|
111302
|
+
});
|
|
111303
|
+
if (useHotReloadStable) {
|
|
111304
|
+
console.log(`-- Workspace Stable Render (not used \u2014 stable content is in per-turn hook) --`);
|
|
111305
|
+
console.log();
|
|
111306
|
+
} else if (stableResult.concatenated.trim().length > 0) {
|
|
111307
|
+
console.log(`-- Workspace Stable Render (${formatBytes(stableResult.concatenated.length)}) --`);
|
|
111308
|
+
console.log(stableResult.concatenated);
|
|
111309
|
+
console.log();
|
|
111310
|
+
} else {
|
|
111311
|
+
console.log("-- Workspace Stable Render (0 bytes, not set up) --");
|
|
111312
|
+
console.log();
|
|
111313
|
+
}
|
|
111314
|
+
const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(resolved);
|
|
111315
|
+
const progressGuidance = useSwitchroomPlugin ? buildProgressUpdateGuidance() : "";
|
|
111316
|
+
if (progressGuidance.length > 0) {
|
|
111317
|
+
console.log(`-- Progress Updates Guidance (${formatBytes(progressGuidance.length)}) --`);
|
|
111318
|
+
console.log(progressGuidance);
|
|
111319
|
+
console.log();
|
|
111320
|
+
}
|
|
111321
|
+
const baseSystemPromptAppend = resolved.system_prompt_append ?? "";
|
|
111322
|
+
if (baseSystemPromptAppend.trim().length > 0) {
|
|
111323
|
+
console.log(`-- User System Prompt Append (${formatBytes(baseSystemPromptAppend.length)}) --`);
|
|
111324
|
+
console.log(baseSystemPromptAppend);
|
|
111325
|
+
console.log();
|
|
111326
|
+
}
|
|
111327
|
+
console.log(`=== Append System Prompt (per-session) ===
|
|
111328
|
+
`);
|
|
111329
|
+
const handoffContent = existsSync82(handoffPath) ? readFileSync74(handoffPath, "utf-8") : "";
|
|
111330
|
+
if (handoffContent.trim().length > 0) {
|
|
111331
|
+
console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
|
|
111332
|
+
console.log(handoffContent);
|
|
111333
|
+
console.log();
|
|
111334
|
+
} else {
|
|
111335
|
+
console.log("-- Handoff Briefing (0 bytes, no prior session) --");
|
|
111336
|
+
console.log();
|
|
111337
|
+
}
|
|
111338
|
+
console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
|
|
111339
|
+
`);
|
|
111340
|
+
const claudeMdContent = existsSync82(claudeMdPath2) ? readFileSync74(claudeMdPath2, "utf-8") : "";
|
|
111341
|
+
if (claudeMdContent.trim().length > 0) {
|
|
111342
|
+
console.log(`(${formatBytes(claudeMdContent.length)})`);
|
|
111343
|
+
console.log(claudeMdContent);
|
|
111344
|
+
console.log();
|
|
111345
|
+
} else {
|
|
111346
|
+
console.log("(0 bytes, not present)");
|
|
111347
|
+
console.log();
|
|
111348
|
+
}
|
|
111349
|
+
console.log(`=== Persona (SOUL.md) ===
|
|
111350
|
+
`);
|
|
111351
|
+
const soulMdContent = existsSync82(soulMdPath) ? readFileSync74(soulMdPath, "utf-8") : existsSync82(workspaceSoulMdPath) ? readFileSync74(workspaceSoulMdPath, "utf-8") : "";
|
|
111352
|
+
if (soulMdContent.trim().length > 0) {
|
|
111353
|
+
console.log(`(${formatBytes(soulMdContent.length)})`);
|
|
111354
|
+
console.log(soulMdContent);
|
|
111355
|
+
console.log();
|
|
111356
|
+
} else {
|
|
111357
|
+
console.log("(0 bytes, stale placeholder \u2014 Phase 2 item: single source of truth for persona)");
|
|
111358
|
+
console.log();
|
|
111359
|
+
}
|
|
111360
|
+
console.log(`=== Per-Turn Injections (UserPromptSubmit) ===
|
|
111361
|
+
`);
|
|
111362
|
+
if (useHotReloadStable) {
|
|
111363
|
+
if (stableResult.concatenated.trim().length > 0) {
|
|
111364
|
+
console.log(`-- Workspace Stable (hot-reload hook): ${formatBytes(stableResult.concatenated.length)} --`);
|
|
111365
|
+
console.log(stableResult.concatenated);
|
|
111366
|
+
console.log();
|
|
111367
|
+
} else {
|
|
111368
|
+
console.log("-- Workspace Stable (hot-reload hook): 0 bytes, not set up --");
|
|
111369
|
+
console.log();
|
|
111370
|
+
}
|
|
111371
|
+
}
|
|
111372
|
+
const dynamicResult = await buildDynamicBootstrapPrompt({
|
|
111373
|
+
workspaceDir,
|
|
111374
|
+
budget: { warningMode: "off" }
|
|
111375
|
+
});
|
|
111376
|
+
if (dynamicResult.concatenated.trim().length > 0) {
|
|
111377
|
+
console.log(`-- Workspace Dynamic: fired, ${formatBytes(dynamicResult.concatenated.length)} --`);
|
|
111378
|
+
console.log(dynamicResult.concatenated);
|
|
111379
|
+
console.log();
|
|
111380
|
+
} else {
|
|
111381
|
+
console.log("-- Workspace Dynamic: no content (MEMORY.md and daily notes empty or missing) --");
|
|
111382
|
+
console.log();
|
|
111383
|
+
}
|
|
111384
|
+
const hindsightEnabled = isHindsightEnabled(config) && agentConfig.memory?.auto_recall !== false;
|
|
111385
|
+
if (hindsightEnabled) {
|
|
111386
|
+
console.log("-- Hindsight Recall: enabled (exact content unavailable, check hindsight logs) --");
|
|
111387
|
+
console.log();
|
|
111388
|
+
} else {
|
|
111389
|
+
console.log("-- Hindsight Recall: disabled --");
|
|
111390
|
+
console.log();
|
|
111391
|
+
}
|
|
111392
|
+
console.log(`=== User Message (latest turn) ===
|
|
111393
|
+
`);
|
|
111394
|
+
const transcriptPath = findLatestTranscriptJsonl(claudeConfigDir);
|
|
111395
|
+
const userMessage = transcriptPath ? extractLatestUserMessage(transcriptPath) : undefined;
|
|
111396
|
+
if (userMessage) {
|
|
111397
|
+
console.log(`(Turn timestamp: ${userMessage.timestamp})`);
|
|
111398
|
+
console.log(userMessage.text);
|
|
111399
|
+
console.log();
|
|
111400
|
+
} else {
|
|
111401
|
+
console.log("(unavailable: no transcript found or transcript empty)");
|
|
111402
|
+
console.log();
|
|
111403
|
+
}
|
|
111404
|
+
console.log(`=== Totals ===
|
|
111405
|
+
`);
|
|
111406
|
+
const stableBytes = stableResult.concatenated.length + progressGuidance.length + baseSystemPromptAppend.length;
|
|
111407
|
+
const perSessionBytes = handoffContent.length;
|
|
111408
|
+
const claudeMdBytes = claudeMdContent.length;
|
|
111409
|
+
const soulMdBytes = soulMdContent.length;
|
|
111410
|
+
const perTurnBytes = dynamicResult.concatenated.length;
|
|
111411
|
+
const userBytes = userMessage?.text.length ?? 0;
|
|
111412
|
+
const fleetDir = join88(agentsDir, "..", "fleet");
|
|
111413
|
+
const fleetInvPath = join88(fleetDir, "switchroom-invariants.md");
|
|
111414
|
+
const fleetClaudePath = join88(fleetDir, "CLAUDE.md");
|
|
111415
|
+
const fleetInvBytes = existsSync82(fleetInvPath) ? readFileSync74(fleetInvPath, "utf-8").length : 0;
|
|
111416
|
+
const fleetClaudeBytes = existsSync82(fleetClaudePath) ? readFileSync74(fleetClaudePath, "utf-8").length : 0;
|
|
111417
|
+
const fleetBytes = fleetInvBytes + fleetClaudeBytes;
|
|
111418
|
+
const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
|
|
111419
|
+
console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
|
|
111420
|
+
console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
|
|
111421
|
+
console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
|
|
111422
|
+
console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
|
|
111423
|
+
console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
|
|
111424
|
+
console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
|
|
111425
|
+
console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
|
|
111426
|
+
const mcpServers = readMcpServerNames(agentDir);
|
|
111427
|
+
const mcpCount = mcpServers?.length ?? null;
|
|
111428
|
+
const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
|
|
111429
|
+
const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
|
|
111430
|
+
const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
|
|
111431
|
+
console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
|
|
111432
|
+
if (mcpServers && mcpServers.length > 0) {
|
|
111433
|
+
console.log(` [${mcpServers.join(", ")}]`);
|
|
111434
|
+
}
|
|
111435
|
+
const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
|
|
111436
|
+
console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
|
|
111437
|
+
const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
|
|
111438
|
+
const stableHash = sha2562(stableCacheInput);
|
|
111439
|
+
console.log(`Cache stable hash: sha256:${stableHash}`);
|
|
111440
|
+
console.log();
|
|
111441
|
+
}));
|
|
111442
|
+
}
|
|
111443
|
+
|
|
111444
|
+
// src/memory/directive-residue.ts
|
|
111445
|
+
var RESIDUE_CATEGORIES = new Set([
|
|
111446
|
+
"rules-block",
|
|
111447
|
+
"reflect-directive"
|
|
111448
|
+
]);
|
|
111449
|
+
function renderInjectedLine(index, directive) {
|
|
111450
|
+
const priority = directive.priority ?? 0;
|
|
111451
|
+
const name = (directive.name ?? "").trim() || "(unnamed)";
|
|
111452
|
+
const content = (directive.content ?? "").trim();
|
|
111453
|
+
return `${index}. [P${priority}] ${name}: ${content}`;
|
|
111454
|
+
}
|
|
111455
|
+
function measureDirectiveResidue(agent, rows, directivesById) {
|
|
111456
|
+
const residueRows = rows.filter((r) => r.isActive && RESIDUE_CATEGORIES.has(r.category));
|
|
111457
|
+
const lines = residueRows.map((row, i2) => {
|
|
111458
|
+
const directive = directivesById.get(row.id);
|
|
111459
|
+
if (!directive) {
|
|
111460
|
+
throw new Error(`measureDirectiveResidue: row '${row.name}' (id ${row.id}) has no ` + "matching directive in directivesById \u2014 pass the SAME list the " + "rows were built from.");
|
|
111461
|
+
}
|
|
111462
|
+
return renderInjectedLine(i2 + 1, directive);
|
|
111463
|
+
});
|
|
111464
|
+
const residueBytes = lines.reduce((sum, line) => sum + Buffer.byteLength(line, "utf8") + 1, 0);
|
|
111465
|
+
return {
|
|
111466
|
+
agent,
|
|
111467
|
+
residueDirectiveCount: residueRows.length,
|
|
111468
|
+
totalDirectiveCount: rows.length,
|
|
111469
|
+
residueBytes,
|
|
111470
|
+
residueTokensEstimate: estimateTokens(residueBytes)
|
|
111471
|
+
};
|
|
111472
|
+
}
|
|
111473
|
+
|
|
111474
|
+
// src/memory/directive-flip.ts
|
|
111475
|
+
init_rules_block();
|
|
111476
|
+
function evaluateFlipReadiness(measurement, opts) {
|
|
111477
|
+
const reasons = [];
|
|
111478
|
+
if (!opts.rulesBlockEnabled) {
|
|
111479
|
+
reasons.push("memory.rules_block (M1) is not enabled \u2014 enable the rules-block " + "toolchain and migrate the directives into the block BEFORE flipping " + "inject_directives off (two-flag ordering; flipping first strips every " + "guardrail).");
|
|
111480
|
+
}
|
|
111481
|
+
if (measurement.residueBytes > RULES_BLOCK_BUDGET_BYTES) {
|
|
111482
|
+
reasons.push(`directive residue is ${measurement.residueBytes}B, over the ` + `${RULES_BLOCK_BUDGET_BYTES}B rules-block budget (` + `${measurement.residueDirectiveCount} active residue directive(s)). ` + "Triage the directive set down \u2014 retire stale rules or shorten bodies \u2014 " + "until the residue fits, then re-check. The budget, not MAX_DIRECTIVES, " + "is the binding gate.");
|
|
111483
|
+
}
|
|
111484
|
+
return {
|
|
111485
|
+
agent: measurement.agent,
|
|
111486
|
+
ready: reasons.length === 0,
|
|
111487
|
+
reasons,
|
|
111488
|
+
residueBytes: measurement.residueBytes,
|
|
111489
|
+
budgetBytes: RULES_BLOCK_BUDGET_BYTES,
|
|
111490
|
+
residueDirectiveCount: measurement.residueDirectiveCount
|
|
111491
|
+
};
|
|
111492
|
+
}
|
|
111493
|
+
async function measureLiveResidue(admin, agent) {
|
|
111494
|
+
const directives = await admin.list();
|
|
111495
|
+
const rows = buildDirectiveTriageRows(directives);
|
|
111496
|
+
const byId = new Map(directives.map((d) => [d.id, d]));
|
|
111497
|
+
return measureDirectiveResidue(agent, rows, byId);
|
|
111498
|
+
}
|
|
111499
|
+
function flipConfigStanza(agent) {
|
|
111500
|
+
return [
|
|
111501
|
+
`# ${agent} \u2014 Memory v2 M3 Surface-A flip (suppress <active_directives>)`,
|
|
111502
|
+
"memory:",
|
|
111503
|
+
" rules_block: true # M1 must already be on (checked)",
|
|
111504
|
+
" inject_directives: false # M3: stop re-injecting migrated directives"
|
|
111505
|
+
].join(`
|
|
111506
|
+
`);
|
|
111507
|
+
}
|
|
111508
|
+
|
|
111509
|
+
// src/cli/memory-flip.ts
|
|
111510
|
+
function resolveFlipAdmin(program3, agent) {
|
|
111511
|
+
const config = getConfig(program3);
|
|
111512
|
+
if (!config.agents[agent]) {
|
|
111513
|
+
console.error(source_default.red(`Agent "${agent}" is not defined in switchroom.yaml`));
|
|
111514
|
+
process.exit(1);
|
|
111515
|
+
}
|
|
111516
|
+
if (!isHindsightEnabled(config)) {
|
|
111517
|
+
console.error(source_default.red("Hindsight memory is not enabled in this switchroom.yaml."));
|
|
111518
|
+
process.exit(1);
|
|
111519
|
+
}
|
|
111520
|
+
const agentConfig = config.agents[agent];
|
|
111521
|
+
const mcpBaseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
111522
|
+
const apiBaseUrl = mcpBaseUrl.replace(/\/mcp\/?$/, "").replace(/\/+$/, "");
|
|
111523
|
+
const bankId = agentConfig?.memory?.collection ?? agent;
|
|
111524
|
+
return new DirectiveAdmin({ apiBaseUrl, bankId });
|
|
111525
|
+
}
|
|
111526
|
+
function readRulesBlockEnabled(program3, agent) {
|
|
111527
|
+
const config = getConfig(program3);
|
|
111528
|
+
return config.agents[agent]?.memory?.rules_block === true;
|
|
111529
|
+
}
|
|
111530
|
+
function buildFlipPreflightJson(readiness, rulesBlockEnabled) {
|
|
111531
|
+
return {
|
|
111532
|
+
ok: true,
|
|
111533
|
+
agent: readiness.agent,
|
|
111534
|
+
ready: readiness.ready,
|
|
111535
|
+
rules_block: rulesBlockEnabled,
|
|
111536
|
+
residue_bytes: readiness.residueBytes,
|
|
111537
|
+
budget_bytes: readiness.budgetBytes,
|
|
111538
|
+
fits_budget: readiness.residueBytes <= readiness.budgetBytes,
|
|
111539
|
+
residue_directive_count: readiness.residueDirectiveCount,
|
|
111540
|
+
reasons: readiness.reasons
|
|
111541
|
+
};
|
|
111542
|
+
}
|
|
111543
|
+
function renderFlipPreflight(readiness, rulesBlockEnabled) {
|
|
111544
|
+
const fits = readiness.residueBytes <= readiness.budgetBytes;
|
|
111545
|
+
const lines = [];
|
|
111546
|
+
const verdict = readiness.ready ? source_default.green("READY") : source_default.red("NOT-READY");
|
|
111547
|
+
lines.push(`${verdict} \u2014 ${readiness.agent} M3 flip preflight`);
|
|
111548
|
+
lines.push("");
|
|
111549
|
+
lines.push(` residue: ${readiness.residueBytes}B / ${readiness.budgetBytes}B budget ` + `(${readiness.residueDirectiveCount} active residue directive(s)) ` + (fits ? source_default.green("fits") : source_default.red("OVER budget")));
|
|
111550
|
+
lines.push(` rules_block: ${rulesBlockEnabled ? source_default.green("on") : source_default.red("off")}`);
|
|
111551
|
+
if (readiness.ready) {
|
|
111552
|
+
lines.push("");
|
|
111553
|
+
lines.push(" Apply this per-agent stanza to switchroom.yaml, then restart:");
|
|
111554
|
+
lines.push("");
|
|
111555
|
+
for (const l of flipConfigStanza(readiness.agent).split(`
|
|
111556
|
+
`)) {
|
|
111557
|
+
lines.push(` ${l}`);
|
|
111558
|
+
}
|
|
111559
|
+
} else {
|
|
111560
|
+
lines.push("");
|
|
111561
|
+
lines.push(source_default.red(" Blocked:"));
|
|
111562
|
+
for (const reason of readiness.reasons) {
|
|
111563
|
+
lines.push(` - ${reason}`);
|
|
111564
|
+
}
|
|
111565
|
+
}
|
|
111566
|
+
return lines.join(`
|
|
111567
|
+
`);
|
|
111568
|
+
}
|
|
111569
|
+
function registerMemoryFlipCommand(memory, program3, deps = {}) {
|
|
111570
|
+
const makeAdmin = deps.makeAdmin ?? resolveFlipAdmin;
|
|
111571
|
+
memory.command("flip-preflight <agent>").description("Deterministic M3 (Surface-A) flip-readiness gate for <agent>: measure " + "the live post-triage directive residue, check it fits the 6144B " + "rules-block budget AND that memory.rules_block is already on, and " + "print READY + the exact switchroom.yaml stanza, or NOT-READY + why. " + "Never mutates config \u2014 the flip itself is an operator-gated edit.").option("--json", "Machine-readable output").action(withConfigError(async (agent, opts) => {
|
|
111572
|
+
const admin = makeAdmin(program3, agent);
|
|
111573
|
+
const rulesBlockEnabled = readRulesBlockEnabled(program3, agent);
|
|
111574
|
+
let ready;
|
|
111575
|
+
try {
|
|
111576
|
+
const measurement = await measureLiveResidue(admin, agent);
|
|
111577
|
+
const readiness = evaluateFlipReadiness(measurement, { rulesBlockEnabled });
|
|
111578
|
+
ready = readiness.ready;
|
|
111579
|
+
if (opts.json) {
|
|
111580
|
+
console.log(JSON.stringify(buildFlipPreflightJson(readiness, rulesBlockEnabled)));
|
|
111581
|
+
} else {
|
|
111582
|
+
console.log(renderFlipPreflight(readiness, rulesBlockEnabled));
|
|
111583
|
+
}
|
|
111584
|
+
} catch (e) {
|
|
111585
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
111586
|
+
if (opts.json) {
|
|
111587
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
111588
|
+
} else {
|
|
111589
|
+
console.error(source_default.red(`\u2717 flip preflight for ${agent} failed: ${msg}`));
|
|
111590
|
+
}
|
|
111591
|
+
process.exit(1);
|
|
111592
|
+
return;
|
|
111593
|
+
}
|
|
111594
|
+
if (!ready)
|
|
111595
|
+
process.exit(2);
|
|
111596
|
+
}));
|
|
111597
|
+
}
|
|
111598
|
+
|
|
111599
|
+
// src/cli/memory.ts
|
|
111600
|
+
function persistMemoryConfigUrl(configPath, provider, url) {
|
|
111601
|
+
if (!existsSync83(configPath))
|
|
111602
|
+
return false;
|
|
111603
|
+
const raw = readFileSync75(configPath, "utf-8");
|
|
111604
|
+
const doc = import_yaml16.default.parseDocument(raw);
|
|
111605
|
+
if (!doc.has("memory")) {
|
|
111606
|
+
doc.set("memory", { backend: "hindsight", shared_collection: "shared", config: { provider, url } });
|
|
111607
|
+
} else {
|
|
111608
|
+
const memNode = doc.get("memory");
|
|
111609
|
+
if (!memNode.has("config")) {
|
|
111610
|
+
memNode.set("config", { provider, url });
|
|
111611
|
+
} else {
|
|
111612
|
+
const configNode = memNode.get("config");
|
|
111613
|
+
configNode.set("url", url);
|
|
111614
|
+
if (!configNode.has("provider")) {
|
|
111615
|
+
configNode.set("provider", provider);
|
|
111616
|
+
}
|
|
111617
|
+
}
|
|
111618
|
+
}
|
|
111619
|
+
let mode = 420;
|
|
111620
|
+
try {
|
|
111621
|
+
mode = statSync49(configPath).mode & 511;
|
|
111622
|
+
} catch {}
|
|
111623
|
+
writeConfigFileSync(configPath, doc.toString(), mode);
|
|
111624
|
+
return true;
|
|
111625
|
+
}
|
|
111626
|
+
function resolveMemorySetupTag(input) {
|
|
111627
|
+
const explicit = input.explicitTag?.trim();
|
|
111628
|
+
if (explicit) {
|
|
111629
|
+
if (explicit.toLowerCase() === "latest") {
|
|
111630
|
+
return { tag: undefined, reason: "explicit-latest" };
|
|
111631
|
+
}
|
|
111632
|
+
const normalized = normalizeHindsightVersionTag(explicit);
|
|
111633
|
+
if (!normalized)
|
|
111634
|
+
return { tag: explicit, reason: "invalid" };
|
|
111635
|
+
return { tag: normalized, reason: "explicit" };
|
|
111636
|
+
}
|
|
111637
|
+
const pin = normalizeHindsightVersionTag(input.releasePin);
|
|
111638
|
+
if (pin)
|
|
111639
|
+
return { tag: pin, reason: "pin" };
|
|
111640
|
+
return { tag: undefined, reason: "latest" };
|
|
111641
|
+
}
|
|
111642
|
+
function hindsightSecretValues(input) {
|
|
111643
|
+
const out = [];
|
|
111644
|
+
const push = (v) => {
|
|
111645
|
+
if (typeof v === "string" && v.trim().length > 0)
|
|
111646
|
+
out.push(v.trim());
|
|
111647
|
+
};
|
|
111648
|
+
push(input.cpAccessKey);
|
|
111649
|
+
push(input.llm?.api_key);
|
|
111650
|
+
for (const op of ["retain", "reflect", "consolidation"]) {
|
|
111651
|
+
push(input.llm?.[op]?.api_key);
|
|
111652
|
+
}
|
|
111653
|
+
return out;
|
|
111654
|
+
}
|
|
111655
|
+
function hasUnresolvedHindsightVaultRef(config) {
|
|
111656
|
+
const hindsight = config.hindsight;
|
|
111657
|
+
return hindsightSecretValues({
|
|
111658
|
+
llm: hindsight?.llm,
|
|
111659
|
+
cpAccessKey: hindsight?.cp_access_key
|
|
111660
|
+
}).some((v) => isVaultReference(v));
|
|
111661
|
+
}
|
|
111662
|
+
function emitsCleartextHindsightSecret(input) {
|
|
111663
|
+
return hindsightSecretValues(input).some((v) => !isVaultReference(v));
|
|
111664
|
+
}
|
|
111665
|
+
var HINDSIGHT_COMPOSE_CLEARTEXT_SECRETS_WARNING = "This snippet embeds secrets in CLEARTEXT (LLM api_key and/or cp_access_key)." + " Treat the output as sensitive \u2014 do not paste it into shared channels or" + " commit it unredacted.";
|
|
111666
|
+
async function resolveLiteLLMForHindsight(config) {
|
|
111667
|
+
return resolveHindsightLiteLlm(config);
|
|
111668
|
+
}
|
|
111669
|
+
function readRecallLog(agentDir, limit) {
|
|
111670
|
+
const path9 = join89(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
|
|
111671
|
+
if (!existsSync83(path9))
|
|
111672
|
+
return [];
|
|
111673
|
+
let raw;
|
|
111674
|
+
try {
|
|
111675
|
+
raw = readFileSync75(path9, "utf-8");
|
|
111676
|
+
} catch {
|
|
111677
|
+
return [];
|
|
111678
|
+
}
|
|
111679
|
+
const lines = raw.split(`
|
|
111680
|
+
`).filter((l) => l.trim().length > 0);
|
|
111681
|
+
const tail = lines.slice(-limit);
|
|
111682
|
+
const out = [];
|
|
111683
|
+
for (const line of tail) {
|
|
111684
|
+
try {
|
|
111685
|
+
out.push(JSON.parse(line));
|
|
111686
|
+
} catch {}
|
|
111687
|
+
}
|
|
111688
|
+
return out;
|
|
111689
|
+
}
|
|
111690
|
+
function formatRecallLogView(name, entries) {
|
|
111691
|
+
const lines = [source_default.bold(`
|
|
111692
|
+
${name}:`)];
|
|
111693
|
+
const total = entries.length;
|
|
111694
|
+
const hits = entries.filter((e) => e.cache_hit).length;
|
|
111695
|
+
const cappedTurns = entries.filter((e) => e.capped).length;
|
|
111696
|
+
const omittedTurns = entries.filter((e) => typeof e.directives_omitted === "number" && e.directives_omitted > 0).length;
|
|
111697
|
+
const memCounts = entries.map((e) => e.result_count).filter((n) => typeof n === "number");
|
|
111698
|
+
const avg = memCounts.length > 0 ? Math.round(memCounts.reduce((s, n) => s + n, 0) / memCounts.length * 10) / 10 : null;
|
|
111699
|
+
const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
|
|
111700
|
+
const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
|
|
111701
|
+
const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
|
|
111702
|
+
const collapseTurns = entries.filter((e) => typeof e.injected_own_bank_count === "number" && e.injected_own_bank_count === 0 && typeof e.injected_additional_bank_count === "number" && e.injected_additional_bank_count > 0).length;
|
|
111703
|
+
lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "") + (collapseTurns > 0 ? source_default.red(` own_bank_collapse_turns=${collapseTurns}`) : "")));
|
|
111704
|
+
for (const e of entries) {
|
|
111705
|
+
const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
|
|
111706
|
+
const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
|
|
111707
|
+
const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
|
|
111708
|
+
const ids = e.memory_ids && e.memory_ids.length > 0 ? source_default.dim(` ids=${e.memory_ids.slice(0, 3).join(",")}${e.memory_ids.length > 3 ? `\u2026+${e.memory_ids.length - 3}` : ""}`) : "";
|
|
111709
|
+
const own = e.injected_own_bank_count;
|
|
111710
|
+
const additional = e.injected_additional_bank_count;
|
|
111711
|
+
const composition = typeof own === "number" || typeof additional === "number" ? (() => {
|
|
111712
|
+
const text = ` own/add=${own ?? "\u2014"}/${additional ?? "\u2014"}`;
|
|
111713
|
+
return own === 0 && typeof additional === "number" && additional > 0 ? source_default.red(text) : source_default.dim(text);
|
|
111714
|
+
})() : "";
|
|
111715
|
+
const score = typeof e.injected_score_min === "number" && typeof e.injected_score_median === "number" && typeof e.injected_score_max === "number" ? source_default.dim(` score=${e.injected_score_min.toFixed(2)}/` + `${e.injected_score_median.toFixed(2)}/` + `${e.injected_score_max.toFixed(2)}`) : "";
|
|
111716
|
+
lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${composition}${ids}`);
|
|
111717
|
+
}
|
|
111718
|
+
return lines;
|
|
111719
|
+
}
|
|
111720
|
+
function registerMemoryCommand(program3) {
|
|
111721
|
+
const memory = program3.command("memory").description("Hindsight memory operations");
|
|
111722
|
+
memory.command("search <query>").description("Search agent memories via Hindsight").option("-a, --agent <name>", "Search a specific agent's collection").action(withConfigError(async (query, opts) => {
|
|
111723
|
+
const config = getConfig(program3);
|
|
111724
|
+
if (opts.agent) {
|
|
111725
|
+
if (!config.agents[opts.agent]) {
|
|
111726
|
+
console.error(source_default.red(`Agent "${opts.agent}" is not defined in switchroom.yaml`));
|
|
111727
|
+
process.exit(1);
|
|
111728
|
+
}
|
|
111729
|
+
const collection = getCollectionForAgent(opts.agent, config);
|
|
111730
|
+
console.log(source_default.bold(`
|
|
111731
|
+
Search: ${opts.agent} (collection: ${collection})
|
|
111732
|
+
`));
|
|
111733
|
+
console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
|
|
111734
|
+
console.log();
|
|
111735
|
+
return;
|
|
111736
|
+
}
|
|
111737
|
+
const agentNames = Object.keys(config.agents);
|
|
111738
|
+
console.log(source_default.bold(`
|
|
111739
|
+
Searching all eligible collections:
|
|
111740
|
+
`));
|
|
111741
|
+
for (const name of agentNames) {
|
|
111742
|
+
const collection = getCollectionForAgent(name, config);
|
|
111743
|
+
if (isStrictIsolation(name, config)) {
|
|
111744
|
+
console.log(source_default.gray(` ${name} (${collection}) \u2014 skipped (strict isolation)`));
|
|
111745
|
+
continue;
|
|
111746
|
+
}
|
|
111747
|
+
console.log(source_default.cyan(` ${name} (${collection}):`));
|
|
111748
|
+
console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
|
|
111749
|
+
}
|
|
111750
|
+
console.log();
|
|
111751
|
+
}));
|
|
111752
|
+
memory.command("stats").description("List agents with their collection names and isolation mode").action(withConfigError(async () => {
|
|
111753
|
+
const config = getConfig(program3);
|
|
111754
|
+
const agentNames = Object.keys(config.agents);
|
|
111755
|
+
if (agentNames.length === 0) {
|
|
111756
|
+
console.log(source_default.yellow("No agents defined in switchroom.yaml"));
|
|
111757
|
+
return;
|
|
111758
|
+
}
|
|
111759
|
+
const headers = ["Agent", "Collection", "Isolation", "Auto-recall"];
|
|
111760
|
+
const widths = [20, 20, 12, 12];
|
|
111761
|
+
const headerLine = headers.map((h, i2) => source_default.bold(h.padEnd(widths[i2]))).join(" ");
|
|
111762
|
+
console.log(`
|
|
111763
|
+
${headerLine}`);
|
|
111764
|
+
for (const name of agentNames) {
|
|
111765
|
+
const collection = getCollectionForAgent(name, config);
|
|
111766
|
+
const isolation = isStrictIsolation(name, config) ? "strict" : "default";
|
|
111767
|
+
const autoRecall = config.agents[name].memory?.auto_recall ?? true;
|
|
111768
|
+
const row = [
|
|
111769
|
+
name.padEnd(widths[0]),
|
|
111770
|
+
collection.padEnd(widths[1]),
|
|
111771
|
+
isolation.padEnd(widths[2]),
|
|
111772
|
+
(autoRecall ? "yes" : "no").padEnd(widths[3])
|
|
111773
|
+
].join(" ");
|
|
111774
|
+
console.log(` ${row}`);
|
|
111775
|
+
}
|
|
111776
|
+
console.log();
|
|
111777
|
+
const profileBanks = collectProfileBanks(config);
|
|
111778
|
+
if (profileBanks.size > 0) {
|
|
111779
|
+
const owners = new Map;
|
|
111780
|
+
for (const [uname, u] of Object.entries(config.users ?? {})) {
|
|
111781
|
+
if (u.profile_bank) {
|
|
111782
|
+
owners.set(u.profile_bank, [
|
|
111783
|
+
...owners.get(u.profile_bank) ?? [],
|
|
111784
|
+
uname
|
|
111785
|
+
]);
|
|
111786
|
+
}
|
|
111787
|
+
}
|
|
111788
|
+
console.log(source_default.bold(` Profile banks (per-user / shared):
|
|
111789
|
+
`));
|
|
111790
|
+
for (const bank of [...profileBanks].sort()) {
|
|
111791
|
+
const who = owners.get(bank);
|
|
111792
|
+
const label = who ? `user: ${who.join(", ")}` : "shared";
|
|
111793
|
+
console.log(` ${bank.padEnd(widths[1])} ${source_default.gray(label)}`);
|
|
111794
|
+
}
|
|
111795
|
+
console.log();
|
|
111796
|
+
}
|
|
111797
|
+
console.log(source_default.bold(` Hindsight CLI commands:
|
|
111798
|
+
`));
|
|
111799
|
+
for (const name of agentNames) {
|
|
111800
|
+
const collection = getCollectionForAgent(name, config);
|
|
111801
|
+
console.log(source_default.gray(` $ ${getMemoryStats(collection)}`));
|
|
111802
|
+
}
|
|
111803
|
+
console.log();
|
|
111804
|
+
}));
|
|
111805
|
+
memory.command("reflect").description("Show cross-agent reflection plan").action(withConfigError(async () => {
|
|
111806
|
+
const config = getConfig(program3);
|
|
111807
|
+
const { eligible, excluded, commands } = reflectAcrossAgents(config);
|
|
111808
|
+
console.log(source_default.bold(`
|
|
111809
|
+
Cross-agent reflection plan
|
|
111810
|
+
`));
|
|
111811
|
+
if (eligible.length > 0) {
|
|
111812
|
+
console.log(source_default.green(" Eligible collections:"));
|
|
111813
|
+
for (const { agent, collection } of eligible) {
|
|
111814
|
+
console.log(source_default.white(` ${agent} -> ${collection}`));
|
|
111815
|
+
}
|
|
111816
|
+
}
|
|
111817
|
+
if (excluded.length > 0) {
|
|
111818
|
+
console.log(source_default.red(`
|
|
111819
|
+
Excluded (strict isolation):`));
|
|
111820
|
+
for (const { agent, collection } of excluded) {
|
|
111821
|
+
console.log(source_default.gray(` ${agent} -> ${collection}`));
|
|
111822
|
+
}
|
|
111823
|
+
}
|
|
111824
|
+
if (commands.length > 0) {
|
|
111825
|
+
console.log(source_default.bold(`
|
|
111826
|
+
Hindsight CLI commands:
|
|
111827
|
+
`));
|
|
111828
|
+
for (const cmd of commands) {
|
|
111829
|
+
console.log(source_default.gray(` $ ${cmd}`));
|
|
111830
|
+
}
|
|
111831
|
+
} else {
|
|
111832
|
+
console.log(source_default.yellow(`
|
|
111833
|
+
No eligible collections for reflection.`));
|
|
111834
|
+
}
|
|
111835
|
+
console.log();
|
|
111836
|
+
}));
|
|
111837
|
+
memory.command("setup").description("Manage the Hindsight Docker container").option("--stop", "Stop and remove the Hindsight container").option("--status", "Show Hindsight container status").option("--recreate", "Pull the latest image and recreate the container (reusing its current port). Used by `switchroom update` to keep the hindsight singleton current. Also required after any `hindsight.llm` model/provider config change \u2014 env is only re-derived on recreate, a plain docker restart is not enough (see docs/operators/hindsight-model-change.md).").option("--tag <version>", "Pin the hindsight image tag to pull + run (e.g. v0.15.18), overriding the " + "default floating `:latest`. Threaded through by `switchroom rollout` so a " + "version-pinned roll recreates hindsight on the SAME tag as the rest of the " + "fleet. Omit for `:latest` (the standalone default).").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").option("--strict-env", "Refuse the recreate (before the running container is touched) if it would drop " + "env vars that are live on the container but declared in neither `hindsight.env` " + "nor switchroom's managed defaults. Default is to warn loudly and proceed, so a " + "rollout is never wedged half-way with memory down.").option("--gpu", "Force GPU passthrough on for this launch, overriding host autodetection. " + "Use when `~/.switchroom/host-capabilities.json` is wrong or unreadable and " + "you know the host has a working nvidia container toolkit (`docker run --gpus " + "all` hard-fails container create without one). Beats `hindsight.gpu`.").option("--no-gpu", "Force GPU passthrough off for this launch, overriding host autodetection. " + "Also the explicit opt-out for the recreate GPU drop guard.").option("--allow-gpu-drop", "Proceed with a --recreate that would take GPU passthrough away from the " + "running container. Without this the recreate refuses, because that drop " + "moves the embedding model and cross-encoder reranker to CPU on the " + "interactive recall path with no other signal.").action(async (opts) => {
|
|
111838
|
+
if (opts.status) {
|
|
111839
|
+
if (!isDockerAvailable()) {
|
|
111840
|
+
console.log(source_default.red(" Docker is not available."));
|
|
111841
|
+
process.exit(1);
|
|
111842
|
+
}
|
|
111843
|
+
const status = getHindsightStatus();
|
|
111844
|
+
if (status) {
|
|
111845
|
+
console.log(source_default.bold(`
|
|
111846
|
+
Hindsight container status:`));
|
|
111847
|
+
console.log(` ${source_default.cyan("switchroom-hindsight")}: ${status}
|
|
111848
|
+
`);
|
|
111849
|
+
} else {
|
|
111850
|
+
console.log(source_default.yellow(`
|
|
111851
|
+
Hindsight container not found.
|
|
111852
|
+
`));
|
|
111853
|
+
console.log(source_default.gray(" Run 'switchroom memory setup' to start it."));
|
|
111854
|
+
}
|
|
111855
|
+
return;
|
|
111856
|
+
}
|
|
111857
|
+
if (opts.stop) {
|
|
111858
|
+
if (!isDockerAvailable()) {
|
|
111859
|
+
console.log(source_default.red(" Docker is not available."));
|
|
111860
|
+
process.exit(1);
|
|
111861
|
+
}
|
|
111862
|
+
if (!isHindsightContainerExists()) {
|
|
111863
|
+
console.log(source_default.yellow(" No switchroom-hindsight container found."));
|
|
111864
|
+
return;
|
|
111865
|
+
}
|
|
111866
|
+
console.log(source_default.gray(" Stopping switchroom-hindsight..."));
|
|
111867
|
+
stopHindsightRecallPool();
|
|
111868
|
+
stopHindsight();
|
|
111869
|
+
console.log(source_default.green(" Hindsight container stopped and removed."));
|
|
111870
|
+
return;
|
|
111871
|
+
}
|
|
111872
|
+
if (!isDockerAvailable()) {
|
|
111873
|
+
console.log(source_default.red(`
|
|
111874
|
+
Docker is not available.`));
|
|
111875
|
+
console.log(source_default.gray(` Install Docker: https://docs.docker.com/get-docker/
|
|
111876
|
+
`));
|
|
111877
|
+
process.exit(1);
|
|
111878
|
+
}
|
|
111879
|
+
const recreate = opts.recreate === true;
|
|
111880
|
+
let resolvedLlmPromise;
|
|
111881
|
+
const getResolvedLlm = () => {
|
|
111882
|
+
if (!resolvedLlmPromise) {
|
|
111883
|
+
resolvedLlmPromise = resolveHindsightLlmSecrets(getConfig(program3).hindsight?.llm);
|
|
111884
|
+
}
|
|
111885
|
+
return resolvedLlmPromise;
|
|
111886
|
+
};
|
|
111887
|
+
let releasePin;
|
|
111888
|
+
try {
|
|
111889
|
+
releasePin = getConfig(program3).release?.pin ?? undefined;
|
|
111890
|
+
} catch {
|
|
111891
|
+
releasePin = undefined;
|
|
111892
|
+
}
|
|
111893
|
+
const { tag: effectiveTag, reason: tagReason } = resolveMemorySetupTag({
|
|
111894
|
+
explicitTag: opts.tag,
|
|
111895
|
+
releasePin
|
|
111896
|
+
});
|
|
111897
|
+
switch (tagReason) {
|
|
110828
111898
|
case "invalid":
|
|
110829
111899
|
console.error(source_default.red(`
|
|
110830
111900
|
Invalid hindsight image tag: ${effectiveTag}
|
|
@@ -111366,9 +112436,9 @@ Repairing vector index coverage for ${target}${opts.dryRun ? source_default.yell
|
|
|
111366
112436
|
captured += chunk2.toString();
|
|
111367
112437
|
process.stdout.write(chunk2);
|
|
111368
112438
|
});
|
|
111369
|
-
const childExit = await new Promise((
|
|
111370
|
-
child.on("error", () =>
|
|
111371
|
-
child.on("close", (c) =>
|
|
112439
|
+
const childExit = await new Promise((resolve44) => {
|
|
112440
|
+
child.on("error", () => resolve44(127));
|
|
112441
|
+
child.on("close", (c) => resolve44(c ?? 1));
|
|
111372
112442
|
});
|
|
111373
112443
|
const outcome = classifyRepairOutcome({
|
|
111374
112444
|
childExit,
|
|
@@ -111393,7 +112463,7 @@ Repairing vector index coverage for ${target}${opts.dryRun ? source_default.yell
|
|
|
111393
112463
|
process.exit(1);
|
|
111394
112464
|
})() : Object.keys(config.agents);
|
|
111395
112465
|
for (const name of targets) {
|
|
111396
|
-
const agentDir =
|
|
112466
|
+
const agentDir = join89(agentsDir, name);
|
|
111397
112467
|
const entries = readRecallLog(agentDir, limit);
|
|
111398
112468
|
if (opts.json) {
|
|
111399
112469
|
for (const e of entries) {
|
|
@@ -111586,11 +112656,12 @@ Observation-scope cleanup \u2014 DRY RUN (no writes)
|
|
|
111586
112656
|
}));
|
|
111587
112657
|
registerMemoryRuleCommand(memory, program3);
|
|
111588
112658
|
registerMemoryDirectiveCommand(memory, program3);
|
|
112659
|
+
registerMemoryFlipCommand(memory, program3);
|
|
111589
112660
|
}
|
|
111590
112661
|
|
|
111591
112662
|
// src/cli/web.ts
|
|
111592
112663
|
init_source();
|
|
111593
|
-
import { join as
|
|
112664
|
+
import { join as join101 } from "node:path";
|
|
111594
112665
|
import { homedir as homedir50 } from "node:os";
|
|
111595
112666
|
|
|
111596
112667
|
// src/web/server.ts
|
|
@@ -111598,8 +112669,8 @@ init_merge();
|
|
|
111598
112669
|
init_loader();
|
|
111599
112670
|
init_client();
|
|
111600
112671
|
import {
|
|
111601
|
-
readFileSync as
|
|
111602
|
-
existsSync as
|
|
112672
|
+
readFileSync as readFileSync83,
|
|
112673
|
+
existsSync as existsSync91,
|
|
111603
112674
|
realpathSync as realpathSync7,
|
|
111604
112675
|
mkdirSync as mkdirSync50,
|
|
111605
112676
|
openSync as openSync17,
|
|
@@ -111607,7 +112678,7 @@ import {
|
|
|
111607
112678
|
writeSync as writeSync8,
|
|
111608
112679
|
constants as fsConstants6
|
|
111609
112680
|
} from "node:fs";
|
|
111610
|
-
import { resolve as
|
|
112681
|
+
import { resolve as resolve48, extname, join as join100, relative as relative5, dirname as dirname36 } from "node:path";
|
|
111611
112682
|
import { homedir as homedir49 } from "node:os";
|
|
111612
112683
|
import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes15 } from "node:crypto";
|
|
111613
112684
|
|
|
@@ -111616,8 +112687,8 @@ init_lifecycle();
|
|
|
111616
112687
|
init_manager();
|
|
111617
112688
|
init_hindsight2();
|
|
111618
112689
|
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
111619
|
-
import { existsSync as
|
|
111620
|
-
import { resolve as
|
|
112690
|
+
import { existsSync as existsSync87, readFileSync as readFileSync78, statSync as statSync52 } from "node:fs";
|
|
112691
|
+
import { resolve as resolve45, join as join96 } from "node:path";
|
|
111621
112692
|
|
|
111622
112693
|
// src/web/memory-remediation.ts
|
|
111623
112694
|
init_hindsight2();
|
|
@@ -111685,15 +112756,15 @@ async function buildUserProfile(mcpUrl, bank, opts) {
|
|
|
111685
112756
|
init_hindsight();
|
|
111686
112757
|
|
|
111687
112758
|
// src/web/blocked-approvals-read.ts
|
|
111688
|
-
import { readFileSync as
|
|
111689
|
-
import { resolve as
|
|
112759
|
+
import { readFileSync as readFileSync76, readdirSync as readdirSync31 } from "node:fs";
|
|
112760
|
+
import { resolve as resolve44, join as join90 } from "node:path";
|
|
111690
112761
|
import { homedir as homedir42 } from "node:os";
|
|
111691
112762
|
function blockedApprovalsDir(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir42()) {
|
|
111692
|
-
return
|
|
112763
|
+
return resolve44(home2, ".switchroom", "blocked-approvals");
|
|
111693
112764
|
}
|
|
111694
112765
|
var FALLBACK_RECORD_NAME = "blocked-approval.json";
|
|
111695
112766
|
function agentStateRoot(sharedDir) {
|
|
111696
|
-
return
|
|
112767
|
+
return resolve44(sharedDir, "..", "agents");
|
|
111697
112768
|
}
|
|
111698
112769
|
function finiteOrNull(v) {
|
|
111699
112770
|
const n = Number(v);
|
|
@@ -111732,7 +112803,7 @@ function readBlockedApprovalsWithErrors(dir, agentsRoot = agentStateRoot(dir)) {
|
|
|
111732
112803
|
function readSharedRecords(dir) {
|
|
111733
112804
|
let entries;
|
|
111734
112805
|
try {
|
|
111735
|
-
entries =
|
|
112806
|
+
entries = readdirSync31(dir);
|
|
111736
112807
|
} catch (err) {
|
|
111737
112808
|
if (err?.code === "ENOENT") {
|
|
111738
112809
|
return { blocked: [], unreadable: 0 };
|
|
@@ -111746,7 +112817,7 @@ function readSharedRecords(dir) {
|
|
|
111746
112817
|
continue;
|
|
111747
112818
|
let text;
|
|
111748
112819
|
try {
|
|
111749
|
-
text =
|
|
112820
|
+
text = readFileSync76(join90(dir, name), "utf-8");
|
|
111750
112821
|
} catch {
|
|
111751
112822
|
unreadable++;
|
|
111752
112823
|
continue;
|
|
@@ -111765,7 +112836,7 @@ function readSharedRecords(dir) {
|
|
|
111765
112836
|
function readFallbackRecords(agentsRoot, alreadySeen) {
|
|
111766
112837
|
let agents;
|
|
111767
112838
|
try {
|
|
111768
|
-
agents =
|
|
112839
|
+
agents = readdirSync31(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
111769
112840
|
} catch (err) {
|
|
111770
112841
|
if (err?.code === "ENOENT") {
|
|
111771
112842
|
return { blocked: [], unreadable: 0 };
|
|
@@ -111779,7 +112850,7 @@ function readFallbackRecords(agentsRoot, alreadySeen) {
|
|
|
111779
112850
|
continue;
|
|
111780
112851
|
let text;
|
|
111781
112852
|
try {
|
|
111782
|
-
text =
|
|
112853
|
+
text = readFileSync76(join90(agentsRoot, agent, FALLBACK_RECORD_NAME), "utf-8");
|
|
111783
112854
|
} catch (err) {
|
|
111784
112855
|
if (err?.code !== "ENOENT")
|
|
111785
112856
|
unreadable++;
|
|
@@ -111826,7 +112897,7 @@ init_audit_reader();
|
|
|
111826
112897
|
// src/scheduler/dispatch.ts
|
|
111827
112898
|
init_merge();
|
|
111828
112899
|
init_overlay_loader();
|
|
111829
|
-
import { createHash as
|
|
112900
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
111830
112901
|
function collectScheduleEntries(config) {
|
|
111831
112902
|
const out = [];
|
|
111832
112903
|
const agentNames = Object.keys(config.agents).sort();
|
|
@@ -111846,7 +112917,7 @@ function collectScheduleEntries(config) {
|
|
|
111846
112917
|
cron: entry.cron,
|
|
111847
112918
|
...typeof title === "string" && title.length > 0 ? { name: title } : {},
|
|
111848
112919
|
...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
|
|
111849
|
-
promptKey:
|
|
112920
|
+
promptKey: createHash16("sha256").update(auditMaterial).digest("hex").slice(0, 12),
|
|
111850
112921
|
...entry.topic !== undefined ? { topic: entry.topic } : {},
|
|
111851
112922
|
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
111852
112923
|
...entry.model !== undefined ? { model: entry.model } : {},
|
|
@@ -111895,7 +112966,7 @@ import { homedir as homedir45 } from "node:os";
|
|
|
111895
112966
|
// src/web/config-edit-plan.ts
|
|
111896
112967
|
init_schema();
|
|
111897
112968
|
var import_yaml17 = __toESM(require_dist(), 1);
|
|
111898
|
-
import { readFileSync as
|
|
112969
|
+
import { readFileSync as readFileSync77 } from "node:fs";
|
|
111899
112970
|
|
|
111900
112971
|
class ConfigPlanError extends Error {
|
|
111901
112972
|
}
|
|
@@ -111905,7 +112976,7 @@ function composeTransforms(...transforms) {
|
|
|
111905
112976
|
function planConfigEdit(configPath, transform) {
|
|
111906
112977
|
let before;
|
|
111907
112978
|
try {
|
|
111908
|
-
before =
|
|
112979
|
+
before = readFileSync77(configPath, "utf-8");
|
|
111909
112980
|
} catch (err) {
|
|
111910
112981
|
throw new ConfigPlanError(`could not read config: ${err.message}`);
|
|
111911
112982
|
}
|
|
@@ -111940,19 +113011,19 @@ import {
|
|
|
111940
113011
|
} from "node:fs";
|
|
111941
113012
|
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
111942
113013
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
111943
|
-
import { join as
|
|
113014
|
+
import { join as join92 } from "node:path";
|
|
111944
113015
|
|
|
111945
113016
|
class ConfigDiffError extends Error {
|
|
111946
113017
|
}
|
|
111947
113018
|
function generateUnifiedDiff(before, after, name = "switchroom.yaml", gitBin = "git") {
|
|
111948
113019
|
if (before === after)
|
|
111949
113020
|
return "";
|
|
111950
|
-
const dir = mkdtemp(
|
|
113021
|
+
const dir = mkdtemp(join92(tmpdir5(), "switchroom-config-diff-"));
|
|
111951
113022
|
try {
|
|
111952
|
-
mkdirSync47(
|
|
111953
|
-
mkdirSync47(
|
|
111954
|
-
writeFileSync32(
|
|
111955
|
-
writeFileSync32(
|
|
113023
|
+
mkdirSync47(join92(dir, "cur"), { recursive: true });
|
|
113024
|
+
mkdirSync47(join92(dir, "new"), { recursive: true });
|
|
113025
|
+
writeFileSync32(join92(dir, "cur", name), before);
|
|
113026
|
+
writeFileSync32(join92(dir, "new", name), after);
|
|
111956
113027
|
const r = spawnSync16(gitBin, ["diff", "--no-index", "--no-color", "--", `cur/${name}`, `new/${name}`], { cwd: dir, encoding: "utf-8", timeout: 1e4 });
|
|
111957
113028
|
if (r.status === 0)
|
|
111958
113029
|
return "";
|
|
@@ -111984,9 +113055,9 @@ function generateUnifiedDiff(before, after, name = "switchroom.yaml", gitBin = "
|
|
|
111984
113055
|
|
|
111985
113056
|
// src/web/hostd-config-propose.ts
|
|
111986
113057
|
init_client5();
|
|
111987
|
-
import { existsSync as
|
|
113058
|
+
import { existsSync as existsSync85 } from "node:fs";
|
|
111988
113059
|
import { homedir as homedir44 } from "node:os";
|
|
111989
|
-
import { join as
|
|
113060
|
+
import { join as join93 } from "node:path";
|
|
111990
113061
|
var PROPOSE_TIMEOUT_MS = 61 * 60 * 1000;
|
|
111991
113062
|
function resolveHostdOperatorSocket(env2 = process.env) {
|
|
111992
113063
|
const override = env2.SWITCHROOM_HOSTD_OPERATOR_SOCKET;
|
|
@@ -111994,10 +113065,10 @@ function resolveHostdOperatorSocket(env2 = process.env) {
|
|
|
111994
113065
|
return override;
|
|
111995
113066
|
const candidates = [
|
|
111996
113067
|
"/host-home/.switchroom/hostd/operator/sock",
|
|
111997
|
-
|
|
113068
|
+
join93(homedir44(), ".switchroom", "hostd", "operator", "sock")
|
|
111998
113069
|
];
|
|
111999
113070
|
for (const c of candidates) {
|
|
112000
|
-
if (
|
|
113071
|
+
if (existsSync85(c))
|
|
112001
113072
|
return c;
|
|
112002
113073
|
}
|
|
112003
113074
|
return null;
|
|
@@ -112205,7 +113276,7 @@ init_client2();
|
|
|
112205
113276
|
|
|
112206
113277
|
// telegram-plugin/registry/turns-schema.ts
|
|
112207
113278
|
import { chmodSync as chmodSync15 } from "fs";
|
|
112208
|
-
import { join as
|
|
113279
|
+
import { join as join95 } from "path";
|
|
112209
113280
|
|
|
112210
113281
|
// src/util/state-owner.ts
|
|
112211
113282
|
init_atomic();
|
|
@@ -112213,28 +113284,28 @@ import {
|
|
|
112213
113284
|
appendFileSync as appendFileSync4,
|
|
112214
113285
|
closeSync as closeSync16,
|
|
112215
113286
|
constants as constants2,
|
|
112216
|
-
existsSync as
|
|
113287
|
+
existsSync as existsSync86,
|
|
112217
113288
|
fchownSync as fchownSync2,
|
|
112218
113289
|
fstatSync as fstatSync5,
|
|
112219
113290
|
mkdirSync as mkdirSync48,
|
|
112220
113291
|
openSync as openSync16,
|
|
112221
|
-
readdirSync as
|
|
112222
|
-
statSync as
|
|
113292
|
+
readdirSync as readdirSync32,
|
|
113293
|
+
statSync as statSync51,
|
|
112223
113294
|
writeFileSync as writeFileSync33
|
|
112224
113295
|
} from "node:fs";
|
|
112225
|
-
import { dirname as dirname35, join as
|
|
113296
|
+
import { dirname as dirname35, join as join94, relative as relative4, sep as sep6 } from "node:path";
|
|
112226
113297
|
var NOFOLLOW = constants2.O_NOFOLLOW ?? 0;
|
|
112227
113298
|
var ownerCache = new Map;
|
|
112228
113299
|
var loggedFailure = false;
|
|
112229
113300
|
function defaultLog(line) {
|
|
112230
113301
|
process.stderr.write(line);
|
|
112231
113302
|
}
|
|
112232
|
-
function noteFailure(op,
|
|
113303
|
+
function noteFailure(op, path9, err, log) {
|
|
112233
113304
|
if (loggedFailure)
|
|
112234
113305
|
return;
|
|
112235
113306
|
loggedFailure = true;
|
|
112236
113307
|
const code = err.code ?? "";
|
|
112237
|
-
log(`switchroom state-owner: ${op} could not adopt directory ownership for ${
|
|
113308
|
+
log(`switchroom state-owner: ${op} could not adopt directory ownership for ${path9}` + `${code ? ` (${code})` : ""}: ${err.message}. ` + `Continuing; state files may stay owned by the writing uid. ` + `Further ownership warnings this process are suppressed.
|
|
112238
113309
|
`);
|
|
112239
113310
|
}
|
|
112240
113311
|
function isMultiplyLinked(st) {
|
|
@@ -112248,7 +113319,7 @@ function resolveStateOwner(dir) {
|
|
|
112248
113319
|
return cached;
|
|
112249
113320
|
let owner = null;
|
|
112250
113321
|
try {
|
|
112251
|
-
const st =
|
|
113322
|
+
const st = statSync51(dir);
|
|
112252
113323
|
owner = st.uid === 0 && st.gid === 0 ? null : { uid: st.uid, gid: st.gid };
|
|
112253
113324
|
} catch {
|
|
112254
113325
|
owner = null;
|
|
@@ -112256,13 +113327,13 @@ function resolveStateOwner(dir) {
|
|
|
112256
113327
|
ownerCache.set(dir, owner);
|
|
112257
113328
|
return owner;
|
|
112258
113329
|
}
|
|
112259
|
-
function adoptStateOwnership(
|
|
112260
|
-
const target = owner === undefined ? resolveStateOwner(dirname35(
|
|
113330
|
+
function adoptStateOwnership(path9, owner, log = defaultLog) {
|
|
113331
|
+
const target = owner === undefined ? resolveStateOwner(dirname35(path9)) : owner;
|
|
112261
113332
|
if (!target)
|
|
112262
113333
|
return;
|
|
112263
113334
|
let fd = null;
|
|
112264
113335
|
try {
|
|
112265
|
-
fd = openSync16(
|
|
113336
|
+
fd = openSync16(path9, constants2.O_RDONLY | NOFOLLOW);
|
|
112266
113337
|
const st = fstatSync5(fd);
|
|
112267
113338
|
if (st.uid === target.uid && st.gid === target.gid)
|
|
112268
113339
|
return;
|
|
@@ -112271,7 +113342,7 @@ function adoptStateOwnership(path7, owner, log = defaultLog) {
|
|
|
112271
113342
|
fchownSync2(fd, target.uid, target.gid);
|
|
112272
113343
|
} catch (err) {
|
|
112273
113344
|
if (err.code !== "ENOENT") {
|
|
112274
|
-
noteFailure("adopt",
|
|
113345
|
+
noteFailure("adopt", path9, err, log);
|
|
112275
113346
|
}
|
|
112276
113347
|
} finally {
|
|
112277
113348
|
if (fd !== null) {
|
|
@@ -112289,8 +113360,8 @@ function adoptSqliteOwnership(dbPath, log = defaultLog) {
|
|
|
112289
113360
|
adoptStateOwnership(dbPath + suffix, owner, log);
|
|
112290
113361
|
}
|
|
112291
113362
|
}
|
|
112292
|
-
function mkdirStateSync(
|
|
112293
|
-
const first = mkdirSync48(
|
|
113363
|
+
function mkdirStateSync(path9, options = { recursive: true }, log = defaultLog) {
|
|
113364
|
+
const first = mkdirSync48(path9, { ...options, recursive: true });
|
|
112294
113365
|
if (first === undefined)
|
|
112295
113366
|
return;
|
|
112296
113367
|
const owner = resolveStateOwner(dirname35(first));
|
|
@@ -112298,13 +113369,13 @@ function mkdirStateSync(path7, options = { recursive: true }, log = defaultLog)
|
|
|
112298
113369
|
return;
|
|
112299
113370
|
let current = first;
|
|
112300
113371
|
adoptStateOwnership(current, owner, log);
|
|
112301
|
-
const tail = relative4(first,
|
|
113372
|
+
const tail = relative4(first, path9);
|
|
112302
113373
|
if (tail === "" || tail.startsWith(".."))
|
|
112303
113374
|
return;
|
|
112304
113375
|
for (const part of tail.split(sep6)) {
|
|
112305
113376
|
if (part === "")
|
|
112306
113377
|
continue;
|
|
112307
|
-
current =
|
|
113378
|
+
current = join94(current, part);
|
|
112308
113379
|
adoptStateOwnership(current, owner, log);
|
|
112309
113380
|
}
|
|
112310
113381
|
}
|
|
@@ -112381,20 +113452,20 @@ function applySchema(db) {
|
|
|
112381
113452
|
}
|
|
112382
113453
|
function openTurnsDb(agentDir) {
|
|
112383
113454
|
const Database2 = loadDatabaseClass();
|
|
112384
|
-
const dir =
|
|
113455
|
+
const dir = join95(agentDir, "telegram");
|
|
112385
113456
|
mkdirStateSync(dir, { recursive: true, mode: 448 });
|
|
112386
|
-
const
|
|
112387
|
-
const db = new Database2(
|
|
113457
|
+
const path9 = join95(dir, "registry.db");
|
|
113458
|
+
const db = new Database2(path9, { create: true });
|
|
112388
113459
|
applySchema(db);
|
|
112389
113460
|
try {
|
|
112390
|
-
chmodSync15(
|
|
113461
|
+
chmodSync15(path9, 420);
|
|
112391
113462
|
for (const suffix of ["-shm", "-wal"]) {
|
|
112392
113463
|
try {
|
|
112393
|
-
chmodSync15(
|
|
113464
|
+
chmodSync15(path9 + suffix, 420);
|
|
112394
113465
|
} catch {}
|
|
112395
113466
|
}
|
|
112396
113467
|
} catch {}
|
|
112397
|
-
adoptSqliteOwnership(
|
|
113468
|
+
adoptSqliteOwnership(path9);
|
|
112398
113469
|
return db;
|
|
112399
113470
|
}
|
|
112400
113471
|
function mapRow(row) {
|
|
@@ -112596,7 +113667,7 @@ var DASHBOARD_CACHE_TTL = {
|
|
|
112596
113667
|
};
|
|
112597
113668
|
function readContextOccupancy(agentsDir, name) {
|
|
112598
113669
|
try {
|
|
112599
|
-
const raw =
|
|
113670
|
+
const raw = readFileSync78(resolve45(agentsDir, name, "context-occupancy.json"), "utf-8");
|
|
112600
113671
|
const s = JSON.parse(raw);
|
|
112601
113672
|
if (typeof s?.occupancy !== "number")
|
|
112602
113673
|
return null;
|
|
@@ -112607,7 +113678,7 @@ function readContextOccupancy(agentsDir, name) {
|
|
|
112607
113678
|
}
|
|
112608
113679
|
function readLastTurnAt(agentsDir, name) {
|
|
112609
113680
|
try {
|
|
112610
|
-
const db = openTurnsDb(
|
|
113681
|
+
const db = openTurnsDb(resolve45(agentsDir, name));
|
|
112611
113682
|
try {
|
|
112612
113683
|
const turns = listTurnsForAgent(db, { limit: 1 });
|
|
112613
113684
|
return turns.length > 0 ? turns[0].started_at : null;
|
|
@@ -112620,8 +113691,8 @@ function readLastTurnAt(agentsDir, name) {
|
|
|
112620
113691
|
}
|
|
112621
113692
|
function agentBridgeAlive(agentsDir, name, maxAgeMs = 30000, now2 = Date.now()) {
|
|
112622
113693
|
try {
|
|
112623
|
-
const f =
|
|
112624
|
-
return now2 -
|
|
113694
|
+
const f = resolve45(agentsDir, name, "telegram", ".bridge-alive");
|
|
113695
|
+
return now2 - statSync52(f).mtimeMs <= maxAgeMs;
|
|
112625
113696
|
} catch {
|
|
112626
113697
|
return false;
|
|
112627
113698
|
}
|
|
@@ -112719,7 +113790,7 @@ function handleGetTurns(config, sessionId, limit) {
|
|
|
112719
113790
|
const agentName = parsed.agentName;
|
|
112720
113791
|
const hasThread = "threadId" in parsed;
|
|
112721
113792
|
const agentsDir = resolveAgentsDir(config);
|
|
112722
|
-
const agentDir =
|
|
113793
|
+
const agentDir = resolve45(agentsDir, agentName);
|
|
112723
113794
|
const chatId = config.agents?.[agentName]?.channels?.telegram?.chat_id ?? undefined;
|
|
112724
113795
|
const db = openTurnsDb(agentDir);
|
|
112725
113796
|
try {
|
|
@@ -112742,7 +113813,7 @@ function handleListThreadIds(config, agentName) {
|
|
|
112742
113813
|
if (!chatId)
|
|
112743
113814
|
return [];
|
|
112744
113815
|
const agentsDir = resolveAgentsDir(config);
|
|
112745
|
-
const db = openTurnsDb(
|
|
113816
|
+
const db = openTurnsDb(resolve45(agentsDir, agentName));
|
|
112746
113817
|
try {
|
|
112747
113818
|
return listDistinctThreadIds(db, chatId);
|
|
112748
113819
|
} finally {
|
|
@@ -112755,7 +113826,7 @@ function handleListThreadIds(config, agentName) {
|
|
|
112755
113826
|
function handleGetSubagents(config, agentName, status) {
|
|
112756
113827
|
try {
|
|
112757
113828
|
const agentsDir = resolveAgentsDir(config);
|
|
112758
|
-
const agentDir =
|
|
113829
|
+
const agentDir = resolve45(agentsDir, agentName);
|
|
112759
113830
|
const db = openTurnsDb(agentDir);
|
|
112760
113831
|
try {
|
|
112761
113832
|
applySubagentsSchema(db);
|
|
@@ -113012,7 +114083,7 @@ async function handleGetSystemHealth(config, home2) {
|
|
|
113012
114083
|
};
|
|
113013
114084
|
try {
|
|
113014
114085
|
const logPath = defaultAuditLogPath2(home2);
|
|
113015
|
-
if (
|
|
114086
|
+
if (existsSync87(logPath)) {
|
|
113016
114087
|
hostd.auditLogPresent = true;
|
|
113017
114088
|
const raw = readAuditRaw(logPath);
|
|
113018
114089
|
hostd.recent = readAndFilter(raw, {}, 1000).filter((e) => !HOSTD_NOISE_OPS.has(e.op)).slice(-10);
|
|
@@ -113321,7 +114392,7 @@ async function handleGetSchedule(config, deps = {}) {
|
|
|
113321
114392
|
const recentByAgent = {};
|
|
113322
114393
|
const agents = new Set(entries.map((e) => e.agent));
|
|
113323
114394
|
for (const agent of agents) {
|
|
113324
|
-
const rows = readRecentFires(
|
|
114395
|
+
const rows = readRecentFires(resolve45(agentsDir, agent, "scheduler.jsonl"));
|
|
113325
114396
|
if (rows.length > 0)
|
|
113326
114397
|
recentByAgent[agent] = rows.slice(-10);
|
|
113327
114398
|
}
|
|
@@ -113353,11 +114424,11 @@ async function handleGetApprovals() {
|
|
|
113353
114424
|
return { reachable: true, decisions: sorted };
|
|
113354
114425
|
}
|
|
113355
114426
|
function brokerOperatorSocketPath(home2 = process.env.HOME || homedir45()) {
|
|
113356
|
-
return
|
|
114427
|
+
return join96(home2, ".switchroom", "broker-operator", "sock");
|
|
113357
114428
|
}
|
|
113358
114429
|
function resolveBrokerOperatorSocket(home2 = process.env.HOME || homedir45()) {
|
|
113359
114430
|
const p = brokerOperatorSocketPath(home2);
|
|
113360
|
-
return
|
|
114431
|
+
return existsSync87(p) ? p : null;
|
|
113361
114432
|
}
|
|
113362
114433
|
async function handleGetGrants(deps = {}) {
|
|
113363
114434
|
const socket = "socketPath" in deps ? deps.socketPath : resolveBrokerOperatorSocket();
|
|
@@ -113748,8 +114819,8 @@ async function handleGetSummary(deps, now2 = Date.now()) {
|
|
|
113748
114819
|
init_fleet_health_read();
|
|
113749
114820
|
|
|
113750
114821
|
// src/web/webhook-handler.ts
|
|
113751
|
-
import { appendFileSync as appendFileSync5, existsSync as
|
|
113752
|
-
import { join as
|
|
114822
|
+
import { appendFileSync as appendFileSync5, existsSync as existsSync89, mkdirSync as mkdirSync49, readFileSync as readFileSync81, writeFileSync as writeFileSync34 } from "fs";
|
|
114823
|
+
import { join as join98 } from "path";
|
|
113753
114824
|
import { homedir as homedir48 } from "os";
|
|
113754
114825
|
|
|
113755
114826
|
// src/web/webhook-verify.ts
|
|
@@ -113917,7 +114988,7 @@ init_log_rotation();
|
|
|
113917
114988
|
import net4 from "node:net";
|
|
113918
114989
|
function forwardToGateway(socketPath, req, opts = {}) {
|
|
113919
114990
|
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
113920
|
-
return new Promise((
|
|
114991
|
+
return new Promise((resolve47) => {
|
|
113921
114992
|
let settled = false;
|
|
113922
114993
|
let buf = "";
|
|
113923
114994
|
const done = (value) => {
|
|
@@ -113927,7 +114998,7 @@ function forwardToGateway(socketPath, req, opts = {}) {
|
|
|
113927
114998
|
try {
|
|
113928
114999
|
conn.destroy();
|
|
113929
115000
|
} catch {}
|
|
113930
|
-
|
|
115001
|
+
resolve47(value);
|
|
113931
115002
|
};
|
|
113932
115003
|
const conn = net4.createConnection(socketPath);
|
|
113933
115004
|
conn.setEncoding("utf8");
|
|
@@ -113958,20 +115029,20 @@ function forwardToGateway(socketPath, req, opts = {}) {
|
|
|
113958
115029
|
}
|
|
113959
115030
|
|
|
113960
115031
|
// src/web/webhook-edge.ts
|
|
113961
|
-
import { existsSync as
|
|
113962
|
-
import { join as
|
|
115032
|
+
import { existsSync as existsSync88, readFileSync as readFileSync80 } from "fs";
|
|
115033
|
+
import { join as join97 } from "path";
|
|
113963
115034
|
import { homedir as homedir47 } from "os";
|
|
113964
115035
|
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
113965
115036
|
var EDGE_HEADER = "x-switchroom-edge";
|
|
113966
115037
|
function edgeSecretPath() {
|
|
113967
|
-
return
|
|
115038
|
+
return join97(homedir47(), ".switchroom", "webhook-edge-secret");
|
|
113968
115039
|
}
|
|
113969
|
-
function loadEdgeSecret(
|
|
113970
|
-
const p =
|
|
115040
|
+
function loadEdgeSecret(path9) {
|
|
115041
|
+
const p = path9 ?? edgeSecretPath();
|
|
113971
115042
|
try {
|
|
113972
|
-
if (!
|
|
115043
|
+
if (!existsSync88(p))
|
|
113973
115044
|
return null;
|
|
113974
|
-
const raw =
|
|
115045
|
+
const raw = readFileSync80(p, "utf-8").trim();
|
|
113975
115046
|
return raw.length > 0 ? raw : null;
|
|
113976
115047
|
} catch {
|
|
113977
115048
|
return null;
|
|
@@ -114020,17 +115091,17 @@ function jsonReply(status, body, extraHeaders) {
|
|
|
114020
115091
|
}
|
|
114021
115092
|
var DEDUP_MAX = 1000;
|
|
114022
115093
|
var DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
114023
|
-
function loadDedupFile(
|
|
115094
|
+
function loadDedupFile(path9) {
|
|
114024
115095
|
try {
|
|
114025
|
-
if (!
|
|
115096
|
+
if (!existsSync89(path9))
|
|
114026
115097
|
return {};
|
|
114027
|
-
const raw = JSON.parse(
|
|
115098
|
+
const raw = JSON.parse(readFileSync81(path9, "utf-8"));
|
|
114028
115099
|
return typeof raw.deliveries === "object" && raw.deliveries !== null ? raw.deliveries : {};
|
|
114029
115100
|
} catch {
|
|
114030
115101
|
return {};
|
|
114031
115102
|
}
|
|
114032
115103
|
}
|
|
114033
|
-
function saveDedupFile(
|
|
115104
|
+
function saveDedupFile(path9, deliveries, now2) {
|
|
114034
115105
|
const pruned = {};
|
|
114035
115106
|
for (const [id, ts] of Object.entries(deliveries)) {
|
|
114036
115107
|
if (now2 - ts < DEDUP_TTL_MS)
|
|
@@ -114038,7 +115109,7 @@ function saveDedupFile(path7, deliveries, now2) {
|
|
|
114038
115109
|
}
|
|
114039
115110
|
const sorted = Object.entries(pruned).sort((a, b) => b[1] - a[1]).slice(0, DEDUP_MAX);
|
|
114040
115111
|
const final = Object.fromEntries(sorted);
|
|
114041
|
-
writeFileSync34(
|
|
115112
|
+
writeFileSync34(path9, JSON.stringify({ deliveries: final }), {
|
|
114042
115113
|
mode: 384
|
|
114043
115114
|
});
|
|
114044
115115
|
}
|
|
@@ -114046,8 +115117,8 @@ var agentDedupCache = new Map;
|
|
|
114046
115117
|
function createFileDedupStore(resolveAgentDir2) {
|
|
114047
115118
|
return {
|
|
114048
115119
|
check(agent, deliveryId, now2) {
|
|
114049
|
-
const telegramDir =
|
|
114050
|
-
const filePath =
|
|
115120
|
+
const telegramDir = join98(resolveAgentDir2(agent), "telegram");
|
|
115121
|
+
const filePath = join98(telegramDir, "webhook-dedup.json");
|
|
114051
115122
|
if (!agentDedupCache.has(agent)) {
|
|
114052
115123
|
agentDedupCache.set(agent, loadDedupFile(filePath));
|
|
114053
115124
|
}
|
|
@@ -114099,7 +115170,7 @@ function shouldWriteThrottleIssue(agent, source, now2, windowMap) {
|
|
|
114099
115170
|
return true;
|
|
114100
115171
|
}
|
|
114101
115172
|
function writeThrottleIssue(agent, source, now2, telegramDir, log) {
|
|
114102
|
-
const issuesPath =
|
|
115173
|
+
const issuesPath = join98(telegramDir, "issues.jsonl");
|
|
114103
115174
|
try {
|
|
114104
115175
|
mkdirSync49(telegramDir, { recursive: true });
|
|
114105
115176
|
const record = {
|
|
@@ -114124,7 +115195,7 @@ function writeThrottleIssue(agent, source, now2, telegramDir, log) {
|
|
|
114124
115195
|
async function handleWebhookIngest(args, deps = {}) {
|
|
114125
115196
|
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
114126
115197
|
const now2 = (deps.now ?? Date.now)();
|
|
114127
|
-
const resolveAgentDir2 = deps.resolveAgentDir ?? ((a) =>
|
|
115198
|
+
const resolveAgentDir2 = deps.resolveAgentDir ?? ((a) => join98(homedir48(), ".switchroom", "agents", a));
|
|
114128
115199
|
const rateLimiter = deps.rateLimiter ?? defaultRateLimiter;
|
|
114129
115200
|
const dedupStore = deps.dedupStore ?? createFileDedupStore(resolveAgentDir2);
|
|
114130
115201
|
if (!args.agentExists) {
|
|
@@ -114191,7 +115262,7 @@ async function handleWebhookIngest(args, deps = {}) {
|
|
|
114191
115262
|
if (retryAfter !== null) {
|
|
114192
115263
|
if (!args.viaGateway) {
|
|
114193
115264
|
const agentDir2 = resolveAgentDir2(args.agent);
|
|
114194
|
-
const telegramDir2 =
|
|
115265
|
+
const telegramDir2 = join98(agentDir2, "telegram");
|
|
114195
115266
|
if (shouldWriteThrottleIssue(args.agent, source, now2)) {
|
|
114196
115267
|
writeThrottleIssue(args.agent, source, now2, telegramDir2, log);
|
|
114197
115268
|
}
|
|
@@ -114211,7 +115282,7 @@ async function handleWebhookIngest(args, deps = {}) {
|
|
|
114211
115282
|
const eventType = source === "github" ? args.headers.get("x-github-event") ?? "unknown" : source === "linear" ? String(payload.type ?? "unknown").toLowerCase() : args.source;
|
|
114212
115283
|
const rendered = source === "github" ? renderGithubEvent(eventType, payload) : source === "linear" ? renderLinearEvent(eventType, payload) : renderGenericEvent(args.source, payload);
|
|
114213
115284
|
if (args.viaGateway) {
|
|
114214
|
-
const socketPath =
|
|
115285
|
+
const socketPath = join98(resolveAgentDir2(args.agent), "telegram", "webhook.sock");
|
|
114215
115286
|
const forward = deps.forwardFn ?? forwardToGateway;
|
|
114216
115287
|
const deliveryId = source === "github" ? args.headers.get("x-github-delivery") ?? undefined : undefined;
|
|
114217
115288
|
let resp;
|
|
@@ -114250,8 +115321,8 @@ async function handleWebhookIngest(args, deps = {}) {
|
|
|
114250
115321
|
return jsonReply(202, { ok: true, recorded: true, ts: resp.ts });
|
|
114251
115322
|
}
|
|
114252
115323
|
const agentDir = resolveAgentDir2(args.agent);
|
|
114253
|
-
const telegramDir =
|
|
114254
|
-
const logPath =
|
|
115324
|
+
const telegramDir = join98(agentDir, "telegram");
|
|
115325
|
+
const logPath = join98(telegramDir, "webhook-events.jsonl");
|
|
114255
115326
|
try {
|
|
114256
115327
|
mkdirSync49(telegramDir, { recursive: true });
|
|
114257
115328
|
rotateWebhookLogIfNeeded(logPath);
|
|
@@ -114280,9 +115351,9 @@ init_shipped_assets();
|
|
|
114280
115351
|
// src/web/hermes-adapter.ts
|
|
114281
115352
|
init_loader();
|
|
114282
115353
|
import { createConnection as createConnection3 } from "node:net";
|
|
114283
|
-
import { createHash as
|
|
114284
|
-
import { join as
|
|
114285
|
-
import { existsSync as
|
|
115354
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
115355
|
+
import { join as join99, resolve as resolve47 } from "node:path";
|
|
115356
|
+
import { existsSync as existsSync90, readFileSync as readFileSync82 } from "node:fs";
|
|
114286
115357
|
|
|
114287
115358
|
// src/agent-scheduler/channel-target.ts
|
|
114288
115359
|
init_merge();
|
|
@@ -114326,8 +115397,8 @@ function agentLiveness(config, agentName) {
|
|
|
114326
115397
|
const agentsDir = resolveAgentsDir(config);
|
|
114327
115398
|
if (agentBridgeAlive(agentsDir, agentName))
|
|
114328
115399
|
return "active";
|
|
114329
|
-
const alive =
|
|
114330
|
-
if (
|
|
115400
|
+
const alive = join99(agentsDir, agentName, "telegram", ".bridge-alive");
|
|
115401
|
+
if (existsSync90(alive))
|
|
114331
115402
|
return "idle";
|
|
114332
115403
|
return "offline";
|
|
114333
115404
|
}
|
|
@@ -114435,11 +115506,11 @@ function resolveAgentChat(config, sessionId, agentsDir) {
|
|
|
114435
115506
|
const threadId = topicId !== undefined ? topicId === null ? undefined : parseInt(topicId, 10) || undefined : channel.threadId;
|
|
114436
115507
|
return { chatId: channel.chatId, threadId };
|
|
114437
115508
|
}
|
|
114438
|
-
const accessPath =
|
|
114439
|
-
if (!
|
|
115509
|
+
const accessPath = resolve47(agentsDir, agentName, "telegram", "access.json");
|
|
115510
|
+
if (!existsSync90(accessPath))
|
|
114440
115511
|
return null;
|
|
114441
115512
|
try {
|
|
114442
|
-
const raw =
|
|
115513
|
+
const raw = readFileSync82(accessPath, "utf-8");
|
|
114443
115514
|
const access = JSON.parse(raw);
|
|
114444
115515
|
const chatId = access.allowFrom?.[0];
|
|
114445
115516
|
if (!chatId)
|
|
@@ -114450,8 +115521,8 @@ function resolveAgentChat(config, sessionId, agentsDir) {
|
|
|
114450
115521
|
}
|
|
114451
115522
|
}
|
|
114452
115523
|
async function injectInbound(agentsDir, agentName, chatId, threadId, text, promptKey) {
|
|
114453
|
-
const socketPath =
|
|
114454
|
-
if (!
|
|
115524
|
+
const socketPath = resolve47(agentsDir, agentName, "telegram", "gateway.sock");
|
|
115525
|
+
if (!existsSync90(socketPath)) {
|
|
114455
115526
|
return { ok: false, error: `agent ${agentName} gateway socket not found \u2014 is it running?` };
|
|
114456
115527
|
}
|
|
114457
115528
|
const ts = Date.now();
|
|
@@ -114498,8 +115569,8 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
|
|
|
114498
115569
|
});
|
|
114499
115570
|
}
|
|
114500
115571
|
function readHistoryDb(agentsDir, agentName, limit) {
|
|
114501
|
-
const dbPath =
|
|
114502
|
-
if (!
|
|
115572
|
+
const dbPath = resolve47(agentsDir, agentName, "telegram", "history.db");
|
|
115573
|
+
if (!existsSync90(dbPath))
|
|
114503
115574
|
return null;
|
|
114504
115575
|
try {
|
|
114505
115576
|
let SqliteDatabase;
|
|
@@ -114528,7 +115599,7 @@ function readHistoryDb(agentsDir, agentName, limit) {
|
|
|
114528
115599
|
}
|
|
114529
115600
|
}
|
|
114530
115601
|
function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs = 2000) {
|
|
114531
|
-
const dbPath =
|
|
115602
|
+
const dbPath = resolve47(agentsDir, agentName, "telegram", "history.db");
|
|
114532
115603
|
let SqliteDatabase = null;
|
|
114533
115604
|
try {
|
|
114534
115605
|
const meta = import.meta;
|
|
@@ -114538,7 +115609,7 @@ function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs =
|
|
|
114538
115609
|
}
|
|
114539
115610
|
} catch {}
|
|
114540
115611
|
let cursor = 0;
|
|
114541
|
-
if (SqliteDatabase &&
|
|
115612
|
+
if (SqliteDatabase && existsSync90(dbPath)) {
|
|
114542
115613
|
try {
|
|
114543
115614
|
const db = new SqliteDatabase(dbPath, { readonly: true });
|
|
114544
115615
|
try {
|
|
@@ -114550,7 +115621,7 @@ function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs =
|
|
|
114550
115621
|
} catch {}
|
|
114551
115622
|
}
|
|
114552
115623
|
const timer = setInterval(() => {
|
|
114553
|
-
if (!SqliteDatabase || !
|
|
115624
|
+
if (!SqliteDatabase || !existsSync90(dbPath))
|
|
114554
115625
|
return;
|
|
114555
115626
|
try {
|
|
114556
115627
|
const db = new SqliteDatabase(dbPath, { readonly: true });
|
|
@@ -115213,7 +116284,7 @@ async function onHermesMessage(ctx, raw) {
|
|
|
115213
116284
|
sendResponse(ctx, rpcErr(id, -32603, `Could not resolve chat for ${sessionId} \u2014 ensure the agent has telegram.forum_chat_id or channels.telegram.chat_id configured.`));
|
|
115214
116285
|
break;
|
|
115215
116286
|
}
|
|
115216
|
-
const promptKey =
|
|
116287
|
+
const promptKey = createHash17("sha256").update(`${sessionId}:${Date.now()}:${content}`).digest("hex").slice(0, 12);
|
|
115217
116288
|
sendEvent(ctx, "message.start", sessionId, { prompt_key: promptKey });
|
|
115218
116289
|
pendingPromptKeys(ctx).set(sessionId, promptKey);
|
|
115219
116290
|
const result = await injectInbound(agentsDir, submitAgent, chat.chatId, chat.threadId, content, promptKey);
|
|
@@ -115409,9 +116480,9 @@ function resolveWebToken() {
|
|
|
115409
116480
|
if (fromEnv && fromEnv.length > 0)
|
|
115410
116481
|
return fromEnv;
|
|
115411
116482
|
const home2 = process.env.HOME ?? homedir49();
|
|
115412
|
-
const tokenPath =
|
|
115413
|
-
if (
|
|
115414
|
-
const existing =
|
|
116483
|
+
const tokenPath = join100(home2, ".switchroom", "web-token");
|
|
116484
|
+
if (existsSync91(tokenPath)) {
|
|
116485
|
+
const existing = readFileSync83(tokenPath, "utf8").trim();
|
|
115415
116486
|
if (existing.length > 0)
|
|
115416
116487
|
return existing;
|
|
115417
116488
|
}
|
|
@@ -115428,7 +116499,7 @@ function resolveWebToken() {
|
|
|
115428
116499
|
return token;
|
|
115429
116500
|
} catch (err) {
|
|
115430
116501
|
if (err.code === "EEXIST") {
|
|
115431
|
-
const existing =
|
|
116502
|
+
const existing = readFileSync83(tokenPath, "utf8").trim();
|
|
115432
116503
|
if (existing.length > 0)
|
|
115433
116504
|
return existing;
|
|
115434
116505
|
}
|
|
@@ -115501,14 +116572,14 @@ function checkWsAuth(req, token, server) {
|
|
|
115501
116572
|
return presented !== null && constantTimeEqual(presented, token);
|
|
115502
116573
|
}
|
|
115503
116574
|
function loadWebhookSecrets() {
|
|
115504
|
-
const
|
|
115505
|
-
if (!
|
|
116575
|
+
const path9 = join100(homedir49(), ".switchroom", "webhook-secrets.json");
|
|
116576
|
+
if (!existsSync91(path9))
|
|
115506
116577
|
return {};
|
|
115507
116578
|
try {
|
|
115508
|
-
const parsed = JSON.parse(
|
|
116579
|
+
const parsed = JSON.parse(readFileSync83(path9, "utf-8"));
|
|
115509
116580
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
115510
116581
|
} catch (err) {
|
|
115511
|
-
process.stderr.write(`webhook-ingest: failed to parse ${
|
|
116582
|
+
process.stderr.write(`webhook-ingest: failed to parse ${path9}: ${err.message} \u2014 webhooks will return 401 until fixed
|
|
115512
116583
|
`);
|
|
115513
116584
|
return {};
|
|
115514
116585
|
}
|
|
@@ -115695,11 +116766,11 @@ function resolveUiDir(probe2 = {
|
|
|
115695
116766
|
bundleDir: import.meta.dirname,
|
|
115696
116767
|
execPath: process.execPath
|
|
115697
116768
|
}) {
|
|
115698
|
-
return resolveShippedAsset(WEB_UI_ASSET, probe2).path ??
|
|
116769
|
+
return resolveShippedAsset(WEB_UI_ASSET, probe2).path ?? resolve48(probe2.bundleDir, WEB_UI_ASSET.asset);
|
|
115699
116770
|
}
|
|
115700
116771
|
function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
115701
116772
|
const uiDirRaw = resolveUiDir();
|
|
115702
|
-
const uiDir =
|
|
116773
|
+
const uiDir = existsSync91(uiDirRaw) ? realpathSync7(uiDirRaw) : uiDirRaw;
|
|
115703
116774
|
const token = resolveWebToken();
|
|
115704
116775
|
const freshConfig = () => {
|
|
115705
116776
|
if (!configPath)
|
|
@@ -115999,8 +117070,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
115999
117070
|
})();
|
|
116000
117071
|
}
|
|
116001
117072
|
const filePath = resolveDashboardFilePath(pathname);
|
|
116002
|
-
const fullPath =
|
|
116003
|
-
if (!
|
|
117073
|
+
const fullPath = join100(uiDir, filePath);
|
|
117074
|
+
if (!existsSync91(fullPath)) {
|
|
116004
117075
|
return new Response("Not Found", { status: 404 });
|
|
116005
117076
|
}
|
|
116006
117077
|
let realFullPath;
|
|
@@ -116010,12 +117081,12 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
116010
117081
|
return new Response("Not Found", { status: 404 });
|
|
116011
117082
|
}
|
|
116012
117083
|
const rel = relative5(uiDir, realFullPath);
|
|
116013
|
-
if (rel.startsWith("..") ||
|
|
117084
|
+
if (rel.startsWith("..") || resolve48(uiDir, rel) !== realFullPath) {
|
|
116014
117085
|
return new Response("Forbidden", { status: 403 });
|
|
116015
117086
|
}
|
|
116016
117087
|
const ext = extname(realFullPath);
|
|
116017
117088
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
116018
|
-
const content =
|
|
117089
|
+
const content = readFileSync83(realFullPath);
|
|
116019
117090
|
const headers = { "Content-Type": contentType };
|
|
116020
117091
|
const cacheControl = dashboardCacheControl(ext);
|
|
116021
117092
|
if (cacheControl)
|
|
@@ -116138,13 +117209,13 @@ init_helpers();
|
|
|
116138
117209
|
init_loader();
|
|
116139
117210
|
|
|
116140
117211
|
// src/web/startup-guard.ts
|
|
116141
|
-
import { existsSync as
|
|
117212
|
+
import { existsSync as existsSync92, readFileSync as readFileSync84, writeFileSync as writeFileSync35, mkdirSync as mkdirSync51, statSync as statSync53, unlinkSync as unlinkSync18 } from "node:fs";
|
|
116142
117213
|
import { dirname as dirname37 } from "node:path";
|
|
116143
117214
|
function detectConfigMountFault(configPath, deps = {}) {
|
|
116144
|
-
const
|
|
117215
|
+
const stat2 = deps.stat ?? ((p) => statSync53(p));
|
|
116145
117216
|
let st;
|
|
116146
117217
|
try {
|
|
116147
|
-
st =
|
|
117218
|
+
st = stat2(configPath);
|
|
116148
117219
|
} catch {
|
|
116149
117220
|
return null;
|
|
116150
117221
|
}
|
|
@@ -116174,9 +117245,9 @@ function nextCrashBackoff(prior, now2, options = {}) {
|
|
|
116174
117245
|
}
|
|
116175
117246
|
function readCrashState(statePath) {
|
|
116176
117247
|
try {
|
|
116177
|
-
if (!
|
|
117248
|
+
if (!existsSync92(statePath))
|
|
116178
117249
|
return null;
|
|
116179
|
-
const raw = JSON.parse(
|
|
117250
|
+
const raw = JSON.parse(readFileSync84(statePath, "utf-8"));
|
|
116180
117251
|
if (typeof raw.count !== "number" || typeof raw.lastTs !== "number")
|
|
116181
117252
|
return null;
|
|
116182
117253
|
return { count: raw.count, lastTs: raw.lastTs };
|
|
@@ -116192,7 +117263,7 @@ function writeCrashState(statePath, state) {
|
|
|
116192
117263
|
}
|
|
116193
117264
|
function clearCrashState(statePath) {
|
|
116194
117265
|
try {
|
|
116195
|
-
if (
|
|
117266
|
+
if (existsSync92(statePath))
|
|
116196
117267
|
unlinkSync18(statePath);
|
|
116197
117268
|
} catch {}
|
|
116198
117269
|
}
|
|
@@ -116215,7 +117286,7 @@ async function handleWebStartupFailure(err, configPath, stateFile) {
|
|
|
116215
117286
|
throw err;
|
|
116216
117287
|
}
|
|
116217
117288
|
function webCrashStatePath() {
|
|
116218
|
-
return
|
|
117289
|
+
return join101(homedir50(), ".switchroom", "web", ".crashloop-state.json");
|
|
116219
117290
|
}
|
|
116220
117291
|
function registerWebCommand(program3) {
|
|
116221
117292
|
program3.command("web").description("Start the web dashboard for monitoring agents").option("-p, --port <port>", "Port to listen on", "8080").option("-b, --bind <host>", "Host/IP to bind to (default: 127.0.0.1, localhost-only)", "127.0.0.1").action(withConfigError(async (opts) => {
|
|
@@ -116282,9 +117353,9 @@ init_embedded_examples();
|
|
|
116282
117353
|
init_atomic();
|
|
116283
117354
|
init_loader();
|
|
116284
117355
|
init_scaffold();
|
|
116285
|
-
import { existsSync as
|
|
117356
|
+
import { existsSync as existsSync94, readFileSync as readFileSync85, mkdirSync as mkdirSync52, statSync as statSync54, writeFileSync as writeFileSync36 } from "node:fs";
|
|
116286
117357
|
import { execFileSync as execFileSync29 } from "node:child_process";
|
|
116287
|
-
import { resolve as
|
|
117358
|
+
import { resolve as resolve49, dirname as dirname38 } from "node:path";
|
|
116288
117359
|
init_state();
|
|
116289
117360
|
init_vault();
|
|
116290
117361
|
init_manager();
|
|
@@ -116699,7 +117770,7 @@ ${status} ${source_default.bold(`Step ${num3}:`)} ${title}`);
|
|
|
116699
117770
|
init_source();
|
|
116700
117771
|
init_loader();
|
|
116701
117772
|
init_hindsight();
|
|
116702
|
-
import { existsSync as
|
|
117773
|
+
import { existsSync as existsSync93 } from "node:fs";
|
|
116703
117774
|
var HINDSIGHT_READY_RETRIES = 5;
|
|
116704
117775
|
var HINDSIGHT_READY_INTERVAL_MS = 1000;
|
|
116705
117776
|
async function resolveLiteLLMForHindsight2(config, deps = {}) {
|
|
@@ -116736,7 +117807,7 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, d
|
|
|
116736
117807
|
try {
|
|
116737
117808
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
116738
117809
|
const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
116739
|
-
if (passphrase &&
|
|
117810
|
+
if (passphrase && existsSync93(vaultPath)) {
|
|
116740
117811
|
const { getStringSecret: getStringSecret2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
|
|
116741
117812
|
const existing = getStringSecret2(passphrase, vaultPath, "hindsight-api-key");
|
|
116742
117813
|
if (existing) {
|
|
@@ -117006,7 +118077,7 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, d
|
|
|
117006
118077
|
function writeSwitchroomYaml(configPath, text) {
|
|
117007
118078
|
let mode = 420;
|
|
117008
118079
|
try {
|
|
117009
|
-
mode =
|
|
118080
|
+
mode = statSync54(configPath).mode & 511;
|
|
117010
118081
|
} catch {}
|
|
117011
118082
|
writeConfigFileSync(configPath, text, mode);
|
|
117012
118083
|
}
|
|
@@ -117105,7 +118176,7 @@ async function stepConfigFile(configPath, nonInteractive) {
|
|
|
117105
118176
|
existingConfig = null;
|
|
117106
118177
|
}
|
|
117107
118178
|
}
|
|
117108
|
-
if (existingConfig &&
|
|
118179
|
+
if (existingConfig && existsSync94(existingConfig)) {
|
|
117109
118180
|
if (!nonInteractive) {
|
|
117110
118181
|
const useExisting = await askYesNo(` Found ${source_default.cyan(existingConfig)}. Use it?`, true);
|
|
117111
118182
|
if (!useExisting) {
|
|
@@ -117117,9 +118188,9 @@ async function stepConfigFile(configPath, nonInteractive) {
|
|
|
117117
118188
|
console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
|
|
117118
118189
|
const hasExplicitTimezone = config.switchroom?.timezone !== undefined || config.defaults?.timezone !== undefined;
|
|
117119
118190
|
if (!hasExplicitTimezone) {
|
|
117120
|
-
await writeDetectedTimezone(
|
|
118191
|
+
await writeDetectedTimezone(resolve49(existingConfig), nonInteractive);
|
|
117121
118192
|
}
|
|
117122
|
-
return { config, configPath:
|
|
118193
|
+
return { config, configPath: resolve49(existingConfig) };
|
|
117123
118194
|
}
|
|
117124
118195
|
return await copyExampleConfig2(nonInteractive);
|
|
117125
118196
|
}
|
|
@@ -117146,12 +118217,12 @@ async function copyExampleConfig2(nonInteractive) {
|
|
|
117146
118217
|
await writeDetectedTimezone(destFile, nonInteractive);
|
|
117147
118218
|
const config = loadConfig(destFile);
|
|
117148
118219
|
console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
|
|
117149
|
-
return { config, configPath:
|
|
118220
|
+
return { config, configPath: resolve49(destFile) };
|
|
117150
118221
|
}
|
|
117151
118222
|
async function writeDetectedTimezone(destFile, nonInteractive, detect = detectServerTimezone, prompt = ask) {
|
|
117152
118223
|
const detected = detect();
|
|
117153
118224
|
if (detected !== undefined && detected !== "UTC" && isValidTimezone(detected)) {
|
|
117154
|
-
const before2 =
|
|
118225
|
+
const before2 = readFileSync85(destFile, "utf-8");
|
|
117155
118226
|
const after2 = setSwitchroomTimezone(before2, detected);
|
|
117156
118227
|
if (after2 !== before2)
|
|
117157
118228
|
writeSwitchroomYaml(destFile, after2);
|
|
@@ -117172,7 +118243,7 @@ async function writeDetectedTimezone(destFile, nonInteractive, detect = detectSe
|
|
|
117172
118243
|
console.error(source_default.yellow(` \u26a0 "${zone}" is not a valid IANA zone (expected Region/City like ` + '"Australia/Melbourne"). Skipping \u2014 agents will use UTC. ' + "Edit switchroom.timezone in switchroom.yaml by hand."));
|
|
117173
118244
|
return;
|
|
117174
118245
|
}
|
|
117175
|
-
const before =
|
|
118246
|
+
const before = readFileSync85(destFile, "utf-8");
|
|
117176
118247
|
const after = setSwitchroomTimezone(before, zone);
|
|
117177
118248
|
if (after !== before)
|
|
117178
118249
|
writeSwitchroomYaml(destFile, after);
|
|
@@ -117251,7 +118322,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
|
|
|
117251
118322
|
try {
|
|
117252
118323
|
const { openVault: openVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
|
|
117253
118324
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
117254
|
-
if (
|
|
118325
|
+
if (existsSync94(vaultPath)) {
|
|
117255
118326
|
const secrets = openVault2(passphrase, vaultPath);
|
|
117256
118327
|
const key = rawToken.replace("vault:", "");
|
|
117257
118328
|
const entry = secrets[key];
|
|
@@ -117279,7 +118350,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
|
|
|
117279
118350
|
async function storeTokenInVault(config, vaultRef, token) {
|
|
117280
118351
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
117281
118352
|
const key = vaultRef.replace("vault:", "");
|
|
117282
|
-
if (!
|
|
118353
|
+
if (!existsSync94(vaultPath)) {
|
|
117283
118354
|
console.log(source_default.gray(" Creating encrypted vault..."));
|
|
117284
118355
|
let passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
117285
118356
|
if (!passphrase) {
|
|
@@ -117562,13 +118633,13 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
|
|
|
117562
118633
|
return;
|
|
117563
118634
|
}
|
|
117564
118635
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
117565
|
-
if (!
|
|
118636
|
+
if (!existsSync94(vaultPath)) {
|
|
117566
118637
|
console.log(source_default.gray(" Skipping (vault not created yet)."));
|
|
117567
118638
|
return;
|
|
117568
118639
|
}
|
|
117569
118640
|
const credPathRaw = config.vault?.broker?.autoUnlockCredentialPath ?? "~/.switchroom/vault-auto-unlock";
|
|
117570
118641
|
const credPath = resolvePath(credPathRaw);
|
|
117571
|
-
if (config.vault?.broker?.autoUnlock === true &&
|
|
118642
|
+
if (config.vault?.broker?.autoUnlock === true && existsSync94(credPath)) {
|
|
117572
118643
|
console.log(source_default.green(` ${STEP_DONE} Already configured (${credPath})`));
|
|
117573
118644
|
return;
|
|
117574
118645
|
}
|
|
@@ -117646,10 +118717,10 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
|
|
|
117646
118717
|
}
|
|
117647
118718
|
}
|
|
117648
118719
|
function persistApprovalAuthTelegramId(configPath) {
|
|
117649
|
-
if (!
|
|
118720
|
+
if (!existsSync94(configPath)) {
|
|
117650
118721
|
throw new ConfigError(`Cannot persist vault.broker.approvalAuth: config not found at ${configPath}`);
|
|
117651
118722
|
}
|
|
117652
|
-
const content =
|
|
118723
|
+
const content = readFileSync85(configPath, "utf-8");
|
|
117653
118724
|
const result = insertVaultBrokerApprovalAuth(content, "telegram-id");
|
|
117654
118725
|
if (result.kind === "rewritten") {
|
|
117655
118726
|
writeSwitchroomYaml(configPath, result.content);
|
|
@@ -117657,10 +118728,10 @@ function persistApprovalAuthTelegramId(configPath) {
|
|
|
117657
118728
|
return result.kind;
|
|
117658
118729
|
}
|
|
117659
118730
|
function persistDangerousMode(config, configPath) {
|
|
117660
|
-
if (!
|
|
118731
|
+
if (!existsSync94(configPath)) {
|
|
117661
118732
|
throw new ConfigError(`Cannot persist dangerous_mode: config not found at ${configPath}`);
|
|
117662
118733
|
}
|
|
117663
|
-
let content =
|
|
118734
|
+
let content = readFileSync85(configPath, "utf-8");
|
|
117664
118735
|
const agentNames = Object.keys(config.agents);
|
|
117665
118736
|
for (const name of agentNames) {
|
|
117666
118737
|
const agentPattern = new RegExp(`(^ ${name}:\\s*\\n)`, "m");
|
|
@@ -117700,7 +118771,7 @@ async function stepOnboardingGuidance(config, nonInteractive) {
|
|
|
117700
118771
|
const agentNames = Object.keys(config.agents);
|
|
117701
118772
|
let allAuthenticated = true;
|
|
117702
118773
|
for (const name of agentNames) {
|
|
117703
|
-
const agentDir =
|
|
118774
|
+
const agentDir = resolve49(agentsDir, name);
|
|
117704
118775
|
const status = getAuthStatus(name, agentDir);
|
|
117705
118776
|
if (status.authenticated) {
|
|
117706
118777
|
console.log(` ${source_default.green("OK")} ${source_default.bold(name)}` + source_default.gray(` - authenticated (expires: ${status.timeUntilExpiry ?? "unknown"})`));
|
|
@@ -117796,7 +118867,7 @@ async function stepVerification(config, nonInteractive, deps = {}, expect = {})
|
|
|
117796
118867
|
const findings = [];
|
|
117797
118868
|
let applyPending = false;
|
|
117798
118869
|
if (!nonInteractive && agentNames.length > 0) {
|
|
117799
|
-
const composeExists = (deps.composeFileExists ?? (() =>
|
|
118870
|
+
const composeExists = (deps.composeFileExists ?? (() => existsSync94(composeFilePath())))();
|
|
117800
118871
|
if (!composeExists) {
|
|
117801
118872
|
applyPending = true;
|
|
117802
118873
|
log(source_default.gray(" Not started: `switchroom apply` has not generated the compose file yet."));
|
|
@@ -117893,9 +118964,9 @@ init_source();
|
|
|
117893
118964
|
init_loader();
|
|
117894
118965
|
init_lifecycle();
|
|
117895
118966
|
init_compose_env();
|
|
117896
|
-
import { existsSync as
|
|
118967
|
+
import { existsSync as existsSync98, mkdirSync as mkdirSync56, readFileSync as readFileSync89, realpathSync as realpathSync8, statSync as statSync57, chownSync as chownSync13 } from "node:fs";
|
|
117897
118968
|
import { spawnSync as spawnSync20 } from "node:child_process";
|
|
117898
|
-
import { join as
|
|
118969
|
+
import { join as join102, dirname as dirname42 } from "node:path";
|
|
117899
118970
|
import { homedir as homedir51 } from "node:os";
|
|
117900
118971
|
|
|
117901
118972
|
// src/cli/release-yaml.ts
|
|
@@ -117956,7 +119027,7 @@ init_operator_uid();
|
|
|
117956
119027
|
init_atomic();
|
|
117957
119028
|
|
|
117958
119029
|
// src/cli/preflight-mounts.ts
|
|
117959
|
-
import { statSync as
|
|
119030
|
+
import { statSync as statSync55 } from "node:fs";
|
|
117960
119031
|
function parseHostBindSources(composeText) {
|
|
117961
119032
|
const out = [];
|
|
117962
119033
|
const FILE_HINT = /\.(ya?ml|db|log|toml|json|token|id)$|\/\.vault-token$|machine-id$|localtime$|vault-auto-unlock$|\/webkite$/;
|
|
@@ -117983,7 +119054,7 @@ function parseHostBindSources(composeText) {
|
|
|
117983
119054
|
return out;
|
|
117984
119055
|
}
|
|
117985
119056
|
function validateBindSources(composeText, deps = {}) {
|
|
117986
|
-
const
|
|
119057
|
+
const stat2 = deps.stat ?? ((p) => statSync55(p));
|
|
117987
119058
|
const sources = parseHostBindSources(composeText);
|
|
117988
119059
|
const issues = [];
|
|
117989
119060
|
const seen = new Set;
|
|
@@ -117995,7 +119066,7 @@ function validateBindSources(composeText, deps = {}) {
|
|
|
117995
119066
|
checked++;
|
|
117996
119067
|
let st = null;
|
|
117997
119068
|
try {
|
|
117998
|
-
st =
|
|
119069
|
+
st = stat2(source);
|
|
117999
119070
|
} catch {
|
|
118000
119071
|
st = null;
|
|
118001
119072
|
}
|
|
@@ -118029,18 +119100,18 @@ init_self_update();
|
|
|
118029
119100
|
init_shipped_assets();
|
|
118030
119101
|
|
|
118031
119102
|
// src/cli/self-update-io.ts
|
|
118032
|
-
import { createHash as
|
|
119103
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
118033
119104
|
import {
|
|
118034
119105
|
chmodSync as chmodSync16,
|
|
118035
119106
|
closeSync as closeSync18,
|
|
118036
119107
|
copyFileSync as copyFileSync12,
|
|
118037
119108
|
createWriteStream,
|
|
118038
|
-
existsSync as
|
|
119109
|
+
existsSync as existsSync95,
|
|
118039
119110
|
lstatSync as lstatSync13,
|
|
118040
119111
|
mkdirSync as mkdirSync53,
|
|
118041
119112
|
openSync as openSync18,
|
|
118042
|
-
readdirSync as
|
|
118043
|
-
readFileSync as
|
|
119113
|
+
readdirSync as readdirSync33,
|
|
119114
|
+
readFileSync as readFileSync86,
|
|
118044
119115
|
readSync as readSync6,
|
|
118045
119116
|
renameSync as renameSync23,
|
|
118046
119117
|
rmSync as rmSync18,
|
|
@@ -118079,9 +119150,9 @@ function defaultSelfUpdateIO() {
|
|
|
118079
119150
|
throw new Error(`empty response body for ${url}`);
|
|
118080
119151
|
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
118081
119152
|
},
|
|
118082
|
-
sha256File(
|
|
118083
|
-
const hash2 =
|
|
118084
|
-
const fd = openSync18(
|
|
119153
|
+
sha256File(path9) {
|
|
119154
|
+
const hash2 = createHash18("sha256");
|
|
119155
|
+
const fd = openSync18(path9, "r");
|
|
118085
119156
|
try {
|
|
118086
119157
|
const buf = Buffer.allocUnsafe(1 << 20);
|
|
118087
119158
|
for (;; ) {
|
|
@@ -118095,8 +119166,8 @@ function defaultSelfUpdateIO() {
|
|
|
118095
119166
|
}
|
|
118096
119167
|
return hash2.digest("hex");
|
|
118097
119168
|
},
|
|
118098
|
-
probeBinary(
|
|
118099
|
-
const r = spawnSync18(
|
|
119169
|
+
probeBinary(path9) {
|
|
119170
|
+
const r = spawnSync18(path9, ["--version"], {
|
|
118100
119171
|
encoding: "utf-8",
|
|
118101
119172
|
timeout: 30000,
|
|
118102
119173
|
env: { ...process.env, SWITCHROOM_SELF_UPDATED: "" }
|
|
@@ -118136,24 +119207,24 @@ function defaultSelfUpdateIO() {
|
|
|
118136
119207
|
copyFile(src, dest) {
|
|
118137
119208
|
copyFileSync12(src, dest);
|
|
118138
119209
|
},
|
|
118139
|
-
chmodExec(
|
|
118140
|
-
chmodSync16(
|
|
119210
|
+
chmodExec(path9) {
|
|
119211
|
+
chmodSync16(path9, 493);
|
|
118141
119212
|
},
|
|
118142
119213
|
rename(src, dest) {
|
|
118143
119214
|
renameSync23(src, dest);
|
|
118144
119215
|
},
|
|
118145
|
-
remove(
|
|
118146
|
-
rmSync18(
|
|
119216
|
+
remove(path9) {
|
|
119217
|
+
rmSync18(path9, { force: true });
|
|
118147
119218
|
},
|
|
118148
|
-
exists(
|
|
118149
|
-
return
|
|
119219
|
+
exists(path9) {
|
|
119220
|
+
return existsSync95(path9);
|
|
118150
119221
|
},
|
|
118151
|
-
dirname(
|
|
118152
|
-
return dirname39(
|
|
119222
|
+
dirname(path9) {
|
|
119223
|
+
return dirname39(path9);
|
|
118153
119224
|
},
|
|
118154
|
-
readText(
|
|
119225
|
+
readText(path9) {
|
|
118155
119226
|
try {
|
|
118156
|
-
return
|
|
119227
|
+
return readFileSync86(path9, "utf-8");
|
|
118157
119228
|
} catch {
|
|
118158
119229
|
return null;
|
|
118159
119230
|
}
|
|
@@ -118173,19 +119244,19 @@ function defaultSelfUpdateIO() {
|
|
|
118173
119244
|
symlink(target, linkPath) {
|
|
118174
119245
|
symlinkSync5(target, linkPath);
|
|
118175
119246
|
},
|
|
118176
|
-
isSymlink(
|
|
119247
|
+
isSymlink(path9) {
|
|
118177
119248
|
try {
|
|
118178
|
-
return lstatSync13(
|
|
119249
|
+
return lstatSync13(path9).isSymbolicLink();
|
|
118179
119250
|
} catch {
|
|
118180
119251
|
return false;
|
|
118181
119252
|
}
|
|
118182
119253
|
},
|
|
118183
|
-
removeTree(
|
|
118184
|
-
rmSync18(
|
|
119254
|
+
removeTree(path9) {
|
|
119255
|
+
rmSync18(path9, { recursive: true, force: true });
|
|
118185
119256
|
},
|
|
118186
119257
|
listDir(dir) {
|
|
118187
119258
|
try {
|
|
118188
|
-
return
|
|
119259
|
+
return readdirSync33(dir);
|
|
118189
119260
|
} catch {
|
|
118190
119261
|
return [];
|
|
118191
119262
|
}
|
|
@@ -118200,7 +119271,7 @@ init_install_cron();
|
|
|
118200
119271
|
|
|
118201
119272
|
// src/memory/mm-refresh-cron.ts
|
|
118202
119273
|
import { execFileSync as execFileSync30 } from "node:child_process";
|
|
118203
|
-
import { chownSync as chownSync12, existsSync as
|
|
119274
|
+
import { chownSync as chownSync12, existsSync as existsSync96, mkdirSync as mkdirSync54, readFileSync as readFileSync87, renameSync as renameSync24, statSync as statSync56, writeFileSync as writeFileSync37 } from "node:fs";
|
|
118204
119275
|
import { dirname as dirname40 } from "node:path";
|
|
118205
119276
|
var CRON_PATH3 = "/etc/cron.d/mental-model-refresh";
|
|
118206
119277
|
var CRON_SCHEDULE2 = "27 4 * * *";
|
|
@@ -118217,20 +119288,20 @@ function renderCron3(opts) {
|
|
|
118217
119288
|
`;
|
|
118218
119289
|
}
|
|
118219
119290
|
function installCron3(opts) {
|
|
118220
|
-
const
|
|
119291
|
+
const path9 = opts.path ?? CRON_PATH3;
|
|
118221
119292
|
const content = renderCron3(opts);
|
|
118222
|
-
if (
|
|
119293
|
+
if (existsSync96(path9)) {
|
|
118223
119294
|
try {
|
|
118224
|
-
if (
|
|
118225
|
-
return { status: "unchanged", path:
|
|
119295
|
+
if (readFileSync87(path9, "utf8") === content) {
|
|
119296
|
+
return { status: "unchanged", path: path9, content };
|
|
118226
119297
|
}
|
|
118227
119298
|
} catch {}
|
|
118228
119299
|
}
|
|
118229
|
-
mkdirSync54(dirname40(
|
|
118230
|
-
const tmp = `${
|
|
119300
|
+
mkdirSync54(dirname40(path9), { recursive: true });
|
|
119301
|
+
const tmp = `${path9}.${process.pid}.tmp`;
|
|
118231
119302
|
writeFileSync37(tmp, content, { mode: 420 });
|
|
118232
|
-
renameSync24(tmp,
|
|
118233
|
-
return { status: "installed", path:
|
|
119303
|
+
renameSync24(tmp, path9);
|
|
119304
|
+
return { status: "installed", path: path9, content };
|
|
118234
119305
|
}
|
|
118235
119306
|
var defaultIdResolver3 = (user) => {
|
|
118236
119307
|
const uid = Number(execFileSync30("id", ["-u", user], { encoding: "utf8" }).trim());
|
|
@@ -118241,27 +119312,27 @@ var defaultIdResolver3 = (user) => {
|
|
|
118241
119312
|
return { uid, gid };
|
|
118242
119313
|
};
|
|
118243
119314
|
function ensureLogFile3(opts) {
|
|
118244
|
-
const
|
|
118245
|
-
if (
|
|
118246
|
-
return { status: "unchanged", path:
|
|
119315
|
+
const path9 = opts.path ?? CRON_LOG_PATH3;
|
|
119316
|
+
if (existsSync96(path9))
|
|
119317
|
+
return { status: "unchanged", path: path9 };
|
|
118247
119318
|
const { uid, gid } = (opts.resolveIds ?? defaultIdResolver3)(opts.user);
|
|
118248
|
-
mkdirSync54(dirname40(
|
|
118249
|
-
writeFileSync37(
|
|
119319
|
+
mkdirSync54(dirname40(path9), { recursive: true });
|
|
119320
|
+
writeFileSync37(path9, "", { mode: 420, flag: "a" });
|
|
118250
119321
|
const isRoot = process.getuid?.() === 0;
|
|
118251
119322
|
let alreadyOwned = false;
|
|
118252
119323
|
try {
|
|
118253
|
-
const st =
|
|
119324
|
+
const st = statSync56(path9);
|
|
118254
119325
|
alreadyOwned = st.uid === uid && st.gid === gid;
|
|
118255
119326
|
} catch {}
|
|
118256
119327
|
if (isRoot && !alreadyOwned) {
|
|
118257
119328
|
try {
|
|
118258
|
-
chownSync12(
|
|
119329
|
+
chownSync12(path9, uid, gid);
|
|
118259
119330
|
} catch (e) {
|
|
118260
119331
|
if (e.code !== "EPERM")
|
|
118261
119332
|
throw e;
|
|
118262
119333
|
}
|
|
118263
119334
|
}
|
|
118264
|
-
return { status: "created", path:
|
|
119335
|
+
return { status: "created", path: path9 };
|
|
118265
119336
|
}
|
|
118266
119337
|
function renderLogrotate3(opts) {
|
|
118267
119338
|
return `${opts.logPath} {
|
|
@@ -118277,20 +119348,20 @@ function renderLogrotate3(opts) {
|
|
|
118277
119348
|
`;
|
|
118278
119349
|
}
|
|
118279
119350
|
function installLogrotate3(opts) {
|
|
118280
|
-
const
|
|
119351
|
+
const path9 = opts.path ?? LOGROTATE_PATH3;
|
|
118281
119352
|
const content = renderLogrotate3({ logPath: opts.logPath ?? CRON_LOG_PATH3, user: opts.user });
|
|
118282
|
-
if (
|
|
119353
|
+
if (existsSync96(path9)) {
|
|
118283
119354
|
try {
|
|
118284
|
-
if (
|
|
118285
|
-
return { status: "unchanged", path:
|
|
119355
|
+
if (readFileSync87(path9, "utf8") === content) {
|
|
119356
|
+
return { status: "unchanged", path: path9, content };
|
|
118286
119357
|
}
|
|
118287
119358
|
} catch {}
|
|
118288
119359
|
}
|
|
118289
|
-
mkdirSync54(dirname40(
|
|
118290
|
-
const tmp = `${
|
|
119360
|
+
mkdirSync54(dirname40(path9), { recursive: true });
|
|
119361
|
+
const tmp = `${path9}.${process.pid}.tmp`;
|
|
118291
119362
|
writeFileSync37(tmp, content, { mode: 420 });
|
|
118292
|
-
renameSync24(tmp,
|
|
118293
|
-
return { status: "installed", path:
|
|
119363
|
+
renameSync24(tmp, path9);
|
|
119364
|
+
return { status: "installed", path: path9, content };
|
|
118294
119365
|
}
|
|
118295
119366
|
function parseCronUser3(content) {
|
|
118296
119367
|
for (const line of content.split(`
|
|
@@ -118307,20 +119378,20 @@ function parseCronUser3(content) {
|
|
|
118307
119378
|
return null;
|
|
118308
119379
|
}
|
|
118309
119380
|
function reconcileCron3(opts) {
|
|
118310
|
-
const
|
|
118311
|
-
if (!
|
|
118312
|
-
return { status: "absent", path:
|
|
119381
|
+
const path9 = opts.path ?? CRON_PATH3;
|
|
119382
|
+
if (!existsSync96(path9))
|
|
119383
|
+
return { status: "absent", path: path9 };
|
|
118313
119384
|
let user = opts.fallbackUser;
|
|
118314
119385
|
try {
|
|
118315
|
-
user = parseCronUser3(
|
|
119386
|
+
user = parseCronUser3(readFileSync87(path9, "utf8")) ?? opts.fallbackUser;
|
|
118316
119387
|
} catch {}
|
|
118317
119388
|
if (!user) {
|
|
118318
|
-
throw new Error(`cannot reconcile ${
|
|
119389
|
+
throw new Error(`cannot reconcile ${path9}: no cron-user parseable from the existing fragment and no fallback user available`);
|
|
118319
119390
|
}
|
|
118320
|
-
const r = installCron3({ user, binary: opts.binary, path:
|
|
119391
|
+
const r = installCron3({ user, binary: opts.binary, path: path9 });
|
|
118321
119392
|
return {
|
|
118322
119393
|
status: r.status === "installed" ? "reconciled" : "unchanged",
|
|
118323
|
-
path:
|
|
119394
|
+
path: path9,
|
|
118324
119395
|
content: r.content
|
|
118325
119396
|
};
|
|
118326
119397
|
}
|
|
@@ -118330,7 +119401,7 @@ init_install_cron2();
|
|
|
118330
119401
|
|
|
118331
119402
|
// src/host-cli-self-heal/install-timer.ts
|
|
118332
119403
|
import { spawnSync as spawnSync19 } from "node:child_process";
|
|
118333
|
-
import { existsSync as
|
|
119404
|
+
import { existsSync as existsSync97, mkdirSync as mkdirSync55, readFileSync as readFileSync88, renameSync as renameSync25, writeFileSync as writeFileSync38 } from "node:fs";
|
|
118334
119405
|
import { dirname as dirname41 } from "node:path";
|
|
118335
119406
|
var SERVICE_PATH = "/etc/systemd/system/switchroom-self-heal.service";
|
|
118336
119407
|
var TIMER_PATH = "/etc/systemd/system/switchroom-self-heal.timer";
|
|
@@ -118376,22 +119447,22 @@ function renderTimer() {
|
|
|
118376
119447
|
` + `WantedBy=timers.target
|
|
118377
119448
|
`;
|
|
118378
119449
|
}
|
|
118379
|
-
function writeUnitIdempotent(
|
|
118380
|
-
if (
|
|
119450
|
+
function writeUnitIdempotent(path9, content) {
|
|
119451
|
+
if (existsSync97(path9)) {
|
|
118381
119452
|
try {
|
|
118382
|
-
if (
|
|
119453
|
+
if (readFileSync88(path9, "utf8") === content)
|
|
118383
119454
|
return "unchanged";
|
|
118384
119455
|
} catch {}
|
|
118385
119456
|
}
|
|
118386
|
-
mkdirSync55(dirname41(
|
|
118387
|
-
const tmp = `${
|
|
119457
|
+
mkdirSync55(dirname41(path9), { recursive: true });
|
|
119458
|
+
const tmp = `${path9}.${process.pid}.tmp`;
|
|
118388
119459
|
writeFileSync38(tmp, content, { mode: 420 });
|
|
118389
|
-
renameSync25(tmp,
|
|
119460
|
+
renameSync25(tmp, path9);
|
|
118390
119461
|
return "written";
|
|
118391
119462
|
}
|
|
118392
119463
|
function homeFromPasswd2(user) {
|
|
118393
119464
|
try {
|
|
118394
|
-
for (const line of
|
|
119465
|
+
for (const line of readFileSync88("/etc/passwd", "utf8").split(`
|
|
118395
119466
|
`)) {
|
|
118396
119467
|
const f = line.split(":");
|
|
118397
119468
|
if (f[0] === user && f[5])
|
|
@@ -118408,7 +119479,7 @@ function resolveOperatorUser(env2) {
|
|
|
118408
119479
|
}
|
|
118409
119480
|
function installSelfHealTimer(deps = {}) {
|
|
118410
119481
|
const env2 = deps.env ?? process.env;
|
|
118411
|
-
const systemdBooted = deps.systemdBooted ?? (() =>
|
|
119482
|
+
const systemdBooted = deps.systemdBooted ?? (() => existsSync97(SYSTEMD_MARKER));
|
|
118412
119483
|
const geteuid = deps.geteuid ?? (() => typeof process.geteuid === "function" ? process.geteuid() : undefined);
|
|
118413
119484
|
const homeForUser = deps.homeForUser ?? homeFromPasswd2;
|
|
118414
119485
|
const servicePath = deps.servicePath ?? SERVICE_PATH;
|
|
@@ -118469,22 +119540,22 @@ function installSelfHealTimer(deps = {}) {
|
|
|
118469
119540
|
// src/cli/update.ts
|
|
118470
119541
|
function defaultPersistPin(configPath) {
|
|
118471
119542
|
return (pin) => {
|
|
118472
|
-
const
|
|
118473
|
-
const before =
|
|
119543
|
+
const path9 = configPath ?? findConfigFile();
|
|
119544
|
+
const before = readFileSync89(path9, "utf8");
|
|
118474
119545
|
const after = setReleasePinInConfig(before, pin);
|
|
118475
119546
|
if (after === before)
|
|
118476
119547
|
return;
|
|
118477
|
-
writeConfigFileSync(
|
|
119548
|
+
writeConfigFileSync(path9, after, statSync57(path9).mode & 511);
|
|
118478
119549
|
try {
|
|
118479
119550
|
if (typeof process.geteuid === "function" && process.geteuid() === 0) {
|
|
118480
119551
|
const uid = resolveOperatorUid();
|
|
118481
119552
|
if (uid !== undefined)
|
|
118482
|
-
chownSync13(
|
|
119553
|
+
chownSync13(path9, uid, uid);
|
|
118483
119554
|
}
|
|
118484
119555
|
} catch {}
|
|
118485
119556
|
};
|
|
118486
119557
|
}
|
|
118487
|
-
var DEFAULT_COMPOSE_PATH2 =
|
|
119558
|
+
var DEFAULT_COMPOSE_PATH2 = join102(homedir51(), ".switchroom", "compose", "docker-compose.yml");
|
|
118488
119559
|
var PACKAGED_INSTALL_KINDS = new Set([
|
|
118489
119560
|
"static-binary",
|
|
118490
119561
|
"npm-global",
|
|
@@ -118493,9 +119564,9 @@ var PACKAGED_INSTALL_KINDS = new Set([
|
|
|
118493
119564
|
function runningFromSwitchroomCheckout(scriptPath) {
|
|
118494
119565
|
let dir = dirname42(scriptPath);
|
|
118495
119566
|
for (let i2 = 0;i2 < 12; i2++) {
|
|
118496
|
-
if (
|
|
119567
|
+
if (existsSync98(join102(dir, ".git"))) {
|
|
118497
119568
|
try {
|
|
118498
|
-
const pkg = JSON.parse(
|
|
119569
|
+
const pkg = JSON.parse(readFileSync89(join102(dir, "package.json"), "utf-8"));
|
|
118499
119570
|
if (pkg.name === "switchroom")
|
|
118500
119571
|
return true;
|
|
118501
119572
|
} catch {}
|
|
@@ -118562,7 +119633,7 @@ function planUpdate(opts) {
|
|
|
118562
119633
|
bundleDir: import.meta.dirname,
|
|
118563
119634
|
execPath: process.execPath,
|
|
118564
119635
|
scriptPath,
|
|
118565
|
-
inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" ||
|
|
119636
|
+
inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync98("/.dockerenv")
|
|
118566
119637
|
});
|
|
118567
119638
|
if (detection.kind !== "static-binary") {
|
|
118568
119639
|
say(source_default.gray(` host CLI not self-updated: ${detection.reason}
|
|
@@ -118645,7 +119716,7 @@ function planUpdate(opts) {
|
|
|
118645
119716
|
steps.push({
|
|
118646
119717
|
name: "pull-images",
|
|
118647
119718
|
description: "Pull broker / kernel / agent images from GHCR",
|
|
118648
|
-
skipReason: opts.skipImages ? "--skip-images flag set" : !
|
|
119719
|
+
skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync98(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
|
|
118649
119720
|
run: () => {
|
|
118650
119721
|
const r = runner("docker", [
|
|
118651
119722
|
"compose",
|
|
@@ -118902,13 +119973,13 @@ function planUpdate(opts) {
|
|
|
118902
119973
|
bundleDir: import.meta.dirname,
|
|
118903
119974
|
execPath: process.execPath
|
|
118904
119975
|
});
|
|
118905
|
-
const dest =
|
|
119976
|
+
const dest = join102(homedir51(), ".switchroom", "skills", "_bundled");
|
|
118906
119977
|
if (resolution.path === null) {
|
|
118907
119978
|
const detection = detectInstallKind(opts.installProbe ?? {
|
|
118908
119979
|
bundleDir: import.meta.dirname,
|
|
118909
119980
|
execPath: process.execPath,
|
|
118910
119981
|
scriptPath,
|
|
118911
|
-
inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" ||
|
|
119982
|
+
inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync98("/.dockerenv")
|
|
118912
119983
|
});
|
|
118913
119984
|
const detail = `no shipped skills/ payload found for this install ` + `(${detection.kind}; ${describeShippedAssetSearch(resolution)})`;
|
|
118914
119985
|
if (PACKAGED_INSTALL_KINDS.has(detection.kind)) {
|
|
@@ -118947,11 +120018,11 @@ function planUpdate(opts) {
|
|
|
118947
120018
|
run: () => {
|
|
118948
120019
|
if (opts.syncBundledSkillsFn)
|
|
118949
120020
|
return;
|
|
118950
|
-
const dest =
|
|
118951
|
-
if (!
|
|
120021
|
+
const dest = join102(homedir51(), ".switchroom", "skills", "_bundled");
|
|
120022
|
+
if (!existsSync98(dest)) {
|
|
118952
120023
|
return;
|
|
118953
120024
|
}
|
|
118954
|
-
const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !
|
|
120025
|
+
const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !existsSync98(join102(dest, key)));
|
|
118955
120026
|
if (missing.length > 0) {
|
|
118956
120027
|
throw new Error(`verify-bundled-skills: builtin default skill(s) missing from the pool after sync: ` + `${missing.join(", ")}. These ship in the CLI package and must exist in ${dest}. ` + `This is a broken sync or a packaging regression \u2014 the pool is not converged.`);
|
|
118957
120028
|
}
|
|
@@ -118986,7 +120057,7 @@ function planUpdate(opts) {
|
|
|
118986
120057
|
description: "docker compose up -d --remove-orphans (recreates services with new images / compose)",
|
|
118987
120058
|
run: () => {
|
|
118988
120059
|
try {
|
|
118989
|
-
const composeText =
|
|
120060
|
+
const composeText = readFileSync89(composePath, "utf8");
|
|
118990
120061
|
const pf = validateBindSources(composeText);
|
|
118991
120062
|
if (!pf.ok)
|
|
118992
120063
|
throw new Error(formatPreflightError(pf));
|
|
@@ -119061,14 +120132,14 @@ function defaultStatusProbe(composePath) {
|
|
|
119061
120132
|
} catch {}
|
|
119062
120133
|
if (scriptPath) {
|
|
119063
120134
|
try {
|
|
119064
|
-
cliBuiltAt = new Date(
|
|
120135
|
+
cliBuiltAt = new Date(statSync57(scriptPath).mtimeMs).toISOString();
|
|
119065
120136
|
} catch {}
|
|
119066
120137
|
let dir = dirname42(scriptPath);
|
|
119067
120138
|
for (let i2 = 0;i2 < 8; i2++) {
|
|
119068
|
-
const pkgPath =
|
|
119069
|
-
if (
|
|
120139
|
+
const pkgPath = join102(dir, "package.json");
|
|
120140
|
+
if (existsSync98(pkgPath)) {
|
|
119070
120141
|
try {
|
|
119071
|
-
const pkg = JSON.parse(
|
|
120142
|
+
const pkg = JSON.parse(readFileSync89(pkgPath, "utf-8"));
|
|
119072
120143
|
if (typeof pkg.version === "string")
|
|
119073
120144
|
cliVersion = pkg.version;
|
|
119074
120145
|
} catch (err) {
|
|
@@ -119089,7 +120160,7 @@ function defaultStatusProbe(composePath) {
|
|
|
119089
120160
|
warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
|
|
119090
120161
|
}
|
|
119091
120162
|
const services = [];
|
|
119092
|
-
if (!
|
|
120163
|
+
if (!existsSync98(composePath)) {
|
|
119093
120164
|
warnings.push(`compose file not found at ${composePath}; service status unknown`);
|
|
119094
120165
|
return { cliVersion, cliBuiltAt, services, warnings };
|
|
119095
120166
|
}
|
|
@@ -119352,22 +120423,22 @@ function registerUpdateCommand(program3) {
|
|
|
119352
120423
|
// src/cli/rollout.ts
|
|
119353
120424
|
init_helpers();
|
|
119354
120425
|
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
119355
|
-
import { readFileSync as
|
|
120426
|
+
import { readFileSync as readFileSync91, chownSync as chownSync14, statSync as statSync60 } from "node:fs";
|
|
119356
120427
|
import { homedir as homedir53 } from "node:os";
|
|
119357
120428
|
|
|
119358
120429
|
// src/cli/rollout-pin-journal.ts
|
|
119359
120430
|
import {
|
|
119360
|
-
existsSync as
|
|
119361
|
-
readFileSync as
|
|
120431
|
+
existsSync as existsSync99,
|
|
120432
|
+
readFileSync as readFileSync90,
|
|
119362
120433
|
writeFileSync as writeFileSync39,
|
|
119363
120434
|
renameSync as renameSync26,
|
|
119364
120435
|
unlinkSync as unlinkSync19,
|
|
119365
|
-
statSync as
|
|
120436
|
+
statSync as statSync58,
|
|
119366
120437
|
mkdirSync as mkdirSync57
|
|
119367
120438
|
} from "node:fs";
|
|
119368
120439
|
import { homedir as homedir52 } from "node:os";
|
|
119369
|
-
import { createHash as
|
|
119370
|
-
import { join as
|
|
120440
|
+
import { createHash as createHash19 } from "node:crypto";
|
|
120441
|
+
import { join as join103, basename as basename9, resolve as resolve51, dirname as dirname43 } from "node:path";
|
|
119371
120442
|
init_flock();
|
|
119372
120443
|
var PIN_JOURNAL_MAX_AGE_MS = 15 * 60 * 1000;
|
|
119373
120444
|
var STATE_DIR_NAME = ".switchroom";
|
|
@@ -119376,16 +120447,16 @@ function pinJournalDir(configPath) {
|
|
|
119376
120447
|
if (override && override.trim().length > 0)
|
|
119377
120448
|
return override.trim();
|
|
119378
120449
|
if (configPath) {
|
|
119379
|
-
const dir = dirname43(
|
|
120450
|
+
const dir = dirname43(resolve51(configPath));
|
|
119380
120451
|
if (basename9(dir) === STATE_DIR_NAME)
|
|
119381
120452
|
return dir;
|
|
119382
120453
|
}
|
|
119383
|
-
return
|
|
120454
|
+
return join103(homedir52(), STATE_DIR_NAME);
|
|
119384
120455
|
}
|
|
119385
120456
|
function pinJournalPath(configPath) {
|
|
119386
|
-
const abs =
|
|
119387
|
-
const key =
|
|
119388
|
-
return
|
|
120457
|
+
const abs = resolve51(configPath);
|
|
120458
|
+
const key = createHash19("sha256").update(abs).digest("hex").slice(0, 12);
|
|
120459
|
+
return join103(pinJournalDir(abs), `.rollout-pin-journal.${basename9(abs)}.${key}.json`);
|
|
119389
120460
|
}
|
|
119390
120461
|
function isPidAlive(pid) {
|
|
119391
120462
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -119419,9 +120490,9 @@ function readPinJournal(configPath, warn = (m) => process.stderr.write(m)) {
|
|
|
119419
120490
|
const p = pinJournalPath(configPath);
|
|
119420
120491
|
let raw;
|
|
119421
120492
|
try {
|
|
119422
|
-
raw =
|
|
120493
|
+
raw = readFileSync90(p, "utf8");
|
|
119423
120494
|
} catch (e) {
|
|
119424
|
-
if (
|
|
120495
|
+
if (existsSync99(p)) {
|
|
119425
120496
|
warn(`\u26a0\ufe0f rollout pin journal: ${p} exists but could not be read ` + `(${e.message}). A provisional \`release.pin\` may be ` + `uncommitted \u2014 verify it host-side before the next reconcile.
|
|
119426
120497
|
`);
|
|
119427
120498
|
}
|
|
@@ -119472,7 +120543,7 @@ function beginPinPersist(configPath, pin, opts = {}) {
|
|
|
119472
120543
|
warn(`\u26a0\ufe0f rollout pin journal: overwriting an ABANDONED journal at ${p} ` + `(pid ${existing.pid} recorded ${existing.at}, provisional pin ` + `${existing.pin}). Its roll never committed or reverted; \`release.pin\` ` + `in ${configPath} may still name that unproven build \u2014 verify it.
|
|
119473
120544
|
`);
|
|
119474
120545
|
}
|
|
119475
|
-
const priorPin = getReleasePinFromConfig(
|
|
120546
|
+
const priorPin = getReleasePinFromConfig(readFileSync90(configPath, "utf8"));
|
|
119476
120547
|
const journal = {
|
|
119477
120548
|
v: 1,
|
|
119478
120549
|
configPath,
|
|
@@ -119493,7 +120564,7 @@ function commitPinPersist(configPath) {
|
|
|
119493
120564
|
unlinkSync19(p);
|
|
119494
120565
|
return null;
|
|
119495
120566
|
} catch (e) {
|
|
119496
|
-
if (!
|
|
120567
|
+
if (!existsSync99(p))
|
|
119497
120568
|
return null;
|
|
119498
120569
|
return `rollout pin journal: FAILED to clear ${p} after a SUCCESSFUL roll ` + `(${e.message}). Delete it host-side \u2014 while it exists, ` + `recovery may revert a proven \`release.pin\`.`;
|
|
119499
120570
|
}
|
|
@@ -119515,11 +120586,11 @@ function rollbackPinPersist(configPath, opts = {}) {
|
|
|
119515
120586
|
}
|
|
119516
120587
|
}
|
|
119517
120588
|
try {
|
|
119518
|
-
const current =
|
|
120589
|
+
const current = readFileSync90(configPath, "utf8");
|
|
119519
120590
|
const next = journal.priorPin ? setReleasePinInConfig(current, journal.priorPin) : deleteReleasePinInConfig(current);
|
|
119520
120591
|
let mode = 384;
|
|
119521
120592
|
try {
|
|
119522
|
-
mode =
|
|
120593
|
+
mode = statSync58(configPath).mode & 511;
|
|
119523
120594
|
} catch {}
|
|
119524
120595
|
if (opts.writeConfig) {
|
|
119525
120596
|
opts.writeConfig(configPath, next, mode);
|
|
@@ -119569,8 +120640,8 @@ init_host_cli_stamp();
|
|
|
119569
120640
|
|
|
119570
120641
|
// src/cli/host-cli-upgrade.ts
|
|
119571
120642
|
init_self_update();
|
|
119572
|
-
import { lchownSync, lstatSync as lstatSync14, readdirSync as
|
|
119573
|
-
import { basename as basename10, dirname as dirname44, join as
|
|
120643
|
+
import { lchownSync, lstatSync as lstatSync14, readdirSync as readdirSync34, statSync as statSync59 } from "node:fs";
|
|
120644
|
+
import { basename as basename10, dirname as dirname44, join as join104 } from "node:path";
|
|
119574
120645
|
init_shipped_assets();
|
|
119575
120646
|
var HOST_CLI_UPGRADE_SENTINEL = "SWITCHROOM_HOST_CLI_UPGRADE:";
|
|
119576
120647
|
function encodeHostCliUpgradeResult(r) {
|
|
@@ -119606,7 +120677,7 @@ function defaultIo() {
|
|
|
119606
120677
|
return {
|
|
119607
120678
|
isFile: (p) => {
|
|
119608
120679
|
try {
|
|
119609
|
-
return
|
|
120680
|
+
return statSync59(p).isFile();
|
|
119610
120681
|
} catch {
|
|
119611
120682
|
return false;
|
|
119612
120683
|
}
|
|
@@ -119636,7 +120707,7 @@ function defaultIo() {
|
|
|
119636
120707
|
chown: (p, uid, gid) => lchownSync(p, uid, gid),
|
|
119637
120708
|
list: (d) => {
|
|
119638
120709
|
try {
|
|
119639
|
-
return
|
|
120710
|
+
return readdirSync34(d);
|
|
119640
120711
|
} catch {
|
|
119641
120712
|
return [];
|
|
119642
120713
|
}
|
|
@@ -119667,7 +120738,7 @@ function handBackOwnership(paths, owner, io) {
|
|
|
119667
120738
|
if (kind !== "dir")
|
|
119668
120739
|
continue;
|
|
119669
120740
|
for (const child of io.list(p))
|
|
119670
|
-
stack.push(
|
|
120741
|
+
stack.push(join104(p, child));
|
|
119671
120742
|
}
|
|
119672
120743
|
return failures;
|
|
119673
120744
|
}
|
|
@@ -119706,7 +120777,7 @@ async function runHostCliUpgrade(opts, io = defaultIo(), log = () => {}) {
|
|
|
119706
120777
|
const installRoot = payloadInstallRoot(binary);
|
|
119707
120778
|
const failures = handBackOwnership([
|
|
119708
120779
|
binary,
|
|
119709
|
-
|
|
120780
|
+
join104(dirname44(binary), ".switchroom-versions"),
|
|
119710
120781
|
dirname44(installRoot),
|
|
119711
120782
|
installRoot,
|
|
119712
120783
|
payloadVersionDir(installRoot, pin)
|
|
@@ -119802,21 +120873,21 @@ function planHostCliHeal(opts) {
|
|
|
119802
120873
|
reason: `the host CLI is a ${stamp.installKind} install \u2014 only a static-binary ` + `install can be replaced by swapping one file, so an operator must run ` + `the upgrade host-side`
|
|
119803
120874
|
};
|
|
119804
120875
|
}
|
|
119805
|
-
const
|
|
119806
|
-
if (!
|
|
119807
|
-
return { action: "skip", reason: `recorded host CLI path "${
|
|
120876
|
+
const path9 = stamp.path;
|
|
120877
|
+
if (!path9.startsWith("/") || path9.includes("/..")) {
|
|
120878
|
+
return { action: "skip", reason: `recorded host CLI path "${path9}" is not a plain absolute path` };
|
|
119808
120879
|
}
|
|
119809
|
-
const installDir =
|
|
119810
|
-
const name =
|
|
120880
|
+
const installDir = path9.slice(0, path9.lastIndexOf("/"));
|
|
120881
|
+
const name = path9.slice(path9.lastIndexOf("/") + 1);
|
|
119811
120882
|
if (name !== "switchroom") {
|
|
119812
|
-
return { action: "skip", reason: `recorded host CLI path "${
|
|
120883
|
+
return { action: "skip", reason: `recorded host CLI path "${path9}" is not named \`switchroom\`` };
|
|
119813
120884
|
}
|
|
119814
120885
|
const prefixHostPath = installDir.slice(0, installDir.lastIndexOf("/"));
|
|
119815
120886
|
const dirName = installDir.slice(installDir.lastIndexOf("/") + 1);
|
|
119816
120887
|
if (prefixHostPath.length === 0 || dirName.length === 0) {
|
|
119817
120888
|
return {
|
|
119818
120889
|
action: "skip",
|
|
119819
|
-
reason: `recorded host CLI path "${
|
|
120890
|
+
reason: `recorded host CLI path "${path9}" has no install prefix to bind \u2014 ` + `refusing to mount the host root`
|
|
119820
120891
|
};
|
|
119821
120892
|
}
|
|
119822
120893
|
return {
|
|
@@ -120501,13 +121572,13 @@ function createRolloutDeps(params) {
|
|
|
120501
121572
|
}
|
|
120502
121573
|
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
120503
121574
|
};
|
|
120504
|
-
const writeConfigPreservingOwnership = (
|
|
120505
|
-
writeConfigFileSync(
|
|
121575
|
+
const writeConfigPreservingOwnership = (path9, text, mode) => {
|
|
121576
|
+
writeConfigFileSync(path9, text, mode);
|
|
120506
121577
|
try {
|
|
120507
121578
|
if (typeof process.geteuid === "function" && process.geteuid() === 0) {
|
|
120508
121579
|
const uid = resolveOperatorUid();
|
|
120509
121580
|
if (uid !== undefined) {
|
|
120510
|
-
chownSync14(
|
|
121581
|
+
chownSync14(path9, uid, uid);
|
|
120511
121582
|
} else if (hostdCtx) {
|
|
120512
121583
|
warn("\u26a0\ufe0f rollout (hostd path): could not resolve operator uid; " + "skipping config chown-back to avoid leaving switchroom.yaml " + "root-owned. Verify ownership host-side if other yaml verbs " + `EACCES.
|
|
120513
121584
|
`);
|
|
@@ -120566,12 +121637,12 @@ function createRolloutDeps(params) {
|
|
|
120566
121637
|
return stamp ? { hostCli: stamp } : {};
|
|
120567
121638
|
})(),
|
|
120568
121639
|
persistPin: (pin) => {
|
|
120569
|
-
const before =
|
|
121640
|
+
const before = readFileSync91(configPath, "utf8");
|
|
120570
121641
|
const after = setReleasePinInConfig(before, pin);
|
|
120571
121642
|
if (after === before)
|
|
120572
121643
|
return false;
|
|
120573
121644
|
beginPinPersist(configPath, pin);
|
|
120574
|
-
writeConfigPreservingOwnership(configPath, after,
|
|
121645
|
+
writeConfigPreservingOwnership(configPath, after, statSync60(configPath).mode & 511);
|
|
120575
121646
|
return true;
|
|
120576
121647
|
},
|
|
120577
121648
|
commitPin: () => {
|
|
@@ -120847,7 +121918,7 @@ init_source();
|
|
|
120847
121918
|
init_helpers();
|
|
120848
121919
|
init_loader();
|
|
120849
121920
|
init_lifecycle();
|
|
120850
|
-
import { resolve as
|
|
121921
|
+
import { resolve as resolve52 } from "node:path";
|
|
120851
121922
|
|
|
120852
121923
|
// src/cli/version.ts
|
|
120853
121924
|
init_source();
|
|
@@ -120855,8 +121926,8 @@ init_helpers();
|
|
|
120855
121926
|
init_lifecycle();
|
|
120856
121927
|
init_resolve_version();
|
|
120857
121928
|
import { execSync as execSync3 } from "node:child_process";
|
|
120858
|
-
import { existsSync as
|
|
120859
|
-
import { dirname as dirname45, join as
|
|
121929
|
+
import { existsSync as existsSync100, readFileSync as readFileSync92 } from "node:fs";
|
|
121930
|
+
import { dirname as dirname45, join as join105 } from "node:path";
|
|
120860
121931
|
function getClaudeCodeVersion() {
|
|
120861
121932
|
try {
|
|
120862
121933
|
const out = execSync3("claude --version 2>/dev/null", {
|
|
@@ -120906,11 +121977,11 @@ function formatUptime3(timestamp) {
|
|
|
120906
121977
|
function locateSwitchroomInstallDir() {
|
|
120907
121978
|
let dir = import.meta.dirname;
|
|
120908
121979
|
for (let i2 = 0;i2 < 10 && dir && dir !== "/"; i2++) {
|
|
120909
|
-
const pkgPath =
|
|
120910
|
-
if (
|
|
121980
|
+
const pkgPath = join105(dir, "package.json");
|
|
121981
|
+
if (existsSync100(pkgPath)) {
|
|
120911
121982
|
try {
|
|
120912
|
-
const pkg = JSON.parse(
|
|
120913
|
-
if (pkg.name === "switchroom" &&
|
|
121983
|
+
const pkg = JSON.parse(readFileSync92(pkgPath, "utf-8"));
|
|
121984
|
+
if (pkg.name === "switchroom" && existsSync100(join105(dir, ".git"))) {
|
|
120914
121985
|
return dir;
|
|
120915
121986
|
}
|
|
120916
121987
|
} catch {}
|
|
@@ -121003,7 +122074,7 @@ function registerRestartCommand(program3) {
|
|
|
121003
122074
|
}
|
|
121004
122075
|
const didRestart = res.restarted || !graceful;
|
|
121005
122076
|
if (didRestart) {
|
|
121006
|
-
const agentDir =
|
|
122077
|
+
const agentDir = resolve52(agentsDir, name);
|
|
121007
122078
|
const converged = waitForAuthConverge(name, agentDir);
|
|
121008
122079
|
if (!converged) {
|
|
121009
122080
|
console.log(source_default.yellow(` ${name}: agent is up but auth status didn't converge in 30s \u2014 check logs`));
|
|
@@ -121086,37 +122157,37 @@ Dependency manifest`));
|
|
|
121086
122157
|
// src/cli/handoff.ts
|
|
121087
122158
|
init_helpers();
|
|
121088
122159
|
init_loader();
|
|
121089
|
-
import { resolve as
|
|
122160
|
+
import { resolve as resolve53 } from "node:path";
|
|
121090
122161
|
init_merge();
|
|
121091
122162
|
|
|
121092
122163
|
// src/agents/session-retention.ts
|
|
121093
122164
|
import {
|
|
121094
|
-
existsSync as
|
|
121095
|
-
readdirSync as
|
|
121096
|
-
statSync as
|
|
122165
|
+
existsSync as existsSync101,
|
|
122166
|
+
readdirSync as readdirSync35,
|
|
122167
|
+
statSync as statSync61,
|
|
121097
122168
|
unlinkSync as unlinkSync20
|
|
121098
122169
|
} from "node:fs";
|
|
121099
|
-
import { join as
|
|
122170
|
+
import { join as join106 } from "node:path";
|
|
121100
122171
|
var DEFAULT_SESSION_RETENTION_MAX_COUNT = 20;
|
|
121101
122172
|
var DEFAULT_SESSION_RETENTION_MAX_AGE_DAYS = 30;
|
|
121102
122173
|
var MIN_KEEP = 2;
|
|
121103
122174
|
function collectSessionJsonl(claudeConfigDir) {
|
|
121104
|
-
const projects =
|
|
121105
|
-
if (!
|
|
122175
|
+
const projects = join106(claudeConfigDir, "projects");
|
|
122176
|
+
if (!existsSync101(projects))
|
|
121106
122177
|
return [];
|
|
121107
122178
|
const found = [];
|
|
121108
122179
|
const walk2 = (dir) => {
|
|
121109
122180
|
let entries;
|
|
121110
122181
|
try {
|
|
121111
|
-
entries =
|
|
122182
|
+
entries = readdirSync35(dir);
|
|
121112
122183
|
} catch {
|
|
121113
122184
|
return;
|
|
121114
122185
|
}
|
|
121115
122186
|
for (const name of entries) {
|
|
121116
|
-
const full =
|
|
122187
|
+
const full = join106(dir, name);
|
|
121117
122188
|
let st;
|
|
121118
122189
|
try {
|
|
121119
|
-
st =
|
|
122190
|
+
st = statSync61(full);
|
|
121120
122191
|
} catch {
|
|
121121
122192
|
continue;
|
|
121122
122193
|
}
|
|
@@ -121187,7 +122258,7 @@ function registerHandoffCommand(program3) {
|
|
|
121187
122258
|
}
|
|
121188
122259
|
agentConfig = resolveAgentConfig(config.defaults, config.profiles, rawAgent);
|
|
121189
122260
|
const agentsDir = resolveAgentsDir(config);
|
|
121190
|
-
agentDir =
|
|
122261
|
+
agentDir = resolve53(agentsDir, agentName);
|
|
121191
122262
|
} catch (err) {
|
|
121192
122263
|
if (!(err instanceof ConfigError))
|
|
121193
122264
|
throw err;
|
|
@@ -121202,7 +122273,7 @@ function registerHandoffCommand(program3) {
|
|
|
121202
122273
|
`);
|
|
121203
122274
|
return;
|
|
121204
122275
|
}
|
|
121205
|
-
const claudeConfigDir =
|
|
122276
|
+
const claudeConfigDir = resolve53(agentDir, ".claude");
|
|
121206
122277
|
const jsonl = findLatestSessionJsonl(claudeConfigDir);
|
|
121207
122278
|
if (!jsonl) {
|
|
121208
122279
|
process.stderr.write(`handoff: no session JSONL under ${claudeConfigDir}/projects; skipping
|
|
@@ -121234,18 +122305,18 @@ function registerHandoffCommand(program3) {
|
|
|
121234
122305
|
// src/issues/store.ts
|
|
121235
122306
|
import {
|
|
121236
122307
|
closeSync as closeSync19,
|
|
121237
|
-
existsSync as
|
|
122308
|
+
existsSync as existsSync102,
|
|
121238
122309
|
mkdirSync as mkdirSync58,
|
|
121239
122310
|
openSync as openSync19,
|
|
121240
|
-
readdirSync as
|
|
121241
|
-
readFileSync as
|
|
122311
|
+
readdirSync as readdirSync36,
|
|
122312
|
+
readFileSync as readFileSync93,
|
|
121242
122313
|
renameSync as renameSync27,
|
|
121243
|
-
statSync as
|
|
122314
|
+
statSync as statSync62,
|
|
121244
122315
|
unlinkSync as unlinkSync21,
|
|
121245
122316
|
writeFileSync as writeFileSync40,
|
|
121246
122317
|
writeSync as writeSync9
|
|
121247
122318
|
} from "node:fs";
|
|
121248
|
-
import { join as
|
|
122319
|
+
import { join as join107 } from "node:path";
|
|
121249
122320
|
import { randomBytes as randomBytes16 } from "node:crypto";
|
|
121250
122321
|
import { execSync as execSync4 } from "node:child_process";
|
|
121251
122322
|
|
|
@@ -121272,12 +122343,12 @@ function computeFingerprint(source, code) {
|
|
|
121272
122343
|
var ISSUES_FILE = "issues.jsonl";
|
|
121273
122344
|
var ISSUES_LOCK = "issues.lock";
|
|
121274
122345
|
function readAll(stateDir) {
|
|
121275
|
-
const
|
|
121276
|
-
if (!
|
|
122346
|
+
const path9 = join107(stateDir, ISSUES_FILE);
|
|
122347
|
+
if (!existsSync102(path9))
|
|
121277
122348
|
return [];
|
|
121278
122349
|
let raw;
|
|
121279
122350
|
try {
|
|
121280
|
-
raw =
|
|
122351
|
+
raw = readFileSync93(path9, "utf-8");
|
|
121281
122352
|
} catch {
|
|
121282
122353
|
return [];
|
|
121283
122354
|
}
|
|
@@ -121349,8 +122420,8 @@ function record(stateDir, input, nowFn = Date.now) {
|
|
|
121349
122420
|
return result;
|
|
121350
122421
|
});
|
|
121351
122422
|
}
|
|
121352
|
-
function
|
|
121353
|
-
if (!
|
|
122423
|
+
function resolve54(stateDir, fingerprint, nowFn = Date.now) {
|
|
122424
|
+
if (!existsSync102(join107(stateDir, ISSUES_FILE)))
|
|
121354
122425
|
return 0;
|
|
121355
122426
|
return withLock(stateDir, () => {
|
|
121356
122427
|
const all = readAll(stateDir);
|
|
@@ -121368,7 +122439,7 @@ function resolve53(stateDir, fingerprint, nowFn = Date.now) {
|
|
|
121368
122439
|
});
|
|
121369
122440
|
}
|
|
121370
122441
|
function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
121371
|
-
if (!
|
|
122442
|
+
if (!existsSync102(join107(stateDir, ISSUES_FILE)))
|
|
121372
122443
|
return 0;
|
|
121373
122444
|
return withLock(stateDir, () => {
|
|
121374
122445
|
const all = readAll(stateDir);
|
|
@@ -121386,7 +122457,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
|
121386
122457
|
});
|
|
121387
122458
|
}
|
|
121388
122459
|
function prune(stateDir, opts = {}) {
|
|
121389
|
-
if (!
|
|
122460
|
+
if (!existsSync102(join107(stateDir, ISSUES_FILE)))
|
|
121390
122461
|
return 0;
|
|
121391
122462
|
return withLock(stateDir, () => {
|
|
121392
122463
|
const all = readAll(stateDir);
|
|
@@ -121419,21 +122490,21 @@ function ensureDir2(stateDir) {
|
|
|
121419
122490
|
mkdirSync58(stateDir, { recursive: true });
|
|
121420
122491
|
}
|
|
121421
122492
|
function writeAll(stateDir, events) {
|
|
121422
|
-
const
|
|
122493
|
+
const path9 = join107(stateDir, ISSUES_FILE);
|
|
121423
122494
|
sweepOrphanTmpFiles(stateDir);
|
|
121424
|
-
const tmp = `${
|
|
122495
|
+
const tmp = `${path9}.tmp-${process.pid}-${randomBytes16(4).toString("hex")}`;
|
|
121425
122496
|
const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
|
|
121426
122497
|
`) + `
|
|
121427
122498
|
`;
|
|
121428
122499
|
writeFileSync40(tmp, body, "utf-8");
|
|
121429
|
-
renameSync27(tmp,
|
|
122500
|
+
renameSync27(tmp, path9);
|
|
121430
122501
|
}
|
|
121431
122502
|
var ORPHAN_TMP_TTL_MS = 60000;
|
|
121432
122503
|
var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
|
|
121433
122504
|
function sweepOrphanTmpFiles(stateDir) {
|
|
121434
122505
|
let entries;
|
|
121435
122506
|
try {
|
|
121436
|
-
entries =
|
|
122507
|
+
entries = readdirSync36(stateDir);
|
|
121437
122508
|
} catch {
|
|
121438
122509
|
return;
|
|
121439
122510
|
}
|
|
@@ -121441,10 +122512,10 @@ function sweepOrphanTmpFiles(stateDir) {
|
|
|
121441
122512
|
for (const entry of entries) {
|
|
121442
122513
|
if (!entry.startsWith(TMP_PREFIX))
|
|
121443
122514
|
continue;
|
|
121444
|
-
const tmpPath =
|
|
122515
|
+
const tmpPath = join107(stateDir, entry);
|
|
121445
122516
|
try {
|
|
121446
|
-
const
|
|
121447
|
-
if (
|
|
122517
|
+
const stat2 = statSync62(tmpPath);
|
|
122518
|
+
if (stat2.mtimeMs < cutoff) {
|
|
121448
122519
|
unlinkSync21(tmpPath);
|
|
121449
122520
|
}
|
|
121450
122521
|
} catch {}
|
|
@@ -121453,7 +122524,7 @@ function sweepOrphanTmpFiles(stateDir) {
|
|
|
121453
122524
|
var LOCK_RETRY_MS = 25;
|
|
121454
122525
|
var LOCK_TIMEOUT_MS = 1e4;
|
|
121455
122526
|
function withLock(stateDir, fn) {
|
|
121456
|
-
const lockPath =
|
|
122527
|
+
const lockPath = join107(stateDir, ISSUES_LOCK);
|
|
121457
122528
|
const startedAt = Date.now();
|
|
121458
122529
|
let fd = null;
|
|
121459
122530
|
while (fd === null) {
|
|
@@ -121488,7 +122559,7 @@ function withLock(stateDir, fn) {
|
|
|
121488
122559
|
function tryStealStaleLock(lockPath) {
|
|
121489
122560
|
let pidStr;
|
|
121490
122561
|
try {
|
|
121491
|
-
pidStr =
|
|
122562
|
+
pidStr = readFileSync93(lockPath, "utf-8").trim();
|
|
121492
122563
|
} catch {
|
|
121493
122564
|
return true;
|
|
121494
122565
|
}
|
|
@@ -121637,11 +122708,11 @@ function registerIssuesCommand(program3) {
|
|
|
121637
122708
|
const stateDir = resolveStateDir2(opts);
|
|
121638
122709
|
let flipped;
|
|
121639
122710
|
if (opts.source && opts.code) {
|
|
121640
|
-
flipped =
|
|
122711
|
+
flipped = resolve54(stateDir, computeFingerprint(opts.source, opts.code));
|
|
121641
122712
|
} else if (opts.source && !fingerprint) {
|
|
121642
122713
|
flipped = resolveAllBySource(stateDir, opts.source);
|
|
121643
122714
|
} else if (fingerprint) {
|
|
121644
|
-
flipped =
|
|
122715
|
+
flipped = resolve54(stateDir, fingerprint);
|
|
121645
122716
|
} else {
|
|
121646
122717
|
process.stderr.write(`issues resolve: need either <fingerprint>, --source, or --source + --code
|
|
121647
122718
|
`);
|
|
@@ -121736,20 +122807,20 @@ function relTime(deltaMs) {
|
|
|
121736
122807
|
|
|
121737
122808
|
// src/cli/deps.ts
|
|
121738
122809
|
init_source();
|
|
121739
|
-
import { existsSync as
|
|
122810
|
+
import { existsSync as existsSync105 } from "node:fs";
|
|
121740
122811
|
import { homedir as homedir56 } from "node:os";
|
|
121741
|
-
import { join as
|
|
122812
|
+
import { join as join110, resolve as resolve55 } from "node:path";
|
|
121742
122813
|
|
|
121743
122814
|
// src/deps/python.ts
|
|
121744
|
-
import { createHash as
|
|
122815
|
+
import { createHash as createHash20 } from "node:crypto";
|
|
121745
122816
|
import {
|
|
121746
|
-
existsSync as
|
|
122817
|
+
existsSync as existsSync103,
|
|
121747
122818
|
mkdirSync as mkdirSync59,
|
|
121748
|
-
readFileSync as
|
|
122819
|
+
readFileSync as readFileSync94,
|
|
121749
122820
|
rmSync as rmSync19,
|
|
121750
122821
|
writeFileSync as writeFileSync41
|
|
121751
122822
|
} from "node:fs";
|
|
121752
|
-
import { dirname as dirname46, join as
|
|
122823
|
+
import { dirname as dirname46, join as join108 } from "node:path";
|
|
121753
122824
|
import { homedir as homedir54 } from "node:os";
|
|
121754
122825
|
import { execFileSync as execFileSync31 } from "node:child_process";
|
|
121755
122826
|
|
|
@@ -121762,26 +122833,26 @@ class PythonEnvError extends Error {
|
|
|
121762
122833
|
}
|
|
121763
122834
|
}
|
|
121764
122835
|
function defaultPythonCacheRoot() {
|
|
121765
|
-
return
|
|
122836
|
+
return join108(homedir54(), ".switchroom", "deps", "python");
|
|
121766
122837
|
}
|
|
121767
|
-
function hashFile(
|
|
121768
|
-
return
|
|
122838
|
+
function hashFile(path9) {
|
|
122839
|
+
return createHash20("sha256").update(readFileSync94(path9)).digest("hex");
|
|
121769
122840
|
}
|
|
121770
122841
|
function ensurePythonEnv(opts) {
|
|
121771
122842
|
const { skillName, requirementsPath, force = false } = opts;
|
|
121772
122843
|
const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
|
|
121773
122844
|
const hostPython = opts.pythonBin ?? "python3";
|
|
121774
|
-
if (!
|
|
122845
|
+
if (!existsSync103(requirementsPath)) {
|
|
121775
122846
|
throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
|
|
121776
122847
|
}
|
|
121777
|
-
const venvDir =
|
|
121778
|
-
const stampPath =
|
|
121779
|
-
const binDir =
|
|
121780
|
-
const pythonBin =
|
|
121781
|
-
const pipBin =
|
|
122848
|
+
const venvDir = join108(cacheRoot, skillName);
|
|
122849
|
+
const stampPath = join108(venvDir, ".requirements.sha256");
|
|
122850
|
+
const binDir = join108(venvDir, "bin");
|
|
122851
|
+
const pythonBin = join108(binDir, "python");
|
|
122852
|
+
const pipBin = join108(binDir, "pip");
|
|
121782
122853
|
const targetHash = hashFile(requirementsPath);
|
|
121783
|
-
if (!force &&
|
|
121784
|
-
const existingHash =
|
|
122854
|
+
if (!force && existsSync103(stampPath) && existsSync103(pythonBin)) {
|
|
122855
|
+
const existingHash = readFileSync94(stampPath, "utf8").trim();
|
|
121785
122856
|
if (existingHash === targetHash) {
|
|
121786
122857
|
return {
|
|
121787
122858
|
skillName,
|
|
@@ -121793,7 +122864,7 @@ function ensurePythonEnv(opts) {
|
|
|
121793
122864
|
};
|
|
121794
122865
|
}
|
|
121795
122866
|
}
|
|
121796
|
-
if (
|
|
122867
|
+
if (existsSync103(venvDir)) {
|
|
121797
122868
|
rmSync19(venvDir, { recursive: true, force: true });
|
|
121798
122869
|
}
|
|
121799
122870
|
mkdirSync59(dirname46(venvDir), { recursive: true });
|
|
@@ -121828,16 +122899,16 @@ function ensurePythonEnv(opts) {
|
|
|
121828
122899
|
}
|
|
121829
122900
|
|
|
121830
122901
|
// src/deps/node.ts
|
|
121831
|
-
import { createHash as
|
|
122902
|
+
import { createHash as createHash21 } from "node:crypto";
|
|
121832
122903
|
import {
|
|
121833
122904
|
copyFileSync as copyFileSync13,
|
|
121834
|
-
existsSync as
|
|
122905
|
+
existsSync as existsSync104,
|
|
121835
122906
|
mkdirSync as mkdirSync60,
|
|
121836
|
-
readFileSync as
|
|
122907
|
+
readFileSync as readFileSync95,
|
|
121837
122908
|
rmSync as rmSync20,
|
|
121838
122909
|
writeFileSync as writeFileSync42
|
|
121839
122910
|
} from "node:fs";
|
|
121840
|
-
import { dirname as dirname47, join as
|
|
122911
|
+
import { dirname as dirname47, join as join109 } from "node:path";
|
|
121841
122912
|
import { homedir as homedir55 } from "node:os";
|
|
121842
122913
|
import { execFileSync as execFileSync32 } from "node:child_process";
|
|
121843
122914
|
|
|
@@ -121845,614 +122916,188 @@ class NodeEnvError extends Error {
|
|
|
121845
122916
|
stderr;
|
|
121846
122917
|
constructor(message, stderr) {
|
|
121847
122918
|
super(message);
|
|
121848
|
-
this.name = "NodeEnvError";
|
|
121849
|
-
this.stderr = stderr;
|
|
121850
|
-
}
|
|
121851
|
-
}
|
|
121852
|
-
var ALL_LOCKFILES = [
|
|
121853
|
-
"bun.lock",
|
|
121854
|
-
"bun.lockb",
|
|
121855
|
-
"package-lock.json",
|
|
121856
|
-
"pnpm-lock.yaml",
|
|
121857
|
-
"yarn.lock"
|
|
121858
|
-
];
|
|
121859
|
-
var LOCKFILES_FOR = {
|
|
121860
|
-
bun: ["bun.lock", "bun.lockb"],
|
|
121861
|
-
npm: ["package-lock.json"]
|
|
121862
|
-
};
|
|
121863
|
-
function defaultNodeCacheRoot() {
|
|
121864
|
-
return join108(homedir55(), ".switchroom", "deps", "node");
|
|
121865
|
-
}
|
|
121866
|
-
function hashDepInputs(packageJsonPath) {
|
|
121867
|
-
const sourceDir = dirname47(packageJsonPath);
|
|
121868
|
-
const hasher = createHash20("sha256");
|
|
121869
|
-
hasher.update(`package.json
|
|
121870
|
-
`);
|
|
121871
|
-
hasher.update(readFileSync94(packageJsonPath));
|
|
121872
|
-
for (const lockName of ALL_LOCKFILES) {
|
|
121873
|
-
const lockPath = join108(sourceDir, lockName);
|
|
121874
|
-
if (existsSync103(lockPath)) {
|
|
121875
|
-
hasher.update(`
|
|
121876
|
-
`);
|
|
121877
|
-
hasher.update(lockName);
|
|
121878
|
-
hasher.update(`
|
|
121879
|
-
`);
|
|
121880
|
-
hasher.update(readFileSync94(lockPath));
|
|
121881
|
-
}
|
|
121882
|
-
}
|
|
121883
|
-
return hasher.digest("hex");
|
|
121884
|
-
}
|
|
121885
|
-
function ensureNodeEnv(opts) {
|
|
121886
|
-
const { skillName, packageJsonPath, force = false } = opts;
|
|
121887
|
-
const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
|
|
121888
|
-
const installer = opts.installer ?? "bun";
|
|
121889
|
-
if (!existsSync103(packageJsonPath)) {
|
|
121890
|
-
throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
|
|
121891
|
-
}
|
|
121892
|
-
const sourceDir = dirname47(packageJsonPath);
|
|
121893
|
-
const envDir = join108(cacheRoot, skillName);
|
|
121894
|
-
const stampPath = join108(envDir, ".package.sha256");
|
|
121895
|
-
const nodeModulesDir = join108(envDir, "node_modules");
|
|
121896
|
-
const binDir = join108(nodeModulesDir, ".bin");
|
|
121897
|
-
const targetHash = hashDepInputs(packageJsonPath);
|
|
121898
|
-
if (!force && existsSync103(stampPath) && existsSync103(nodeModulesDir)) {
|
|
121899
|
-
const existingHash = readFileSync94(stampPath, "utf8").trim();
|
|
121900
|
-
if (existingHash === targetHash) {
|
|
121901
|
-
return {
|
|
121902
|
-
skillName,
|
|
121903
|
-
dir: envDir,
|
|
121904
|
-
nodeModulesDir,
|
|
121905
|
-
binDir,
|
|
121906
|
-
rebuilt: false
|
|
121907
|
-
};
|
|
121908
|
-
}
|
|
121909
|
-
}
|
|
121910
|
-
if (existsSync103(envDir)) {
|
|
121911
|
-
rmSync20(envDir, { recursive: true, force: true });
|
|
121912
|
-
}
|
|
121913
|
-
mkdirSync60(envDir, { recursive: true });
|
|
121914
|
-
copyFileSync13(packageJsonPath, join108(envDir, "package.json"));
|
|
121915
|
-
let copiedLockfile = false;
|
|
121916
|
-
for (const lockName of LOCKFILES_FOR[installer]) {
|
|
121917
|
-
const lockPath = join108(sourceDir, lockName);
|
|
121918
|
-
if (existsSync103(lockPath)) {
|
|
121919
|
-
copyFileSync13(lockPath, join108(envDir, lockName));
|
|
121920
|
-
copiedLockfile = true;
|
|
121921
|
-
}
|
|
121922
|
-
}
|
|
121923
|
-
try {
|
|
121924
|
-
if (installer === "bun") {
|
|
121925
|
-
const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
|
|
121926
|
-
execFileSync32("bun", args, { cwd: envDir, stdio: "pipe" });
|
|
121927
|
-
} else {
|
|
121928
|
-
const args = copiedLockfile ? ["ci"] : ["install"];
|
|
121929
|
-
execFileSync32("npm", args, { cwd: envDir, stdio: "pipe" });
|
|
121930
|
-
}
|
|
121931
|
-
} catch (err) {
|
|
121932
|
-
const e = err;
|
|
121933
|
-
throw new NodeEnvError(`Failed to install node deps for skill "${skillName}" with ${installer}: ${e.message}`, e.stderr?.toString());
|
|
121934
|
-
}
|
|
121935
|
-
writeFileSync42(stampPath, targetHash + `
|
|
121936
|
-
`);
|
|
121937
|
-
return {
|
|
121938
|
-
skillName,
|
|
121939
|
-
dir: envDir,
|
|
121940
|
-
nodeModulesDir,
|
|
121941
|
-
binDir,
|
|
121942
|
-
rebuilt: true
|
|
121943
|
-
};
|
|
121944
|
-
}
|
|
121945
|
-
|
|
121946
|
-
// src/cli/deps.ts
|
|
121947
|
-
function builtinSkillsRoot() {
|
|
121948
|
-
return resolve54(homedir56(), ".switchroom/skills/_bundled");
|
|
121949
|
-
}
|
|
121950
|
-
function registerDepsCommand(program3) {
|
|
121951
|
-
const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
|
|
121952
|
-
deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
|
|
121953
|
-
const skillsRoot = builtinSkillsRoot();
|
|
121954
|
-
if (!existsSync104(skillsRoot)) {
|
|
121955
|
-
console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
|
|
121956
|
-
process.exit(1);
|
|
121957
|
-
}
|
|
121958
|
-
const skillDir = join109(skillsRoot, skill);
|
|
121959
|
-
if (!existsSync104(skillDir)) {
|
|
121960
|
-
console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
|
|
121961
|
-
process.exit(1);
|
|
121962
|
-
}
|
|
121963
|
-
const requirementsPath = join109(skillDir, "requirements.txt");
|
|
121964
|
-
const packageJsonPath = join109(skillDir, "package.json");
|
|
121965
|
-
const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync104(requirementsPath));
|
|
121966
|
-
const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync104(packageJsonPath));
|
|
121967
|
-
let did = 0;
|
|
121968
|
-
if (wantPython) {
|
|
121969
|
-
if (!existsSync104(requirementsPath)) {
|
|
121970
|
-
console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
|
|
121971
|
-
process.exit(1);
|
|
121972
|
-
}
|
|
121973
|
-
try {
|
|
121974
|
-
console.log(source_default.gray(`Rebuilding Python env for ${skill}...`));
|
|
121975
|
-
const env2 = ensurePythonEnv({
|
|
121976
|
-
skillName: skill,
|
|
121977
|
-
requirementsPath,
|
|
121978
|
-
force: true
|
|
121979
|
-
});
|
|
121980
|
-
console.log(source_default.green(` \u2713 Python env at ${env2.venvDir} (python: ${env2.pythonBin})`));
|
|
121981
|
-
did++;
|
|
121982
|
-
} catch (err) {
|
|
121983
|
-
if (err instanceof PythonEnvError) {
|
|
121984
|
-
console.error(source_default.red(` Python env failed: ${err.message}`));
|
|
121985
|
-
if (err.stderr)
|
|
121986
|
-
console.error(source_default.dim(err.stderr));
|
|
121987
|
-
process.exit(1);
|
|
121988
|
-
}
|
|
121989
|
-
throw err;
|
|
121990
|
-
}
|
|
121991
|
-
}
|
|
121992
|
-
if (wantNode) {
|
|
121993
|
-
if (!existsSync104(packageJsonPath)) {
|
|
121994
|
-
console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
|
|
121995
|
-
process.exit(1);
|
|
121996
|
-
}
|
|
121997
|
-
try {
|
|
121998
|
-
console.log(source_default.gray(`Rebuilding Node env for ${skill}...`));
|
|
121999
|
-
const env2 = ensureNodeEnv({
|
|
122000
|
-
skillName: skill,
|
|
122001
|
-
packageJsonPath,
|
|
122002
|
-
force: true
|
|
122003
|
-
});
|
|
122004
|
-
console.log(source_default.green(` \u2713 Node env at ${env2.dir} (node_modules: ${env2.nodeModulesDir})`));
|
|
122005
|
-
did++;
|
|
122006
|
-
} catch (err) {
|
|
122007
|
-
if (err instanceof NodeEnvError) {
|
|
122008
|
-
console.error(source_default.red(` Node env failed: ${err.message}`));
|
|
122009
|
-
if (err.stderr)
|
|
122010
|
-
console.error(source_default.dim(err.stderr));
|
|
122011
|
-
process.exit(1);
|
|
122012
|
-
}
|
|
122013
|
-
throw err;
|
|
122014
|
-
}
|
|
122015
|
-
}
|
|
122016
|
-
if (did === 0) {
|
|
122017
|
-
console.error(source_default.yellow(`Skill "${skill}" has neither requirements.txt nor package.json \u2014 nothing to rebuild.`));
|
|
122018
|
-
process.exit(1);
|
|
122019
|
-
}
|
|
122020
|
-
});
|
|
122021
|
-
}
|
|
122022
|
-
|
|
122023
|
-
// src/cli/workspace.ts
|
|
122024
|
-
init_helpers();
|
|
122025
|
-
init_loader();
|
|
122026
|
-
import { existsSync as existsSync105 } from "node:fs";
|
|
122027
|
-
import { resolve as resolve55, sep as sep7 } from "node:path";
|
|
122028
|
-
import { spawnSync as spawnSync23 } from "node:child_process";
|
|
122029
|
-
|
|
122030
|
-
// src/agents/workspace.ts
|
|
122031
|
-
import { readFile as readFile2, stat } from "node:fs/promises";
|
|
122032
|
-
import path8 from "node:path";
|
|
122033
|
-
|
|
122034
|
-
// src/agents/bootstrap-budget.ts
|
|
122035
|
-
import path7 from "node:path";
|
|
122036
|
-
var DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
|
|
122037
|
-
var DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
|
|
122038
|
-
var DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
|
|
122039
|
-
|
|
122040
|
-
class BootstrapBudgetExceededError extends Error {
|
|
122041
|
-
analysis;
|
|
122042
|
-
constructor(analysis, message) {
|
|
122043
|
-
super(message);
|
|
122044
|
-
this.analysis = analysis;
|
|
122045
|
-
this.name = "BootstrapBudgetExceededError";
|
|
122046
|
-
}
|
|
122047
|
-
}
|
|
122048
|
-
function normalizePositiveLimit(value) {
|
|
122049
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
122050
|
-
return 1;
|
|
122051
|
-
}
|
|
122052
|
-
return Math.floor(value);
|
|
122053
|
-
}
|
|
122054
|
-
function formatWarningCause(cause) {
|
|
122055
|
-
return cause === "per-file-limit" ? "max/file" : "max/total";
|
|
122056
|
-
}
|
|
122057
|
-
function normalizeSeenSignatures(signatures) {
|
|
122058
|
-
if (!Array.isArray(signatures) || signatures.length === 0) {
|
|
122059
|
-
return [];
|
|
122060
|
-
}
|
|
122061
|
-
const seen = new Set;
|
|
122062
|
-
const result = [];
|
|
122063
|
-
for (const signature of signatures) {
|
|
122064
|
-
const value = typeof signature === "string" ? signature.trim() : "";
|
|
122065
|
-
if (!value || seen.has(value)) {
|
|
122066
|
-
continue;
|
|
122067
|
-
}
|
|
122068
|
-
seen.add(value);
|
|
122069
|
-
result.push(value);
|
|
122070
|
-
}
|
|
122071
|
-
return result;
|
|
122072
|
-
}
|
|
122073
|
-
function appendSeenSignature(signatures, signature) {
|
|
122074
|
-
if (!signature.trim()) {
|
|
122075
|
-
return signatures;
|
|
122076
|
-
}
|
|
122077
|
-
if (signatures.includes(signature)) {
|
|
122078
|
-
return signatures;
|
|
122079
|
-
}
|
|
122080
|
-
const next = [...signatures, signature];
|
|
122081
|
-
if (next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX) {
|
|
122082
|
-
return next;
|
|
122083
|
-
}
|
|
122084
|
-
return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
|
|
122085
|
-
}
|
|
122086
|
-
function buildBootstrapInjectionStats(params) {
|
|
122087
|
-
const injectedByPath = new Map;
|
|
122088
|
-
const injectedByBaseName = new Map;
|
|
122089
|
-
for (const file of params.injectedFiles) {
|
|
122090
|
-
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
|
|
122091
|
-
if (!pathValue) {
|
|
122092
|
-
continue;
|
|
122093
|
-
}
|
|
122094
|
-
if (!injectedByPath.has(pathValue)) {
|
|
122095
|
-
injectedByPath.set(pathValue, file.content);
|
|
122096
|
-
}
|
|
122097
|
-
const normalizedPath = pathValue.replace(/\\/g, "/");
|
|
122098
|
-
const baseName = path7.posix.basename(normalizedPath);
|
|
122099
|
-
if (!injectedByBaseName.has(baseName)) {
|
|
122100
|
-
injectedByBaseName.set(baseName, file.content);
|
|
122101
|
-
}
|
|
122102
|
-
}
|
|
122103
|
-
return params.bootstrapFiles.map((file) => {
|
|
122104
|
-
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
|
|
122105
|
-
const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
|
|
122106
|
-
const injected = (pathValue ? injectedByPath.get(pathValue) : undefined) ?? injectedByPath.get(file.name) ?? injectedByBaseName.get(file.name);
|
|
122107
|
-
const injectedChars = injected ? injected.length : 0;
|
|
122108
|
-
const truncated = !file.missing && injectedChars < rawChars;
|
|
122109
|
-
return {
|
|
122110
|
-
name: file.name,
|
|
122111
|
-
path: pathValue || file.name,
|
|
122112
|
-
missing: file.missing,
|
|
122113
|
-
rawChars,
|
|
122114
|
-
injectedChars,
|
|
122115
|
-
truncated
|
|
122116
|
-
};
|
|
122117
|
-
});
|
|
122118
|
-
}
|
|
122119
|
-
function analyzeBootstrapBudget(params) {
|
|
122120
|
-
const bootstrapMaxChars = normalizePositiveLimit(params.bootstrapMaxChars);
|
|
122121
|
-
const bootstrapTotalMaxChars = normalizePositiveLimit(params.bootstrapTotalMaxChars);
|
|
122122
|
-
const nearLimitRatio = typeof params.nearLimitRatio === "number" && Number.isFinite(params.nearLimitRatio) && params.nearLimitRatio > 0 && params.nearLimitRatio < 1 ? params.nearLimitRatio : DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO;
|
|
122123
|
-
const nonMissing = params.files.filter((file) => !file.missing);
|
|
122124
|
-
const rawChars = nonMissing.reduce((sum, file) => sum + file.rawChars, 0);
|
|
122125
|
-
const injectedChars = nonMissing.reduce((sum, file) => sum + file.injectedChars, 0);
|
|
122126
|
-
const totalNearLimit = injectedChars >= Math.ceil(bootstrapTotalMaxChars * nearLimitRatio);
|
|
122127
|
-
const totalOverLimit = injectedChars >= bootstrapTotalMaxChars;
|
|
122128
|
-
const files = params.files.map((file) => {
|
|
122129
|
-
if (file.missing) {
|
|
122130
|
-
return { ...file, nearLimit: false, causes: [] };
|
|
122131
|
-
}
|
|
122132
|
-
const perFileOverLimit = file.rawChars > bootstrapMaxChars;
|
|
122133
|
-
const nearLimit = file.rawChars >= Math.ceil(bootstrapMaxChars * nearLimitRatio);
|
|
122134
|
-
const causes = [];
|
|
122135
|
-
if (file.truncated) {
|
|
122136
|
-
if (perFileOverLimit) {
|
|
122137
|
-
causes.push("per-file-limit");
|
|
122138
|
-
}
|
|
122139
|
-
if (totalOverLimit) {
|
|
122140
|
-
causes.push("total-limit");
|
|
122141
|
-
}
|
|
122142
|
-
}
|
|
122143
|
-
return { ...file, nearLimit, causes };
|
|
122144
|
-
});
|
|
122145
|
-
const truncatedFiles = files.filter((file) => file.truncated);
|
|
122146
|
-
const nearLimitFiles = files.filter((file) => file.nearLimit);
|
|
122147
|
-
return {
|
|
122148
|
-
files,
|
|
122149
|
-
truncatedFiles,
|
|
122150
|
-
nearLimitFiles,
|
|
122151
|
-
totalNearLimit,
|
|
122152
|
-
hasTruncation: truncatedFiles.length > 0,
|
|
122153
|
-
totals: {
|
|
122154
|
-
rawChars,
|
|
122155
|
-
injectedChars,
|
|
122156
|
-
truncatedChars: Math.max(0, rawChars - injectedChars),
|
|
122157
|
-
bootstrapMaxChars,
|
|
122158
|
-
bootstrapTotalMaxChars,
|
|
122159
|
-
nearLimitRatio
|
|
122160
|
-
}
|
|
122161
|
-
};
|
|
122162
|
-
}
|
|
122163
|
-
function buildBootstrapTruncationSignature(analysis) {
|
|
122164
|
-
if (!analysis.hasTruncation) {
|
|
122165
|
-
return;
|
|
122166
|
-
}
|
|
122167
|
-
const files = analysis.truncatedFiles.map((file) => ({
|
|
122168
|
-
path: file.path || file.name,
|
|
122169
|
-
rawChars: file.rawChars,
|
|
122170
|
-
injectedChars: file.injectedChars,
|
|
122171
|
-
causes: [...file.causes].sort()
|
|
122172
|
-
})).sort((a, b) => {
|
|
122173
|
-
const pathCmp = a.path.localeCompare(b.path);
|
|
122174
|
-
if (pathCmp !== 0) {
|
|
122175
|
-
return pathCmp;
|
|
122176
|
-
}
|
|
122177
|
-
if (a.rawChars !== b.rawChars) {
|
|
122178
|
-
return a.rawChars - b.rawChars;
|
|
122179
|
-
}
|
|
122180
|
-
if (a.injectedChars !== b.injectedChars) {
|
|
122181
|
-
return a.injectedChars - b.injectedChars;
|
|
122182
|
-
}
|
|
122183
|
-
return a.causes.join("+").localeCompare(b.causes.join("+"));
|
|
122184
|
-
});
|
|
122185
|
-
return JSON.stringify({
|
|
122186
|
-
bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
|
|
122187
|
-
bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
|
|
122188
|
-
files
|
|
122189
|
-
});
|
|
122190
|
-
}
|
|
122191
|
-
function formatBootstrapTruncationWarningLines(params) {
|
|
122192
|
-
if (!params.analysis.hasTruncation) {
|
|
122193
|
-
return [];
|
|
122194
|
-
}
|
|
122195
|
-
const maxFiles = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
|
|
122196
|
-
const lines = [];
|
|
122197
|
-
const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
|
|
122198
|
-
acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
|
|
122199
|
-
return acc;
|
|
122200
|
-
}, new Map);
|
|
122201
|
-
const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
|
|
122202
|
-
for (const file of topFiles) {
|
|
122203
|
-
const pct = file.rawChars > 0 ? Math.round((file.rawChars - file.injectedChars) / file.rawChars * 100) : 0;
|
|
122204
|
-
const causeText = file.causes.length > 0 ? file.causes.map((cause) => formatWarningCause(cause)).join(", ") : "";
|
|
122205
|
-
const nameLabel = (duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0 ? `${file.name} (${file.path})` : file.name;
|
|
122206
|
-
lines.push(`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`);
|
|
122207
|
-
}
|
|
122208
|
-
if (params.analysis.truncatedFiles.length > topFiles.length) {
|
|
122209
|
-
lines.push(`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`);
|
|
122210
|
-
}
|
|
122211
|
-
lines.push("If unintentional, raise switchroom workspace bootstrapMaxChars and/or bootstrapTotalMaxChars config.");
|
|
122212
|
-
return lines;
|
|
122213
|
-
}
|
|
122214
|
-
function buildBootstrapPromptWarning(params) {
|
|
122215
|
-
const signature = buildBootstrapTruncationSignature(params.analysis);
|
|
122216
|
-
let seenSignatures = normalizeSeenSignatures(params.seenSignatures);
|
|
122217
|
-
if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
|
|
122218
|
-
seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
|
|
122219
|
-
}
|
|
122220
|
-
const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
|
|
122221
|
-
const warningShown = params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
|
|
122222
|
-
const warningSignaturesSeen = signature && params.mode !== "off" ? appendSeenSignature(seenSignatures, signature) : seenSignatures;
|
|
122223
|
-
return {
|
|
122224
|
-
signature,
|
|
122225
|
-
warningShown,
|
|
122226
|
-
lines: warningShown ? formatBootstrapTruncationWarningLines({
|
|
122227
|
-
analysis: params.analysis,
|
|
122228
|
-
maxFiles: params.maxFiles
|
|
122229
|
-
}) : [],
|
|
122230
|
-
warningSignaturesSeen
|
|
122231
|
-
};
|
|
122232
|
-
}
|
|
122233
|
-
|
|
122234
|
-
// src/agents/bootstrap-types.ts
|
|
122235
|
-
var DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
|
122236
|
-
var DEFAULT_SOUL_DEFAULT_FILENAME = "SOUL.default.md";
|
|
122237
|
-
var DEFAULT_SOUL_FILENAME = "SOUL.md";
|
|
122238
|
-
var DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
|
122239
|
-
var DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
122240
|
-
var DEFAULT_USER_FILENAME = "USER.md";
|
|
122241
|
-
var DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
122242
|
-
var DEFAULT_MEMORY_FILENAME = "MEMORY.md";
|
|
122243
|
-
|
|
122244
|
-
// src/agents/workspace.ts
|
|
122245
|
-
var DEFAULT_WORKSPACE_DIR_NAME = "workspace";
|
|
122246
|
-
var DEFAULT_MEMORY_SUBDIR = "memory";
|
|
122247
|
-
var STABLE_BOOTSTRAP_FILENAMES = [
|
|
122248
|
-
DEFAULT_AGENTS_FILENAME,
|
|
122249
|
-
DEFAULT_SOUL_DEFAULT_FILENAME,
|
|
122250
|
-
DEFAULT_SOUL_FILENAME,
|
|
122251
|
-
DEFAULT_IDENTITY_FILENAME,
|
|
122252
|
-
DEFAULT_USER_FILENAME,
|
|
122253
|
-
DEFAULT_TOOLS_FILENAME,
|
|
122254
|
-
DEFAULT_BOOTSTRAP_FILENAME
|
|
122255
|
-
];
|
|
122256
|
-
var DYNAMIC_BOOTSTRAP_FILENAMES = [
|
|
122257
|
-
DEFAULT_MEMORY_FILENAME
|
|
122258
|
-
];
|
|
122259
|
-
var DEFAULT_BOOTSTRAP_MAX_CHARS = 12000;
|
|
122260
|
-
var DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 64000;
|
|
122261
|
-
var DEFAULT_DYNAMIC_TOTAL_MAX_CHARS = 24000;
|
|
122262
|
-
function resolveAgentWorkspaceDir(agentDir) {
|
|
122263
|
-
return path8.join(agentDir, DEFAULT_WORKSPACE_DIR_NAME);
|
|
122264
|
-
}
|
|
122265
|
-
async function readOptionalFile(filePath) {
|
|
122266
|
-
try {
|
|
122267
|
-
const content = await readFile2(filePath, "utf8");
|
|
122268
|
-
return content;
|
|
122269
|
-
} catch (err) {
|
|
122270
|
-
if (isErrnoException(err) && (err.code === "ENOENT" || err.code === "EISDIR")) {
|
|
122271
|
-
return;
|
|
122272
|
-
}
|
|
122273
|
-
throw err;
|
|
122274
|
-
}
|
|
122275
|
-
}
|
|
122276
|
-
function isErrnoException(value) {
|
|
122277
|
-
return value instanceof Error && typeof value.code === "string";
|
|
122278
|
-
}
|
|
122279
|
-
async function loadNamedFile(workspaceDir, name, relativePath) {
|
|
122280
|
-
const filePath = path8.join(workspaceDir, relativePath ?? name);
|
|
122281
|
-
const content = await readOptionalFile(filePath);
|
|
122282
|
-
if (content === undefined) {
|
|
122283
|
-
return { name, path: filePath, missing: true };
|
|
122284
|
-
}
|
|
122285
|
-
return { name, path: filePath, content, missing: false };
|
|
122286
|
-
}
|
|
122287
|
-
async function loadStableBootstrapFiles(workspaceDir, options) {
|
|
122288
|
-
const defaultFiles = await Promise.all(STABLE_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
|
|
122289
|
-
const extras = options?.extraStableFiles ?? [];
|
|
122290
|
-
if (extras.length === 0) {
|
|
122291
|
-
return defaultFiles;
|
|
122292
|
-
}
|
|
122293
|
-
const extraFiles = await Promise.all(extras.map((name) => loadNamedFile(workspaceDir, name)));
|
|
122294
|
-
return [...defaultFiles, ...extraFiles];
|
|
122295
|
-
}
|
|
122296
|
-
async function loadDynamicBootstrapFiles(workspaceDir, options) {
|
|
122297
|
-
const now2 = options?.now ?? new Date;
|
|
122298
|
-
const includeYesterday = options?.includeYesterday ?? true;
|
|
122299
|
-
const files = await Promise.all(DYNAMIC_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
|
|
122300
|
-
const todayRelative = dailyMemoryRelativePath(now2);
|
|
122301
|
-
const todayName = DEFAULT_MEMORY_FILENAME;
|
|
122302
|
-
const today = await loadNamedFile(workspaceDir, todayName, todayRelative);
|
|
122303
|
-
today.path = path8.join(workspaceDir, todayRelative);
|
|
122304
|
-
files.push(today);
|
|
122305
|
-
if (includeYesterday) {
|
|
122306
|
-
const yesterdayRelative = dailyMemoryRelativePath(addDays(now2, -1));
|
|
122307
|
-
const yesterday = await loadNamedFile(workspaceDir, todayName, yesterdayRelative);
|
|
122308
|
-
yesterday.path = path8.join(workspaceDir, yesterdayRelative);
|
|
122309
|
-
files.push(yesterday);
|
|
122919
|
+
this.name = "NodeEnvError";
|
|
122920
|
+
this.stderr = stderr;
|
|
122310
122921
|
}
|
|
122311
|
-
return files;
|
|
122312
|
-
}
|
|
122313
|
-
function pad22(n) {
|
|
122314
|
-
return n < 10 ? `0${n}` : String(n);
|
|
122315
|
-
}
|
|
122316
|
-
function dailyMemoryRelativePath(date) {
|
|
122317
|
-
const y = date.getFullYear();
|
|
122318
|
-
const m = pad22(date.getMonth() + 1);
|
|
122319
|
-
const d = pad22(date.getDate());
|
|
122320
|
-
return path8.join(DEFAULT_MEMORY_SUBDIR, `${y}-${m}-${d}.md`);
|
|
122321
122922
|
}
|
|
122322
|
-
|
|
122323
|
-
|
|
122324
|
-
|
|
122325
|
-
|
|
122923
|
+
var ALL_LOCKFILES = [
|
|
122924
|
+
"bun.lock",
|
|
122925
|
+
"bun.lockb",
|
|
122926
|
+
"package-lock.json",
|
|
122927
|
+
"pnpm-lock.yaml",
|
|
122928
|
+
"yarn.lock"
|
|
122929
|
+
];
|
|
122930
|
+
var LOCKFILES_FOR = {
|
|
122931
|
+
bun: ["bun.lock", "bun.lockb"],
|
|
122932
|
+
npm: ["package-lock.json"]
|
|
122933
|
+
};
|
|
122934
|
+
function defaultNodeCacheRoot() {
|
|
122935
|
+
return join109(homedir55(), ".switchroom", "deps", "node");
|
|
122326
122936
|
}
|
|
122327
|
-
function
|
|
122328
|
-
|
|
122329
|
-
|
|
122330
|
-
|
|
122331
|
-
|
|
122332
|
-
|
|
122937
|
+
function hashDepInputs(packageJsonPath) {
|
|
122938
|
+
const sourceDir = dirname47(packageJsonPath);
|
|
122939
|
+
const hasher = createHash21("sha256");
|
|
122940
|
+
hasher.update(`package.json
|
|
122941
|
+
`);
|
|
122942
|
+
hasher.update(readFileSync95(packageJsonPath));
|
|
122943
|
+
for (const lockName of ALL_LOCKFILES) {
|
|
122944
|
+
const lockPath = join109(sourceDir, lockName);
|
|
122945
|
+
if (existsSync104(lockPath)) {
|
|
122946
|
+
hasher.update(`
|
|
122947
|
+
`);
|
|
122948
|
+
hasher.update(lockName);
|
|
122949
|
+
hasher.update(`
|
|
122950
|
+
`);
|
|
122951
|
+
hasher.update(readFileSync95(lockPath));
|
|
122952
|
+
}
|
|
122333
122953
|
}
|
|
122334
|
-
|
|
122335
|
-
const sampleMarker = `
|
|
122336
|
-
\u2026(truncated ${name}: kept ${headRoomInitial}+${Math.max(0, maxChars - headRoomInitial - 96)} chars of ${content.length})\u2026
|
|
122337
|
-
`;
|
|
122338
|
-
const markerReserve = Math.max(64, sampleMarker.length + 8);
|
|
122339
|
-
const headRoom = Math.min(headRoomInitial, Math.max(0, maxChars - markerReserve - 8));
|
|
122340
|
-
const tailRoom = Math.max(0, maxChars - headRoom - markerReserve);
|
|
122341
|
-
const head = content.slice(0, Math.max(0, headRoom));
|
|
122342
|
-
const tail = tailRoom > 0 ? content.slice(content.length - tailRoom) : "";
|
|
122343
|
-
const result = `${head}
|
|
122344
|
-
\u2026(truncated ${name}: kept ${headRoom}+${tailRoom} chars of ${content.length})\u2026
|
|
122345
|
-
${tail}`;
|
|
122346
|
-
return result.length <= maxChars ? result : result.slice(0, maxChars);
|
|
122954
|
+
return hasher.digest("hex");
|
|
122347
122955
|
}
|
|
122348
|
-
function
|
|
122349
|
-
const {
|
|
122350
|
-
|
|
122351
|
-
|
|
122352
|
-
|
|
122353
|
-
|
|
122354
|
-
|
|
122355
|
-
|
|
122356
|
-
|
|
122357
|
-
const
|
|
122358
|
-
|
|
122359
|
-
const
|
|
122360
|
-
const
|
|
122361
|
-
|
|
122362
|
-
|
|
122363
|
-
|
|
122364
|
-
|
|
122365
|
-
|
|
122366
|
-
|
|
122367
|
-
|
|
122368
|
-
|
|
122369
|
-
|
|
122370
|
-
|
|
122371
|
-
break;
|
|
122956
|
+
function ensureNodeEnv(opts) {
|
|
122957
|
+
const { skillName, packageJsonPath, force = false } = opts;
|
|
122958
|
+
const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
|
|
122959
|
+
const installer = opts.installer ?? "bun";
|
|
122960
|
+
if (!existsSync104(packageJsonPath)) {
|
|
122961
|
+
throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
|
|
122962
|
+
}
|
|
122963
|
+
const sourceDir = dirname47(packageJsonPath);
|
|
122964
|
+
const envDir = join109(cacheRoot, skillName);
|
|
122965
|
+
const stampPath = join109(envDir, ".package.sha256");
|
|
122966
|
+
const nodeModulesDir = join109(envDir, "node_modules");
|
|
122967
|
+
const binDir = join109(nodeModulesDir, ".bin");
|
|
122968
|
+
const targetHash = hashDepInputs(packageJsonPath);
|
|
122969
|
+
if (!force && existsSync104(stampPath) && existsSync104(nodeModulesDir)) {
|
|
122970
|
+
const existingHash = readFileSync95(stampPath, "utf8").trim();
|
|
122971
|
+
if (existingHash === targetHash) {
|
|
122972
|
+
return {
|
|
122973
|
+
skillName,
|
|
122974
|
+
dir: envDir,
|
|
122975
|
+
nodeModulesDir,
|
|
122976
|
+
binDir,
|
|
122977
|
+
rebuilt: false
|
|
122978
|
+
};
|
|
122372
122979
|
}
|
|
122373
|
-
|
|
122374
|
-
|
|
122375
|
-
|
|
122376
|
-
|
|
122377
|
-
|
|
122378
|
-
|
|
122379
|
-
|
|
122380
|
-
|
|
122980
|
+
}
|
|
122981
|
+
if (existsSync104(envDir)) {
|
|
122982
|
+
rmSync20(envDir, { recursive: true, force: true });
|
|
122983
|
+
}
|
|
122984
|
+
mkdirSync60(envDir, { recursive: true });
|
|
122985
|
+
copyFileSync13(packageJsonPath, join109(envDir, "package.json"));
|
|
122986
|
+
let copiedLockfile = false;
|
|
122987
|
+
for (const lockName of LOCKFILES_FOR[installer]) {
|
|
122988
|
+
const lockPath = join109(sourceDir, lockName);
|
|
122989
|
+
if (existsSync104(lockPath)) {
|
|
122990
|
+
copyFileSync13(lockPath, join109(envDir, lockName));
|
|
122991
|
+
copiedLockfile = true;
|
|
122381
122992
|
}
|
|
122382
122993
|
}
|
|
122383
|
-
|
|
122384
|
-
|
|
122385
|
-
|
|
122386
|
-
|
|
122387
|
-
|
|
122388
|
-
|
|
122389
|
-
|
|
122390
|
-
const analysis = analyzeBootstrapBudget({
|
|
122391
|
-
files: stats,
|
|
122392
|
-
bootstrapMaxChars: budget.bootstrapMaxChars,
|
|
122393
|
-
bootstrapTotalMaxChars: budget.bootstrapTotalMaxChars,
|
|
122394
|
-
nearLimitRatio: budget.nearLimitRatio ?? DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO
|
|
122395
|
-
});
|
|
122396
|
-
const warning = buildBootstrapPromptWarning({
|
|
122397
|
-
analysis,
|
|
122398
|
-
mode: warningMode,
|
|
122399
|
-
seenSignatures,
|
|
122400
|
-
maxFiles: warningMaxFiles
|
|
122401
|
-
});
|
|
122402
|
-
if (warningMode === "error" && analysis.hasTruncation) {
|
|
122403
|
-
const errorLines = [
|
|
122404
|
-
"Bootstrap budget exceeded. The following files exceed limits:",
|
|
122405
|
-
""
|
|
122406
|
-
];
|
|
122407
|
-
for (const file of analysis.truncatedFiles) {
|
|
122408
|
-
const excessChars = file.rawChars - file.injectedChars;
|
|
122409
|
-
const causes = file.causes.map((c) => c === "per-file-limit" ? `per-file limit (${analysis.totals.bootstrapMaxChars.toLocaleString()} chars)` : `total limit (${analysis.totals.bootstrapTotalMaxChars.toLocaleString()} chars)`);
|
|
122410
|
-
errorLines.push(` ${file.name}: ${file.rawChars.toLocaleString()} bytes (exceeds ${causes.join(", ")})`);
|
|
122411
|
-
errorLines.push(` Trim ${excessChars.toLocaleString()} bytes, or pass --warning-mode warn to proceed with truncation.`);
|
|
122994
|
+
try {
|
|
122995
|
+
if (installer === "bun") {
|
|
122996
|
+
const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
|
|
122997
|
+
execFileSync32("bun", args, { cwd: envDir, stdio: "pipe" });
|
|
122998
|
+
} else {
|
|
122999
|
+
const args = copiedLockfile ? ["ci"] : ["install"];
|
|
123000
|
+
execFileSync32("npm", args, { cwd: envDir, stdio: "pipe" });
|
|
122412
123001
|
}
|
|
122413
|
-
|
|
122414
|
-
|
|
123002
|
+
} catch (err) {
|
|
123003
|
+
const e = err;
|
|
123004
|
+
throw new NodeEnvError(`Failed to install node deps for skill "${skillName}" with ${installer}: ${e.message}`, e.stderr?.toString());
|
|
122415
123005
|
}
|
|
122416
|
-
|
|
122417
|
-
|
|
122418
|
-
|
|
122419
|
-
|
|
122420
|
-
|
|
122421
|
-
|
|
122422
|
-
|
|
122423
|
-
|
|
122424
|
-
bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
|
|
122425
|
-
nearLimitRatio: params.budget?.nearLimitRatio
|
|
123006
|
+
writeFileSync42(stampPath, targetHash + `
|
|
123007
|
+
`);
|
|
123008
|
+
return {
|
|
123009
|
+
skillName,
|
|
123010
|
+
dir: envDir,
|
|
123011
|
+
nodeModulesDir,
|
|
123012
|
+
binDir,
|
|
123013
|
+
rebuilt: true
|
|
122426
123014
|
};
|
|
122427
|
-
return projectBootstrapFiles({
|
|
122428
|
-
files,
|
|
122429
|
-
heading: "Project Context (stable workspace files)",
|
|
122430
|
-
budget,
|
|
122431
|
-
seenSignatures: params.seenSignatures,
|
|
122432
|
-
warningMode: params.budget?.warningMode,
|
|
122433
|
-
warningMaxFiles: params.budget?.warningMaxFiles
|
|
122434
|
-
});
|
|
122435
123015
|
}
|
|
122436
|
-
|
|
122437
|
-
|
|
122438
|
-
|
|
122439
|
-
|
|
122440
|
-
|
|
122441
|
-
|
|
122442
|
-
|
|
122443
|
-
|
|
122444
|
-
|
|
122445
|
-
|
|
122446
|
-
|
|
122447
|
-
|
|
122448
|
-
|
|
122449
|
-
|
|
122450
|
-
|
|
122451
|
-
|
|
122452
|
-
|
|
123016
|
+
|
|
123017
|
+
// src/cli/deps.ts
|
|
123018
|
+
function builtinSkillsRoot() {
|
|
123019
|
+
return resolve55(homedir56(), ".switchroom/skills/_bundled");
|
|
123020
|
+
}
|
|
123021
|
+
function registerDepsCommand(program3) {
|
|
123022
|
+
const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
|
|
123023
|
+
deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
|
|
123024
|
+
const skillsRoot = builtinSkillsRoot();
|
|
123025
|
+
if (!existsSync105(skillsRoot)) {
|
|
123026
|
+
console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
|
|
123027
|
+
process.exit(1);
|
|
123028
|
+
}
|
|
123029
|
+
const skillDir = join110(skillsRoot, skill);
|
|
123030
|
+
if (!existsSync105(skillDir)) {
|
|
123031
|
+
console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
|
|
123032
|
+
process.exit(1);
|
|
123033
|
+
}
|
|
123034
|
+
const requirementsPath = join110(skillDir, "requirements.txt");
|
|
123035
|
+
const packageJsonPath = join110(skillDir, "package.json");
|
|
123036
|
+
const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync105(requirementsPath));
|
|
123037
|
+
const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync105(packageJsonPath));
|
|
123038
|
+
let did = 0;
|
|
123039
|
+
if (wantPython) {
|
|
123040
|
+
if (!existsSync105(requirementsPath)) {
|
|
123041
|
+
console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
|
|
123042
|
+
process.exit(1);
|
|
123043
|
+
}
|
|
123044
|
+
try {
|
|
123045
|
+
console.log(source_default.gray(`Rebuilding Python env for ${skill}...`));
|
|
123046
|
+
const env2 = ensurePythonEnv({
|
|
123047
|
+
skillName: skill,
|
|
123048
|
+
requirementsPath,
|
|
123049
|
+
force: true
|
|
123050
|
+
});
|
|
123051
|
+
console.log(source_default.green(` \u2713 Python env at ${env2.venvDir} (python: ${env2.pythonBin})`));
|
|
123052
|
+
did++;
|
|
123053
|
+
} catch (err) {
|
|
123054
|
+
if (err instanceof PythonEnvError) {
|
|
123055
|
+
console.error(source_default.red(` Python env failed: ${err.message}`));
|
|
123056
|
+
if (err.stderr)
|
|
123057
|
+
console.error(source_default.dim(err.stderr));
|
|
123058
|
+
process.exit(1);
|
|
123059
|
+
}
|
|
123060
|
+
throw err;
|
|
123061
|
+
}
|
|
123062
|
+
}
|
|
123063
|
+
if (wantNode) {
|
|
123064
|
+
if (!existsSync105(packageJsonPath)) {
|
|
123065
|
+
console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
|
|
123066
|
+
process.exit(1);
|
|
123067
|
+
}
|
|
123068
|
+
try {
|
|
123069
|
+
console.log(source_default.gray(`Rebuilding Node env for ${skill}...`));
|
|
123070
|
+
const env2 = ensureNodeEnv({
|
|
123071
|
+
skillName: skill,
|
|
123072
|
+
packageJsonPath,
|
|
123073
|
+
force: true
|
|
123074
|
+
});
|
|
123075
|
+
console.log(source_default.green(` \u2713 Node env at ${env2.dir} (node_modules: ${env2.nodeModulesDir})`));
|
|
123076
|
+
did++;
|
|
123077
|
+
} catch (err) {
|
|
123078
|
+
if (err instanceof NodeEnvError) {
|
|
123079
|
+
console.error(source_default.red(` Node env failed: ${err.message}`));
|
|
123080
|
+
if (err.stderr)
|
|
123081
|
+
console.error(source_default.dim(err.stderr));
|
|
123082
|
+
process.exit(1);
|
|
123083
|
+
}
|
|
123084
|
+
throw err;
|
|
123085
|
+
}
|
|
123086
|
+
}
|
|
123087
|
+
if (did === 0) {
|
|
123088
|
+
console.error(source_default.yellow(`Skill "${skill}" has neither requirements.txt nor package.json \u2014 nothing to rebuild.`));
|
|
123089
|
+
process.exit(1);
|
|
123090
|
+
}
|
|
122453
123091
|
});
|
|
122454
123092
|
}
|
|
122455
123093
|
|
|
123094
|
+
// src/cli/workspace.ts
|
|
123095
|
+
init_helpers();
|
|
123096
|
+
init_loader();
|
|
123097
|
+
import { existsSync as existsSync106 } from "node:fs";
|
|
123098
|
+
import { resolve as resolve56, sep as sep7 } from "node:path";
|
|
123099
|
+
import { spawnSync as spawnSync23 } from "node:child_process";
|
|
123100
|
+
|
|
122456
123101
|
// src/agents/memory-search.ts
|
|
122457
123102
|
import { readFile as readFile3, readdir, realpath, stat as stat2 } from "node:fs/promises";
|
|
122458
123103
|
import path9 from "node:path";
|
|
@@ -122730,8 +123375,8 @@ function registerWorkspaceCommand(program3) {
|
|
|
122730
123375
|
const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
|
|
122731
123376
|
if (!dir)
|
|
122732
123377
|
return;
|
|
122733
|
-
const resolvedWorkspace =
|
|
122734
|
-
const target =
|
|
123378
|
+
const resolvedWorkspace = resolve56(dir);
|
|
123379
|
+
const target = resolve56(resolvedWorkspace, file ?? "AGENTS.md");
|
|
122735
123380
|
if (!isInsideWorkspace(resolvedWorkspace, target)) {
|
|
122736
123381
|
process.stderr.write(`workspace edit: refusing path traversal outside workspace dir (${target})
|
|
122737
123382
|
`);
|
|
@@ -122799,8 +123444,8 @@ function registerWorkspaceCommand(program3) {
|
|
|
122799
123444
|
const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
|
|
122800
123445
|
if (!dir)
|
|
122801
123446
|
return;
|
|
122802
|
-
const gitDir =
|
|
122803
|
-
if (!
|
|
123447
|
+
const gitDir = resolve56(dir, ".git");
|
|
123448
|
+
if (!existsSync106(gitDir)) {
|
|
122804
123449
|
process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
|
|
122805
123450
|
`);
|
|
122806
123451
|
return;
|
|
@@ -122853,8 +123498,8 @@ function registerWorkspaceCommand(program3) {
|
|
|
122853
123498
|
const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
|
|
122854
123499
|
if (!dir)
|
|
122855
123500
|
return;
|
|
122856
|
-
const gitDir =
|
|
122857
|
-
if (!
|
|
123501
|
+
const gitDir = resolve56(dir, ".git");
|
|
123502
|
+
if (!existsSync106(gitDir)) {
|
|
122858
123503
|
process.stdout.write(`Workspace is not a git repository.
|
|
122859
123504
|
`);
|
|
122860
123505
|
return;
|
|
@@ -122877,9 +123522,9 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
|
|
|
122877
123522
|
return;
|
|
122878
123523
|
}
|
|
122879
123524
|
const agentsDir = resolveAgentsDir(config);
|
|
122880
|
-
const agentDir =
|
|
123525
|
+
const agentDir = resolve56(agentsDir, agentName);
|
|
122881
123526
|
const dir = resolveAgentWorkspaceDir(agentDir);
|
|
122882
|
-
if (!
|
|
123527
|
+
if (!existsSync106(dir)) {
|
|
122883
123528
|
process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
|
|
122884
123529
|
`);
|
|
122885
123530
|
return;
|
|
@@ -122915,8 +123560,8 @@ function safeParseInt(value, fallback) {
|
|
|
122915
123560
|
init_helpers();
|
|
122916
123561
|
init_loader();
|
|
122917
123562
|
init_merge();
|
|
122918
|
-
import { copyFileSync as copyFileSync14, existsSync as
|
|
122919
|
-
import { join as
|
|
123563
|
+
import { copyFileSync as copyFileSync14, existsSync as existsSync107, readFileSync as readFileSync96, writeFileSync as writeFileSync43 } from "node:fs";
|
|
123564
|
+
import { join as join111, resolve as resolve57 } from "node:path";
|
|
122920
123565
|
init_scaffold();
|
|
122921
123566
|
init_profiles();
|
|
122922
123567
|
init_schema();
|
|
@@ -122931,9 +123576,9 @@ function resolveSoulTargetOrExit(program3, agentName) {
|
|
|
122931
123576
|
const profileName = merged.extends ?? DEFAULT_PROFILE;
|
|
122932
123577
|
const profilePath = getProfilePath(profileName);
|
|
122933
123578
|
const agentsDir = resolveAgentsDir(config);
|
|
122934
|
-
const agentDir =
|
|
123579
|
+
const agentDir = resolve57(agentsDir, agentName);
|
|
122935
123580
|
const workspaceDir = resolveAgentWorkspaceDir(agentDir);
|
|
122936
|
-
if (!
|
|
123581
|
+
if (!existsSync107(workspaceDir)) {
|
|
122937
123582
|
console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
|
|
122938
123583
|
process.exit(1);
|
|
122939
123584
|
}
|
|
@@ -122942,7 +123587,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
|
|
|
122942
123587
|
profileName,
|
|
122943
123588
|
profilePath,
|
|
122944
123589
|
workspaceDir,
|
|
122945
|
-
soulPath:
|
|
123590
|
+
soulPath: join111(workspaceDir, "SOUL.md"),
|
|
122946
123591
|
soul: merged.soul
|
|
122947
123592
|
};
|
|
122948
123593
|
}
|
|
@@ -122959,11 +123604,11 @@ function registerSoulCommand(program3) {
|
|
|
122959
123604
|
const t = resolveSoulTargetOrExit(program3, agentName);
|
|
122960
123605
|
if (!t)
|
|
122961
123606
|
return;
|
|
122962
|
-
if (!
|
|
123607
|
+
if (!existsSync107(t.soulPath)) {
|
|
122963
123608
|
console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
|
|
122964
123609
|
process.exit(1);
|
|
122965
123610
|
}
|
|
122966
|
-
process.stdout.write(
|
|
123611
|
+
process.stdout.write(readFileSync96(t.soulPath, "utf-8"));
|
|
122967
123612
|
}));
|
|
122968
123613
|
cmd.command("reset <agent>").description("Re-seed SOUL.md from the agent's current profile " + "(backs the existing file up to SOUL.md.bak first)").option("-y, --yes", "Skip the confirmation prompt").action(withConfigError(async (agentName, opts) => {
|
|
122969
123614
|
const t = resolveSoulTargetOrExit(program3, agentName);
|
|
@@ -122974,7 +123619,7 @@ function registerSoulCommand(program3) {
|
|
|
122974
123619
|
console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
|
|
122975
123620
|
process.exit(1);
|
|
122976
123621
|
}
|
|
122977
|
-
const exists =
|
|
123622
|
+
const exists = existsSync107(t.soulPath);
|
|
122978
123623
|
if (exists && !opts.yes) {
|
|
122979
123624
|
if (!isInteractive()) {
|
|
122980
123625
|
console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
|
|
@@ -122989,7 +123634,7 @@ function registerSoulCommand(program3) {
|
|
|
122989
123634
|
let backupPath;
|
|
122990
123635
|
if (exists) {
|
|
122991
123636
|
backupPath = `${t.soulPath}.bak`;
|
|
122992
|
-
if (
|
|
123637
|
+
if (existsSync107(backupPath)) {
|
|
122993
123638
|
backupPath = `${t.soulPath}.bak.${Date.now()}`;
|
|
122994
123639
|
}
|
|
122995
123640
|
copyFileSync14(t.soulPath, backupPath);
|
|
@@ -123005,279 +123650,6 @@ function registerSoulCommand(program3) {
|
|
|
123005
123650
|
}));
|
|
123006
123651
|
}
|
|
123007
123652
|
|
|
123008
|
-
// src/cli/debug.ts
|
|
123009
|
-
init_helpers();
|
|
123010
|
-
init_loader();
|
|
123011
|
-
import { existsSync as existsSync107, readFileSync as readFileSync96, readdirSync as readdirSync36, statSync as statSync62 } from "node:fs";
|
|
123012
|
-
import { resolve as resolve57, join as join111 } from "node:path";
|
|
123013
|
-
import { createHash as createHash21 } from "node:crypto";
|
|
123014
|
-
init_merge();
|
|
123015
|
-
init_hindsight2();
|
|
123016
|
-
function formatBytes(bytes) {
|
|
123017
|
-
return `${bytes.toLocaleString()} bytes`;
|
|
123018
|
-
}
|
|
123019
|
-
function estimateTokens(bytes) {
|
|
123020
|
-
return Math.round(bytes / 3.7);
|
|
123021
|
-
}
|
|
123022
|
-
function readMcpServerNames(agentDir) {
|
|
123023
|
-
const mcpPath = join111(agentDir, ".mcp.json");
|
|
123024
|
-
if (!existsSync107(mcpPath))
|
|
123025
|
-
return [];
|
|
123026
|
-
try {
|
|
123027
|
-
const parsed = JSON.parse(readFileSync96(mcpPath, "utf-8"));
|
|
123028
|
-
return Object.keys(parsed.mcpServers ?? {});
|
|
123029
|
-
} catch {
|
|
123030
|
-
return null;
|
|
123031
|
-
}
|
|
123032
|
-
}
|
|
123033
|
-
function sha2562(content) {
|
|
123034
|
-
return createHash21("sha256").update(content).digest("hex").slice(0, 16);
|
|
123035
|
-
}
|
|
123036
|
-
function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
123037
|
-
const projectsDir = join111(claudeConfigDir, "projects");
|
|
123038
|
-
if (!existsSync107(projectsDir))
|
|
123039
|
-
return;
|
|
123040
|
-
try {
|
|
123041
|
-
const entries = readdirSync36(projectsDir, { withFileTypes: true });
|
|
123042
|
-
let latest;
|
|
123043
|
-
for (const entry of entries) {
|
|
123044
|
-
if (!entry.isDirectory())
|
|
123045
|
-
continue;
|
|
123046
|
-
const projectPath = join111(projectsDir, entry.name);
|
|
123047
|
-
const transcriptPath = join111(projectPath, "transcript.jsonl");
|
|
123048
|
-
if (!existsSync107(transcriptPath))
|
|
123049
|
-
continue;
|
|
123050
|
-
const stat3 = statSync62(transcriptPath);
|
|
123051
|
-
if (!latest || stat3.mtimeMs > latest.mtime) {
|
|
123052
|
-
latest = { path: transcriptPath, mtime: stat3.mtimeMs };
|
|
123053
|
-
}
|
|
123054
|
-
}
|
|
123055
|
-
return latest?.path;
|
|
123056
|
-
} catch {
|
|
123057
|
-
return;
|
|
123058
|
-
}
|
|
123059
|
-
}
|
|
123060
|
-
function extractLatestUserMessage(transcriptPath) {
|
|
123061
|
-
try {
|
|
123062
|
-
const content = readFileSync96(transcriptPath, "utf-8");
|
|
123063
|
-
const lines = content.trim().split(`
|
|
123064
|
-
`).filter(Boolean);
|
|
123065
|
-
for (let i2 = lines.length - 1;i2 >= 0; i2--) {
|
|
123066
|
-
const line = lines[i2];
|
|
123067
|
-
try {
|
|
123068
|
-
const event = JSON.parse(line);
|
|
123069
|
-
if (event.type === "message" && event.role === "user" && typeof event.content === "string") {
|
|
123070
|
-
const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : "unknown";
|
|
123071
|
-
return { text: event.content, timestamp };
|
|
123072
|
-
}
|
|
123073
|
-
} catch {
|
|
123074
|
-
continue;
|
|
123075
|
-
}
|
|
123076
|
-
}
|
|
123077
|
-
} catch {
|
|
123078
|
-
return;
|
|
123079
|
-
}
|
|
123080
|
-
}
|
|
123081
|
-
function buildProgressUpdateGuidance() {
|
|
123082
|
-
return `## Progress updates (human-style check-ins)
|
|
123083
|
-
|
|
123084
|
-
You're talking to a human colleague on Telegram. Alongside the emoji status
|
|
123085
|
-
ladder, send a short \`progress_update\` at inflection points, the moments a
|
|
123086
|
-
senior colleague would ping the person who asked them to do something:
|
|
123087
|
-
|
|
123088
|
-
- **Plan formed:** "Got it. Going to do X first, then Y, then Z."
|
|
123089
|
-
- **Pivot or blocker:** "First approach didn't work because <reason>. Trying
|
|
123090
|
-
<alternative> instead."
|
|
123091
|
-
- **Chunk finished:** "Done with X. Starting Y now."
|
|
123092
|
-
|
|
123093
|
-
Keep them short (one or two sentences). Don't narrate every step, the pinned
|
|
123094
|
-
progress card shows that for free. Don't send an update on a trivial one-shot
|
|
123095
|
-
task. Send them when a colleague would genuinely want to know what's happening.
|
|
123096
|
-
|
|
123097
|
-
Final answers still go through \`reply\` as usual,
|
|
123098
|
-
\`progress_update\` is only for mid-turn check-ins.`;
|
|
123099
|
-
}
|
|
123100
|
-
function registerDebugCommand(program3) {
|
|
123101
|
-
const cmd = program3.command("debug", { hidden: true }).description("Observability tools for inspecting agent prompt layering [advanced]");
|
|
123102
|
-
cmd.command("turn <agent>").description("Dump the exact prompt layering the model saw on the most recent turn").option("--last <n>", "Show N-th most recent turn instead of latest", "1").action(withConfigError(async (agentName, opts) => {
|
|
123103
|
-
const config = getConfig(program3);
|
|
123104
|
-
const agentConfig = config.agents[agentName];
|
|
123105
|
-
if (!agentConfig) {
|
|
123106
|
-
console.error(`Agent '${agentName}' not found in switchroom.yaml`);
|
|
123107
|
-
process.exit(1);
|
|
123108
|
-
}
|
|
123109
|
-
const agentsDir = resolveAgentsDir(config);
|
|
123110
|
-
const agentDir = resolve57(agentsDir, agentName);
|
|
123111
|
-
if (!existsSync107(agentDir)) {
|
|
123112
|
-
console.error(`Agent directory not found: ${agentDir}`);
|
|
123113
|
-
process.exit(1);
|
|
123114
|
-
}
|
|
123115
|
-
const workspaceDir = resolveAgentWorkspaceDir(agentDir);
|
|
123116
|
-
const claudeConfigDir = join111(agentDir, ".claude");
|
|
123117
|
-
const claudeMdPath2 = join111(agentDir, "CLAUDE.md");
|
|
123118
|
-
const soulMdPath = join111(agentDir, "SOUL.md");
|
|
123119
|
-
const workspaceSoulMdPath = join111(workspaceDir, "SOUL.md");
|
|
123120
|
-
const handoffPath = join111(agentDir, ".handoff.md");
|
|
123121
|
-
const lastN = parseInt(opts.last, 10);
|
|
123122
|
-
if (isNaN(lastN) || lastN < 1) {
|
|
123123
|
-
console.error("--last must be a positive integer");
|
|
123124
|
-
process.exit(1);
|
|
123125
|
-
}
|
|
123126
|
-
if (lastN > 1) {
|
|
123127
|
-
console.error("Note: --last N where N > 1 not yet implemented (only latest turn supported)");
|
|
123128
|
-
process.exit(1);
|
|
123129
|
-
}
|
|
123130
|
-
console.log(`=== Debug Turn Dump: ${agentName} ===
|
|
123131
|
-
`);
|
|
123132
|
-
console.log(`=== Append System Prompt (stable) ===
|
|
123133
|
-
`);
|
|
123134
|
-
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
123135
|
-
const useHotReloadStable = resolved.channels?.telegram?.hotReloadStable === true;
|
|
123136
|
-
const stableResult = await buildStableBootstrapPrompt({
|
|
123137
|
-
workspaceDir,
|
|
123138
|
-
budget: { warningMode: "off" }
|
|
123139
|
-
});
|
|
123140
|
-
if (useHotReloadStable) {
|
|
123141
|
-
console.log(`-- Workspace Stable Render (not used \u2014 stable content is in per-turn hook) --`);
|
|
123142
|
-
console.log();
|
|
123143
|
-
} else if (stableResult.concatenated.trim().length > 0) {
|
|
123144
|
-
console.log(`-- Workspace Stable Render (${formatBytes(stableResult.concatenated.length)}) --`);
|
|
123145
|
-
console.log(stableResult.concatenated);
|
|
123146
|
-
console.log();
|
|
123147
|
-
} else {
|
|
123148
|
-
console.log("-- Workspace Stable Render (0 bytes, not set up) --");
|
|
123149
|
-
console.log();
|
|
123150
|
-
}
|
|
123151
|
-
const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(resolved);
|
|
123152
|
-
const progressGuidance = useSwitchroomPlugin ? buildProgressUpdateGuidance() : "";
|
|
123153
|
-
if (progressGuidance.length > 0) {
|
|
123154
|
-
console.log(`-- Progress Updates Guidance (${formatBytes(progressGuidance.length)}) --`);
|
|
123155
|
-
console.log(progressGuidance);
|
|
123156
|
-
console.log();
|
|
123157
|
-
}
|
|
123158
|
-
const baseSystemPromptAppend = resolved.system_prompt_append ?? "";
|
|
123159
|
-
if (baseSystemPromptAppend.trim().length > 0) {
|
|
123160
|
-
console.log(`-- User System Prompt Append (${formatBytes(baseSystemPromptAppend.length)}) --`);
|
|
123161
|
-
console.log(baseSystemPromptAppend);
|
|
123162
|
-
console.log();
|
|
123163
|
-
}
|
|
123164
|
-
console.log(`=== Append System Prompt (per-session) ===
|
|
123165
|
-
`);
|
|
123166
|
-
const handoffContent = existsSync107(handoffPath) ? readFileSync96(handoffPath, "utf-8") : "";
|
|
123167
|
-
if (handoffContent.trim().length > 0) {
|
|
123168
|
-
console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
|
|
123169
|
-
console.log(handoffContent);
|
|
123170
|
-
console.log();
|
|
123171
|
-
} else {
|
|
123172
|
-
console.log("-- Handoff Briefing (0 bytes, no prior session) --");
|
|
123173
|
-
console.log();
|
|
123174
|
-
}
|
|
123175
|
-
console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
|
|
123176
|
-
`);
|
|
123177
|
-
const claudeMdContent = existsSync107(claudeMdPath2) ? readFileSync96(claudeMdPath2, "utf-8") : "";
|
|
123178
|
-
if (claudeMdContent.trim().length > 0) {
|
|
123179
|
-
console.log(`(${formatBytes(claudeMdContent.length)})`);
|
|
123180
|
-
console.log(claudeMdContent);
|
|
123181
|
-
console.log();
|
|
123182
|
-
} else {
|
|
123183
|
-
console.log("(0 bytes, not present)");
|
|
123184
|
-
console.log();
|
|
123185
|
-
}
|
|
123186
|
-
console.log(`=== Persona (SOUL.md) ===
|
|
123187
|
-
`);
|
|
123188
|
-
const soulMdContent = existsSync107(soulMdPath) ? readFileSync96(soulMdPath, "utf-8") : existsSync107(workspaceSoulMdPath) ? readFileSync96(workspaceSoulMdPath, "utf-8") : "";
|
|
123189
|
-
if (soulMdContent.trim().length > 0) {
|
|
123190
|
-
console.log(`(${formatBytes(soulMdContent.length)})`);
|
|
123191
|
-
console.log(soulMdContent);
|
|
123192
|
-
console.log();
|
|
123193
|
-
} else {
|
|
123194
|
-
console.log("(0 bytes, stale placeholder \u2014 Phase 2 item: single source of truth for persona)");
|
|
123195
|
-
console.log();
|
|
123196
|
-
}
|
|
123197
|
-
console.log(`=== Per-Turn Injections (UserPromptSubmit) ===
|
|
123198
|
-
`);
|
|
123199
|
-
if (useHotReloadStable) {
|
|
123200
|
-
if (stableResult.concatenated.trim().length > 0) {
|
|
123201
|
-
console.log(`-- Workspace Stable (hot-reload hook): ${formatBytes(stableResult.concatenated.length)} --`);
|
|
123202
|
-
console.log(stableResult.concatenated);
|
|
123203
|
-
console.log();
|
|
123204
|
-
} else {
|
|
123205
|
-
console.log("-- Workspace Stable (hot-reload hook): 0 bytes, not set up --");
|
|
123206
|
-
console.log();
|
|
123207
|
-
}
|
|
123208
|
-
}
|
|
123209
|
-
const dynamicResult = await buildDynamicBootstrapPrompt({
|
|
123210
|
-
workspaceDir,
|
|
123211
|
-
budget: { warningMode: "off" }
|
|
123212
|
-
});
|
|
123213
|
-
if (dynamicResult.concatenated.trim().length > 0) {
|
|
123214
|
-
console.log(`-- Workspace Dynamic: fired, ${formatBytes(dynamicResult.concatenated.length)} --`);
|
|
123215
|
-
console.log(dynamicResult.concatenated);
|
|
123216
|
-
console.log();
|
|
123217
|
-
} else {
|
|
123218
|
-
console.log("-- Workspace Dynamic: no content (MEMORY.md and daily notes empty or missing) --");
|
|
123219
|
-
console.log();
|
|
123220
|
-
}
|
|
123221
|
-
const hindsightEnabled = isHindsightEnabled(config) && agentConfig.memory?.auto_recall !== false;
|
|
123222
|
-
if (hindsightEnabled) {
|
|
123223
|
-
console.log("-- Hindsight Recall: enabled (exact content unavailable, check hindsight logs) --");
|
|
123224
|
-
console.log();
|
|
123225
|
-
} else {
|
|
123226
|
-
console.log("-- Hindsight Recall: disabled --");
|
|
123227
|
-
console.log();
|
|
123228
|
-
}
|
|
123229
|
-
console.log(`=== User Message (latest turn) ===
|
|
123230
|
-
`);
|
|
123231
|
-
const transcriptPath = findLatestTranscriptJsonl(claudeConfigDir);
|
|
123232
|
-
const userMessage = transcriptPath ? extractLatestUserMessage(transcriptPath) : undefined;
|
|
123233
|
-
if (userMessage) {
|
|
123234
|
-
console.log(`(Turn timestamp: ${userMessage.timestamp})`);
|
|
123235
|
-
console.log(userMessage.text);
|
|
123236
|
-
console.log();
|
|
123237
|
-
} else {
|
|
123238
|
-
console.log("(unavailable: no transcript found or transcript empty)");
|
|
123239
|
-
console.log();
|
|
123240
|
-
}
|
|
123241
|
-
console.log(`=== Totals ===
|
|
123242
|
-
`);
|
|
123243
|
-
const stableBytes = stableResult.concatenated.length + progressGuidance.length + baseSystemPromptAppend.length;
|
|
123244
|
-
const perSessionBytes = handoffContent.length;
|
|
123245
|
-
const claudeMdBytes = claudeMdContent.length;
|
|
123246
|
-
const soulMdBytes = soulMdContent.length;
|
|
123247
|
-
const perTurnBytes = dynamicResult.concatenated.length;
|
|
123248
|
-
const userBytes = userMessage?.text.length ?? 0;
|
|
123249
|
-
const fleetDir = join111(agentsDir, "..", "fleet");
|
|
123250
|
-
const fleetInvPath = join111(fleetDir, "switchroom-invariants.md");
|
|
123251
|
-
const fleetClaudePath = join111(fleetDir, "CLAUDE.md");
|
|
123252
|
-
const fleetInvBytes = existsSync107(fleetInvPath) ? readFileSync96(fleetInvPath, "utf-8").length : 0;
|
|
123253
|
-
const fleetClaudeBytes = existsSync107(fleetClaudePath) ? readFileSync96(fleetClaudePath, "utf-8").length : 0;
|
|
123254
|
-
const fleetBytes = fleetInvBytes + fleetClaudeBytes;
|
|
123255
|
-
const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
|
|
123256
|
-
console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
|
|
123257
|
-
console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
|
|
123258
|
-
console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
|
|
123259
|
-
console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
|
|
123260
|
-
console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
|
|
123261
|
-
console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
|
|
123262
|
-
console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
|
|
123263
|
-
const mcpServers = readMcpServerNames(agentDir);
|
|
123264
|
-
const mcpCount = mcpServers?.length ?? null;
|
|
123265
|
-
const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
|
|
123266
|
-
const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
|
|
123267
|
-
const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
|
|
123268
|
-
console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
|
|
123269
|
-
if (mcpServers && mcpServers.length > 0) {
|
|
123270
|
-
console.log(` [${mcpServers.join(", ")}]`);
|
|
123271
|
-
}
|
|
123272
|
-
const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
|
|
123273
|
-
console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
|
|
123274
|
-
const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
|
|
123275
|
-
const stableHash = sha2562(stableCacheInput);
|
|
123276
|
-
console.log(`Cache stable hash: sha256:${stableHash}`);
|
|
123277
|
-
console.log();
|
|
123278
|
-
}));
|
|
123279
|
-
}
|
|
123280
|
-
|
|
123281
123653
|
// src/cli/worktree.ts
|
|
123282
123654
|
init_source();
|
|
123283
123655
|
init_claim();
|