@lore-co/cli 0.1.16 → 0.1.18
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/README.md +76 -14
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +3 -26
- package/dist/ask.js.map +1 -1
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +566 -39
- package/dist/cli.js.map +1 -1
- package/dist/context-fallback.d.ts +37 -0
- package/dist/context-fallback.d.ts.map +1 -0
- package/dist/context-fallback.js +259 -0
- package/dist/context-fallback.js.map +1 -0
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/invocation-health-writer.d.ts +19 -0
- package/dist/invocation-health-writer.d.ts.map +1 -0
- package/dist/invocation-health-writer.js +131 -0
- package/dist/invocation-health-writer.js.map +1 -0
- package/dist/reliability-store.d.ts +283 -0
- package/dist/reliability-store.d.ts.map +1 -0
- package/dist/reliability-store.js +1913 -0
- package/dist/reliability-store.js.map +1 -0
- package/dist/runtime-version.d.ts +2 -0
- package/dist/runtime-version.d.ts.map +1 -0
- package/dist/runtime-version.js +5 -0
- package/dist/runtime-version.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1041 -152
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.js +2 -2
- package/dist/signed-snapshot.d.ts +59 -0
- package/dist/signed-snapshot.d.ts.map +1 -0
- package/dist/signed-snapshot.js +303 -0
- package/dist/signed-snapshot.js.map +1 -0
- package/dist/update.js +3 -3
- package/package.json +19 -3
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { access, chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { NATIVE_CODING_AGENT_NAMES, WorkspaceIdentityResponseSchema, isNativeCodingAgent, } from "@lore-co/core";
|
|
8
|
+
import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
|
|
9
9
|
import { runHook, } from "./runtime.js";
|
|
10
|
+
import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
|
|
10
11
|
import { runAskCommand } from "./ask.js";
|
|
11
12
|
import { runGuardCommand } from "./guard.js";
|
|
12
13
|
import { runContinueCommand, runResumeCommand } from "./handoff.js";
|
|
@@ -18,6 +19,7 @@ import { runHostCommand } from "./host.js";
|
|
|
18
19
|
import { runDemoCommand } from "./demo.js";
|
|
19
20
|
import { updateCommand } from "./update.js";
|
|
20
21
|
import { runSelfHostCommand } from "./self-host.js";
|
|
22
|
+
import { ReliabilityStore, ReliabilityStoreError, integrationInvocationStateKey, } from "./reliability-store.js";
|
|
21
23
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
22
24
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
23
25
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
@@ -42,6 +44,13 @@ const POLYTOKEN_HOOK_EVENTS = [
|
|
|
42
44
|
"pre_user_prompt",
|
|
43
45
|
"post_model_turn",
|
|
44
46
|
];
|
|
47
|
+
const AGENT_MANIFESTS = {
|
|
48
|
+
claude: CLAUDE_RELIABILITY_INTEGRATION,
|
|
49
|
+
codex: CODEX_RELIABILITY_INTEGRATION,
|
|
50
|
+
cursor: CURSOR_RELIABILITY_INTEGRATION,
|
|
51
|
+
opencode: OPENCODE_RELIABILITY_INTEGRATION,
|
|
52
|
+
polytoken: POLYTOKEN_RELIABILITY_INTEGRATION,
|
|
53
|
+
};
|
|
45
54
|
const ROOT_HELP = `lore
|
|
46
55
|
Connect local coding agents to Lore shared engineering memory.
|
|
47
56
|
|
|
@@ -113,7 +122,7 @@ Examples:
|
|
|
113
122
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
114
123
|
`;
|
|
115
124
|
const STATUS_HELP = `lore status
|
|
116
|
-
Show
|
|
125
|
+
Show exact installation, declared capabilities, and last invocation health.
|
|
117
126
|
|
|
118
127
|
Usage:
|
|
119
128
|
lore status [--json]
|
|
@@ -123,7 +132,7 @@ Examples:
|
|
|
123
132
|
lore status --json
|
|
124
133
|
`;
|
|
125
134
|
const DOCTOR_HELP = `lore doctor
|
|
126
|
-
Check local security,
|
|
135
|
+
Check local security, exact wiring, invocation health, binaries, and Lore health.
|
|
127
136
|
|
|
128
137
|
Usage:
|
|
129
138
|
lore doctor [--json]
|
|
@@ -133,8 +142,9 @@ Examples:
|
|
|
133
142
|
lore doctor --json
|
|
134
143
|
`;
|
|
135
144
|
const DISCONNECT_HELP = `lore disconnect
|
|
136
|
-
Remove
|
|
137
|
-
|
|
145
|
+
Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
|
|
146
|
+
Durable captures, dead letters, verified snapshots, unrelated agent settings,
|
|
147
|
+
plugins, and Lore-created backups are retained.
|
|
138
148
|
|
|
139
149
|
Usage:
|
|
140
150
|
lore disconnect [--json]
|
|
@@ -163,6 +173,12 @@ export function getLorePaths(home, environment = process.env) {
|
|
|
163
173
|
config: resolve(loreDirectory, "config.json"),
|
|
164
174
|
runtime: resolve(loreDirectory, "bin", "lore-hook.mjs"),
|
|
165
175
|
runtimeRepository: resolve(loreDirectory, "bin", "repository.js"),
|
|
176
|
+
runtimeReliabilityStore: resolve(loreDirectory, "bin", "reliability-store.js"),
|
|
177
|
+
runtimeSignedSnapshot: resolve(loreDirectory, "bin", "signed-snapshot.js"),
|
|
178
|
+
runtimeCanonicalJson: resolve(loreDirectory, "bin", "canonical-json.js"),
|
|
179
|
+
runtimeContextFallback: resolve(loreDirectory, "bin", "context-fallback.js"),
|
|
180
|
+
runtimeInvocationHealthWriter: resolve(loreDirectory, "bin", "invocation-health-writer.js"),
|
|
181
|
+
runtimeVersion: resolve(loreDirectory, "bin", "runtime-version.js"),
|
|
166
182
|
runtimePackage: resolve(loreDirectory, "bin", "package.json"),
|
|
167
183
|
state: resolve(loreDirectory, "state"),
|
|
168
184
|
queue: resolve(loreDirectory, "queue"),
|
|
@@ -452,6 +468,120 @@ export function countLoreOpenCodePlugins(input) {
|
|
|
452
468
|
? input.plugin.filter(isLoreOpenCodePlugin).length
|
|
453
469
|
: 0;
|
|
454
470
|
}
|
|
471
|
+
function installationUnitId(event, index, total) {
|
|
472
|
+
return total === 1 ? event : `installation-${index + 1}/${event}`;
|
|
473
|
+
}
|
|
474
|
+
function installationUnitState(ownedCount, exactCount) {
|
|
475
|
+
if (ownedCount === 0) {
|
|
476
|
+
return "missing";
|
|
477
|
+
}
|
|
478
|
+
if (ownedCount > 1 || exactCount > 1) {
|
|
479
|
+
return "duplicate";
|
|
480
|
+
}
|
|
481
|
+
return exactCount === 1 ? "exact" : "drifted";
|
|
482
|
+
}
|
|
483
|
+
function exactConfiguration(left, right) {
|
|
484
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
485
|
+
}
|
|
486
|
+
function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
487
|
+
const configuration = objectConfiguration(input, agent);
|
|
488
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
489
|
+
throw new Error(`${agent} configuration field "hooks" must be an object`);
|
|
490
|
+
}
|
|
491
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
492
|
+
return HOOK_EVENTS.map((event) => {
|
|
493
|
+
const groups = hooks[event];
|
|
494
|
+
if (groups !== undefined && !Array.isArray(groups)) {
|
|
495
|
+
throw new Error(`${agent} hook event "${event}" must be an array`);
|
|
496
|
+
}
|
|
497
|
+
const candidates = Array.isArray(groups) ? groups : [];
|
|
498
|
+
const expected = {
|
|
499
|
+
...(event === "PreToolUse" ? { matcher: GUARD_TOOL_MATCHER } : {}),
|
|
500
|
+
hooks: [eventHandler(agent, event, paths)],
|
|
501
|
+
};
|
|
502
|
+
const ownedCount = candidates.reduce((count, candidate) => {
|
|
503
|
+
if (!isObject(candidate) || !Array.isArray(candidate.hooks)) {
|
|
504
|
+
return count;
|
|
505
|
+
}
|
|
506
|
+
return count + candidate.hooks.filter(isLoreHook).length;
|
|
507
|
+
}, 0);
|
|
508
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
509
|
+
return {
|
|
510
|
+
id: installationUnitId(event, index, total),
|
|
511
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function cursorInstallationUnits(input, paths, index, total) {
|
|
516
|
+
const configuration = objectConfiguration(input, "cursor");
|
|
517
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
518
|
+
throw new Error('Cursor configuration field "version" must be 1');
|
|
519
|
+
}
|
|
520
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
521
|
+
throw new Error('Cursor configuration field "hooks" must be an object');
|
|
522
|
+
}
|
|
523
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
524
|
+
return CURSOR_HOOK_EVENTS.map((event) => {
|
|
525
|
+
const values = hooks[event];
|
|
526
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
527
|
+
throw new Error(`Cursor hook event "${event}" must be an array`);
|
|
528
|
+
}
|
|
529
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
530
|
+
const expected = cursorEventHandler(event, paths);
|
|
531
|
+
const ownedCount = candidates.filter(isLoreHook).length;
|
|
532
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
533
|
+
return {
|
|
534
|
+
id: installationUnitId(event, index, total),
|
|
535
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
function polytokenInstallationUnits(input, paths, index, total) {
|
|
540
|
+
if (!Array.isArray(input)) {
|
|
541
|
+
throw new Error("Polytoken hooks must contain a JSON array");
|
|
542
|
+
}
|
|
543
|
+
return POLYTOKEN_HOOK_EVENTS.map((event) => {
|
|
544
|
+
const expected = polytokenEventHandler(event, paths);
|
|
545
|
+
const owned = input.filter((candidate) => isLorePolytokenHook(candidate) &&
|
|
546
|
+
(!isObject(candidate) || candidate.event === event));
|
|
547
|
+
const exactCount = owned.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
548
|
+
return {
|
|
549
|
+
id: installationUnitId(event, index, total),
|
|
550
|
+
state: installationUnitState(owned.length, exactCount),
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function openCodeInstallationUnits(input, index, total) {
|
|
555
|
+
const configuration = objectConfiguration(input, "opencode");
|
|
556
|
+
if (configuration.plugin !== undefined &&
|
|
557
|
+
!Array.isArray(configuration.plugin)) {
|
|
558
|
+
throw new Error('OpenCode configuration field "plugin" must be an array');
|
|
559
|
+
}
|
|
560
|
+
const plugins = Array.isArray(configuration.plugin)
|
|
561
|
+
? configuration.plugin
|
|
562
|
+
: [];
|
|
563
|
+
const owned = plugins.filter(isLoreOpenCodePlugin);
|
|
564
|
+
const exactCount = owned.filter((candidate) => candidate === LORE_OPENCODE_PLUGIN).length;
|
|
565
|
+
return [
|
|
566
|
+
{
|
|
567
|
+
id: installationUnitId("plugin", index, total),
|
|
568
|
+
state: installationUnitState(owned.length, exactCount),
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
function inspectInstallationUnits(input, agent, paths, index, total) {
|
|
573
|
+
switch (agent) {
|
|
574
|
+
case "claude":
|
|
575
|
+
case "codex":
|
|
576
|
+
return commandHookInstallationUnits(input, agent, paths, index, total);
|
|
577
|
+
case "cursor":
|
|
578
|
+
return cursorInstallationUnits(input, paths, index, total);
|
|
579
|
+
case "polytoken":
|
|
580
|
+
return polytokenInstallationUnits(input, paths, index, total);
|
|
581
|
+
case "opencode":
|
|
582
|
+
return openCodeInstallationUnits(input, index, total);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
455
585
|
async function readJsonDocument(path) {
|
|
456
586
|
try {
|
|
457
587
|
const [raw, metadata] = await Promise.all([
|
|
@@ -511,7 +641,7 @@ async function writeMergedJson(path, document, value, now) {
|
|
|
511
641
|
}
|
|
512
642
|
function parseConnectorConfig(value) {
|
|
513
643
|
if (!isObject(value) ||
|
|
514
|
-
value.version !== 1 ||
|
|
644
|
+
(value.version !== 1 && value.version !== 2) ||
|
|
515
645
|
typeof value.apiUrl !== "string" ||
|
|
516
646
|
typeof value.token !== "string" ||
|
|
517
647
|
!Array.isArray(value.agents) ||
|
|
@@ -547,10 +677,15 @@ function parseConnectorConfig(value) {
|
|
|
547
677
|
const dashboardUrl = typeof value.dashboardUrl === "string"
|
|
548
678
|
? normalizeApiUrl(value.dashboardUrl)
|
|
549
679
|
: undefined;
|
|
680
|
+
const parsedWorkspaceId = WorkspaceIdentityResponseSchema.shape.workspaceId.safeParse(value.workspaceId);
|
|
681
|
+
const workspaceId = parsedWorkspaceId.success
|
|
682
|
+
? parsedWorkspaceId.data
|
|
683
|
+
: undefined;
|
|
550
684
|
return {
|
|
551
|
-
version:
|
|
685
|
+
version: 2,
|
|
552
686
|
apiUrl: value.apiUrl,
|
|
553
687
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
688
|
+
...(workspaceId === undefined ? {} : { workspaceId }),
|
|
554
689
|
token: value.token,
|
|
555
690
|
agents,
|
|
556
691
|
...(agentConfigPaths === undefined ||
|
|
@@ -979,20 +1114,65 @@ async function installRuntime(paths) {
|
|
|
979
1114
|
await Promise.all([
|
|
980
1115
|
rm(paths.runtime, { force: true }),
|
|
981
1116
|
rm(paths.runtimeRepository, { force: true }),
|
|
1117
|
+
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1118
|
+
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
1119
|
+
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1120
|
+
rm(paths.runtimeContextFallback, { force: true }),
|
|
1121
|
+
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
1122
|
+
rm(paths.runtimeVersion, { force: true }),
|
|
982
1123
|
rm(paths.runtimePackage, { force: true }),
|
|
983
1124
|
]);
|
|
984
1125
|
return;
|
|
985
1126
|
}
|
|
986
1127
|
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
987
|
-
const
|
|
1128
|
+
const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
|
|
1129
|
+
const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
|
|
988
1130
|
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
989
1131
|
readFile(resolve(sourceDirectory, "repository.js"), "utf8"),
|
|
1132
|
+
readFile(resolve(sourceDirectory, "reliability-store.js"), "utf8"),
|
|
1133
|
+
readFile(resolve(sourceDirectory, "signed-snapshot.js"), "utf8"),
|
|
1134
|
+
readFile(canonicalJsonModule, "utf8"),
|
|
1135
|
+
readFile(resolve(sourceDirectory, "context-fallback.js"), "utf8"),
|
|
1136
|
+
readFile(resolve(sourceDirectory, "invocation-health-writer.js"), "utf8"),
|
|
990
1137
|
]);
|
|
1138
|
+
const standaloneSignedSnapshot = signedSnapshot.replaceAll("@lore-co/core/canonical-json", "./canonical-json.js");
|
|
1139
|
+
if (standaloneSignedSnapshot === signedSnapshot) {
|
|
1140
|
+
throw new Error("Signed snapshot runtime dependency could not be localized");
|
|
1141
|
+
}
|
|
1142
|
+
const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
|
|
1143
|
+
const runtimeFiles = {
|
|
1144
|
+
"lore-hook.mjs": runtime,
|
|
1145
|
+
"repository.js": repository,
|
|
1146
|
+
"reliability-store.js": reliabilityStore,
|
|
1147
|
+
"signed-snapshot.js": standaloneSignedSnapshot,
|
|
1148
|
+
"canonical-json.js": canonicalJson,
|
|
1149
|
+
"context-fallback.js": contextFallback,
|
|
1150
|
+
"invocation-health-writer.js": invocationHealthWriter,
|
|
1151
|
+
"runtime-version.js": runtimeVersion,
|
|
1152
|
+
};
|
|
1153
|
+
const runtimeManifest = {
|
|
1154
|
+
type: "module",
|
|
1155
|
+
loreRuntime: {
|
|
1156
|
+
version: LORE_VERSION,
|
|
1157
|
+
files: Object.fromEntries(Object.entries(runtimeFiles).map(([name, contents]) => [
|
|
1158
|
+
name,
|
|
1159
|
+
createHash("sha256").update(contents).digest("hex"),
|
|
1160
|
+
])),
|
|
1161
|
+
},
|
|
1162
|
+
};
|
|
991
1163
|
await Promise.all([
|
|
992
|
-
atomicWrite(paths.runtime, runtime, 0o700),
|
|
993
1164
|
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
994
|
-
atomicWrite(paths.
|
|
1165
|
+
atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
|
|
1166
|
+
atomicWrite(paths.runtimeSignedSnapshot, standaloneSignedSnapshot, 0o600),
|
|
1167
|
+
atomicWrite(paths.runtimeCanonicalJson, canonicalJson, 0o600),
|
|
1168
|
+
atomicWrite(paths.runtimeContextFallback, contextFallback, 0o600),
|
|
1169
|
+
atomicWrite(paths.runtimeInvocationHealthWriter, invocationHealthWriter, 0o600),
|
|
1170
|
+
atomicWrite(paths.runtimeVersion, runtimeVersion, 0o600),
|
|
1171
|
+
atomicWrite(paths.runtimePackage, `${JSON.stringify(runtimeManifest, null, 2)}\n`, 0o600),
|
|
995
1172
|
]);
|
|
1173
|
+
// The hook entrypoint is the generation activation marker. Updating it last
|
|
1174
|
+
// prevents a new runtime from loading before all of its local modules exist.
|
|
1175
|
+
await atomicWrite(paths.runtime, runtime, 0o700);
|
|
996
1176
|
}
|
|
997
1177
|
function agentConfigPath(agent, paths) {
|
|
998
1178
|
return targetFor(agent).configPath(paths);
|
|
@@ -1165,16 +1345,30 @@ async function connectCommand(args) {
|
|
|
1165
1345
|
.map((installation) => installation.configPath);
|
|
1166
1346
|
}
|
|
1167
1347
|
const config = {
|
|
1168
|
-
version:
|
|
1348
|
+
version: 2,
|
|
1169
1349
|
apiUrl,
|
|
1170
1350
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1351
|
+
workspaceId: identity.workspaceId,
|
|
1171
1352
|
token: token.trim(),
|
|
1172
1353
|
agents,
|
|
1173
1354
|
agentConfigPaths,
|
|
1174
1355
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
1175
1356
|
timeoutMs,
|
|
1176
1357
|
};
|
|
1358
|
+
const reliabilityStore = new ReliabilityStore(identity.workspaceId, {
|
|
1359
|
+
home: paths.home,
|
|
1360
|
+
});
|
|
1361
|
+
await reliabilityStore.initialize();
|
|
1362
|
+
await reliabilityStore.migrateLegacyQueue(paths.queue);
|
|
1177
1363
|
await atomicWrite(paths.config, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
1364
|
+
if (existing !== null &&
|
|
1365
|
+
existing.workspaceId === undefined &&
|
|
1366
|
+
existing.apiUrl === apiUrl &&
|
|
1367
|
+
existing.token === token.trim()) {
|
|
1368
|
+
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(apiUrl, token.trim()), { home: paths.home });
|
|
1369
|
+
await credentialStore.transferPendingTo(reliabilityStore);
|
|
1370
|
+
}
|
|
1371
|
+
await reliabilityStore.releaseAuthBlocked(now);
|
|
1178
1372
|
const result = {
|
|
1179
1373
|
connected: true,
|
|
1180
1374
|
apiUrl: config.apiUrl,
|
|
@@ -1195,34 +1389,250 @@ async function queueCount(paths) {
|
|
|
1195
1389
|
return 0;
|
|
1196
1390
|
}
|
|
1197
1391
|
}
|
|
1198
|
-
|
|
1392
|
+
function reliabilityWorkspaceKey(config) {
|
|
1393
|
+
return (config.workspaceId ??
|
|
1394
|
+
credentialReliabilityWorkspaceKey(config.apiUrl, config.token));
|
|
1395
|
+
}
|
|
1396
|
+
function credentialReliabilityWorkspaceKey(apiUrl, token) {
|
|
1397
|
+
return `credential-${createHash("sha256")
|
|
1398
|
+
.update(`${apiUrl}\0${token}`)
|
|
1399
|
+
.digest("hex")
|
|
1400
|
+
.slice(0, 32)}`;
|
|
1401
|
+
}
|
|
1402
|
+
async function inspectReliabilityStore(config, paths) {
|
|
1403
|
+
if (config === null) {
|
|
1404
|
+
return { inspection: null, error: null };
|
|
1405
|
+
}
|
|
1406
|
+
try {
|
|
1407
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
1408
|
+
home: paths.home,
|
|
1409
|
+
});
|
|
1410
|
+
return { inspection: await store.inspectExisting(), error: null };
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
return {
|
|
1414
|
+
inspection: null,
|
|
1415
|
+
error: error instanceof Error
|
|
1416
|
+
? error.message
|
|
1417
|
+
: "Local reliability store is unavailable",
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const MAX_RUNTIME_MODULE_BYTES = 2 * 1024 * 1024;
|
|
1422
|
+
const MAX_RUNTIME_MANIFEST_BYTES = 64 * 1024;
|
|
1423
|
+
async function readRuntimeFile(path, maxBytes, executable = false) {
|
|
1424
|
+
const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
1425
|
+
try {
|
|
1426
|
+
const metadata = await handle.stat();
|
|
1427
|
+
if (!metadata.isFile() ||
|
|
1428
|
+
metadata.size > maxBytes ||
|
|
1429
|
+
(executable && (metadata.mode & 0o111) === 0)) {
|
|
1430
|
+
throw new Error("Runtime file is unsafe or exceeds its size limit");
|
|
1431
|
+
}
|
|
1432
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
1433
|
+
let bytes = 0;
|
|
1434
|
+
while (bytes < buffer.length) {
|
|
1435
|
+
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
1436
|
+
if (result.bytesRead === 0) {
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
bytes += result.bytesRead;
|
|
1440
|
+
}
|
|
1441
|
+
if (bytes > maxBytes) {
|
|
1442
|
+
throw new Error("Runtime file exceeds its size limit");
|
|
1443
|
+
}
|
|
1444
|
+
return buffer.subarray(0, bytes).toString("utf8");
|
|
1445
|
+
}
|
|
1446
|
+
finally {
|
|
1447
|
+
await handle.close();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async function inspectRuntimeInstallation(required, paths) {
|
|
1451
|
+
if (!required) {
|
|
1452
|
+
return { state: "not_required", version: null };
|
|
1453
|
+
}
|
|
1454
|
+
if (IS_STANDALONE_BINARY) {
|
|
1455
|
+
try {
|
|
1456
|
+
await access(process.execPath, fsConstants.R_OK | fsConstants.X_OK);
|
|
1457
|
+
return { state: "installed", version: LORE_VERSION };
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
return { state: "missing", version: null };
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const runtimeFiles = {
|
|
1464
|
+
"lore-hook.mjs": paths.runtime,
|
|
1465
|
+
"repository.js": paths.runtimeRepository,
|
|
1466
|
+
"reliability-store.js": paths.runtimeReliabilityStore,
|
|
1467
|
+
"signed-snapshot.js": paths.runtimeSignedSnapshot,
|
|
1468
|
+
"canonical-json.js": paths.runtimeCanonicalJson,
|
|
1469
|
+
"context-fallback.js": paths.runtimeContextFallback,
|
|
1470
|
+
"invocation-health-writer.js": paths.runtimeInvocationHealthWriter,
|
|
1471
|
+
"runtime-version.js": paths.runtimeVersion,
|
|
1472
|
+
};
|
|
1473
|
+
let contents;
|
|
1474
|
+
let runtimePackage;
|
|
1475
|
+
try {
|
|
1476
|
+
[contents, runtimePackage] = await Promise.all([
|
|
1477
|
+
Promise.all(Object.entries(runtimeFiles).map(async ([name, path]) => {
|
|
1478
|
+
return [
|
|
1479
|
+
name,
|
|
1480
|
+
await readRuntimeFile(path, MAX_RUNTIME_MODULE_BYTES, name === "lore-hook.mjs"),
|
|
1481
|
+
];
|
|
1482
|
+
})),
|
|
1483
|
+
readRuntimeFile(paths.runtimePackage, MAX_RUNTIME_MANIFEST_BYTES),
|
|
1484
|
+
]);
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
const missing = typeof error === "object" &&
|
|
1488
|
+
error !== null &&
|
|
1489
|
+
"code" in error &&
|
|
1490
|
+
error.code === "ENOENT";
|
|
1491
|
+
return { state: missing ? "missing" : "stale", version: null };
|
|
1492
|
+
}
|
|
1493
|
+
let parsed;
|
|
1494
|
+
try {
|
|
1495
|
+
parsed = JSON.parse(runtimePackage);
|
|
1496
|
+
}
|
|
1497
|
+
catch {
|
|
1498
|
+
return { state: "stale", version: null };
|
|
1499
|
+
}
|
|
1500
|
+
const loreRuntime = isObject(parsed) && isObject(parsed.loreRuntime)
|
|
1501
|
+
? parsed.loreRuntime
|
|
1502
|
+
: null;
|
|
1503
|
+
const hashes = loreRuntime !== null && isObject(loreRuntime.files)
|
|
1504
|
+
? loreRuntime.files
|
|
1505
|
+
: null;
|
|
1506
|
+
const version = loreRuntime !== null && typeof loreRuntime.version === "string"
|
|
1507
|
+
? loreRuntime.version
|
|
1508
|
+
: null;
|
|
1509
|
+
const hashEntries = hashes === null ? [] : Object.keys(hashes).sort();
|
|
1510
|
+
const expectedEntries = Object.keys(runtimeFiles).sort();
|
|
1511
|
+
const hashesValid = hashes !== null &&
|
|
1512
|
+
canonicalJson(hashEntries) === canonicalJson(expectedEntries) &&
|
|
1513
|
+
contents.every(([name, content]) => hashes[name] ===
|
|
1514
|
+
createHash("sha256").update(content).digest("hex"));
|
|
1515
|
+
return version === LORE_VERSION && hashesValid
|
|
1516
|
+
? { state: "installed", version }
|
|
1517
|
+
: { state: "stale", version };
|
|
1518
|
+
}
|
|
1519
|
+
async function invocationHealth(config, paths, manifest) {
|
|
1520
|
+
if (config === null) {
|
|
1521
|
+
return {
|
|
1522
|
+
health: deriveReliabilityInvocationHealth(null, manifest.integrationId),
|
|
1523
|
+
error: null,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
1528
|
+
home: paths.home,
|
|
1529
|
+
});
|
|
1530
|
+
const record = await store.readStateExisting(integrationInvocationStateKey(manifest.integrationId));
|
|
1531
|
+
const parsed = ReliabilityInvocationRecordSchema.safeParse(record);
|
|
1532
|
+
if (record !== null &&
|
|
1533
|
+
(!parsed.success ||
|
|
1534
|
+
parsed.data.integrationId !== manifest.integrationId)) {
|
|
1535
|
+
return {
|
|
1536
|
+
health: deriveReliabilityInvocationHealth(undefined, manifest.integrationId),
|
|
1537
|
+
error: "corrupt_record",
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
const health = deriveReliabilityInvocationHealth(record, manifest.integrationId);
|
|
1541
|
+
return {
|
|
1542
|
+
health,
|
|
1543
|
+
error: manifest.integrationId === "plugin/opencode" &&
|
|
1544
|
+
health.runtimeVersion !== null &&
|
|
1545
|
+
health.runtimeVersion !== LORE_VERSION
|
|
1546
|
+
? "runtime_version_mismatch"
|
|
1547
|
+
: null,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
return {
|
|
1552
|
+
health: deriveReliabilityInvocationHealth(undefined, manifest.integrationId),
|
|
1553
|
+
error: error instanceof ReliabilityStoreError
|
|
1554
|
+
? error.code.toLowerCase()
|
|
1555
|
+
: "unavailable",
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function getAgentStatus(agent, config, paths, runtime) {
|
|
1199
1560
|
const target = targetFor(agent);
|
|
1561
|
+
const manifest = AGENT_MANIFESTS[agent];
|
|
1200
1562
|
const configuredPaths = config?.agentConfigPaths?.[agent];
|
|
1201
1563
|
const configPaths = config?.agents.includes(agent) === true && configuredPaths !== undefined
|
|
1202
1564
|
? configuredPaths
|
|
1203
1565
|
: [agentConfigPath(agent, paths)];
|
|
1204
1566
|
let installed = 0;
|
|
1205
1567
|
let existing = 0;
|
|
1206
|
-
|
|
1568
|
+
let invalidConfiguration = false;
|
|
1569
|
+
const units = [];
|
|
1570
|
+
for (const [index, path] of configPaths.entries()) {
|
|
1207
1571
|
try {
|
|
1208
1572
|
const document = await readJsonDocument(path);
|
|
1209
1573
|
installed += countAgentIntegrations(document.value, agent);
|
|
1210
1574
|
existing += document.exists ? 1 : 0;
|
|
1575
|
+
units.push(...inspectInstallationUnits(document.value === undefined
|
|
1576
|
+
? target.emptyValue()
|
|
1577
|
+
: document.value, agent, paths, index, configPaths.length));
|
|
1211
1578
|
}
|
|
1212
1579
|
catch {
|
|
1213
|
-
|
|
1580
|
+
invalidConfiguration = true;
|
|
1214
1581
|
}
|
|
1215
1582
|
}
|
|
1583
|
+
const invocation = await invocationHealth(config, paths, manifest);
|
|
1584
|
+
const stalePluginRuntime = agent === "opencode" &&
|
|
1585
|
+
invocation.error === "runtime_version_mismatch";
|
|
1586
|
+
const agentRuntime = target.runtimeRequired
|
|
1587
|
+
? runtime
|
|
1588
|
+
: stalePluginRuntime
|
|
1589
|
+
? {
|
|
1590
|
+
state: "stale",
|
|
1591
|
+
version: invocation.health.runtimeVersion,
|
|
1592
|
+
}
|
|
1593
|
+
: {
|
|
1594
|
+
state: "not_required",
|
|
1595
|
+
version: null,
|
|
1596
|
+
};
|
|
1597
|
+
const expected = target.expected * configPaths.length;
|
|
1598
|
+
const exactUnits = units.filter((unit) => unit.state === "exact").length;
|
|
1599
|
+
const installationState = invalidConfiguration
|
|
1600
|
+
? "invalid"
|
|
1601
|
+
: installed === 0
|
|
1602
|
+
? "missing"
|
|
1603
|
+
: agentRuntime.state === "stale"
|
|
1604
|
+
? "stale"
|
|
1605
|
+
: exactUnits === expected &&
|
|
1606
|
+
agentRuntime.state !== "missing"
|
|
1607
|
+
? "complete"
|
|
1608
|
+
: "partial";
|
|
1609
|
+
const executable = await target.executable(paths);
|
|
1216
1610
|
return {
|
|
1217
1611
|
agent,
|
|
1612
|
+
integrationId: manifest.integrationId,
|
|
1218
1613
|
configured: config?.agents.includes(agent) ?? false,
|
|
1219
|
-
executable
|
|
1614
|
+
executable,
|
|
1220
1615
|
configExists: existing === configPaths.length,
|
|
1221
1616
|
configFile: configPaths[0] ?? agentConfigPath(agent, paths),
|
|
1222
1617
|
...(configPaths.length > 1 ? { configFiles: configPaths } : {}),
|
|
1223
1618
|
integration: target.integration,
|
|
1224
1619
|
installed,
|
|
1225
|
-
expected
|
|
1620
|
+
expected,
|
|
1621
|
+
capabilities: manifest.capabilities,
|
|
1622
|
+
installation: {
|
|
1623
|
+
state: installationState,
|
|
1624
|
+
configured: config?.agents.includes(agent) ?? false,
|
|
1625
|
+
executable,
|
|
1626
|
+
installedUnits: exactUnits,
|
|
1627
|
+
expectedUnits: expected,
|
|
1628
|
+
units,
|
|
1629
|
+
runtime: agentRuntime.state,
|
|
1630
|
+
runtimeVersion: agentRuntime.version,
|
|
1631
|
+
expectedRuntimeVersion: target.runtimeRequired || stalePluginRuntime ? LORE_VERSION : null,
|
|
1632
|
+
issue: invalidConfiguration ? "invalid_configuration" : null,
|
|
1633
|
+
},
|
|
1634
|
+
invocation: invocation.health,
|
|
1635
|
+
invocationError: invocation.error,
|
|
1226
1636
|
};
|
|
1227
1637
|
}
|
|
1228
1638
|
async function statusData(paths) {
|
|
@@ -1235,28 +1645,25 @@ async function statusData(paths) {
|
|
|
1235
1645
|
// Missing configuration is represented as disconnected.
|
|
1236
1646
|
}
|
|
1237
1647
|
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
1241
|
-
: [
|
|
1242
|
-
access(paths.runtime, fsConstants.R_OK | fsConstants.X_OK),
|
|
1243
|
-
access(paths.runtimeRepository, fsConstants.R_OK),
|
|
1244
|
-
access(paths.runtimePackage, fsConstants.R_OK),
|
|
1245
|
-
]).then(() => true, () => false)
|
|
1246
|
-
: Promise.resolve(false);
|
|
1247
|
-
const [runtimeInstalled, queuedTurns, agents] = await Promise.all([
|
|
1248
|
-
runtimeInstalledCheck,
|
|
1648
|
+
const runtime = await inspectRuntimeInstallation(runtimeRequired, paths);
|
|
1649
|
+
const [legacyQueuedTurns, reliability, agents] = await Promise.all([
|
|
1249
1650
|
queueCount(paths),
|
|
1250
|
-
|
|
1651
|
+
inspectReliabilityStore(config, paths),
|
|
1652
|
+
Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths, runtime))),
|
|
1251
1653
|
]);
|
|
1252
1654
|
return {
|
|
1655
|
+
schemaVersion: "lore-cli-status-v1",
|
|
1656
|
+
integrationContractVersion: RELIABILITY_INTEGRATION_CONTRACT_VERSION,
|
|
1253
1657
|
connected: config !== null,
|
|
1254
1658
|
apiUrl: config?.apiUrl ?? null,
|
|
1255
1659
|
config: paths.config,
|
|
1256
1660
|
configMode,
|
|
1257
1661
|
runtimeRequired,
|
|
1258
|
-
runtimeInstalled,
|
|
1259
|
-
|
|
1662
|
+
runtimeInstalled: runtime.state === "installed",
|
|
1663
|
+
runtimeVersion: runtime.version,
|
|
1664
|
+
queuedTurns: legacyQueuedTurns + (reliability.inspection?.outbox.total ?? 0),
|
|
1665
|
+
reliability: reliability.inspection,
|
|
1666
|
+
reliabilityError: reliability.error,
|
|
1260
1667
|
agents,
|
|
1261
1668
|
};
|
|
1262
1669
|
}
|
|
@@ -1267,9 +1674,12 @@ async function statusCommand(args) {
|
|
|
1267
1674
|
}
|
|
1268
1675
|
const data = await statusData(getLorePaths());
|
|
1269
1676
|
const agentLines = data.agents
|
|
1270
|
-
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, ${agent.
|
|
1677
|
+
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, installation ${agent.installation.state}, invocation ${agent.invocation.state}${agent.invocationError === null ? "" : ` (${agent.invocationError})`}, capture ${agent.capabilities.captureDurability}, guard ${agent.capabilities.guardBoundaryCoverage}/${agent.capabilities.enforcement}`)
|
|
1271
1678
|
.join("\n");
|
|
1272
|
-
|
|
1679
|
+
const metricRate = (value) => value === null || value === undefined
|
|
1680
|
+
? "-"
|
|
1681
|
+
: `${(value * 100).toFixed(1)}%`;
|
|
1682
|
+
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n`);
|
|
1273
1683
|
}
|
|
1274
1684
|
async function disconnectCommand(args) {
|
|
1275
1685
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -1303,6 +1713,12 @@ async function disconnectCommand(args) {
|
|
|
1303
1713
|
rm(paths.config, { force: true }),
|
|
1304
1714
|
rm(paths.runtime, { force: true }),
|
|
1305
1715
|
rm(paths.runtimeRepository, { force: true }),
|
|
1716
|
+
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1717
|
+
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
1718
|
+
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1719
|
+
rm(paths.runtimeContextFallback, { force: true }),
|
|
1720
|
+
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
1721
|
+
rm(paths.runtimeVersion, { force: true }),
|
|
1306
1722
|
rm(paths.runtimePackage, { force: true }),
|
|
1307
1723
|
rm(paths.state, { recursive: true, force: true }),
|
|
1308
1724
|
rm(paths.queue, { recursive: true, force: true }),
|
|
@@ -1363,6 +1779,46 @@ async function apiChecks(config) {
|
|
|
1363
1779
|
: detail,
|
|
1364
1780
|
});
|
|
1365
1781
|
}
|
|
1782
|
+
try {
|
|
1783
|
+
const response = await fetch(`${config.apiUrl}/v1/reliability/metrics`, {
|
|
1784
|
+
headers: {
|
|
1785
|
+
authorization: `Bearer ${config.token}`,
|
|
1786
|
+
"user-agent": `lore-cli/${LORE_VERSION}`,
|
|
1787
|
+
},
|
|
1788
|
+
signal: AbortSignal.timeout(3_000),
|
|
1789
|
+
});
|
|
1790
|
+
if (!response.ok) {
|
|
1791
|
+
throw new Error("Operational metrics request failed");
|
|
1792
|
+
}
|
|
1793
|
+
const metrics = OperationalMetricsResponseSchema.parse(await response.json());
|
|
1794
|
+
const captureBacklog = metrics.captureReplay.captureExtraction.pending +
|
|
1795
|
+
metrics.captureReplay.captureExtraction.running;
|
|
1796
|
+
const embeddingBacklog = metrics.captureReplay.memoryEmbedding.pending +
|
|
1797
|
+
metrics.captureReplay.memoryEmbedding.running;
|
|
1798
|
+
const deadLetters = metrics.captureReplay.captureExtraction.dead +
|
|
1799
|
+
metrics.captureReplay.memoryEmbedding.dead;
|
|
1800
|
+
const detectedFailures = metrics.silentFailures.count +
|
|
1801
|
+
metrics.guard.failures +
|
|
1802
|
+
deadLetters;
|
|
1803
|
+
checks.push({
|
|
1804
|
+
name: "server-operational-metrics",
|
|
1805
|
+
status: metrics.silentFailures.count > 0 || deadLetters > 0
|
|
1806
|
+
? "error"
|
|
1807
|
+
: metrics.guard.failures > 0 ||
|
|
1808
|
+
captureBacklog > 0 ||
|
|
1809
|
+
embeddingBacklog > 0
|
|
1810
|
+
? "warning"
|
|
1811
|
+
: "ok",
|
|
1812
|
+
detail: `${detectedFailures} detected failure(s), ${captureBacklog} capture and ${embeddingBacklog} embedding job(s) in backlog; ${metrics.guard.latency.samples} Guard latency sample(s)`,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
catch {
|
|
1816
|
+
checks.push({
|
|
1817
|
+
name: "server-operational-metrics",
|
|
1818
|
+
status: "warning",
|
|
1819
|
+
detail: "authenticated operational metrics are unavailable",
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1366
1822
|
return checks;
|
|
1367
1823
|
}
|
|
1368
1824
|
async function doctorCommand(args) {
|
|
@@ -1415,14 +1871,80 @@ async function doctorCommand(args) {
|
|
|
1415
1871
|
});
|
|
1416
1872
|
checks.push({
|
|
1417
1873
|
name: `${agent.agent}-${agent.integration}`,
|
|
1418
|
-
status: agent.
|
|
1419
|
-
detail: `${agent.
|
|
1874
|
+
status: agent.installation.state === "complete" ? "ok" : "error",
|
|
1875
|
+
detail: `${agent.installation.state}: ${agent.installation.installedUnits}/${agent.installation.expectedUnits} exact Lore ${agent.integration} units`,
|
|
1876
|
+
});
|
|
1877
|
+
checks.push({
|
|
1878
|
+
name: `${agent.agent}-invocation`,
|
|
1879
|
+
status: agent.invocationError !== null
|
|
1880
|
+
? "error"
|
|
1881
|
+
: agent.invocation.state === "successful"
|
|
1882
|
+
? "ok"
|
|
1883
|
+
: agent.invocation.state === "failed" ||
|
|
1884
|
+
agent.invocation.state === "incomplete"
|
|
1885
|
+
? "error"
|
|
1886
|
+
: "warning",
|
|
1887
|
+
detail: agent.invocationError !== null
|
|
1888
|
+
? agent.invocationError === "runtime_version_mismatch"
|
|
1889
|
+
? `observed Lore ${agent.invocation.runtimeVersion}; expected ${LORE_VERSION}`
|
|
1890
|
+
: `health record unavailable (${agent.invocationError})`
|
|
1891
|
+
: agent.invocation.state === "successful"
|
|
1892
|
+
? `last completed ${agent.invocation.lastSuccessfulAt}`
|
|
1893
|
+
: agent.invocation.state === "failed"
|
|
1894
|
+
? `last invocation failed (${agent.invocation.failureCode})`
|
|
1895
|
+
: agent.invocation.state === "incomplete"
|
|
1896
|
+
? `last invocation did not complete (${agent.invocation.lastAttemptedAt})`
|
|
1897
|
+
: agent.invocation.state === "never_observed"
|
|
1898
|
+
? "installed, but no invocation has been observed yet"
|
|
1899
|
+
: "invocation health is unavailable",
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
checks.push({
|
|
1903
|
+
name: "reliability-store",
|
|
1904
|
+
status: status.reliabilityError !== null
|
|
1905
|
+
? "error"
|
|
1906
|
+
: status.reliability === null
|
|
1907
|
+
? "warning"
|
|
1908
|
+
: "ok",
|
|
1909
|
+
detail: status.reliabilityError ??
|
|
1910
|
+
status.reliability?.directory ??
|
|
1911
|
+
"not initialized",
|
|
1912
|
+
});
|
|
1913
|
+
if (status.reliability !== null) {
|
|
1914
|
+
checks.push({
|
|
1915
|
+
name: "durable-capture-outbox",
|
|
1916
|
+
status: status.reliability.outbox.authBlocked > 0 ||
|
|
1917
|
+
status.reliability.outbox.dead > 0
|
|
1918
|
+
? "error"
|
|
1919
|
+
: status.reliability.outbox.ready > 0 ||
|
|
1920
|
+
status.reliability.outbox.inFlight > 0
|
|
1921
|
+
? "warning"
|
|
1922
|
+
: "ok",
|
|
1923
|
+
detail: `${status.reliability.outbox.total} total; ${status.reliability.outbox.ready} ready, ${status.reliability.outbox.authBlocked} authentication-blocked, ${status.reliability.outbox.dead} dead-letter`,
|
|
1924
|
+
});
|
|
1925
|
+
checks.push({
|
|
1926
|
+
name: "verified-snapshot-cache",
|
|
1927
|
+
status: status.reliability.trustKeys > 0 ? "ok" : "warning",
|
|
1928
|
+
detail: `${status.reliability.cache.context} context, ${status.reliability.cache.policy} policy, ${status.reliability.trustKeys} pinned key(s)`,
|
|
1929
|
+
});
|
|
1930
|
+
const operational = status.reliability.operational;
|
|
1931
|
+
const detectedFailures = operational.guard.failures + operational.retrieval.failed;
|
|
1932
|
+
checks.push({
|
|
1933
|
+
name: "local-operational-metrics",
|
|
1934
|
+
status: detectedFailures > 0 ? "warning" : "ok",
|
|
1935
|
+
detail: `${detectedFailures} detected failure(s); Guard coverage ${operational.guard.coverageRate === null ? "not yet sampled" : `${(operational.guard.coverageRate * 100).toFixed(1)}%`}, context fallback ${operational.retrieval.fallbackRate === null ? "not yet sampled" : `${(operational.retrieval.fallbackRate * 100).toFixed(1)}%`}`,
|
|
1420
1936
|
});
|
|
1421
1937
|
}
|
|
1938
|
+
const incompleteInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "incomplete").length;
|
|
1939
|
+
const failedInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "failed").length;
|
|
1422
1940
|
checks.push({
|
|
1423
|
-
name: "
|
|
1424
|
-
status:
|
|
1425
|
-
|
|
1941
|
+
name: "integration-invocation-completion",
|
|
1942
|
+
status: incompleteInvocations > 0
|
|
1943
|
+
? "error"
|
|
1944
|
+
: failedInvocations > 0
|
|
1945
|
+
? "warning"
|
|
1946
|
+
: "ok",
|
|
1947
|
+
detail: `${incompleteInvocations} incomplete and ${failedInvocations} explicitly failed latest invocation(s); target 0`,
|
|
1426
1948
|
});
|
|
1427
1949
|
if (config !== null) {
|
|
1428
1950
|
checks.push(...(await apiChecks(config)));
|
|
@@ -1508,6 +2030,11 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
1508
2030
|
case "hook":
|
|
1509
2031
|
await runHook(commandArgs);
|
|
1510
2032
|
return;
|
|
2033
|
+
case "internal-health-write": {
|
|
2034
|
+
const encoded = commandArgs[0] ?? (await readInvocationHealthWriteInput());
|
|
2035
|
+
await runInvocationHealthWrite(encoded);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
1511
2038
|
default:
|
|
1512
2039
|
throw new Error(`Unknown command: ${command}\nTry: lore --help`);
|
|
1513
2040
|
}
|