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 +30 -0
- package/dist/commands/tasks/tasks.js +32 -7
- package/dist/tasks/scheduler-sync.js +22 -2
- package/dist/workflows/runtime/plan-classifier.js +5 -2
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/README.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +4 -4
- package/docs/migration/v0.9.0-troubleshooting.md +6 -6
- package/docs/reference/configuration.md +3 -2
- package/docs/reference/workflow-schema.md +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
340
|
+
const allEntries = rawEntries.map((entry) => {
|
|
341
|
+
const contextPath = "contextPath" in entry ? entry.contextPath : "";
|
|
342
|
+
// #846: recover the resolved bundle path this entry was installed
|
|
343
|
+
// under from its own scheduler-context descriptor. Any failure (no
|
|
344
|
+
// descriptor, unreadable, corrupt, owned by another user) leaves
|
|
345
|
+
// ownerBundlePath unset — belongsToBundle must never treat that as
|
|
346
|
+
// "mine".
|
|
347
|
+
const ownerBundlePath = contextPath ? resolveInstalledOwnerPath(contextPath) : undefined;
|
|
348
|
+
return {
|
|
349
|
+
...entry,
|
|
350
|
+
...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
|
|
351
|
+
...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
|
|
352
|
+
binding: "binding" in entry ? [...entry.binding] : [],
|
|
353
|
+
contextPath,
|
|
354
|
+
...(ownerBundlePath !== undefined ? { ownerBundlePath } : {}),
|
|
355
|
+
};
|
|
356
|
+
});
|
|
347
357
|
const nativeArtifacts = inspection.artifacts;
|
|
348
358
|
const common = {
|
|
349
359
|
sourceRoot: stashDir,
|
|
350
360
|
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
351
361
|
bundleName: resolved.source.name,
|
|
362
|
+
// #846: only meaningful for a primary/unconfigured-bundle sync. A
|
|
363
|
+
// `--bundle <target>` entry's scheduler-context descriptor records the
|
|
364
|
+
// invoking process's OWN primary AKM_BUNDLE_DIR, not the targeted
|
|
365
|
+
// bundle's directory, so path-scoping stays gated on the case it's
|
|
366
|
+
// actually valid for (see belongsToBundle).
|
|
367
|
+
...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
|
|
352
368
|
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
353
369
|
backend: sched.name,
|
|
354
370
|
installed: allEntries,
|
|
@@ -773,6 +789,15 @@ function groupInstalledBindings(entries, invocation) {
|
|
|
773
789
|
}
|
|
774
790
|
return [...groups.values()].map((group) => ({ ...group, taskIds: group.taskIds.sort() }));
|
|
775
791
|
}
|
|
792
|
+
/** Best-effort recovery of an installed binding's owning bundle path (#846). */
|
|
793
|
+
function resolveInstalledOwnerPath(contextPath) {
|
|
794
|
+
try {
|
|
795
|
+
return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
776
801
|
function inspectInstalledBinding(entry, invocation) {
|
|
777
802
|
const status = [];
|
|
778
803
|
const binding = entry.binding;
|
|
@@ -478,6 +478,21 @@ function installOptionsFor(input, current) {
|
|
|
478
478
|
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
479
479
|
}
|
|
480
480
|
function belongsToBundle(entry, input) {
|
|
481
|
+
if (input.bundlePath !== undefined && entry.target === input.bundleName) {
|
|
482
|
+
// Path-scoped (#846), primary/unconfigured-bundle sync only: the name
|
|
483
|
+
// already matches, but a display name derived from a directory
|
|
484
|
+
// basename is not an identity — two unrelated bundles can legitimately
|
|
485
|
+
// share one. Require the entry's own scheduler-context descriptor to
|
|
486
|
+
// additionally confirm the resolved path. An entry whose owning path
|
|
487
|
+
// cannot be established is never assumed to be ours — that silent
|
|
488
|
+
// assumption is exactly what let an isolated/foreign bundle's sync
|
|
489
|
+
// reach for another bundle's real scheduler entries. (`bundlePath` is
|
|
490
|
+
// only set for a primary sync — a `--bundle <target>` entry's
|
|
491
|
+
// descriptor reflects the invoking process's OWN primary directory,
|
|
492
|
+
// not the targeted bundle's, so it is not a meaningful signal there;
|
|
493
|
+
// that case keeps relying on config-name uniqueness below.)
|
|
494
|
+
return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
|
|
495
|
+
}
|
|
481
496
|
if (entry.target === input.bundleName || entry.target === input.bundleTarget)
|
|
482
497
|
return true;
|
|
483
498
|
return false;
|
|
@@ -487,8 +502,13 @@ function assertNoForeignIds(desired, input) {
|
|
|
487
502
|
const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
|
|
488
503
|
if (!foreign)
|
|
489
504
|
return;
|
|
490
|
-
const where = foreign.
|
|
491
|
-
|
|
505
|
+
const where = foreign.ownerBundlePath
|
|
506
|
+
? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
|
|
507
|
+
: foreign.target
|
|
508
|
+
? `bundle ${JSON.stringify(foreign.target)}`
|
|
509
|
+
: "the default bundle";
|
|
510
|
+
const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
|
|
511
|
+
throw new UsageError(`Scheduler id ${JSON.stringify(foreign.id)} is already scheduled from ${where}${mine}; desired source ids must not collide across bundles.`, "RESOURCE_ALREADY_EXISTS");
|
|
492
512
|
}
|
|
493
513
|
function assertUniqueDesiredIds(desired) {
|
|
494
514
|
const seen = new Set();
|
|
@@ -122,8 +122,11 @@ export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
|
|
|
122
122
|
}
|
|
123
123
|
else if (run.status === "failed") {
|
|
124
124
|
// `workflow abandon` marks the run failed while intentionally leaving its
|
|
125
|
-
// current step
|
|
126
|
-
|
|
125
|
+
// current step unchanged so `resume` can reopen the same work. An active
|
|
126
|
+
// run leaves a pending step; a blocked run leaves a blocked step; and an
|
|
127
|
+
// execution failure already carries a failed step. All three are honest
|
|
128
|
+
// failed-run spines that `resumeWorkflowRun` normalizes back to pending.
|
|
129
|
+
if (!current || (current.status !== "failed" && current.status !== "pending" && current.status !== "blocked"))
|
|
127
130
|
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
128
131
|
}
|
|
129
132
|
else if (run.status === "completed") {
|
package/docs/migration/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Upgrade guides and per-release migration notes.
|
|
4
4
|
|
|
5
|
-
- [v0.9.1 -> v0.9.2 migration guide](v0.9.1-to-v0.9.2.md) -- Task-v2/task-v3 to task source v4 conversion, the
|
|
5
|
+
- [v0.9.1 -> v0.9.2 migration guide](v0.9.1-to-v0.9.2.md) -- Task-v2/task-v3 to task source v4 conversion, the durable-v4-family workflow boundary at executable `irVersion: 5`, and release behavior changes
|
|
6
6
|
- [v0.9.2 release note](release-notes/0.9.2.md) -- Self-contained terminal upgrade summary shipped for `akm help migrate 0.9.2`
|
|
7
7
|
- [v0.8 -> current v0.9 migration guide](v0.8-to-v0.9.md) -- Package upgrade with fresh current config/state and explicit task conversion
|
|
8
8
|
- [v0.7 -> v0.8 migration guide](v0.7-to-v0.8.md) -- Task schema and 0.8-era changes
|
|
@@ -8,7 +8,8 @@ live one level up in `docs/migration/`.
|
|
|
8
8
|
## Available notes
|
|
9
9
|
|
|
10
10
|
- [0.9.2](0.9.2.md) — task source v4 migration, workflow source IR v1 and
|
|
11
|
-
durable
|
|
11
|
+
durable-v4-family `irVersion: 5`, command diagnostics, and strategy judgment
|
|
12
|
+
migration
|
|
12
13
|
|
|
13
14
|
## Adding notes for a new release
|
|
14
15
|
|
|
@@ -17,7 +17,7 @@ in place is not.
|
|
|
17
17
|
- old `index.db`, `workflow.db`, task-history JSONL, or legacy lock/cache
|
|
18
18
|
layouts;
|
|
19
19
|
- old ref grammar or old workflow/task execution paths;
|
|
20
|
-
- in-flight
|
|
20
|
+
- in-flight workflow plans older than `irVersion: 5`.
|
|
21
21
|
|
|
22
22
|
Those formats are not compatibility inputs to the current runtime. Keep an
|
|
23
23
|
archive if you need historical inspection; do not place it in the live 0.9
|
|
@@ -108,9 +108,9 @@ existing scheduler entry.
|
|
|
108
108
|
## Workflow boundary
|
|
109
109
|
|
|
110
110
|
Current Markdown and GitHub-shaped YAML workflows compile to the same source
|
|
111
|
-
IR and freeze durable plan
|
|
112
|
-
plan. Do not copy an old workflow database
|
|
113
|
-
new runs from current authored sources.
|
|
111
|
+
IR and freeze the durable plan v4 family's executable `irVersion: 5` format.
|
|
112
|
+
That is the only executable stored plan. Do not copy an old workflow database
|
|
113
|
+
expecting old runs to resume; start new runs from current authored sources.
|
|
114
114
|
|
|
115
115
|
## Recovery
|
|
116
116
|
|
|
@@ -109,13 +109,13 @@ backup over a file while a task sync or scheduler process is running.
|
|
|
109
109
|
|
|
110
110
|
## A workflow will not resume
|
|
111
111
|
|
|
112
|
-
Only durable plan
|
|
113
|
-
decoded by a compatibility runtime.
|
|
114
|
-
or YAML workflow source.
|
|
112
|
+
Only the durable plan v4 family's `irVersion: 5` executes. Pre-`irVersion`-5
|
|
113
|
+
stored plans are rejected rather than decoded by a compatibility runtime.
|
|
114
|
+
Start a new run from the current Markdown or YAML workflow source.
|
|
115
115
|
|
|
116
|
-
For
|
|
117
|
-
run uses its frozen plan. A plan-hash or schema failure is
|
|
118
|
-
corruption and must fail closed.
|
|
116
|
+
For an `irVersion: 5` run, a missing or changed authored source is not a resume
|
|
117
|
+
blocker: the run uses its frozen plan. A plan-hash or schema failure is
|
|
118
|
+
durable-state corruption and must fail closed.
|
|
119
119
|
|
|
120
120
|
## A stale transaction journal is reported
|
|
121
121
|
|
|
@@ -147,8 +147,9 @@ defaults at the layer that selected it; explicit sibling fields and nearer
|
|
|
147
147
|
layers still win. The resulting request carries the exact model ID and merged
|
|
148
148
|
inference object. Engine lowerers consume that exact selection and never run
|
|
149
149
|
alias resolution again. New workflow starts persist the exact request and
|
|
150
|
-
symbolic runner selection in durable plan v4
|
|
151
|
-
material without resolving aliases
|
|
150
|
+
symbolic runner selection in the durable plan v4 family's executable
|
|
151
|
+
`irVersion: 5`; resume consumes that frozen material without resolving aliases
|
|
152
|
+
again.
|
|
152
153
|
|
|
153
154
|
Copy the complete installed starter into the user configuration directory when
|
|
154
155
|
you want to customize all fields:
|
|
@@ -1021,16 +1021,17 @@ a committed *value*, so it cannot carry "whatever this build agent's
|
|
|
1021
1021
|
Values passed through this way are **not** redacted from the command's output
|
|
1022
1022
|
the way `env:` binding values are, so never list a credential here.
|
|
1023
1023
|
|
|
1024
|
-
#### Durable v4 forbids `inherit_env`
|
|
1024
|
+
#### Durable v4 (`irVersion: 5`) forbids `inherit_env`
|
|
1025
1025
|
|
|
1026
|
-
Every new workflow start freezes
|
|
1027
|
-
`inherit_env: true` and any other
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
part of the unit's input hash.
|
|
1026
|
+
Every new workflow start freezes the durable plan v4 family's current
|
|
1027
|
+
executable format, `irVersion: 5`. It rejects `inherit_env: true` and any other
|
|
1028
|
+
request for whole-process inheritance; use exact named environment bindings
|
|
1029
|
+
and `pass_env:` instead. Both mechanisms are dispatch-significant, keep the
|
|
1030
|
+
visible environment surface bounded, and form part of the unit's input hash.
|
|
1031
1031
|
|
|
1032
|
-
The historical `inherit_env` spelling is unsupported. Pre
|
|
1033
|
-
rejected; they are never upgraded or replayed through a second
|
|
1032
|
+
The historical `inherit_env` spelling is unsupported. Pre-`irVersion`-5 stored
|
|
1033
|
+
plans are rejected; they are never upgraded or replayed through a second
|
|
1034
|
+
runtime.
|
|
1034
1035
|
|
|
1035
1036
|
### What `akm show` reports for an exec step
|
|
1036
1037
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.2
|
|
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": [
|