akm-opencode 0.5.2 → 0.7.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # akm-opencode
2
2
 
3
- OpenCode plugin for the [AKM](https://github.com/itlackey/akm) CLI (v0.5.0+). Registers tools that let your AI agent **search**, **show**, and **manage** stash assets — skills, commands, agents, knowledge, memories, scripts, workflows, vaults, and wikis — plus **agentic hooks** that auto-load relevant assets into each turn, record feedback when assets are used, and harvest session memories so the stash improves with every session.
3
+ OpenCode plugin for the [AKM](https://github.com/itlackey/akm) CLI (v0.7.0+). Registers tools that let your AI agent **search**, **show**, and **manage** stash assets — skills, commands, agents, knowledge, memories, lessons, scripts, workflows, vaults, and wikis — **operate the v0.7.0 proposal queue** and **distill lessons** through dedicated tools, plus **agentic hooks** that auto-load relevant assets into each turn, record feedback when assets are used (skipping proposed-quality drafts), and harvest session memories so the stash improves with every session.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,16 +14,16 @@ Add to your OpenCode config (`opencode.json`):
14
14
 
15
15
  ## Tools
16
16
 
17
- The plugin exposes a trimmed surface of **14 high-value tools**. Long-tail verbs (`add`, `save`, `import`, `clone`, `update`, `remove`, `list`-sources, `registry-search`, `index`-reindex, `config`, `upgrade`, ad-hoc `run`) are reachable via `akm_help` plus the raw `akm` CLI through the `bash` tool.
17
+ The plugin exposes **19 high-value tools**. Long-tail verbs (`add`, `save`, `import`, `clone`, `update`, `remove`, `list`-sources, `registry-search`, `index`-reindex, `config`, `upgrade`, ad-hoc `run`, raw `agent`) are reachable via `akm_help` plus the raw `akm` CLI through the `bash` tool.
18
18
 
19
19
  | Tool | Description |
20
20
  |------|-------------|
21
- | `akm_search` | Search the local stash, the registry, or both. Type filter accepts `skill`, `command`, `agent`, `knowledge`, `memory`, `script`, `workflow`, `vault`, `wiki`, `any` |
21
+ | `akm_search` | Search the local stash, the registry, or both. Type filter accepts `skill`, `command`, `agent`, `knowledge`, `lesson`, `memory`, `script`, `workflow`, `vault`, `wiki`, `any`; proposed hits can be included explicitly |
22
22
  | `akm_show` | Show a stash asset by its ref |
23
23
  | `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
24
24
  | `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
25
25
  | `akm_remember` | Record a memory in the default stash |
26
- | `akm_feedback` | Record positive or negative feedback for a stash asset (skipped automatically for `memory:` and `vault:` refs) |
26
+ | `akm_feedback` | Record positive or negative feedback for a stash asset (skipped automatically for `memory:`, `vault:`, `lesson:`, and proposed-quality refs) |
27
27
  | `akm_curate` | Curate the stash for a task or topic and return ranked matches the agent can use |
28
28
  | `akm_evolve` | Dispatch the AKM curator subagent into a child session, capture the report as a memory, and seed the curator-context cache so it survives compaction |
29
29
  | `akm_parent_messages` | Summarize the parent OpenCode session so dispatched stash subagents can inherit upstream context |
@@ -31,6 +31,11 @@ The plugin exposes a trimmed surface of **14 high-value tools**. Long-tail verbs
31
31
  | `akm_vault` | Vault `list` / `show` (key names) / `create` / `set` / `unset` / `load` (opaque shell-eval text). **Values never surface** through `list`/`show`; `load` output is meant for `eval` and must not be displayed back |
32
32
  | `akm_wiki` | Manage wikis (`create`, `register`, `list`, `show`, `pages`, `search`, `stash`, `lint`, `ingest`, `remove`) |
33
33
  | `akm_workflow` | Drive workflow runs (`start`, `next`, `complete`, `status`, `list`, `create`, `template`, `resume`) |
34
+ | `akm_proposal` | Operate the v0.7.0 proposal queue (`list` / `show` / `diff` / `accept` / `reject`). Always confirm with the user before `accept`/`reject` — those operations require explicit approval |
35
+ | `akm_reflect` | Generate a reflection proposal via the configured agent CLI; output lands in the proposal queue only |
36
+ | `akm_propose` | Generate a new-asset proposal via the configured agent CLI; the result is `quality:"proposed"` until accepted |
37
+ | `akm_distill` | Distill an AKM ref (typically `memory:*` or `knowledge:*`) into a proposed `lesson` (gated by `llm.features.feedback_distillation`) |
38
+ | `akm_setup` | Detect installed agent CLIs (`opencode`, `claude`, `codex`, `gemini`, `aider`) and persist `agent.default`. Required once per machine for `akm_reflect` / `akm_propose` |
34
39
  | `akm_help` | Discover the right `akm` CLI invocation for non-first-class verbs. Returns a curated quick-reference table plus live `akm <subcommand> --help` output |
35
40
 
36
41
  ## Compound-engineering hooks
@@ -41,14 +46,15 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
41
46
 
42
47
  | Event | What happens |
43
48
  | --- | --- |
44
- | **`session.created`** (event hook) | Warms the stash index in the background and caches `akm hints` plus active workflow status for the next system transform so the agent knows the CLI surface area at turn 0. |
45
- | **`chat.message`** | Runs `akm curate "<prompt>"` on each user message (prompts shorter than `AKM_CURATE_MIN_CHARS` are skipped). The top matches are stored for injection. Memory intents (prompts mentioning "remember" / "memory") are tracked in the session buffer. |
46
- | **`experimental.chat.system.transform`** | Appends cached hints, active workflow state, the last curator report, and the current prompt's curated context to the model's system prompt. Hints and workflow state are re-injected after transcript compaction. |
49
+ | **`session.created`** (event hook) | Warms the stash index in the background, caches `akm hints` plus active workflow status, and runs a scoped `akm curate --run <sessionID>` so fresh sessions see relevant stash context before the first user message. |
50
+ | **`chat.message`** | Runs `akm curate "<prompt>" --run <sessionID>` on each user message (prompts shorter than `AKM_CURATE_MIN_CHARS` are skipped). The top matches are stored for injection. Memory intents (prompts mentioning "remember" / "memory") are tracked in the session buffer. |
51
+ | **`experimental.chat.system.transform`** | Appends cached hints, active workflow state, pending proposal summaries, the last curator report, and the current prompt's curated context to the model's system prompt. Hints and workflow state are re-injected after transcript compaction. |
47
52
  | **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
53
+ | **`permission.ask`** / **`command.execute.before`** | Detects risky raw `akm` CLI commands executed through shell/commands and denies them until the user explicitly approves the exact operation. |
48
54
  | **`tool.execute.after`** (`akm_*` tools) | Logs asset usage, accumulates refs into the session buffer, records `akm feedback <ref> --positive` / `--negative` asynchronously with per-call dedupe, checkpoints memories every `AKM_MEMORY_CHECKPOINT_EVERY` successful asset-touching tool calls, and scans child-agent free text for additional refs. |
49
55
  | **`experimental.session.compacting`** | Pushes hints, curated context, active workflows, and the last curator report into the compaction prompt so they survive transcript shrinking. |
50
56
  | **`shell.env`** | Exposes `AKM_STASH_DIR`, `AKM_PROJECT`, and `AKM_PLUGIN_VERSION` to shell tools so plain `akm` calls inherit the right context. |
51
- | **`stop`** / **`session.idle`** / **`session.compacted`** / **`session.deleted`** | Flushes the per-session buffer into a `memory:opencode-session-YYYYMMDD-<sid>` memory so every meaningful session contributes durable context for future searches. Requires at least two observations before persisting. |
57
+ | **`stop`** / **`session.idle`** / **`session.compacted`** / **`session.deleted`** | Flushes the per-session buffer into a `memory:opencode-session-YYYYMMDD-<sid>` memory so every meaningful session contributes durable context for future searches. Requires at least two observations before persisting. When `AKM_INDEX_ON_SESSION_END=1`, the hook follows a successful flush with `akm index` so upstream inference/graph passes run immediately. |
52
58
 
53
59
  ### Environment overrides
54
60
 
@@ -58,12 +64,18 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
58
64
  | `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
59
65
  | `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
60
66
  | `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
67
+ | `AKM_INDEX_ON_SESSION_END` | `0` | Set to `1` to run `akm index` after a session-end memory is captured. |
61
68
  | `AKM_CURATE_LIMIT` | `5` | Max curated results injected into context per prompt. |
62
69
  | `AKM_CURATE_MIN_CHARS` | `16` | Minimum prompt length before curation runs. |
63
70
  | `AKM_CURATE_TIMEOUT` | `8` | Wall-clock seconds for `akm` invocations inside hooks. |
71
+ | `AKM_CONTEXT_BUDGET_CHARS` | `4000` | Max total characters injected into system/compaction context for a single turn. |
64
72
  | `AKM_CURATOR_CONTEXT_MAX_CHARS` | `4000` | Max cached curator-report characters re-injected into system/compaction context; the full report is still persisted as memory. |
65
73
  | `AKM_MEMORY_CHECKPOINT_EVERY` | `8` | Number of successful asset-touching tool calls between mid-session checkpoint memories. |
66
74
  | `AKM_RETROSPECTIVE_FEEDBACK_PATTERN` | `\b(thanks|perfect|worked)\b` | Case-insensitive regex used for lightweight positive retrospective feedback on the most recent refs. |
75
+ | `AKM_RETROSPECTIVE_NEGATIVE_PATTERN` | `\b(wrong|failed|broken|didn't work|did not work|bad)\b` | Case-insensitive regex used for negative retrospective feedback signals. |
76
+ | `AKM_SCOPE_KEYS` | `user,agent,run,channel` | Comma-separated list of scope fields to attach on every `akm_remember`, `akm_curate`, and `akm_feedback` call. Remove a key to opt out of that dimension. |
77
+ | `AKM_PENDING_PROPOSAL_TIMEOUT` | `2` | Seconds allowed for lightweight pending-proposal count checks during context injection. |
78
+ | `AKM_PACKAGE_REF` | `akm-cli@latest` | Override the npm/bun package spec used for auto-install (e.g. pin to `akm-cli@0.7.0` in CI). |
67
79
 
68
80
  ### Curator agent
69
81
 
@@ -74,6 +86,27 @@ memories, parent-session context, live stash), produces a prioritized action
74
86
  list, and persists its latest report as `memory:akm-curator-YYYYMMDD-<sid>` so
75
87
  future curator runs can build on it.
76
88
 
89
+ ## AKM v1 workflows
90
+
91
+ The plugin injects a concise AKM workflow instruction pack into context so agents:
92
+
93
+ - search or curate before writing from scratch;
94
+ - show an asset before relying on it;
95
+ - record feedback after the result is known;
96
+ - treat `lesson:*` as first-class durable assets;
97
+ - treat proposed-quality assets as uncurated until accepted;
98
+ - use `akm_help` to route `proposal`, `distill`, `reflect`, and `propose` CLI workflows;
99
+ - require explicit user approval before proposal acceptance/rejection, push saves, source removal, CLI upgrades, update-all, or vault value access.
100
+
101
+ The package also ships OpenCode command docs for common workflows:
102
+
103
+ - `/akm-review-proposals`
104
+ - `/akm-distill-lesson`
105
+ - `/akm-reflect-on-failure`
106
+ - `/akm-propose-asset`
107
+ - `/akm-evolve-session`
108
+ - `/akm-workflow-status`
109
+
77
110
  ### Registry discovery
78
111
 
79
112
  Search registries with `akm_search` using `source: "registry"` or `source: "both"`. Registry hits include `id`, `installRef`, and `action` fields. Use `installRef` when feeding a result into `akm add` (run via `akm_help` topic="add" or directly through bash); registry-specific IDs are not installable refs.
@@ -142,9 +175,11 @@ stash/
142
175
  ├── agents/ # markdown files
143
176
  ├── knowledge/ # markdown files
144
177
  ├── memories/ # markdown memory files (akm remember)
178
+ ├── lessons/ # first-class durable learnings (lesson:<name>) — produced by akm distill, accepted via akm_proposal accept
145
179
  ├── workflows/ # multi-step procedures (workflow:<name>)
146
180
  ├── vaults/ # .env secret stores (vault:<name>) — values never surface through structured output
147
- └── wikis/ # per-wiki directories <name>/{schema,index,log}.md + raw/ + pages
181
+ ├── wikis/ # per-wiki directories <name>/{schema,index,log}.md + raw/ + pages
182
+ └── .akm/proposals/ # v0.7.0 proposal queue — drafts that never leak into search or commits
148
183
  ```
149
184
 
150
185
  ## Vaults
@@ -20,7 +20,9 @@ Inputs you should inspect:
20
20
  Signals to act on:
21
21
  - Hot refs: assets repeatedly appearing in positive tool outcomes. Call akm_feedback <ref> positive --note "curator: consistently useful" to reinforce.
22
22
  - Cold refs: assets tied to failures or user complaints. Record akm_feedback <ref> negative --note "<excerpt>" and open the asset for review.
23
+ - Lesson candidates: repeated memories or failures that should become a proposed lesson. Use akm_help topic="distill" before raw CLI distill commands.
23
24
  - Missing coverage: recurring user prompts with no matching asset. Draft a new skill, command, knowledge doc, wiki page, or workflow in the working stash and reindex via the akm CLI (see akm_help topic="reindex").
25
+ - Pending proposals: list or diff them via akm_help topic="proposal" and recommend accept, reject, or revise. Never accept or reject without explicit user approval.
24
26
  - Duplicates / drift: near-identical descriptions or overlapping responsibilities. Propose a consolidation.
25
27
  - Stale memories: session summaries that never get recalled. Propose removal (see akm_help topic="remove") once distilled into a durable knowledge doc or wiki page.
26
28
  - Wiki hygiene: for each wiki returned by akm_wiki list, run akm_wiki lint <name> and report orphans, broken xrefs, uncited raws, and stale indexes as fix candidates.
@@ -42,9 +44,15 @@ Output shape: end every run with a markdown report that has these sections:
42
44
  ## Cold assets (investigate)
43
45
  - <ref> — failure signal — proposed fix
44
46
 
47
+ ## Lesson candidates
48
+ - <theme> — evidence refs — distill or reflect command to run
49
+
45
50
  ## Coverage gaps
46
51
  - <theme> — proposed asset (type, name, one-line description)
47
52
 
53
+ ## Pending proposals
54
+ - <proposal id> — summary — accept/reject/revise recommendation
55
+
48
56
  ## Duplicates / drift
49
57
  - <ref a> vs <ref b> — consolidation proposal
50
58
 
@@ -0,0 +1,7 @@
1
+ Distill repeated evidence into a proposed lesson.
2
+
3
+ 1. Search or curate for candidate refs.
4
+ 2. Show the strongest evidence refs.
5
+ 3. Call `akm_help` with `topic: "distill"`.
6
+ 4. Run `akm distill <ref>` only after the evidence is clear.
7
+ 5. Report the resulting proposal and remind the user that proposed assets are not curated until accepted.
@@ -0,0 +1,15 @@
1
+ Run a full AKM session evolution review.
2
+
3
+ End with these sections:
4
+
5
+ ## Hot assets
6
+ ## Cold assets
7
+ ## Lesson candidates
8
+ ## Coverage gaps
9
+ ## Pending proposals
10
+ ## Duplicates / drift
11
+ ## Wiki health
12
+ ## Workflow health
13
+ ## Housekeeping
14
+
15
+ Use `akm_help` before long-tail raw CLI commands, and never accept or reject proposals or run risky AKM commands without explicit user approval.
@@ -0,0 +1,8 @@
1
+ Create a proposed AKM asset for a coverage gap.
2
+
3
+ 1. Search or curate first.
4
+ 2. Confirm the gap is real.
5
+ 3. Call `akm_help` with `topic: "propose"`.
6
+ 4. Choose the smallest suitable asset type.
7
+ 5. Run `akm propose <type> <name> --task "..."`.
8
+ 6. Show proposal review commands and remind the user that proposed assets are not curated until accepted.
@@ -0,0 +1,7 @@
1
+ Reflect on an existing AKM asset after failure or drift.
2
+
3
+ 1. Identify the failure evidence and touched refs.
4
+ 2. Record negative feedback when justified.
5
+ 3. Call `akm_help` with `topic: "reflect"`.
6
+ 4. Run `akm reflect <ref> --task "..."`.
7
+ 5. List resulting pending proposals and do not accept or reject them without explicit user approval.
@@ -0,0 +1,7 @@
1
+ Review pending AKM proposals safely.
2
+
3
+ 1. Call `akm_help` with `topic: "proposal"`.
4
+ 2. Run `akm proposal list --status pending --format json`.
5
+ 3. For relevant proposals, run `akm proposal show <id>` and `akm proposal diff <id>`.
6
+ 4. Summarize the likely accept, reject, or revise outcome.
7
+ 5. Do not run `akm proposal accept` or `akm proposal reject` unless the user explicitly approves the exact command.
@@ -0,0 +1,7 @@
1
+ Inspect the current AKM workflow state safely.
2
+
3
+ 1. List active workflow runs.
4
+ 2. Show blocked or failed steps.
5
+ 3. Identify the next evidence needed.
6
+ 4. Recommend only safe next actions.
7
+ 5. Do not bypass review, approval, or verification requirements.
package/index.ts CHANGED
@@ -5,7 +5,7 @@ import path from "node:path"
5
5
  import { fileURLToPath } from "node:url"
6
6
 
7
7
  let resolvedAkmCommand = "akm"
8
- const autoInstallPackageRef = "akm-cli@latest"
8
+ const autoInstallPackageRef = process.env.AKM_PACKAGE_REF ?? "akm-cli@latest"
9
9
  const moduleDir = path.dirname(fileURLToPath(import.meta.url))
10
10
  const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/
11
11
 
@@ -13,6 +13,7 @@ const AKM_AUTO_FEEDBACK = (process.env.AKM_AUTO_FEEDBACK ?? "1") !== "0"
13
13
  const AKM_AUTO_MEMORY = (process.env.AKM_AUTO_MEMORY ?? "1") !== "0"
14
14
  const AKM_AUTO_CURATE = (process.env.AKM_AUTO_CURATE ?? "1") !== "0"
15
15
  const AKM_AUTO_HINTS = (process.env.AKM_AUTO_HINTS ?? "1") !== "0"
16
+ const AKM_PENDING_PROPOSAL_TIMEOUT_MS = Math.max(500, (Number(process.env.AKM_PENDING_PROPOSAL_TIMEOUT ?? "2") || 2) * 1_000)
16
17
  const AKM_CURATE_LIMIT = Math.max(1, Number(process.env.AKM_CURATE_LIMIT ?? "5") || 5)
17
18
  const AKM_CURATE_MIN_CHARS = Math.max(1, Number(process.env.AKM_CURATE_MIN_CHARS ?? "16") || 16)
18
19
  const AKM_CURATE_TIMEOUT_MS = Math.max(1_000, (Number(process.env.AKM_CURATE_TIMEOUT ?? "8") || 8) * 1_000)
@@ -21,6 +22,8 @@ const AKM_CURATOR_CONTEXT_MAX_CHARS = Math.max(500, Number(process.env.AKM_CURAT
21
22
  const SESSION_DATE_TAG_LENGTH = 8
22
23
  const CHECKPOINT_DATE_TAG_LENGTH = 15
23
24
  const AKM_RETROSPECTIVE_FEEDBACK_RE = createRetrospectiveFeedbackRegex()
25
+ const AKM_RETROSPECTIVE_NEGATIVE_RE = createRetrospectiveNegativeRegex()
26
+ const AKM_EXPLICIT_CORRECTION_RE = createExplicitCorrectionRegex()
24
27
  const PLUGIN_VERSION = readPackageVersion()
25
28
 
26
29
  // Per-session state that drives the compound-engineering loop.
@@ -46,12 +49,30 @@ type SessionBufferEntry = {
46
49
  const sessionBuffer = new Map<string, SessionBufferEntry[]>()
47
50
  const sessionFinalMemoryCaptured = new Set<string>()
48
51
  const sessionSuccessfulAssetTouchCount = new Map<string, number>()
52
+ const pendingProposalSummaryCache = new Map<string, { count: number; expiresAt: number; unsupported?: boolean }>()
53
+ const retrospectiveState = new Map<string, { recentRefs: string[]; lastNegativeSignalAt?: number }>()
49
54
  let cachedAkmStashDir: string | undefined
50
55
 
51
56
  // Asset-ref grammar matching the stash skill: [origin//]type:name.
52
57
  // We validate normalized tokens individually instead of running a global regex
53
58
  // over arbitrary tool output to keep extraction predictable and ReDoS-safe.
54
- const AKM_REF_PATTERN = /^(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|script|workflow|vault|wiki):[A-Za-z0-9._/\-]+$/
59
+ const AKM_REF_PATTERN = /^(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|script|workflow|vault|wiki|lesson):[A-Za-z0-9._/\-]+$/
60
+ const PROPOSED_QUALITY_WARNING = "Do not treat proposed assets as curated until accepted."
61
+ const AKM_WORKFLOW_INSTRUCTION = [
62
+ "# AKM workflow (v0.7.0)",
63
+ "",
64
+ "Use AKM as a reusable knowledge and workflow stash.",
65
+ "",
66
+ "Before writing from scratch:",
67
+ "1. Use `akm_search` or `akm_curate`.",
68
+ "2. Use `akm_show <ref>` before relying on an asset.",
69
+ "3. Record `akm_feedback` after the result is known.",
70
+ "4. Use the dedicated v0.7.0 tools for the proposal flow: `akm_proposal` (list/show/diff/accept/reject), `akm_reflect`, `akm_propose`, and `akm_distill`. Fall back to `akm_help` for any verb without a dedicated tool.",
71
+ "5. Treat `lesson:*` as first-class durable learning assets — they are produced by `akm_distill <ref>` as proposals and accepted via `akm_proposal action=accept`.",
72
+ "6. Run `akm_setup` once on a fresh machine to detect installed agent CLIs and persist `agent.default`. Reflect/propose require it.",
73
+ `7. ${PROPOSED_QUALITY_WARNING}`,
74
+ "8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
75
+ ].join("\n")
55
76
 
56
77
  function readPackageVersion(): string {
57
78
  try {
@@ -72,6 +93,19 @@ function createRetrospectiveFeedbackRegex(): RegExp {
72
93
  }
73
94
  }
74
95
 
96
+ function createRetrospectiveNegativeRegex(): RegExp {
97
+ const pattern = process.env.AKM_RETROSPECTIVE_NEGATIVE_PATTERN ?? "\\b(wrong|failed|broken|didn't work|did not work|bad)\\b"
98
+ try {
99
+ return new RegExp(pattern, "i")
100
+ } catch {
101
+ return /\b(wrong|failed|broken|didn't work|did not work|bad)\b/i
102
+ }
103
+ }
104
+
105
+ function createExplicitCorrectionRegex(): RegExp {
106
+ return /\b(this was wrong|that was wrong|you were wrong|incorrect|not correct)\b/i
107
+ }
108
+
75
109
  const CURATOR_AGENT_PROMPT_FALLBACK = `You are the AKM curator — a compound-engineering agent that keeps the user's AKM stash improving every time the main agent finishes a task.
76
110
 
77
111
  Inputs you should inspect:
@@ -83,7 +117,9 @@ Inputs you should inspect:
83
117
  Signals to act on:
84
118
  - Hot refs: assets repeatedly appearing in positive tool outcomes. Call akm_feedback <ref> positive --note "curator: consistently useful" to reinforce.
85
119
  - Cold refs: assets tied to failures or user complaints. Record akm_feedback <ref> negative --note "<excerpt>" and open the asset for review.
120
+ - Lesson candidates: repeated memories or failures that should become a proposed lesson. Use akm_help topic="distill" before raw CLI distill commands.
86
121
  - Missing coverage: recurring user prompts with no matching asset. Draft a new skill, command, knowledge doc, wiki page, or workflow in the working stash and reindex via the akm CLI (see akm_help topic="reindex").
122
+ - Pending proposals: list or diff them via akm_help topic="proposal" and recommend accept, reject, or revise. Never accept or reject without explicit user approval.
87
123
  - Duplicates / drift: near-identical descriptions or overlapping responsibilities. Propose a consolidation.
88
124
  - Stale memories: session summaries that never get recalled. Propose removal (see akm_help topic="remove") once distilled into a durable knowledge doc or wiki page.
89
125
  - Wiki hygiene: for each wiki returned by akm_wiki list, run akm_wiki lint <name> and report orphans, broken xrefs, uncited raws, and stale indexes as fix candidates.
@@ -105,9 +141,15 @@ Output shape: end every run with a markdown report that has these sections:
105
141
  ## Cold assets (investigate)
106
142
  - <ref> — failure signal — proposed fix
107
143
 
144
+ ## Lesson candidates
145
+ - <theme> — evidence refs — distill or reflect command to run
146
+
108
147
  ## Coverage gaps
109
148
  - <theme> — proposed asset (type, name, one-line description)
110
149
 
150
+ ## Pending proposals
151
+ - <proposal id> — summary — accept/reject/revise recommendation
152
+
111
153
  ## Duplicates / drift
112
154
  - <ref a> vs <ref b> — consolidation proposal
113
155
 
@@ -207,6 +249,10 @@ function nowIso(): string {
207
249
  return new Date().toISOString()
208
250
  }
209
251
 
252
+ function shouldIndexOnSessionEnd(): boolean {
253
+ return (process.env.AKM_INDEX_ON_SESSION_END ?? "0") === "1"
254
+ }
255
+
210
256
  function buildDateTag(options?: { includeTime?: boolean }): string {
211
257
  const compactIso = new Date().toISOString().replace(/[-:]/g, "")
212
258
  return compactIso.slice(0, options?.includeTime ? CHECKPOINT_DATE_TAG_LENGTH : SESSION_DATE_TAG_LENGTH)
@@ -257,28 +303,87 @@ function runCliSyncRaw(args: string[], timeoutMs: number): { ok: true; stdout: s
257
303
  }
258
304
  }
259
305
 
260
- function runCurateForPrompt(text: string): string | null {
261
- if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
262
- const result = runCliSyncRaw(
263
- [
264
- "--for-agent",
265
- "--format",
266
- "text",
267
- "--detail",
268
- "summary",
269
- "-q",
270
- "curate",
271
- text,
272
- "--limit",
273
- String(AKM_CURATE_LIMIT),
274
- ],
275
- AKM_CURATE_TIMEOUT_MS,
306
+ function appendRunScopeArgs(args: string[], sessionID: string | undefined): string[] {
307
+ return sessionID ? [...args, "--run", sessionID] : args
308
+ }
309
+
310
+ function getScopeFields(): Array<"user" | "agent" | "run" | "channel"> {
311
+ const configured = process.env.AKM_SCOPE_KEYS?.split(",").map((part) => part.trim()).filter(Boolean)
312
+ const values = configured && configured.length > 0 ? configured : ["user", "agent", "run", "channel"]
313
+ return values.filter((value): value is "user" | "agent" | "run" | "channel" =>
314
+ value === "user" || value === "agent" || value === "run" || value === "channel",
276
315
  )
316
+ }
317
+
318
+ function buildScopedArgs(context: Record<string, unknown> | undefined): string[] {
319
+ if (!context) return []
320
+ const scopeFields = new Set(getScopeFields())
321
+ const args: string[] = []
322
+ const user = typeof context.userID === "string"
323
+ ? context.userID
324
+ : typeof context.user === "string"
325
+ ? context.user
326
+ : undefined
327
+ const agent = typeof context.agent === "string" ? context.agent : undefined
328
+ const run = typeof context.sessionID === "string" ? context.sessionID : typeof context.run === "string" ? context.run : undefined
329
+ const channel = typeof context.channel === "string"
330
+ ? context.channel
331
+ : typeof context.variant === "string"
332
+ ? context.variant
333
+ : undefined
334
+
335
+ if (scopeFields.has("user") && user) args.push("--user", user)
336
+ if (scopeFields.has("agent") && agent) args.push("--agent", agent)
337
+ if (scopeFields.has("run") && run) args.push("--run", run)
338
+ if (scopeFields.has("channel") && channel) args.push("--channel", channel)
339
+ return args
340
+ }
341
+
342
+ function runCurate(args: string[]): string | null {
343
+ const result = runCliSyncRaw(args, AKM_CURATE_TIMEOUT_MS)
277
344
  if (!result.ok) return null
278
345
  const body = result.stdout.trim()
279
346
  return body || null
280
347
  }
281
348
 
349
+ function runCurateForPrompt(text: string, sessionID?: string): string | null {
350
+ if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
351
+ return runCurate(
352
+ appendRunScopeArgs(
353
+ [
354
+ "--detail",
355
+ "agent",
356
+ "--format",
357
+ "text",
358
+ "-q",
359
+ "curate",
360
+ text,
361
+ "--limit",
362
+ String(AKM_CURATE_LIMIT),
363
+ ],
364
+ sessionID,
365
+ ),
366
+ )
367
+ }
368
+
369
+ function runCurateForSession(sessionID: string): string | null {
370
+ return runCurate(
371
+ appendRunScopeArgs(
372
+ [
373
+ "--detail",
374
+ "agent",
375
+ "--format",
376
+ "text",
377
+ "-q",
378
+ "curate",
379
+ "--limit",
380
+ String(AKM_CURATE_LIMIT),
381
+ ],
382
+ sessionID,
383
+ ),
384
+ )
385
+ }
386
+
282
387
  function runHintsForSession(): string | null {
283
388
  const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
284
389
  if (!result.ok) return null
@@ -341,6 +446,17 @@ function formatCuratorReportContext(report: string): string {
341
446
  return `# AKM curator report\n${report}`
342
447
  }
343
448
 
449
+ function formatPendingProposalContext(count: number): string {
450
+ const summaryLine = count === 1 ? "There is 1 pending AKM proposal." : `There are ${count} pending AKM proposals.`
451
+ return [
452
+ "# AKM pending proposals",
453
+ "",
454
+ summaryLine,
455
+ "Use `/akm-review-proposals` or `akm_help topic=proposal` to review them.",
456
+ PROPOSED_QUALITY_WARNING,
457
+ ].join("\n")
458
+ }
459
+
344
460
  function summarizeCuratorReportForContext(report: string): string {
345
461
  if (report.length <= AKM_CURATOR_CONTEXT_MAX_CHARS) return report
346
462
  return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
@@ -384,6 +500,139 @@ function warmIndexInBackground(): void {
384
500
  }
385
501
  }
386
502
 
503
+ function safeJsonParse<T>(raw: string): T | undefined {
504
+ try {
505
+ return JSON.parse(raw) as T
506
+ } catch {
507
+ return undefined
508
+ }
509
+ }
510
+
511
+ function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventType: string, extra: Record<string, unknown>) {
512
+ return writePluginLog(client, level, eventType, {
513
+ subsystem: "workflow-compliance",
514
+ eventType,
515
+ pluginVersion: PLUGIN_VERSION,
516
+ ...extra,
517
+ })
518
+ }
519
+
520
+ function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
521
+ if (!sessionID || refs.length === 0) return
522
+ const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
523
+ state.recentRefs = [...new Set([...state.recentRefs, ...refs])].slice(-8)
524
+ retrospectiveState.set(sessionID, state)
525
+ }
526
+
527
+ async function getPendingProposalCount(client: LogCapableClient, sessionID?: string): Promise<{ count: number; unsupported?: boolean }> {
528
+ const cacheKey = sessionID ?? "global"
529
+ const cached = pendingProposalSummaryCache.get(cacheKey)
530
+ if (cached && cached.expiresAt > Date.now()) return cached
531
+
532
+ const command = resolveAkmCommand()
533
+ if (typeof command !== "string") return { count: 0, unsupported: true }
534
+ try {
535
+ const stdout = execFileSync(command, ["proposal", "list", "--status", "pending", "--format", "json"], {
536
+ encoding: "utf8",
537
+ timeout: AKM_PENDING_PROPOSAL_TIMEOUT_MS,
538
+ })
539
+ const parsed = safeJsonParse<{ proposals?: unknown[]; hits?: unknown[] }>(stdout)
540
+ const count = Array.isArray(parsed?.proposals) ? parsed.proposals.length : Array.isArray(parsed?.hits) ? parsed.hits.length : 0
541
+ const result = { count, expiresAt: Date.now() + 60_000 }
542
+ pendingProposalSummaryCache.set(cacheKey, result)
543
+ return result
544
+ } catch (error: unknown) {
545
+ const message = formatCliError(error)
546
+ const unsupported = /unknown|unsupported|not found|invalid/i.test(message)
547
+ const result = { count: 0, unsupported, expiresAt: Date.now() + 60_000 }
548
+ pendingProposalSummaryCache.set(cacheKey, result)
549
+ return result
550
+ }
551
+ }
552
+
553
+ async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID: string | undefined, text: string) {
554
+ if (!sessionID) return
555
+ const state = retrospectiveState.get(sessionID)
556
+ const recentRefs = state?.recentRefs ?? []
557
+ if (recentRefs.length === 0) return
558
+
559
+ const explicitCorrection = AKM_EXPLICIT_CORRECTION_RE.test(text)
560
+ const negative = explicitCorrection || AKM_RETROSPECTIVE_NEGATIVE_RE.test(text)
561
+ if (!negative) return
562
+
563
+ if (!explicitCorrection) {
564
+ const now = Date.now()
565
+ if (!state?.lastNegativeSignalAt || now - state.lastNegativeSignalAt > 2 * 60 * 1000) {
566
+ retrospectiveState.set(sessionID, { recentRefs, lastNegativeSignalAt: now })
567
+ return
568
+ }
569
+ }
570
+
571
+ const targetRef = recentRefs[recentRefs.length - 1]
572
+ const raw = await runCli(client, ["feedback", targetRef, "--negative", "--note", text.slice(0, 280)], {
573
+ toolName: "akm_feedback",
574
+ sessionID,
575
+ })
576
+ const parsed = safeJsonParse<{ ok?: boolean }>(raw)
577
+ if (parsed?.ok !== false) {
578
+ await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
579
+ sessionID,
580
+ toolName: "akm_feedback",
581
+ assetRef: targetRef,
582
+ outcome: "success",
583
+ reason: explicitCorrection ? "explicit correction" : "negative retrospective signal",
584
+ })
585
+ }
586
+ retrospectiveState.set(sessionID, { recentRefs })
587
+ }
588
+
589
+ type RiskyCommandAssessment = {
590
+ category: string
591
+ reason: string
592
+ approval: string
593
+ }
594
+
595
+ function assessRiskyAkmCommand(command: string): RiskyCommandAssessment | undefined {
596
+ const args = splitArguments(command)
597
+ const akmIndex = args.findIndex((arg) => arg === "akm" || arg.endsWith("/akm") || arg.endsWith("\\akm.exe"))
598
+ if (akmIndex === -1) return undefined
599
+ const tokens = args.slice(akmIndex + 1)
600
+ if (tokens[0] === "proposal" && tokens[1] === "accept") {
601
+ return { category: "proposal-accept", reason: "Proposal acceptance changes curated AKM content.", approval: "Ask the user to approve `akm proposal accept <id>`." }
602
+ }
603
+ if (tokens[0] === "proposal" && tokens[1] === "reject") {
604
+ return { category: "proposal-reject", reason: "Proposal rejection is a durable curation decision.", approval: "Ask the user to approve `akm proposal reject <id> --reason \"...\"`." }
605
+ }
606
+ if (tokens[0] === "save" && tokens.includes("--push")) {
607
+ return { category: "save-push", reason: "Pushing stash changes must be explicitly approved.", approval: "Ask the user to approve `akm save --push`." }
608
+ }
609
+ if (tokens[0] === "remove") {
610
+ return { category: "remove", reason: "Removing AKM sources is destructive.", approval: "Ask the user to approve the exact `akm remove ...` command." }
611
+ }
612
+ if (tokens[0] === "vault" && ["show", "load", "set", "unset"].includes(tokens[1] ?? "")) {
613
+ return { category: `vault-${tokens[1]}`, reason: "Vault access or mutation is sensitive.", approval: `Ask the user to approve the exact \`akm vault ${tokens[1]} ...\` command.` }
614
+ }
615
+ if (tokens[0] === "config" && tokens[1] === "set" && (tokens[2]?.startsWith("llm.features.") ?? false)) {
616
+ return { category: "config-llm-features", reason: "Changing AKM LLM feature flags alters autonomous behavior.", approval: "Ask the user to approve the exact `akm config set llm.features.* ...` command." }
617
+ }
618
+ if (tokens[0] === "update" && tokens.includes("--all")) {
619
+ return { category: "update-all", reason: "Updating all AKM kits changes many assets at once.", approval: "Ask the user to approve `akm update --all`." }
620
+ }
621
+ if (tokens[0] === "upgrade") {
622
+ return { category: "upgrade", reason: "Upgrading the AKM CLI changes the toolchain.", approval: "Ask the user to approve the exact `akm upgrade` command." }
623
+ }
624
+ return undefined
625
+ }
626
+
627
+ function blockedCommandMessage(command: string, assessment: RiskyCommandAssessment): string {
628
+ return [
629
+ `Blocked risky AKM command: ${command}`,
630
+ assessment.reason,
631
+ assessment.approval,
632
+ "Retry only after explicit user approval in this conversation.",
633
+ ].join("\n")
634
+ }
635
+
387
636
  function queueFeedback(
388
637
  client: LogCapableClient,
389
638
  ref: string,
@@ -545,6 +794,25 @@ function maybeCheckpointSessionMemory(sessionID: string): string | null {
545
794
  return captured
546
795
  }
547
796
 
797
+ async function maybeIndexSessionMemory(
798
+ client: LogCapableClient,
799
+ sessionID: string,
800
+ reason: string,
801
+ ref: string,
802
+ ): Promise<void> {
803
+ if (!shouldIndexOnSessionEnd()) return
804
+ const result = runCliSyncRaw(["index"], AKM_CURATE_TIMEOUT_MS)
805
+ if (result.ok) return
806
+ await writePluginLog(client, "warn", "AKM session indexing failed", {
807
+ subsystem: "memory",
808
+ actor: "system",
809
+ sessionID,
810
+ reason,
811
+ ref,
812
+ error: result.error,
813
+ })
814
+ }
815
+
548
816
  const AKM_REF_EDGE_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?", "(", ")", "[", "]", "{", "}", "'", "\"", "`"])
549
817
 
550
818
  function normalizeExtractedRef(ref: string): string {
@@ -612,10 +880,47 @@ const AKM_HINTS_PREFIX = [
612
880
  "# AKM is available in this session",
613
881
  "",
614
882
  "You have an AKM stash on this machine. Before writing anything from scratch, call `akm_search` or `akm_curate` to see if the stash already covers it. Record `akm_feedback <ref> positive|negative` whenever an asset materially helps or misses, and use `akm_remember` to persist durable learnings so future sessions inherit them.",
883
+ "",
884
+ AKM_WORKFLOW_INSTRUCTION,
615
885
  ].join("\n")
616
886
 
617
887
  const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
618
888
  const 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."
889
+ const AKM_CONTEXT_TRUNCATED_MARKER = "\n\n[truncated for context]"
890
+
891
+ function getContextBudgetChars(): number {
892
+ const parsed = Number(process.env.AKM_CONTEXT_BUDGET_CHARS)
893
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000
894
+ }
895
+
896
+ function truncateContextBlock(block: string, maxChars: number): string {
897
+ if (block.length <= maxChars) return block
898
+ if (maxChars <= AKM_CONTEXT_TRUNCATED_MARKER.length) return block.slice(0, maxChars)
899
+ return `${block.slice(0, maxChars - AKM_CONTEXT_TRUNCATED_MARKER.length)}${AKM_CONTEXT_TRUNCATED_MARKER}`
900
+ }
901
+
902
+ function applyContextBudget(blocks: string[]): string[] {
903
+ const budget = getContextBudgetChars()
904
+ const injected: string[] = []
905
+ let remaining = budget
906
+ for (const block of blocks) {
907
+ if (!block) continue
908
+ // The host effectively concatenates injected blocks into one prompt body;
909
+ // we budget for a single newline separator between adjacent blocks.
910
+ const separatorCost = injected.length > 0 ? 1 : 0
911
+ if (remaining <= separatorCost) break
912
+ const allowed = remaining - separatorCost
913
+ if (block.length <= allowed) {
914
+ injected.push(block)
915
+ remaining -= separatorCost + block.length
916
+ continue
917
+ }
918
+ const truncated = truncateContextBlock(block, allowed)
919
+ if (truncated) injected.push(truncated)
920
+ break
921
+ }
922
+ return injected
923
+ }
619
924
 
620
925
  // Curated quick-reference for the long-tail of `akm` CLI verbs that no longer
621
926
  // have a dedicated tool wrapper. Surfaced through akm_help so agents can
@@ -628,6 +933,35 @@ type AkmHelpEntry = {
628
933
  }
629
934
 
630
935
  const AKM_HELP_QUICK_REFERENCE: readonly AkmHelpEntry[] = [
936
+ {
937
+ task: "Review pending proposals and decide whether to accept, reject, or revise them",
938
+ command: "akm proposal list --status pending --format json; akm proposal show <id>; akm proposal diff <id>",
939
+ notes: "Accept/reject requires explicit user approval.",
940
+ keywords: ["proposal", "review proposals", "pending proposals", "accept proposal", "reject proposal"],
941
+ },
942
+ {
943
+ task: "Distill repeated evidence into a proposed lesson",
944
+ command: "akm distill <ref>",
945
+ notes: "Distill creates a proposal; proposed assets are not curated until accepted.",
946
+ keywords: ["distill", "lesson", "proposed lesson"],
947
+ },
948
+ {
949
+ task: "Reflect on an existing asset after failure or drift",
950
+ command: "akm reflect <ref> --task \"...\"",
951
+ keywords: ["reflect", "drift", "failure"],
952
+ },
953
+ {
954
+ task: "Create a proposed asset for a coverage gap",
955
+ command: "akm propose <type> <name> --task \"...\"",
956
+ notes: PROPOSED_QUALITY_WARNING,
957
+ keywords: ["propose", "coverage gap", "proposed asset"],
958
+ },
959
+ {
960
+ task: "Search including proposed-quality assets",
961
+ command: "akm search <query> --include-proposed",
962
+ notes: PROPOSED_QUALITY_WARNING,
963
+ keywords: ["include-proposed", "proposed quality", "lesson"],
964
+ },
631
965
  {
632
966
  task: "Install a kit or register an external source (npm, GitHub, git, URL, local dir)",
633
967
  command: "akm add <package-ref> [--name <n>] [--type wiki] [--writable] [--trust] [--provider <p>] [--max-pages N] [--max-depth N]",
@@ -977,6 +1311,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
977
1311
  }
978
1312
 
979
1313
  const fullArgs = args.includes("--format") ? [...args] : [...args, "--format", "json"]
1314
+ const proposalId = args[0] === "proposal" && typeof args[2] === "string" ? args[2] : null
980
1315
 
981
1316
  try {
982
1317
  const stdout = execFileSync(command, fullArgs, {
@@ -994,6 +1329,59 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
994
1329
  stdout,
995
1330
  stderr: "",
996
1331
  })
1332
+ const parsed = safeJsonParse<SearchResponse>(stdout)
1333
+ const refs = args[0] === "search" || args[0] === "curate"
1334
+ ? [...new Set([...(parsed?.hits?.flatMap((hit) => hit.ref ? [hit.ref] : []) ?? []), ...extractRefsFromText(stdout)])]
1335
+ : extractRefsFromText(stdout)
1336
+ noteRecentRefs(meta.sessionID, refs)
1337
+ if (meta.toolName === "akm_search") {
1338
+ await emitWorkflowTelemetry(client, "info", "akm.search.invoked", {
1339
+ sessionID: meta.sessionID,
1340
+ toolName: meta.toolName,
1341
+ assetRef: refs[0] ?? null,
1342
+ proposalId: null,
1343
+ outcome: "success",
1344
+ directory: meta.directory,
1345
+ })
1346
+ }
1347
+ if (meta.toolName === "akm_curate") {
1348
+ await emitWorkflowTelemetry(client, "info", "akm.curate.invoked", {
1349
+ sessionID: meta.sessionID,
1350
+ toolName: meta.toolName,
1351
+ assetRef: refs[0] ?? null,
1352
+ proposalId: null,
1353
+ outcome: "success",
1354
+ directory: meta.directory,
1355
+ })
1356
+ }
1357
+ if (meta.toolName === "akm_show") {
1358
+ await emitWorkflowTelemetry(client, "info", "akm.show.invoked", {
1359
+ sessionID: meta.sessionID,
1360
+ toolName: meta.toolName,
1361
+ assetRef: args[1] ?? refs[0] ?? null,
1362
+ proposalId,
1363
+ outcome: "success",
1364
+ directory: meta.directory,
1365
+ })
1366
+ }
1367
+ if (args[0] === "proposal" && ["show", "diff"].includes(args[1] ?? "")) {
1368
+ await emitWorkflowTelemetry(client, "info", "akm.proposal.reviewed", {
1369
+ sessionID: meta.sessionID,
1370
+ toolName: meta.toolName,
1371
+ proposalId,
1372
+ outcome: "requested",
1373
+ directory: meta.directory,
1374
+ })
1375
+ }
1376
+ if (args[0] === "proposal" && ["accept", "reject"].includes(args[1] ?? "")) {
1377
+ await emitWorkflowTelemetry(client, "info", args[1] === "accept" ? "akm.proposal.accept.requested" : "akm.proposal.reject.requested", {
1378
+ sessionID: meta.sessionID,
1379
+ toolName: meta.toolName,
1380
+ proposalId,
1381
+ outcome: "requested",
1382
+ directory: meta.directory,
1383
+ })
1384
+ }
997
1385
  return stdout
998
1386
  } catch (error: unknown) {
999
1387
  const message = formatCliError(error)
@@ -1008,6 +1396,16 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
1008
1396
  stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
1009
1397
  stderr: toLogString((error as { stderr?: unknown }).stderr) ?? message,
1010
1398
  })
1399
+ if (meta.toolName.startsWith("akm_")) {
1400
+ await emitWorkflowTelemetry(client, "warn", `${meta.toolName}.failed`, {
1401
+ sessionID: meta.sessionID,
1402
+ toolName: meta.toolName,
1403
+ proposalId,
1404
+ outcome: "error",
1405
+ reason: message,
1406
+ directory: meta.directory,
1407
+ })
1408
+ }
1011
1409
  return JSON.stringify({ ok: false, error: message })
1012
1410
  }
1013
1411
  }
@@ -1017,6 +1415,7 @@ type AssetType =
1017
1415
  | "agent"
1018
1416
  | "command"
1019
1417
  | "knowledge"
1418
+ | "lesson"
1020
1419
  | "memory"
1021
1420
  | "script"
1022
1421
  | "skill"
@@ -1028,6 +1427,7 @@ const ASSET_TYPES = [
1028
1427
  "agent",
1029
1428
  "command",
1030
1429
  "knowledge",
1430
+ "lesson",
1031
1431
  "memory",
1032
1432
  "script",
1033
1433
  "skill",
@@ -1095,6 +1495,7 @@ type SearchHit = {
1095
1495
  action?: string
1096
1496
  editHint?: string
1097
1497
  curated?: boolean
1498
+ quality?: string
1098
1499
  }
1099
1500
 
1100
1501
  type SearchResponse = {
@@ -1254,6 +1655,18 @@ function classifyToolFeedback(value: unknown): "positive" | "negative" | undefin
1254
1655
  return undefined
1255
1656
  }
1256
1657
 
1658
+ function withProposedWarnings(raw: string): string {
1659
+ const parsed = safeJsonParse<SearchResponse>(raw)
1660
+ if (!parsed) return raw
1661
+ const hasProposed = parsed.hits?.some((hit) => hit.quality === "proposed") ?? false
1662
+ if (!hasProposed) return raw
1663
+ const warnings = parsed.warnings ?? []
1664
+ return JSON.stringify({
1665
+ ...parsed,
1666
+ warnings: warnings.includes(PROPOSED_QUALITY_WARNING) ? warnings : [...warnings, PROPOSED_QUALITY_WARNING],
1667
+ })
1668
+ }
1669
+
1257
1670
  function truncateLogText(value: string, limit = 1_000): string {
1258
1671
  return value.length > limit ? `${value.slice(0, limit)}…` : value
1259
1672
  }
@@ -1496,6 +1909,7 @@ function createSearchArgs(input: {
1496
1909
  limit?: number
1497
1910
  source?: "local" | "stash" | "registry" | "both"
1498
1911
  defaultSource?: "local" | "stash" | "registry" | "both"
1912
+ includeProposed?: boolean
1499
1913
  }): string[] {
1500
1914
  const args = ["search", input.query]
1501
1915
  if (input.type) args.push("--type", input.type)
@@ -1505,6 +1919,7 @@ function createSearchArgs(input: {
1505
1919
  } else if (input.defaultSource) {
1506
1920
  args.push("--source", normalizeSearchSource(input.defaultSource))
1507
1921
  }
1922
+ if (input.includeProposed) args.push("--include-proposed")
1508
1923
  args.push("--detail", "normal")
1509
1924
  return args
1510
1925
  }
@@ -1552,7 +1967,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1552
1967
  if (type === "session.created" || type === "session.updated") {
1553
1968
  if (!sid) return
1554
1969
  if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
1555
- if (type === "session.created") warmIndexInBackground()
1970
+ if (type === "session.created") {
1971
+ warmIndexInBackground()
1972
+ if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
1973
+ const curated = runCurateForSession(sid)
1974
+ if (curated) {
1975
+ bumpCuratedVersion(sid)
1976
+ sessionCurated.set(sid, curated)
1977
+ }
1978
+ }
1979
+ }
1556
1980
  if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
1557
1981
  const hints = runHintsForSession()
1558
1982
  if (hints) sessionHints.set(sid, hints)
@@ -1560,6 +1984,10 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1560
1984
  if (!sessionWorkflow.has(sid)) {
1561
1985
  sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
1562
1986
  }
1987
+ const proposalSummary = await getPendingProposalCount(logClient, sid)
1988
+ if (!proposalSummary.unsupported && proposalSummary.count > 0) {
1989
+ markContextEpochDirty(sid)
1990
+ }
1563
1991
  } else if (type === "session.compacted" || type === "session.idle" || type === "session.deleted") {
1564
1992
  if (!sid) return
1565
1993
  const captured = captureSessionMemory(sid, type)
@@ -1571,6 +1999,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1571
1999
  reason: type,
1572
2000
  ref: captured,
1573
2001
  })
2002
+ await maybeIndexSessionMemory(logClient, sid, type, captured)
1574
2003
  }
1575
2004
  // Drop per-session state so a re-created session does not inherit
1576
2005
  // stale hints/curation.
@@ -1608,6 +2037,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1608
2037
  reason: "stop",
1609
2038
  ref: captured,
1610
2039
  })
2040
+ await maybeIndexSessionMemory(logClient, sid, "stop", captured)
1611
2041
  }
1612
2042
  } catch {
1613
2043
  // Best-effort only.
@@ -1619,14 +2049,14 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1619
2049
  if (!sid) return
1620
2050
  if (!Array.isArray(output.context)) return
1621
2051
  markContextEpochDirty(sid)
1622
- const hints = sessionHints.get(sid)
1623
- if (hints) output.context.push(`${AKM_HINTS_PREFIX}\n\n${hints}`)
1624
- const curated = sessionCurated.get(sid)
1625
- if (curated) output.context.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
1626
- const workflow = sessionWorkflow.get(sid)
1627
- if (workflow) output.context.push(formatWorkflowContext(workflow))
1628
- const curatorReport = sessionCuratorReport.get(sid)
1629
- if (curatorReport) output.context.push(formatCuratorReportContext(curatorReport))
2052
+ const blocks = [
2053
+ sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
2054
+ sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
2055
+ sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
2056
+ (await getPendingProposalCount(logClient, sid)).count > 0 && !(await getPendingProposalCount(logClient, sid)).unsupported ? formatPendingProposalContext((await getPendingProposalCount(logClient, sid)).count) : "",
2057
+ sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
2058
+ ]
2059
+ output.context.push(...applyContextBudget(blocks))
1630
2060
  } catch {
1631
2061
  // Never break compaction because of plugin context.
1632
2062
  }
@@ -1644,19 +2074,26 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1644
2074
  const epoch = sessionContextEpoch.get(sid) ?? 0
1645
2075
  const injectedEpoch = sessionContextInjectedEpoch.get(sid)
1646
2076
  if (sid && injectedEpoch !== epoch) {
1647
- const hints = sessionHints.get(sid)
1648
- if (hints) output.system.push(`${AKM_HINTS_PREFIX}\n\n${hints}`)
1649
- const workflow = sessionWorkflow.get(sid)
1650
- if (workflow) output.system.push(formatWorkflowContext(workflow))
1651
- const curatorReport = sessionCuratorReport.get(sid)
1652
- if (curatorReport) output.system.push(formatCuratorReportContext(curatorReport))
2077
+ const blocks = [
2078
+ sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
2079
+ sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
2080
+ sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
2081
+ (await getPendingProposalCount(logClient, sid)).count > 0 && !(await getPendingProposalCount(logClient, sid)).unsupported ? formatPendingProposalContext((await getPendingProposalCount(logClient, sid)).count) : "",
2082
+ sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
2083
+ ]
2084
+ output.system.push(...applyContextBudget(blocks))
1653
2085
  sessionContextInjectedEpoch.set(sid, epoch)
2086
+ if (sessionCurated.has(sid)) {
2087
+ // Startup curation is already included in the epoch-scoped block,
2088
+ // so mark that version as consumed to avoid a duplicate inject below.
2089
+ sessionCuratedInjectedVersion.set(sid, sessionCuratedVersion.get(sid) ?? 0)
2090
+ }
1654
2091
  }
1655
2092
  const curated = sid ? sessionCurated.get(sid) : undefined
1656
2093
  const curatedVersion = sessionCuratedVersion.get(sid) ?? 0
1657
2094
  if (curated) {
1658
2095
  if (sessionCuratedInjectedVersion.get(sid) !== curatedVersion) {
1659
- output.system.push(`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`)
2096
+ output.system.push(...applyContextBudget([`${AKM_CURATED_HEADER}\n${curated}${AKM_CURATED_TAIL}`]))
1660
2097
  sessionCuratedInjectedVersion.set(sid, curatedVersion)
1661
2098
  }
1662
2099
  }
@@ -1681,6 +2118,53 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1681
2118
  // Never break tool execution from the pre-hook.
1682
2119
  }
1683
2120
  },
2121
+ "permission.ask": async (input, output) => {
2122
+ try {
2123
+ const command = typeof input?.metadata?.command === "string"
2124
+ ? input.metadata.command
2125
+ : Array.isArray(input?.patterns)
2126
+ ? input.patterns.join(" && ")
2127
+ : ""
2128
+ if (!command.includes("akm")) return
2129
+ await emitWorkflowTelemetry(logClient, "info", "akm.raw_cli.invoked", {
2130
+ sessionID: input.sessionID,
2131
+ toolName: "bash",
2132
+ outcome: "requested",
2133
+ command,
2134
+ })
2135
+ const assessment = assessRiskyAkmCommand(command)
2136
+ if (!assessment) return
2137
+ output.status = "deny"
2138
+ await emitWorkflowTelemetry(logClient, "warn", "akm.raw_cli.blocked", {
2139
+ sessionID: input.sessionID,
2140
+ toolName: "bash",
2141
+ outcome: "blocked",
2142
+ reason: assessment.reason,
2143
+ command,
2144
+ category: assessment.category,
2145
+ })
2146
+ } catch {
2147
+ // Best-effort only.
2148
+ }
2149
+ },
2150
+ "command.execute.before": async (input, output) => {
2151
+ try {
2152
+ const command = `${input.command ?? ""} ${input.arguments ?? ""}`.trim()
2153
+ const assessment = assessRiskyAkmCommand(command)
2154
+ if (!assessment) return
2155
+ output.parts = [{ type: "text", text: blockedCommandMessage(command, assessment) }]
2156
+ await emitWorkflowTelemetry(logClient, "warn", "akm.raw_cli.blocked", {
2157
+ sessionID: input.sessionID,
2158
+ toolName: String(input.command ?? "bash"),
2159
+ outcome: "blocked",
2160
+ reason: assessment.reason,
2161
+ command,
2162
+ category: assessment.category,
2163
+ })
2164
+ } catch {
2165
+ // Best-effort only.
2166
+ }
2167
+ },
1684
2168
  "shell.env": async (_input, output) => {
1685
2169
  try {
1686
2170
  output.env.AKM_PROJECT = worktree
@@ -1706,7 +2190,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1706
2190
  // Compound-engineering loop: on every user message, curate the stash and
1707
2191
  // stash the result so experimental.chat.system.transform can inject it.
1708
2192
  if (AKM_AUTO_CURATE && input.sessionID) {
1709
- const curated = runCurateForPrompt(text)
2193
+ const curated = runCurateForPrompt(text, input.sessionID)
1710
2194
  if (curated) {
1711
2195
  sessionCurated.set(input.sessionID, curated)
1712
2196
  bumpCuratedVersion(input.sessionID)
@@ -1736,6 +2220,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1736
2220
  }, dedupe)
1737
2221
  }
1738
2222
  }
2223
+ await recordRetrospectiveFeedback(logClient, input.sessionID, text)
1739
2224
  },
1740
2225
  "tool.execute.after": async (input, output) => {
1741
2226
  if (!input.tool.startsWith("akm_")) return
@@ -1771,8 +2256,9 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1771
2256
  // Auto-feedback + session buffering: record every asset ref the tool
1772
2257
  // touched so the stash ranking improves over time and so Stop/Compact
1773
2258
  // has material to flush into a session summary memory.
1774
- const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
1775
- if (refResult.refs.length > 0 && input.sessionID) {
2259
+ const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
2260
+ noteRecentRefs(input.sessionID, refResult.refs)
2261
+ if (refResult.refs.length > 0 && input.sessionID) {
1776
2262
  for (const ref of refResult.refs) {
1777
2263
  addBufferEntry(input.sessionID, {
1778
2264
  kind: "tool-ref",
@@ -1839,9 +2325,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1839
2325
  .enum(["local", "stash", "registry", "both"])
1840
2326
  .optional()
1841
2327
  .describe("Search source. 'stash' searches local stash directories, 'registry' searches registries, and 'both' searches all sources. 'local' remains a backward-compatible alias for 'stash'."),
2328
+ include_proposed: tool.schema.boolean().optional().describe("Include proposed-quality results. Proposed assets are not curated until accepted."),
1842
2329
  },
1843
- async execute({ query, type, limit, source }) {
1844
- return runCli(client as unknown as LogCapableClient, createSearchArgs({ query, type, limit, source }), { toolName: "akm_search" })
2330
+ async execute({ query, type, limit, source, include_proposed }, context) {
2331
+ const raw = await runCli(
2332
+ client as unknown as LogCapableClient,
2333
+ createSearchArgs({ query, type, limit, source, includeProposed: include_proposed }),
2334
+ { toolName: "akm_search", sessionID: context.sessionID, directory: context.directory },
2335
+ )
2336
+ return withProposedWarnings(raw)
1845
2337
  },
1846
2338
  }),
1847
2339
  akm_show: tool({
@@ -1879,11 +2371,12 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1879
2371
  name: tool.schema.string().optional().describe("Optional memory name."),
1880
2372
  force: tool.schema.boolean().optional().describe("Overwrite an existing memory with the same name."),
1881
2373
  },
1882
- async execute({ content, name, force }) {
2374
+ async execute({ content, name, force }, context) {
1883
2375
  const args = ["remember", content]
1884
2376
  if (name) args.push("--name", name)
1885
2377
  if (force) args.push("--force")
1886
- return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_remember" })
2378
+ args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
2379
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_remember", sessionID: context.sessionID, directory: context.directory })
1887
2380
  },
1888
2381
  }),
1889
2382
  akm_feedback: tool({
@@ -1893,10 +2386,20 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1893
2386
  sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
1894
2387
  note: tool.schema.string().optional().describe("Optional note to attach to the feedback."),
1895
2388
  },
1896
- async execute({ ref, sentiment, note }) {
2389
+ async execute({ ref, sentiment, note }, context) {
1897
2390
  const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"]
1898
2391
  if (note) args.push("--note", note)
1899
- return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback" })
2392
+ args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
2393
+ const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback", sessionID: context.sessionID, directory: context.directory })
2394
+ await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
2395
+ sessionID: context.sessionID,
2396
+ toolName: "akm_feedback",
2397
+ assetRef: ref,
2398
+ outcome: "success",
2399
+ reason: sentiment,
2400
+ directory: context.directory,
2401
+ })
2402
+ return raw
1900
2403
  },
1901
2404
  }),
1902
2405
  akm_curate: tool({
@@ -1906,20 +2409,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
1906
2409
  limit: tool.schema.number().optional().describe("Maximum number of curated matches to return. Defaults to 6."),
1907
2410
  detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
1908
2411
  },
1909
- async execute({ query, limit, detail }) {
1910
- const args = [
1911
- "--for-agent",
1912
- "--format",
1913
- "text",
1914
- "--detail",
1915
- detail ?? "summary",
1916
- "-q",
1917
- "curate",
1918
- query,
1919
- "--limit",
1920
- String(limit ?? 6),
1921
- ]
1922
- return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate" })
2412
+ async execute({ query, limit, detail }, context) {
2413
+ const args = ["curate", query, "--limit", String(limit ?? 6)]
2414
+ if (detail) args.push("--detail", detail)
2415
+ args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
2416
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate", sessionID: context.sessionID, directory: context.directory })
1923
2417
  },
1924
2418
  }),
1925
2419
  akm_evolve: tool({
@@ -2462,6 +2956,89 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2462
2956
  }
2463
2957
  },
2464
2958
  }),
2959
+ akm_proposal: tool({
2960
+ description: "Operate the AKM v0.7.0 proposal queue — list/show/diff/accept/reject pending drafts. All proposal-producing commands (reflect, propose, distill, plus plugin-emitted proposals) write through this queue. Acceptance runs full validation before promoting; rejection archives the draft. Always confirm with the user before action='accept' or 'reject'.",
2961
+ args: {
2962
+ action: tool.schema.enum(["list", "show", "diff", "accept", "reject"]).describe("Proposal subcommand."),
2963
+ id: tool.schema.string().optional().describe("Proposal id. Required for show/diff/accept/reject."),
2964
+ status: tool.schema.enum(["pending", "accepted", "rejected"]).optional().describe("Filter for action='list'."),
2965
+ reason: tool.schema.string().optional().describe("Required for action='reject'. Recorded with the archived proposal."),
2966
+ },
2967
+ async execute({ action, id, status, reason }) {
2968
+ const logMeta = { toolName: "akm_proposal" }
2969
+ switch (action) {
2970
+ case "list": {
2971
+ const args = ["proposal", "list"]
2972
+ if (status) args.push("--status", status)
2973
+ return runCli(client as unknown as LogCapableClient, args, logMeta)
2974
+ }
2975
+ case "show": {
2976
+ if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='show'." })
2977
+ return runCli(client as unknown as LogCapableClient, ["proposal", "show", id], logMeta)
2978
+ }
2979
+ case "diff": {
2980
+ if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='diff'." })
2981
+ return runCli(client as unknown as LogCapableClient, ["proposal", "diff", id], logMeta)
2982
+ }
2983
+ case "accept": {
2984
+ if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='accept'. Confirm with the user before accepting." })
2985
+ return runCli(client as unknown as LogCapableClient, ["proposal", "accept", id], logMeta)
2986
+ }
2987
+ case "reject": {
2988
+ if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='reject'. Confirm with the user before rejecting." })
2989
+ if (!reason || !reason.trim()) return JSON.stringify({ ok: false, error: "'reason' is required for action='reject'. Ask the user why the proposal is being rejected." })
2990
+ return runCli(client as unknown as LogCapableClient, ["proposal", "reject", id, "--reason", reason], logMeta)
2991
+ }
2992
+ }
2993
+ },
2994
+ }),
2995
+ akm_reflect: tool({
2996
+ description: "Generate a reflection proposal for an AKM ref via the configured agent CLI. Output lands in the proposal queue only — never mutates live stash content. Requires `agent.default` to be set (run akm_setup first if missing).",
2997
+ args: {
2998
+ ref: tool.schema.string().optional().describe("[origin//]type:name ref to reflect on. Optional — when omitted the agent reflects on overall session signal."),
2999
+ task: tool.schema.string().optional().describe("Free-form task description guiding the reflection."),
3000
+ },
3001
+ async execute({ ref, task }) {
3002
+ const args = ["reflect"]
3003
+ if (ref) args.push(ref)
3004
+ if (task) args.push("--task", task)
3005
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_reflect" })
3006
+ },
3007
+ }),
3008
+ akm_propose: tool({
3009
+ description: "Generate a new-asset proposal via the configured agent CLI. The asset is drafted as `quality:\"proposed\"` and lands in the proposal queue — never directly into curated content. Requires `agent.default` (run akm_setup first if missing).",
3010
+ args: {
3011
+ type: tool.schema.enum(["skill", "command", "agent", "knowledge", "lesson", "script", "workflow", "wiki"]).describe("Asset type for the new proposal."),
3012
+ name: tool.schema.string().describe("Slug for the new asset (matches the standard ref grammar)."),
3013
+ task: tool.schema.string().describe("Required. Describes what the asset should do."),
3014
+ },
3015
+ async execute({ type, name, task }) {
3016
+ if (!task || !task.trim()) return JSON.stringify({ ok: false, error: "'task' is required for akm_propose." })
3017
+ const args = ["propose", type, name, "--task", task]
3018
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_propose" })
3019
+ },
3020
+ }),
3021
+ akm_distill: tool({
3022
+ description: "Distill an AKM ref into a proposed `lesson` using the bounded in-tree LLM. Gated by `llm.features.feedback_distillation` (default false); when the gate is off the call returns a fallback warning instead of a proposal. Lessons require `description` and `when_to_use` frontmatter and are stored under `lessons/<name>.md` after acceptance.",
3023
+ args: {
3024
+ ref: tool.schema.string().describe("[origin//]type:name ref to distill — most often memory:<name> or knowledge:<name>."),
3025
+ },
3026
+ async execute({ ref }) {
3027
+ if (!ref || !ref.trim()) return JSON.stringify({ ok: false, error: "'ref' is required for akm_distill." })
3028
+ return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
3029
+ },
3030
+ }),
3031
+ akm_setup: tool({
3032
+ description: "Detect installed agent CLIs (opencode, claude, codex, gemini, aider) and persist `agent.default`. Required once per machine before akm_reflect / akm_propose can shell out. Idempotent — pass force=true to re-run detection even when agent.default is already set.",
3033
+ args: {
3034
+ force: tool.schema.boolean().optional().describe("Re-run detection even when agent.default is already configured."),
3035
+ },
3036
+ async execute({ force }) {
3037
+ const args = ["setup"]
3038
+ if (force) args.push("--force")
3039
+ return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_setup" })
3040
+ },
3041
+ }),
2465
3042
  akm_help: tool({
2466
3043
  description: "Discover the right `akm` CLI command and args for tasks not covered by a first-class tool — e.g. save/push, import, clone, update, remove, list sources, registry search, reindex, config, CLI upgrade, run script. Returns a curated quick-reference plus live `akm --help` output. Pass `command` to drill into a specific subcommand.",
2467
3044
  args: {
@@ -2490,6 +3067,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2490
3067
  hints: topic ? lookupAkmHelpHint(topic) : [],
2491
3068
  quickReference: AKM_HELP_QUICK_REFERENCE,
2492
3069
  help: helpText,
3070
+ workflowTopics: [
3071
+ "proposal",
3072
+ "distill",
3073
+ "reflect",
3074
+ "propose",
3075
+ "lesson",
3076
+ "include-proposed",
3077
+ "llm-features",
3078
+ "vault-safety",
3079
+ ],
2493
3080
  })
2494
3081
  },
2495
3082
  }),
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
- "description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including v0.5.0 vaults, wikis, and workflows, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
5
+ "description": "OpenCode plugin for AKM v0.7.0 - search, show, and manage extension assets via the akm CLI, including vaults, wikis, workflows, the proposal queue, lesson assets, and reflect/propose/distill commands, with agentic hooks that auto-load relevant stash assets, record feedback, and harvest session memories so the stash improves every session.",
6
6
  "keywords": [
7
7
  "opencode",
8
8
  "opencode-ai",
@@ -26,6 +26,7 @@
26
26
  "files": [
27
27
  "index.ts",
28
28
  "agent/",
29
+ "commands/",
29
30
  "README.md"
30
31
  ],
31
32
  "main": "./index.ts",