akm-cli 0.10.261015-beta → 0.10.261017-beta
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 +173 -0
- package/README.md +8 -10
- package/SECURITY.md +7 -99
- package/STABILITY.md +58 -98
- package/dist/assets/hints/cli-hints-full.md +6 -4
- package/dist/assets/hints/cli-hints-short.md +4 -2
- package/dist/assets/opencode/akm-plugin.js +16 -8
- package/dist/assets/prompts/workflow-unit-preamble.md +3 -1
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
- package/dist/cli.js +32 -41
- package/dist/commands/feedback-cli.js +5 -4
- package/dist/commands/health/plugin-staleness.js +86 -22
- package/dist/commands/health.js +15 -16
- package/dist/commands/proposal/proposal-cli.js +1 -1
- package/dist/commands/read/registry-search.js +4 -1
- package/dist/commands/read/search-cli.js +2 -2
- package/dist/commands/read/search.js +6 -6
- package/dist/commands/read/show.js +15 -3
- package/dist/commands/sources/self-update.js +9 -5
- package/dist/commands/tasks/tasks.js +12 -13
- package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
- package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/adapters/claude-adapter.js +1 -1
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
- package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
- package/dist/core/asset/markdown.js +9 -0
- package/dist/core/bundle-rename.js +17 -7
- package/dist/core/config/config-walker.js +35 -32
- package/dist/core/config/schema/connections.js +6 -3
- package/dist/core/config/schema/harnesses.js +8 -1
- package/dist/core/config/schema/primitives.js +12 -1
- package/dist/core/config/schema/sources-bundles.js +2 -2
- package/dist/core/npm-package.js +48 -13
- package/dist/indexer/bundle-identity-guard.js +6 -6
- package/dist/indexer/index-written-assets.js +3 -3
- package/dist/indexer/indexer.js +12 -8
- package/dist/indexer/materialize-embeddings.js +12 -2
- package/dist/indexer/passes/metadata.js +103 -46
- package/dist/indexer/search/ranking.js +7 -3
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/usage/usage-events.js +30 -8
- package/dist/indexer/walk/walker.js +1 -1
- package/dist/output/renderers.js +2 -2
- package/dist/output/shapes/helpers.js +5 -3
- package/dist/output/text/command-format.js +5 -6
- package/dist/output/text/show-format.js +7 -0
- package/dist/registry/semver.js +12 -0
- package/dist/scripts/akm-migrate-node.js +21653 -5734
- package/dist/scripts/akm-migrate.js +21661 -5742
- package/dist/sources/providers/git-provider.js +2 -4
- package/dist/sources/providers/provider-utils.js +8 -13
- package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
- package/dist/storage/repositories/events-repository.js +15 -0
- package/dist/storage/repositories/improve-ledger-repository.js +13 -0
- package/dist/storage/repositories/index-entries-repository.js +43 -5
- package/dist/storage/repositories/outcome-repository.js +12 -0
- package/dist/storage/repositories/proposals-repository.js +5 -5
- package/dist/storage/repositories/salience-repository.js +13 -0
- package/dist/storage/repositories/task-history-repository.js +8 -7
- package/dist/storage/repositories/workflow-runs-repository.js +10 -8
- package/dist/tasks/backends/cron.js +19 -0
- package/docs/README.md +24 -60
- package/docs/agents/README.md +3 -8
- package/docs/architecture/README.md +18 -20
- package/docs/architecture/adapters.md +40 -43
- package/docs/architecture/architecture.md +171 -151
- package/docs/architecture/improvement.md +95 -91
- package/docs/architecture/internals/classification.md +101 -119
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
- package/docs/architecture/internals/health-advisories.md +59 -46
- package/docs/architecture/internals/improve-workflow.md +166 -693
- package/docs/architecture/internals/indexing.md +65 -465
- package/docs/architecture/internals/storage-locations.md +301 -848
- package/docs/architecture/persisted-data-compat.md +6 -6
- package/docs/architecture/specs/okf-support.md +67 -95
- package/docs/architecture/specs/ref.md +67 -124
- package/docs/architecture/testing/testing-workflow.md +114 -175
- package/docs/architecture/workflow-engine.md +34 -33
- package/docs/guides/README.md +5 -5
- package/docs/guides/author-bundles.md +72 -98
- package/docs/guides/author-workflows.md +11 -18
- package/docs/guides/bundles.md +91 -123
- package/docs/guides/capture-knowledge.md +10 -9
- package/docs/guides/concepts.md +45 -30
- package/docs/guides/discover-and-load.md +51 -36
- package/docs/guides/getting-started.md +27 -26
- package/docs/guides/improve-the-library.md +98 -80
- package/docs/guides/recipes/headless-install.md +4 -4
- package/docs/guides/recipes/website-source.md +9 -5
- package/docs/guides/run-workflows.md +10 -11
- package/docs/guides/scheduling.md +11 -17
- package/docs/guides/use-with-any-agent.md +15 -11
- package/docs/guides/wikis.md +6 -9
- package/docs/integration/bundling-akm.md +21 -26
- package/docs/maintainers/README.md +2 -2
- package/docs/maintainers/local-development.md +3 -16
- package/docs/maintainers/release-checklist.md +15 -8
- package/docs/migration/README.md +2 -1
- package/docs/migration/release-notes/0.10.0.md +167 -22
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.9-to-v0.10.md +47 -7
- package/docs/product-surface.md +10 -8
- package/docs/reference/README.md +26 -15
- package/docs/reference/asset-types.md +11 -12
- package/docs/reference/bundle-types.md +6 -5
- package/docs/reference/cli.md +74 -31
- package/docs/reference/configuration.md +46 -60
- package/docs/reference/data-and-telemetry.md +204 -421
- package/docs/reference/env-and-secrets.md +80 -83
- package/docs/reference/memory.md +1 -1
- package/docs/reference/refs.md +23 -36
- package/docs/reference/registry.md +138 -348
- package/docs/reference/supported-formats.md +8 -8
- package/docs/reference/tasks.md +13 -19
- package/docs/reference/website-sources.md +23 -15
- package/docs/reference/workflow-schema.md +28 -47
- package/docs/reference/workflows.md +33 -37
- package/package.json +4 -4
- package/schemas/akm-config.json +535 -302
package/dist/cli.js
CHANGED
|
@@ -112,9 +112,6 @@ function applyEarlyStderrFlags(argv) {
|
|
|
112
112
|
setVerbose(true);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
// resolveHelpMigrateVersionArg moved to ./cli/invocation (chunk-9 WI-9.9
|
|
116
|
-
// argv-normalization fold — it re-scanned process.argv, same as
|
|
117
|
-
// findCittyTopLevelCommand and parseAllFlagValues below).
|
|
118
115
|
/**
|
|
119
116
|
* Stderr-only human-friendly hint after a non-interactive `setup` invocation.
|
|
120
117
|
* Default --format is `json`, so a CI or piped consumer sees only the JSON on
|
|
@@ -155,10 +152,9 @@ const setupCommand = defineCommand({
|
|
|
155
152
|
description: "Interactive configuration wizard. Enables agent programs (Claude Code, Codex, OpenCode, ...), sets up a model connection and embeddings, and picks bundle sources and registries. Flags answer individual questions; --yes takes the detected default for the rest, for scripting.",
|
|
156
153
|
},
|
|
157
154
|
args: {
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// this in is a `--help` visibility fix, not a behavior change.
|
|
155
|
+
// `setup` is a raw `defineCommand` (not `defineJsonCommand`), so it does
|
|
156
|
+
// not get `GLOBAL_OUTPUT_ARGS` for free. Three of its four run branches
|
|
157
|
+
// call `output()`; spreading this in is a `--help` visibility fix.
|
|
162
158
|
...GLOBAL_OUTPUT_ARGS,
|
|
163
159
|
endpoint: {
|
|
164
160
|
type: "string",
|
|
@@ -285,11 +281,11 @@ const healthCommand = defineCommand({
|
|
|
285
281
|
description: "Check akm runtime health, artifacts, and improve metrics",
|
|
286
282
|
},
|
|
287
283
|
args: {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// `--output`
|
|
284
|
+
// `health` is a raw `defineCommand` (not `defineJsonCommand`), so it does
|
|
285
|
+
// not get `GLOBAL_OUTPUT_ARGS` for free. `--format`/`--detail`/
|
|
286
|
+
// `--output` parse correctly here regardless (this command has
|
|
291
287
|
// no positional for a stray value to fall into), so this is purely a
|
|
292
|
-
// `--help` visibility / consistency
|
|
288
|
+
// `--help` visibility / consistency measure.
|
|
293
289
|
...GLOBAL_OUTPUT_ARGS,
|
|
294
290
|
since: {
|
|
295
291
|
type: "string",
|
|
@@ -389,8 +385,8 @@ const healthCommand = defineCommand({
|
|
|
389
385
|
}
|
|
390
386
|
output("health", base);
|
|
391
387
|
});
|
|
392
|
-
//
|
|
393
|
-
//
|
|
388
|
+
// `emitJsonError` (src/cli/shared.ts) does not force-exit on the error
|
|
389
|
+
// path — it sets `process.exitCode` and returns, so a `--report`
|
|
394
390
|
// failure thrown AFTER `resultStatus` was already assigned (e.g. the
|
|
395
391
|
// proposal-queue read above) would otherwise leave `resultStatus`
|
|
396
392
|
// populated here too. Skip the status-derived exit entirely once
|
|
@@ -443,14 +439,12 @@ const completionsCommand = defineCommand({
|
|
|
443
439
|
},
|
|
444
440
|
},
|
|
445
441
|
run({ args }) {
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
// same error-classification wrapper other bare `defineCommand`s in this
|
|
453
|
-
// file use, e.g. `help migrate` below).
|
|
442
|
+
// An unsupported `--shell` value must reach the standard JSON envelope as
|
|
443
|
+
// the classified exit-2 usage error, not escape to citty's top-level error
|
|
444
|
+
// handling as a raw stack trace (`completions` is format-exempt, so it
|
|
445
|
+
// stays a raw `defineCommand` rather than `defineJsonCommand`, but still
|
|
446
|
+
// needs the same error-classification wrapper other bare `defineCommand`s
|
|
447
|
+
// in this file use, e.g. `help migrate` below).
|
|
454
448
|
return runWithJsonErrors(() => {
|
|
455
449
|
if (args.shell !== "bash") {
|
|
456
450
|
throw new UsageError(`Unsupported shell: ${args.shell}. Only bash is supported.`);
|
|
@@ -678,7 +672,7 @@ const EXIT_GENERAL = EXIT_CODES.GENERAL;
|
|
|
678
672
|
const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
|
|
679
673
|
// ── Top-level driver (replaces citty's `runMain`) ───────────────────────────
|
|
680
674
|
//
|
|
681
|
-
//
|
|
675
|
+
// citty's own `runMain` catches EVERY error escaping `runCommand` —
|
|
682
676
|
// including its unexported `CLIError`, thrown for "Unknown command …", "No
|
|
683
677
|
// command specified.", "Missing required argument/positional …", and invalid
|
|
684
678
|
// enum values — and unconditionally calls `process.exit(1)`, regardless of
|
|
@@ -692,7 +686,7 @@ const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
|
|
|
692
686
|
// of `runMain`, replicating `runMain`'s `--help` / `--version`
|
|
693
687
|
// short-circuits and its CLIError → usage-banner rendering, but classifying
|
|
694
688
|
// a CLIError as USAGE (2) instead of GENERAL (1). Every other error escaping
|
|
695
|
-
// this boundary keeps the
|
|
689
|
+
// this boundary keeps the GENERAL (1) mapping — this only
|
|
696
690
|
// reclassifies the one error family citty itself throws before any of our
|
|
697
691
|
// own command bodies (and their `runWithJsonErrors` / `emitJsonError`
|
|
698
692
|
// classification) ever run.
|
|
@@ -762,7 +756,7 @@ function resolveCittyCommandPath(cmd, rawArgs, path = []) {
|
|
|
762
756
|
* dropping the `akm ` root prefix every top-level command's `--help` already
|
|
763
757
|
* shows. Building a synthetic parent whose `meta.name` is the full prefix
|
|
764
758
|
* (`akm task`) fixes it for any depth without patching the vendored
|
|
765
|
-
* dependency
|
|
759
|
+
* dependency.
|
|
766
760
|
*/
|
|
767
761
|
function buildUsageParentForPath(path) {
|
|
768
762
|
// `version` carries through too — citty's renderUsage falls back to
|
|
@@ -776,7 +770,7 @@ function buildUsageParentForPath(path) {
|
|
|
776
770
|
},
|
|
777
771
|
};
|
|
778
772
|
}
|
|
779
|
-
// ── Sectioned root help
|
|
773
|
+
// ── Sectioned root help ────────────────────────────────────────────────
|
|
780
774
|
//
|
|
781
775
|
// citty's own generated COMMANDS list is a flat, unordered dump of every
|
|
782
776
|
// top-level command — fine for a handful of commands, not for the ~28 this
|
|
@@ -853,7 +847,7 @@ function renderCommandSections() {
|
|
|
853
847
|
return HELP_SECTIONS.map(({ title, commands }) => `${title}\n${formatCommandRows(commands)}`).join("\n\n");
|
|
854
848
|
}
|
|
855
849
|
/**
|
|
856
|
-
* Sectioned replacement for citty's root COMMANDS list
|
|
850
|
+
* Sectioned replacement for citty's root COMMANDS list. Reuses
|
|
857
851
|
* `renderUsage(main)` for the banner + exit-code table (kept verbatim) +
|
|
858
852
|
* USAGE line + global OPTIONS, then cuts the string before citty's own
|
|
859
853
|
* "COMMANDS" heading (present because `main` has `subCommands`) and appends
|
|
@@ -938,7 +932,7 @@ function cittyCliErrorUsageCode(error) {
|
|
|
938
932
|
}
|
|
939
933
|
/**
|
|
940
934
|
* Route a citty `CLIError` through the standard JSON envelope instead of
|
|
941
|
-
* citty's own usage-banner + raw `console.error(message)
|
|
935
|
+
* citty's own usage-banner + raw `console.error(message)`: the
|
|
942
936
|
* one-line diagnosis is the error's own message (e.g. "Unknown command
|
|
943
937
|
* foo"), and the hint is a short pointer — with a did-you-mean suggestion
|
|
944
938
|
* prepended when the unresolved token is close to a real sibling command —
|
|
@@ -962,12 +956,10 @@ export function normalizeCittyCliError(error, rawArgs) {
|
|
|
962
956
|
}
|
|
963
957
|
/**
|
|
964
958
|
* The CLI's real startup sequence, extracted into a function so error paths
|
|
965
|
-
* can `return` early — top-level `return` is a syntax error in an ES module
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
* and returns, every direct call site here needs its own explicit `return;`
|
|
970
|
-
* to stop the rest of startup from running after a fatal early error.
|
|
959
|
+
* can `return` early — top-level `return` is a syntax error in an ES module.
|
|
960
|
+
* `emitJsonError` (src/cli/shared.ts) only records `process.exitCode` and
|
|
961
|
+
* returns, so every direct call site here needs its own explicit `return;` to
|
|
962
|
+
* stop the rest of startup from running after a fatal early error.
|
|
971
963
|
*/
|
|
972
964
|
export async function runCli() {
|
|
973
965
|
try {
|
|
@@ -978,7 +970,7 @@ export async function runCli() {
|
|
|
978
970
|
return;
|
|
979
971
|
}
|
|
980
972
|
// Mint the ParsedInvocation singleton from the (normalized) argv — the ONE
|
|
981
|
-
// place argv is parsed for the whole process
|
|
973
|
+
// place argv is parsed for the whole process.
|
|
982
974
|
// Every out-of-cli.ts command module reads argv state through
|
|
983
975
|
// `getParsedInvocation()` from here on instead of re-scanning process.argv.
|
|
984
976
|
setParsedInvocation(process.argv);
|
|
@@ -1024,11 +1016,10 @@ export async function runCli() {
|
|
|
1024
1016
|
return;
|
|
1025
1017
|
}
|
|
1026
1018
|
const commandPath = resolveCittyCommandPath(main, process.argv.slice(2));
|
|
1027
|
-
//
|
|
1019
|
+
// Every command that renders through output() honours all six --format
|
|
1028
1020
|
// values. The declared exempt set (src/output/format-exempt.ts) does not
|
|
1029
1021
|
// render an envelope at all, so warn rather than pretend: silently ignoring
|
|
1030
|
-
// the flag
|
|
1031
|
-
// warning, not an error, because the flag is harmless here and scripts that
|
|
1022
|
+
// the flag makes the behaviour hard to discover. A warning, not an error, because the flag is harmless here and scripts that
|
|
1032
1023
|
// pass --format globally to a mixed batch of commands should still work.
|
|
1033
1024
|
const invocation = getParsedInvocation();
|
|
1034
1025
|
if ((invocation.hasFlag("--format") || invocation.getFlagValue("--format") !== undefined) &&
|
|
@@ -1082,7 +1073,7 @@ export async function runCli() {
|
|
|
1082
1073
|
if (HELP_FLAGS.some((flag) => ownArgs.includes(flag))) {
|
|
1083
1074
|
const resolved = resolveDeepestCittyCommand(main, rawArgs);
|
|
1084
1075
|
if (resolved === main) {
|
|
1085
|
-
// Root `--help
|
|
1076
|
+
// Root `--help`: the sectioned overview, not citty's
|
|
1086
1077
|
// flat COMMANDS dump.
|
|
1087
1078
|
process.stdout.write(`${await renderSectionedRootHelp()}\n`);
|
|
1088
1079
|
}
|
|
@@ -1097,14 +1088,14 @@ export async function runCli() {
|
|
|
1097
1088
|
return;
|
|
1098
1089
|
}
|
|
1099
1090
|
// citty's parseArgs (strict: false) silently ignores undeclared flags, so
|
|
1100
|
-
// a typo
|
|
1101
|
-
// --version, so those keep working on any command.
|
|
1091
|
+
// a typo would otherwise run the command with defaults and exit 0. Checked
|
|
1092
|
+
// here, after --help and --version, so those keep working on any command.
|
|
1102
1093
|
assertKnownFlags(main, rawArgs);
|
|
1103
1094
|
await runCommand(main, { rawArgs });
|
|
1104
1095
|
}
|
|
1105
1096
|
catch (error) {
|
|
1106
1097
|
if (isCittyCliError(error)) {
|
|
1107
|
-
//
|
|
1098
|
+
// Reclassify citty's own "unknown command" / "no command
|
|
1108
1099
|
// specified" / "missing required argument" family as USAGE (2) and
|
|
1109
1100
|
// route it through the SAME JSON envelope every other command's
|
|
1110
1101
|
// failure uses (`emitJsonError`), rather than citty's own usage-banner
|
|
@@ -435,10 +435,11 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
435
435
|
"next search; no reindex is needed. Each recorded positive raises the asset's\n" +
|
|
436
436
|
"score and each negative lowers it, the same way for every asset: the more\n" +
|
|
437
437
|
"positives, the higher it ranks (5 outrank 2), and each further vote counts\n" +
|
|
438
|
-
"for a little less. The effect is small
|
|
439
|
-
"
|
|
440
|
-
"feedback
|
|
441
|
-
"score that improve and
|
|
438
|
+
"for a little less. The effect is small: in keyword-only search one vote\n" +
|
|
439
|
+
"moves a hit one place, and with embeddings it breaks only near-ties. Only\n" +
|
|
440
|
+
"feedback recorded as the user counts, not feedback from improve, task or\n" +
|
|
441
|
+
"audit runs. Feedback also moves the utility score that improve and\n" +
|
|
442
|
+
"`akm metrics` read.",
|
|
442
443
|
},
|
|
443
444
|
args: {
|
|
444
445
|
// Optional in citty so run() is invoked even when omitted; we re-validate
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* local marketplace clone is not proof of what is newest upstream — the
|
|
36
36
|
* incident above involved a clone that hadn't seen the fix's tag at all — so
|
|
37
37
|
* the only way to ever detect drift is to ask the remote what tags exist.
|
|
38
|
-
* That query is a `git ls-remote --
|
|
38
|
+
* That query is a `git ls-remote --symref` (lists refs; fetches nothing,
|
|
39
39
|
* writes nothing) with a short timeout, and any failure (offline, no
|
|
40
40
|
* remote, timeout) degrades to "installed version reported, no staleness
|
|
41
41
|
* claim" rather than a false positive or a hang. Like `cli-version`, this
|
|
@@ -54,7 +54,8 @@ import os from "node:os";
|
|
|
54
54
|
import path from "node:path";
|
|
55
55
|
import { OPENCODE_V1, OPENCODE_V2, readPackageVersion, } from "../../integrations/harnesses/opencode/plugin-layout.js";
|
|
56
56
|
import { opencodeCacheDir } from "../../integrations/harnesses/opencode/roots.js";
|
|
57
|
-
import { isExactSemver,
|
|
57
|
+
import { highestVersion, isExactSemver, isPrerelease, isSemverRange, satisfiesRange } from "../../registry/semver.js";
|
|
58
|
+
import { openCodeConfigSpec } from "../sources/plugin-upgrade.js";
|
|
58
59
|
/**
|
|
59
60
|
* Root directory holding Claude Code's plugin cache + marketplace clones.
|
|
60
61
|
* Resolved per call (not memoized) so `AKM_CLAUDE_PLUGINS_DIR` can be set
|
|
@@ -78,12 +79,26 @@ function defaultOpencodeCacheRoot() {
|
|
|
78
79
|
}
|
|
79
80
|
const OPENCODE_PLUGIN_LOCATIONS = [
|
|
80
81
|
{
|
|
81
|
-
// OpenCode 1: <cache>/packages
|
|
82
|
+
// OpenCode 1: <cache>/packages/<spec>/node_modules/akm-cli, the one spec the global config names (#1265).
|
|
82
83
|
flavor: OPENCODE_V1,
|
|
83
84
|
advisory: "opencode-plugin-version",
|
|
84
85
|
label: "OpenCode's",
|
|
85
86
|
remedy: "update the OpenCode plugin",
|
|
86
|
-
folders: (cacheRoot) =>
|
|
87
|
+
folders: (cacheRoot) => {
|
|
88
|
+
// OpenCode caches each spec in its own folder, and a bare name is its `@latest`, as `akm upgrade` reads it. With
|
|
89
|
+
// no config entry, or no folder for the spec, the bare `packages/akm-opencode` is read, as this check always has.
|
|
90
|
+
const { pkg } = OPENCODE_V1;
|
|
91
|
+
const bare = { folder: path.join(cacheRoot, OPENCODE_V1.cacheFolder(pkg)) };
|
|
92
|
+
const named = openCodeConfigSpec(OPENCODE_V1)?.spec;
|
|
93
|
+
if (named === undefined)
|
|
94
|
+
return [bare];
|
|
95
|
+
const spec = named === pkg ? `${pkg}@latest` : named;
|
|
96
|
+
const folder = path.join(cacheRoot, OPENCODE_V1.cacheFolder(spec));
|
|
97
|
+
if (!fs.existsSync(folder))
|
|
98
|
+
return [bare];
|
|
99
|
+
const version = spec.slice(pkg.length + 1);
|
|
100
|
+
return [{ spec, folder, ...(isExactSemver(version) ? { pinned: version } : {}) }];
|
|
101
|
+
},
|
|
87
102
|
},
|
|
88
103
|
{
|
|
89
104
|
// OpenCode 2: <cache>/npm/akm-opencode-v2@<spec>/<epoch-ms>/node_modules/akm-cli, newest build per spec.
|
|
@@ -110,11 +125,11 @@ const OPENCODE_PLUGIN_LOCATIONS = [
|
|
|
110
125
|
*/
|
|
111
126
|
function findBundledAkm(plugin, cacheRoot) {
|
|
112
127
|
const found = [];
|
|
113
|
-
for (const { spec, folder } of plugin.folders(cacheRoot)) {
|
|
128
|
+
for (const { spec, pinned, folder } of plugin.folders(cacheRoot)) {
|
|
114
129
|
const nodeModules = plugin.flavor.nodeModulesDir(folder);
|
|
115
130
|
const version = nodeModules && readPackageVersion(path.join(nodeModules, "akm-cli", "package.json"));
|
|
116
131
|
if (version !== undefined)
|
|
117
|
-
found.push(spec ? { spec,
|
|
132
|
+
found.push({ ...(spec ? { spec } : {}), ...(pinned ? { pinned } : {}), version });
|
|
118
133
|
}
|
|
119
134
|
return found;
|
|
120
135
|
}
|
|
@@ -122,6 +137,17 @@ function buildOpencodeBundledAdvisory(plugin, bundled, cliVersion) {
|
|
|
122
137
|
const bundledVersion = bundled.version;
|
|
123
138
|
const stale = bundledVersion !== cliVersion;
|
|
124
139
|
const where = bundled.spec ? ` (${bundled.spec})` : "";
|
|
140
|
+
if (bundled.pinned) {
|
|
141
|
+
// The config chose this build: there is nothing to update it to, so no staleness verdict.
|
|
142
|
+
return {
|
|
143
|
+
name: plugin.advisory,
|
|
144
|
+
kind: "deterministic",
|
|
145
|
+
status: "pass",
|
|
146
|
+
confidence: "high",
|
|
147
|
+
message: `${plugin.label} plugin is pinned to v${bundled.pinned}${where}; its bundled akm-cli is v${bundledVersion}, and a pinned plugin is not compared with the running CLI v${cliVersion}.`,
|
|
148
|
+
evidence: { bundledVersion, cliVersion, spec: bundled.spec, pinnedVersion: bundled.pinned },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
125
151
|
return {
|
|
126
152
|
name: plugin.advisory,
|
|
127
153
|
kind: "deterministic",
|
|
@@ -161,7 +187,7 @@ function detectInstalledPlugins(pluginsRoot) {
|
|
|
161
187
|
}
|
|
162
188
|
if (versions.length === 0)
|
|
163
189
|
continue;
|
|
164
|
-
const latest =
|
|
190
|
+
const latest = highestVersion(versions);
|
|
165
191
|
if (!latest)
|
|
166
192
|
continue;
|
|
167
193
|
const versionDir = path.join(pluginDir, latest);
|
|
@@ -204,16 +230,31 @@ function readVersionRange(pluginDir) {
|
|
|
204
230
|
}
|
|
205
231
|
const LS_REMOTE_TIMEOUT_MS = 5_000;
|
|
206
232
|
/**
|
|
207
|
-
* `git ls-remote --
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
233
|
+
* `git ls-remote --symref origin` against the marketplace clone's configured remote (lists refs only, fetches no
|
|
234
|
+
* objects, writes no local refs), read for the channel the clone follows:
|
|
235
|
+
*
|
|
236
|
+
* - **The remote's default branch** (stable): every stable release tag; the newest is the highest of them.
|
|
237
|
+
* - **Any other branch** (a channel such as `next`): the release tags on that branch's head. A channel branch is
|
|
238
|
+
* reset to each build it publishes, so its head IS the newest build of the channel, whatever semver order says
|
|
239
|
+
* about other tags (retired build schemes can sort above current builds).
|
|
240
|
+
* - **No branch** (a marketplace added at a tag ref, which Claude Code clones detached): the release tag the clone
|
|
241
|
+
* is pinned to. A pinned marketplace never offers anything newer, so `claude plugin update` could not act on a
|
|
242
|
+
* newer tag elsewhere. A detached clone at an untagged commit reads like the default branch.
|
|
243
|
+
*
|
|
244
|
+
* Returns `undefined` (never throws) when the directory is not a git checkout, has no `origin` remote, or the
|
|
245
|
+
* command fails/times out (e.g. offline): all "cannot determine availability", not "up to date". A channel head
|
|
246
|
+
* with no release tag returns `[]`, which reads the same way.
|
|
212
247
|
*/
|
|
213
248
|
const realListRemoteTags = (marketplaceDir) => {
|
|
214
249
|
let result;
|
|
250
|
+
let branch;
|
|
215
251
|
try {
|
|
216
|
-
|
|
252
|
+
branch =
|
|
253
|
+
spawnSync("git", ["-C", marketplaceDir, "branch", "--show-current"], {
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
timeout: LS_REMOTE_TIMEOUT_MS,
|
|
256
|
+
}).stdout?.trim() ?? "";
|
|
257
|
+
result = spawnSync("git", ["-C", marketplaceDir, "ls-remote", "--symref", "origin"], {
|
|
217
258
|
encoding: "utf8",
|
|
218
259
|
timeout: LS_REMOTE_TIMEOUT_MS,
|
|
219
260
|
});
|
|
@@ -223,16 +264,39 @@ const realListRemoteTags = (marketplaceDir) => {
|
|
|
223
264
|
}
|
|
224
265
|
if (result.status !== 0 || !result.stdout)
|
|
225
266
|
return undefined;
|
|
226
|
-
const
|
|
267
|
+
const refs = result.stdout
|
|
227
268
|
.split("\n")
|
|
228
269
|
.map((line) => line.trim())
|
|
229
270
|
.filter(Boolean)
|
|
230
|
-
.map((line) => line.split("\t")
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
.map((line) => line.split("\t"));
|
|
272
|
+
const defaultBranch = refs
|
|
273
|
+
.find(([value, name]) => name === "HEAD" && value?.startsWith("ref: refs/heads/"))?.[0]
|
|
274
|
+
?.replace(/^ref: refs\/heads\//, "");
|
|
275
|
+
const tags = refs
|
|
276
|
+
.filter(([, name]) => typeof name === "string" && name.startsWith("refs/tags/"))
|
|
277
|
+
.map(([sha, name]) => ({
|
|
278
|
+
sha: sha,
|
|
279
|
+
version: name
|
|
280
|
+
.replace(/^refs\/tags\//, "")
|
|
281
|
+
.replace(/\^\{\}$/, "")
|
|
282
|
+
.replace(/^v/, ""),
|
|
283
|
+
}))
|
|
284
|
+
.filter((tag) => isExactSemver(tag.version));
|
|
285
|
+
const tagsAt = (sha) => [
|
|
286
|
+
...new Set(tags.filter((tag) => tag.sha === sha).map((tag) => tag.version)),
|
|
287
|
+
];
|
|
288
|
+
const onChannelBranch = branch !== "" && defaultBranch !== undefined && branch !== defaultBranch;
|
|
289
|
+
if (onChannelBranch)
|
|
290
|
+
return tagsAt(refs.find(([, name]) => name === `refs/heads/${branch}`)?.[0]);
|
|
291
|
+
if (branch === "") {
|
|
292
|
+
const pinned = tagsAt(spawnSync("git", ["-C", marketplaceDir, "rev-parse", "HEAD"], {
|
|
293
|
+
encoding: "utf8",
|
|
294
|
+
timeout: LS_REMOTE_TIMEOUT_MS,
|
|
295
|
+
}).stdout?.trim());
|
|
296
|
+
if (pinned.length > 0)
|
|
297
|
+
return pinned;
|
|
298
|
+
}
|
|
299
|
+
return [...new Set(tags.map((tag) => tag.version).filter((version) => !isPrerelease(version)))];
|
|
236
300
|
};
|
|
237
301
|
/**
|
|
238
302
|
* Build one `plugin-version` advisory per detected `akm` harness plugin,
|
|
@@ -263,14 +327,14 @@ function buildAdvisory(plugin, cliVersion, pluginsRoot, listRemoteTags, probe) {
|
|
|
263
327
|
let availableVersion;
|
|
264
328
|
try {
|
|
265
329
|
const tags = probe && fs.existsSync(marketplaceDir) ? listRemoteTags(marketplaceDir) : undefined;
|
|
266
|
-
availableVersion = tags && tags.length > 0 ?
|
|
330
|
+
availableVersion = tags && tags.length > 0 ? highestVersion(tags) : undefined;
|
|
267
331
|
}
|
|
268
332
|
catch {
|
|
269
333
|
availableVersion = undefined;
|
|
270
334
|
}
|
|
271
335
|
const stale = availableVersion !== undefined &&
|
|
272
336
|
plugin.version !== availableVersion &&
|
|
273
|
-
|
|
337
|
+
highestVersion([plugin.version, availableVersion]) === availableVersion;
|
|
274
338
|
// Point 3: does the plugin's own declared range admit the running CLI?
|
|
275
339
|
const versionRange = readVersionRange(plugin.pluginDir);
|
|
276
340
|
const rangeKnown = versionRange !== undefined && isSemverRange(versionRange);
|
package/dist/commands/health.js
CHANGED
|
@@ -72,21 +72,21 @@ function validateAkmHealthOptions(options) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
// ── akmHealth phase helpers (
|
|
76
|
-
//
|
|
75
|
+
// ── akmHealth phase helpers (the function's natural
|
|
76
|
+
// gather/advise/check/assemble phases) ──────────────────────────────────────
|
|
77
77
|
/** Event type appended + read back by the state.db round-trip probe. */
|
|
78
78
|
const HEALTH_PROBE_EVENT = "health_probe";
|
|
79
|
-
/** Synthetic sentinel ref
|
|
79
|
+
/** Synthetic sentinel ref: a colon-free
|
|
80
80
|
* `<subsystem>/_<marker>` label. `health` has no asset stash-subdir, so
|
|
81
81
|
* `health/_probe` names the subsystem. */
|
|
82
82
|
const HEALTH_PROBE_REF = "health/_probe";
|
|
83
83
|
/**
|
|
84
84
|
* Verify state.db can accept a write and read it back — WITHOUT leaving any
|
|
85
|
-
* permanent trace
|
|
86
|
-
*
|
|
87
|
-
* check
|
|
88
|
-
*
|
|
89
|
-
*
|
|
85
|
+
* permanent trace. Leaving the `health_probe` event in place after every
|
|
86
|
+
* `akm health` invocation would grow state.db without bound for a read-only
|
|
87
|
+
* health check run on a cron (the only purge is `improve`'s retention pass,
|
|
88
|
+
* which a health-only user never runs). The probe row inserted here is
|
|
89
|
+
* deleted again inside the SAME connection once the
|
|
90
90
|
* round trip is confirmed, so the net effect on the `events` table is always
|
|
91
91
|
* zero rows — the round trip still genuinely exercises append + read against
|
|
92
92
|
* the real table, it just doesn't accumulate.
|
|
@@ -132,7 +132,7 @@ export function probeStateDbRoundTrip(stateDbPath) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
135
|
+
* Dedupe stuck-active rows by task_id (keeping the oldest/largest age
|
|
136
136
|
* per id) so the `active-runs` check can name WHICH tasks are stuck instead of
|
|
137
137
|
* just a count. No pid/liveness probing — purely a projection of the rows
|
|
138
138
|
* already read.
|
|
@@ -148,7 +148,7 @@ function dedupeStuckActiveTasks(rows, now) {
|
|
|
148
148
|
return [...byTask.entries()].map(([taskId, ageMs]) => ({ taskId, ageMs }));
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
151
|
+
* Group task_history rows by task_id and return the one with the
|
|
152
152
|
* highest fail rate among tasks with at least MIN_ROWS_FOR_WORST_TASK_FAIL_RATE
|
|
153
153
|
* rows in the window — surfaces a consistently-failing task that a large,
|
|
154
154
|
* mostly-healthy population would otherwise hide behind the aggregate rate.
|
|
@@ -184,7 +184,7 @@ function gatherTaskHistoryPhase(db, since, stateDbPath, now, readOnly) {
|
|
|
184
184
|
const requiredTables = ["events", "proposals", "schema_migrations", "task_history"];
|
|
185
185
|
const missingTables = requiredTables.filter((name) => !tableNames.includes(name));
|
|
186
186
|
const probe = readOnly ? { ok: true, durationMs: null, skipped: true } : probeStateDbRoundTrip(stateDbPath);
|
|
187
|
-
//
|
|
187
|
+
// Read-only, independent of the round-trip probe above — integrity_check
|
|
188
188
|
// catches corruption a successful append/read cannot (out-of-order rowids,
|
|
189
189
|
// bad index entry counts), and the freelist reading is purely informational.
|
|
190
190
|
const stateDbIntegrity = runStateDbIntegrityCheck(stateDbPath);
|
|
@@ -251,7 +251,7 @@ function gatherAncillaryAdvisories(db, options, egressConfigView) {
|
|
|
251
251
|
const indexStateMismatch = detectIndexStateGenerationMismatch(db);
|
|
252
252
|
if (indexStateMismatch)
|
|
253
253
|
advisories.push(indexStateMismatch);
|
|
254
|
-
//
|
|
254
|
+
// Surface a `stash-git-exposure` advisory when env/secret assets are
|
|
255
255
|
// git-tracked AND a remote is configured (the leak moment). Best-effort.
|
|
256
256
|
// Cheap guard: only shell out to git when the stash has its OWN `.git` (or a
|
|
257
257
|
// test injected a fake seam), so the hot path never spawns for a non-git
|
|
@@ -295,8 +295,7 @@ function gatherAncillaryAdvisories(db, options, egressConfigView) {
|
|
|
295
295
|
catch {
|
|
296
296
|
// Non-fatal.
|
|
297
297
|
}
|
|
298
|
-
//
|
|
299
|
-
// own never gets the purge sweep, so its memory-cleanup archive only ever
|
|
298
|
+
// A bundle with no git history of its own never gets the purge sweep, so its memory-cleanup archive only ever
|
|
300
299
|
// grows — report its size and file count instead. Best-effort — an
|
|
301
300
|
// unreadable archive must not abort the health report.
|
|
302
301
|
try {
|
|
@@ -425,7 +424,7 @@ function detectIndexStateGenerationMismatch(stateDb) {
|
|
|
425
424
|
}
|
|
426
425
|
}
|
|
427
426
|
/**
|
|
428
|
-
*
|
|
427
|
+
* Window-compare mode. Resolves `--window-compare`/`--windows` into
|
|
429
428
|
* per-window bundles and computes deltas between the earliest and latest
|
|
430
429
|
* window. Top-level metrics retain the primary `--since` query.
|
|
431
430
|
*/
|
|
@@ -655,7 +654,7 @@ export async function akmHealth(options = {}) {
|
|
|
655
654
|
const hardFailure = hardChecks.some((check) => check.status === "fail");
|
|
656
655
|
const deterministicWarnings = [...hardChecks, ...advisories].some((check) => check.status === "warn" && check.kind === "deterministic");
|
|
657
656
|
const status = hardFailure ? "fail" : deterministicWarnings ? "warn" : "pass";
|
|
658
|
-
// ── Per-run mode
|
|
657
|
+
// ── Per-run mode ────────────────────────────────────────────
|
|
659
658
|
const runs = options.groupBy === "run" ? perRunSummaries : undefined;
|
|
660
659
|
return {
|
|
661
660
|
schemaVersion: 3,
|
|
@@ -390,7 +390,7 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
390
390
|
},
|
|
391
391
|
promote: {
|
|
392
392
|
type: "boolean",
|
|
393
|
-
description: "Promote (accept) matching proposals.
|
|
393
|
+
description: "Promote (accept) matching proposals. Without it, the strategy's apply mode decides, and the default strategy's is promote (akm#1274).",
|
|
394
394
|
default: false,
|
|
395
395
|
},
|
|
396
396
|
judgment: {
|
|
@@ -14,8 +14,11 @@ export async function searchRegistry(query, options) {
|
|
|
14
14
|
return { query: "", hits: [], warnings: [] };
|
|
15
15
|
}
|
|
16
16
|
const limit = clampLimit(options?.limit);
|
|
17
|
+
// AKM_REGISTRY_URL outranks the caller's list: `akm search` passes
|
|
18
|
+
// `config.registries`, which would otherwise shadow the override.
|
|
19
|
+
const envOverride = process.env.AKM_REGISTRY_URL?.trim() ? resolveRegistries() : undefined;
|
|
17
20
|
// resolveRegistries() already filters by enabled; explicit registries are filtered here
|
|
18
|
-
const raw = options?.registries ?? resolveRegistries();
|
|
21
|
+
const raw = envOverride ?? options?.registries ?? resolveRegistries();
|
|
19
22
|
const warnings = [];
|
|
20
23
|
const entries = (options?.registries ? raw.filter((r) => r.enabled !== false) : raw).filter((entry) => {
|
|
21
24
|
if (!hasRegistryUrlCredentials(entry.url))
|
|
@@ -177,7 +177,7 @@ function rejectExtraShowPositionals(positionals, ref) {
|
|
|
177
177
|
if (extra.length === 0)
|
|
178
178
|
return;
|
|
179
179
|
throw new UsageError(`akm show takes a single ref, but got ${extra.map((token) => `"${token}"`).join(" ")} after "${ref}". ` +
|
|
180
|
-
`Use \`akm show ${ref}#<heading-slug>\` to read one section, or \`akm show ${ref}\` for the whole item.`, "INVALID_FLAG_VALUE",
|
|
180
|
+
`Use \`akm show ${ref}#<heading-slug>\` to read one section, or \`akm show ${ref}\` for the whole item.`, "INVALID_FLAG_VALUE", `Run \`akm show ${ref} --detail brief\` for the document's outline (\`toc\`, no body).`);
|
|
181
181
|
}
|
|
182
182
|
export const showCommand = defineJsonCommand({
|
|
183
183
|
meta: {
|
|
@@ -187,7 +187,7 @@ export const showCommand = defineJsonCommand({
|
|
|
187
187
|
args: {
|
|
188
188
|
ref: {
|
|
189
189
|
type: "positional",
|
|
190
|
-
description: "Asset ref ([bundle//]conceptId[#fragment]). On a markdown document `#fragment` selects one section by heading slug
|
|
190
|
+
description: "Asset ref ([bundle//]conceptId[#fragment]). On a markdown document `#fragment` selects one section by heading slug; `--detail brief` returns the outline (`toc`, each heading's slug) without the body. Example: `akm show knowledge/guide.md#auth`.",
|
|
191
191
|
required: true,
|
|
192
192
|
},
|
|
193
193
|
filter: {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `akm search` — entry point.
|
|
6
6
|
*
|
|
7
7
|
* Search consults the local FTS5 index. There is one query path
|
|
8
|
-
* because there is one data store
|
|
8
|
+
* because there is one data store; there is no provider fan-out.
|
|
9
9
|
*
|
|
10
10
|
* The orchestration here is thin: build the FTS query, optionally interleave
|
|
11
11
|
* a registry search behind `--from registry|all`, and log a usage event.
|
|
@@ -214,9 +214,9 @@ function resolveEntryIds(db, hits) {
|
|
|
214
214
|
try {
|
|
215
215
|
const entryId = getEntryIdByFilePath(db, hit.path);
|
|
216
216
|
if (entryId !== undefined) {
|
|
217
|
-
//
|
|
218
|
-
// derived from the resolved entry row's `item_ref` (
|
|
219
|
-
//
|
|
217
|
+
// Persist the DURABLE fully-qualified `bundle//conceptId` spelling,
|
|
218
|
+
// derived from the resolved entry row's `item_ref` (durable keys come
|
|
219
|
+
// from the resolved item, never raw input).
|
|
220
220
|
const itemRef = getItemRefById(db, entryId);
|
|
221
221
|
if (itemRef !== null) {
|
|
222
222
|
results.push({ entryId, ref: itemRef, hit });
|
|
@@ -254,7 +254,7 @@ function logSearchEvent(rawQuery, response, mode = "keyword", eventSource = "use
|
|
|
254
254
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
255
255
|
// detect akm search invocations without relying on stdout scraping.
|
|
256
256
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
257
|
-
//
|
|
257
|
+
// Include registry hit refs so a show following a registry-only search generates a select event
|
|
258
258
|
const registryHitRefs = (response.registryHits ?? []).map((h) => `registry:${h.id}`);
|
|
259
259
|
const allResultRefs = [...stashHits.map((h) => h.ref), ...registryHitRefs];
|
|
260
260
|
appendEvent({
|
|
@@ -267,7 +267,7 @@ function logSearchEvent(rawQuery, response, mode = "keyword", eventSource = "use
|
|
|
267
267
|
// Under contention these usage hints are skipped, not waited for.
|
|
268
268
|
withIndexDb((db) => {
|
|
269
269
|
const resolved = resolveEntryIds(db, stashHits.slice(0, 50));
|
|
270
|
-
// usage_events telemetry
|
|
270
|
+
// usage_events telemetry writes to state.db;
|
|
271
271
|
// entry_id/entry_ref are resolved from index.db above and carried across.
|
|
272
272
|
const stashHitCount = response.hits.length;
|
|
273
273
|
const registryHitCount = Array.isArray(response.registryHits) ? response.registryHits.length : 0;
|