akm-cli 0.9.2 → 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 CHANGED
@@ -6,6 +6,43 @@ 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
+
11
+ ### Fixed
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
+
9
46
  ## [0.9.2] - 2026-08-29
10
47
 
11
48
  ### Fixed
@@ -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 raw = parseConfigText(text, sourcePath);
145
- if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
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
- issues.push({ path: [key], message: `${key} is protected by AKM` });
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
- issues.push({ path: [key], message: `${key} is protected by AKM` });
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 isPlainObject2(value) {
21865
+ function isPlainObject3(value) {
21781
21866
  return value !== null && typeof value === "object" && !Array.isArray(value);
21782
21867
  }
21783
21868
  function parseActorMapping(value) {
21784
- if (!isPlainObject2(value))
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 (!isPlainObject2(item))
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 = isPlainObject2(data.generated) ? data.generated : undefined;
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 isPlainObject3(value) {
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 (!isPlainObject3(value))
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] = isPlainObject3(current) && isPlainObject3(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
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 raw = parseConfigText(text, sourcePath);
27634
- if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
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
- issues.push({ path: [key], message: `${key} is protected by AKM` });
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 isPlainObject2(value) {
21787
+ function isPlainObject3(value) {
21703
21788
  return value !== null && typeof value === "object" && !Array.isArray(value);
21704
21789
  }
21705
21790
  function parseActorMapping(value) {
21706
- if (!isPlainObject2(value))
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 (!isPlainObject2(item))
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 = isPlainObject2(data.generated) ? data.generated : undefined;
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 isPlainObject3(value) {
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 (!isPlainObject3(value))
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] = isPlainObject3(current) && isPlainObject3(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
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 raw = parseConfigText(text, sourcePath);
27556
- if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
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 [shellExecutable(task.shell, platform, env), "-NoProfile", "-NonInteractive", "-Command", command];
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.2",
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": [