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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { deepMergeConfig } from "../../core/config/deep-merge.js";
|
|
6
8
|
import { ConfigError } from "../../core/errors.js";
|
|
7
9
|
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
8
10
|
import { collectSensitiveValues } from "../../core/redaction.js";
|
|
11
|
+
import { warn } from "../../core/warn.js";
|
|
9
12
|
import { getHarness } from "../harnesses/index.js";
|
|
10
13
|
import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
|
|
11
14
|
import { getBuiltinAgentProfile } from "./profiles.js";
|
|
@@ -22,6 +25,51 @@ function envName(reference) {
|
|
|
22
25
|
const match = /^\$(?:\{)?([A-Za-z_][A-Za-z0-9_]*)(?:\})?$/.exec(reference);
|
|
23
26
|
return match?.[1];
|
|
24
27
|
}
|
|
28
|
+
/** Expand a leading `~` the same way `loadSetupConfigFromFile` does for `--from <file>`. */
|
|
29
|
+
function expandHomePath(filePath) {
|
|
30
|
+
return filePath.startsWith("~") ? path.join(os.homedir(), filePath.slice(1)) : filePath;
|
|
31
|
+
}
|
|
32
|
+
/** Trim exactly one trailing newline (`\n` or `\r\n`) — never interior whitespace. */
|
|
33
|
+
function trimTrailingNewline(raw) {
|
|
34
|
+
return raw.replace(/\r?\n$/, "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read a file-backed credential (#905) at the dispatch boundary. Never
|
|
38
|
+
* includes the file's content in a thrown message — only the engine name and
|
|
39
|
+
* path, so a misconfigured `apiKeyFile` cannot leak its (partial) contents
|
|
40
|
+
* into a log or error report.
|
|
41
|
+
*/
|
|
42
|
+
function readApiKeyFile(engineName, filePath) {
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const code = err instanceof Error ? err.code : undefined;
|
|
49
|
+
const reason = code === "ENOENT" ? "does not exist" : "could not be read";
|
|
50
|
+
throw new ConfigError(`Engine "${engineName}" apiKeyFile ${reason}: ${filePath}`, "INVALID_CONFIG_FILE");
|
|
51
|
+
}
|
|
52
|
+
const value = trimTrailingNewline(raw);
|
|
53
|
+
if (value.length === 0) {
|
|
54
|
+
throw new ConfigError(`Engine "${engineName}" apiKeyFile is empty: ${filePath}`, "INVALID_CONFIG_FILE");
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort, non-throwing read of a file-backed credential's current value
|
|
60
|
+
* (#905), for redaction inventories and health probes that must never fail
|
|
61
|
+
* just because a value collector ran ahead of the real dispatch — a missing
|
|
62
|
+
* or empty file is reported by {@link readApiKeyFile} at the actual call.
|
|
63
|
+
*/
|
|
64
|
+
export function lookupApiKeyFileValue(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
const value = trimTrailingNewline(fs.readFileSync(filePath, "utf8"));
|
|
67
|
+
return value.length > 0 ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
25
73
|
function selectedEngineName(config, layers, llmOnly) {
|
|
26
74
|
for (let index = layers.length - 1; index >= 0; index--) {
|
|
27
75
|
const layer = layers[index];
|
|
@@ -50,6 +98,11 @@ function resolveCredential(name, engine, config) {
|
|
|
50
98
|
throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE");
|
|
51
99
|
return { names: [explicit], required: true };
|
|
52
100
|
}
|
|
101
|
+
// #905: an explicit apiKeyFile is its own credential source — resolved
|
|
102
|
+
// separately onto `ResolvedLlmUse.apiKeyFile` — so it does not also fall
|
|
103
|
+
// through to the implicit AKM_ENGINE_<NAME>_API_KEY convention below.
|
|
104
|
+
if (ownValue(engine, "apiKeyFile") !== undefined)
|
|
105
|
+
return undefined;
|
|
53
106
|
const specific = `AKM_ENGINE_${name.toUpperCase().replaceAll("-", "_")}_API_KEY`;
|
|
54
107
|
const defaults = ownValue(config, "defaults");
|
|
55
108
|
return (defaults ? ownValue(defaults, "llmEngine") : undefined) === name
|
|
@@ -81,6 +134,20 @@ export function resolveCredentialFromEnv(credential, envSource = process.env) {
|
|
|
81
134
|
}
|
|
82
135
|
return undefined;
|
|
83
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The enforcing credential seam for one resolved LLM engine or SDK fallback
|
|
139
|
+
* (#905): the symbolic env-var descriptor first (throws if a required one is
|
|
140
|
+
* missing), then the file-backed alternative when no env descriptor applies.
|
|
141
|
+
* Call this once per operation — lease acquisition, or direct materialize —
|
|
142
|
+
* so a whole operation observes one stable credential value instead of
|
|
143
|
+
* re-reading the file on every dispatch within it.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveLlmCredentialValue(engine, credential, apiKeyFile, envSource = process.env) {
|
|
146
|
+
const envValue = resolveCredentialFromEnv(credential, envSource);
|
|
147
|
+
if (envValue !== undefined)
|
|
148
|
+
return envValue;
|
|
149
|
+
return apiKeyFile !== undefined ? readApiKeyFile(engine, apiKeyFile) : undefined;
|
|
150
|
+
}
|
|
84
151
|
/** Collect materialized engine credentials for output and persistence redaction. */
|
|
85
152
|
export function collectEngineCredentialValues(config, envSource = process.env) {
|
|
86
153
|
const values = new Set();
|
|
@@ -92,6 +159,15 @@ export function collectEngineCredentialValues(config, envSource = process.env) {
|
|
|
92
159
|
if (value)
|
|
93
160
|
values.add(value);
|
|
94
161
|
}
|
|
162
|
+
// #905: file-backed credential — best-effort, so a broken apiKeyFile on
|
|
163
|
+
// one engine never stops redaction from collecting every other engine's
|
|
164
|
+
// credential too.
|
|
165
|
+
const apiKeyFile = ownValue(engine, "apiKeyFile");
|
|
166
|
+
if (apiKeyFile !== undefined) {
|
|
167
|
+
const value = lookupApiKeyFileValue(expandHomePath(apiKeyFile));
|
|
168
|
+
if (value)
|
|
169
|
+
values.add(value);
|
|
170
|
+
}
|
|
95
171
|
}
|
|
96
172
|
return collectSensitiveValues(values);
|
|
97
173
|
}
|
|
@@ -130,7 +206,20 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
|
|
|
130
206
|
}
|
|
131
207
|
const engine = resolveEngineConfig(name, config);
|
|
132
208
|
if (engine.kind !== "llm") {
|
|
133
|
-
|
|
209
|
+
const defaults = ownValue(config, "defaults");
|
|
210
|
+
const fallbackName = ownValue(engine, "llmEngine") ?? (defaults ? ownValue(defaults, "llmEngine") : undefined);
|
|
211
|
+
const fallbackEngine = fallbackName ? resolveEngineConfig(fallbackName, config) : undefined;
|
|
212
|
+
if (!fallbackEngine || fallbackEngine.kind !== "llm") {
|
|
213
|
+
if (options.optional)
|
|
214
|
+
return undefined;
|
|
215
|
+
throw new ConfigError(fallbackName
|
|
216
|
+
? `Engine "${name}" is not an LLM engine, and its llmEngine fallback "${fallbackName}" is not one either.`
|
|
217
|
+
: `Engine "${name}" is not an LLM engine, and has no llmEngine fallback configured.`, "INVALID_CONFIG_FILE");
|
|
218
|
+
}
|
|
219
|
+
warn(`[akm] Engine "${name}" is an agent engine, not an LLM engine; using its llmEngine "${fallbackName}" instead.`);
|
|
220
|
+
return options.optional
|
|
221
|
+
? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
|
|
222
|
+
: resolveLlmEngineUse(config, [{ engine: fallbackName }]);
|
|
134
223
|
}
|
|
135
224
|
let connection = rawLlmConnection(engine);
|
|
136
225
|
for (const layer of layers) {
|
|
@@ -145,14 +234,22 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
|
|
|
145
234
|
if (connection[key] === undefined)
|
|
146
235
|
delete connection[key];
|
|
147
236
|
}
|
|
237
|
+
const apiKeyFile = ownValue(engine, "apiKeyFile");
|
|
148
238
|
return {
|
|
149
239
|
engine: name,
|
|
150
240
|
connection: sterileRecord(connection),
|
|
151
241
|
credential: resolveCredential(name, engine, config),
|
|
242
|
+
...(apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(apiKeyFile) } : {}),
|
|
152
243
|
timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS),
|
|
153
244
|
};
|
|
154
245
|
}
|
|
155
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* Inject an already-resolved credential value into a connection. Callers
|
|
248
|
+
* resolve the value themselves via {@link resolveLlmCredentialValue} (or its
|
|
249
|
+
* lease-cached equivalent) — this function never reads env or disk itself, so
|
|
250
|
+
* a frozen `ResolvedLlmUse`/`RunnerSpec` plan object can be materialized
|
|
251
|
+
* repeatedly without re-triggering I/O per call.
|
|
252
|
+
*/
|
|
156
253
|
export function materializeLlmConnectionWithCredential(resolved, credentialValue) {
|
|
157
254
|
const extraParams = ownValue(resolved.connection, "extraParams");
|
|
158
255
|
if (extraParams !== undefined) {
|
|
@@ -167,9 +264,13 @@ export function materializeLlmConnectionWithCredential(resolved, credentialValue
|
|
|
167
264
|
timeoutMs: resolved.timeoutMs,
|
|
168
265
|
});
|
|
169
266
|
}
|
|
170
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Read and inject one resolved credential at the runtime boundary: the
|
|
269
|
+
* symbolic `$VAR` reference, or the file-backed alternative (#905) when the
|
|
270
|
+
* engine has no env descriptor.
|
|
271
|
+
*/
|
|
171
272
|
export function materializeLlmConnection(resolved, envSource = process.env) {
|
|
172
|
-
return materializeLlmConnectionWithCredential(resolved,
|
|
273
|
+
return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, envSource));
|
|
173
274
|
}
|
|
174
275
|
function lowerAgentEngine(name, engine, config) {
|
|
175
276
|
const harness = getHarness(engine.platform);
|
|
@@ -217,6 +318,7 @@ function lowerAgentEngine(name, engine, config) {
|
|
|
217
318
|
? {
|
|
218
319
|
fallbackConnection: fallback.connection,
|
|
219
320
|
...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
|
|
321
|
+
...(fallback.apiKeyFile ? { fallbackApiKeyFile: fallback.apiKeyFile } : {}),
|
|
220
322
|
fallbackTimeoutMs: fallback.timeoutMs,
|
|
221
323
|
}
|
|
222
324
|
: {}),
|
|
@@ -237,6 +339,7 @@ export function resolveEngine(name, config) {
|
|
|
237
339
|
engine: name,
|
|
238
340
|
connection: resolved.connection,
|
|
239
341
|
...(resolved.credential ? { credential: resolved.credential } : {}),
|
|
342
|
+
...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}),
|
|
240
343
|
timeoutMs: resolved.timeoutMs,
|
|
241
344
|
};
|
|
242
345
|
}
|
|
@@ -210,17 +210,29 @@ function snapshotRunnerSpec(input, options = {}) {
|
|
|
210
210
|
if (own(cloned, "timeoutMs"))
|
|
211
211
|
validateTimeout(cloned.timeoutMs, "execution runner material.timeoutMs");
|
|
212
212
|
if (kind === "llm") {
|
|
213
|
-
assertKeys(cloned, ["kind", "engine", "connection", "credential", "timeoutMs"], "execution runner material");
|
|
213
|
+
assertKeys(cloned, ["kind", "engine", "connection", "credential", "apiKeyFile", "timeoutMs"], "execution runner material");
|
|
214
214
|
validateConnection(cloned.connection, "execution runner material.connection", !options.allowMissingLlmModel);
|
|
215
215
|
if (own(cloned, "credential"))
|
|
216
216
|
validateCredential(cloned.credential, "execution runner material.credential");
|
|
217
|
+
// #905: a path, not the secret itself — as safe to freeze as `credential`'s
|
|
218
|
+
// env-var name.
|
|
219
|
+
validateOptionalString(cloned, "apiKeyFile", "execution runner material");
|
|
217
220
|
}
|
|
218
221
|
else if (kind === "agent") {
|
|
219
222
|
assertKeys(cloned, ["kind", "engine", "profile", "timeoutMs"], "execution runner material");
|
|
220
223
|
validateProfile(cloned.profile, "execution runner material.profile");
|
|
221
224
|
}
|
|
222
225
|
else if (kind === "sdk") {
|
|
223
|
-
assertKeys(cloned, [
|
|
226
|
+
assertKeys(cloned, [
|
|
227
|
+
"kind",
|
|
228
|
+
"engine",
|
|
229
|
+
"profile",
|
|
230
|
+
"fallbackConnection",
|
|
231
|
+
"fallbackCredential",
|
|
232
|
+
"fallbackApiKeyFile",
|
|
233
|
+
"fallbackTimeoutMs",
|
|
234
|
+
"timeoutMs",
|
|
235
|
+
], "execution runner material");
|
|
224
236
|
validateProfile(cloned.profile, "execution runner material.profile");
|
|
225
237
|
if (own(cloned, "fallbackConnection")) {
|
|
226
238
|
validateConnection(cloned.fallbackConnection, "execution runner material.fallbackConnection", !options.allowMissingSdkFallbackModel);
|
|
@@ -228,6 +240,7 @@ function snapshotRunnerSpec(input, options = {}) {
|
|
|
228
240
|
if (own(cloned, "fallbackCredential")) {
|
|
229
241
|
validateCredential(cloned.fallbackCredential, "execution runner material.fallbackCredential");
|
|
230
242
|
}
|
|
243
|
+
validateOptionalString(cloned, "fallbackApiKeyFile", "execution runner material");
|
|
231
244
|
if (own(cloned, "fallbackTimeoutMs")) {
|
|
232
245
|
validateTimeout(cloned.fallbackTimeoutMs, "execution runner material.fallbackTimeoutMs");
|
|
233
246
|
}
|
|
@@ -9,6 +9,7 @@ import { writeFileAtomic } from "../../core/common.js";
|
|
|
9
9
|
import { ENGINE_NAME_PATTERN_SOURCE } from "../../core/config/engine-semantics.js";
|
|
10
10
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
11
11
|
import { getConfigDir } from "../../core/paths.js";
|
|
12
|
+
import { warnOnce } from "../../core/warn.js";
|
|
12
13
|
import { cloneExecutionJsonObject } from "../../execution/json.js";
|
|
13
14
|
/**
|
|
14
15
|
* Installed and operator-owned model intent aliases (#802 / WP2).
|
|
@@ -218,10 +219,16 @@ export function resolveModelMapAlias(input, engine, map) {
|
|
|
218
219
|
const profile = ownValue(tier, selectedEngine);
|
|
219
220
|
if (profile !== undefined)
|
|
220
221
|
return selectionFromProfile(input, profile);
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
const knownAliasUnmappedForEngine = tier !== undefined;
|
|
223
|
+
if (knownAliasUnmappedForEngine) {
|
|
224
|
+
warnOnce(`model-map-alias-no-engine-mapping:${alias}:${selectedEngine}`, `[akm] Model alias ${JSON.stringify(input)} has no mapping for engine ${JSON.stringify(engine)}; using ${JSON.stringify(input)} as the literal model name. Add $.aliases.${alias}.${engine} to models.json to map it.`);
|
|
223
225
|
}
|
|
224
|
-
return Object.freeze({
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
input,
|
|
228
|
+
interpretation: "exact",
|
|
229
|
+
model: input,
|
|
230
|
+
...(knownAliasUnmappedForEngine ? { unmappedForEngine: true } : {}),
|
|
231
|
+
});
|
|
225
232
|
}
|
|
226
233
|
export function userModelMapPath(env = process.env) {
|
|
227
234
|
return path.join(getConfigDir(env), "models.json");
|
|
@@ -235,25 +242,24 @@ function modelMapFileError(label, filePath, action) {
|
|
|
235
242
|
* absence; dangling links and every non-regular type are configuration errors.
|
|
236
243
|
*/
|
|
237
244
|
function readModelMapFile(filePath, label, optional) {
|
|
238
|
-
let
|
|
245
|
+
let targetStat;
|
|
239
246
|
try {
|
|
240
|
-
|
|
247
|
+
targetStat = fs.statSync(filePath);
|
|
241
248
|
}
|
|
242
249
|
catch (error) {
|
|
243
250
|
if (optional && error?.code === "ENOENT")
|
|
244
251
|
return undefined;
|
|
245
252
|
throw modelMapFileError(label, filePath, optional ? "inspected" : "found");
|
|
246
253
|
}
|
|
247
|
-
if (!
|
|
248
|
-
throw new ConfigError(`Unable to read ${label.toLowerCase()} because it is not a readable regular file: ${filePath}.`, "INVALID_CONFIG_FILE", "
|
|
254
|
+
if (!targetStat.isFile()) {
|
|
255
|
+
throw new ConfigError(`Unable to read ${label.toLowerCase()} because it is not a readable regular file: ${filePath}.`, "INVALID_CONFIG_FILE", "Replace it with a readable regular models.json file, or a symlink to one.");
|
|
249
256
|
}
|
|
250
|
-
const noFollow = process.platform !== "win32" && typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
251
257
|
const nonblock = process.platform !== "win32" && typeof fs.constants.O_NONBLOCK === "number" ? fs.constants.O_NONBLOCK : 0;
|
|
252
258
|
let fd;
|
|
253
259
|
try {
|
|
254
|
-
fd = fs.openSync(filePath, fs.constants.O_RDONLY |
|
|
260
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | nonblock);
|
|
255
261
|
const openedStat = fs.fstatSync(fd);
|
|
256
|
-
if (!sameFileIdentity(
|
|
262
|
+
if (!sameFileIdentity(targetStat, openedStat)) {
|
|
257
263
|
throw new ConfigError(`${label} changed while it was being opened: ${filePath}.`, "INVALID_CONFIG_FILE", "Retry after ensuring no other process is replacing models.json.");
|
|
258
264
|
}
|
|
259
265
|
const text = fs.readFileSync(fd, "utf8");
|
|
@@ -315,9 +315,18 @@ export function buildReflectPrompt(input) {
|
|
|
315
315
|
// asset content into shorter prose, drops concrete structure, or strips
|
|
316
316
|
// load-bearing frontmatter. Loud and explicit so small models follow.
|
|
317
317
|
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
318
|
+
// Guard-audit finding 15: this used to also hand back a maxOutputChars
|
|
319
|
+
// value so an LLM-path caller could convert it into a hard `max_tokens`
|
|
320
|
+
// cap on the API request. llm/client.ts's own doc comment (and
|
|
321
|
+
// commands/improve/reflect.ts's recorded history of responses actually
|
|
322
|
+
// getting cut off) is explicit that a character-derived max_tokens causes
|
|
323
|
+
// silent truncation — a real model's output is measured in tokens, not
|
|
324
|
+
// characters, and the ratio between the two varies enough that any fixed
|
|
325
|
+
// conversion either truncates legitimate output or provides no real cap at
|
|
326
|
+
// all. The size policy below is already enforced twice more (the prompt
|
|
327
|
+
// rules the model reads, and the post-processor's own size check), so nothing
|
|
328
|
+
// is lost by not adding a THIRD, byte-derived enforcement point that can
|
|
329
|
+
// only ever cut a response off early, never usefully re-check it.
|
|
321
330
|
if (input.ref && input.assetContent?.trim()) {
|
|
322
331
|
// Strip frontmatter to get source body length — mirrors checkReflectSize which
|
|
323
332
|
// compares body-only lengths. Inline regex avoids importing parseFrontmatter.
|
|
@@ -332,8 +341,6 @@ export function buildReflectPrompt(input) {
|
|
|
332
341
|
const showCharBounds = sourceBodyLen >= 200;
|
|
333
342
|
const minChars = Math.max(Math.round(0.5 * sourceBodyLen), 150);
|
|
334
343
|
const maxChars = Math.min(Math.max(Math.round(2.5 * sourceBodyLen), 2500), 25000);
|
|
335
|
-
if (showCharBounds)
|
|
336
|
-
maxOutputChars = maxChars;
|
|
337
344
|
sections.push([
|
|
338
345
|
"## Content preservation rules (MUST follow)",
|
|
339
346
|
"1. PRESERVE ALL concrete content: code blocks, fenced snippets, CLI commands, numbered/bulleted checklists, tables, YAML/JSON examples, file paths, configuration keys, environment variable names, and CSS/HTML selectors. These are load-bearing — do NOT replace them with prose summaries.",
|
|
@@ -353,7 +360,7 @@ export function buildReflectPrompt(input) {
|
|
|
353
360
|
sections.push(`IMPORTANT: The JSON "ref" field is REQUIRED. It MUST be exactly: "${input.ref}"`);
|
|
354
361
|
}
|
|
355
362
|
sections.push(reflectResponseContract(input));
|
|
356
|
-
return { prompt: sections.join("\n\n")
|
|
363
|
+
return { prompt: sections.join("\n\n") };
|
|
357
364
|
}
|
|
358
365
|
/**
|
|
359
366
|
* Build the prompt for `akm propose <type> <name> --task ...`. Asks the
|
|
@@ -20,7 +20,7 @@ import { assertNever } from "../../core/assert.js";
|
|
|
20
20
|
import { collectSensitiveValues, isEnvPassthroughValueSafeToExpose, redactSensitiveText, redactSensitiveValue, } from "../../core/redaction.js";
|
|
21
21
|
import { spawnEnvNamesFor } from "../../core/spawn-env.js";
|
|
22
22
|
import { closeServer as disposeOpencodeSdkServers, opencodeSdkServerEnvironmentNames, runOpencodeSdk, } from "../harnesses/opencode-sdk/sdk-runner.js";
|
|
23
|
-
import { lookupCredentialFromEnv, materializeLlmConnection, materializeLlmConnectionWithCredential,
|
|
23
|
+
import { lookupApiKeyFileValue, lookupCredentialFromEnv, materializeLlmConnection, materializeLlmConnectionWithCredential, resolveLlmCredentialValue, } from "./engine-resolution.js";
|
|
24
24
|
import { materializeLlmRunnerConnection, materializeLlmRunnerConnectionWithCredential, } from "./runner.js";
|
|
25
25
|
import { runAgent } from "./spawn.js";
|
|
26
26
|
const liveRunnerDispatchLeases = new WeakMap();
|
|
@@ -78,6 +78,7 @@ function runnerLeaseBinding(spec) {
|
|
|
78
78
|
endpoint: spec.connection.endpoint,
|
|
79
79
|
provider: spec.connection.provider ?? null,
|
|
80
80
|
credential: credentialBinding(spec.credential),
|
|
81
|
+
apiKeyFile: spec.apiKeyFile ?? null,
|
|
81
82
|
});
|
|
82
83
|
case "agent":
|
|
83
84
|
return JSON.stringify({
|
|
@@ -99,6 +100,7 @@ function runnerLeaseBinding(spec) {
|
|
|
99
100
|
fallbackEndpoint: spec.fallbackConnection?.endpoint ?? null,
|
|
100
101
|
fallbackProvider: spec.fallbackConnection?.provider ?? null,
|
|
101
102
|
fallbackCredential: credentialBinding(spec.fallbackCredential),
|
|
103
|
+
fallbackApiKeyFile: spec.fallbackApiKeyFile ?? null,
|
|
102
104
|
});
|
|
103
105
|
default:
|
|
104
106
|
return assertNever(spec);
|
|
@@ -131,8 +133,12 @@ export function acquireRunnerDispatchLease(spec, envSource = process.env) {
|
|
|
131
133
|
return typeof property === "string" ? environment.read(property) : undefined;
|
|
132
134
|
},
|
|
133
135
|
});
|
|
134
|
-
const primaryCredential = spec.kind === "llm"
|
|
135
|
-
|
|
136
|
+
const primaryCredential = spec.kind === "llm"
|
|
137
|
+
? resolveLlmCredentialValue(spec.engine, spec.credential, spec.apiKeyFile, credentialSource)
|
|
138
|
+
: undefined;
|
|
139
|
+
const fallbackCredential = spec.kind === "sdk"
|
|
140
|
+
? resolveLlmCredentialValue(spec.engine, spec.fallbackCredential, spec.fallbackApiKeyFile, credentialSource)
|
|
141
|
+
: undefined;
|
|
136
142
|
const handle = Object.create(null);
|
|
137
143
|
Object.defineProperty(handle, "toJSON", {
|
|
138
144
|
configurable: false,
|
|
@@ -217,6 +223,12 @@ export function collectDispatchSensitiveValues(spec, opts, envSource = opts.envS
|
|
|
217
223
|
add(lookupCredentialFromEnv(spec.credential, envSource));
|
|
218
224
|
if (spec.kind === "sdk")
|
|
219
225
|
add(lookupCredentialFromEnv(spec.fallbackCredential, envSource));
|
|
226
|
+
// #905: a file-backed credential is read at dispatch, so it must be in the
|
|
227
|
+
// scrub set for the same reason the env-backed one is.
|
|
228
|
+
if (spec.kind === "llm" && spec.apiKeyFile)
|
|
229
|
+
add(lookupApiKeyFileValue(spec.apiKeyFile));
|
|
230
|
+
if (spec.kind === "sdk" && spec.fallbackApiKeyFile)
|
|
231
|
+
add(lookupApiKeyFileValue(spec.fallbackApiKeyFile));
|
|
220
232
|
if (spec.kind !== "llm") {
|
|
221
233
|
for (const value of Object.values(spec.profile.env ?? {}))
|
|
222
234
|
add(value);
|
|
@@ -315,6 +327,7 @@ export async function executeRunner(spec, prompt, opts, seams = {}, lease) {
|
|
|
315
327
|
engine: spec.engine ?? "unnamed-sdk-fallback",
|
|
316
328
|
connection: spec.fallbackConnection,
|
|
317
329
|
...(spec.fallbackCredential ? { credential: spec.fallbackCredential } : {}),
|
|
330
|
+
...(spec.fallbackApiKeyFile ? { apiKeyFile: spec.fallbackApiKeyFile } : {}),
|
|
318
331
|
timeoutMs: spec.fallbackTimeoutMs !== undefined
|
|
319
332
|
? spec.fallbackTimeoutMs
|
|
320
333
|
: Object.hasOwn(spec.fallbackConnection, "timeoutMs")
|
|
@@ -8,6 +8,7 @@ export function materializeLlmRunnerConnection(runner) {
|
|
|
8
8
|
engine: runner.engine,
|
|
9
9
|
connection: runner.connection,
|
|
10
10
|
...(runner.credential ? { credential: runner.credential } : {}),
|
|
11
|
+
...(runner.apiKeyFile ? { apiKeyFile: runner.apiKeyFile } : {}),
|
|
11
12
|
timeoutMs: runner.timeoutMs ?? null,
|
|
12
13
|
});
|
|
13
14
|
}
|
|
@@ -17,6 +18,7 @@ export function materializeLlmRunnerConnectionWithCredential(runner, credentialV
|
|
|
17
18
|
engine: runner.engine,
|
|
18
19
|
connection: runner.connection,
|
|
19
20
|
...(runner.credential ? { credential: runner.credential } : {}),
|
|
21
|
+
...(runner.apiKeyFile ? { apiKeyFile: runner.apiKeyFile } : {}),
|
|
20
22
|
timeoutMs: runner.timeoutMs ?? null,
|
|
21
23
|
}, credentialValue);
|
|
22
24
|
}
|
|
@@ -9,14 +9,24 @@ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquire
|
|
|
9
9
|
import { acquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
|
|
10
10
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
11
11
|
import { getDataDir, getLockfileLockPath, getLockfilePath } from "../core/paths.js";
|
|
12
|
+
import { warn } from "../core/warn.js";
|
|
12
13
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const LOCK_ACQUIRE_TIMEOUT_MS = 30_000;
|
|
15
|
+
const LOCK_RETRY_INITIAL_DELAY_MS = 50;
|
|
16
|
+
const LOCK_RETRY_MAX_DELAY_MS = 1_000;
|
|
17
|
+
let lockAcquireTimeoutMsForTests;
|
|
18
|
+
export function _setLockAcquireTimeoutMsForTests(ms) {
|
|
19
|
+
lockAcquireTimeoutMsForTests = ms;
|
|
20
|
+
}
|
|
15
21
|
async function acquireLockSentinel() {
|
|
16
22
|
const sentinelPath = getLockfileLockPath();
|
|
17
23
|
// Ensure the directory exists before attempting to create the sentinel.
|
|
18
24
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
19
|
-
|
|
25
|
+
const timeoutMs = lockAcquireTimeoutMsForTests ?? LOCK_ACQUIRE_TIMEOUT_MS;
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
let delayMs = LOCK_RETRY_INITIAL_DELAY_MS;
|
|
28
|
+
let announced = false;
|
|
29
|
+
for (;;) {
|
|
20
30
|
const releaseBarrier = acquireMaintenanceBarrier();
|
|
21
31
|
try {
|
|
22
32
|
const ownership = tryAcquireLockSync(sentinelPath, createLockPayload());
|
|
@@ -31,12 +41,17 @@ async function acquireLockSentinel() {
|
|
|
31
41
|
finally {
|
|
32
42
|
releaseBarrier();
|
|
33
43
|
}
|
|
34
|
-
// Another process holds the lock
|
|
35
|
-
if (
|
|
36
|
-
|
|
44
|
+
// Another process holds the lock.
|
|
45
|
+
if (Date.now() >= deadline) {
|
|
46
|
+
throw new ConfigError(`Could not acquire lockfile sentinel at ${sentinelPath} after ${(timeoutMs / 1000).toFixed(1)}s; refusing to write without exclusive ownership.`, "INVALID_CONFIG_FILE");
|
|
47
|
+
}
|
|
48
|
+
if (!announced) {
|
|
49
|
+
announced = true;
|
|
50
|
+
warn("[akm] Waiting for another akm process to release the lockfile...");
|
|
37
51
|
}
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
53
|
+
delayMs = Math.min(delayMs * 2, LOCK_RETRY_MAX_DELAY_MS);
|
|
38
54
|
}
|
|
39
|
-
throw new ConfigError(`Could not acquire lockfile sentinel at ${sentinelPath}; refusing to write without exclusive ownership.`, "INVALID_CONFIG_FILE");
|
|
40
55
|
}
|
|
41
56
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
42
57
|
export function readLockfile() {
|
package/dist/llm/client.js
CHANGED
|
@@ -447,3 +447,19 @@ export async function probeLlmReachable(config) {
|
|
|
447
447
|
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Reachability probe for `akm health` (#914): one GET against the
|
|
452
|
+
* OpenAI-compatible `/models` route, bounded by `timeoutMs`. Any HTTP
|
|
453
|
+
* response counts as reachable — the question is whether the endpoint
|
|
454
|
+
* answers, not whether the route exists or the credential is right — so a
|
|
455
|
+
* cold local server is never asked to load a model just to be checked.
|
|
456
|
+
*/
|
|
457
|
+
export async function probeLlmEndpoint(config, timeoutMs = 3_000) {
|
|
458
|
+
try {
|
|
459
|
+
await fetch(`${config.endpoint.replace(/\/+$/, "")}/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
460
|
+
return { reachable: true };
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
464
|
+
}
|
|
465
|
+
}
|
package/dist/llm/index-passes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import {
|
|
4
|
+
import { warn } from "../core/warn.js";
|
|
5
5
|
import { cloneExecutionJsonObject } from "../execution/json.js";
|
|
6
6
|
import { lowerResolvedExecutionRequest } from "../integrations/agent/execution-lowering.js";
|
|
7
7
|
import { prepareInlineExecution } from "../integrations/agent/inline-execution.js";
|
|
@@ -48,7 +48,8 @@ export function resolveIndexPassExecution(passName, config) {
|
|
|
48
48
|
});
|
|
49
49
|
const lowered = lowerResolvedExecutionRequest(prepared.request, prepared.config);
|
|
50
50
|
if (lowered.runner.kind !== "llm") {
|
|
51
|
-
|
|
51
|
+
warn("[akm] Index pass %s requires an LLM engine; %s is not one. Skipping this pass.", passName, selectedEngine);
|
|
52
|
+
return Object.freeze({ runner: undefined, notices: NO_LOWERING_NOTICES });
|
|
52
53
|
}
|
|
53
54
|
return Object.freeze({ runner: lowered.runner, notices: lowered.notices });
|
|
54
55
|
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
// #484: stamp schemaVersion + shape discriminator on passthrough envelopes so
|
|
5
5
|
// third-party consumers can pin a schema version and dispatch on shape uniformly.
|
|
6
|
-
//
|
|
6
|
+
// #918: also stamp `ok: true` so a caller branching on `.ok` sees the same
|
|
7
|
+
// field on every success envelope that it sees on the `{ok:false,...}` error
|
|
8
|
+
// envelope. Never overwrites an existing `shape`, `schemaVersion`, or `ok`;
|
|
9
|
+
// a command whose exit code grades the outcome emits through
|
|
10
|
+
// `outputWithExitCode` (src/cli/shared.ts), which sets `ok` from that code.
|
|
7
11
|
//
|
|
8
12
|
// Builds a shallow copy rather than mutating `result` in place: several
|
|
9
13
|
// command results (e.g. `akm task sync --dry-run`'s `SchedulerPlanPreview`,
|
|
@@ -20,9 +24,11 @@ function makeStampHandler(command) {
|
|
|
20
24
|
if (typeof result !== "object" || Array.isArray(result))
|
|
21
25
|
return result;
|
|
22
26
|
const obj = result;
|
|
23
|
-
if (obj.shape !== undefined && obj.schemaVersion !== undefined)
|
|
24
|
-
return obj;
|
|
25
27
|
return {
|
|
28
|
+
// `ok` first so it heads the printed envelope instead of trailing a
|
|
29
|
+
// (possibly large) dump — the spread below still wins with `obj`'s own
|
|
30
|
+
// `ok` value when present, only its position is fixed here.
|
|
31
|
+
ok: obj.ok ?? true,
|
|
26
32
|
...obj,
|
|
27
33
|
shape: obj.shape ?? command,
|
|
28
34
|
schemaVersion: obj.schemaVersion ?? 1,
|
|
@@ -70,6 +76,7 @@ const PASSTHROUGH_COMMANDS = [
|
|
|
70
76
|
"task-run",
|
|
71
77
|
"task-sync",
|
|
72
78
|
"task-sync-dry-run",
|
|
79
|
+
"task-validate",
|
|
73
80
|
"update",
|
|
74
81
|
"upgrade",
|
|
75
82
|
"workflow-abandon",
|
package/dist/output/shapes.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* and throws for unknown commands (v1 spec §9 — exhaustive registry, no silent
|
|
18
18
|
* fallback).
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { warnOnce } from "../core/warn.js";
|
|
21
21
|
import { curateShapes } from "./shapes/curate.js";
|
|
22
22
|
import { envListShapes } from "./shapes/env-list.js";
|
|
23
23
|
import { eventsShapes } from "./shapes/events.js";
|
|
@@ -67,12 +67,14 @@ registerOutputShapes(BUILT_IN_OUTPUT_SHAPES);
|
|
|
67
67
|
*/
|
|
68
68
|
const SHAPE_SUMMARY_COMMANDS = new Set(["show"]);
|
|
69
69
|
export function shapeForCommand(command, result, detail, shape = "human") {
|
|
70
|
+
let effectiveShape = shape;
|
|
70
71
|
if (shape === "summary" && !SHAPE_SUMMARY_COMMANDS.has(command)) {
|
|
71
|
-
|
|
72
|
+
warnOnce(`shape-summary-unsupported:${command}`, `[output] '--shape summary' is not supported for 'akm ${command}' (only 'akm show' has a summary projection); falling back to 'agent'.`);
|
|
73
|
+
effectiveShape = "agent";
|
|
72
74
|
}
|
|
73
75
|
const handler = getOutputShapeHandler(command);
|
|
74
76
|
if (handler) {
|
|
75
|
-
return handler(result, detail,
|
|
77
|
+
return handler(result, detail, effectiveShape);
|
|
76
78
|
}
|
|
77
79
|
// v1 spec §9 (output-shape registry exhaustive): no silent JSON.stringify
|
|
78
80
|
// fallback. A missing case here is a registration bug — fail loudly so
|
|
@@ -172,7 +172,14 @@ export function formatWorkflowRunPlain(result) {
|
|
|
172
172
|
const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
|
|
173
173
|
if (!run)
|
|
174
174
|
return null;
|
|
175
|
-
const lines = [
|
|
175
|
+
const lines = [];
|
|
176
|
+
// #919: `workflow run <ref>` silently resuming an existing active run (the
|
|
177
|
+
// #485 concurrency guard) is now announced up front, with the escape hatch.
|
|
178
|
+
if (result.resumed === true) {
|
|
179
|
+
lines.push(`resuming existing run ${String(run.id ?? "unknown")} for ${String(run.workflowRef ?? "this ref")}; ` +
|
|
180
|
+
`pass --new to start a fresh run`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(`run: ${String(run.id ?? "unknown")}`, `status: ${String(run.status ?? "unknown")}`);
|
|
176
183
|
// Creation-time notices (e.g. the implicit engine fallback) must survive the
|
|
177
184
|
// text renderer: JSON/YAML pass them through, and dropping them here would
|
|
178
185
|
// hide the announcement from the DEFAULT output mode — where it matters most.
|