akm-cli 0.9.7 → 0.9.8-beta.2
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 +310 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/data-dir-usage.js +165 -0
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +18 -37
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/commands/workflow-cli.js +11 -61
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-io.js +21 -8
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +74 -2
- package/dist/core/state-db.js +2 -1
- package/dist/core/warn.js +15 -0
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +43 -22
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/dir-staleness.js +30 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +179 -248
- package/dist/scripts/akm-migrate.js +179 -248
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +24 -28
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +16 -1
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +15 -0
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/task-to-v3.js +10 -2
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
- package/docs/reference/cli.md +3 -1
- package/docs/reference/data-and-telemetry.md +1 -0
- package/docs/reference/tasks.md +11 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -9
- package/dist/workflows/source-ir/compare.js +0 -17
package/dist/core/common.js
CHANGED
|
@@ -277,8 +277,14 @@ function isValidDirectory(dir) {
|
|
|
277
277
|
try {
|
|
278
278
|
return fs.statSync(dir).isDirectory();
|
|
279
279
|
}
|
|
280
|
-
catch {
|
|
281
|
-
|
|
280
|
+
catch (error) {
|
|
281
|
+
// Genuinely absent — the caller's "not found" fallback applies. Any other
|
|
282
|
+
// stat failure (e.g. EACCES) is not "doesn't exist"; treating it as such
|
|
283
|
+
// produced the wrong message ("Run akm bundle create") for a directory
|
|
284
|
+
// that exists but cannot be read.
|
|
285
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
286
|
+
return false;
|
|
287
|
+
throw new ConfigError(`Unable to read bundle directory at "${dir}".`, "STASH_DIR_UNREADABLE");
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
/**
|
|
@@ -356,6 +362,10 @@ function readStashDirFromConfig() {
|
|
|
356
362
|
export function toPosix(input) {
|
|
357
363
|
return input.replace(/\\/g, "/");
|
|
358
364
|
}
|
|
365
|
+
/** Locale-independent code-point ordering — a stable `Array.prototype.sort` comparator for strings (paths, names, ids). */
|
|
366
|
+
export function compareCodePoints(left, right) {
|
|
367
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
368
|
+
}
|
|
359
369
|
export function hasErrnoCode(error, code) {
|
|
360
370
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
361
371
|
return false;
|
|
@@ -873,7 +883,61 @@ export function asNonEmptyString(value) {
|
|
|
873
883
|
const trimmed = value.trim();
|
|
874
884
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
875
885
|
}
|
|
886
|
+
// ── env-file assignment scanning ─────────────────────────────────────────────
|
|
887
|
+
/** Matches a `KEY=value` assignment line, capturing only the key. */
|
|
888
|
+
export const ENV_ASSIGN_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
889
|
+
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
890
|
+
export function scanEnvKeyNames(text) {
|
|
891
|
+
const keys = [];
|
|
892
|
+
const seen = new Set();
|
|
893
|
+
for (const line of text.split(/\r?\n/)) {
|
|
894
|
+
const m = line.match(ENV_ASSIGN_LINE_RE);
|
|
895
|
+
if (!m)
|
|
896
|
+
continue;
|
|
897
|
+
const key = m[1];
|
|
898
|
+
if (!key)
|
|
899
|
+
continue;
|
|
900
|
+
if (seen.has(key))
|
|
901
|
+
continue;
|
|
902
|
+
seen.add(key);
|
|
903
|
+
keys.push(key);
|
|
904
|
+
}
|
|
905
|
+
return keys;
|
|
906
|
+
}
|
|
907
|
+
/** True when `value` contains no lone/unpaired UTF-16 surrogate (every surrogate is part of a valid pair). */
|
|
908
|
+
export function wellFormedUnicode(value) {
|
|
909
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
910
|
+
const code = value.charCodeAt(index);
|
|
911
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
912
|
+
const next = value.charCodeAt(index + 1);
|
|
913
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
914
|
+
return false;
|
|
915
|
+
index += 1;
|
|
916
|
+
}
|
|
917
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
876
922
|
// ── Generic data utilities ───────────────────────────────────────────────────
|
|
923
|
+
/**
|
|
924
|
+
* Narrow an unknown value to a plain-ish record: an `object` that is neither
|
|
925
|
+
* `null` nor an `Array`. Does not distinguish a literal `{}` from a `Date`,
|
|
926
|
+
* `Map`, or class instance — callers that need that distinction use a
|
|
927
|
+
* stricter predicate instead.
|
|
928
|
+
*/
|
|
929
|
+
export function isRecord(value) {
|
|
930
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
931
|
+
}
|
|
932
|
+
/** `JSON.parse` that returns `undefined` instead of throwing. */
|
|
933
|
+
export function tryParseJson(raw) {
|
|
934
|
+
try {
|
|
935
|
+
return JSON.parse(raw);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
877
941
|
/**
|
|
878
942
|
* Coerce an unknown value to a filtered, trimmed string array.
|
|
879
943
|
* Non-strings and empty/whitespace-only entries are dropped.
|
|
@@ -115,17 +115,30 @@ export function backupExistingConfig(configPath, now = new Date()) {
|
|
|
115
115
|
return { timestamped, latest };
|
|
116
116
|
}
|
|
117
117
|
function pruneOldBackups(backupDir) {
|
|
118
|
+
pruneToNewest(backupDir, MAX_CONFIG_BACKUPS, (entry) => entry.isFile() && isTimestampedConfigBackup(entry.name));
|
|
119
|
+
}
|
|
120
|
+
function isTimestampedConfigBackup(name) {
|
|
121
|
+
return name.startsWith("config-") && name.endsWith(".json") && name !== "config.latest.json";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Keep the `keep` most-recently-modified entries of `dir` that `select`
|
|
125
|
+
* admits and remove the rest. Best-effort: an unreadable dir is a no-op, an
|
|
126
|
+
* unreadable entry sorts oldest (pruned first), and a failed removal is
|
|
127
|
+
* retried by the next call. Shared by the config backups above and the task
|
|
128
|
+
* migration snapshots (#897).
|
|
129
|
+
*/
|
|
130
|
+
export function pruneToNewest(dir, keep, select) {
|
|
118
131
|
let entries;
|
|
119
132
|
try {
|
|
120
|
-
entries = fs.readdirSync(
|
|
133
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
121
134
|
}
|
|
122
135
|
catch {
|
|
123
136
|
return;
|
|
124
137
|
}
|
|
125
|
-
const
|
|
126
|
-
.filter(
|
|
127
|
-
.map((
|
|
128
|
-
const full = path.join(
|
|
138
|
+
const candidates = entries
|
|
139
|
+
.filter(select)
|
|
140
|
+
.map((entry) => {
|
|
141
|
+
const full = path.join(dir, entry.name);
|
|
129
142
|
let mtime = 0;
|
|
130
143
|
try {
|
|
131
144
|
mtime = fs.statSync(full).mtimeMs;
|
|
@@ -136,12 +149,12 @@ function pruneOldBackups(backupDir) {
|
|
|
136
149
|
return { path: full, mtime };
|
|
137
150
|
})
|
|
138
151
|
.sort((a, b) => b.mtime - a.mtime);
|
|
139
|
-
for (const stale of
|
|
152
|
+
for (const stale of candidates.slice(keep)) {
|
|
140
153
|
try {
|
|
141
|
-
fs.
|
|
154
|
+
fs.rmSync(stale.path, { recursive: true, force: true });
|
|
142
155
|
}
|
|
143
156
|
catch {
|
|
144
|
-
// Best-effort prune; next
|
|
157
|
+
// Best-effort prune; the next call will retry.
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
160
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* - z.record(...) → JSON-parse value, validate
|
|
22
22
|
*/
|
|
23
23
|
import { z } from "zod";
|
|
24
|
+
import { isRecord } from "../common.js";
|
|
24
25
|
import { UsageError } from "../errors.js";
|
|
25
26
|
import { AkmConfigBaseSchema, EngineConfigSchema, listTopLevelConfigKeys } from "./config-schema.js";
|
|
26
27
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
@@ -110,7 +111,7 @@ function resolveSchemaAt(path, config, raw) {
|
|
|
110
111
|
// Cannot descend into a non-object schema.
|
|
111
112
|
return undefined;
|
|
112
113
|
}
|
|
113
|
-
existing =
|
|
114
|
+
existing = isRecord(existing) ? existing[segment] : undefined;
|
|
114
115
|
}
|
|
115
116
|
// Preserve leaf refinements/transforms for validation. Traversal unwraps at
|
|
116
117
|
// the start of each loop iteration, while coercion unwraps independently.
|
|
@@ -130,7 +131,7 @@ function selectUnionObjectOption(candidates, segment, existing, raw) {
|
|
|
130
131
|
if (requested)
|
|
131
132
|
return requested;
|
|
132
133
|
}
|
|
133
|
-
if (
|
|
134
|
+
if (isRecord(existing)) {
|
|
134
135
|
const selected = options.find((option) => Object.entries(option.shape).some(([key, field]) => key in existing && unwrap(field) instanceof z.ZodLiteral && field.safeParse(existing[key]).success));
|
|
135
136
|
if (selected)
|
|
136
137
|
return segment in selected.shape ? selected : undefined;
|
|
@@ -197,9 +198,7 @@ export function configSet(config, dotted, raw) {
|
|
|
197
198
|
// Merge a JSON object patch with the existing value before that current
|
|
198
199
|
// schema transform,
|
|
199
200
|
// otherwise the synthesized default can overwrite an explicit false.
|
|
200
|
-
const candidate = judgmentObjectPath &&
|
|
201
|
-
? deepMergeConfig(isPlainObject(existing) ? existing : {}, value)
|
|
202
|
-
: value;
|
|
201
|
+
const candidate = judgmentObjectPath && isRecord(value) ? deepMergeConfig(isRecord(existing) ? existing : {}, value) : value;
|
|
203
202
|
// Validate the coerced value against the leaf schema. This catches enum
|
|
204
203
|
// mismatches, out-of-range numbers, schema-level shape errors (writable
|
|
205
204
|
// npm/website sources via .superRefine, strict-mode unknown keys in nested
|
|
@@ -220,7 +219,7 @@ export function configSet(config, dotted, raw) {
|
|
|
220
219
|
.join("\n");
|
|
221
220
|
throw new UsageError(`Invalid value for ${dotted}:\n${lines}`, "INVALID_FLAG_VALUE");
|
|
222
221
|
}
|
|
223
|
-
const next = setPath(config, path,
|
|
222
|
+
const next = setPath(config, path, isRecord(existing) && isRecord(parsed.data) ? deepMergeConfig(existing, parsed.data) : parsed.data);
|
|
224
223
|
// Targeted invariant: defaultWriteTarget must point at a configured bundle
|
|
225
224
|
// (#464.a). Whole-config validation happens at save time; this check fires
|
|
226
225
|
// at set time so the user sees the typo immediately.
|
|
@@ -277,7 +276,7 @@ function rejectApiKeyPath(path, dotted) {
|
|
|
277
276
|
function parseObjectPatch(raw, key) {
|
|
278
277
|
try {
|
|
279
278
|
const value = JSON.parse(raw);
|
|
280
|
-
if (!
|
|
279
|
+
if (!isRecord(value))
|
|
281
280
|
throw new Error("expected an object");
|
|
282
281
|
return value;
|
|
283
282
|
}
|
|
@@ -285,9 +284,6 @@ function parseObjectPatch(raw, key) {
|
|
|
285
284
|
throw new UsageError(`Invalid JSON object for ${key}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_JSON_CONFIG_VALUE");
|
|
286
285
|
}
|
|
287
286
|
}
|
|
288
|
-
function isPlainObject(value) {
|
|
289
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
290
|
-
}
|
|
291
287
|
function recipeForApiKey(path, _dotted) {
|
|
292
288
|
if (path[0] === "embedding")
|
|
293
289
|
return "AKM_EMBED_API_KEY";
|
|
@@ -146,13 +146,16 @@ export function acquireConfigReadFence() {
|
|
|
146
146
|
*/
|
|
147
147
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
148
148
|
const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
149
|
-
// #852 (following #815):
|
|
150
|
-
// `reasoning_effort`, a documented 0.9.1 workaround —
|
|
151
|
-
// engine field they now shadow
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
149
|
+
// #852 (following #815): a config still using legacy `extraParams` keys —
|
|
150
|
+
// e.g. `reasoning_effort`, a documented 0.9.1 workaround — needs to be
|
|
151
|
+
// rewritten onto the first-class engine field they now shadow. This used
|
|
152
|
+
// to happen silently, in memory, on every load; that ran forever and never
|
|
153
|
+
// converged. The lift itself is now `akm migrate apply`'s job (see
|
|
154
|
+
// src/commands/migrate/config-extra-params.ts) and persists to disk, so a
|
|
155
|
+
// config that has not been migrated yet fails closed here instead of
|
|
156
|
+
// silently drifting from what's on disk.
|
|
155
157
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
158
|
+
const { lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
156
159
|
if (conflicts.length > 0) {
|
|
157
160
|
const lines = conflicts
|
|
158
161
|
.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`)
|
|
@@ -160,9 +163,9 @@ export function parseAndValidateConfigText(text, sourcePath) {
|
|
|
160
163
|
throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:\n${lines}\n\nEach extraParams key above has a first-class equivalent and akm will not guess which value you meant — remove the extraParams entry once the field carries the value you want.`, "INVALID_CONFIG_FILE");
|
|
161
164
|
}
|
|
162
165
|
if (lifted.length > 0) {
|
|
163
|
-
|
|
166
|
+
throw new ConfigError(`Config${where} uses deprecated extraParams keys with first-class equivalents:\n - ${lifted.join("\n - ")}\n\nRun \`akm migrate apply\` to rewrite the config file, or move the values onto the first-class fields yourself.`, "INVALID_CONFIG_FILE");
|
|
164
167
|
}
|
|
165
|
-
const parsed = AkmConfigSchema.safeParse(
|
|
168
|
+
const parsed = AkmConfigSchema.safeParse(parsedRaw);
|
|
166
169
|
if (!parsed.success) {
|
|
167
170
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
168
171
|
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
@@ -1,6 +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 { isRecord } from "./common.js";
|
|
4
5
|
export const EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS = [
|
|
5
6
|
"model",
|
|
6
7
|
"messages",
|
|
@@ -116,31 +117,34 @@ export function formatExtraParamsIssue(label, issue) {
|
|
|
116
117
|
const suffix = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("");
|
|
117
118
|
return `${label}${suffix} ${issue.message}`;
|
|
118
119
|
}
|
|
119
|
-
function isPlainObject(value) {
|
|
120
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
-
}
|
|
122
120
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `extraParams.reasoning_effort`
|
|
126
|
-
*
|
|
121
|
+
* Compute the legacy `extraParams` -> first-class-field lift for a raw
|
|
122
|
+
* parsed config object: a 0.9.1-shaped config using (e.g.)
|
|
123
|
+
* `extraParams.reasoning_effort` now needs `reasoningEffort` set instead,
|
|
124
|
+
* since that field became first-class — and therefore protected — in 0.9.2
|
|
125
|
+
* (#852, following #815).
|
|
127
126
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
127
|
+
* Pure: never touches the filesystem. Two callers use this differently:
|
|
128
|
+
* `akm migrate apply` (src/commands/migrate/config-extra-params.ts) uses the
|
|
129
|
+
* returned `config` to persist the rewrite to disk, once; `parseAndValidateConfigText`
|
|
130
|
+
* (src/core/config/config.ts) calls this only to detect whether a lift is
|
|
131
|
+
* needed and discards `config` — an unmigrated config fails closed there
|
|
132
|
+
* with a pointer to `akm migrate apply` rather than silently drifting from
|
|
133
|
+
* what's on disk. `conflicts` (an extraParams key and its first-class field
|
|
134
|
+
* set to different values) is a genuine authoring error in both callers and
|
|
135
|
+
* is never auto-resolved.
|
|
132
136
|
*/
|
|
133
137
|
export function liftLegacyEngineExtraParams(raw) {
|
|
134
138
|
const lifted = [];
|
|
135
139
|
const conflicts = [];
|
|
136
140
|
const rawEngines = raw.engines;
|
|
137
|
-
if (!
|
|
141
|
+
if (!isRecord(rawEngines)) {
|
|
138
142
|
return { config: raw, lifted, conflicts };
|
|
139
143
|
}
|
|
140
144
|
const engines = {};
|
|
141
145
|
let anyEngineChanged = false;
|
|
142
146
|
for (const [name, engineValue] of Object.entries(rawEngines)) {
|
|
143
|
-
if (!
|
|
147
|
+
if (!isRecord(engineValue) || !isRecord(engineValue.extraParams)) {
|
|
144
148
|
engines[name] = engineValue;
|
|
145
149
|
continue;
|
|
146
150
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
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
4
|
import { cloneExecutionJsonObject } from "../execution/json.js";
|
|
5
|
+
import { isRecord } from "./common.js";
|
|
5
6
|
const COMMON_FIELDS = [
|
|
6
7
|
"schemaVersion",
|
|
7
8
|
"ok",
|
|
@@ -50,9 +51,6 @@ const V2_FIELDS = new Set([...COMMON_FIELDS, "strategy", "strategyFilteredRefs"]
|
|
|
50
51
|
function fail(message) {
|
|
51
52
|
throw new Error(`invalid improve-result envelope: ${message}`);
|
|
52
53
|
}
|
|
53
|
-
function isRecord(value) {
|
|
54
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
-
}
|
|
56
54
|
function requireExactFields(value, allowed) {
|
|
57
55
|
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
58
56
|
if (unknown.length > 0)
|
package/dist/core/json-schema.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
* exactly that for `output:` and `params` schemas). It deliberately does NOT
|
|
54
54
|
* change {@link validateJsonSchemaSubset}'s permissive evaluation semantics.
|
|
55
55
|
*/
|
|
56
|
+
import { isRecord } from "./common.js";
|
|
56
57
|
/**
|
|
57
58
|
* Deepest schema nesting either walk descends into — {@link
|
|
58
59
|
* validateJsonSchemaSubset} evaluating a value, and {@link
|
|
@@ -168,9 +169,6 @@ function pointerFor(path) {
|
|
|
168
169
|
function pushIssue(issues, path, keyword, kind, message) {
|
|
169
170
|
issues.push({ path: [...path], pointer: pointerFor(path), keyword, kind, message });
|
|
170
171
|
}
|
|
171
|
-
function isPlainObject(value) {
|
|
172
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
|
-
}
|
|
174
172
|
/** Values the runtime's reference-equality `enum` check can enforce correctly. */
|
|
175
173
|
function isSupportedEnumValue(value) {
|
|
176
174
|
return (value === null ||
|
|
@@ -228,7 +226,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
|
|
|
228
226
|
continue;
|
|
229
227
|
}
|
|
230
228
|
branches.forEach((branch, index) => {
|
|
231
|
-
if (
|
|
229
|
+
if (isRecord(branch)) {
|
|
232
230
|
checkDefinitionNode(branch, [...path, keyword, index], issues, depth + 1);
|
|
233
231
|
}
|
|
234
232
|
else {
|
|
@@ -237,7 +235,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
|
|
|
237
235
|
});
|
|
238
236
|
}
|
|
239
237
|
if (schema.not !== undefined) {
|
|
240
|
-
if (
|
|
238
|
+
if (isRecord(schema.not)) {
|
|
241
239
|
checkDefinitionNode(schema.not, [...path, "not"], issues, depth + 1);
|
|
242
240
|
}
|
|
243
241
|
else {
|
|
@@ -250,12 +248,12 @@ function checkDefinitionNode(schema, path, issues, depth) {
|
|
|
250
248
|
}
|
|
251
249
|
}
|
|
252
250
|
if (schema.properties !== undefined) {
|
|
253
|
-
if (!
|
|
251
|
+
if (!isRecord(schema.properties)) {
|
|
254
252
|
pushIssue(issues, [...path, "properties"], "properties", "malformed", `"properties" must be an object mapping property names to schemas`);
|
|
255
253
|
}
|
|
256
254
|
else {
|
|
257
255
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
258
|
-
if (
|
|
256
|
+
if (isRecord(propSchema)) {
|
|
259
257
|
checkDefinitionNode(propSchema, [...path, "properties", key], issues, depth + 1);
|
|
260
258
|
}
|
|
261
259
|
else {
|
|
@@ -265,7 +263,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
|
|
|
265
263
|
}
|
|
266
264
|
}
|
|
267
265
|
if (schema.items !== undefined) {
|
|
268
|
-
if (
|
|
266
|
+
if (isRecord(schema.items)) {
|
|
269
267
|
checkDefinitionNode(schema.items, [...path, "items"], issues, depth + 1);
|
|
270
268
|
}
|
|
271
269
|
else if (Array.isArray(schema.items)) {
|
|
@@ -276,7 +274,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
|
|
|
276
274
|
}
|
|
277
275
|
}
|
|
278
276
|
if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") {
|
|
279
|
-
if (
|
|
277
|
+
if (isRecord(schema.additionalProperties)) {
|
|
280
278
|
pushIssue(issues, [...path, "additionalProperties"], "additionalProperties", "unsupported", `schema-form "additionalProperties" is not enforced by the workflow schema subset — only "additionalProperties: false" is`);
|
|
281
279
|
}
|
|
282
280
|
else {
|
|
@@ -329,7 +327,7 @@ function combinatorBranches(schema, keyword) {
|
|
|
329
327
|
const raw = schema[keyword];
|
|
330
328
|
if (!Array.isArray(raw))
|
|
331
329
|
return [];
|
|
332
|
-
return raw.filter(
|
|
330
|
+
return raw.filter(isRecord);
|
|
333
331
|
}
|
|
334
332
|
/** First error of each failing branch, truncated — enough to act on without dumping every branch. */
|
|
335
333
|
function summarizeBranchFailures(failures) {
|
|
@@ -364,7 +362,7 @@ function validateCombinators(value, schema, path, ctx) {
|
|
|
364
362
|
}
|
|
365
363
|
}
|
|
366
364
|
const not = schema.not;
|
|
367
|
-
if (
|
|
365
|
+
if (isRecord(not) && branchErrors(value, not, path, ctx).length === 0) {
|
|
368
366
|
ctx.errors.push(`${path}: value must not match the "not" schema`);
|
|
369
367
|
}
|
|
370
368
|
}
|
|
@@ -35,6 +35,8 @@ export const STATE_MIGRATION_SAFETY_BY_ID = Object.freeze({
|
|
|
35
35
|
"022-workflow-unit-attempts": "additive",
|
|
36
36
|
"023-child-workflow-runs": "additive",
|
|
37
37
|
"024-workflow-run-outputs": "additive",
|
|
38
|
+
"025-task-history-vocabulary-backfill": "data-preserving-rebuild",
|
|
39
|
+
"026-proposals-strip-legacy-fragment-refs": "data-preserving-rebuild",
|
|
38
40
|
});
|
|
39
41
|
export const STATE_MIGRATIONS = [
|
|
40
42
|
// ── Migration 001 — initial schema ──────────────────────────────────────────
|
|
@@ -1079,6 +1081,74 @@ export const STATE_MIGRATIONS = [
|
|
|
1079
1081
|
ALTER TABLE workflow_runs ADD COLUMN outputs_json TEXT;
|
|
1080
1082
|
`,
|
|
1081
1083
|
},
|
|
1084
|
+
// ── Migration 025 — task_history D8 result-vocabulary backfill ─────────────
|
|
1085
|
+
//
|
|
1086
|
+
// The D8 re-code (docs/architecture/decisions/0005-task-result-vocabulary-
|
|
1087
|
+
// and-legacy-read-mapping.md) renamed the written `target_kind` vocabulary
|
|
1088
|
+
// and marked every new row with `targetVocab: 2` in `metadata_json`, but
|
|
1089
|
+
// left every row written before it in the OLD vocabulary forever, pushing
|
|
1090
|
+
// the "which generation is this row" decision onto three separate read
|
|
1091
|
+
// sites (src/tasks/run/task-history.ts, src/commands/health/
|
|
1092
|
+
// improve-metrics.ts, src/commands/health/windows.ts). That is exactly the
|
|
1093
|
+
// shape `docs/plans/specs/p4-deletions-closeout.md` row B-51 ratified
|
|
1094
|
+
// keeping "forever" — SUPERSEDED here: `task_history` is DB-owned data, the
|
|
1095
|
+
// remap is deterministic and total, so it belongs in a one-time migration,
|
|
1096
|
+
// not three permanently-recurring read-side shims.
|
|
1097
|
+
//
|
|
1098
|
+
// The remap, applied only to rows with no `targetVocab` marker yet (a
|
|
1099
|
+
// NULL/absent `$.targetVocab` in the JSON column — already-marked or
|
|
1100
|
+
// malformed metadata_json is left untouched; malformed rows are a
|
|
1101
|
+
// pre-existing, unrelated skip-and-warn concern at read time):
|
|
1102
|
+
// - target_kind 'prompt' -> 'command' (the prepared command/agent-LLM
|
|
1103
|
+
// arm; D8 renamed its written label, this backfills the old one)
|
|
1104
|
+
// - target_kind 'command' -> 'shell' (the legacy native shell/script
|
|
1105
|
+
// arm's shared old label; D8 split it into 'shell'/'script', and only
|
|
1106
|
+
// 'shell' is reconstructable — the old vocabulary never distinguished
|
|
1107
|
+
// script from shell, matching the read mapping this migration retires)
|
|
1108
|
+
// Only 'prompt' and legacy 'command' rows actually change meaning between
|
|
1109
|
+
// vocabularies (see the read mapping this migration retires); every other
|
|
1110
|
+
// target_kind ('workflow', NULL, or anything else) already reads
|
|
1111
|
+
// identically in both, so it is left alone — including its metadata_json,
|
|
1112
|
+
// since no reader consults `targetVocab` once the shim is deleted.
|
|
1113
|
+
{
|
|
1114
|
+
id: "025-task-history-vocabulary-backfill",
|
|
1115
|
+
up: `
|
|
1116
|
+
UPDATE task_history
|
|
1117
|
+
SET target_kind = 'command',
|
|
1118
|
+
metadata_json = json_set(metadata_json, '$.targetVocab', 2)
|
|
1119
|
+
WHERE target_kind = 'prompt'
|
|
1120
|
+
AND json_valid(metadata_json)
|
|
1121
|
+
AND json_extract(metadata_json, '$.targetVocab') IS NULL;
|
|
1122
|
+
|
|
1123
|
+
UPDATE task_history
|
|
1124
|
+
SET target_kind = 'shell',
|
|
1125
|
+
metadata_json = json_set(metadata_json, '$.targetVocab', 2)
|
|
1126
|
+
WHERE target_kind = 'command'
|
|
1127
|
+
AND json_valid(metadata_json)
|
|
1128
|
+
AND json_extract(metadata_json, '$.targetVocab') IS NULL;
|
|
1129
|
+
`,
|
|
1130
|
+
},
|
|
1131
|
+
// ── Migration 026 — strip the retired proposal-ref export fragment (#898) ──
|
|
1132
|
+
//
|
|
1133
|
+
// Older releases could write a proposal `ref` carrying the export-fragment
|
|
1134
|
+
// selector (`[bundle//]conceptId#fragment`, see src/core/asset/asset-ref.ts).
|
|
1135
|
+
// `currentProposalRef` (proposals-repository.ts) now rejects any fragment, so
|
|
1136
|
+
// those rows failed to parse on every read and could not be listed, repaired,
|
|
1137
|
+
// or deleted through the CLI. Every surface that still reads an archived row
|
|
1138
|
+
// is concept-scoped, so the fragment carries nothing they use.
|
|
1139
|
+
//
|
|
1140
|
+
// `#` is legal in a ref only as the fragment separator, so truncating at the
|
|
1141
|
+
// first `#` is exactly the split `parseBundleRef` performs. `ref` has no
|
|
1142
|
+
// UNIQUE constraint, and no writer ever produced an empty concept id, so
|
|
1143
|
+
// every legacy row normalizes without a drop path.
|
|
1144
|
+
{
|
|
1145
|
+
id: "026-proposals-strip-legacy-fragment-refs",
|
|
1146
|
+
up: `
|
|
1147
|
+
UPDATE proposals
|
|
1148
|
+
SET ref = substr(ref, 1, instr(ref, '#') - 1)
|
|
1149
|
+
WHERE ref LIKE '%#%';
|
|
1150
|
+
`,
|
|
1151
|
+
},
|
|
1082
1152
|
];
|
|
1083
1153
|
assertMigrationRegistry(STATE_MIGRATIONS);
|
|
1084
1154
|
function assertStateMigrationSafetyRegistry() {
|
|
@@ -1117,7 +1187,8 @@ export function runMigrations(db, options) {
|
|
|
1117
1187
|
}
|
|
1118
1188
|
if (!options.allowHistoricalDestructiveStateUpgrade) {
|
|
1119
1189
|
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
1120
|
-
"Run `akm upgrade --force` to snapshot it before migration 001
|
|
1190
|
+
"Run `akm upgrade --force` to snapshot it before migration 001, " +
|
|
1191
|
+
"or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
|
|
1121
1192
|
}
|
|
1122
1193
|
const ledger = assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
|
|
1123
1194
|
if (ledger.migrationIds.length !== 0) {
|
|
@@ -1147,7 +1218,8 @@ export function runMigrations(db, options) {
|
|
|
1147
1218
|
assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
|
|
1148
1219
|
if (!options?.allowHistoricalDestructiveStateUpgrade) {
|
|
1149
1220
|
throw new Error(`Refusing to apply historical destructive state migration ${migration.id} during an ordinary managed open. ` +
|
|
1150
|
-
"Run `akm upgrade --force` to create a sibling state.db safety copy and apply it deliberately
|
|
1221
|
+
"Run `akm upgrade --force` to create a sibling state.db safety copy and apply it deliberately, " +
|
|
1222
|
+
"or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
|
|
1151
1223
|
}
|
|
1152
1224
|
if (!options.beforeHistoricalDestructiveMigration) {
|
|
1153
1225
|
throw new Error(`Historical destructive state migration ${migration.id} requires a verified safety-copy hook.`);
|
package/dist/core/state-db.js
CHANGED
|
@@ -407,7 +407,8 @@ export function openStateDatabase(dbPath, options) {
|
|
|
407
407
|
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
408
408
|
if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
|
|
409
409
|
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
410
|
-
"Run `akm upgrade --force` to create a verified snapshot before migration 001
|
|
410
|
+
"Run `akm upgrade --force` to create a verified snapshot before migration 001, " +
|
|
411
|
+
"or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
finally {
|
package/dist/core/warn.js
CHANGED
|
@@ -122,6 +122,21 @@ export function warn(...args) {
|
|
|
122
122
|
console.warn(...args);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
const warnedOnceKeys = new Set();
|
|
126
|
+
/**
|
|
127
|
+
* `warn()` at most once per process for a given `key`. For diagnostics that
|
|
128
|
+
* would otherwise repeat on every read of the same bad row or value.
|
|
129
|
+
*/
|
|
130
|
+
export function warnOnce(key, ...args) {
|
|
131
|
+
if (warnedOnceKeys.has(key))
|
|
132
|
+
return;
|
|
133
|
+
warnedOnceKeys.add(key);
|
|
134
|
+
warn(...args);
|
|
135
|
+
}
|
|
136
|
+
/** TEST-ONLY. Forget every `warnOnce` key so a test can re-trigger a warning. */
|
|
137
|
+
export function _resetWarnOnceForTests() {
|
|
138
|
+
warnedOnceKeys.clear();
|
|
139
|
+
}
|
|
125
140
|
/**
|
|
126
141
|
* Emit an error to stderr unless --quiet is active.
|
|
127
142
|
* Always written to the log file if one is active.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { isRecord } from "../core/common.js";
|
|
7
8
|
import { UsageError } from "../core/errors.js";
|
|
8
9
|
export function freezeExecutableIdentity(requested, options = {}) {
|
|
9
10
|
const absolutePath = resolveExecutable(requested, options.cwd ?? process.cwd(), options.path ?? process.env.PATH ?? "");
|
|
@@ -96,9 +97,6 @@ function resolveExecutable(requested, cwd, pathValue) {
|
|
|
96
97
|
}
|
|
97
98
|
throw new UsageError(`Executable ${JSON.stringify(requested)} was not found.`, "INVALID_FLAG_VALUE");
|
|
98
99
|
}
|
|
99
|
-
function isRecord(value) {
|
|
100
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
101
|
-
}
|
|
102
100
|
function invalid(label) {
|
|
103
101
|
return new UsageError(`Frozen ${label} identity is invalid.`, "INVALID_FLAG_VALUE");
|
|
104
102
|
}
|
|
@@ -5,17 +5,12 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { parseFrontmatter } from "../core/asset/frontmatter.js";
|
|
8
|
+
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
8
9
|
import { UsageError } from "../core/errors.js";
|
|
9
10
|
import { createExecutionSourceIdentity } from "./source.js";
|
|
10
11
|
function errorMessage(cause) {
|
|
11
12
|
return cause instanceof Error ? cause.message : String(cause);
|
|
12
13
|
}
|
|
13
|
-
function toPosix(value) {
|
|
14
|
-
return value.replaceAll("\\", "/");
|
|
15
|
-
}
|
|
16
|
-
function compareCodePoints(left, right) {
|
|
17
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
18
|
-
}
|
|
19
14
|
function physicalIdentity(realPath, stat) {
|
|
20
15
|
return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
|
|
21
16
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import { loadConfig } from "../core/config/config.js";
|
|
32
|
+
import { rethrowIfTestIsolationError } from "../core/errors.js";
|
|
32
33
|
import { getDbPath } from "../core/paths.js";
|
|
33
34
|
import { warn } from "../core/warn.js";
|
|
34
35
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
@@ -50,7 +51,11 @@ function indexBundlePrefixes(dbPath) {
|
|
|
50
51
|
.prepare("SELECT DISTINCT bundle_id AS b FROM entries WHERE bundle_id IS NOT NULL AND bundle_id != ''")
|
|
51
52
|
.all().map((row) => row.b);
|
|
52
53
|
}
|
|
53
|
-
catch {
|
|
54
|
+
catch (error) {
|
|
55
|
+
rethrowIfTestIsolationError(error);
|
|
56
|
+
// Best-effort startup diagnostic: a locked/corrupted index just means this
|
|
57
|
+
// pass skips the drift check (retried on the next command) rather than
|
|
58
|
+
// blocking every command on a heuristic warning.
|
|
54
59
|
return undefined;
|
|
55
60
|
}
|
|
56
61
|
finally {
|