akm-opencode 0.9.20202610010009 → 0.9.21202610012143

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +90 -29
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # akm-opencode
2
2
 
3
- OpenCode plugin for [AKM](https://github.com/itlackey/akm) `0.9.20`. It exposes exactly five public tools and uses lifecycle hooks to bring relevant AKM context into a session. The dependency is exact-pinned because the plugin calls AKM's in-process `dist/` APIs.
3
+ OpenCode plugin for [AKM](https://github.com/itlackey/akm) `0.9.21`. It exposes exactly five public tools and uses lifecycle hooks to bring relevant AKM context into a session. The dependency is exact-pinned because the plugin calls AKM's in-process `dist/` APIs.
4
4
 
5
5
  ## Installation
6
6
 
@@ -56,7 +56,7 @@ The plugin subscribes to OpenCode lifecycle events. Hook failures are logged thr
56
56
 
57
57
  The session observation buffer that retrospective feedback reads from survives every non-terminal event: it is bounded by `AKM_SESSION_BUFFER_MAX_ENTRIES` and dropped only on `session.deleted`. Discarding it at `session.idle` would empty it between turns, so "thanks, that worked" would credit nothing in exactly the sessions that used the most assets.
58
58
 
59
- Automatic feedback skips references that AKM reports as ineligible, and no `akm_*` tool call submits feedback on its own. A *successful* `akm_show` / `akm_search` / `akm_curate` only inspected a concept, which is not evidence that it helped, so on OpenCode the positive signal comes from a retrospective confirmation instead. A *failed* call (an error result, a timeout, an ambiguous ref) is not feedback on the asset either, and is only logged. Same rules as the Claude plugin, which applies them to the `akm` subcommand of a Bash invocation.
59
+ Automatic feedback skips references that AKM reports as ineligible, and no `akm_*` tool call submits feedback on its own. A *successful* `akm_show` / `akm_search` / `akm_curate` only inspected a concept, which is not evidence that it helped, so on OpenCode the positive signal comes from a retrospective confirmation instead. A *failed* call (an error result, a timeout, an ambiguous ref) is not feedback on the asset either, and is only logged: its ref is not remembered as used, so a later "thanks, that worked" does not credit it and a later correction does not blame it. Same rules as the Claude plugin, which applies them to the `akm` subcommand of a Bash invocation.
60
60
 
61
61
  ### Automatic learning proposals
62
62
 
package/dist/index.js CHANGED
@@ -119,6 +119,7 @@ function shouldSubmitAutomaticFeedback(signal) {
119
119
 
120
120
  // ../claude/shared/memory-events.ts
121
121
  import { appendFileSync, existsSync, mkdirSync } from "fs";
122
+ import { homedir } from "os";
122
123
  import path from "path";
123
124
 
124
125
  // ../claude/shared/redaction.ts
@@ -376,7 +377,7 @@ function rotateIfOversized(filePath) {
376
377
 
377
378
  // ../claude/shared/memory-events.ts
378
379
  function getHarnessStateDir(harness) {
379
- const root = process.env.XDG_STATE_HOME ?? path.join(process.env.HOME ?? ".", ".local", "state");
380
+ const root = process.env.XDG_STATE_HOME ?? path.join(homedir(), ".local", "state");
380
381
  return path.join(root, harness === "claude-code" ? "akm-claude" : "akm-opencode");
381
382
  }
382
383
  function getEventLogPath(harness) {
@@ -1008,8 +1009,45 @@ function isFile(file) {
1008
1009
  return false;
1009
1010
  }
1010
1011
  }
1012
+ var SCRIPT_EXTENSIONS = new Set([
1013
+ ".sh",
1014
+ ".ts",
1015
+ ".js",
1016
+ ".ps1",
1017
+ ".cmd",
1018
+ ".bat",
1019
+ ".py",
1020
+ ".rb",
1021
+ ".go",
1022
+ ".pl",
1023
+ ".php",
1024
+ ".lua",
1025
+ ".r",
1026
+ ".swift",
1027
+ ".kt",
1028
+ ".kts"
1029
+ ]);
1030
+ function markdownFile(directPath) {
1031
+ return isFile(`${directPath}.md`) || directPath.endsWith(".md") && isFile(directPath);
1032
+ }
1033
+ function isAsset(type, directPath) {
1034
+ if (type === "skills")
1035
+ return isFile(path3.join(directPath, "SKILL.md"));
1036
+ if (type === "scripts")
1037
+ return SCRIPT_EXTENSIONS.has(path3.extname(directPath).toLowerCase()) && isFile(directPath);
1038
+ if (type === "secrets")
1039
+ return !/\.(lock|sensitive)$/.test(directPath) && isFile(directPath);
1040
+ if (type === "tasks")
1041
+ return isFile(`${directPath}.yml`);
1042
+ if (type === "env") {
1043
+ return isFile(`${directPath}.env`) || path3.basename(directPath) === "default" && isFile(path3.join(path3.dirname(directPath), ".env"));
1044
+ }
1045
+ return markdownFile(directPath) || type === "workflows" && isFile(`${directPath}.yml`);
1046
+ }
1047
+ var OUTSIDE_THEIR_DIRECTORY = new Set(["scripts", "agents", "commands", "facts", "instructions", "knowledge", "lessons", "memories", "sessions"]);
1011
1048
  function conceptExistsInAnyBundle(conceptId, bundleRoots) {
1012
1049
  const type = conceptId.split("/", 1)[0];
1050
+ const rest = conceptId.slice(type.length + 1);
1013
1051
  for (const root of bundleRoots) {
1014
1052
  if (!root)
1015
1053
  continue;
@@ -1017,16 +1055,10 @@ function conceptExistsInAnyBundle(conceptId, bundleRoots) {
1017
1055
  const directPath = path3.resolve(resolvedRoot, conceptId);
1018
1056
  if (directPath !== resolvedRoot && !directPath.startsWith(`${resolvedRoot}${path3.sep}`))
1019
1057
  continue;
1020
- if (type === "skills") {
1021
- if (isFile(path3.join(directPath, "SKILL.md")))
1022
- return true;
1023
- } else if (type === "scripts" || type === "secrets") {
1024
- if (isFile(directPath))
1025
- return true;
1026
- } else {
1027
- if (isFile(`${directPath}.md`) || directPath.endsWith(".md") && isFile(directPath))
1028
- return true;
1029
- if (type === "memories" && isFile(`${directPath}.derived.md`))
1058
+ if (isAsset(type, directPath))
1059
+ return true;
1060
+ if (OUTSIDE_THEIR_DIRECTORY.has(type) && rest.includes("/") && !rest.startsWith(`${type}/`)) {
1061
+ if (isAsset(type, path3.resolve(resolvedRoot, rest)))
1030
1062
  return true;
1031
1063
  }
1032
1064
  }
@@ -1050,14 +1082,39 @@ function validateRefCandidates(candidates, bundleRoots) {
1050
1082
  // ../claude/shared/recall-policy.ts
1051
1083
  var STASH_BOILERPLATE = "This is an **AKM stash** \u2014 a structured knowledge repository that stores reusable";
1052
1084
  var COMPACTION_CONTINUATION = "This session is being continued from a previous conversation";
1085
+ var TAG = /<(\/?)([A-Za-z][\w:.-]*)(?:\s[^<>]*)?>/g;
1086
+ function leadingBlockEnd(text) {
1087
+ const tags = new RegExp(TAG);
1088
+ let name;
1089
+ let depth = 0;
1090
+ for (let tag = tags.exec(text);tag; tag = tags.exec(text)) {
1091
+ if (name === undefined) {
1092
+ if (tag.index !== 0 || tag[1])
1093
+ return -1;
1094
+ name = tag[2];
1095
+ } else if (tag[2] !== name)
1096
+ continue;
1097
+ depth += tag[1] ? -1 : 1;
1098
+ if (depth === 0)
1099
+ return tag.index + tag[0].length;
1100
+ }
1101
+ return -1;
1102
+ }
1103
+ function stripLeadingEnvelopes(text) {
1104
+ let rest = text.trimStart();
1105
+ for (let end = leadingBlockEnd(rest);end > 0; end = leadingBlockEnd(rest))
1106
+ rest = rest.slice(end).trimStart();
1107
+ return rest;
1108
+ }
1053
1109
  function isNonTaskPrompt(text) {
1054
- const stripped = text.trimStart();
1055
- if (stripped === STASH_BOILERPLATE || stripped.startsWith(COMPACTION_CONTINUATION))
1110
+ const typed = stripLeadingEnvelopes(text);
1111
+ if (typed === STASH_BOILERPLATE || typed.startsWith(COMPACTION_CONTINUATION))
1056
1112
  return true;
1057
- return stripped.startsWith("<") && stripped.includes("</");
1113
+ return typed === "" ? text.trim() !== "" : typed.startsWith("<") && typed.includes("</");
1058
1114
  }
1059
1115
  function shouldRecall(prompt, options) {
1060
- const text = prompt.trim();
1116
+ const trimmed = prompt.trim();
1117
+ const text = stripLeadingEnvelopes(trimmed) || trimmed;
1061
1118
  const lower = text.toLowerCase();
1062
1119
  const scopeHints = [];
1063
1120
  if (!text)
@@ -1219,7 +1276,7 @@ var retrospectiveState = new Map;
1219
1276
  var cachedAkmBundleDir;
1220
1277
  var PROPOSED_QUALITY_WARNING = "Do not treat proposed assets as curated until accepted.";
1221
1278
  var AKM_WORKFLOW_INSTRUCTION = [
1222
- "# AKM workflow (v0.9.20)",
1279
+ "# AKM workflow (v0.9.21)",
1223
1280
  "",
1224
1281
  "Use AKM as a reusable knowledge and workflow bundle.",
1225
1282
  "",
@@ -2373,12 +2430,12 @@ var AKM_HINTS_PREFIX = [
2373
2430
  '- **`akm_search` (known name)** \u2014 use ONLY when you already know an asset exists (e.g. after `akm_show` returned "not found") and need to locate its exact ref. Do not use as a discovery tool.',
2374
2431
  "- **`akm_show <bundle>//meta`** \u2014 when working in or with an unfamiliar bundle, read its optional `.meta/` orientation (purpose, key assets, conventions, maintainer) before diving in. `akm_show meta` reads your working bundle's `.meta/index.md`; `akm_show meta:<name>` reads other `.meta/` docs (e.g. `meta:about`). These docs are direct-read and never appear in `akm_search`.",
2375
2432
  "",
2376
- "Record `akm_feedback <ref> positive|negative` whenever an asset's content materially helps, or proves wrong, stale or unhelpful (a failed akm call is not feedback on the asset), and use `akm_remember` to persist durable learnings so future sessions inherit them.",
2433
+ "When an asset's content is wrong, stale or incomplete, call `akm_feedback <ref> negative` with a note saying <what is wrong and what should change>: negative feedback is what triggers a review and fix of the asset, so make the note specific. Call it with `positive` when an asset materially helped (it only improves ranking). A failed akm call is not feedback on the asset. Use `akm_remember` to persist durable learnings so future sessions inherit them.",
2377
2434
  "",
2378
2435
  AKM_WORKFLOW_INSTRUCTION
2379
2436
  ].join(`
2380
2437
  `);
2381
- var AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content, and record `akm_feedback <ref> positive|negative` once you know whether the asset helped.";
2438
+ var AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content. If an asset is wrong, stale or incomplete, call `akm_feedback <ref> negative` with a note saying <what is wrong and what should change>: that queues it for review and a fix. Use `positive` when it helped.";
2382
2439
  var AKM_CONTEXT_TRUNCATED_MARKER = `
2383
2440
 
2384
2441
  [truncated for context]`;
@@ -3276,6 +3333,7 @@ ${hint}` : hint;
3276
3333
  const candidateRefs = [...new Set([...allArgRefs, ...allOutputRefs])];
3277
3334
  const parsedForRefs = isAkmTool ? parseToolOutput(output.output) : null;
3278
3335
  const allRefs = isAkmTool && parsedForRefs ? extractToolRefs(input.tool, input.args, parsedForRefs) : validateRefCandidates(candidateRefs, [await getAkmBundleDir(logClient) ?? ""]);
3336
+ const failedCall = classifyToolFeedback(parsedForRefs) === "negative";
3279
3337
  if (allRefs.length > 0) {
3280
3338
  writeStructuredEvent({
3281
3339
  event: "tool_ref_observed",
@@ -3285,13 +3343,15 @@ ${hint}` : hint;
3285
3343
  refs: allRefs,
3286
3344
  outcome: { status: "ok" }
3287
3345
  });
3288
- for (const ref of allRefs) {
3289
- addBufferEntry(input.sessionID, {
3290
- kind: "tool-ref",
3291
- toolName: input.tool,
3292
- ref,
3293
- status: "unknown"
3294
- });
3346
+ if (!failedCall) {
3347
+ for (const ref of allRefs) {
3348
+ addBufferEntry(input.sessionID, {
3349
+ kind: "tool-ref",
3350
+ toolName: input.tool,
3351
+ ref,
3352
+ status: "unknown"
3353
+ });
3354
+ }
3295
3355
  }
3296
3356
  }
3297
3357
  if (input.tool === "read") {
@@ -3328,7 +3388,8 @@ ${hint}` : hint;
3328
3388
  if ((input.tool === "akm_show" || input.tool === "akm_curate") && feedback !== "negative") {
3329
3389
  noteShownRefs(input.sessionID, toolRefs);
3330
3390
  }
3331
- noteRecentRefs(input.sessionID, toolRefs);
3391
+ if (!failedCall)
3392
+ noteRecentRefs(input.sessionID, toolRefs);
3332
3393
  writeStructuredEvent({
3333
3394
  event: "tool_observation",
3334
3395
  sessionId: input.sessionID,
@@ -3337,7 +3398,7 @@ ${hint}` : hint;
3337
3398
  refs: toolRefs,
3338
3399
  outcome: { status: feedback === "negative" ? "failed" : "ok" }
3339
3400
  });
3340
- if (toolRefs.length > 0 && input.sessionID) {
3401
+ if (toolRefs.length > 0 && input.sessionID && !failedCall) {
3341
3402
  for (const ref of toolRefs) {
3342
3403
  addBufferEntry(input.sessionID, {
3343
3404
  kind: "tool-ref",
@@ -3416,11 +3477,11 @@ ${hint}` : hint;
3416
3477
  }
3417
3478
  }),
3418
3479
  akm_feedback: tool({
3419
- description: "Record positive or negative feedback for a bundle asset so AKM can improve future ranking. Call it after akm_show whenever the asset's content materially helped, or proved wrong, stale or unhelpful. A failed akm call is not feedback on the asset.",
3480
+ description: "Record feedback for a bundle asset. Negative feedback with a note queues the asset for review: the next improve run proposes a fix based on the note, so say what is wrong and what should change. Positive feedback raises the asset's ranking and never triggers a rewrite. Call it after akm_show when the asset's content materially helped, or proved wrong, stale or incomplete. A failed akm call is not feedback on the asset.",
3420
3481
  args: {
3421
3482
  ref: tool.schema.string().describe("Asset ref to record feedback for."),
3422
3483
  sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
3423
- note: tool.schema.string().optional().describe("Optional note to attach to the feedback.")
3484
+ note: tool.schema.string().optional().describe("What is wrong and what should change. Required for negative feedback; it guides the fix.")
3424
3485
  },
3425
3486
  async execute({ ref, sentiment, note }, context) {
3426
3487
  const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.9.20202610010009",
3
+ "version": "0.9.21202610012143",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for AKM with discovery tools, feedback, memory, and automatic learning proposals from corrections and recurring workflows.",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@opencode-ai/plugin": "^1.18.14",
41
- "akm-cli": "0.9.20"
41
+ "akm-cli": "0.9.21"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^26.1.1",