akm-opencode 0.9.202808211043 → 0.9.11202609031834

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.9.202808211043",
3
+ "version": "0.9.11202609031834",
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": [
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@opencode-ai/plugin": "^1.18.14",
42
- "akm-cli": "^0.9.0"
42
+ "akm-cli": "^0.9.8"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^26.1.1",
@@ -13,28 +13,56 @@
13
13
  // same code path runs on both sides.
14
14
  //
15
15
  // A single caret clause anchored at the stable release covers the whole
16
- // supported line: `^0.9.0` admits every stable 0.9.x (0.9.0, 0.9.3, ...).
17
- // The pre-release floor (`^0.9.0-rc.14`) was retired when akm-cli 0.9.0
18
- // stable shipped: release candidates are dead once the release exists, and
19
- // keeping the RC floor would have kept recommending a prerelease install ref.
16
+ // supported line: `^0.9.8` admits stable 0.9.8 and later 0.9.x releases.
17
+ // 0.9.8 is the compatibility floor, and the reason is a one-way one: 0.9.8
18
+ // adds state migrations 025 and 026, so once any 0.9.8 command opens
19
+ // `state.db` an older CLI refuses to open it at all. A home these plugins
20
+ // have driven is therefore a 0.9.8+ home, and admitting 0.9.7 would point
21
+ // the gate at a CLI that cannot read it. Its ref grammar, progressive-search
22
+ // metadata, token-budgeted curate packing, workflow lifecycle, and
23
+ // task/workflow wire contracts are the ones these plugins implement.
20
24
  //
21
- // KNOWN GAP: NO prerelease satisfies this range — not 0.9.0-rc.15, and not a
22
- // *future* line such as 0.9.1-rc.1. That is node-semver's documented behavior
25
+ // KNOWN GAP: NO prerelease satisfies this range — not 0.9.8-rc.1, and not a
26
+ // *future* line such as 0.9.9-rc.1. That is node-semver's documented behavior
23
27
  // and the vendored matcher reproduces it: a prerelease only satisfies a range
24
28
  // whose lower bound is a prerelease with the same major.minor.patch. Admitting
25
29
  // a prerelease line again is an explicit one-clause edit here
26
- // (`^0.9.0 || ^0.9.1-rc.1`) when such a build actually needs testing — a
30
+ // (`^0.9.8 || ^0.9.9-rc.1`) when such a build actually needs testing — a
27
31
  // deliberate opt-in rather than a range that silently accepts untested
28
32
  // prereleases.
29
33
  //
30
- // 0.8.0 support was dropped for the 0.9.0 release: 0.9.0-only runtime paths
31
- // (`akm proposal extract --session-id`, curate `--detail brief`) fail against
32
- // 0.8.x, so accepting 0.8.x here would silently pass the version gate onto a
33
- // CLI the plugin no longer fully works with.
34
+ // Earlier 0.9 releases are deliberately excluded as well: accepting them
35
+ // would silently pass the version gate onto a CLI with retired ref and
36
+ // workflow contracts, or one that cannot open a migrated state.db.
37
+ //
38
+ // #106 asked, as a maintainer decision, whether a caret range is the right
39
+ // matcher at all given akm's own STABILITY.md: "0.9.x patch releases may
40
+ // also contain breaking changes" while the 0.9.x line pays off technical
41
+ // debt pre-1.0. Verified against that document and against the akm 0.9.12
42
+ // branch (itlackey/akm, release/0.9.12) while checking this plugin for
43
+ // compatibility: every akm surface these plugins call is tier Stable
44
+ // (search, curate, show, info, feedback, workflow list) EXCEPT `akm proposal
45
+ // extract`, which is tier Evolving — "payload shapes may shift" — and is
46
+ // exactly the surface `extractSession()`/`lastExtractFailureWarning()` parse
47
+ // most deeply. 0.9.12 did change that envelope again (added `engine`,
48
+ // `engineKind`, `skipReasons`, and an aggregate `warnings[]` line for an
49
+ // all-skip run — see akm#912/#913) — the Evolving tag is not decorative.
50
+ // Decision: keep the caret range, not because the risk is unreal but because
51
+ // a narrower pin doesn't address it — the failure mode #106 reported was
52
+ // never "the version gate passed when it shouldn't have" (an *already
53
+ // installed* plugin at 0.9.1 would need 0.9.1 itself to have shipped a
54
+ // tighter range, which a future release cannot retroactively fix), it was
55
+ // "a breaking change degraded to silence downstream of the gate". That is
56
+ // what #107/#108/#109 fix directly: every read of the extract envelope goes
57
+ // through safeJsonParse with every field optional-chained, so an Evolving
58
+ // surface changing shape degrades to "say less" (a missing field silently
59
+ // omitted) rather than a crash or a fabricated warning. That is the
60
+ // appropriate mitigation for an Evolving dependency the gate cannot pin
61
+ // away without also rejecting users on every newer patch release.
34
62
 
35
63
  import { satisfies } from "./vendor-semver"
36
64
 
37
- export const AKM_VERSION_RANGE = "^0.9.0"
65
+ export const AKM_VERSION_RANGE = "^0.9.8"
38
66
 
39
67
  /**
40
68
  * True when `version` is a valid semver string that satisfies
@@ -0,0 +1,108 @@
1
+ /**
2
+ * #110 — client-side filtering and rendering for `akm curate`'s `--format
3
+ * json` response, used only by the opt-in relevance-floor path
4
+ * (AKM_CURATE_MIN_SCORE) in both hooks.
5
+ *
6
+ * Why this exists: `akm curate --format text` (the default the hooks have
7
+ * always used) never prints a per-item relevance score — `formatCuratePlain`
8
+ * on the CLI side has no `score` line — so the hooks had no way to tell "five
9
+ * weak hits" from "five strong hits" and always emitted whatever the CLI
10
+ * returned. `--shape agent` DOES carry `score` (and `type`) on each item in
11
+ * `--format json`, so the fix lives here: switch to JSON, decide, and render
12
+ * just enough text to stay useful — NOT a full port of the CLI's own
13
+ * formatter (next-steps footer, warnings block, etc. — the hooks already
14
+ * append their own tip line and provenance banner around this).
15
+ *
16
+ * This module is intentionally not wired into the default (score-floor
17
+ * disabled) code path: that path keeps calling `--format text` exactly as
18
+ * before, so existing behavior — and every test that fakes the CLI's text
19
+ * output — is untouched. See akm-plugins#110 for the fuller design tradeoff
20
+ * (a renderer that fully replaces `--format text` in all cases, so
21
+ * type-priority ranking could apply unconditionally rather than only when
22
+ * the floor is enabled, was considered and deferred as a larger, riskier
23
+ * change than this issue's evidence justified doing in one pass).
24
+ */
25
+
26
+ export interface CuratedItem {
27
+ type?: unknown
28
+ name?: unknown
29
+ ref?: unknown
30
+ description?: unknown
31
+ reason?: unknown
32
+ score?: unknown
33
+ }
34
+
35
+ // Asset types a human or agent authored directly for THIS stash, as opposed
36
+ // to a bulk-imported snapshot of external content (a website crawl, a wiki
37
+ // page, a transcript). #110's evidence: on a real machine, a GitHub Copilot
38
+ // blog post and three doc-site snapshots outranked same-day lessons and
39
+ // memories written about the exact subject being asked about. This is a
40
+ // preference, not a filter — imported types still show, just after authored
41
+ // ones — because a snapshot can still be the best (or only) available hit.
42
+ const AUTHORED_ASSET_TYPES = new Set([
43
+ "lesson",
44
+ "memory",
45
+ "knowledge",
46
+ "skill",
47
+ "command",
48
+ "agent",
49
+ "instruction",
50
+ "fact",
51
+ "workflow",
52
+ "task",
53
+ "env",
54
+ "secret",
55
+ ])
56
+
57
+ function isAuthoredType(type: unknown): boolean {
58
+ return typeof type === "string" && AUTHORED_ASSET_TYPES.has(type)
59
+ }
60
+
61
+ /**
62
+ * Keep items scoring at or above `minScore`, then stable-sort authored asset
63
+ * types ahead of imported/scraped ones. Items with no numeric `score` are
64
+ * kept regardless of `minScore` — a missing score is not evidence the item is
65
+ * weak, it means this response shape did not carry one, and inventing a
66
+ * reason to drop it would repeat the mistake #107 fixed on the extract side
67
+ * (a specific wrong guess is worse than none).
68
+ *
69
+ * akm-cli has already decided which N items are the top-N for this query;
70
+ * this only decides which of those N are worth showing, and in what order —
71
+ * it never asks the CLI for more candidates to backfill a dropped slot, so a
72
+ * strict floor can legitimately shrink the result set to nothing.
73
+ */
74
+ export function filterAndRankCuratedItems(rawItems: unknown, minScore: number): CuratedItem[] {
75
+ if (!Array.isArray(rawItems)) return []
76
+ const items = rawItems.filter((item): item is CuratedItem => !!item && typeof item === "object")
77
+ const survivors =
78
+ minScore > 0 ? items.filter((item) => typeof item.score !== "number" || item.score >= minScore) : items
79
+ return survivors
80
+ .map((item, index) => ({ item, index }))
81
+ .sort((a, b) => {
82
+ const rankDiff = Number(isAuthoredType(b.item.type)) - Number(isAuthoredType(a.item.type))
83
+ // Stable within each group: preserve akm-cli's own relative order
84
+ // (its score-based ranking) rather than re-sorting by score here too.
85
+ return rankDiff !== 0 ? rankDiff : a.index - b.index
86
+ })
87
+ .map(({ item }) => item)
88
+ }
89
+
90
+ /**
91
+ * Compact plain-text rendering of already-filtered/ranked items. Deliberately
92
+ * smaller than the CLI's own `formatCuratePlain`: no "Next steps" footer or
93
+ * warnings block, because both hooks that call this already append their own
94
+ * tip line (CURATED_CONTEXT_TAIL) and provenance banner around the result.
95
+ */
96
+ export function renderCuratedItems(query: string, items: readonly CuratedItem[]): string {
97
+ const lines: string[] = [`Curated results for "${query}"`]
98
+ for (const item of items) {
99
+ const type = typeof item.type === "string" ? item.type : "unknown"
100
+ const name = typeof item.name === "string" ? item.name : "unnamed"
101
+ lines.push("")
102
+ lines.push(`[${type}] ${name}`)
103
+ if (typeof item.description === "string" && item.description) lines.push(` ${item.description}`)
104
+ if (typeof item.ref === "string" && item.ref) lines.push(` ref: ${item.ref}`)
105
+ if (typeof item.reason === "string" && item.reason) lines.push(` why: ${item.reason}`)
106
+ }
107
+ return lines.join("\n")
108
+ }
@@ -25,6 +25,10 @@ export type AkmMemoryEventType =
25
25
  | "subagent_started"
26
26
  | "post_compact_summary"
27
27
  | "feedback_recorded"
28
+ // The opencode write gate (#99) emits exactly one of these per watched
29
+ // write-tool invocation, with a named reason in `input.reason`. One name, not
30
+ // two: fire and skip are the same event with different outcome.status.
31
+ | "write_gate"
28
32
 
29
33
  export type AkmMemoryEvent = {
30
34
  version: 1
@@ -20,7 +20,7 @@ export type RecallDecision = {
20
20
  scopeHints?: string[]
21
21
  }
22
22
 
23
- const REF_RE = /(?:skill|command|agent|knowledge|workflow|lesson|wiki|memory|env|secret):[A-Za-z0-9._/-]+/i
23
+ import { extractAkmRefsFromString } from "./ref-extraction"
24
24
 
25
25
  export function shouldRecall(prompt: string, options?: { activeWorkflow?: boolean; recentAssetFailure?: boolean }): RecallDecision {
26
26
  const text = prompt.trim()
@@ -35,7 +35,11 @@ export function shouldRecall(prompt: string, options?: { activeWorkflow?: boolea
35
35
  if (/\b(hi|hello|how are you|good morning|good night)\b/i.test(lower) && text.length < 40) {
36
36
  return { shouldRecall: false, reason: "skip-chitchat", query: text, scopeHints }
37
37
  }
38
- if (/\bakm\b|\bstash\b/.test(lower) || REF_RE.test(text)) {
38
+ // Reuse the resolver-facing parser rather than carrying a second, stale
39
+ // grammar here. AKM 0.9.7 refs are [bundle//]conceptId[#fragment]; retired
40
+ // type:name strings must not turn an otherwise low-signal prompt into an
41
+ // explicit AKM recall.
42
+ if (/\bakm\b|\bbundle\b/.test(lower) || extractAkmRefsFromString(text).length > 0) {
39
43
  return { shouldRecall: true, reason: "explicit-akm", query: text, scopeHints: ["akm"] }
40
44
  }
41
45
  if (/\b(remember|memory|prior session|previous decision)\b/.test(lower)) {
@@ -47,7 +51,7 @@ export function shouldRecall(prompt: string, options?: { activeWorkflow?: boolea
47
51
  if (/\b(dispatch|agent|subagent|reviewer|planner|curator)\b/.test(lower)) {
48
52
  return { shouldRecall: true, reason: "agent-dispatch", query: text, scopeHints: ["agent"] }
49
53
  }
50
- if (/\b(command|slash command|run the stash command)\b/.test(lower)) {
54
+ if (/\b(command|slash command|run the bundle command)\b/.test(lower)) {
51
55
  return { shouldRecall: true, reason: "command-dispatch", query: text, scopeHints: ["command"] }
52
56
  }
53
57
  if (/\b(wiki|docs|knowledge base|ingest|lint)\b/.test(lower)) {