akm-cli 0.9.2-alpha.5 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,8 +6,38 @@ 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.2] - 2026-08-29
10
+
9
11
  ### Fixed
10
12
 
13
+ - **A blocked workflow run became permanently unresumable after `akm
14
+ workflow abandon` (#847).** Abandon correctly moved the run to `failed`
15
+ but left its current step `blocked`; the durable-spine validator then
16
+ rejected that honest abandoned shape as corruption before `resume` could
17
+ reopen the step. Failed-run validation now accepts the three legitimate
18
+ current-step states — `pending` for an abandoned active run, `blocked` for
19
+ an abandoned blocked run, and `failed` for an execution failure — and
20
+ `resume` normalizes each back to `pending` as promised by the CLI help.
21
+
22
+ - **`akm task sync` could compute another bundle's real scheduler entries as
23
+ drift and try to remove them (#846).** Removal was scoped by a bundle
24
+ *display name* (`bundleName`/`--bundle` target) — a value derived from a
25
+ directory basename that two unrelated bundles can legitimately share (an
26
+ unconfigured bundle's name is deduped only against its own config's
27
+ bundles, never against other bundles actually installed on the machine).
28
+ A primary/unconfigured-bundle sync now additionally confirms a
29
+ name-matching installed entry's *resolved bundle path*, recovered from
30
+ that entry's own scheduler-context descriptor, before treating it as
31
+ eligible for reconcile — and refuses (rather than assumes) when that path
32
+ can't be established. **Backward compatibility:** every entry installed by
33
+ this codebase already carries a scheduler-context descriptor (the
34
+ `--scheduler-context` file used to restore the scheduled process's
35
+ environment), so existing installations resolve correctly with no user
36
+ action required. An entry whose descriptor is missing, unreadable, or
37
+ owned by a different OS user is never assumed to belong to the invoking
38
+ bundle; such an entry is simply left untouched by sync (it will not be
39
+ auto-repaired or removed) until it is reinstalled or removed by hand.
40
+
11
41
  - **Scheduled tasks on Windows ran but recorded no output.** A task fired by
12
42
  Task Scheduler logged `exit_code=0` with an empty log: the command really
13
43
  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;
@@ -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.2",
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": [