akm-cli 0.9.28-alpha.4 → 0.9.28-alpha.5

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,20 @@ 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.28-alpha.5] - 2026-10-08
10
+
11
+ ### Added
12
+
13
+ - **`akm upgrade --next` installs the `@next` prerelease of akm and its OpenCode plugin.** The CLI target is
14
+ the `next` dist-tag of `akm-cli` when it is newer than the latest stable release, else the stable release
15
+ (never a downgrade); npm/Bun/pnpm installs name that exact version and binary installs take its GitHub
16
+ release. The OpenCode lockstep and cache refresh follow `akm-opencode@next` when the OpenCode config's
17
+ `plugin` list names it (akm never edits that config); with a bare `"akm-opencode"` the entry is `skipped`
18
+ with a message saying to set `"plugin": ["akm-opencode@next"]`, and lockstep stays on the `@latest` pin.
19
+ Missing, older-than-`@latest` or unpinned `@next` fails closed like the stable lockstep. Claude Code and
20
+ Codex are unchanged (no prerelease channel). The result gains `channel`. Works with `--check`, `--force`
21
+ and `-q`. See [`akm upgrade`](docs/reference/cli.md#prereleases-next).
22
+
9
23
  ## [0.9.28-alpha.4] - 2026-10-08
10
24
 
11
25
  ### Added
@@ -331,6 +331,7 @@ akm lint --fix # Auto-fix Tier 1 issues
331
331
  akm lint --fail-on-flagged # Exit non-zero when summary.flagged > 0 (CI-friendly)
332
332
  akm upgrade # Upgrade akm using its install method, then update installed harness plugins
333
333
  akm upgrade --check # Report pending CLI and plugin updates, changing nothing
334
+ akm upgrade --next # Follow prereleases (@next); OpenCode needs "akm-opencode@next" in its plugin list
334
335
  akm help migrate 0.6.0 # Print migration notes for a release (or: latest)
335
336
  akm help bundle # Print options and subcommands for one command
336
337
  akm help agents --full # Print this reference
@@ -13,12 +13,13 @@ import * as childProcess from "node:child_process";
13
13
  import fs from "node:fs";
14
14
  import os from "node:os";
15
15
  import path from "node:path";
16
- import { IS_WINDOWS } from "../../core/common.js";
16
+ import { IS_WINDOWS, stripJsonComments } from "../../core/common.js";
17
17
  import { moveToTrash } from "../../core/trash.js";
18
18
  import { semverOrder } from "../../runtime.js";
19
19
  const MARKETPLACE = "akm-plugins";
20
20
  const PLUGIN_ID = `akm@${MARKETPLACE}`;
21
21
  const OPENCODE_PACKAGE = "akm-opencode";
22
+ const OPENCODE_NEXT_SPEC = `${OPENCODE_PACKAGE}@next`;
22
23
  const READ_TIMEOUT_MS = 30_000;
23
24
  const REFRESH_TIMEOUT_MS = 120_000;
24
25
  const PREFETCH_TIMEOUT_MS = 180_000;
@@ -136,11 +137,48 @@ function upgradeCodex(dryRun) {
136
137
  const after = codexPluginVersion();
137
138
  return refreshed("codex", before.version, "version" in after ? after.version : undefined);
138
139
  }
139
- /** The cached `akm-opencode` OpenCode installed on first use, or undefined when there is none. */
140
- export function detectOpenCodeCache() {
141
- const cacheHome = process.env.XDG_CACHE_HOME?.trim() ||
142
- path.join(process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir(), ".cache");
143
- const dir = path.join(cacheHome, "opencode", "packages", `${OPENCODE_PACKAGE}@latest`);
140
+ function homeDir() {
141
+ return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir();
142
+ }
143
+ /**
144
+ * Whether the user's global OpenCode config asks for `akm-opencode@next`.
145
+ * A bare `akm-opencode` resolves to `@latest` when OpenCode prefetches it, so
146
+ * only a config that names the tag can follow prereleases. The config is only
147
+ * read, never written; project configs are not looked at (akm does not know
148
+ * which project OpenCode runs in).
149
+ */
150
+ export function openCodeConfigRequestsNext() {
151
+ const configHome = process.env.XDG_CONFIG_HOME?.trim() || path.join(homeDir(), ".config");
152
+ const files = [
153
+ process.env.OPENCODE_CONFIG?.trim(),
154
+ path.join(configHome, "opencode", "opencode.json"),
155
+ path.join(configHome, "opencode", "opencode.jsonc"),
156
+ ];
157
+ for (const file of files) {
158
+ if (!file)
159
+ continue;
160
+ try {
161
+ const config = JSON.parse(stripJsonComments(fs.readFileSync(file, "utf8")));
162
+ if (!Array.isArray(config.plugin))
163
+ continue;
164
+ // An entry is a spec string or a `[spec, options]` pair.
165
+ if (config.plugin.some((entry) => (Array.isArray(entry) ? entry[0] : entry) === OPENCODE_NEXT_SPEC))
166
+ return true;
167
+ }
168
+ catch {
169
+ // Missing or unparseable: this file does not request it.
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ /**
175
+ * The cached `akm-opencode` OpenCode installed on first use, or undefined when
176
+ * there is none. OpenCode names the folder after the spec it resolved, so a
177
+ * bare `akm-opencode` lands in `@latest` and `akm-opencode@next` in `@next`.
178
+ */
179
+ export function detectOpenCodeCache(tag = "latest") {
180
+ const cacheHome = process.env.XDG_CACHE_HOME?.trim() || path.join(homeDir(), ".cache");
181
+ const dir = path.join(cacheHome, "opencode", "packages", `${OPENCODE_PACKAGE}@${tag}`);
144
182
  if (!fs.existsSync(dir))
145
183
  return undefined;
146
184
  const readVersion = (file) => {
@@ -154,17 +192,33 @@ export function detectOpenCodeCache() {
154
192
  };
155
193
  return { dir, version: readVersion(path.join(dir, "node_modules", OPENCODE_PACKAGE, "package.json")) };
156
194
  }
157
- /** What npm's `akm-opencode@latest` is, and which akm-cli it pins. */
158
- export function lookupOpenCodeLatest() {
159
- const view = runCommand(IS_WINDOWS ? "npm.cmd" : "npm", ["view", `${OPENCODE_PACKAGE}@latest`, "version", "dependencies.akm-cli", "--json"], READ_TIMEOUT_MS);
195
+ /** What npm's `akm-opencode@<tag>` is, and which akm-cli it pins. */
196
+ export function lookupOpenCodeLatest(tag = "latest") {
197
+ const spec = `${OPENCODE_PACKAGE}@${tag}`;
198
+ const view = runCommand(IS_WINDOWS ? "npm.cmd" : "npm", ["view", spec, "version", "dependencies.akm-cli", "--json"], READ_TIMEOUT_MS);
160
199
  if (!view.ok)
161
200
  return { error: view.error };
162
201
  const parsed = parseJson(view.stdout);
163
202
  if (typeof parsed?.version !== "string")
164
- return { error: "npm did not report a version for akm-opencode@latest" };
203
+ return { error: `npm did not report a version for ${spec}` };
165
204
  const akmCli = parsed["dependencies.akm-cli"];
166
205
  return { version: parsed.version, akmCli: typeof akmCli === "string" ? akmCli : undefined };
167
206
  }
207
+ /** `akm-opencode@next`, which must exist and be no older than `@latest`: a prerelease channel that trails the stable one is not a target. */
208
+ export function lookupOpenCodeNext() {
209
+ const next = lookupOpenCodeLatest("next");
210
+ if ("error" in next)
211
+ return next;
212
+ const latest = lookupOpenCodeLatest("latest");
213
+ if ("error" in latest)
214
+ return { error: `could not compare with ${OPENCODE_PACKAGE}@latest: ${latest.error}` };
215
+ if (semverOrder(next.version, latest.version) < 0) {
216
+ return {
217
+ error: `${OPENCODE_NEXT_SPEC} (${next.version}) is older than ${OPENCODE_PACKAGE}@latest (${latest.version})`,
218
+ };
219
+ }
220
+ return next;
221
+ }
168
222
  /** Whether an OpenCode process is running (its prefetch would be replaced under it). `undefined` when that cannot be told. */
169
223
  function openCodeRunning() {
170
224
  if (IS_WINDOWS) {
@@ -198,11 +252,15 @@ function openCodeRunning() {
198
252
  return undefined;
199
253
  }
200
254
  }
201
- function upgradeOpenCode(dryRun, cache, latest) {
255
+ const NOT_FOLLOWING_NEXT = `--next: OpenCode resolves a bare "${OPENCODE_PACKAGE}" to @latest, so it is not updated to a prerelease; ` +
256
+ `set "plugin": ["${OPENCODE_NEXT_SPEC}"] in your OpenCode config to follow prereleases`;
257
+ function upgradeOpenCode(dryRun, cache, latest, tag, notFollowingNext) {
202
258
  if (!cache || !latest)
203
- return skipped("opencode", "no cached akm-opencode plugin");
259
+ return skipped("opencode", `no cached ${OPENCODE_PACKAGE}@${tag} plugin`);
204
260
  if ("error" in latest)
205
261
  return failed("opencode", latest.error);
262
+ if (notFollowingNext)
263
+ return skipped("opencode", NOT_FOLLOWING_NEXT);
206
264
  if (cache.version === latest.version)
207
265
  return { harness: "opencode", outcome: "current", from: cache.version, to: latest.version };
208
266
  const base = { harness: "opencode", from: cache.version, to: latest.version };
@@ -239,17 +297,27 @@ function upgradeOpenCode(dryRun, cache, latest) {
239
297
  finally {
240
298
  fs.rmSync(workDir, { recursive: true, force: true });
241
299
  }
242
- const after = detectOpenCodeCache();
300
+ const after = detectOpenCodeCache(tag);
243
301
  if (!after)
244
302
  return failed("opencode", "opencode did not re-create the plugin cache (the old cache is in the trash)");
245
303
  return { ...base, outcome: "updated", to: after.version ?? latest.version };
246
304
  }
247
305
  // ── Orchestration ───────────────────────────────────────────────────────────
306
+ const NO_PRERELEASE_CHANNEL = "--next: no prerelease channel; the akm-plugins marketplace is followed as usual and works with any 0.9.x akm";
307
+ /** The note `--next` adds to a harness whose plugin has no prerelease channel. */
308
+ function withNextNote(entry) {
309
+ if (entry.outcome === "skipped")
310
+ return entry;
311
+ return { ...entry, message: entry.message ? `${entry.message}; ${NO_PRERELEASE_CHANNEL}` : NO_PRERELEASE_CHANNEL };
312
+ }
248
313
  export function upgradePlugins(opts) {
314
+ const { cache, latest, tag, notFollowingNext } = opts.openCode;
315
+ const claude = upgradeClaudeCode(opts.dryRun);
316
+ const codex = upgradeCodex(opts.dryRun);
249
317
  return [
250
- upgradeClaudeCode(opts.dryRun),
251
- upgradeCodex(opts.dryRun),
252
- upgradeOpenCode(opts.dryRun, opts.openCode.cache, opts.openCode.latest),
318
+ opts.next ? withNextNote(claude) : claude,
319
+ opts.next ? withNextNote(codex) : codex,
320
+ upgradeOpenCode(opts.dryRun, cache, latest, tag, notFollowingNext),
253
321
  ];
254
322
  }
255
323
  /**
@@ -264,7 +332,7 @@ export function upgradePlugins(opts) {
264
332
  * `lockstep.reason` says why. Moving the CLI ahead of a pin nobody could read is
265
333
  * what lockstep exists to prevent; the OpenCode entry reports the failure.
266
334
  */
267
- export function applyLockstep(check, latest) {
335
+ export function applyLockstep(check, latest, tag = "latest") {
268
336
  if (!latest)
269
337
  return check;
270
338
  // Held back only when there is a release this upgrade would otherwise install.
@@ -272,8 +340,8 @@ export function applyLockstep(check, latest) {
272
340
  const pinned = "error" in latest ? undefined : latest.akmCli;
273
341
  if (!pinned) {
274
342
  const reason = "error" in latest
275
- ? `could not read the akm-cli pin of ${OPENCODE_PACKAGE}@latest: ${latest.error}`
276
- : `${OPENCODE_PACKAGE}@latest declares no akm-cli dependency`;
343
+ ? `could not read the akm-cli pin of ${OPENCODE_PACKAGE}@${tag}: ${latest.error}`
344
+ : `${OPENCODE_PACKAGE}@${tag} declares no akm-cli dependency`;
277
345
  return {
278
346
  ...check,
279
347
  latestVersion: check.currentVersion,
@@ -303,23 +371,40 @@ export function applyLockstep(check, latest) {
303
371
  lockstep,
304
372
  };
305
373
  }
374
+ /**
375
+ * Which OpenCode plugin build the CLI is held to, and which cache refreshes.
376
+ * Under `--next` that is `akm-opencode@next`, but only when the user's OpenCode
377
+ * config names that tag: otherwise OpenCode keeps resolving `@latest`, so the
378
+ * lockstep stays against the `@latest` pin and the entry is reported as skipped.
379
+ */
380
+ function resolveOpenCodeTarget(next) {
381
+ const followNext = next && openCodeConfigRequestsNext();
382
+ const tag = followNext ? "next" : "latest";
383
+ const cache = detectOpenCodeCache(tag);
384
+ const latest = cache ? (followNext ? lookupOpenCodeNext() : lookupOpenCodeLatest()) : undefined;
385
+ return { cache, latest, tag, notFollowingNext: next && !followNext };
386
+ }
306
387
  /** `akm upgrade`: the CLI step (held to the OpenCode plugin's akm), then the plugins. */
307
388
  export async function runUpgrade(args, currentVersion, deps) {
308
- const cache = detectOpenCodeCache();
309
- const latest = cache ? lookupOpenCodeLatest() : undefined;
310
- const check = applyLockstep(await deps.checkForUpdate(currentVersion), latest);
311
- const openCode = { cache, latest };
389
+ const next = args.next === true;
390
+ const channel = next ? "next" : "latest";
391
+ const openCode = resolveOpenCodeTarget(next);
392
+ const check = {
393
+ ...applyLockstep(await deps.checkForUpdate(currentVersion, channel), openCode.latest, openCode.tag),
394
+ channel,
395
+ };
312
396
  if (args.check) {
313
- return { mode: "check", result: { ...check, plugins: upgradePlugins({ dryRun: true, openCode }) } };
397
+ return { mode: "check", result: { ...check, plugins: upgradePlugins({ dryRun: true, next, openCode }) } };
314
398
  }
315
399
  const upgraded = await deps.performUpgrade(check, {
316
400
  force: args.force,
317
401
  skipPostUpgrade: args.skipPostUpgrade,
318
- // A package manager install must name the version, or `@latest` goes past the pin.
319
- ...(check.lockstep?.heldBack ? { targetVersion: check.latestVersion } : {}),
402
+ // A package manager install must name the version, or `@latest` goes past the pin
403
+ // (or past the prerelease that `--next` chose).
404
+ ...(check.lockstep?.heldBack || (next && check.latestVersion) ? { targetVersion: check.latestVersion } : {}),
320
405
  });
321
- const plugins = upgradePlugins({ dryRun: false, openCode });
322
- const result = { ...upgraded, ...(check.lockstep ? { lockstep: check.lockstep } : {}), plugins };
406
+ const plugins = upgradePlugins({ dryRun: false, next, openCode });
407
+ const result = { ...upgraded, channel, ...(check.lockstep ? { lockstep: check.lockstep } : {}), plugins };
323
408
  // The install may have succeeded, but an upgrade whose migration is
324
409
  // blocked or could not run is not done, and neither is one whose plugin
325
410
  // step failed.
@@ -174,7 +174,24 @@ export function getAkmBinaryName() {
174
174
  return "akm-windows-x64.exe";
175
175
  throw new ConfigError(`Unsupported platform for binary upgrade: ${platform}/${arch}`, "UNSUPPORTED_PLATFORM");
176
176
  }
177
- export async function checkForUpdate(currentVersion, fetchOptions) {
177
+ /** The `next` dist-tag of this package on the npm registry, or undefined when none is published. */
178
+ async function lookupNextVersion(fetchOptions) {
179
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(getInstalledPackageName())}/dist-tags`;
180
+ const response = await fetchWithRetry(url, { headers: { accept: "application/json" } }, fetchOptions);
181
+ if (!response.ok) {
182
+ throw new Error(`Failed to check for the next prerelease: ${response.status} ${response.statusText}`);
183
+ }
184
+ const tags = JSON.parse(await readBodyWithByteCap(response, MAX_CHECKSUM_METADATA_BYTES));
185
+ return typeof tags.next === "string" && tags.next !== "" ? tags.next : undefined;
186
+ }
187
+ /**
188
+ * The newest release. With `channel: "next"` that includes prereleases: the
189
+ * `next` dist-tag when it is newer than the latest stable release, else the
190
+ * stable release (a prerelease older than a stable one is never a target).
191
+ * A binary install downloads the GitHub release tagged `v<version>`, which a
192
+ * prerelease has too, so the version is all the install step needs.
193
+ */
194
+ export async function checkForUpdate(currentVersion, fetchOptions, channel = "latest") {
178
195
  const installMethod = detectInstallMethod();
179
196
  const url = `https://api.github.com/repos/${REPO}/releases/latest`;
180
197
  const response = await fetchWithRetry(url, { headers: githubHeaders() }, fetchOptions);
@@ -183,7 +200,12 @@ export async function checkForUpdate(currentVersion, fetchOptions) {
183
200
  }
184
201
  const release = JSON.parse(await readBodyWithByteCap(response, MAX_CHECKSUM_METADATA_BYTES));
185
202
  const latestTag = release.tag_name ?? "";
186
- const latestVersion = latestTag.replace(/^v/, "");
203
+ let latestVersion = latestTag.replace(/^v/, "");
204
+ if (channel === "next") {
205
+ const next = await lookupNextVersion(fetchOptions);
206
+ if (next && (latestVersion === "" || semverOrder(latestVersion, next) < 0))
207
+ latestVersion = next;
208
+ }
187
209
  return {
188
210
  currentVersion,
189
211
  latestVersion,
@@ -54,9 +54,17 @@ export const upgradeCommand = defineJsonCommand({
54
54
  description: "Skip the post-upgrade index rebuild",
55
55
  default: false,
56
56
  },
57
+ next: {
58
+ type: "boolean",
59
+ description: "Install the newest prerelease (the @next npm tag) of akm and, if OpenCode's config names akm-opencode@next, of its plugin",
60
+ default: false,
61
+ },
57
62
  },
58
63
  async run({ args }) {
59
- const run = await runUpgrade({ check: args.check, force: args.force, skipPostUpgrade: args["skip-post-upgrade"] }, pkgVersion, { checkForUpdate, performUpgrade: (check, opts) => performUpgrade(check, opts) });
64
+ const run = await runUpgrade({ check: args.check, force: args.force, skipPostUpgrade: args["skip-post-upgrade"], next: args.next }, pkgVersion, {
65
+ checkForUpdate: (version, channel) => checkForUpdate(version, undefined, channel),
66
+ performUpgrade: (check, opts) => performUpgrade(check, opts),
67
+ });
60
68
  if (run.mode === "check") {
61
69
  output("upgrade", run.result);
62
70
  return;
@@ -693,7 +693,7 @@ function formatUpgradeHead(r) {
693
693
  return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
694
694
  }
695
695
  if (r.updateAvailable === true) {
696
- return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade' to install)`;
696
+ return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade${r.channel === "next" ? " --next" : ""}' to install)`;
697
697
  }
698
698
  if (r.updateAvailable === false && r.latestVersion) {
699
699
  return `akm v${r.currentVersion} is already the latest version`;
@@ -30493,7 +30493,7 @@ function formatUpgradeHead(r) {
30493
30493
  return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
30494
30494
  }
30495
30495
  if (r.updateAvailable === true) {
30496
- return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade' to install)`;
30496
+ return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade${r.channel === "next" ? " --next" : ""}' to install)`;
30497
30497
  }
30498
30498
  if (r.updateAvailable === false && r.latestVersion) {
30499
30499
  return `akm v${r.currentVersion} is already the latest version`;
@@ -29821,7 +29821,7 @@ function formatUpgradeHead(r) {
29821
29821
  return `akm upgraded: v${r.currentVersion} \u2192 v${r.newVersion}`;
29822
29822
  }
29823
29823
  if (r.updateAvailable === true) {
29824
- return `akm v${r.currentVersion} \u2192 v${r.latestVersion} available (run 'akm upgrade' to install)`;
29824
+ return `akm v${r.currentVersion} \u2192 v${r.latestVersion} available (run 'akm upgrade${r.channel === "next" ? " --next" : ""}' to install)`;
29825
29825
  }
29826
29826
  if (r.updateAvailable === false && r.latestVersion) {
29827
29827
  return `akm v${r.currentVersion} is already the latest version`;
@@ -1339,6 +1339,7 @@ computed, with a 256 MiB binary limit. Release/checksum metadata is capped at
1339
1339
  akm upgrade # Install a newer release if there is one, run every pending migration, then update the harness plugins
1340
1340
  akm upgrade --check # Report pending updates, CLI and plugins, without changing anything (no migration step)
1341
1341
  akm upgrade --force # Force the install even if already on latest
1342
+ akm upgrade --next # Install the newest prerelease (@next) instead of the latest stable release
1342
1343
  ```
1343
1344
 
1344
1345
  | Flag | Description |
@@ -1346,6 +1347,7 @@ akm upgrade --force # Force the install even if already on latest
1346
1347
  | `--check` | Report pending updates (CLI and per-harness plugins) without changing anything |
1347
1348
  | `--force` | Force upgrade even if on latest version |
1348
1349
  | `--skip-post-upgrade` | Skip the post-upgrade index rebuild |
1350
+ | `--next` | Follow the `next` prerelease channel: install the newest prerelease of akm (and of the OpenCode plugin, see [Prereleases](#prereleases-next)). Works with `--check`, `--force` and `-q` |
1349
1351
 
1350
1352
  Offline, or to migrate without a release check, run `akm migrate apply`
1351
1353
  directly: it is the same step.
@@ -1399,6 +1401,41 @@ be read (the npm lookup failed, or `akm-opencode@latest` declares no
1399
1401
  OpenCode entry is `failed` and the run exits `1`; the next `akm upgrade`
1400
1402
  retries.
1401
1403
 
1404
+ #### Prereleases (`--next`)
1405
+
1406
+ `akm upgrade --next` means "newest available, prereleases included". The result
1407
+ carries `channel: "next"` (`"latest"` otherwise); no other field changes.
1408
+
1409
+ - **CLI.** The target is the `next` dist-tag of `akm-cli` (read from the npm
1410
+ registry, so it works for binary installs too) when that is newer than the
1411
+ latest stable release; otherwise the latest stable release. It never moves
1412
+ backwards. npm, Bun and pnpm installs run `<manager> add|install -g akm-cli@<that exact version>`;
1413
+ standalone binaries download the GitHub release tagged `v<that version>`
1414
+ (prereleases are GitHub releases too) and verify its checksum as usual.
1415
+ - **OpenCode.** OpenCode resolves a bare `"akm-opencode"` in its `plugin` list
1416
+ to `@latest`, and caches `@next` in its own folder
1417
+ (`$XDG_CACHE_HOME/opencode/packages/akm-opencode@next`). akm does not edit
1418
+ your OpenCode config, so it can only follow `@next` when you ask for it:
1419
+
1420
+ ```json
1421
+ { "plugin": ["akm-opencode@next"] }
1422
+ ```
1423
+
1424
+ With that line in the global OpenCode config (`~/.config/opencode/opencode.json`
1425
+ or `.jsonc`, or the file named by `OPENCODE_CONFIG`), lockstep and the cache
1426
+ refresh use `akm-opencode@next` (its version and its `akm-cli` pin) instead of
1427
+ `@latest`. If `akm-opencode@next` is missing, older than `@latest`, or its
1428
+ `akm-cli` pin is unreadable, the CLI is held where it is and the OpenCode entry
1429
+ is `failed`, exactly as in the stable lockstep above.
1430
+
1431
+ Without that line the OpenCode entry is `skipped`, its message names the line
1432
+ to add, and lockstep stays against the `@latest` pin, so the CLI does not go
1433
+ past what OpenCode will run.
1434
+ - **Claude Code and Codex.** Unchanged: their plugin comes from the `akm-plugins`
1435
+ git marketplace, which has no prerelease channel and accepts any 0.9.x akm,
1436
+ prereleases included. Their entries carry a note saying so.
1437
+ - `--check --next` reports all of this and changes nothing.
1438
+
1402
1439
  #### Containers
1403
1440
 
1404
1441
  With the plugin step, a container entrypoint needs only:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.28-alpha.4",
3
+ "version": "0.9.28-alpha.5",
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": [