akm-opencode 0.9.11202609031834 → 0.9.11202609031957

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/index.ts CHANGED
@@ -11,6 +11,7 @@ import os from "node:os"
11
11
  import path from "node:path"
12
12
  import { fileURLToPath } from "node:url"
13
13
  import { filterAndRankCuratedItems, renderCuratedItems } from "./shared/curate-render"
14
+ import { compareSemver } from "./shared/vendor-semver"
14
15
  import { classifyFeedbackSignal, createExplicitCorrectionRegex, createRetrospectiveFeedbackRegex, createRetrospectiveNegativeRegex, shouldSubmitAutomaticFeedback } from "./shared/feedback-signals"
15
16
  import { appendMemoryEvent, getEventLogPath, type AkmMemoryEvent } from "./shared/memory-events"
16
17
  import { AKM_VERSION_RANGE, satisfiesAkmVersionRange } from "./shared/akm-version"
@@ -2367,7 +2368,7 @@ type AkmResolutionTrail = Array<{
2367
2368
  command: string
2368
2369
  source: "bundled" | "path" | "local_build"
2369
2370
  version: string | null
2370
- outcome: "selected" | "version_out_of_range" | "missing" | "probe_failed"
2371
+ outcome: "selected" | "superseded" | "version_out_of_range" | "missing" | "probe_failed"
2371
2372
  failureReason: string | null
2372
2373
  }>
2373
2374
 
@@ -2375,17 +2376,29 @@ let lastAkmResolutionTrail: AkmResolutionTrail = []
2375
2376
 
2376
2377
  function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displayCommand: string; version: string; source: "bundled" | "path" | "local_build" } | null {
2377
2378
  const candidates: Array<{ command: string; argsPrefix: string[]; displayCommand: string; source: "bundled" | "path" | "local_build" }> = []
2378
- // Resolution precedence — first compatible candidate wins:
2379
+ // Resolution: probe every candidate, then take the NEWEST compatible one.
2380
+ //
2381
+ // This used to be first-compatible-wins with PATH ahead of the bundled dep,
2382
+ // on the reasoning that the user's own akm wrote their config and has its
2383
+ // native deps built. That reasoning is sound about which candidates are
2384
+ // ELIGIBLE; it is not a reason to stop at the first one. Because the floor is
2385
+ // a range (^0.9.8) and not an exact pin, first-match silently bound a plugin
2386
+ // that depends on a much newer akm-cli to whatever ancient-but-in-range `akm`
2387
+ // happened to sit earliest on PATH — for as long as that stayed true. That is
2388
+ // the failure mode behind akm-plugins#106: a plugin driving a CLI it was not
2389
+ // built against, indefinitely, with nothing reporting the mismatch.
2390
+ //
2391
+ // Picking the newest compatible candidate keeps every prior eligibility rule
2392
+ // (a config-incompatible or unbuilt candidate still fails its `--version`
2393
+ // probe and is skipped, so the probe remains the compatibility gate) while
2394
+ // removing the arbitrary dependence on PATH order. When the user's own akm is
2395
+ // current — the normal case — it is still selected, because it ties with or
2396
+ // beats the bundled copy and ties resolve in candidate order.
2397
+ //
2398
+ // Candidate order therefore now only breaks ties:
2379
2399
  // 1. AKM_LOCAL_BUILD_CLI — explicit dev override
2380
- // 2. PATH / user installs — the akm the user actually installed, which wrote
2381
- // their config and has its native deps (e.g. embeddings) built
2382
- // 3. bundled akm-cli — last-resort fallback for users with no akm
2383
- // The bundled CLI is deliberately LAST: preferring it over the user's own
2384
- // install would ignore a newer user akm that understands a newer config and
2385
- // route through a bundled copy whose native postinstalls may be unbuilt. A
2386
- // config-INCOMPATIBLE candidate fails its `--version` probe — older akm builds
2387
- // validate config on every invocation and exit non-zero — so it is skipped
2388
- // silently and the version probe doubles as a config-compatibility gate.
2400
+ // 2. PATH / user installs — preferred at equal version
2401
+ // 3. bundled akm-cli — the dependency npm resolved for this plugin
2389
2402
  const localBuild = getLocalBuildAkmCommand()
2390
2403
  if (localBuild) candidates.push({ ...localBuild, source: "local_build" })
2391
2404
  for (const command of getPathAkmCandidates()) {
@@ -2400,6 +2413,7 @@ function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displ
2400
2413
 
2401
2414
  const trail: AkmResolutionTrail = []
2402
2415
  const seen = new Set<string>()
2416
+ const eligible: Array<{ candidate: (typeof candidates)[number]; version: string }> = []
2403
2417
  for (const candidate of candidates) {
2404
2418
  const cacheKey = `${candidate.command}::${candidate.argsPrefix.join(" ")}`
2405
2419
  if (!candidate.command || seen.has(cacheKey)) continue
@@ -2417,11 +2431,27 @@ function getResolvedAkmDetails(): { command: string; argsPrefix: string[]; displ
2417
2431
  trail.push({ command: candidate.displayCommand, source: candidate.source, version: probe.version, outcome: "version_out_of_range", failureReason: null })
2418
2432
  continue
2419
2433
  }
2420
- trail.push({ command: candidate.displayCommand, source: candidate.source, version: probe.version, outcome: "selected", failureReason: null })
2421
- lastAkmResolutionTrail = trail
2422
- return { ...candidate, version: probe.version }
2434
+ eligible.push({ candidate, version: probe.version })
2435
+ }
2436
+
2437
+ // Newest compatible wins; candidate order breaks ties, so an up-to-date user
2438
+ // install still beats the bundled copy at equal version.
2439
+ let best: { candidate: (typeof candidates)[number]; version: string } | null = null
2440
+ for (const entry of eligible) {
2441
+ if (!best || compareSemver(entry.version, best.version) > 0) best = entry
2442
+ }
2443
+ for (const entry of eligible) {
2444
+ const isSelected = best !== null && entry.candidate === best.candidate
2445
+ trail.push({
2446
+ command: entry.candidate.displayCommand,
2447
+ source: entry.candidate.source,
2448
+ version: entry.version,
2449
+ outcome: isSelected ? "selected" : "superseded",
2450
+ failureReason: null,
2451
+ })
2423
2452
  }
2424
2453
  lastAkmResolutionTrail = trail
2454
+ if (best) return { ...best.candidate, version: best.version }
2425
2455
  return null
2426
2456
  }
2427
2457
 
@@ -3670,6 +3700,9 @@ export const AkmPlugin = Object.assign(akmPlugin, {
3670
3700
  // file that re-imports here would leak into tests/opencode-plugin.test.ts.
3671
3701
  // tests/opencode-curate-floor.test.ts therefore drives these two seams from
3672
3702
  // a subprocess instead, which shares no module registry with anything.
3703
+ // #106 — lets a test observe WHICH akm the resolver selected (and at what
3704
+ // version), which is the whole behaviour the newest-compatible rule changes.
3705
+ __resolvedAkmDetailsForTests: getResolvedAkmDetails,
3673
3706
  __buildCurateArgsForTests: buildCurateArgs,
3674
3707
  __renderCuratedJsonResponseForTests: renderCuratedJsonResponse,
3675
3708
  __resetWriteGateForTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.9.11202609031834",
3
+ "version": "0.9.11202609031957",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for AKM 0.9 with in-process search, show, and curate tools plus feedback and remember.",
6
6
  "keywords": [
@@ -76,6 +76,24 @@ function parseCaretRange(token: string): CaretRange | null {
76
76
  return { min, maxExclusive, allowPrereleases: min.prerelease.length > 0 }
77
77
  }
78
78
 
79
+ /**
80
+ * Compare two versions by semver precedence: negative when `a < b`, positive
81
+ * when `a > b`, 0 when equal. Unparseable input sorts LOWEST, so a candidate
82
+ * whose `--version` could not be read never wins a comparison.
83
+ *
84
+ * Exists so callers can pick the NEWEST compatible candidate rather than the
85
+ * first one that happens to satisfy a range — a range is an eligibility test,
86
+ * not an ordering.
87
+ */
88
+ export function compareSemver(a: string, b: string): number {
89
+ const pa = parse(a)
90
+ const pb = parse(b)
91
+ if (!pa && !pb) return 0
92
+ if (!pa) return -1
93
+ if (!pb) return 1
94
+ return cmp(pa, pb)
95
+ }
96
+
79
97
  export function satisfies(version: string, range: string): boolean {
80
98
  const v = parse(version)
81
99
  if (!v) return false