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 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
- ...entry,
342
- ...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
343
- ...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
344
- binding: "binding" in entry ? [...entry.binding] : [],
345
- contextPath: "contextPath" in entry ? entry.contextPath : "",
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 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
@@ -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.target ? `bundle ${JSON.stringify(foreign.target)}` : "the default bundle";
491
- throw new UsageError(`Scheduler id ${JSON.stringify(foreign.id)} is already scheduled from ${where}; desired source ids must not collide across bundles.`, "RESOURCE_ALREADY_EXISTS");
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 pending so `resume` can reopen the same work.
126
- if (!current || (current.status !== "failed" && current.status !== "pending"))
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") {
@@ -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 single durable-v4 workflow boundary, and release behavior changes
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 v4, command diagnostics, and strategy judgment migration
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 pre-v4 workflow plans.
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 IR v4. Durable v4 is the only executable stored
112
- plan. Do not copy an old workflow database expecting old runs to resume; start
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 IR v4 executes. Pre-v4 stored plans are rejected rather than
113
- decoded by a compatibility runtime. Start a new run from the current Markdown
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 a v4 run, a missing or changed authored source is not a resume blocker: the
117
- run uses its frozen plan. A plan-hash or schema failure is durable-state
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; resume consumes that frozen
151
- material without resolving aliases again.
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 a durable v4 plan. V4 rejects
1027
- `inherit_env: true` and any other request for whole-process inheritance; use
1028
- exact named environment bindings and `pass_env:` instead. Both mechanisms are
1029
- dispatch-significant, keep the visible environment surface bounded, and form
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-v4 stored plans are
1033
- rejected; they are never upgraded or replayed through a second runtime.
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.2-alpha.5",
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": [