akm-cli 0.9.10 → 0.9.12
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/CHANGELOG.md +226 -0
- package/STABILITY.md +28 -15
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +209 -78
- package/dist/commands/health/improve-metrics.js +12 -0
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +15 -7
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/eval-cases.js +9 -2
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +32 -4
- package/dist/commands/improve/loop-stages.js +13 -3
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +36 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/tasks/validate.js +186 -0
- package/dist/commands/url-checker.js +75 -16
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/bundle-id.js +7 -1
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +19 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-result.js +8 -0
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +124 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/indexer/search/search-source.js +3 -2
- package/dist/integrations/agent/engine-resolution.js +107 -4
- package/dist/integrations/agent/execution-lowering.js +15 -2
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/agent/runner-dispatch.js +16 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +10 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +2028 -1613
- package/dist/scripts/akm-migrate.js +2027 -1612
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +15 -1
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/scheduler-sync.js +51 -25
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/dispatch-redaction.js +21 -7
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +76 -150
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/integration/bundling-akm.md +1 -1
- package/docs/migration/v0.8-to-v0.9.md +32 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +105 -22
- package/docs/reference/configuration.md +12 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/tasks.md +8 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +11 -8
|
@@ -111,7 +111,10 @@ export const configCommand = defineGroupCommand({
|
|
|
111
111
|
},
|
|
112
112
|
}),
|
|
113
113
|
set: defineJsonCommand({
|
|
114
|
-
meta: {
|
|
114
|
+
meta: {
|
|
115
|
+
name: "set",
|
|
116
|
+
description: "Set a configuration value by key; prints the resulting config with ok: true",
|
|
117
|
+
},
|
|
115
118
|
args: {
|
|
116
119
|
key: {
|
|
117
120
|
type: "positional",
|
|
@@ -124,7 +127,7 @@ export const configCommand = defineGroupCommand({
|
|
|
124
127
|
// writes don't pollute their host's output stream.
|
|
125
128
|
silent: {
|
|
126
129
|
type: "boolean",
|
|
127
|
-
description: "Suppress the post-write config dump on stdout. Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
130
|
+
description: "Suppress the post-write config dump on stdout entirely (prints nothing; exit code is the status). Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
128
131
|
default: false,
|
|
129
132
|
},
|
|
130
133
|
},
|
|
@@ -136,12 +139,15 @@ export const configCommand = defineGroupCommand({
|
|
|
136
139
|
},
|
|
137
140
|
}),
|
|
138
141
|
unset: defineJsonCommand({
|
|
139
|
-
meta: {
|
|
142
|
+
meta: {
|
|
143
|
+
name: "unset",
|
|
144
|
+
description: "Unset an optional configuration key or whole embedding/engine section; prints the resulting config with ok: true",
|
|
145
|
+
},
|
|
140
146
|
args: {
|
|
141
147
|
key: { type: "positional", required: true, description: "Config key to unset" },
|
|
142
148
|
silent: {
|
|
143
149
|
type: "boolean",
|
|
144
|
-
description: "Suppress the post-write config dump on stdout.",
|
|
150
|
+
description: "Suppress the post-write config dump on stdout entirely (prints nothing; exit code is the status).",
|
|
145
151
|
default: false,
|
|
146
152
|
},
|
|
147
153
|
},
|
|
@@ -84,13 +84,20 @@ export function resolveEnvBinding(target, options = {}) {
|
|
|
84
84
|
const dangerous = keys.filter(isDangerousEnvKey);
|
|
85
85
|
if (dangerous.length > 0) {
|
|
86
86
|
const detail = `Env "${envRef}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
87
|
-
const decision = decideDangerousEnvInjection({
|
|
87
|
+
const decision = decideDangerousEnvInjection({
|
|
88
|
+
dangerousKeys: dangerous,
|
|
89
|
+
thirdParty: Boolean(source.registryId),
|
|
90
|
+
allowInsecure: options.allowInsecure,
|
|
91
|
+
});
|
|
88
92
|
if (decision === "block") {
|
|
89
93
|
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
90
|
-
` Review the file, then copy the values into a first-party env if you trust them
|
|
94
|
+
` Review the file, then copy the values into a first-party env if you trust them, ` +
|
|
95
|
+
`or pass --allow-insecure once you have.`, "INVALID_FLAG_VALUE");
|
|
91
96
|
}
|
|
92
97
|
if (decision === "warn") {
|
|
93
|
-
warn(
|
|
98
|
+
warn(options.allowInsecure && source.registryId
|
|
99
|
+
? `${detail} Injecting anyway (--allow-insecure).`
|
|
100
|
+
: `${detail} Injecting anyway (first-party stash).`);
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
// Audit trail: keys only, never values.
|
|
@@ -255,6 +255,7 @@ async function runEnvInjected(target, opts) {
|
|
|
255
255
|
const { values: envValues } = resolveEnvBinding(target, {
|
|
256
256
|
only: opts.only,
|
|
257
257
|
except: opts.except,
|
|
258
|
+
allowInsecure: opts.allowInsecure,
|
|
258
259
|
});
|
|
259
260
|
const mergedEnv = buildChildEnv(process.env, {
|
|
260
261
|
clean: opts.clean === true,
|
|
@@ -323,6 +324,11 @@ const envRunCommand = defineJsonCommand({
|
|
|
323
324
|
type: "string",
|
|
324
325
|
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
325
326
|
},
|
|
327
|
+
"allow-insecure": {
|
|
328
|
+
type: "boolean",
|
|
329
|
+
description: "Allow injecting a process-hijacking variable (e.g. LD_PRELOAD, GIT_SSH_COMMAND) from a third-party stash, which otherwise blocks. Use only after explicitly reviewing the env file.",
|
|
330
|
+
default: false,
|
|
331
|
+
},
|
|
326
332
|
},
|
|
327
333
|
async run({ args }) {
|
|
328
334
|
await runEnvInjected(args.target, {
|
|
@@ -330,6 +336,7 @@ const envRunCommand = defineJsonCommand({
|
|
|
330
336
|
except: parseKeyListFlag(args.except),
|
|
331
337
|
clean: args.clean === true,
|
|
332
338
|
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
339
|
+
allowInsecure: args["allow-insecure"] === true,
|
|
333
340
|
});
|
|
334
341
|
},
|
|
335
342
|
});
|
|
@@ -32,11 +32,13 @@ import path from "node:path";
|
|
|
32
32
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
33
33
|
import { getStringArg } from "../../cli/parse-args.js";
|
|
34
34
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
35
|
+
import { decideDangerousEnvInjection } from "../../core/activation-policy.js";
|
|
35
36
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-placement.js";
|
|
36
37
|
import { loadConfig } from "../../core/config/config.js";
|
|
37
38
|
import { makeSecretRef, resolveSecretPath, resolveSecretWriteTarget, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
38
39
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
39
40
|
import { appendEvent } from "../../core/events.js";
|
|
41
|
+
import { warn } from "../../core/warn.js";
|
|
40
42
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
41
43
|
import { readStdin } from "../../runtime.js";
|
|
42
44
|
import { buildChildEnv } from "./child-env.js";
|
|
@@ -176,10 +178,6 @@ const secretRunCommand = defineJsonCommand({
|
|
|
176
178
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
177
179
|
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
178
180
|
}
|
|
179
|
-
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
180
|
-
if (isDangerousEnvKey(varName)) {
|
|
181
|
-
throw new UsageError(`Refusing to inject a secret into "${varName}": it is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`, "INVALID_FLAG_VALUE");
|
|
182
|
-
}
|
|
183
181
|
const command = getParsedInvocation().passthroughArgs();
|
|
184
182
|
if (command.length === 0) {
|
|
185
183
|
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
@@ -188,6 +186,19 @@ const secretRunCommand = defineJsonCommand({
|
|
|
188
186
|
if (!fs.existsSync(absPath)) {
|
|
189
187
|
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
190
188
|
}
|
|
189
|
+
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
190
|
+
if (isDangerousEnvKey(varName)) {
|
|
191
|
+
const detail = `"${varName}" is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`;
|
|
192
|
+
const decision = decideDangerousEnvInjection({
|
|
193
|
+
dangerousKeys: [varName],
|
|
194
|
+
thirdParty: Boolean(source.registryId),
|
|
195
|
+
});
|
|
196
|
+
if (decision === "block") {
|
|
197
|
+
throw new UsageError(`Refusing to inject a secret from a third-party stash into ${detail}\n` +
|
|
198
|
+
" Review the secret, then copy it into a first-party secret if you trust it.", "INVALID_FLAG_VALUE");
|
|
199
|
+
}
|
|
200
|
+
warn(`Injecting a secret into ${detail} Injecting anyway (first-party stash).`);
|
|
201
|
+
}
|
|
191
202
|
const { readValue } = await import("./secret.js");
|
|
192
203
|
const mergedEnv = buildChildEnv(process.env, {
|
|
193
204
|
clean: args.clean === true,
|
|
@@ -4,17 +4,56 @@
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { loadConfig } from "../../core/config/config.js";
|
|
6
6
|
import { ConfigError } from "../../core/errors.js";
|
|
7
|
+
import { EXTRACT_INFRASTRUCTURE_SKIP_REASONS } from "../../core/improve-types.js";
|
|
7
8
|
import { listPendingStateMigrations } from "../../core/state-db.js";
|
|
8
9
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
9
|
-
import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
|
|
10
|
+
import { lookupApiKeyFileValue, resolveEngine } from "../../integrations/agent/engine-resolution.js";
|
|
10
11
|
import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution-definitions.js";
|
|
11
12
|
import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
|
|
12
13
|
import { resolveImprovePlan } from "../improve/improve-strategies.js";
|
|
13
14
|
import { ACTIVE_RUN_WARN_MS, TASK_FAIL_RATE_WARN } from "./types.js";
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
/** Probe one connection's reachability, once per endpoint; `undefined` when no probe seam is supplied. */
|
|
16
|
+
function probeConnectionReachable(connection, deps, cache) {
|
|
17
|
+
if (!deps.probeReachable)
|
|
18
|
+
return Promise.resolve(undefined);
|
|
19
|
+
const key = connection.endpoint.replace(/\/+$/, "");
|
|
20
|
+
let pending = cache.get(key);
|
|
21
|
+
if (!pending) {
|
|
22
|
+
pending = deps.probeReachable(connection);
|
|
23
|
+
cache.set(key, pending);
|
|
24
|
+
}
|
|
25
|
+
return pending;
|
|
26
|
+
}
|
|
27
|
+
function reachabilityEvidence(reach) {
|
|
28
|
+
return reach
|
|
29
|
+
? { probed: true, reachable: reach.reachable, ...(reach.error ? { error: reach.error } : {}) }
|
|
30
|
+
: { probed: false, reachable: null };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* #914: an unreachable LLM engine is a hard `fail` when it is the resolved
|
|
34
|
+
* `default-llm-engine` (extraction and improve depend on it) but only a
|
|
35
|
+
* `warn` elsewhere. Only a reachability warn escalates; a credential warn
|
|
36
|
+
* stays a warn.
|
|
37
|
+
*/
|
|
38
|
+
function escalateDefaultLlmEngineFailure(result) {
|
|
39
|
+
if (result.status === "warn" && result.evidence?.reachable === false) {
|
|
40
|
+
return { ...result, status: "fail" };
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/** Rolling window the `session-extraction` check reads from the extract ledger (#914). */
|
|
45
|
+
export const SESSION_EXTRACTION_LEDGER_WINDOW_DAYS = 7;
|
|
46
|
+
function credentialAvailable(credential, env, apiKeyFile) {
|
|
47
|
+
if (credential?.required)
|
|
48
|
+
return credential.names.some((name) => Boolean(env[name]?.trim()));
|
|
49
|
+
// #905: an engine with no env descriptor may still require a file-backed
|
|
50
|
+
// credential — probe it too, rather than reporting an unreadable/empty
|
|
51
|
+
// apiKeyFile as available just because it carries no env var names.
|
|
52
|
+
if (apiKeyFile !== undefined)
|
|
53
|
+
return lookupApiKeyFileValue(apiKeyFile) !== undefined;
|
|
54
|
+
return true;
|
|
16
55
|
}
|
|
17
|
-
function runConfiguredEngineProbe(checkName, engineName, config, deps) {
|
|
56
|
+
async function runConfiguredEngineProbe(checkName, engineName, config, deps, reachabilityCache) {
|
|
18
57
|
if (!engineName) {
|
|
19
58
|
return {
|
|
20
59
|
name: checkName,
|
|
@@ -57,6 +96,7 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
|
|
|
57
96
|
const fallbackEngine = configuredEngine.llmEngine ?? config.defaults?.llmEngine;
|
|
58
97
|
let fallback;
|
|
59
98
|
let fallbackCredential;
|
|
99
|
+
let fallbackApiKeyFile;
|
|
60
100
|
let sdkRunner;
|
|
61
101
|
const resolve = deps.resolveEngine ?? resolveEngine;
|
|
62
102
|
try {
|
|
@@ -70,6 +110,7 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
|
|
|
70
110
|
if (sdkRunner?.fallbackConnection && fallbackEngine) {
|
|
71
111
|
fallback = { kind: "llm", engine: fallbackEngine, connection: sdkRunner.fallbackConnection };
|
|
72
112
|
fallbackCredential = sdkRunner.fallbackCredential;
|
|
113
|
+
fallbackApiKeyFile = sdkRunner.fallbackApiKeyFile;
|
|
73
114
|
}
|
|
74
115
|
else if (fallbackEngine) {
|
|
75
116
|
try {
|
|
@@ -77,6 +118,7 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
|
|
|
77
118
|
if (resolved.kind === "llm") {
|
|
78
119
|
fallback = resolved;
|
|
79
120
|
fallbackCredential = resolved.credential;
|
|
121
|
+
fallbackApiKeyFile = resolved.apiKeyFile;
|
|
80
122
|
}
|
|
81
123
|
}
|
|
82
124
|
catch {
|
|
@@ -85,59 +127,91 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
|
|
|
85
127
|
}
|
|
86
128
|
const configuredModel = configuredEngine.model;
|
|
87
129
|
const effectiveModel = sdkRunner?.profile.model ?? configuredModel ?? fallback?.connection.model;
|
|
88
|
-
const fallbackCredentialAvailable = credentialAvailable(fallbackCredential, env);
|
|
130
|
+
const fallbackCredentialAvailable = credentialAvailable(fallbackCredential, env, fallbackApiKeyFile);
|
|
89
131
|
const missing = [
|
|
90
132
|
!packageAvailable ? "@opencode-ai/sdk package" : undefined,
|
|
91
133
|
!binaryAvailable ? `${binary} binary` : undefined,
|
|
92
134
|
fallbackEngine && !fallback ? "configured fallback LLM connection" : undefined,
|
|
93
135
|
!fallbackCredentialAvailable ? "required fallback credential" : undefined,
|
|
94
136
|
].filter((value) => value !== undefined);
|
|
137
|
+
const sdkEvidence = {
|
|
138
|
+
engine: engineName,
|
|
139
|
+
platform: configuredEngine.platform,
|
|
140
|
+
runtimeKind: "sdk",
|
|
141
|
+
binary,
|
|
142
|
+
binaryAvailable,
|
|
143
|
+
package: "@opencode-ai/sdk",
|
|
144
|
+
packageAvailable,
|
|
145
|
+
model: effectiveModel ?? null,
|
|
146
|
+
configuredModel: configuredModel ?? null,
|
|
147
|
+
modelSource: configuredModel ? "sdk" : effectiveModel ? "fallback" : null,
|
|
148
|
+
fallbackEngine: fallbackEngine ?? null,
|
|
149
|
+
fallbackEndpoint: fallback?.connection.endpoint ?? null,
|
|
150
|
+
fallbackModel: fallback?.connection.model ?? null,
|
|
151
|
+
requiredCredentialAvailable: fallbackCredentialAvailable,
|
|
152
|
+
};
|
|
153
|
+
if (missing.length > 0) {
|
|
154
|
+
return {
|
|
155
|
+
name: checkName,
|
|
156
|
+
kind: "deterministic",
|
|
157
|
+
status: "warn",
|
|
158
|
+
confidence: "high",
|
|
159
|
+
message: `SDK engine "${engineName}" is incomplete: missing ${missing.join(", ")}.`,
|
|
160
|
+
evidence: sdkEvidence,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// The SDK's LLM fallback is the only endpoint this engine talks to;
|
|
164
|
+
// probe it once every local precondition above is satisfied (#914).
|
|
165
|
+
const reach = fallback ? await probeConnectionReachable(fallback.connection, deps, reachabilityCache) : undefined;
|
|
95
166
|
return {
|
|
96
167
|
name: checkName,
|
|
97
168
|
kind: "deterministic",
|
|
98
|
-
status:
|
|
169
|
+
status: reach && !reach.reachable ? "warn" : "pass",
|
|
99
170
|
confidence: "high",
|
|
100
|
-
message:
|
|
171
|
+
message: !fallback
|
|
101
172
|
? `SDK engine "${engineName}" is available.`
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
binaryAvailable,
|
|
109
|
-
package: "@opencode-ai/sdk",
|
|
110
|
-
packageAvailable,
|
|
111
|
-
model: effectiveModel ?? null,
|
|
112
|
-
configuredModel: configuredModel ?? null,
|
|
113
|
-
modelSource: configuredModel ? "sdk" : effectiveModel ? "fallback" : null,
|
|
114
|
-
fallbackEngine: fallbackEngine ?? null,
|
|
115
|
-
fallbackEndpoint: fallback?.connection.endpoint ?? null,
|
|
116
|
-
fallbackModel: fallback?.connection.model ?? null,
|
|
117
|
-
requiredCredentialAvailable: fallbackCredentialAvailable,
|
|
118
|
-
},
|
|
173
|
+
: !reach
|
|
174
|
+
? `SDK engine "${engineName}" is available. Reachability was not probed.`
|
|
175
|
+
: reach.reachable
|
|
176
|
+
? `SDK engine "${engineName}" is available and its LLM fallback is reachable.`
|
|
177
|
+
: `SDK engine "${engineName}" is available, but its LLM fallback is not reachable: ${reach.error ?? "unknown error"}`,
|
|
178
|
+
evidence: fallback ? { ...sdkEvidence, ...reachabilityEvidence(reach) } : sdkEvidence,
|
|
119
179
|
};
|
|
120
180
|
}
|
|
121
181
|
try {
|
|
122
182
|
const runner = (deps.resolveEngine ?? resolveEngine)(engineName, config);
|
|
123
183
|
if (runner.kind === "llm") {
|
|
124
|
-
const requiredCredentialAvailable = credentialAvailable(runner.credential, env);
|
|
184
|
+
const requiredCredentialAvailable = credentialAvailable(runner.credential, env, runner.apiKeyFile);
|
|
185
|
+
const llmEvidence = {
|
|
186
|
+
engine: engineName,
|
|
187
|
+
platform: null,
|
|
188
|
+
runtimeKind: "llm",
|
|
189
|
+
model: runner.connection.model,
|
|
190
|
+
endpoint: runner.connection.endpoint,
|
|
191
|
+
requiredCredentialAvailable,
|
|
192
|
+
};
|
|
193
|
+
if (!requiredCredentialAvailable) {
|
|
194
|
+
return {
|
|
195
|
+
name: checkName,
|
|
196
|
+
kind: "deterministic",
|
|
197
|
+
status: "warn",
|
|
198
|
+
confidence: "high",
|
|
199
|
+
message: `LLM engine "${engineName}" is configured, but its required credential is unavailable.`,
|
|
200
|
+
evidence: llmEvidence,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const reach = await probeConnectionReachable(runner.connection, deps, reachabilityCache);
|
|
125
204
|
return {
|
|
126
205
|
name: checkName,
|
|
127
206
|
kind: "deterministic",
|
|
128
|
-
status:
|
|
207
|
+
status: reach && !reach.reachable ? "warn" : "pass",
|
|
129
208
|
confidence: "high",
|
|
130
|
-
message:
|
|
131
|
-
? `LLM engine "${engineName}" is configured.`
|
|
132
|
-
:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
runtimeKind: "llm",
|
|
137
|
-
model: runner.connection.model,
|
|
138
|
-
endpoint: runner.connection.endpoint,
|
|
139
|
-
requiredCredentialAvailable,
|
|
140
|
-
},
|
|
209
|
+
message: !reach
|
|
210
|
+
? `LLM engine "${engineName}" is configured. Reachability was not probed.`
|
|
211
|
+
: reach.reachable
|
|
212
|
+
? `LLM engine "${engineName}" is configured and reachable.`
|
|
213
|
+
: `LLM engine "${engineName}" is not reachable: ${reach.error ?? "unknown error"}`,
|
|
214
|
+
evidence: { ...llmEvidence, ...reachabilityEvidence(reach) },
|
|
141
215
|
};
|
|
142
216
|
}
|
|
143
217
|
const profile = runner.profile;
|
|
@@ -188,8 +262,10 @@ function projectEngineProbe(result, name) {
|
|
|
188
262
|
}
|
|
189
263
|
function projectSelectedEngineProbe(availability, engineName, checkName) {
|
|
190
264
|
const result = availability.get(engineName);
|
|
191
|
-
if (result)
|
|
192
|
-
|
|
265
|
+
if (result) {
|
|
266
|
+
const projected = projectEngineProbe(result, checkName);
|
|
267
|
+
return checkName === "default-llm-engine" ? escalateDefaultLlmEngineFailure(projected) : projected;
|
|
268
|
+
}
|
|
193
269
|
return {
|
|
194
270
|
name: checkName,
|
|
195
271
|
kind: "deterministic",
|
|
@@ -235,30 +311,36 @@ function configuredEnginesProjection(engineNames, availability) {
|
|
|
235
311
|
evidence: { engines },
|
|
236
312
|
};
|
|
237
313
|
}
|
|
238
|
-
export function runDefaultEngineProbe(deps = {}) {
|
|
314
|
+
export async function runDefaultEngineProbe(deps = {}) {
|
|
239
315
|
// Probe the effective view: an install with no `defaults.engine` but a usable
|
|
240
316
|
// opencode binary DOES have a working default, and reporting otherwise would
|
|
241
317
|
// contradict what `workflow run` / `task run` actually do.
|
|
242
318
|
const { config } = withEngineFallback(deps.loadConfig?.() ?? loadConfig(), deps.which);
|
|
243
|
-
return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps);
|
|
319
|
+
return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps, new Map());
|
|
244
320
|
}
|
|
245
|
-
export function runDefaultLlmEngineProbe(deps = {}) {
|
|
321
|
+
export async function runDefaultLlmEngineProbe(deps = {}) {
|
|
246
322
|
const config = deps.loadConfig?.() ?? loadConfig();
|
|
247
|
-
|
|
323
|
+
const engineName = config.defaults?.llmEngine;
|
|
324
|
+
if (!engineName)
|
|
325
|
+
return unconfiguredEngineProbe("default-llm-engine");
|
|
326
|
+
const result = await runConfiguredEngineProbe("configured-engine", engineName, config, deps, new Map());
|
|
327
|
+
return projectSelectedEngineProbe(new Map([[engineName, result]]), engineName, "default-llm-engine");
|
|
248
328
|
}
|
|
249
329
|
/** Probe every explicitly configured engine without exposing connection or model material. */
|
|
250
|
-
export function runConfiguredEnginesProbe(deps = {}) {
|
|
330
|
+
export async function runConfiguredEnginesProbe(deps = {}) {
|
|
251
331
|
const config = deps.loadConfig?.() ?? loadConfig();
|
|
252
332
|
const engineNames = Object.keys(config.engines ?? {}).sort();
|
|
253
|
-
const
|
|
333
|
+
const cache = new Map();
|
|
334
|
+
const availability = new Map(await Promise.all(engineNames.map(async (engine) => [engine, await runConfiguredEngineProbe("configured-engine", engine, config, deps, cache)])));
|
|
254
335
|
return configuredEnginesProjection(engineNames, availability);
|
|
255
336
|
}
|
|
256
337
|
/**
|
|
257
338
|
* Build the per-health-run availability snapshot. Each distinct selected or
|
|
258
|
-
* explicitly configured engine is probed once
|
|
259
|
-
*
|
|
339
|
+
* explicitly configured engine is probed once (reachability probes further
|
|
340
|
+
* de-duped by endpoint, see {@link ReachabilityCache}); the three public
|
|
341
|
+
* checks are projections of that immutable result set.
|
|
260
342
|
*/
|
|
261
|
-
export function runHealthEngineProbes(deps = {}) {
|
|
343
|
+
export async function runHealthEngineProbes(deps = {}) {
|
|
262
344
|
const configured = deps.loadConfig?.() ?? loadConfig();
|
|
263
345
|
const effective = withEngineFallback(configured, deps.which).config;
|
|
264
346
|
const defaultEngineName = effective.defaults?.engine;
|
|
@@ -267,7 +349,8 @@ export function runHealthEngineProbes(deps = {}) {
|
|
|
267
349
|
const probeNames = [...new Set([...explicitEngineNames, defaultEngineName, defaultLlmEngineName])]
|
|
268
350
|
.filter((name) => name !== undefined)
|
|
269
351
|
.sort();
|
|
270
|
-
const
|
|
352
|
+
const cache = new Map();
|
|
353
|
+
const availability = new Map(await Promise.all(probeNames.map(async (engine) => [engine, await runConfiguredEngineProbe("configured-engine", engine, effective, deps, cache)])));
|
|
271
354
|
return Object.freeze({
|
|
272
355
|
defaultEngine: defaultEngineName
|
|
273
356
|
? projectSelectedEngineProbe(availability, defaultEngineName, "default-engine")
|
|
@@ -287,7 +370,7 @@ export function runActiveImproveStrategyProbe(deps = {}) {
|
|
|
287
370
|
const unavailableProcesses = Object.entries(plan.processes).flatMap(([name, process]) => {
|
|
288
371
|
if (!process.enabled || !process.runner)
|
|
289
372
|
return [];
|
|
290
|
-
return credentialAvailable(process.runner.credential, env) ? [] : [name];
|
|
373
|
+
return credentialAvailable(process.runner.credential, env, process.runner.apiKeyFile) ? [] : [name];
|
|
291
374
|
});
|
|
292
375
|
if (plan.triageJudgment) {
|
|
293
376
|
const judgmentCredential = plan.triageJudgment.kind === "llm"
|
|
@@ -295,20 +378,41 @@ export function runActiveImproveStrategyProbe(deps = {}) {
|
|
|
295
378
|
: plan.triageJudgment.kind === "sdk"
|
|
296
379
|
? plan.triageJudgment.fallbackCredential
|
|
297
380
|
: undefined;
|
|
298
|
-
|
|
381
|
+
const judgmentApiKeyFile = plan.triageJudgment.kind === "llm"
|
|
382
|
+
? plan.triageJudgment.apiKeyFile
|
|
383
|
+
: plan.triageJudgment.kind === "sdk"
|
|
384
|
+
? plan.triageJudgment.fallbackApiKeyFile
|
|
385
|
+
: undefined;
|
|
386
|
+
if (!credentialAvailable(judgmentCredential, env, judgmentApiKeyFile)) {
|
|
299
387
|
unavailableProcesses.push("triage.judgment");
|
|
388
|
+
}
|
|
300
389
|
}
|
|
390
|
+
// #913: name the engine each process actually resolved to, so a
|
|
391
|
+
// strategy-level `engine` pin that shadows `defaults.llmEngine` is
|
|
392
|
+
// visible on `akm health` instead of requiring config archaeology.
|
|
393
|
+
const engines = {};
|
|
394
|
+
for (const [name, process] of Object.entries(plan.processes)) {
|
|
395
|
+
if (process.enabled && process.runner)
|
|
396
|
+
engines[name] = process.runner.engine;
|
|
397
|
+
}
|
|
398
|
+
if (plan.triageJudgment)
|
|
399
|
+
engines["triage.judgment"] = plan.triageJudgment.engine;
|
|
400
|
+
const engineList = Object.entries(engines)
|
|
401
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
402
|
+
.map(([process, engine]) => `${process}: "${engine}"`)
|
|
403
|
+
.join(", ");
|
|
301
404
|
return {
|
|
302
405
|
name: "active-improve-strategy",
|
|
303
406
|
kind: "deterministic",
|
|
304
407
|
status: unavailableProcesses.length === 0 ? "pass" : "warn",
|
|
305
408
|
confidence: "high",
|
|
306
409
|
message: unavailableProcesses.length === 0
|
|
307
|
-
? `Active improve strategy "${plan.strategy.name}" has available process engines.`
|
|
308
|
-
: `Active improve strategy "${plan.strategy.name}" has unavailable required credentials for: ${unavailableProcesses.join(", ")}.`,
|
|
410
|
+
? `Active improve strategy "${plan.strategy.name}" has available process engines${engineList ? ` (${engineList})` : ""}.`
|
|
411
|
+
: `Active improve strategy "${plan.strategy.name}" has unavailable required credentials for: ${unavailableProcesses.join(", ")}${engineList ? ` (engines: ${engineList})` : ""}.`,
|
|
309
412
|
evidence: {
|
|
310
413
|
strategy: plan.strategy.name,
|
|
311
414
|
unavailableProcesses,
|
|
415
|
+
engines,
|
|
312
416
|
},
|
|
313
417
|
};
|
|
314
418
|
}
|
|
@@ -421,8 +525,11 @@ export function runSelectedModelAliasesProbe(deps = {}) {
|
|
|
421
525
|
const outcomes = selected.map(({ engine, alias, modelMapKey }) => {
|
|
422
526
|
try {
|
|
423
527
|
const resolution = resolveModelMapAlias(alias, modelMapKey, modelMap.map);
|
|
424
|
-
|
|
425
|
-
|
|
528
|
+
if (resolution.interpretation === "alias") {
|
|
529
|
+
return { kind: "alias", evidence: { engine, alias, modelMapKey } };
|
|
530
|
+
}
|
|
531
|
+
return resolution.unmappedForEngine
|
|
532
|
+
? { kind: "missing", evidence: { engine, alias, modelMapKey } }
|
|
426
533
|
: { kind: "exact" };
|
|
427
534
|
}
|
|
428
535
|
catch {
|
|
@@ -648,34 +755,58 @@ export const HEALTH_CHECKS = [
|
|
|
648
755
|
},
|
|
649
756
|
},
|
|
650
757
|
{
|
|
758
|
+
// #914: derived from the `extract_sessions_seen` LEDGER, not
|
|
759
|
+
// `improve_runs.result_json` — the hook-driven `akm proposal extract
|
|
760
|
+
// --session-id ...` (the standard Claude Code plugin setup) never writes
|
|
761
|
+
// an `improve_runs` row, so the old improve_runs-only source reported
|
|
762
|
+
// "not active" as `pass` unconditionally on a plugin-driven machine, no
|
|
763
|
+
// matter how healthy or broken extraction actually was.
|
|
651
764
|
name: "session-extraction",
|
|
652
765
|
channel: "advisory",
|
|
653
766
|
run: (ctx) => {
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
sxWarnReasons.push(`${sx.warnings} harness error(s)`);
|
|
658
|
-
if (sx.ran && sx.sessionsScanned >= 5 && sx.proposalsCreated === 0)
|
|
659
|
-
sxWarnReasons.push("no proposals generated across scanned sessions");
|
|
660
|
-
return {
|
|
767
|
+
const { since: ledgerSince, rows } = ctx.sessionExtractionLedger;
|
|
768
|
+
const days = SESSION_EXTRACTION_LEDGER_WINDOW_DAYS;
|
|
769
|
+
const base = {
|
|
661
770
|
name: "session-extraction",
|
|
662
771
|
kind: "heuristic",
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
772
|
+
confidence: "medium",
|
|
773
|
+
evidence: { ledgerSince, ledgerRows: rows },
|
|
774
|
+
};
|
|
775
|
+
const totalRows = rows.reduce((sum, row) => sum + row.count, 0);
|
|
776
|
+
if (totalRows === 0) {
|
|
777
|
+
return { ...base, status: "unknown", message: `No extraction recorded in the last ${days} days.` };
|
|
778
|
+
}
|
|
779
|
+
// `read_failed` / `exception` land in the ledger as `failed`, not `skipped`.
|
|
780
|
+
const nothingHarvested = rows.every((row) => row.outcome === "skipped" || row.outcome === "failed");
|
|
781
|
+
const knownSkipReasons = [...new Set(rows.flatMap((row) => (row.skipReason ? [row.skipReason] : [])))].sort();
|
|
782
|
+
const allKnownReasonsAreInfra = knownSkipReasons.every((reason) => EXTRACT_INFRASTRUCTURE_SKIP_REASONS.includes(reason));
|
|
783
|
+
if (nothingHarvested && allKnownReasonsAreInfra) {
|
|
784
|
+
const reasonClauses = knownSkipReasons.map((reason) => {
|
|
785
|
+
const engines = [
|
|
786
|
+
...new Set(rows.flatMap((row) => (row.skipReason === reason && row.engine ? [row.engine] : []))),
|
|
787
|
+
].sort();
|
|
788
|
+
return engines.length > 0 ? `${reason} (engine ${engines.map((e) => `"${e}"`).join(", ")})` : reason;
|
|
789
|
+
});
|
|
790
|
+
if (rows.some((row) => row.skipReason === null))
|
|
791
|
+
reasonClauses.push("unknown reason");
|
|
792
|
+
const verb = rows.some((row) => row.outcome === "failed") ? "skipped or failed" : "skipped";
|
|
793
|
+
return {
|
|
794
|
+
...base,
|
|
795
|
+
status: "warn",
|
|
796
|
+
message: `${totalRows} of ${totalRows} sessions in the last ${days} days ${verb}: ${reasonClauses.join(", ")}.`,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const outcomeCounts = new Map();
|
|
800
|
+
for (const row of rows)
|
|
801
|
+
outcomeCounts.set(row.outcome, (outcomeCounts.get(row.outcome) ?? 0) + row.count);
|
|
802
|
+
const outcomeSummary = [...outcomeCounts.entries()]
|
|
803
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
804
|
+
.map(([outcome, count]) => `${outcome}: ${count}`)
|
|
805
|
+
.join(", ");
|
|
806
|
+
return {
|
|
807
|
+
...base,
|
|
808
|
+
status: "pass",
|
|
809
|
+
message: `Session extraction active in the last ${days} days (${outcomeSummary}).`,
|
|
679
810
|
};
|
|
680
811
|
},
|
|
681
812
|
},
|
|
@@ -76,6 +76,9 @@ function createUnknownImproveMetrics() {
|
|
|
76
76
|
coverageGapCount: 0,
|
|
77
77
|
evalCasesWritten: 0,
|
|
78
78
|
deadUrlCount: 0,
|
|
79
|
+
deadUrlsChecked: 0,
|
|
80
|
+
deadUrlsTotal: 0,
|
|
81
|
+
deadUrlsSkipped: 0,
|
|
79
82
|
memorySummary: { eligible: 0, derived: 0 },
|
|
80
83
|
memoryCleanup: {
|
|
81
84
|
pruneCandidates: 0,
|
|
@@ -333,6 +336,12 @@ function applyMiscCounters(metrics, result) {
|
|
|
333
336
|
metrics.evalCasesWritten += toFiniteNumber(result.evalCasesWritten);
|
|
334
337
|
if (Array.isArray(result.deadUrls))
|
|
335
338
|
metrics.deadUrlCount += result.deadUrls.length;
|
|
339
|
+
const deadUrlCoverage = result.deadUrlCoverage;
|
|
340
|
+
if (deadUrlCoverage && typeof deadUrlCoverage === "object") {
|
|
341
|
+
metrics.deadUrlsChecked += toFiniteNumber(deadUrlCoverage.checked);
|
|
342
|
+
metrics.deadUrlsTotal += toFiniteNumber(deadUrlCoverage.total);
|
|
343
|
+
metrics.deadUrlsSkipped += toFiniteNumber(deadUrlCoverage.skipped);
|
|
344
|
+
}
|
|
336
345
|
}
|
|
337
346
|
function applyMemorySummary(metrics, result) {
|
|
338
347
|
const memorySummary = result.memorySummary;
|
|
@@ -568,6 +577,9 @@ function mergeImproveMetrics(dst, src) {
|
|
|
568
577
|
dst.coverageGapCount += src.coverageGapCount;
|
|
569
578
|
dst.evalCasesWritten += src.evalCasesWritten;
|
|
570
579
|
dst.deadUrlCount += src.deadUrlCount;
|
|
580
|
+
dst.deadUrlsChecked += src.deadUrlsChecked;
|
|
581
|
+
dst.deadUrlsTotal += src.deadUrlsTotal;
|
|
582
|
+
dst.deadUrlsSkipped += src.deadUrlsSkipped;
|
|
571
583
|
// NOTE: memorySummary (derived/eligible) is a WHOLE-STASH snapshot recorded on
|
|
572
584
|
// every run, NOT a per-run increment — summing it across the window inflates
|
|
573
585
|
// it ~N× (the 1.2M-eligible bug). It is set from the most recent run in
|