akm-cli 0.9.28-alpha.2 → 0.9.28-alpha.4
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 +33 -0
- package/dist/assets/hints/cli-hints-full.md +2 -2
- package/dist/commands/improve/consolidate/pair-pass.js +3 -0
- package/dist/commands/improve/distill.js +6 -0
- package/dist/commands/sources/plugin-upgrade.js +328 -0
- package/dist/commands/sources/self-update.js +3 -3
- package/dist/commands/sources/sources-cli.js +15 -11
- package/dist/core/trash.js +73 -0
- package/dist/indexer/walk/matchers.js +3 -2
- package/dist/output/text/command-format.js +26 -0
- package/dist/scripts/akm-migrate-node.js +25 -1
- package/dist/scripts/akm-migrate.js +25 -1
- package/docs/integration/bundling-akm.md +6 -1
- package/docs/reference/cli.md +79 -3
- package/docs/reference/data-and-telemetry.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ 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.4] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`akm upgrade` also updates the akm plugin of each installed harness (#1007).** After the CLI step
|
|
14
|
+
it refreshes Claude Code (`claude plugin marketplace update`, then `claude plugin update`) and
|
|
15
|
+
Codex (`codex plugin marketplace upgrade`) when the `akm-plugins` marketplace is configured and the
|
|
16
|
+
plugin is installed, and replaces a stale cached `akm-opencode` (moved to the trash, then re-fetched
|
|
17
|
+
by `opencode debug config`) unless OpenCode is running, in which case it is `deferred`. It only
|
|
18
|
+
updates: a missing plugin is never installed. The result gains `plugins` (one `outcome` per harness:
|
|
19
|
+
`updated`, `current`, `skipped`, `deferred`, `failed`; under `--check`, which changes nothing,
|
|
20
|
+
`pending` for OpenCode and `unknown` for Claude Code and Codex, whose check needs a fetch) and a failed plugin exits 1. With the OpenCode plugin present the CLI moves to the akm-cli
|
|
21
|
+
that `akm-opencode@latest` pins instead of the newest release, reported under `lockstep`; if the pin
|
|
22
|
+
cannot be read the CLI is held where it is rather than moved ahead of the plugin. The
|
|
23
|
+
Containers entrypoint and the Codex hook trust entries are documented under
|
|
24
|
+
[`akm upgrade`](docs/reference/cli.md#upgrade).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **A skill's reference file with a shell `$1` is no longer indexed as a command (#1063).** A file under
|
|
29
|
+
`skills/**` other than `SKILL.md` that showed `local var="$1"` in a code block was retyped to
|
|
30
|
+
`commands/skills/<name>/...`. A skill's folder now counts as a declared context, as `memories/` and the other typed
|
|
31
|
+
directories already did, so the file stays a skill resource (`knowledge/skills/<name>/...`). A `$1` under
|
|
32
|
+
`commands/` or in a loose file is still a command.
|
|
33
|
+
|
|
34
|
+
## [0.9.28-alpha.3] - 2026-10-08
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- A consolidate pair-pass retire proposal now records the judge's claim lists in its `retirement` metadata as `onlyInRetired` and `onlyInSuccessor`, named by role rather than by the judge's A/B, so what the judge found only on each side survives past the run. Proposals minted before this lack both fields; no row is added for a pair akm keeps.
|
|
39
|
+
|
|
40
|
+
- A distill lesson the quality gate rejects or sends to review now keeps its text: the `distill_invoked` event and the distill result carry `rejectedContent`, cut to 2000 characters (the judge prompt's cap). Before, a rejected lesson left only its score and reason. It stays local in `state.db`.
|
|
41
|
+
|
|
9
42
|
## [0.9.28-alpha.2] - 2026-10-08
|
|
10
43
|
|
|
11
44
|
### Added
|
|
@@ -329,8 +329,8 @@ akm bundle list # List all sources
|
|
|
329
329
|
akm lint # Structural lint over the bundle; exits 0 regardless of findings
|
|
330
330
|
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
|
-
akm upgrade # Upgrade akm using its install method
|
|
333
|
-
akm upgrade --check #
|
|
332
|
+
akm upgrade # Upgrade akm using its install method, then update installed harness plugins
|
|
333
|
+
akm upgrade --check # Report pending CLI and plugin updates, changing nothing
|
|
334
334
|
akm help migrate 0.6.0 # Print migration notes for a release (or: latest)
|
|
335
335
|
akm help bundle # Print options and subcommands for one command
|
|
336
336
|
akm help agents --full # Print this reference
|
|
@@ -637,6 +637,9 @@ async function judgeOne(ctx, candidate) {
|
|
|
637
637
|
cosine: candidate.cosine,
|
|
638
638
|
judgeLabel: verdict.relation,
|
|
639
639
|
judgeReason: verdict.reason,
|
|
640
|
+
// The judge's A is the older side and B the newer (see orderByAge); record the lists by role.
|
|
641
|
+
onlyInRetired: retired === older ? verdict.onlyInA : verdict.onlyInB,
|
|
642
|
+
onlyInSuccessor: retired === older ? verdict.onlyInB : verdict.onlyInA,
|
|
640
643
|
retiredContentHash: retiredHash,
|
|
641
644
|
successorContentHash: successorHash,
|
|
642
645
|
reason,
|
|
@@ -638,6 +638,8 @@ function rejectDistilled(run, proposalRef, content, score, reason, meta) {
|
|
|
638
638
|
ledgerRef: run.ledgerRef,
|
|
639
639
|
});
|
|
640
640
|
}
|
|
641
|
+
/** Longest lesson text kept on a rejection, the same cap the quality judge's prompt reads (stage.ts). */
|
|
642
|
+
const REJECTED_CONTENT_MAX_CHARS = 2000;
|
|
641
643
|
/**
|
|
642
644
|
* Record a distill quality-gate outcome and return its envelope.
|
|
643
645
|
* `quality_rejected` lands in the improve ledger under the input's key (its
|
|
@@ -673,6 +675,8 @@ export function writeQualityRejection(args) {
|
|
|
673
675
|
}
|
|
674
676
|
}
|
|
675
677
|
const eligMeta = args.eligibilitySource ? { eligibilitySource: args.eligibilitySource } : {};
|
|
678
|
+
// The text the gate turned away, kept in the local event and the result so a rejection can be audited.
|
|
679
|
+
const rejectedContent = args.content.slice(0, REJECTED_CONTENT_MAX_CHARS);
|
|
676
680
|
appendEvent({
|
|
677
681
|
eventType: "distill_invoked",
|
|
678
682
|
ref: ledgerRef,
|
|
@@ -681,6 +685,7 @@ export function writeQualityRejection(args) {
|
|
|
681
685
|
proposalRef: args.proposalRef,
|
|
682
686
|
score: args.score,
|
|
683
687
|
reason: args.reason,
|
|
688
|
+
rejectedContent,
|
|
684
689
|
...meta,
|
|
685
690
|
...eligMeta,
|
|
686
691
|
},
|
|
@@ -693,6 +698,7 @@ export function writeQualityRejection(args) {
|
|
|
693
698
|
proposalRef: args.proposalRef,
|
|
694
699
|
score: args.score,
|
|
695
700
|
reason: args.reason,
|
|
701
|
+
rejectedContent,
|
|
696
702
|
...(proposal ? { proposalId: proposal.id, proposal } : {}),
|
|
697
703
|
...meta,
|
|
698
704
|
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The plugin half of `akm upgrade` (#1007): refresh the akm plugin of each
|
|
6
|
+
* installed agent harness, and keep the CLI in version lockstep with the
|
|
7
|
+
* OpenCode plugin. Update only: a plugin that is not installed is never
|
|
8
|
+
* installed, and a harness without one is skipped. Every external command
|
|
9
|
+
* runs with a timeout and its failure is captured on that harness's entry,
|
|
10
|
+
* so a broken harness never aborts the CLI upgrade.
|
|
11
|
+
*/
|
|
12
|
+
import * as childProcess from "node:child_process";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { IS_WINDOWS } from "../../core/common.js";
|
|
17
|
+
import { moveToTrash } from "../../core/trash.js";
|
|
18
|
+
import { semverOrder } from "../../runtime.js";
|
|
19
|
+
const MARKETPLACE = "akm-plugins";
|
|
20
|
+
const PLUGIN_ID = `akm@${MARKETPLACE}`;
|
|
21
|
+
const OPENCODE_PACKAGE = "akm-opencode";
|
|
22
|
+
const READ_TIMEOUT_MS = 30_000;
|
|
23
|
+
const REFRESH_TIMEOUT_MS = 120_000;
|
|
24
|
+
const PREFETCH_TIMEOUT_MS = 180_000;
|
|
25
|
+
function runCommand(command, args, timeoutMs, opts) {
|
|
26
|
+
const result = childProcess.spawnSync(command, args, {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
env: process.env,
|
|
29
|
+
stdio: "pipe",
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
killSignal: "SIGKILL",
|
|
32
|
+
cwd: opts?.cwd,
|
|
33
|
+
});
|
|
34
|
+
const label = `${command} ${args.join(" ")}`;
|
|
35
|
+
if (result.error) {
|
|
36
|
+
const code = result.error.code;
|
|
37
|
+
if (code === "ETIMEDOUT")
|
|
38
|
+
return { ok: false, missing: false, error: `\`${label}\` timed out after ${timeoutMs / 1000}s` };
|
|
39
|
+
return { ok: false, missing: code === "ENOENT", error: `\`${label}\` could not run: ${result.error.message}` };
|
|
40
|
+
}
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
const detail = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
|
|
43
|
+
return { ok: false, missing: false, error: `\`${label}\` failed: ${detail}` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, stdout: result.stdout ?? "" };
|
|
46
|
+
}
|
|
47
|
+
function parseJson(text) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function skipped(harness, message) {
|
|
56
|
+
return { harness, outcome: "skipped", message };
|
|
57
|
+
}
|
|
58
|
+
function failed(harness, message) {
|
|
59
|
+
return { harness, outcome: "failed", message };
|
|
60
|
+
}
|
|
61
|
+
/** An entry for a refresh that ran: `updated` when the installed version moved, else `current`. */
|
|
62
|
+
function refreshed(harness, from, to) {
|
|
63
|
+
const moved = from !== undefined && to !== undefined && from !== to;
|
|
64
|
+
return { harness, outcome: moved ? "updated" : "current", from, to };
|
|
65
|
+
}
|
|
66
|
+
/** Unknown, in `--check`: knowing whether a newer build exists needs a marketplace fetch, which `--check` does not do. */
|
|
67
|
+
function unknownRefresh(harness, from) {
|
|
68
|
+
return {
|
|
69
|
+
harness,
|
|
70
|
+
outcome: "unknown",
|
|
71
|
+
from,
|
|
72
|
+
message: "checking needs a fetch of the akm-plugins marketplace, which --check does not do; `akm upgrade` refreshes it",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function claudePluginVersion() {
|
|
76
|
+
const listed = runCommand("claude", ["plugin", "list", "--json"], READ_TIMEOUT_MS);
|
|
77
|
+
if (!listed.ok)
|
|
78
|
+
return { error: listed };
|
|
79
|
+
const plugins = parseJson(listed.stdout);
|
|
80
|
+
const plugin = Array.isArray(plugins) ? plugins.find((p) => p?.id === PLUGIN_ID) : undefined;
|
|
81
|
+
return { version: plugin ? String(plugin.version ?? "") : undefined };
|
|
82
|
+
}
|
|
83
|
+
function detectClaudeCode() {
|
|
84
|
+
const marketplaces = runCommand("claude", ["plugin", "marketplace", "list", "--json"], READ_TIMEOUT_MS);
|
|
85
|
+
if (!marketplaces.ok) {
|
|
86
|
+
return { entry: skipped("claude-code", marketplaces.missing ? "claude is not on PATH" : marketplaces.error) };
|
|
87
|
+
}
|
|
88
|
+
const list = parseJson(marketplaces.stdout);
|
|
89
|
+
if (!Array.isArray(list) || !list.some((m) => m?.name === MARKETPLACE)) {
|
|
90
|
+
return { entry: skipped("claude-code", `the ${MARKETPLACE} marketplace is not configured`) };
|
|
91
|
+
}
|
|
92
|
+
const plugin = claudePluginVersion();
|
|
93
|
+
if ("error" in plugin)
|
|
94
|
+
return { entry: skipped("claude-code", plugin.error.error) };
|
|
95
|
+
if (plugin.version === undefined)
|
|
96
|
+
return { entry: skipped("claude-code", "the akm plugin is not installed") };
|
|
97
|
+
return { version: plugin.version };
|
|
98
|
+
}
|
|
99
|
+
function upgradeClaudeCode(dryRun) {
|
|
100
|
+
const detected = detectClaudeCode();
|
|
101
|
+
if ("entry" in detected)
|
|
102
|
+
return detected.entry;
|
|
103
|
+
if (dryRun)
|
|
104
|
+
return unknownRefresh("claude-code", detected.version);
|
|
105
|
+
const marketplace = runCommand("claude", ["plugin", "marketplace", "update", MARKETPLACE], REFRESH_TIMEOUT_MS);
|
|
106
|
+
if (!marketplace.ok)
|
|
107
|
+
return failed("claude-code", marketplace.error);
|
|
108
|
+
const update = runCommand("claude", ["plugin", "update", PLUGIN_ID], REFRESH_TIMEOUT_MS);
|
|
109
|
+
if (!update.ok)
|
|
110
|
+
return failed("claude-code", update.error);
|
|
111
|
+
const after = claudePluginVersion();
|
|
112
|
+
return refreshed("claude-code", detected.version, "version" in after ? after.version : undefined);
|
|
113
|
+
}
|
|
114
|
+
// ── Codex ───────────────────────────────────────────────────────────────────
|
|
115
|
+
function codexPluginVersion() {
|
|
116
|
+
const listed = runCommand("codex", ["plugin", "list", "--marketplace", MARKETPLACE, "--json"], READ_TIMEOUT_MS);
|
|
117
|
+
if (!listed.ok)
|
|
118
|
+
return { error: listed };
|
|
119
|
+
const parsed = parseJson(listed.stdout);
|
|
120
|
+
const plugin = parsed?.installed?.find((p) => p?.pluginId === PLUGIN_ID);
|
|
121
|
+
return { version: plugin ? String(plugin.version ?? "") : undefined };
|
|
122
|
+
}
|
|
123
|
+
function upgradeCodex(dryRun) {
|
|
124
|
+
const before = codexPluginVersion();
|
|
125
|
+
if ("error" in before)
|
|
126
|
+
return skipped("codex", before.error.missing ? "codex is not on PATH" : before.error.error);
|
|
127
|
+
if (before.version === undefined) {
|
|
128
|
+
return skipped("codex", `the akm plugin is not installed from the ${MARKETPLACE} marketplace`);
|
|
129
|
+
}
|
|
130
|
+
if (dryRun)
|
|
131
|
+
return unknownRefresh("codex", before.version);
|
|
132
|
+
// Also refreshes the installed plugin's cache, so no re-add is needed.
|
|
133
|
+
const upgrade = runCommand("codex", ["plugin", "marketplace", "upgrade", MARKETPLACE], REFRESH_TIMEOUT_MS);
|
|
134
|
+
if (!upgrade.ok)
|
|
135
|
+
return failed("codex", upgrade.error);
|
|
136
|
+
const after = codexPluginVersion();
|
|
137
|
+
return refreshed("codex", before.version, "version" in after ? after.version : undefined);
|
|
138
|
+
}
|
|
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`);
|
|
144
|
+
if (!fs.existsSync(dir))
|
|
145
|
+
return undefined;
|
|
146
|
+
const readVersion = (file) => {
|
|
147
|
+
try {
|
|
148
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
149
|
+
return typeof pkg.version === "string" ? pkg.version : undefined;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return { dir, version: readVersion(path.join(dir, "node_modules", OPENCODE_PACKAGE, "package.json")) };
|
|
156
|
+
}
|
|
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);
|
|
160
|
+
if (!view.ok)
|
|
161
|
+
return { error: view.error };
|
|
162
|
+
const parsed = parseJson(view.stdout);
|
|
163
|
+
if (typeof parsed?.version !== "string")
|
|
164
|
+
return { error: "npm did not report a version for akm-opencode@latest" };
|
|
165
|
+
const akmCli = parsed["dependencies.akm-cli"];
|
|
166
|
+
return { version: parsed.version, akmCli: typeof akmCli === "string" ? akmCli : undefined };
|
|
167
|
+
}
|
|
168
|
+
/** Whether an OpenCode process is running (its prefetch would be replaced under it). `undefined` when that cannot be told. */
|
|
169
|
+
function openCodeRunning() {
|
|
170
|
+
if (IS_WINDOWS) {
|
|
171
|
+
const tasks = runCommand("tasklist", ["/FI", "IMAGENAME eq opencode.exe", "/NH"], READ_TIMEOUT_MS);
|
|
172
|
+
return tasks.ok ? /opencode\.exe/i.test(tasks.stdout) : undefined;
|
|
173
|
+
}
|
|
174
|
+
// `-f` because a Node-wrapped opencode shows up as `node …/opencode`; the
|
|
175
|
+
// pattern needs `opencode` to be a whole path segment so `akm-opencode`
|
|
176
|
+
// in some other command's arguments does not match.
|
|
177
|
+
const pgrep = runCommand("pgrep", ["-f", "(^|/)opencode( |$)"], READ_TIMEOUT_MS);
|
|
178
|
+
if (pgrep.ok)
|
|
179
|
+
return true;
|
|
180
|
+
if (!pgrep.missing)
|
|
181
|
+
return false; // pgrep exits 1 when nothing matched
|
|
182
|
+
if (process.platform !== "linux")
|
|
183
|
+
return undefined;
|
|
184
|
+
try {
|
|
185
|
+
return fs.readdirSync("/proc").some((pid) => {
|
|
186
|
+
if (!/^\d+$/.test(pid) || Number(pid) === process.pid)
|
|
187
|
+
return false;
|
|
188
|
+
try {
|
|
189
|
+
const argv = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
|
|
190
|
+
return argv.slice(0, 2).some((arg) => path.basename(arg) === "opencode");
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function upgradeOpenCode(dryRun, cache, latest) {
|
|
202
|
+
if (!cache || !latest)
|
|
203
|
+
return skipped("opencode", "no cached akm-opencode plugin");
|
|
204
|
+
if ("error" in latest)
|
|
205
|
+
return failed("opencode", latest.error);
|
|
206
|
+
if (cache.version === latest.version)
|
|
207
|
+
return { harness: "opencode", outcome: "current", from: cache.version, to: latest.version };
|
|
208
|
+
const base = { harness: "opencode", from: cache.version, to: latest.version };
|
|
209
|
+
const running = openCodeRunning();
|
|
210
|
+
if (running !== false) {
|
|
211
|
+
return {
|
|
212
|
+
...base,
|
|
213
|
+
outcome: "deferred",
|
|
214
|
+
message: running === true
|
|
215
|
+
? "OpenCode is running; the plugin cache is replaced by a later `akm upgrade` once it has exited"
|
|
216
|
+
: "could not tell whether OpenCode is running; the plugin cache is replaced by a later `akm upgrade`",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (dryRun)
|
|
220
|
+
return { ...base, outcome: "pending" };
|
|
221
|
+
// OpenCode must be runnable before the cache goes: it does the prefetch.
|
|
222
|
+
const probe = runCommand("opencode", ["--version"], READ_TIMEOUT_MS);
|
|
223
|
+
if (!probe.ok)
|
|
224
|
+
return skipped("opencode", probe.missing ? "opencode is not on PATH" : probe.error);
|
|
225
|
+
try {
|
|
226
|
+
moveToTrash(cache.dir);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return failed("opencode", `could not move ${cache.dir} to the trash: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
// Any OpenCode command that resolves the config installs the plugin again;
|
|
232
|
+
// from a temp dir, so no project's opencode.json is read.
|
|
233
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-opencode-prefetch-"));
|
|
234
|
+
try {
|
|
235
|
+
const prefetch = runCommand("opencode", ["debug", "config"], PREFETCH_TIMEOUT_MS, { cwd: workDir });
|
|
236
|
+
if (!prefetch.ok)
|
|
237
|
+
return failed("opencode", `${prefetch.error} (the old cache is in the trash)`);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
const after = detectOpenCodeCache();
|
|
243
|
+
if (!after)
|
|
244
|
+
return failed("opencode", "opencode did not re-create the plugin cache (the old cache is in the trash)");
|
|
245
|
+
return { ...base, outcome: "updated", to: after.version ?? latest.version };
|
|
246
|
+
}
|
|
247
|
+
// ── Orchestration ───────────────────────────────────────────────────────────
|
|
248
|
+
export function upgradePlugins(opts) {
|
|
249
|
+
return [
|
|
250
|
+
upgradeClaudeCode(opts.dryRun),
|
|
251
|
+
upgradeCodex(opts.dryRun),
|
|
252
|
+
upgradeOpenCode(opts.dryRun, opts.openCode.cache, opts.openCode.latest),
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* When the OpenCode plugin is present the CLI target is the akm-cli that
|
|
257
|
+
* `akm-opencode@latest` pins, not the newest release: the plugin runs that
|
|
258
|
+
* exact akm in-process, against databases a newer CLI may already have
|
|
259
|
+
* migrated. The CLI never moves backwards to meet the pin.
|
|
260
|
+
*
|
|
261
|
+
* It fails closed: when the plugin is present but its pin cannot be read (the
|
|
262
|
+
* npm lookup failed, or the package declares no akm-cli), the CLI is held where
|
|
263
|
+
* it is (`updateAvailable: false`, `latestVersion` = the current version) and
|
|
264
|
+
* `lockstep.reason` says why. Moving the CLI ahead of a pin nobody could read is
|
|
265
|
+
* what lockstep exists to prevent; the OpenCode entry reports the failure.
|
|
266
|
+
*/
|
|
267
|
+
export function applyLockstep(check, latest) {
|
|
268
|
+
if (!latest)
|
|
269
|
+
return check;
|
|
270
|
+
// Held back only when there is a release this upgrade would otherwise install.
|
|
271
|
+
const wouldInstall = semverOrder(check.currentVersion, check.latestVersion) < 0;
|
|
272
|
+
const pinned = "error" in latest ? undefined : latest.akmCli;
|
|
273
|
+
if (!pinned) {
|
|
274
|
+
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`;
|
|
277
|
+
return {
|
|
278
|
+
...check,
|
|
279
|
+
latestVersion: check.currentVersion,
|
|
280
|
+
updateAvailable: false,
|
|
281
|
+
lockstep: {
|
|
282
|
+
plugin: OPENCODE_PACKAGE,
|
|
283
|
+
pinnedVersion: null,
|
|
284
|
+
newestVersion: check.latestVersion,
|
|
285
|
+
heldBack: wouldInstall,
|
|
286
|
+
reason,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const heldBack = semverOrder(pinned, check.latestVersion) < 0 && wouldInstall;
|
|
291
|
+
const lockstep = {
|
|
292
|
+
plugin: OPENCODE_PACKAGE,
|
|
293
|
+
pinnedVersion: pinned,
|
|
294
|
+
newestVersion: check.latestVersion,
|
|
295
|
+
heldBack,
|
|
296
|
+
};
|
|
297
|
+
if (!heldBack)
|
|
298
|
+
return { ...check, lockstep };
|
|
299
|
+
return {
|
|
300
|
+
...check,
|
|
301
|
+
latestVersion: pinned,
|
|
302
|
+
updateAvailable: semverOrder(check.currentVersion, pinned) < 0,
|
|
303
|
+
lockstep,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/** `akm upgrade`: the CLI step (held to the OpenCode plugin's akm), then the plugins. */
|
|
307
|
+
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 };
|
|
312
|
+
if (args.check) {
|
|
313
|
+
return { mode: "check", result: { ...check, plugins: upgradePlugins({ dryRun: true, openCode }) } };
|
|
314
|
+
}
|
|
315
|
+
const upgraded = await deps.performUpgrade(check, {
|
|
316
|
+
force: args.force,
|
|
317
|
+
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 } : {}),
|
|
320
|
+
});
|
|
321
|
+
const plugins = upgradePlugins({ dryRun: false, openCode });
|
|
322
|
+
const result = { ...upgraded, ...(check.lockstep ? { lockstep: check.lockstep } : {}), plugins };
|
|
323
|
+
// The install may have succeeded, but an upgrade whose migration is
|
|
324
|
+
// blocked or could not run is not done, and neither is one whose plugin
|
|
325
|
+
// step failed.
|
|
326
|
+
const migrationFailed = upgraded.migration?.status === "blocked" || upgraded.migration?.status === "failed";
|
|
327
|
+
return { mode: "upgrade", result, failed: migrationFailed || plugins.some((p) => p.outcome === "failed") };
|
|
328
|
+
}
|
|
@@ -237,7 +237,7 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
237
237
|
migration: await runMigrationStep(runTool),
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
-
const packageManagerCommand = getPackageManagerUpgradeCommand(installMethod);
|
|
240
|
+
const packageManagerCommand = getPackageManagerUpgradeCommand(installMethod, undefined, opts?.targetVersion);
|
|
241
241
|
if (packageManagerCommand) {
|
|
242
242
|
return runPackageManagerUpgrade({
|
|
243
243
|
packageManagerCommand,
|
|
@@ -574,8 +574,8 @@ function resolveNodePackageManagerCommand(name) {
|
|
|
574
574
|
const adjacent = path.join(path.dirname(process.execPath), `${name}${extension}`);
|
|
575
575
|
return fs.existsSync(adjacent) ? adjacent : name;
|
|
576
576
|
}
|
|
577
|
-
export function getPackageManagerUpgradeCommand(installMethod, packageName = getInstalledPackageName()) {
|
|
578
|
-
const pkgRef = `${packageName}
|
|
577
|
+
export function getPackageManagerUpgradeCommand(installMethod, packageName = getInstalledPackageName(), version = "latest") {
|
|
578
|
+
const pkgRef = `${packageName}@${version}`;
|
|
579
579
|
if (installMethod === "bun") {
|
|
580
580
|
return {
|
|
581
581
|
command: "bun",
|
|
@@ -33,12 +33,21 @@ import { UsageError } from "../../core/errors.js";
|
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
34
34
|
import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
|
|
35
35
|
import { pkgVersion } from "../../version.js";
|
|
36
|
+
import { runUpgrade } from "./plugin-upgrade.js";
|
|
36
37
|
import { checkForUpdate, performUpgrade } from "./self-update.js";
|
|
37
38
|
import { akmClone } from "./source-clone.js";
|
|
38
39
|
export const upgradeCommand = defineJsonCommand({
|
|
39
|
-
meta: {
|
|
40
|
+
meta: {
|
|
41
|
+
name: "upgrade",
|
|
42
|
+
description: "Upgrade akm to the latest release, then update the akm plugin of each installed harness " +
|
|
43
|
+
"(Claude Code, Codex, OpenCode). With the OpenCode plugin installed, akm moves to the version that plugin pins.",
|
|
44
|
+
},
|
|
40
45
|
args: {
|
|
41
|
-
check: {
|
|
46
|
+
check: {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
description: "Report pending updates, CLI and per-harness plugins, without changing anything",
|
|
49
|
+
default: false,
|
|
50
|
+
},
|
|
42
51
|
force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
|
|
43
52
|
"skip-post-upgrade": {
|
|
44
53
|
type: "boolean",
|
|
@@ -47,17 +56,12 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
47
56
|
},
|
|
48
57
|
},
|
|
49
58
|
async run({ args }) {
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
output("upgrade",
|
|
59
|
+
const run = await runUpgrade({ check: args.check, force: args.force, skipPostUpgrade: args["skip-post-upgrade"] }, pkgVersion, { checkForUpdate, performUpgrade: (check, opts) => performUpgrade(check, opts) });
|
|
60
|
+
if (run.mode === "check") {
|
|
61
|
+
output("upgrade", run.result);
|
|
53
62
|
return;
|
|
54
63
|
}
|
|
55
|
-
|
|
56
|
-
const result = await performUpgrade(check, { force: args.force, skipPostUpgrade });
|
|
57
|
-
// The install may have succeeded, but an upgrade whose migration is
|
|
58
|
-
// blocked or could not run is not done: exit like `akm migrate apply` does.
|
|
59
|
-
const migrationFailed = result.migration?.status === "blocked" || result.migration?.status === "failed";
|
|
60
|
-
outputWithExitCode("upgrade", result, migrationFailed ? EXIT_CODES.GENERAL : undefined);
|
|
64
|
+
outputWithExitCode("upgrade", run.result, run.failed ? EXIT_CODES.GENERAL : undefined);
|
|
61
65
|
},
|
|
62
66
|
});
|
|
63
67
|
// `sync` body, standalone so the git-commit/push logic stays in one place.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import * as childProcess from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
const TRASH_TOOL_TIMEOUT_MS = 30_000;
|
|
9
|
+
function homeDir() {
|
|
10
|
+
return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir();
|
|
11
|
+
}
|
|
12
|
+
/** `[command, ...args]` trash tools tried in order on Linux; the target path is appended. */
|
|
13
|
+
const LINUX_TRASH_TOOLS = [["trash-put"], ["gio", "trash"]];
|
|
14
|
+
/**
|
|
15
|
+
* FreeDesktop trash without a helper tool: move `target` into
|
|
16
|
+
* `$XDG_DATA_HOME/Trash/files` and write the `.trashinfo` that lets a file
|
|
17
|
+
* manager restore it. A plain rename, so it fails (rather than copying) when
|
|
18
|
+
* the trash is on another filesystem.
|
|
19
|
+
*/
|
|
20
|
+
function moveToXdgTrash(target) {
|
|
21
|
+
const dataHome = process.env.XDG_DATA_HOME?.trim() || path.join(homeDir(), ".local", "share");
|
|
22
|
+
const filesDir = path.join(dataHome, "Trash", "files");
|
|
23
|
+
const infoDir = path.join(dataHome, "Trash", "info");
|
|
24
|
+
fs.mkdirSync(filesDir, { recursive: true });
|
|
25
|
+
fs.mkdirSync(infoDir, { recursive: true });
|
|
26
|
+
const base = path.basename(target);
|
|
27
|
+
let name = base;
|
|
28
|
+
for (let n = 1; fs.existsSync(path.join(filesDir, name)) || fs.existsSync(path.join(infoDir, `${name}.trashinfo`)); n++) {
|
|
29
|
+
name = `${base}.${n}`;
|
|
30
|
+
}
|
|
31
|
+
const deletedAt = new Date().toISOString().slice(0, 19);
|
|
32
|
+
fs.writeFileSync(path.join(infoDir, `${name}.trashinfo`), `[Trash Info]\nPath=${encodeURI(path.resolve(target))}\nDeletionDate=${deletedAt}\n`);
|
|
33
|
+
try {
|
|
34
|
+
fs.renameSync(target, path.join(filesDir, name));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
fs.unlinkSync(path.join(infoDir, `${name}.trashinfo`));
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Move `target` to the operating system's trash. Never deletes permanently:
|
|
43
|
+
* when no trash is available the target is left where it is and this throws.
|
|
44
|
+
* Linux tries `trash-put`, then `gio trash`, then the FreeDesktop trash
|
|
45
|
+
* directory; macOS moves into `~/.Trash`; Windows has no supported trash here.
|
|
46
|
+
*/
|
|
47
|
+
export function moveToTrash(target, tools = LINUX_TRASH_TOOLS) {
|
|
48
|
+
if (!fs.existsSync(target))
|
|
49
|
+
return;
|
|
50
|
+
if (process.platform === "win32") {
|
|
51
|
+
throw new Error("moving to the Recycle Bin is not supported on Windows; the directory was left in place");
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === "darwin") {
|
|
54
|
+
const trashDir = path.join(homeDir(), ".Trash");
|
|
55
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
56
|
+
fs.renameSync(target, path.join(trashDir, `${path.basename(target)}.${Date.now()}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const [command, ...args] of tools) {
|
|
60
|
+
const result = childProcess.spawnSync(command, [...args, target], {
|
|
61
|
+
// The live environment, as the harness commands get it: Bun's default is the env the process started with.
|
|
62
|
+
env: process.env,
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: "pipe",
|
|
65
|
+
timeout: TRASH_TOOL_TIMEOUT_MS,
|
|
66
|
+
});
|
|
67
|
+
if (!result.error && result.status === 0 && !fs.existsSync(target))
|
|
68
|
+
return;
|
|
69
|
+
// A tool that is missing falls through to the next; one that ran and
|
|
70
|
+
// failed does too, because the target is still in place.
|
|
71
|
+
}
|
|
72
|
+
moveToXdgTrash(target);
|
|
73
|
+
}
|
|
@@ -159,11 +159,12 @@ function matchDirectoryHint(dirName, ctx, specificity) {
|
|
|
159
159
|
* True when some ancestor directory already DECLARES this file's type via
|
|
160
160
|
* `DIR_TYPE_MAP` — the same walk `classifyByDirectory` performs. Derived from
|
|
161
161
|
* path fields alone, so `smartMdPathCandidates` can apply it without reading
|
|
162
|
-
* bytes.
|
|
162
|
+
* bytes. A skill's folder is a declared context too: a file in it is a skill
|
|
163
|
+
* resource, so a shell `$1` in one of its snippets must not retype it (#1063).
|
|
163
164
|
*/
|
|
164
165
|
function hasDeclaredDirType(ctx) {
|
|
165
166
|
if (isNestedSkillResource(ctx))
|
|
166
|
-
return
|
|
167
|
+
return true;
|
|
167
168
|
return ctx.ancestorDirs.some((dir) => matchDirectoryHint(dir, ctx, 0) !== null);
|
|
168
169
|
}
|
|
169
170
|
function classifyByExtension(ctx) {
|
|
@@ -662,7 +662,33 @@ export function formatUpdatePlain(r) {
|
|
|
662
662
|
}
|
|
663
663
|
return lines.length > 0 ? lines.join("\n") : `update: nothing to update`;
|
|
664
664
|
}
|
|
665
|
+
const HARNESS_LABELS = { "claude-code": "Claude Code", codex: "Codex", opencode: "OpenCode" };
|
|
666
|
+
/** The plugin lines worth printing: nothing for a plugin that is current or has no plugin to update. */
|
|
667
|
+
function formatUpgradePluginLines(r) {
|
|
668
|
+
const lines = [];
|
|
669
|
+
const lockstep = r.lockstep;
|
|
670
|
+
if (lockstep?.heldBack) {
|
|
671
|
+
lines.push(lockstep.pinnedVersion
|
|
672
|
+
? `akm is held at v${lockstep.pinnedVersion} (v${lockstep.newestVersion} is out): the OpenCode plugin pins that version`
|
|
673
|
+
: `akm is not upgraded to v${lockstep.newestVersion}: ${lockstep.reason}`);
|
|
674
|
+
}
|
|
675
|
+
const plugins = Array.isArray(r.plugins) ? r.plugins : [];
|
|
676
|
+
for (const p of plugins) {
|
|
677
|
+
if (p.outcome === "current" || p.outcome === "skipped")
|
|
678
|
+
continue;
|
|
679
|
+
const versions = p.from && p.to && p.from !== p.to ? ` v${p.from} → v${p.to}` : "";
|
|
680
|
+
const note = p.message ? ` (${p.message})` : "";
|
|
681
|
+
lines.push(`${HARNESS_LABELS[String(p.harness)] ?? p.harness} plugin ${p.outcome}${versions}${note}`);
|
|
682
|
+
}
|
|
683
|
+
return lines;
|
|
684
|
+
}
|
|
665
685
|
export function formatUpgradePlain(r) {
|
|
686
|
+
const lines = formatUpgradePluginLines(r);
|
|
687
|
+
const head = formatUpgradeHead(r);
|
|
688
|
+
const all = head === null ? lines : [head, ...lines];
|
|
689
|
+
return all.length > 0 ? all.join("\n") : null;
|
|
690
|
+
}
|
|
691
|
+
function formatUpgradeHead(r) {
|
|
666
692
|
if (r.upgraded === true) {
|
|
667
693
|
return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
|
|
668
694
|
}
|
|
@@ -30464,7 +30464,31 @@ function formatUpdatePlain(r) {
|
|
|
30464
30464
|
return lines.length > 0 ? lines.join(`
|
|
30465
30465
|
`) : `update: nothing to update`;
|
|
30466
30466
|
}
|
|
30467
|
+
var HARNESS_LABELS = { "claude-code": "Claude Code", codex: "Codex", opencode: "OpenCode" };
|
|
30468
|
+
function formatUpgradePluginLines(r) {
|
|
30469
|
+
const lines = [];
|
|
30470
|
+
const lockstep = r.lockstep;
|
|
30471
|
+
if (lockstep?.heldBack) {
|
|
30472
|
+
lines.push(lockstep.pinnedVersion ? `akm is held at v${lockstep.pinnedVersion} (v${lockstep.newestVersion} is out): the OpenCode plugin pins that version` : `akm is not upgraded to v${lockstep.newestVersion}: ${lockstep.reason}`);
|
|
30473
|
+
}
|
|
30474
|
+
const plugins = Array.isArray(r.plugins) ? r.plugins : [];
|
|
30475
|
+
for (const p of plugins) {
|
|
30476
|
+
if (p.outcome === "current" || p.outcome === "skipped")
|
|
30477
|
+
continue;
|
|
30478
|
+
const versions = p.from && p.to && p.from !== p.to ? ` v${p.from} → v${p.to}` : "";
|
|
30479
|
+
const note = p.message ? ` (${p.message})` : "";
|
|
30480
|
+
lines.push(`${HARNESS_LABELS[String(p.harness)] ?? p.harness} plugin ${p.outcome}${versions}${note}`);
|
|
30481
|
+
}
|
|
30482
|
+
return lines;
|
|
30483
|
+
}
|
|
30467
30484
|
function formatUpgradePlain(r) {
|
|
30485
|
+
const lines = formatUpgradePluginLines(r);
|
|
30486
|
+
const head = formatUpgradeHead(r);
|
|
30487
|
+
const all = head === null ? lines : [head, ...lines];
|
|
30488
|
+
return all.length > 0 ? all.join(`
|
|
30489
|
+
`) : null;
|
|
30490
|
+
}
|
|
30491
|
+
function formatUpgradeHead(r) {
|
|
30468
30492
|
if (r.upgraded === true) {
|
|
30469
30493
|
return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
|
|
30470
30494
|
}
|
|
@@ -40449,7 +40473,7 @@ function matchDirectoryHint(dirName, ctx, specificity) {
|
|
|
40449
40473
|
}
|
|
40450
40474
|
function hasDeclaredDirType(ctx) {
|
|
40451
40475
|
if (isNestedSkillResource(ctx))
|
|
40452
|
-
return
|
|
40476
|
+
return true;
|
|
40453
40477
|
return ctx.ancestorDirs.some((dir) => matchDirectoryHint(dir, ctx, 0) !== null);
|
|
40454
40478
|
}
|
|
40455
40479
|
function classifyByExtension(ctx) {
|
|
@@ -29792,7 +29792,31 @@ function formatUpdatePlain(r) {
|
|
|
29792
29792
|
return lines.length > 0 ? lines.join(`
|
|
29793
29793
|
`) : `update: nothing to update`;
|
|
29794
29794
|
}
|
|
29795
|
+
var HARNESS_LABELS = { "claude-code": "Claude Code", codex: "Codex", opencode: "OpenCode" };
|
|
29796
|
+
function formatUpgradePluginLines(r) {
|
|
29797
|
+
const lines = [];
|
|
29798
|
+
const lockstep = r.lockstep;
|
|
29799
|
+
if (lockstep?.heldBack) {
|
|
29800
|
+
lines.push(lockstep.pinnedVersion ? `akm is held at v${lockstep.pinnedVersion} (v${lockstep.newestVersion} is out): the OpenCode plugin pins that version` : `akm is not upgraded to v${lockstep.newestVersion}: ${lockstep.reason}`);
|
|
29801
|
+
}
|
|
29802
|
+
const plugins = Array.isArray(r.plugins) ? r.plugins : [];
|
|
29803
|
+
for (const p of plugins) {
|
|
29804
|
+
if (p.outcome === "current" || p.outcome === "skipped")
|
|
29805
|
+
continue;
|
|
29806
|
+
const versions = p.from && p.to && p.from !== p.to ? ` v${p.from} \u2192 v${p.to}` : "";
|
|
29807
|
+
const note = p.message ? ` (${p.message})` : "";
|
|
29808
|
+
lines.push(`${HARNESS_LABELS[String(p.harness)] ?? p.harness} plugin ${p.outcome}${versions}${note}`);
|
|
29809
|
+
}
|
|
29810
|
+
return lines;
|
|
29811
|
+
}
|
|
29795
29812
|
function formatUpgradePlain(r) {
|
|
29813
|
+
const lines = formatUpgradePluginLines(r);
|
|
29814
|
+
const head = formatUpgradeHead(r);
|
|
29815
|
+
const all = head === null ? lines : [head, ...lines];
|
|
29816
|
+
return all.length > 0 ? all.join(`
|
|
29817
|
+
`) : null;
|
|
29818
|
+
}
|
|
29819
|
+
function formatUpgradeHead(r) {
|
|
29796
29820
|
if (r.upgraded === true) {
|
|
29797
29821
|
return `akm upgraded: v${r.currentVersion} \u2192 v${r.newVersion}`;
|
|
29798
29822
|
}
|
|
@@ -39777,7 +39801,7 @@ function matchDirectoryHint(dirName, ctx, specificity) {
|
|
|
39777
39801
|
}
|
|
39778
39802
|
function hasDeclaredDirType(ctx) {
|
|
39779
39803
|
if (isNestedSkillResource(ctx))
|
|
39780
|
-
return
|
|
39804
|
+
return true;
|
|
39781
39805
|
return ctx.ancestorDirs.some((dir) => matchDirectoryHint(dir, ctx, 0) !== null);
|
|
39782
39806
|
}
|
|
39783
39807
|
function classifyByExtension(ctx) {
|
|
@@ -186,8 +186,13 @@ migration didn't finish is not done. This makes plain `akm upgrade` (no
|
|
|
186
186
|
flags) a safe, idempotent container entrypoint step on every boot: on an
|
|
187
187
|
already-current install with nothing pending it is a fast no-op that exits 0.
|
|
188
188
|
|
|
189
|
+
After the install step it also updates the akm plugin of each agent harness
|
|
190
|
+
present in the image (Claude Code, Codex, OpenCode), reported under `plugins`;
|
|
191
|
+
see [`akm upgrade`](../reference/cli.md#upgrade) for the rules, the OpenCode
|
|
192
|
+
version lockstep and the Codex hook-trust entries a headless image needs.
|
|
193
|
+
|
|
189
194
|
**`--check`** skips the migration step entirely — it only compares versions
|
|
190
|
-
and reports `updateAvailable
|
|
195
|
+
and reports `updateAvailable` (plus the pending per-harness plugin updates). Use it for a version-drift alert, not as your
|
|
191
196
|
boot check.
|
|
192
197
|
|
|
193
198
|
## `akm health`
|
package/docs/reference/cli.md
CHANGED
|
@@ -1336,20 +1336,96 @@ computed, with a 256 MiB binary limit. Release/checksum metadata is capped at
|
|
|
1336
1336
|
1 MiB; an oversized response is cancelled and the staged file is removed.
|
|
1337
1337
|
|
|
1338
1338
|
```sh
|
|
1339
|
-
akm upgrade # Install a newer release if there is one,
|
|
1340
|
-
akm upgrade --check #
|
|
1339
|
+
akm upgrade # Install a newer release if there is one, run every pending migration, then update the harness plugins
|
|
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
1342
|
```
|
|
1343
1343
|
|
|
1344
1344
|
| Flag | Description |
|
|
1345
1345
|
| --- | --- |
|
|
1346
|
-
| `--check` |
|
|
1346
|
+
| `--check` | Report pending updates (CLI and per-harness plugins) without changing anything |
|
|
1347
1347
|
| `--force` | Force upgrade even if on latest version |
|
|
1348
1348
|
| `--skip-post-upgrade` | Skip the post-upgrade index rebuild |
|
|
1349
1349
|
|
|
1350
1350
|
Offline, or to migrate without a release check, run `akm migrate apply`
|
|
1351
1351
|
directly: it is the same step.
|
|
1352
1352
|
|
|
1353
|
+
#### Harness plugins
|
|
1354
|
+
|
|
1355
|
+
After the CLI step, `akm upgrade` updates the akm plugin of each agent harness
|
|
1356
|
+
it finds. It only updates: a plugin that is not installed is never installed,
|
|
1357
|
+
and a harness without the akm plugin is skipped. Each external command runs
|
|
1358
|
+
with a timeout, and a failure is recorded on that harness's entry without
|
|
1359
|
+
stopping the CLI upgrade.
|
|
1360
|
+
|
|
1361
|
+
| Harness | Updated when | What runs |
|
|
1362
|
+
| --- | --- | --- |
|
|
1363
|
+
| Claude Code | `claude` is on `PATH`, the `akm-plugins` marketplace is configured and `akm@akm-plugins` is installed | `claude plugin marketplace update akm-plugins`, then `claude plugin update akm@akm-plugins` |
|
|
1364
|
+
| Codex | `codex` is on `PATH` and `akm@akm-plugins` is installed from the `akm-plugins` marketplace | `codex plugin marketplace upgrade akm-plugins` (this also refreshes the installed plugin cache) |
|
|
1365
|
+
| OpenCode | `akm-opencode` is cached at `$XDG_CACHE_HOME/opencode/packages/akm-opencode@latest` (default `~/.cache/opencode/...`) and `opencode` is on `PATH` | If npm's `akm-opencode@latest` differs from the cached version and no OpenCode process is running: the cache directory is moved to the trash (never deleted), then `opencode debug config` is run from a temporary directory to fetch the new version. While OpenCode runs the update is `deferred` to a later `akm upgrade`. |
|
|
1366
|
+
|
|
1367
|
+
Claude Code does not update third-party marketplaces on its own (the Claude
|
|
1368
|
+
desktop app turns plugin updates off), and OpenCode never re-checks a cached
|
|
1369
|
+
plugin, so neither moves without this step. Codex refreshes its git
|
|
1370
|
+
marketplaces at every start.
|
|
1371
|
+
|
|
1372
|
+
The result gains a `plugins` array, one entry per harness:
|
|
1373
|
+
`{ "harness": "claude-code" | "codex" | "opencode", "outcome": ..., "from"?, "to"?, "message"? }`.
|
|
1374
|
+
`outcome` is `updated`, `current`, `skipped`, `deferred` or `failed`; under
|
|
1375
|
+
`--check` it can also be `pending` or `unknown`. `--check` does not fetch the
|
|
1376
|
+
Claude Code or Codex marketplaces, so for them it reports `unknown` (whether a
|
|
1377
|
+
newer build exists needs a fetch it does not do) rather than guessing; for
|
|
1378
|
+
OpenCode it compares the cached version with npm, so it reports `pending` when
|
|
1379
|
+
an update is due. A `failed` plugin makes `akm upgrade` exit `1` (not
|
|
1380
|
+
`--check`). On an up-to-date machine the text output prints nothing for
|
|
1381
|
+
plugins.
|
|
1382
|
+
|
|
1383
|
+
**Version lockstep.** The OpenCode plugin runs its own exact-pinned akm-cli
|
|
1384
|
+
in-process, so a CLI newer than the plugin's pin would run the plugin against
|
|
1385
|
+
databases it did not migrate. When the OpenCode plugin is present, the CLI
|
|
1386
|
+
target is the akm-cli that `akm-opencode@latest` depends on
|
|
1387
|
+
(`npm view akm-opencode@latest dependencies.akm-cli`), not the newest release.
|
|
1388
|
+
The result then carries `lockstep: { plugin, pinnedVersion, newestVersion, heldBack }`,
|
|
1389
|
+
`latestVersion` is the pinned version, the install names that exact version
|
|
1390
|
+
rather than `@latest`, and the text output says the CLI is held back. The CLI
|
|
1391
|
+
is never moved backwards to meet the pin. Claude Code and Codex have no
|
|
1392
|
+
in-process copy and are not part of this rule.
|
|
1393
|
+
|
|
1394
|
+
Lockstep fails closed. When the OpenCode plugin is cached but the pin cannot
|
|
1395
|
+
be read (the npm lookup failed, or `akm-opencode@latest` declares no
|
|
1396
|
+
`akm-cli`), the CLI is not upgraded: `updateAvailable` is `false`,
|
|
1397
|
+
`latestVersion` is the current version, and `lockstep` is
|
|
1398
|
+
`{ plugin, pinnedVersion: null, newestVersion, heldBack: true, reason }`. The
|
|
1399
|
+
OpenCode entry is `failed` and the run exits `1`; the next `akm upgrade`
|
|
1400
|
+
retries.
|
|
1401
|
+
|
|
1402
|
+
#### Containers
|
|
1403
|
+
|
|
1404
|
+
With the plugin step, a container entrypoint needs only:
|
|
1405
|
+
|
|
1406
|
+
```sh
|
|
1407
|
+
akm upgrade -q || true
|
|
1408
|
+
( while sleep 86400; do akm upgrade -q; done ) & # long-running containers only
|
|
1409
|
+
exec "$@"
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
Codex hooks need one-time trust, and nobody can open `/hooks` in a container.
|
|
1413
|
+
Bake the trust entries into the image's `~/.codex/config.toml`, one per hook the
|
|
1414
|
+
plugin declares (today `session_start` and `user_prompt_submit`):
|
|
1415
|
+
|
|
1416
|
+
```toml
|
|
1417
|
+
[hooks.state."akm@akm-plugins:plugin.json#hooks[0]:session_start:0:0"]
|
|
1418
|
+
trusted_hash = "sha256:<hash>"
|
|
1419
|
+
|
|
1420
|
+
[hooks.state."akm@akm-plugins:plugin.json#hooks[0]:user_prompt_submit:0:0"]
|
|
1421
|
+
trusted_hash = "sha256:<hash>"
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
Copy the two `trusted_hash` values from a machine where you have trusted the
|
|
1425
|
+
hooks in `/hooks` (they are in that machine's `~/.codex/config.toml`). The
|
|
1426
|
+
hashes stay the same across plugin version bumps and change only when a hook's
|
|
1427
|
+
command changes, so an upgraded plugin keeps running without a prompt.
|
|
1428
|
+
|
|
1353
1429
|
Checksum verification is not optional and has no flag. If a release's
|
|
1354
1430
|
`checksums.txt` is genuinely unreachable, the recovery hatch is the
|
|
1355
1431
|
`AKM_UPGRADE_SKIP_CHECKSUM=1` environment variable (Internal — deliberately
|
|
@@ -196,7 +196,7 @@ the set of types the code actually emits at HEAD (verified against every
|
|
|
196
196
|
| `reflect_completed` | Reflect phase produced a proposal | `ref` |
|
|
197
197
|
| `improve_reflect_outcome` | Per-asset reflect result | `ref`, `ok`, `durationMs`, `reason` |
|
|
198
198
|
| `propose_invoked` | `akm proposal new` | `ref` |
|
|
199
|
-
| `distill_invoked` | Distill phase inside the `akm improve`/`akm proposal new` pipeline. **`akm distill` is not a CLI command** — there is no standalone verb by that name | `ref`, outcome (`queued`, `skipped` with a `skipReason` such as `lesson_exists`, `nothing_reusable` or `conflict_noop`, `llm_failed`, `validation_failed`, `quality_rejected`, `review_needed`) |
|
|
199
|
+
| `distill_invoked` | Distill phase inside the `akm improve`/`akm proposal new` pipeline. **`akm distill` is not a CLI command** — there is no standalone verb by that name | `ref`, outcome (`queued`, `skipped` with a `skipReason` such as `lesson_exists`, `nothing_reusable` or `conflict_noop`, `llm_failed`, `validation_failed`, `quality_rejected`, `review_needed`). A `quality_rejected` or `review_needed` event also carries `score`, `reason`, the judge's per-criterion `criteria` when it ran, and `rejectedContent`, the turned-away lesson text cut to 2000 characters (the judge prompt's cap), kept locally in `state.db` |
|
|
200
200
|
| `extract_invoked` | `akm proposal extract --type <harness>` / `--auto`, or improve-stage session extraction | `outcome`, `sessionId`, `harness` |
|
|
201
201
|
| `extract_triaged` | The pre-LLM extract triage gate evaluated at least one session | `evaluated`, `passed`, `triagedOut`, `sourceRun` (aggregated) |
|
|
202
202
|
| `schema_repair_invoked` | The schema-repair pass inside `akm improve` (`runSchemaRepairPass`) attempts to patch missing frontmatter on an asset that failed schema validation. **There is no `akm lint --repair` flag** — `lint` has `--fix`/`--auto-fix`, unrelated to this event | `ref`, outcome |
|
|
@@ -313,6 +313,7 @@ Contents:
|
|
|
313
313
|
- Source (which process generated it — e.g. `reflect`, `distill`)
|
|
314
314
|
- Full proposal content (Markdown text)
|
|
315
315
|
- Created/updated timestamps
|
|
316
|
+
- For a `consolidate-pair` retire proposal, the pair judge's verdict in its `retirement` metadata: label, reason, cosine, and the claims only the retired side holds (`onlyInRetired`) and only the kept side holds (`onlyInSuccessor`), each at most 20 entries of 200 characters
|
|
316
317
|
|
|
317
318
|
Beside it, the `improve_ledger` table records what each improve stage last did
|
|
318
319
|
with each asset — one row per bundle, asset ref and stage: the outcome
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.28-alpha.
|
|
3
|
+
"version": "0.9.28-alpha.4",
|
|
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": [
|