akm-cli 0.9.2-alpha.5 → 0.9.3
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 +67 -0
- package/dist/commands/tasks/tasks.js +32 -7
- package/dist/core/config/config.js +19 -3
- package/dist/core/extra-params.js +115 -1
- package/dist/scripts/akm-migrate-node.js +110 -11
- package/dist/scripts/akm-migrate.js +110 -11
- package/dist/tasks/run/run-native-task.js +28 -1
- package/dist/tasks/scheduler-sync.js +22 -2
- package/dist/workflows/runtime/plan-classifier.js +5 -2
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/README.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +4 -4
- package/docs/migration/v0.9.0-troubleshooting.md +6 -6
- package/docs/reference/configuration.md +3 -2
- package/docs/reference/workflow-schema.md +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,75 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.3] - 2026-08-29
|
|
10
|
+
|
|
9
11
|
### Fixed
|
|
10
12
|
|
|
13
|
+
- **A failing Windows `shell: powershell`/`pwsh` task could lose its exit
|
|
14
|
+
code's fidelity through `-Command` (#845).** `-Command` derives its own
|
|
15
|
+
process exit code from `$?`, so a genuinely failing command already
|
|
16
|
+
produced a nonzero exit and `status: "failed"` — but any native exit code
|
|
17
|
+
outside `{0, 1}` was collapsed to `1`, discarding the real value reported
|
|
18
|
+
in task history. The inner shell invocation built by `shellCommand()` now
|
|
19
|
+
appends a guard that reads `$?` first (reproducing `-Command`'s own
|
|
20
|
+
completed/failed determination, immune to a stale `$LASTEXITCODE` from an
|
|
21
|
+
earlier native call in the same command) and only then upgrades to the
|
|
22
|
+
precise native exit code when the failing last statement actually set
|
|
23
|
+
one — a bare `exit $LASTEXITCODE` was rejected because `$LASTEXITCODE`
|
|
24
|
+
stays `$null` for a pure-PowerShell command, and `exit $null` is exit
|
|
25
|
+
code `0`, which would have turned a failed cmdlet into a false
|
|
26
|
+
`"completed"`. Verified via unit tests asserting the exact argv `-Command`
|
|
27
|
+
string on `platform: "win32"`; the runtime exit-code behavior itself is
|
|
28
|
+
unverified on a real Windows host pending the owner's manual run (see the
|
|
29
|
+
PR body for the procedure) — Windows scheduler paths still have no
|
|
30
|
+
automated coverage (#770).
|
|
31
|
+
- **A valid 0.9.1 config hard-failed to load after upgrading to 0.9.2
|
|
32
|
+
(#852).** `reasoningEffort` became a first-class `engines.<name>` field in
|
|
33
|
+
0.9.2 (#815), so `extraParams.reasoning_effort` — the documented 0.9.1
|
|
34
|
+
workaround for LM Studio, where `enableThinking` is a no-op — started
|
|
35
|
+
tripping the extraParams protected-key check and hard-failing every
|
|
36
|
+
command that loads config, with no migration. Config load now lifts
|
|
37
|
+
`extraParams.{reasoning_effort,temperature,maxtokens,enablethinking}`
|
|
38
|
+
onto their first-class field automatically (in-memory only; the file is
|
|
39
|
+
not rewritten, and a warning names each lifted key) unless the
|
|
40
|
+
extraParams value and the first-class field disagree, in which case
|
|
41
|
+
config load still fails, now naming both values and the field to keep.
|
|
42
|
+
Any other protected `extraParams` key without a first-class equivalent
|
|
43
|
+
still hard-fails, but the error now names the fix instead of only the
|
|
44
|
+
rule.
|
|
45
|
+
|
|
46
|
+
## [0.9.2] - 2026-08-29
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **A blocked workflow run became permanently unresumable after `akm
|
|
51
|
+
workflow abandon` (#847).** Abandon correctly moved the run to `failed`
|
|
52
|
+
but left its current step `blocked`; the durable-spine validator then
|
|
53
|
+
rejected that honest abandoned shape as corruption before `resume` could
|
|
54
|
+
reopen the step. Failed-run validation now accepts the three legitimate
|
|
55
|
+
current-step states — `pending` for an abandoned active run, `blocked` for
|
|
56
|
+
an abandoned blocked run, and `failed` for an execution failure — and
|
|
57
|
+
`resume` normalizes each back to `pending` as promised by the CLI help.
|
|
58
|
+
|
|
59
|
+
- **`akm task sync` could compute another bundle's real scheduler entries as
|
|
60
|
+
drift and try to remove them (#846).** Removal was scoped by a bundle
|
|
61
|
+
*display name* (`bundleName`/`--bundle` target) — a value derived from a
|
|
62
|
+
directory basename that two unrelated bundles can legitimately share (an
|
|
63
|
+
unconfigured bundle's name is deduped only against its own config's
|
|
64
|
+
bundles, never against other bundles actually installed on the machine).
|
|
65
|
+
A primary/unconfigured-bundle sync now additionally confirms a
|
|
66
|
+
name-matching installed entry's *resolved bundle path*, recovered from
|
|
67
|
+
that entry's own scheduler-context descriptor, before treating it as
|
|
68
|
+
eligible for reconcile — and refuses (rather than assumes) when that path
|
|
69
|
+
can't be established. **Backward compatibility:** every entry installed by
|
|
70
|
+
this codebase already carries a scheduler-context descriptor (the
|
|
71
|
+
`--scheduler-context` file used to restore the scheduled process's
|
|
72
|
+
environment), so existing installations resolve correctly with no user
|
|
73
|
+
action required. An entry whose descriptor is missing, unreadable, or
|
|
74
|
+
owned by a different OS user is never assumed to belong to the invoking
|
|
75
|
+
bundle; such an entry is simply left untouched by sync (it will not be
|
|
76
|
+
auto-repaired or removed) until it is reinstalled or removed by hand.
|
|
77
|
+
|
|
11
78
|
- **Scheduled tasks on Windows ran but recorded no output.** A task fired by
|
|
12
79
|
Task Scheduler logged `exit_code=0` with an empty log: the command really
|
|
13
80
|
ran, but nothing it printed was captured. Captured runs asked for their own
|
|
@@ -337,18 +337,34 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
337
337
|
}
|
|
338
338
|
const inspection = await sched.inspectBindings({ rebind: options.rebind === true });
|
|
339
339
|
const rawEntries = [...inspection.installed];
|
|
340
|
-
const allEntries = rawEntries.map((entry) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
340
|
+
const allEntries = rawEntries.map((entry) => {
|
|
341
|
+
const contextPath = "contextPath" in entry ? entry.contextPath : "";
|
|
342
|
+
// #846: recover the resolved bundle path this entry was installed
|
|
343
|
+
// under from its own scheduler-context descriptor. Any failure (no
|
|
344
|
+
// descriptor, unreadable, corrupt, owned by another user) leaves
|
|
345
|
+
// ownerBundlePath unset — belongsToBundle must never treat that as
|
|
346
|
+
// "mine".
|
|
347
|
+
const ownerBundlePath = contextPath ? resolveInstalledOwnerPath(contextPath) : undefined;
|
|
348
|
+
return {
|
|
349
|
+
...entry,
|
|
350
|
+
...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
|
|
351
|
+
...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
|
|
352
|
+
binding: "binding" in entry ? [...entry.binding] : [],
|
|
353
|
+
contextPath,
|
|
354
|
+
...(ownerBundlePath !== undefined ? { ownerBundlePath } : {}),
|
|
355
|
+
};
|
|
356
|
+
});
|
|
347
357
|
const nativeArtifacts = inspection.artifacts;
|
|
348
358
|
const common = {
|
|
349
359
|
sourceRoot: stashDir,
|
|
350
360
|
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
351
361
|
bundleName: resolved.source.name,
|
|
362
|
+
// #846: only meaningful for a primary/unconfigured-bundle sync. A
|
|
363
|
+
// `--bundle <target>` entry's scheduler-context descriptor records the
|
|
364
|
+
// invoking process's OWN primary AKM_BUNDLE_DIR, not the targeted
|
|
365
|
+
// bundle's directory, so path-scoping stays gated on the case it's
|
|
366
|
+
// actually valid for (see belongsToBundle).
|
|
367
|
+
...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
|
|
352
368
|
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
353
369
|
backend: sched.name,
|
|
354
370
|
installed: allEntries,
|
|
@@ -773,6 +789,15 @@ function groupInstalledBindings(entries, invocation) {
|
|
|
773
789
|
}
|
|
774
790
|
return [...groups.values()].map((group) => ({ ...group, taskIds: group.taskIds.sort() }));
|
|
775
791
|
}
|
|
792
|
+
/** Best-effort recovery of an installed binding's owning bundle path (#846). */
|
|
793
|
+
function resolveInstalledOwnerPath(contextPath) {
|
|
794
|
+
try {
|
|
795
|
+
return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
776
801
|
function inspectInstalledBinding(entry, invocation) {
|
|
777
802
|
const status = [];
|
|
778
803
|
const binding = entry.binding;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { ConfigError } from "../errors.js";
|
|
7
|
+
import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
7
8
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
8
9
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
9
10
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
@@ -141,14 +142,29 @@ export function acquireConfigReadFence() {
|
|
|
141
142
|
* canonical shape before defaults are merged.
|
|
142
143
|
*/
|
|
143
144
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
145
|
+
const parsedRaw = parseConfigText(text, sourcePath);
|
|
146
|
+
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
146
147
|
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
147
148
|
}
|
|
149
|
+
// #852 (following #815): lift legacy `extraParams` keys — e.g.
|
|
150
|
+
// `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
|
|
151
|
+
// engine field they now shadow, before the protected-key check in
|
|
152
|
+
// `ExtraParamsSchema` gets a chance to hard-reject them. In-memory only;
|
|
153
|
+
// never written back to the file.
|
|
154
|
+
const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
155
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
156
|
+
if (conflicts.length > 0) {
|
|
157
|
+
const lines = conflicts
|
|
158
|
+
.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`)
|
|
159
|
+
.join("\n");
|
|
160
|
+
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
|
+
}
|
|
162
|
+
if (lifted.length > 0) {
|
|
163
|
+
warn(`Config${where} uses deprecated extraParams keys with first-class equivalents; treating them as the first-class fields for this run (not written back to the file):\n - ${lifted.join("\n - ")}`);
|
|
164
|
+
}
|
|
148
165
|
const parsed = AkmConfigSchema.safeParse(raw);
|
|
149
166
|
if (!parsed.success) {
|
|
150
167
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
151
|
-
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
152
168
|
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
153
169
|
}
|
|
154
170
|
const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
|
|
@@ -25,6 +25,46 @@ export const EXTRA_PARAMS_CREDENTIAL_KEYS = [
|
|
|
25
25
|
];
|
|
26
26
|
const PROTECTED_TOP_LEVEL_KEYS = new Set(EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS);
|
|
27
27
|
const CREDENTIAL_KEYS = new Set(EXTRA_PARAMS_CREDENTIAL_KEYS);
|
|
28
|
+
// ── Protected-key remedies (#852) ───────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// `EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS` rightly stops a provider extra from
|
|
31
|
+
// shadowing an AKM-managed field, but naming the rule without naming the fix
|
|
32
|
+
// leaves the reader stuck (#852). Every protected key gets a one-line remedy
|
|
33
|
+
// baked into its issue message; keys with a genuine scalar first-class field
|
|
34
|
+
// are also eligible for the automatic config-load lift below.
|
|
35
|
+
/** normalizeExtraParamKey(key) -> the first-class engine field it shadows. */
|
|
36
|
+
const LEGACY_EXTRA_PARAMS_FIELD = {
|
|
37
|
+
model: "model",
|
|
38
|
+
temperature: "temperature",
|
|
39
|
+
maxtokens: "maxTokens",
|
|
40
|
+
enablethinking: "enableThinking",
|
|
41
|
+
reasoningeffort: "reasoningEffort",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Subset of {@link LEGACY_EXTRA_PARAMS_FIELD} that {@link liftLegacyEngineExtraParams}
|
|
45
|
+
* will move onto the first-class field automatically. `model` is deliberately
|
|
46
|
+
* excluded: unlike the others it was never a "no first-class field yet"
|
|
47
|
+
* workaround (`model` has always been required on an LLM engine), so a
|
|
48
|
+
* mismatch is far more likely a genuine mistake than a stale 0.9.1 config —
|
|
49
|
+
* it stays a hard rejection rather than being silently reinterpreted.
|
|
50
|
+
*/
|
|
51
|
+
const LIFTABLE_EXTRA_PARAMS_KEYS = new Set(["temperature", "maxtokens", "enablethinking", "reasoningeffort"]);
|
|
52
|
+
/** Remedies for protected keys with no scalar first-class field to lift onto. */
|
|
53
|
+
const EXTRA_PARAMS_NO_FIELD_REMEDY = {
|
|
54
|
+
messages: "AKM builds the request messages internally — remove it from extraParams",
|
|
55
|
+
responseformat: "AKM controls the response format internally — remove it from extraParams",
|
|
56
|
+
stream: "AKM controls streaming internally — remove it from extraParams",
|
|
57
|
+
streamoptions: "AKM controls streaming internally — remove it from extraParams",
|
|
58
|
+
chattemplatekwargs: "set engines.<name>.enableThinking instead of chat_template_kwargs.enable_thinking",
|
|
59
|
+
};
|
|
60
|
+
function protectedKeyRemedy(normalized) {
|
|
61
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
62
|
+
if (field) {
|
|
63
|
+
const note = normalized === "reasoningeffort" ? " (moved to a first-class field in 0.9.2)" : "";
|
|
64
|
+
return `set engines.<name>.${field} instead${note}`;
|
|
65
|
+
}
|
|
66
|
+
return EXTRA_PARAMS_NO_FIELD_REMEDY[normalized];
|
|
67
|
+
}
|
|
28
68
|
export function normalizeExtraParamKey(key) {
|
|
29
69
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
30
70
|
}
|
|
@@ -57,7 +97,11 @@ export function validateExtraParams(value) {
|
|
|
57
97
|
for (const [key, child] of Object.entries(entry)) {
|
|
58
98
|
const normalized = normalizeExtraParamKey(key);
|
|
59
99
|
if (path.length === 0 && PROTECTED_TOP_LEVEL_KEYS.has(normalized)) {
|
|
60
|
-
|
|
100
|
+
const remedy = protectedKeyRemedy(normalized);
|
|
101
|
+
issues.push({
|
|
102
|
+
path: [key],
|
|
103
|
+
message: remedy ? `${key} is protected by AKM — ${remedy}.` : `${key} is protected by AKM`,
|
|
104
|
+
});
|
|
61
105
|
}
|
|
62
106
|
if (CREDENTIAL_KEYS.has(normalized)) {
|
|
63
107
|
issues.push({ path: [...path, key], message: `${key} cannot carry credentials` });
|
|
@@ -72,3 +116,73 @@ export function formatExtraParamsIssue(label, issue) {
|
|
|
72
116
|
const suffix = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("");
|
|
73
117
|
return `${label}${suffix} ${issue.message}`;
|
|
74
118
|
}
|
|
119
|
+
function isPlainObject(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Lift legacy `extraParams` keys onto their first-class engine field before
|
|
124
|
+
* schema validation runs, so a 0.9.1-shaped config using (e.g.)
|
|
125
|
+
* `extraParams.reasoning_effort` keeps loading now that `reasoningEffort` is
|
|
126
|
+
* a first-class — and therefore protected — field (#852, following #815).
|
|
127
|
+
*
|
|
128
|
+
* In-memory only: this never rewrites the config file. Callers should warn
|
|
129
|
+
* using the returned `lifted` descriptions so the user knows to update the
|
|
130
|
+
* file by hand, and reject using `conflicts` rather than silently preferring
|
|
131
|
+
* either value.
|
|
132
|
+
*/
|
|
133
|
+
export function liftLegacyEngineExtraParams(raw) {
|
|
134
|
+
const lifted = [];
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const rawEngines = raw.engines;
|
|
137
|
+
if (!isPlainObject(rawEngines)) {
|
|
138
|
+
return { config: raw, lifted, conflicts };
|
|
139
|
+
}
|
|
140
|
+
const engines = {};
|
|
141
|
+
let anyEngineChanged = false;
|
|
142
|
+
for (const [name, engineValue] of Object.entries(rawEngines)) {
|
|
143
|
+
if (!isPlainObject(engineValue) || !isPlainObject(engineValue.extraParams)) {
|
|
144
|
+
engines[name] = engineValue;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const engine = { ...engineValue };
|
|
148
|
+
const extraParams = { ...engineValue.extraParams };
|
|
149
|
+
let engineChanged = false;
|
|
150
|
+
for (const [rawKey, value] of Object.entries(engineValue.extraParams)) {
|
|
151
|
+
const normalized = normalizeExtraParamKey(rawKey);
|
|
152
|
+
if (!LIFTABLE_EXTRA_PARAMS_KEYS.has(normalized))
|
|
153
|
+
continue;
|
|
154
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
155
|
+
if (!field)
|
|
156
|
+
continue;
|
|
157
|
+
const existing = engine[field];
|
|
158
|
+
if (existing !== undefined && existing !== value) {
|
|
159
|
+
conflicts.push({ engine: name, key: rawKey, field, extraParamsValue: value, fieldValue: existing });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
delete extraParams[rawKey];
|
|
163
|
+
engineChanged = true;
|
|
164
|
+
if (existing === value) {
|
|
165
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} is redundant — engines.${name}.${field} is already set to the same value; dropped the extraParams entry`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
engine[field] = value;
|
|
169
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} -> engines.${name}.${field}`);
|
|
170
|
+
}
|
|
171
|
+
if (!engineChanged) {
|
|
172
|
+
engines[name] = engineValue;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
anyEngineChanged = true;
|
|
176
|
+
if (Object.keys(extraParams).length > 0) {
|
|
177
|
+
engine.extraParams = extraParams;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
delete engine.extraParams;
|
|
181
|
+
}
|
|
182
|
+
engines[name] = engine;
|
|
183
|
+
}
|
|
184
|
+
if (!anyEngineChanged) {
|
|
185
|
+
return { config: raw, lifted, conflicts };
|
|
186
|
+
}
|
|
187
|
+
return { config: { ...raw, engines }, lifted, conflicts };
|
|
188
|
+
}
|
|
@@ -16471,6 +16471,29 @@ var EXTRA_PARAMS_CREDENTIAL_KEYS = [
|
|
|
16471
16471
|
];
|
|
16472
16472
|
var PROTECTED_TOP_LEVEL_KEYS = new Set(EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS);
|
|
16473
16473
|
var CREDENTIAL_KEYS = new Set(EXTRA_PARAMS_CREDENTIAL_KEYS);
|
|
16474
|
+
var LEGACY_EXTRA_PARAMS_FIELD = {
|
|
16475
|
+
model: "model",
|
|
16476
|
+
temperature: "temperature",
|
|
16477
|
+
maxtokens: "maxTokens",
|
|
16478
|
+
enablethinking: "enableThinking",
|
|
16479
|
+
reasoningeffort: "reasoningEffort"
|
|
16480
|
+
};
|
|
16481
|
+
var LIFTABLE_EXTRA_PARAMS_KEYS = new Set(["temperature", "maxtokens", "enablethinking", "reasoningeffort"]);
|
|
16482
|
+
var EXTRA_PARAMS_NO_FIELD_REMEDY = {
|
|
16483
|
+
messages: "AKM builds the request messages internally — remove it from extraParams",
|
|
16484
|
+
responseformat: "AKM controls the response format internally — remove it from extraParams",
|
|
16485
|
+
stream: "AKM controls streaming internally — remove it from extraParams",
|
|
16486
|
+
streamoptions: "AKM controls streaming internally — remove it from extraParams",
|
|
16487
|
+
chattemplatekwargs: "set engines.<name>.enableThinking instead of chat_template_kwargs.enable_thinking"
|
|
16488
|
+
};
|
|
16489
|
+
function protectedKeyRemedy(normalized) {
|
|
16490
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
16491
|
+
if (field) {
|
|
16492
|
+
const note = normalized === "reasoningeffort" ? " (moved to a first-class field in 0.9.2)" : "";
|
|
16493
|
+
return `set engines.<name>.${field} instead${note}`;
|
|
16494
|
+
}
|
|
16495
|
+
return EXTRA_PARAMS_NO_FIELD_REMEDY[normalized];
|
|
16496
|
+
}
|
|
16474
16497
|
function normalizeExtraParamKey(key) {
|
|
16475
16498
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
16476
16499
|
}
|
|
@@ -16498,7 +16521,11 @@ function validateExtraParams(value) {
|
|
|
16498
16521
|
for (const [key, child] of Object.entries(entry)) {
|
|
16499
16522
|
const normalized = normalizeExtraParamKey(key);
|
|
16500
16523
|
if (path9.length === 0 && PROTECTED_TOP_LEVEL_KEYS.has(normalized)) {
|
|
16501
|
-
|
|
16524
|
+
const remedy = protectedKeyRemedy(normalized);
|
|
16525
|
+
issues.push({
|
|
16526
|
+
path: [key],
|
|
16527
|
+
message: remedy ? `${key} is protected by AKM — ${remedy}.` : `${key} is protected by AKM`
|
|
16528
|
+
});
|
|
16502
16529
|
}
|
|
16503
16530
|
if (CREDENTIAL_KEYS.has(normalized)) {
|
|
16504
16531
|
issues.push({ path: [...path9, key], message: `${key} cannot carry credentials` });
|
|
@@ -16513,6 +16540,64 @@ function formatExtraParamsIssue(label, issue) {
|
|
|
16513
16540
|
const suffix = issue.path.map((part) => typeof part === "number" ? `[${part}]` : `.${part}`).join("");
|
|
16514
16541
|
return `${label}${suffix} ${issue.message}`;
|
|
16515
16542
|
}
|
|
16543
|
+
function isPlainObject2(value) {
|
|
16544
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16545
|
+
}
|
|
16546
|
+
function liftLegacyEngineExtraParams(raw) {
|
|
16547
|
+
const lifted = [];
|
|
16548
|
+
const conflicts = [];
|
|
16549
|
+
const rawEngines = raw.engines;
|
|
16550
|
+
if (!isPlainObject2(rawEngines)) {
|
|
16551
|
+
return { config: raw, lifted, conflicts };
|
|
16552
|
+
}
|
|
16553
|
+
const engines = {};
|
|
16554
|
+
let anyEngineChanged = false;
|
|
16555
|
+
for (const [name, engineValue] of Object.entries(rawEngines)) {
|
|
16556
|
+
if (!isPlainObject2(engineValue) || !isPlainObject2(engineValue.extraParams)) {
|
|
16557
|
+
engines[name] = engineValue;
|
|
16558
|
+
continue;
|
|
16559
|
+
}
|
|
16560
|
+
const engine = { ...engineValue };
|
|
16561
|
+
const extraParams = { ...engineValue.extraParams };
|
|
16562
|
+
let engineChanged = false;
|
|
16563
|
+
for (const [rawKey, value] of Object.entries(engineValue.extraParams)) {
|
|
16564
|
+
const normalized = normalizeExtraParamKey(rawKey);
|
|
16565
|
+
if (!LIFTABLE_EXTRA_PARAMS_KEYS.has(normalized))
|
|
16566
|
+
continue;
|
|
16567
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
16568
|
+
if (!field)
|
|
16569
|
+
continue;
|
|
16570
|
+
const existing = engine[field];
|
|
16571
|
+
if (existing !== undefined && existing !== value) {
|
|
16572
|
+
conflicts.push({ engine: name, key: rawKey, field, extraParamsValue: value, fieldValue: existing });
|
|
16573
|
+
continue;
|
|
16574
|
+
}
|
|
16575
|
+
delete extraParams[rawKey];
|
|
16576
|
+
engineChanged = true;
|
|
16577
|
+
if (existing === value) {
|
|
16578
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} is redundant — engines.${name}.${field} is already set to the same value; dropped the extraParams entry`);
|
|
16579
|
+
continue;
|
|
16580
|
+
}
|
|
16581
|
+
engine[field] = value;
|
|
16582
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} -> engines.${name}.${field}`);
|
|
16583
|
+
}
|
|
16584
|
+
if (!engineChanged) {
|
|
16585
|
+
engines[name] = engineValue;
|
|
16586
|
+
continue;
|
|
16587
|
+
}
|
|
16588
|
+
anyEngineChanged = true;
|
|
16589
|
+
if (Object.keys(extraParams).length > 0) {
|
|
16590
|
+
engine.extraParams = extraParams;
|
|
16591
|
+
} else {
|
|
16592
|
+
delete engine.extraParams;
|
|
16593
|
+
}
|
|
16594
|
+
engines[name] = engine;
|
|
16595
|
+
}
|
|
16596
|
+
if (!anyEngineChanged) {
|
|
16597
|
+
return { config: raw, lifted, conflicts };
|
|
16598
|
+
}
|
|
16599
|
+
return { config: { ...raw, engines }, lifted, conflicts };
|
|
16600
|
+
}
|
|
16516
16601
|
|
|
16517
16602
|
// src/workflows/source-ir/compare.ts
|
|
16518
16603
|
function compareWorkflowSourceCodePoints(left, right) {
|
|
@@ -21777,11 +21862,11 @@ var CONSUMED_FRONTMATTER_KEYS = [
|
|
|
21777
21862
|
"stale_after",
|
|
21778
21863
|
"okf_version"
|
|
21779
21864
|
];
|
|
21780
|
-
function
|
|
21865
|
+
function isPlainObject3(value) {
|
|
21781
21866
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21782
21867
|
}
|
|
21783
21868
|
function parseActorMapping(value) {
|
|
21784
|
-
if (!
|
|
21869
|
+
if (!isPlainObject3(value))
|
|
21785
21870
|
return;
|
|
21786
21871
|
const by = nonEmptyString(value.by);
|
|
21787
21872
|
if (by === undefined)
|
|
@@ -21806,7 +21891,7 @@ function parseOkfSources(value) {
|
|
|
21806
21891
|
return;
|
|
21807
21892
|
const out = [];
|
|
21808
21893
|
for (const item of value) {
|
|
21809
|
-
if (!
|
|
21894
|
+
if (!isPlainObject3(item))
|
|
21810
21895
|
continue;
|
|
21811
21896
|
const resource = nonEmptyString(item.resource);
|
|
21812
21897
|
if (resource === undefined)
|
|
@@ -21912,7 +21997,7 @@ function recognize8(c, file) {
|
|
|
21912
21997
|
const description = nonEmptyString(data.description);
|
|
21913
21998
|
const tags = readTags(data.tags);
|
|
21914
21999
|
const links = resolveOkfLinks(body, file.relPath);
|
|
21915
|
-
const generatedMapping =
|
|
22000
|
+
const generatedMapping = isPlainObject3(data.generated) ? data.generated : undefined;
|
|
21916
22001
|
const generatedAt = generatedMapping ? nonEmptyString(generatedMapping.at) : undefined;
|
|
21917
22002
|
const generatedBy = generatedMapping ? nonEmptyString(generatedMapping.by) : undefined;
|
|
21918
22003
|
const legacyTimestamp = nonEmptyString(data.timestamp);
|
|
@@ -27542,7 +27627,7 @@ var AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
|
|
|
27542
27627
|
|
|
27543
27628
|
// src/core/config/deep-merge.ts
|
|
27544
27629
|
var UNSAFE_KEYS3 = new Set(["__proto__", "constructor", "prototype"]);
|
|
27545
|
-
function
|
|
27630
|
+
function isPlainObject4(value) {
|
|
27546
27631
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27547
27632
|
return false;
|
|
27548
27633
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -27551,7 +27636,7 @@ function isPlainObject3(value) {
|
|
|
27551
27636
|
function copyConfigValue(value) {
|
|
27552
27637
|
if (Array.isArray(value))
|
|
27553
27638
|
return value.map(copyConfigValue);
|
|
27554
|
-
if (!
|
|
27639
|
+
if (!isPlainObject4(value))
|
|
27555
27640
|
return value;
|
|
27556
27641
|
const copy = {};
|
|
27557
27642
|
for (const key of Object.keys(value)) {
|
|
@@ -27570,7 +27655,7 @@ function deepMergeConfig(base, override) {
|
|
|
27570
27655
|
if (next === undefined)
|
|
27571
27656
|
continue;
|
|
27572
27657
|
const current = result[key];
|
|
27573
|
-
result[key] =
|
|
27658
|
+
result[key] = isPlainObject4(current) && isPlainObject4(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
|
|
27574
27659
|
}
|
|
27575
27660
|
return result;
|
|
27576
27661
|
}
|
|
@@ -27630,15 +27715,29 @@ function loadUserConfig() {
|
|
|
27630
27715
|
return finalConfig;
|
|
27631
27716
|
}
|
|
27632
27717
|
function parseAndValidateConfigText(text, sourcePath) {
|
|
27633
|
-
const
|
|
27634
|
-
if (
|
|
27718
|
+
const parsedRaw = parseConfigText(text, sourcePath);
|
|
27719
|
+
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
27635
27720
|
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
27636
27721
|
}
|
|
27722
|
+
const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
27723
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
27724
|
+
if (conflicts.length > 0) {
|
|
27725
|
+
const lines = conflicts.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`).join(`
|
|
27726
|
+
`);
|
|
27727
|
+
throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:
|
|
27728
|
+
${lines}
|
|
27729
|
+
|
|
27730
|
+
Each 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");
|
|
27731
|
+
}
|
|
27732
|
+
if (lifted.length > 0) {
|
|
27733
|
+
warn(`Config${where} uses deprecated extraParams keys with first-class equivalents; treating them as the first-class fields for this run (not written back to the file):
|
|
27734
|
+
- ${lifted.join(`
|
|
27735
|
+
- `)}`);
|
|
27736
|
+
}
|
|
27637
27737
|
const parsed = AkmConfigSchema.safeParse(raw);
|
|
27638
27738
|
if (!parsed.success) {
|
|
27639
27739
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join(`
|
|
27640
27740
|
`);
|
|
27641
|
-
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
27642
27741
|
throw new ConfigError(`Invalid config${where}:
|
|
27643
27742
|
${lines}`, "INVALID_CONFIG_FILE");
|
|
27644
27743
|
}
|
|
@@ -16393,6 +16393,29 @@ var EXTRA_PARAMS_CREDENTIAL_KEYS = [
|
|
|
16393
16393
|
];
|
|
16394
16394
|
var PROTECTED_TOP_LEVEL_KEYS = new Set(EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS);
|
|
16395
16395
|
var CREDENTIAL_KEYS = new Set(EXTRA_PARAMS_CREDENTIAL_KEYS);
|
|
16396
|
+
var LEGACY_EXTRA_PARAMS_FIELD = {
|
|
16397
|
+
model: "model",
|
|
16398
|
+
temperature: "temperature",
|
|
16399
|
+
maxtokens: "maxTokens",
|
|
16400
|
+
enablethinking: "enableThinking",
|
|
16401
|
+
reasoningeffort: "reasoningEffort"
|
|
16402
|
+
};
|
|
16403
|
+
var LIFTABLE_EXTRA_PARAMS_KEYS = new Set(["temperature", "maxtokens", "enablethinking", "reasoningeffort"]);
|
|
16404
|
+
var EXTRA_PARAMS_NO_FIELD_REMEDY = {
|
|
16405
|
+
messages: "AKM builds the request messages internally \u2014 remove it from extraParams",
|
|
16406
|
+
responseformat: "AKM controls the response format internally \u2014 remove it from extraParams",
|
|
16407
|
+
stream: "AKM controls streaming internally \u2014 remove it from extraParams",
|
|
16408
|
+
streamoptions: "AKM controls streaming internally \u2014 remove it from extraParams",
|
|
16409
|
+
chattemplatekwargs: "set engines.<name>.enableThinking instead of chat_template_kwargs.enable_thinking"
|
|
16410
|
+
};
|
|
16411
|
+
function protectedKeyRemedy(normalized) {
|
|
16412
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
16413
|
+
if (field) {
|
|
16414
|
+
const note = normalized === "reasoningeffort" ? " (moved to a first-class field in 0.9.2)" : "";
|
|
16415
|
+
return `set engines.<name>.${field} instead${note}`;
|
|
16416
|
+
}
|
|
16417
|
+
return EXTRA_PARAMS_NO_FIELD_REMEDY[normalized];
|
|
16418
|
+
}
|
|
16396
16419
|
function normalizeExtraParamKey(key) {
|
|
16397
16420
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
16398
16421
|
}
|
|
@@ -16420,7 +16443,11 @@ function validateExtraParams(value) {
|
|
|
16420
16443
|
for (const [key, child] of Object.entries(entry)) {
|
|
16421
16444
|
const normalized = normalizeExtraParamKey(key);
|
|
16422
16445
|
if (path9.length === 0 && PROTECTED_TOP_LEVEL_KEYS.has(normalized)) {
|
|
16423
|
-
|
|
16446
|
+
const remedy = protectedKeyRemedy(normalized);
|
|
16447
|
+
issues.push({
|
|
16448
|
+
path: [key],
|
|
16449
|
+
message: remedy ? `${key} is protected by AKM \u2014 ${remedy}.` : `${key} is protected by AKM`
|
|
16450
|
+
});
|
|
16424
16451
|
}
|
|
16425
16452
|
if (CREDENTIAL_KEYS.has(normalized)) {
|
|
16426
16453
|
issues.push({ path: [...path9, key], message: `${key} cannot carry credentials` });
|
|
@@ -16435,6 +16462,64 @@ function formatExtraParamsIssue(label, issue) {
|
|
|
16435
16462
|
const suffix = issue.path.map((part) => typeof part === "number" ? `[${part}]` : `.${part}`).join("");
|
|
16436
16463
|
return `${label}${suffix} ${issue.message}`;
|
|
16437
16464
|
}
|
|
16465
|
+
function isPlainObject2(value) {
|
|
16466
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16467
|
+
}
|
|
16468
|
+
function liftLegacyEngineExtraParams(raw) {
|
|
16469
|
+
const lifted = [];
|
|
16470
|
+
const conflicts = [];
|
|
16471
|
+
const rawEngines = raw.engines;
|
|
16472
|
+
if (!isPlainObject2(rawEngines)) {
|
|
16473
|
+
return { config: raw, lifted, conflicts };
|
|
16474
|
+
}
|
|
16475
|
+
const engines = {};
|
|
16476
|
+
let anyEngineChanged = false;
|
|
16477
|
+
for (const [name, engineValue] of Object.entries(rawEngines)) {
|
|
16478
|
+
if (!isPlainObject2(engineValue) || !isPlainObject2(engineValue.extraParams)) {
|
|
16479
|
+
engines[name] = engineValue;
|
|
16480
|
+
continue;
|
|
16481
|
+
}
|
|
16482
|
+
const engine = { ...engineValue };
|
|
16483
|
+
const extraParams = { ...engineValue.extraParams };
|
|
16484
|
+
let engineChanged = false;
|
|
16485
|
+
for (const [rawKey, value] of Object.entries(engineValue.extraParams)) {
|
|
16486
|
+
const normalized = normalizeExtraParamKey(rawKey);
|
|
16487
|
+
if (!LIFTABLE_EXTRA_PARAMS_KEYS.has(normalized))
|
|
16488
|
+
continue;
|
|
16489
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
16490
|
+
if (!field)
|
|
16491
|
+
continue;
|
|
16492
|
+
const existing = engine[field];
|
|
16493
|
+
if (existing !== undefined && existing !== value) {
|
|
16494
|
+
conflicts.push({ engine: name, key: rawKey, field, extraParamsValue: value, fieldValue: existing });
|
|
16495
|
+
continue;
|
|
16496
|
+
}
|
|
16497
|
+
delete extraParams[rawKey];
|
|
16498
|
+
engineChanged = true;
|
|
16499
|
+
if (existing === value) {
|
|
16500
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} is redundant \u2014 engines.${name}.${field} is already set to the same value; dropped the extraParams entry`);
|
|
16501
|
+
continue;
|
|
16502
|
+
}
|
|
16503
|
+
engine[field] = value;
|
|
16504
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} -> engines.${name}.${field}`);
|
|
16505
|
+
}
|
|
16506
|
+
if (!engineChanged) {
|
|
16507
|
+
engines[name] = engineValue;
|
|
16508
|
+
continue;
|
|
16509
|
+
}
|
|
16510
|
+
anyEngineChanged = true;
|
|
16511
|
+
if (Object.keys(extraParams).length > 0) {
|
|
16512
|
+
engine.extraParams = extraParams;
|
|
16513
|
+
} else {
|
|
16514
|
+
delete engine.extraParams;
|
|
16515
|
+
}
|
|
16516
|
+
engines[name] = engine;
|
|
16517
|
+
}
|
|
16518
|
+
if (!anyEngineChanged) {
|
|
16519
|
+
return { config: raw, lifted, conflicts };
|
|
16520
|
+
}
|
|
16521
|
+
return { config: { ...raw, engines }, lifted, conflicts };
|
|
16522
|
+
}
|
|
16438
16523
|
|
|
16439
16524
|
// src/workflows/source-ir/compare.ts
|
|
16440
16525
|
function compareWorkflowSourceCodePoints(left, right) {
|
|
@@ -21699,11 +21784,11 @@ var CONSUMED_FRONTMATTER_KEYS = [
|
|
|
21699
21784
|
"stale_after",
|
|
21700
21785
|
"okf_version"
|
|
21701
21786
|
];
|
|
21702
|
-
function
|
|
21787
|
+
function isPlainObject3(value) {
|
|
21703
21788
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21704
21789
|
}
|
|
21705
21790
|
function parseActorMapping(value) {
|
|
21706
|
-
if (!
|
|
21791
|
+
if (!isPlainObject3(value))
|
|
21707
21792
|
return;
|
|
21708
21793
|
const by = nonEmptyString(value.by);
|
|
21709
21794
|
if (by === undefined)
|
|
@@ -21728,7 +21813,7 @@ function parseOkfSources(value) {
|
|
|
21728
21813
|
return;
|
|
21729
21814
|
const out = [];
|
|
21730
21815
|
for (const item of value) {
|
|
21731
|
-
if (!
|
|
21816
|
+
if (!isPlainObject3(item))
|
|
21732
21817
|
continue;
|
|
21733
21818
|
const resource = nonEmptyString(item.resource);
|
|
21734
21819
|
if (resource === undefined)
|
|
@@ -21834,7 +21919,7 @@ function recognize8(c, file) {
|
|
|
21834
21919
|
const description = nonEmptyString(data.description);
|
|
21835
21920
|
const tags = readTags(data.tags);
|
|
21836
21921
|
const links = resolveOkfLinks(body, file.relPath);
|
|
21837
|
-
const generatedMapping =
|
|
21922
|
+
const generatedMapping = isPlainObject3(data.generated) ? data.generated : undefined;
|
|
21838
21923
|
const generatedAt = generatedMapping ? nonEmptyString(generatedMapping.at) : undefined;
|
|
21839
21924
|
const generatedBy = generatedMapping ? nonEmptyString(generatedMapping.by) : undefined;
|
|
21840
21925
|
const legacyTimestamp = nonEmptyString(data.timestamp);
|
|
@@ -27464,7 +27549,7 @@ var AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
|
|
|
27464
27549
|
|
|
27465
27550
|
// src/core/config/deep-merge.ts
|
|
27466
27551
|
var UNSAFE_KEYS3 = new Set(["__proto__", "constructor", "prototype"]);
|
|
27467
|
-
function
|
|
27552
|
+
function isPlainObject4(value) {
|
|
27468
27553
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
27469
27554
|
return false;
|
|
27470
27555
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -27473,7 +27558,7 @@ function isPlainObject3(value) {
|
|
|
27473
27558
|
function copyConfigValue(value) {
|
|
27474
27559
|
if (Array.isArray(value))
|
|
27475
27560
|
return value.map(copyConfigValue);
|
|
27476
|
-
if (!
|
|
27561
|
+
if (!isPlainObject4(value))
|
|
27477
27562
|
return value;
|
|
27478
27563
|
const copy = {};
|
|
27479
27564
|
for (const key of Object.keys(value)) {
|
|
@@ -27492,7 +27577,7 @@ function deepMergeConfig(base, override) {
|
|
|
27492
27577
|
if (next === undefined)
|
|
27493
27578
|
continue;
|
|
27494
27579
|
const current = result[key];
|
|
27495
|
-
result[key] =
|
|
27580
|
+
result[key] = isPlainObject4(current) && isPlainObject4(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
|
|
27496
27581
|
}
|
|
27497
27582
|
return result;
|
|
27498
27583
|
}
|
|
@@ -27552,15 +27637,29 @@ function loadUserConfig() {
|
|
|
27552
27637
|
return finalConfig;
|
|
27553
27638
|
}
|
|
27554
27639
|
function parseAndValidateConfigText(text, sourcePath) {
|
|
27555
|
-
const
|
|
27556
|
-
if (
|
|
27640
|
+
const parsedRaw = parseConfigText(text, sourcePath);
|
|
27641
|
+
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
27557
27642
|
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
27558
27643
|
}
|
|
27644
|
+
const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
27645
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
27646
|
+
if (conflicts.length > 0) {
|
|
27647
|
+
const lines = conflicts.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`).join(`
|
|
27648
|
+
`);
|
|
27649
|
+
throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:
|
|
27650
|
+
${lines}
|
|
27651
|
+
|
|
27652
|
+
Each extraParams key above has a first-class equivalent and akm will not guess which value you meant \u2014 remove the extraParams entry once the field carries the value you want.`, "INVALID_CONFIG_FILE");
|
|
27653
|
+
}
|
|
27654
|
+
if (lifted.length > 0) {
|
|
27655
|
+
warn(`Config${where} uses deprecated extraParams keys with first-class equivalents; treating them as the first-class fields for this run (not written back to the file):
|
|
27656
|
+
- ${lifted.join(`
|
|
27657
|
+
- `)}`);
|
|
27658
|
+
}
|
|
27559
27659
|
const parsed = AkmConfigSchema.safeParse(raw);
|
|
27560
27660
|
if (!parsed.success) {
|
|
27561
27661
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join(`
|
|
27562
27662
|
`);
|
|
27563
|
-
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
27564
27663
|
throw new ConfigError(`Invalid config${where}:
|
|
27565
27664
|
${lines}`, "INVALID_CONFIG_FILE");
|
|
27566
27665
|
}
|
|
@@ -64,13 +64,40 @@ export function shellCommand(task, platform = process.platform, env = process.en
|
|
|
64
64
|
return [task.shell, "-c", command];
|
|
65
65
|
case "pwsh":
|
|
66
66
|
case "powershell":
|
|
67
|
-
return [
|
|
67
|
+
return [
|
|
68
|
+
shellExecutable(task.shell, platform, env),
|
|
69
|
+
"-NoProfile",
|
|
70
|
+
"-NonInteractive",
|
|
71
|
+
"-Command",
|
|
72
|
+
withExitCodePropagation(command),
|
|
73
|
+
];
|
|
68
74
|
case "cmd":
|
|
69
75
|
return [shellExecutable("cmd", platform, env), "/d", "/s", "/c", command];
|
|
70
76
|
default:
|
|
71
77
|
return assertNever(task.shell, "shellCommand");
|
|
72
78
|
}
|
|
73
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* `-Command` (documented identically for powershell.exe 5.1 and pwsh 7+, in
|
|
82
|
+
* about_PowerShell_exe / about_Pwsh) already derives its own process exit
|
|
83
|
+
* code from `$?`, so a genuinely failing last statement already yields a
|
|
84
|
+
* nonzero exit and status "failed" — but any native exit code outside {0, 1}
|
|
85
|
+
* is collapsed to 1, discarding the real value that task history reports.
|
|
86
|
+
*
|
|
87
|
+
* Appending a bare `exit $LASTEXITCODE` to recover it is unsafe on its own:
|
|
88
|
+
* `$LASTEXITCODE` stays `$null` for a command that never runs a native
|
|
89
|
+
* executable (a pure PowerShell/cmdlet command), and `exit $null` resolves
|
|
90
|
+
* to exit code 0 — turning a failed cmdlet into a false "completed".
|
|
91
|
+
*
|
|
92
|
+
* Reading `$?` first, before anything else can run, reproduces -Command's
|
|
93
|
+
* own completed/failed determination exactly (immune to that regression and
|
|
94
|
+
* to a `$LASTEXITCODE` left stale by an earlier native call in the same
|
|
95
|
+
* command), then upgrades to the precise native exit code only when the
|
|
96
|
+
* failing last statement actually set one.
|
|
97
|
+
*/
|
|
98
|
+
function withExitCodePropagation(command) {
|
|
99
|
+
return `${command}; if ($?) { exit 0 } elseif ($LASTEXITCODE -ne $null) { exit $LASTEXITCODE } else { exit 1 }`;
|
|
100
|
+
}
|
|
74
101
|
/**
|
|
75
102
|
* Bind an unambiguous leading bare `akm` (including the task-v2 migrator's
|
|
76
103
|
* quoted form) to this installation. Explicit paths and arbitrary shell
|
|
@@ -478,6 +478,21 @@ function installOptionsFor(input, current) {
|
|
|
478
478
|
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
479
479
|
}
|
|
480
480
|
function belongsToBundle(entry, input) {
|
|
481
|
+
if (input.bundlePath !== undefined && entry.target === input.bundleName) {
|
|
482
|
+
// Path-scoped (#846), primary/unconfigured-bundle sync only: the name
|
|
483
|
+
// already matches, but a display name derived from a directory
|
|
484
|
+
// basename is not an identity — two unrelated bundles can legitimately
|
|
485
|
+
// share one. Require the entry's own scheduler-context descriptor to
|
|
486
|
+
// additionally confirm the resolved path. An entry whose owning path
|
|
487
|
+
// cannot be established is never assumed to be ours — that silent
|
|
488
|
+
// assumption is exactly what let an isolated/foreign bundle's sync
|
|
489
|
+
// reach for another bundle's real scheduler entries. (`bundlePath` is
|
|
490
|
+
// only set for a primary sync — a `--bundle <target>` entry's
|
|
491
|
+
// descriptor reflects the invoking process's OWN primary directory,
|
|
492
|
+
// not the targeted bundle's, so it is not a meaningful signal there;
|
|
493
|
+
// that case keeps relying on config-name uniqueness below.)
|
|
494
|
+
return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
|
|
495
|
+
}
|
|
481
496
|
if (entry.target === input.bundleName || entry.target === input.bundleTarget)
|
|
482
497
|
return true;
|
|
483
498
|
return false;
|
|
@@ -487,8 +502,13 @@ function assertNoForeignIds(desired, input) {
|
|
|
487
502
|
const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
|
|
488
503
|
if (!foreign)
|
|
489
504
|
return;
|
|
490
|
-
const where = foreign.
|
|
491
|
-
|
|
505
|
+
const where = foreign.ownerBundlePath
|
|
506
|
+
? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
|
|
507
|
+
: foreign.target
|
|
508
|
+
? `bundle ${JSON.stringify(foreign.target)}`
|
|
509
|
+
: "the default bundle";
|
|
510
|
+
const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
|
|
511
|
+
throw new UsageError(`Scheduler id ${JSON.stringify(foreign.id)} is already scheduled from ${where}${mine}; desired source ids must not collide across bundles.`, "RESOURCE_ALREADY_EXISTS");
|
|
492
512
|
}
|
|
493
513
|
function assertUniqueDesiredIds(desired) {
|
|
494
514
|
const seen = new Set();
|
|
@@ -122,8 +122,11 @@ export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
|
|
|
122
122
|
}
|
|
123
123
|
else if (run.status === "failed") {
|
|
124
124
|
// `workflow abandon` marks the run failed while intentionally leaving its
|
|
125
|
-
// current step
|
|
126
|
-
|
|
125
|
+
// current step unchanged so `resume` can reopen the same work. An active
|
|
126
|
+
// run leaves a pending step; a blocked run leaves a blocked step; and an
|
|
127
|
+
// execution failure already carries a failed step. All three are honest
|
|
128
|
+
// failed-run spines that `resumeWorkflowRun` normalizes back to pending.
|
|
129
|
+
if (!current || (current.status !== "failed" && current.status !== "pending" && current.status !== "blocked"))
|
|
127
130
|
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
128
131
|
}
|
|
129
132
|
else if (run.status === "completed") {
|
package/docs/migration/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Upgrade guides and per-release migration notes.
|
|
4
4
|
|
|
5
|
-
- [v0.9.1 -> v0.9.2 migration guide](v0.9.1-to-v0.9.2.md) -- Task-v2/task-v3 to task source v4 conversion, the
|
|
5
|
+
- [v0.9.1 -> v0.9.2 migration guide](v0.9.1-to-v0.9.2.md) -- Task-v2/task-v3 to task source v4 conversion, the durable-v4-family workflow boundary at executable `irVersion: 5`, and release behavior changes
|
|
6
6
|
- [v0.9.2 release note](release-notes/0.9.2.md) -- Self-contained terminal upgrade summary shipped for `akm help migrate 0.9.2`
|
|
7
7
|
- [v0.8 -> current v0.9 migration guide](v0.8-to-v0.9.md) -- Package upgrade with fresh current config/state and explicit task conversion
|
|
8
8
|
- [v0.7 -> v0.8 migration guide](v0.7-to-v0.8.md) -- Task schema and 0.8-era changes
|
|
@@ -8,7 +8,8 @@ live one level up in `docs/migration/`.
|
|
|
8
8
|
## Available notes
|
|
9
9
|
|
|
10
10
|
- [0.9.2](0.9.2.md) — task source v4 migration, workflow source IR v1 and
|
|
11
|
-
durable
|
|
11
|
+
durable-v4-family `irVersion: 5`, command diagnostics, and strategy judgment
|
|
12
|
+
migration
|
|
12
13
|
|
|
13
14
|
## Adding notes for a new release
|
|
14
15
|
|
|
@@ -17,7 +17,7 @@ in place is not.
|
|
|
17
17
|
- old `index.db`, `workflow.db`, task-history JSONL, or legacy lock/cache
|
|
18
18
|
layouts;
|
|
19
19
|
- old ref grammar or old workflow/task execution paths;
|
|
20
|
-
- in-flight
|
|
20
|
+
- in-flight workflow plans older than `irVersion: 5`.
|
|
21
21
|
|
|
22
22
|
Those formats are not compatibility inputs to the current runtime. Keep an
|
|
23
23
|
archive if you need historical inspection; do not place it in the live 0.9
|
|
@@ -108,9 +108,9 @@ existing scheduler entry.
|
|
|
108
108
|
## Workflow boundary
|
|
109
109
|
|
|
110
110
|
Current Markdown and GitHub-shaped YAML workflows compile to the same source
|
|
111
|
-
IR and freeze durable plan
|
|
112
|
-
plan. Do not copy an old workflow database
|
|
113
|
-
new runs from current authored sources.
|
|
111
|
+
IR and freeze the durable plan v4 family's executable `irVersion: 5` format.
|
|
112
|
+
That is the only executable stored plan. Do not copy an old workflow database
|
|
113
|
+
expecting old runs to resume; start new runs from current authored sources.
|
|
114
114
|
|
|
115
115
|
## Recovery
|
|
116
116
|
|
|
@@ -109,13 +109,13 @@ backup over a file while a task sync or scheduler process is running.
|
|
|
109
109
|
|
|
110
110
|
## A workflow will not resume
|
|
111
111
|
|
|
112
|
-
Only durable plan
|
|
113
|
-
decoded by a compatibility runtime.
|
|
114
|
-
or YAML workflow source.
|
|
112
|
+
Only the durable plan v4 family's `irVersion: 5` executes. Pre-`irVersion`-5
|
|
113
|
+
stored plans are rejected rather than decoded by a compatibility runtime.
|
|
114
|
+
Start a new run from the current Markdown or YAML workflow source.
|
|
115
115
|
|
|
116
|
-
For
|
|
117
|
-
run uses its frozen plan. A plan-hash or schema failure is
|
|
118
|
-
corruption and must fail closed.
|
|
116
|
+
For an `irVersion: 5` run, a missing or changed authored source is not a resume
|
|
117
|
+
blocker: the run uses its frozen plan. A plan-hash or schema failure is
|
|
118
|
+
durable-state corruption and must fail closed.
|
|
119
119
|
|
|
120
120
|
## A stale transaction journal is reported
|
|
121
121
|
|
|
@@ -147,8 +147,9 @@ defaults at the layer that selected it; explicit sibling fields and nearer
|
|
|
147
147
|
layers still win. The resulting request carries the exact model ID and merged
|
|
148
148
|
inference object. Engine lowerers consume that exact selection and never run
|
|
149
149
|
alias resolution again. New workflow starts persist the exact request and
|
|
150
|
-
symbolic runner selection in durable plan v4
|
|
151
|
-
material without resolving aliases
|
|
150
|
+
symbolic runner selection in the durable plan v4 family's executable
|
|
151
|
+
`irVersion: 5`; resume consumes that frozen material without resolving aliases
|
|
152
|
+
again.
|
|
152
153
|
|
|
153
154
|
Copy the complete installed starter into the user configuration directory when
|
|
154
155
|
you want to customize all fields:
|
|
@@ -1021,16 +1021,17 @@ a committed *value*, so it cannot carry "whatever this build agent's
|
|
|
1021
1021
|
Values passed through this way are **not** redacted from the command's output
|
|
1022
1022
|
the way `env:` binding values are, so never list a credential here.
|
|
1023
1023
|
|
|
1024
|
-
#### Durable v4 forbids `inherit_env`
|
|
1024
|
+
#### Durable v4 (`irVersion: 5`) forbids `inherit_env`
|
|
1025
1025
|
|
|
1026
|
-
Every new workflow start freezes
|
|
1027
|
-
`inherit_env: true` and any other
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
part of the unit's input hash.
|
|
1026
|
+
Every new workflow start freezes the durable plan v4 family's current
|
|
1027
|
+
executable format, `irVersion: 5`. It rejects `inherit_env: true` and any other
|
|
1028
|
+
request for whole-process inheritance; use exact named environment bindings
|
|
1029
|
+
and `pass_env:` instead. Both mechanisms are dispatch-significant, keep the
|
|
1030
|
+
visible environment surface bounded, and form part of the unit's input hash.
|
|
1031
1031
|
|
|
1032
|
-
The historical `inherit_env` spelling is unsupported. Pre
|
|
1033
|
-
rejected; they are never upgraded or replayed through a second
|
|
1032
|
+
The historical `inherit_env` spelling is unsupported. Pre-`irVersion`-5 stored
|
|
1033
|
+
plans are rejected; they are never upgraded or replayed through a second
|
|
1034
|
+
runtime.
|
|
1034
1035
|
|
|
1035
1036
|
### What `akm show` reports for an exec step
|
|
1036
1037
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|