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/README.md +11 -7
- package/index.ts +1265 -101
- package/package.json +2 -2
- package/shared/akm-version.ts +40 -12
- package/shared/curate-render.ts +108 -0
- package/shared/memory-events.ts +4 -0
- package/shared/recall-policy.ts +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
42
|
+
"akm-cli": "^0.9.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^26.1.1",
|
package/shared/akm-version.ts
CHANGED
|
@@ -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.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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.
|
|
22
|
-
// *future* line such as 0.9.
|
|
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.
|
|
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.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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.
|
|
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
|
+
}
|
package/shared/memory-events.ts
CHANGED
|
@@ -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
|
package/shared/recall-policy.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type RecallDecision = {
|
|
|
20
20
|
scopeHints?: string[]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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)) {
|