akm-cli 0.9.11 → 0.9.13
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 +227 -0
- package/STABILITY.md +6 -1
- 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/assets/workflows/workflow-template.md +4 -0
- 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 +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- 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 +14 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/base-linter.js +10 -0
- 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/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- 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 +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- 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/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +56 -6
- package/dist/core/config/config.js +42 -17
- 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 +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +27 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +4 -3
- 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 +12 -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/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +28 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +50 -3
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1737 -1392
- package/dist/scripts/akm-migrate.js +1736 -1391
- 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 +133 -11
- 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/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/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +20 -11
- package/dist/workflows/exec/step-work.js +76 -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/parser.js +1 -1
- 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 +104 -154
- 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/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +92 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
7
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
8
8
|
import { asNonEmptyString } from "../../core/common.js";
|
|
9
|
-
import { isVerbose, warn
|
|
9
|
+
import { isVerbose, warn } from "../../core/warn.js";
|
|
10
10
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
11
11
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
12
|
/**
|
|
@@ -835,10 +835,17 @@ export function isEnrichmentComplete(entry) {
|
|
|
835
835
|
// ── Native Markdown search projection ──────────────────────────────────────
|
|
836
836
|
/**
|
|
837
837
|
* Maximum native Markdown prose carried by the low-weight `content` field.
|
|
838
|
-
*
|
|
839
|
-
*
|
|
838
|
+
*
|
|
839
|
+
* Raised far past any real authored document:
|
|
840
|
+
* this used to sit at 16_384 chars, tight enough that ordinary long-form
|
|
841
|
+
* skills/knowledge docs lost their tail from both FTS and the embedding
|
|
842
|
+
* input with no visible signal (the cut was reported via `warnVerbose`,
|
|
843
|
+
* silent unless `AKM_VERBOSE` was set). The remaining bound exists only to
|
|
844
|
+
* stop a truly pathological single file (a committed data dump, a decompressed
|
|
845
|
+
* log) from ballooning index size — not to shave real content — so a caller
|
|
846
|
+
* that hits it is always told, unconditionally.
|
|
840
847
|
*/
|
|
841
|
-
export const MARKDOWN_CONTENT_MAX_CHARS =
|
|
848
|
+
export const MARKDOWN_CONTENT_MAX_CHARS = 1_000_000;
|
|
842
849
|
/**
|
|
843
850
|
* Locate a leading nested frontmatter block in a body: up to three blank
|
|
844
851
|
* lines, then a `---` line, closed by a later `---` line. Mirrors the
|
|
@@ -1220,7 +1227,11 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1220
1227
|
entry.content = contentProjection;
|
|
1221
1228
|
if (truncationInfo.truncated) {
|
|
1222
1229
|
entry.contentTruncated = true;
|
|
1223
|
-
warnVerbose
|
|
1230
|
+
// Unconditional, not warnVerbose: this bound now sits far past any
|
|
1231
|
+
// real document, so tripping it means
|
|
1232
|
+
// something unusual is in the bundle and the operator should see
|
|
1233
|
+
// it without having to pass --verbose.
|
|
1234
|
+
warn(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
1224
1235
|
}
|
|
1225
1236
|
}
|
|
1226
1237
|
}
|
|
@@ -1,18 +1,6 @@
|
|
|
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
|
-
/**
|
|
5
|
-
* Per-field search text extraction for FTS5 indexing.
|
|
6
|
-
*
|
|
7
|
-
* Extracted from indexer.ts to break the circular dependency:
|
|
8
|
-
* db.ts -> indexer.ts -> db.ts
|
|
9
|
-
*
|
|
10
|
-
* This module imports only from metadata.ts (for the IndexDocument type),
|
|
11
|
-
* so it can be safely imported by both db.ts and indexer.ts.
|
|
12
|
-
*/
|
|
13
|
-
import { warnVerbose } from "../../core/warn.js";
|
|
14
|
-
/** Structured metadata plus bounded body text supplied to embedding providers. */
|
|
15
|
-
export const SEARCH_TEXT_MAX_CHARS = 8_192;
|
|
16
4
|
/**
|
|
17
5
|
* Return per-field search text for multi-column FTS5 indexing.
|
|
18
6
|
*
|
|
@@ -90,25 +78,8 @@ export function buildSearchText(entry) {
|
|
|
90
78
|
const structured = [fields.name, fields.description, fields.tags, fields.hints]
|
|
91
79
|
.filter((field) => field.length > 0)
|
|
92
80
|
.join(" ");
|
|
93
|
-
if (structured.length >= SEARCH_TEXT_MAX_CHARS) {
|
|
94
|
-
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars (content dropped entirely)`);
|
|
95
|
-
return truncateUnicodeSafe(structured, SEARCH_TEXT_MAX_CHARS);
|
|
96
|
-
}
|
|
97
81
|
if (!fields.content)
|
|
98
82
|
return structured;
|
|
99
83
|
const separator = structured ? " " : "";
|
|
100
|
-
|
|
101
|
-
if (fields.content.length > remaining) {
|
|
102
|
-
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars`);
|
|
103
|
-
}
|
|
104
|
-
return `${structured}${separator}${truncateUnicodeSafe(fields.content, remaining)}`;
|
|
105
|
-
}
|
|
106
|
-
function truncateUnicodeSafe(text, maxChars) {
|
|
107
|
-
if (text.length <= maxChars)
|
|
108
|
-
return text;
|
|
109
|
-
let cut = text.slice(0, maxChars);
|
|
110
|
-
const lastCode = cut.charCodeAt(cut.length - 1);
|
|
111
|
-
if (lastCode >= 0xd800 && lastCode <= 0xdbff)
|
|
112
|
-
cut = cut.slice(0, -1);
|
|
113
|
-
return cut.trimEnd();
|
|
84
|
+
return `${structured}${separator}${fields.content}`;
|
|
114
85
|
}
|
|
@@ -8,6 +8,7 @@ import { deepMergeConfig } from "../../core/config/deep-merge.js";
|
|
|
8
8
|
import { ConfigError } from "../../core/errors.js";
|
|
9
9
|
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
10
10
|
import { collectSensitiveValues } from "../../core/redaction.js";
|
|
11
|
+
import { warn } from "../../core/warn.js";
|
|
11
12
|
import { getHarness } from "../harnesses/index.js";
|
|
12
13
|
import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
|
|
13
14
|
import { getBuiltinAgentProfile } from "./profiles.js";
|
|
@@ -205,7 +206,20 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
|
|
|
205
206
|
}
|
|
206
207
|
const engine = resolveEngineConfig(name, config);
|
|
207
208
|
if (engine.kind !== "llm") {
|
|
208
|
-
|
|
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 }]);
|
|
209
223
|
}
|
|
210
224
|
let connection = rawLlmConnection(engine);
|
|
211
225
|
for (const layer of layers) {
|
|
@@ -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
|
|
@@ -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
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
|
|
11
11
|
import { resolveSecret } from "../core/config/config.js";
|
|
12
|
-
import {
|
|
12
|
+
import { isApiKeyReference } from "../core/config/schema/primitives.js";
|
|
13
13
|
import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
|
|
14
14
|
import { redactErrorBody, redactSensitiveText } from "../core/redaction.js";
|
|
15
15
|
import { warn, warnVerbose } from "../core/warn.js";
|
|
16
16
|
import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
|
|
17
|
+
import { resolveSecretFromStore } from "../sources/snapshot-fetchers/secret-seam.js";
|
|
17
18
|
import { emitLlmUsage, extractUsageTokens, } from "./usage-telemetry.js";
|
|
18
19
|
/** Maximum length of an upstream response excerpt included in thrown errors. */
|
|
19
20
|
const ERROR_BODY_MAX_LEN = 200;
|
|
@@ -264,13 +265,16 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
|
|
|
264
265
|
throw new Error(formatExtraParamsIssue("LLM extraParams", issue));
|
|
265
266
|
}
|
|
266
267
|
const headers = { "Content-Type": "application/json" };
|
|
267
|
-
// Resolve ONLY a whole-string
|
|
268
|
-
// hands us a materialized credential after
|
|
269
|
-
// re-running the substitution over a
|
|
270
|
-
// containing `$` — `sk-live$ecret` lost
|
|
271
|
-
// the request failed with an opaque
|
|
272
|
-
// form working for any direct
|
|
273
|
-
|
|
268
|
+
// Resolve ONLY a whole-string reference ($VAR/${VAR} or secret://<name>).
|
|
269
|
+
// The execution boundary normally hands us a materialized credential after
|
|
270
|
+
// resolving the reference upstream, so re-running the substitution over a
|
|
271
|
+
// literal key mangled any credential containing `$` — `sk-live$ecret` lost
|
|
272
|
+
// everything from the `$` onward, and the request failed with an opaque
|
|
273
|
+
// 401. The narrow check keeps the symbolic form working for any direct
|
|
274
|
+
// caller that still passes one.
|
|
275
|
+
const resolvedKey = isApiKeyReference(config.apiKey ?? "")
|
|
276
|
+
? resolveSecret(config.apiKey, resolveSecretFromStore)
|
|
277
|
+
: config.apiKey;
|
|
274
278
|
if (resolvedKey) {
|
|
275
279
|
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
276
280
|
}
|
|
@@ -447,3 +451,19 @@ export async function probeLlmReachable(config) {
|
|
|
447
451
|
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
448
452
|
}
|
|
449
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Reachability probe for `akm health` (#914): one GET against the
|
|
456
|
+
* OpenAI-compatible `/models` route, bounded by `timeoutMs`. Any HTTP
|
|
457
|
+
* response counts as reachable — the question is whether the endpoint
|
|
458
|
+
* answers, not whether the route exists or the credential is right — so a
|
|
459
|
+
* cold local server is never asked to load a model just to be checked.
|
|
460
|
+
*/
|
|
461
|
+
export async function probeLlmEndpoint(config, timeoutMs = 3_000) {
|
|
462
|
+
try {
|
|
463
|
+
await fetch(`${config.endpoint.replace(/\/+$/, "")}/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
464
|
+
return { reachable: true };
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -11,6 +11,7 @@ import { fetchWithTimeout, isHttpUrl, readBodyWithByteCap } from "../../core/com
|
|
|
11
11
|
import { resolveSecret } from "../../core/config/config.js";
|
|
12
12
|
import { redactErrorBody, redactSensitiveText } from "../../core/redaction.js";
|
|
13
13
|
import { warnVerbose } from "../../core/warn.js";
|
|
14
|
+
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
14
15
|
/**
|
|
15
16
|
* Upper bound on the number of documents in one HTTP request, independent of
|
|
16
17
|
* the token budget below. Overridable via `config.batchSize`. Purely a
|
|
@@ -215,7 +216,7 @@ export class RemoteEmbedder {
|
|
|
215
216
|
}
|
|
216
217
|
buildHeaders() {
|
|
217
218
|
const headers = { "Content-Type": "application/json" };
|
|
218
|
-
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
219
|
+
const resolvedKey = resolveSecret(this.config.apiKey, resolveSecretFromStore);
|
|
219
220
|
if (resolvedKey) {
|
|
220
221
|
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
221
222
|
}
|
|
@@ -231,7 +232,7 @@ export class RemoteEmbedder {
|
|
|
231
232
|
* far unredacted and uncapped, at readBodyWithByteCap's 10 MB default.
|
|
232
233
|
*/
|
|
233
234
|
safeErrorBody(body) {
|
|
234
|
-
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
235
|
+
const resolvedKey = resolveSecret(this.config.apiKey, resolveSecretFromStore);
|
|
235
236
|
return redactSensitiveText(redactErrorBody(body), resolvedKey ? [resolvedKey] : []);
|
|
236
237
|
}
|
|
237
238
|
}
|
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,
|
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";
|
|
@@ -66,13 +66,60 @@ registerOutputShapes(BUILT_IN_OUTPUT_SHAPES);
|
|
|
66
66
|
* for a soon-frozen contract, not a silent fallback to `human`).
|
|
67
67
|
*/
|
|
68
68
|
const SHAPE_SUMMARY_COMMANDS = new Set(["show"]);
|
|
69
|
+
// ── `results` collection alias ──────────────────────────────────────────────
|
|
70
|
+
//
|
|
71
|
+
// Every list-returning command names its collection differently (`hits`,
|
|
72
|
+
// `items`, `proposals`, `sources`, ...) — a caller cannot write one accessor
|
|
73
|
+
// across commands without a per-command lookup table, and the wrong guess
|
|
74
|
+
// (`d.get("hits")` against a `curate` response) reads as "no results" rather
|
|
75
|
+
// than "wrong key", with nothing in the envelope to correct it.
|
|
76
|
+
//
|
|
77
|
+
// This maps each list-returning command to the field already holding its
|
|
78
|
+
// collection, and `withResultsAlias` below adds a `results` key pointing at
|
|
79
|
+
// that SAME array (not a copy) to the shaped output — in every `--shape` /
|
|
80
|
+
// `--detail` combination, `human` included, so `--shape agent` needs no
|
|
81
|
+
// separate handling to "guarantee" it. A new list-returning command MUST add
|
|
82
|
+
// an entry here; there is no way to detect a missed one automatically, so the
|
|
83
|
+
// survey deliberately lives in this one place rather than scattered per
|
|
84
|
+
// handler.
|
|
85
|
+
const LIST_RESULT_COLLECTION_KEYS = {
|
|
86
|
+
search: "hits",
|
|
87
|
+
curate: "items",
|
|
88
|
+
"registry-search": "hits",
|
|
89
|
+
"proposal-list": "proposals",
|
|
90
|
+
list: "sources", // `akm bundle list`
|
|
91
|
+
"env-list": "envs",
|
|
92
|
+
"secret-list": "secrets",
|
|
93
|
+
"registry-list": "registries",
|
|
94
|
+
"workflow-list": "runs",
|
|
95
|
+
"task-history": "rows",
|
|
96
|
+
"log-list": "events", // `akm log list`
|
|
97
|
+
};
|
|
98
|
+
function withResultsAlias(command, shaped) {
|
|
99
|
+
const key = LIST_RESULT_COLLECTION_KEYS[command];
|
|
100
|
+
if (!key)
|
|
101
|
+
return shaped;
|
|
102
|
+
if (shaped === null || typeof shaped !== "object" || Array.isArray(shaped))
|
|
103
|
+
return shaped;
|
|
104
|
+
const obj = shaped;
|
|
105
|
+
if ("results" in obj)
|
|
106
|
+
return shaped;
|
|
107
|
+
const collection = obj[key];
|
|
108
|
+
if (!Array.isArray(collection))
|
|
109
|
+
return shaped;
|
|
110
|
+
// Same array reference as `obj[key]`, never a copy, so `results` cannot
|
|
111
|
+
// silently drift out of sync with the semantic key it aliases.
|
|
112
|
+
return { ...obj, results: collection };
|
|
113
|
+
}
|
|
69
114
|
export function shapeForCommand(command, result, detail, shape = "human") {
|
|
115
|
+
let effectiveShape = shape;
|
|
70
116
|
if (shape === "summary" && !SHAPE_SUMMARY_COMMANDS.has(command)) {
|
|
71
|
-
|
|
117
|
+
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'.`);
|
|
118
|
+
effectiveShape = "agent";
|
|
72
119
|
}
|
|
73
120
|
const handler = getOutputShapeHandler(command);
|
|
74
121
|
if (handler) {
|
|
75
|
-
return handler(result, detail,
|
|
122
|
+
return withResultsAlias(command, handler(result, detail, effectiveShape));
|
|
76
123
|
}
|
|
77
124
|
// v1 spec §9 (output-shape registry exhaustive): no silent JSON.stringify
|
|
78
125
|
// fallback. A missing case here is a registration bug — fail loudly so
|
|
@@ -191,6 +191,7 @@ export function formatProposalDrainPlain(r) {
|
|
|
191
191
|
const deferred = Array.isArray(r.deferred) ? r.deferred : [];
|
|
192
192
|
const skippedByCap = Array.isArray(r.skippedByCap) ? r.skippedByCap : [];
|
|
193
193
|
const staged = Array.isArray(r.staged) ? r.staged : [];
|
|
194
|
+
const failed = Array.isArray(r.failed) ? r.failed : [];
|
|
194
195
|
const prefix = r.dryRun === true ? "[dry-run] " : "";
|
|
195
196
|
const lines = [
|
|
196
197
|
`${prefix}Drained proposal queue (strategy=${String(r.strategy ?? "?")}, policy=${policy}, applyMode=${applyMode})`,
|
|
@@ -199,10 +200,14 @@ export function formatProposalDrainPlain(r) {
|
|
|
199
200
|
` deferred: ${deferred.length}`,
|
|
200
201
|
` skippedByCap: ${skippedByCap.length}`,
|
|
201
202
|
` staged: ${staged.length}`,
|
|
203
|
+
` failed: ${failed.length}`,
|
|
202
204
|
];
|
|
203
205
|
for (const d of deferred) {
|
|
204
206
|
lines.push(` - ${String(d.id ?? "?")} (${String(d.reason ?? "?")})`);
|
|
205
207
|
}
|
|
208
|
+
for (const f of failed) {
|
|
209
|
+
lines.push(` ! ${String(f.id ?? "?")} (${String(f.reason ?? "?")}): ${String(f.detail ?? "?")}`);
|
|
210
|
+
}
|
|
206
211
|
appendLoweringNotices(lines, r);
|
|
207
212
|
return lines.join("\n").trimEnd();
|
|
208
213
|
}
|
|
@@ -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.
|