akm-opencode 0.6.0 → 0.7.1
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 +40 -6
- package/agent/akm-curator.md +8 -0
- package/commands/akm-distill-lesson.md +7 -0
- package/commands/akm-evolve-session.md +15 -0
- package/commands/akm-propose-asset.md +8 -0
- package/commands/akm-reflect-on-failure.md +7 -0
- package/commands/akm-review-proposals.md +7 -0
- package/commands/akm-workflow-status.md +7 -0
- package/index.ts +531 -35
- package/package.json +3 -2
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.
|
|
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,17 @@ Add to your OpenCode config (`opencode.json`):
|
|
|
14
14
|
|
|
15
15
|
## Tools
|
|
16
16
|
|
|
17
|
-
The plugin exposes
|
|
17
|
+
The plugin exposes **20 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
|
-
| `
|
|
21
|
+
| `akm_info` | Show `akm info` output together with the installed `akm-opencode` plugin version and install location |
|
|
22
|
+
| `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
23
|
| `akm_show` | Show a stash asset by its ref |
|
|
23
24
|
| `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
|
|
24
25
|
| `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
|
|
25
26
|
| `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
|
|
27
|
+
| `akm_feedback` | Record positive or negative feedback for a stash asset (skipped automatically for `memory:`, `vault:`, `lesson:`, and proposed-quality refs) |
|
|
27
28
|
| `akm_curate` | Curate the stash for a task or topic and return ranked matches the agent can use |
|
|
28
29
|
| `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
30
|
| `akm_parent_messages` | Summarize the parent OpenCode session so dispatched stash subagents can inherit upstream context |
|
|
@@ -31,6 +32,11 @@ The plugin exposes a trimmed surface of **14 high-value tools**. Long-tail verbs
|
|
|
31
32
|
| `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
33
|
| `akm_wiki` | Manage wikis (`create`, `register`, `list`, `show`, `pages`, `search`, `stash`, `lint`, `ingest`, `remove`) |
|
|
33
34
|
| `akm_workflow` | Drive workflow runs (`start`, `next`, `complete`, `status`, `list`, `create`, `template`, `resume`) |
|
|
35
|
+
| `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 |
|
|
36
|
+
| `akm_reflect` | Generate a reflection proposal via the configured agent CLI; output lands in the proposal queue only |
|
|
37
|
+
| `akm_propose` | Generate a new-asset proposal via the configured agent CLI; the result is `quality:"proposed"` until accepted |
|
|
38
|
+
| `akm_distill` | Distill an AKM ref (typically `memory:*` or `knowledge:*`) into a proposed `lesson` (gated by `llm.features.feedback_distillation`) |
|
|
39
|
+
| `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
40
|
| `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
41
|
|
|
36
42
|
## Compound-engineering hooks
|
|
@@ -43,8 +49,9 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
43
49
|
| --- | --- |
|
|
44
50
|
| **`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. |
|
|
45
51
|
| **`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. |
|
|
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. |
|
|
52
|
+
| **`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
53
|
| **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
|
|
54
|
+
| **`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
55
|
| **`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
56
|
| **`experimental.session.compacting`** | Pushes hints, curated context, active workflows, and the last curator report into the compaction prompt so they survive transcript shrinking. |
|
|
50
57
|
| **`shell.env`** | Exposes `AKM_STASH_DIR`, `AKM_PROJECT`, and `AKM_PLUGIN_VERSION` to shell tools so plain `akm` calls inherit the right context. |
|
|
@@ -66,6 +73,10 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
|
|
|
66
73
|
| `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. |
|
|
67
74
|
| `AKM_MEMORY_CHECKPOINT_EVERY` | `8` | Number of successful asset-touching tool calls between mid-session checkpoint memories. |
|
|
68
75
|
| `AKM_RETROSPECTIVE_FEEDBACK_PATTERN` | `\b(thanks|perfect|worked)\b` | Case-insensitive regex used for lightweight positive retrospective feedback on the most recent refs. |
|
|
76
|
+
| `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. |
|
|
77
|
+
| `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. |
|
|
78
|
+
| `AKM_PENDING_PROPOSAL_TIMEOUT` | `2` | Seconds allowed for lightweight pending-proposal count checks during context injection. |
|
|
79
|
+
| `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). |
|
|
69
80
|
|
|
70
81
|
### Curator agent
|
|
71
82
|
|
|
@@ -76,6 +87,27 @@ memories, parent-session context, live stash), produces a prioritized action
|
|
|
76
87
|
list, and persists its latest report as `memory:akm-curator-YYYYMMDD-<sid>` so
|
|
77
88
|
future curator runs can build on it.
|
|
78
89
|
|
|
90
|
+
## AKM v1 workflows
|
|
91
|
+
|
|
92
|
+
The plugin injects a concise AKM workflow instruction pack into context so agents:
|
|
93
|
+
|
|
94
|
+
- search or curate before writing from scratch;
|
|
95
|
+
- show an asset before relying on it;
|
|
96
|
+
- record feedback after the result is known;
|
|
97
|
+
- treat `lesson:*` as first-class durable assets;
|
|
98
|
+
- treat proposed-quality assets as uncurated until accepted;
|
|
99
|
+
- use `akm_help` to route `proposal`, `distill`, `reflect`, and `propose` CLI workflows;
|
|
100
|
+
- require explicit user approval before proposal acceptance/rejection, push saves, source removal, CLI upgrades, update-all, or vault value access.
|
|
101
|
+
|
|
102
|
+
The package also ships OpenCode command docs for common workflows:
|
|
103
|
+
|
|
104
|
+
- `/akm-review-proposals`
|
|
105
|
+
- `/akm-distill-lesson`
|
|
106
|
+
- `/akm-reflect-on-failure`
|
|
107
|
+
- `/akm-propose-asset`
|
|
108
|
+
- `/akm-evolve-session`
|
|
109
|
+
- `/akm-workflow-status`
|
|
110
|
+
|
|
79
111
|
### Registry discovery
|
|
80
112
|
|
|
81
113
|
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.
|
|
@@ -144,9 +176,11 @@ stash/
|
|
|
144
176
|
├── agents/ # markdown files
|
|
145
177
|
├── knowledge/ # markdown files
|
|
146
178
|
├── memories/ # markdown memory files (akm remember)
|
|
179
|
+
├── lessons/ # first-class durable learnings (lesson:<name>) — produced by akm distill, accepted via akm_proposal accept
|
|
147
180
|
├── workflows/ # multi-step procedures (workflow:<name>)
|
|
148
181
|
├── vaults/ # .env secret stores (vault:<name>) — values never surface through structured output
|
|
149
|
-
|
|
182
|
+
├── wikis/ # per-wiki directories <name>/{schema,index,log}.md + raw/ + pages
|
|
183
|
+
└── .akm/proposals/ # v0.7.0 proposal queue — drafts that never leak into search or commits
|
|
150
184
|
```
|
|
151
185
|
|
|
152
186
|
## Vaults
|
package/agent/akm-curator.md
CHANGED
|
@@ -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.
|
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,7 +22,10 @@ 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()
|
|
28
|
+
const PLUGIN_INSTALL_LOCATION = moduleDir
|
|
25
29
|
|
|
26
30
|
// Per-session state that drives the compound-engineering loop.
|
|
27
31
|
// These maps are keyed by OpenCode sessionID.
|
|
@@ -46,12 +50,30 @@ type SessionBufferEntry = {
|
|
|
46
50
|
const sessionBuffer = new Map<string, SessionBufferEntry[]>()
|
|
47
51
|
const sessionFinalMemoryCaptured = new Set<string>()
|
|
48
52
|
const sessionSuccessfulAssetTouchCount = new Map<string, number>()
|
|
53
|
+
const pendingProposalSummaryCache = new Map<string, { count: number; expiresAt: number; unsupported?: boolean }>()
|
|
54
|
+
const retrospectiveState = new Map<string, { recentRefs: string[]; lastNegativeSignalAt?: number }>()
|
|
49
55
|
let cachedAkmStashDir: string | undefined
|
|
50
56
|
|
|
51
57
|
// Asset-ref grammar matching the stash skill: [origin//]type:name.
|
|
52
58
|
// We validate normalized tokens individually instead of running a global regex
|
|
53
59
|
// 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._/\-]+$/
|
|
60
|
+
const AKM_REF_PATTERN = /^(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|script|workflow|vault|wiki|lesson):[A-Za-z0-9._/\-]+$/
|
|
61
|
+
const PROPOSED_QUALITY_WARNING = "Do not treat proposed assets as curated until accepted."
|
|
62
|
+
const AKM_WORKFLOW_INSTRUCTION = [
|
|
63
|
+
"# AKM workflow (v0.7.0)",
|
|
64
|
+
"",
|
|
65
|
+
"Use AKM as a reusable knowledge and workflow stash.",
|
|
66
|
+
"",
|
|
67
|
+
"Before writing from scratch:",
|
|
68
|
+
"1. Use `akm_search` or `akm_curate`.",
|
|
69
|
+
"2. Use `akm_show <ref>` before relying on an asset.",
|
|
70
|
+
"3. Record `akm_feedback` after the result is known.",
|
|
71
|
+
"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.",
|
|
72
|
+
"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`.",
|
|
73
|
+
"6. Run `akm_setup` once on a fresh machine to detect installed agent CLIs and persist `agent.default`. Reflect/propose require it.",
|
|
74
|
+
`7. ${PROPOSED_QUALITY_WARNING}`,
|
|
75
|
+
"8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
|
|
76
|
+
].join("\n")
|
|
55
77
|
|
|
56
78
|
function readPackageVersion(): string {
|
|
57
79
|
try {
|
|
@@ -72,6 +94,19 @@ function createRetrospectiveFeedbackRegex(): RegExp {
|
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
function createRetrospectiveNegativeRegex(): RegExp {
|
|
98
|
+
const pattern = process.env.AKM_RETROSPECTIVE_NEGATIVE_PATTERN ?? "\\b(wrong|failed|broken|didn't work|did not work|bad)\\b"
|
|
99
|
+
try {
|
|
100
|
+
return new RegExp(pattern, "i")
|
|
101
|
+
} catch {
|
|
102
|
+
return /\b(wrong|failed|broken|didn't work|did not work|bad)\b/i
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createExplicitCorrectionRegex(): RegExp {
|
|
107
|
+
return /\b(this was wrong|that was wrong|you were wrong|incorrect|not correct)\b/i
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
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
111
|
|
|
77
112
|
Inputs you should inspect:
|
|
@@ -83,7 +118,9 @@ Inputs you should inspect:
|
|
|
83
118
|
Signals to act on:
|
|
84
119
|
- Hot refs: assets repeatedly appearing in positive tool outcomes. Call akm_feedback <ref> positive --note "curator: consistently useful" to reinforce.
|
|
85
120
|
- Cold refs: assets tied to failures or user complaints. Record akm_feedback <ref> negative --note "<excerpt>" and open the asset for review.
|
|
121
|
+
- Lesson candidates: repeated memories or failures that should become a proposed lesson. Use akm_help topic="distill" before raw CLI distill commands.
|
|
86
122
|
- 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").
|
|
123
|
+
- 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
124
|
- Duplicates / drift: near-identical descriptions or overlapping responsibilities. Propose a consolidation.
|
|
88
125
|
- 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
126
|
- 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 +142,15 @@ Output shape: end every run with a markdown report that has these sections:
|
|
|
105
142
|
## Cold assets (investigate)
|
|
106
143
|
- <ref> — failure signal — proposed fix
|
|
107
144
|
|
|
145
|
+
## Lesson candidates
|
|
146
|
+
- <theme> — evidence refs — distill or reflect command to run
|
|
147
|
+
|
|
108
148
|
## Coverage gaps
|
|
109
149
|
- <theme> — proposed asset (type, name, one-line description)
|
|
110
150
|
|
|
151
|
+
## Pending proposals
|
|
152
|
+
- <proposal id> — summary — accept/reject/revise recommendation
|
|
153
|
+
|
|
111
154
|
## Duplicates / drift
|
|
112
155
|
- <ref a> vs <ref b> — consolidation proposal
|
|
113
156
|
|
|
@@ -265,6 +308,38 @@ function appendRunScopeArgs(args: string[], sessionID: string | undefined): stri
|
|
|
265
308
|
return sessionID ? [...args, "--run", sessionID] : args
|
|
266
309
|
}
|
|
267
310
|
|
|
311
|
+
function getScopeFields(): Array<"user" | "agent" | "run" | "channel"> {
|
|
312
|
+
const configured = process.env.AKM_SCOPE_KEYS?.split(",").map((part) => part.trim()).filter(Boolean)
|
|
313
|
+
const values = configured && configured.length > 0 ? configured : ["user", "agent", "run", "channel"]
|
|
314
|
+
return values.filter((value): value is "user" | "agent" | "run" | "channel" =>
|
|
315
|
+
value === "user" || value === "agent" || value === "run" || value === "channel",
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildScopedArgs(context: Record<string, unknown> | undefined): string[] {
|
|
320
|
+
if (!context) return []
|
|
321
|
+
const scopeFields = new Set(getScopeFields())
|
|
322
|
+
const args: string[] = []
|
|
323
|
+
const user = typeof context.userID === "string"
|
|
324
|
+
? context.userID
|
|
325
|
+
: typeof context.user === "string"
|
|
326
|
+
? context.user
|
|
327
|
+
: undefined
|
|
328
|
+
const agent = typeof context.agent === "string" ? context.agent : undefined
|
|
329
|
+
const run = typeof context.sessionID === "string" ? context.sessionID : typeof context.run === "string" ? context.run : undefined
|
|
330
|
+
const channel = typeof context.channel === "string"
|
|
331
|
+
? context.channel
|
|
332
|
+
: typeof context.variant === "string"
|
|
333
|
+
? context.variant
|
|
334
|
+
: undefined
|
|
335
|
+
|
|
336
|
+
if (scopeFields.has("user") && user) args.push("--user", user)
|
|
337
|
+
if (scopeFields.has("agent") && agent) args.push("--agent", agent)
|
|
338
|
+
if (scopeFields.has("run") && run) args.push("--run", run)
|
|
339
|
+
if (scopeFields.has("channel") && channel) args.push("--channel", channel)
|
|
340
|
+
return args
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
function runCurate(args: string[]): string | null {
|
|
269
344
|
const result = runCliSyncRaw(args, AKM_CURATE_TIMEOUT_MS)
|
|
270
345
|
if (!result.ok) return null
|
|
@@ -277,11 +352,10 @@ function runCurateForPrompt(text: string, sessionID?: string): string | null {
|
|
|
277
352
|
return runCurate(
|
|
278
353
|
appendRunScopeArgs(
|
|
279
354
|
[
|
|
280
|
-
"--
|
|
355
|
+
"--detail",
|
|
356
|
+
"agent",
|
|
281
357
|
"--format",
|
|
282
358
|
"text",
|
|
283
|
-
"--detail",
|
|
284
|
-
"summary",
|
|
285
359
|
"-q",
|
|
286
360
|
"curate",
|
|
287
361
|
text,
|
|
@@ -297,11 +371,10 @@ function runCurateForSession(sessionID: string): string | null {
|
|
|
297
371
|
return runCurate(
|
|
298
372
|
appendRunScopeArgs(
|
|
299
373
|
[
|
|
300
|
-
"--
|
|
374
|
+
"--detail",
|
|
375
|
+
"agent",
|
|
301
376
|
"--format",
|
|
302
377
|
"text",
|
|
303
|
-
"--detail",
|
|
304
|
-
"summary",
|
|
305
378
|
"-q",
|
|
306
379
|
"curate",
|
|
307
380
|
"--limit",
|
|
@@ -374,6 +447,17 @@ function formatCuratorReportContext(report: string): string {
|
|
|
374
447
|
return `# AKM curator report\n${report}`
|
|
375
448
|
}
|
|
376
449
|
|
|
450
|
+
function formatPendingProposalContext(count: number): string {
|
|
451
|
+
const summaryLine = count === 1 ? "There is 1 pending AKM proposal." : `There are ${count} pending AKM proposals.`
|
|
452
|
+
return [
|
|
453
|
+
"# AKM pending proposals",
|
|
454
|
+
"",
|
|
455
|
+
summaryLine,
|
|
456
|
+
"Use `/akm-review-proposals` or `akm_help topic=proposal` to review them.",
|
|
457
|
+
PROPOSED_QUALITY_WARNING,
|
|
458
|
+
].join("\n")
|
|
459
|
+
}
|
|
460
|
+
|
|
377
461
|
function summarizeCuratorReportForContext(report: string): string {
|
|
378
462
|
if (report.length <= AKM_CURATOR_CONTEXT_MAX_CHARS) return report
|
|
379
463
|
return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
|
|
@@ -417,6 +501,139 @@ function warmIndexInBackground(): void {
|
|
|
417
501
|
}
|
|
418
502
|
}
|
|
419
503
|
|
|
504
|
+
function safeJsonParse<T>(raw: string): T | undefined {
|
|
505
|
+
try {
|
|
506
|
+
return JSON.parse(raw) as T
|
|
507
|
+
} catch {
|
|
508
|
+
return undefined
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventType: string, extra: Record<string, unknown>) {
|
|
513
|
+
return writePluginLog(client, level, eventType, {
|
|
514
|
+
subsystem: "workflow-compliance",
|
|
515
|
+
eventType,
|
|
516
|
+
pluginVersion: PLUGIN_VERSION,
|
|
517
|
+
...extra,
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
|
|
522
|
+
if (!sessionID || refs.length === 0) return
|
|
523
|
+
const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
|
|
524
|
+
state.recentRefs = [...new Set([...state.recentRefs, ...refs])].slice(-8)
|
|
525
|
+
retrospectiveState.set(sessionID, state)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function getPendingProposalCount(client: LogCapableClient, sessionID?: string): Promise<{ count: number; unsupported?: boolean }> {
|
|
529
|
+
const cacheKey = sessionID ?? "global"
|
|
530
|
+
const cached = pendingProposalSummaryCache.get(cacheKey)
|
|
531
|
+
if (cached && cached.expiresAt > Date.now()) return cached
|
|
532
|
+
|
|
533
|
+
const command = resolveAkmCommand()
|
|
534
|
+
if (typeof command !== "string") return { count: 0, unsupported: true }
|
|
535
|
+
try {
|
|
536
|
+
const stdout = execFileSync(command, ["proposal", "list", "--status", "pending", "--format", "json"], {
|
|
537
|
+
encoding: "utf8",
|
|
538
|
+
timeout: AKM_PENDING_PROPOSAL_TIMEOUT_MS,
|
|
539
|
+
})
|
|
540
|
+
const parsed = safeJsonParse<{ proposals?: unknown[]; hits?: unknown[] }>(stdout)
|
|
541
|
+
const count = Array.isArray(parsed?.proposals) ? parsed.proposals.length : Array.isArray(parsed?.hits) ? parsed.hits.length : 0
|
|
542
|
+
const result = { count, expiresAt: Date.now() + 60_000 }
|
|
543
|
+
pendingProposalSummaryCache.set(cacheKey, result)
|
|
544
|
+
return result
|
|
545
|
+
} catch (error: unknown) {
|
|
546
|
+
const message = formatCliError(error)
|
|
547
|
+
const unsupported = /unknown|unsupported|not found|invalid/i.test(message)
|
|
548
|
+
const result = { count: 0, unsupported, expiresAt: Date.now() + 60_000 }
|
|
549
|
+
pendingProposalSummaryCache.set(cacheKey, result)
|
|
550
|
+
return result
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID: string | undefined, text: string) {
|
|
555
|
+
if (!sessionID) return
|
|
556
|
+
const state = retrospectiveState.get(sessionID)
|
|
557
|
+
const recentRefs = state?.recentRefs ?? []
|
|
558
|
+
if (recentRefs.length === 0) return
|
|
559
|
+
|
|
560
|
+
const explicitCorrection = AKM_EXPLICIT_CORRECTION_RE.test(text)
|
|
561
|
+
const negative = explicitCorrection || AKM_RETROSPECTIVE_NEGATIVE_RE.test(text)
|
|
562
|
+
if (!negative) return
|
|
563
|
+
|
|
564
|
+
if (!explicitCorrection) {
|
|
565
|
+
const now = Date.now()
|
|
566
|
+
if (!state?.lastNegativeSignalAt || now - state.lastNegativeSignalAt > 2 * 60 * 1000) {
|
|
567
|
+
retrospectiveState.set(sessionID, { recentRefs, lastNegativeSignalAt: now })
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const targetRef = recentRefs[recentRefs.length - 1]
|
|
573
|
+
const raw = await runCli(client, ["feedback", targetRef, "--negative", "--note", text.slice(0, 280)], {
|
|
574
|
+
toolName: "akm_feedback",
|
|
575
|
+
sessionID,
|
|
576
|
+
})
|
|
577
|
+
const parsed = safeJsonParse<{ ok?: boolean }>(raw)
|
|
578
|
+
if (parsed?.ok !== false) {
|
|
579
|
+
await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
|
|
580
|
+
sessionID,
|
|
581
|
+
toolName: "akm_feedback",
|
|
582
|
+
assetRef: targetRef,
|
|
583
|
+
outcome: "success",
|
|
584
|
+
reason: explicitCorrection ? "explicit correction" : "negative retrospective signal",
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
retrospectiveState.set(sessionID, { recentRefs })
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
type RiskyCommandAssessment = {
|
|
591
|
+
category: string
|
|
592
|
+
reason: string
|
|
593
|
+
approval: string
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function assessRiskyAkmCommand(command: string): RiskyCommandAssessment | undefined {
|
|
597
|
+
const args = splitArguments(command)
|
|
598
|
+
const akmIndex = args.findIndex((arg) => arg === "akm" || arg.endsWith("/akm") || arg.endsWith("\\akm.exe"))
|
|
599
|
+
if (akmIndex === -1) return undefined
|
|
600
|
+
const tokens = args.slice(akmIndex + 1)
|
|
601
|
+
if (tokens[0] === "proposal" && tokens[1] === "accept") {
|
|
602
|
+
return { category: "proposal-accept", reason: "Proposal acceptance changes curated AKM content.", approval: "Ask the user to approve `akm proposal accept <id>`." }
|
|
603
|
+
}
|
|
604
|
+
if (tokens[0] === "proposal" && tokens[1] === "reject") {
|
|
605
|
+
return { category: "proposal-reject", reason: "Proposal rejection is a durable curation decision.", approval: "Ask the user to approve `akm proposal reject <id> --reason \"...\"`." }
|
|
606
|
+
}
|
|
607
|
+
if (tokens[0] === "save" && tokens.includes("--push")) {
|
|
608
|
+
return { category: "save-push", reason: "Pushing stash changes must be explicitly approved.", approval: "Ask the user to approve `akm save --push`." }
|
|
609
|
+
}
|
|
610
|
+
if (tokens[0] === "remove") {
|
|
611
|
+
return { category: "remove", reason: "Removing AKM sources is destructive.", approval: "Ask the user to approve the exact `akm remove ...` command." }
|
|
612
|
+
}
|
|
613
|
+
if (tokens[0] === "vault" && ["show", "load", "set", "unset"].includes(tokens[1] ?? "")) {
|
|
614
|
+
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.` }
|
|
615
|
+
}
|
|
616
|
+
if (tokens[0] === "config" && tokens[1] === "set" && (tokens[2]?.startsWith("llm.features.") ?? false)) {
|
|
617
|
+
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." }
|
|
618
|
+
}
|
|
619
|
+
if (tokens[0] === "update" && tokens.includes("--all")) {
|
|
620
|
+
return { category: "update-all", reason: "Updating all AKM kits changes many assets at once.", approval: "Ask the user to approve `akm update --all`." }
|
|
621
|
+
}
|
|
622
|
+
if (tokens[0] === "upgrade") {
|
|
623
|
+
return { category: "upgrade", reason: "Upgrading the AKM CLI changes the toolchain.", approval: "Ask the user to approve the exact `akm upgrade` command." }
|
|
624
|
+
}
|
|
625
|
+
return undefined
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function blockedCommandMessage(command: string, assessment: RiskyCommandAssessment): string {
|
|
629
|
+
return [
|
|
630
|
+
`Blocked risky AKM command: ${command}`,
|
|
631
|
+
assessment.reason,
|
|
632
|
+
assessment.approval,
|
|
633
|
+
"Retry only after explicit user approval in this conversation.",
|
|
634
|
+
].join("\n")
|
|
635
|
+
}
|
|
636
|
+
|
|
420
637
|
function queueFeedback(
|
|
421
638
|
client: LogCapableClient,
|
|
422
639
|
ref: string,
|
|
@@ -664,6 +881,8 @@ const AKM_HINTS_PREFIX = [
|
|
|
664
881
|
"# AKM is available in this session",
|
|
665
882
|
"",
|
|
666
883
|
"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.",
|
|
884
|
+
"",
|
|
885
|
+
AKM_WORKFLOW_INSTRUCTION,
|
|
667
886
|
].join("\n")
|
|
668
887
|
|
|
669
888
|
const AKM_CURATED_HEADER = "# AKM stash — assets relevant to this prompt"
|
|
@@ -715,6 +934,35 @@ type AkmHelpEntry = {
|
|
|
715
934
|
}
|
|
716
935
|
|
|
717
936
|
const AKM_HELP_QUICK_REFERENCE: readonly AkmHelpEntry[] = [
|
|
937
|
+
{
|
|
938
|
+
task: "Review pending proposals and decide whether to accept, reject, or revise them",
|
|
939
|
+
command: "akm proposal list --status pending --format json; akm proposal show <id>; akm proposal diff <id>",
|
|
940
|
+
notes: "Accept/reject requires explicit user approval.",
|
|
941
|
+
keywords: ["proposal", "review proposals", "pending proposals", "accept proposal", "reject proposal"],
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
task: "Distill repeated evidence into a proposed lesson",
|
|
945
|
+
command: "akm distill <ref>",
|
|
946
|
+
notes: "Distill creates a proposal; proposed assets are not curated until accepted.",
|
|
947
|
+
keywords: ["distill", "lesson", "proposed lesson"],
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
task: "Reflect on an existing asset after failure or drift",
|
|
951
|
+
command: "akm reflect <ref> --task \"...\"",
|
|
952
|
+
keywords: ["reflect", "drift", "failure"],
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
task: "Create a proposed asset for a coverage gap",
|
|
956
|
+
command: "akm propose <type> <name> --task \"...\"",
|
|
957
|
+
notes: PROPOSED_QUALITY_WARNING,
|
|
958
|
+
keywords: ["propose", "coverage gap", "proposed asset"],
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
task: "Search including proposed-quality assets",
|
|
962
|
+
command: "akm search <query> --include-proposed",
|
|
963
|
+
notes: PROPOSED_QUALITY_WARNING,
|
|
964
|
+
keywords: ["include-proposed", "proposed quality", "lesson"],
|
|
965
|
+
},
|
|
718
966
|
{
|
|
719
967
|
task: "Install a kit or register an external source (npm, GitHub, git, URL, local dir)",
|
|
720
968
|
command: "akm add <package-ref> [--name <n>] [--type wiki] [--writable] [--trust] [--provider <p>] [--max-pages N] [--max-depth N]",
|
|
@@ -1064,6 +1312,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1064
1312
|
}
|
|
1065
1313
|
|
|
1066
1314
|
const fullArgs = args.includes("--format") ? [...args] : [...args, "--format", "json"]
|
|
1315
|
+
const proposalId = args[0] === "proposal" && typeof args[2] === "string" ? args[2] : null
|
|
1067
1316
|
|
|
1068
1317
|
try {
|
|
1069
1318
|
const stdout = execFileSync(command, fullArgs, {
|
|
@@ -1081,6 +1330,59 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1081
1330
|
stdout,
|
|
1082
1331
|
stderr: "",
|
|
1083
1332
|
})
|
|
1333
|
+
const parsed = safeJsonParse<SearchResponse>(stdout)
|
|
1334
|
+
const refs = args[0] === "search" || args[0] === "curate"
|
|
1335
|
+
? [...new Set([...(parsed?.hits?.flatMap((hit) => hit.ref ? [hit.ref] : []) ?? []), ...extractRefsFromText(stdout)])]
|
|
1336
|
+
: extractRefsFromText(stdout)
|
|
1337
|
+
noteRecentRefs(meta.sessionID, refs)
|
|
1338
|
+
if (meta.toolName === "akm_search") {
|
|
1339
|
+
await emitWorkflowTelemetry(client, "info", "akm.search.invoked", {
|
|
1340
|
+
sessionID: meta.sessionID,
|
|
1341
|
+
toolName: meta.toolName,
|
|
1342
|
+
assetRef: refs[0] ?? null,
|
|
1343
|
+
proposalId: null,
|
|
1344
|
+
outcome: "success",
|
|
1345
|
+
directory: meta.directory,
|
|
1346
|
+
})
|
|
1347
|
+
}
|
|
1348
|
+
if (meta.toolName === "akm_curate") {
|
|
1349
|
+
await emitWorkflowTelemetry(client, "info", "akm.curate.invoked", {
|
|
1350
|
+
sessionID: meta.sessionID,
|
|
1351
|
+
toolName: meta.toolName,
|
|
1352
|
+
assetRef: refs[0] ?? null,
|
|
1353
|
+
proposalId: null,
|
|
1354
|
+
outcome: "success",
|
|
1355
|
+
directory: meta.directory,
|
|
1356
|
+
})
|
|
1357
|
+
}
|
|
1358
|
+
if (meta.toolName === "akm_show") {
|
|
1359
|
+
await emitWorkflowTelemetry(client, "info", "akm.show.invoked", {
|
|
1360
|
+
sessionID: meta.sessionID,
|
|
1361
|
+
toolName: meta.toolName,
|
|
1362
|
+
assetRef: args[1] ?? refs[0] ?? null,
|
|
1363
|
+
proposalId,
|
|
1364
|
+
outcome: "success",
|
|
1365
|
+
directory: meta.directory,
|
|
1366
|
+
})
|
|
1367
|
+
}
|
|
1368
|
+
if (args[0] === "proposal" && ["show", "diff"].includes(args[1] ?? "")) {
|
|
1369
|
+
await emitWorkflowTelemetry(client, "info", "akm.proposal.reviewed", {
|
|
1370
|
+
sessionID: meta.sessionID,
|
|
1371
|
+
toolName: meta.toolName,
|
|
1372
|
+
proposalId,
|
|
1373
|
+
outcome: "requested",
|
|
1374
|
+
directory: meta.directory,
|
|
1375
|
+
})
|
|
1376
|
+
}
|
|
1377
|
+
if (args[0] === "proposal" && ["accept", "reject"].includes(args[1] ?? "")) {
|
|
1378
|
+
await emitWorkflowTelemetry(client, "info", args[1] === "accept" ? "akm.proposal.accept.requested" : "akm.proposal.reject.requested", {
|
|
1379
|
+
sessionID: meta.sessionID,
|
|
1380
|
+
toolName: meta.toolName,
|
|
1381
|
+
proposalId,
|
|
1382
|
+
outcome: "requested",
|
|
1383
|
+
directory: meta.directory,
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1084
1386
|
return stdout
|
|
1085
1387
|
} catch (error: unknown) {
|
|
1086
1388
|
const message = formatCliError(error)
|
|
@@ -1095,6 +1397,16 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1095
1397
|
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
1096
1398
|
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? message,
|
|
1097
1399
|
})
|
|
1400
|
+
if (meta.toolName.startsWith("akm_")) {
|
|
1401
|
+
await emitWorkflowTelemetry(client, "warn", `${meta.toolName}.failed`, {
|
|
1402
|
+
sessionID: meta.sessionID,
|
|
1403
|
+
toolName: meta.toolName,
|
|
1404
|
+
proposalId,
|
|
1405
|
+
outcome: "error",
|
|
1406
|
+
reason: message,
|
|
1407
|
+
directory: meta.directory,
|
|
1408
|
+
})
|
|
1409
|
+
}
|
|
1098
1410
|
return JSON.stringify({ ok: false, error: message })
|
|
1099
1411
|
}
|
|
1100
1412
|
}
|
|
@@ -1104,6 +1416,7 @@ type AssetType =
|
|
|
1104
1416
|
| "agent"
|
|
1105
1417
|
| "command"
|
|
1106
1418
|
| "knowledge"
|
|
1419
|
+
| "lesson"
|
|
1107
1420
|
| "memory"
|
|
1108
1421
|
| "script"
|
|
1109
1422
|
| "skill"
|
|
@@ -1115,6 +1428,7 @@ const ASSET_TYPES = [
|
|
|
1115
1428
|
"agent",
|
|
1116
1429
|
"command",
|
|
1117
1430
|
"knowledge",
|
|
1431
|
+
"lesson",
|
|
1118
1432
|
"memory",
|
|
1119
1433
|
"script",
|
|
1120
1434
|
"skill",
|
|
@@ -1182,6 +1496,7 @@ type SearchHit = {
|
|
|
1182
1496
|
action?: string
|
|
1183
1497
|
editHint?: string
|
|
1184
1498
|
curated?: boolean
|
|
1499
|
+
quality?: string
|
|
1185
1500
|
}
|
|
1186
1501
|
|
|
1187
1502
|
type SearchResponse = {
|
|
@@ -1222,6 +1537,17 @@ function parseCliJson<T>(raw: string): T | CliError {
|
|
|
1222
1537
|
}
|
|
1223
1538
|
}
|
|
1224
1539
|
|
|
1540
|
+
function formatAkmInfoResponse(raw: string): string {
|
|
1541
|
+
const parsed = parseCliJson<Record<string, unknown>>(raw)
|
|
1542
|
+
if (isCliError(parsed)) return JSON.stringify(parsed)
|
|
1543
|
+
return JSON.stringify({
|
|
1544
|
+
ok: true,
|
|
1545
|
+
pluginVersion: PLUGIN_VERSION,
|
|
1546
|
+
pluginInstallLocation: PLUGIN_INSTALL_LOCATION,
|
|
1547
|
+
akmInfo: parsed,
|
|
1548
|
+
})
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1225
1551
|
function blockedToolResponse(args: Record<string, unknown>): string | null {
|
|
1226
1552
|
return typeof args.__akmBlocked === "string"
|
|
1227
1553
|
? JSON.stringify({ ok: false, error: args.__akmBlocked })
|
|
@@ -1341,6 +1667,18 @@ function classifyToolFeedback(value: unknown): "positive" | "negative" | undefin
|
|
|
1341
1667
|
return undefined
|
|
1342
1668
|
}
|
|
1343
1669
|
|
|
1670
|
+
function withProposedWarnings(raw: string): string {
|
|
1671
|
+
const parsed = safeJsonParse<SearchResponse>(raw)
|
|
1672
|
+
if (!parsed) return raw
|
|
1673
|
+
const hasProposed = parsed.hits?.some((hit) => hit.quality === "proposed") ?? false
|
|
1674
|
+
if (!hasProposed) return raw
|
|
1675
|
+
const warnings = parsed.warnings ?? []
|
|
1676
|
+
return JSON.stringify({
|
|
1677
|
+
...parsed,
|
|
1678
|
+
warnings: warnings.includes(PROPOSED_QUALITY_WARNING) ? warnings : [...warnings, PROPOSED_QUALITY_WARNING],
|
|
1679
|
+
})
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1344
1682
|
function truncateLogText(value: string, limit = 1_000): string {
|
|
1345
1683
|
return value.length > limit ? `${value.slice(0, limit)}…` : value
|
|
1346
1684
|
}
|
|
@@ -1583,6 +1921,7 @@ function createSearchArgs(input: {
|
|
|
1583
1921
|
limit?: number
|
|
1584
1922
|
source?: "local" | "stash" | "registry" | "both"
|
|
1585
1923
|
defaultSource?: "local" | "stash" | "registry" | "both"
|
|
1924
|
+
includeProposed?: boolean
|
|
1586
1925
|
}): string[] {
|
|
1587
1926
|
const args = ["search", input.query]
|
|
1588
1927
|
if (input.type) args.push("--type", input.type)
|
|
@@ -1592,6 +1931,7 @@ function createSearchArgs(input: {
|
|
|
1592
1931
|
} else if (input.defaultSource) {
|
|
1593
1932
|
args.push("--source", normalizeSearchSource(input.defaultSource))
|
|
1594
1933
|
}
|
|
1934
|
+
if (input.includeProposed) args.push("--include-proposed")
|
|
1595
1935
|
args.push("--detail", "normal")
|
|
1596
1936
|
return args
|
|
1597
1937
|
}
|
|
@@ -1656,6 +1996,10 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1656
1996
|
if (!sessionWorkflow.has(sid)) {
|
|
1657
1997
|
sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
|
|
1658
1998
|
}
|
|
1999
|
+
const proposalSummary = await getPendingProposalCount(logClient, sid)
|
|
2000
|
+
if (!proposalSummary.unsupported && proposalSummary.count > 0) {
|
|
2001
|
+
markContextEpochDirty(sid)
|
|
2002
|
+
}
|
|
1659
2003
|
} else if (type === "session.compacted" || type === "session.idle" || type === "session.deleted") {
|
|
1660
2004
|
if (!sid) return
|
|
1661
2005
|
const captured = captureSessionMemory(sid, type)
|
|
@@ -1711,23 +2055,6 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1711
2055
|
// Best-effort only.
|
|
1712
2056
|
}
|
|
1713
2057
|
},
|
|
1714
|
-
"experimental.session.compacting": async (input, output) => {
|
|
1715
|
-
try {
|
|
1716
|
-
const sid = input.sessionID
|
|
1717
|
-
if (!sid) return
|
|
1718
|
-
if (!Array.isArray(output.context)) return
|
|
1719
|
-
markContextEpochDirty(sid)
|
|
1720
|
-
const blocks = [
|
|
1721
|
-
sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
|
|
1722
|
-
sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
|
|
1723
|
-
sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
|
|
1724
|
-
sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
|
|
1725
|
-
]
|
|
1726
|
-
output.context.push(...applyContextBudget(blocks))
|
|
1727
|
-
} catch {
|
|
1728
|
-
// Never break compaction because of plugin context.
|
|
1729
|
-
}
|
|
1730
|
-
},
|
|
1731
2058
|
// experimental.chat.system.transform is how OpenCode exposes the
|
|
1732
2059
|
// additionalContext channel. We append the cached hints (once per session)
|
|
1733
2060
|
// and the curated assets (once per turn) so the next LLM call sees them.
|
|
@@ -1745,6 +2072,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1745
2072
|
sessionHints.get(sid) ? `${AKM_HINTS_PREFIX}\n\n${sessionHints.get(sid)}` : "",
|
|
1746
2073
|
sessionCurated.get(sid) ? `${AKM_CURATED_HEADER}\n${sessionCurated.get(sid)}${AKM_CURATED_TAIL}` : "",
|
|
1747
2074
|
sessionWorkflow.get(sid) ? formatWorkflowContext(sessionWorkflow.get(sid)!) : "",
|
|
2075
|
+
(await getPendingProposalCount(logClient, sid)).count > 0 && !(await getPendingProposalCount(logClient, sid)).unsupported ? formatPendingProposalContext((await getPendingProposalCount(logClient, sid)).count) : "",
|
|
1748
2076
|
sessionCuratorReport.get(sid) ? formatCuratorReportContext(sessionCuratorReport.get(sid)!) : "",
|
|
1749
2077
|
]
|
|
1750
2078
|
output.system.push(...applyContextBudget(blocks))
|
|
@@ -1784,6 +2112,53 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1784
2112
|
// Never break tool execution from the pre-hook.
|
|
1785
2113
|
}
|
|
1786
2114
|
},
|
|
2115
|
+
"permission.ask": async (input, output) => {
|
|
2116
|
+
try {
|
|
2117
|
+
const command = typeof input?.metadata?.command === "string"
|
|
2118
|
+
? input.metadata.command
|
|
2119
|
+
: Array.isArray(input?.patterns)
|
|
2120
|
+
? input.patterns.join(" && ")
|
|
2121
|
+
: ""
|
|
2122
|
+
if (!command.includes("akm")) return
|
|
2123
|
+
await emitWorkflowTelemetry(logClient, "info", "akm.raw_cli.invoked", {
|
|
2124
|
+
sessionID: input.sessionID,
|
|
2125
|
+
toolName: "bash",
|
|
2126
|
+
outcome: "requested",
|
|
2127
|
+
command,
|
|
2128
|
+
})
|
|
2129
|
+
const assessment = assessRiskyAkmCommand(command)
|
|
2130
|
+
if (!assessment) return
|
|
2131
|
+
output.status = "deny"
|
|
2132
|
+
await emitWorkflowTelemetry(logClient, "warn", "akm.raw_cli.blocked", {
|
|
2133
|
+
sessionID: input.sessionID,
|
|
2134
|
+
toolName: "bash",
|
|
2135
|
+
outcome: "blocked",
|
|
2136
|
+
reason: assessment.reason,
|
|
2137
|
+
command,
|
|
2138
|
+
category: assessment.category,
|
|
2139
|
+
})
|
|
2140
|
+
} catch {
|
|
2141
|
+
// Best-effort only.
|
|
2142
|
+
}
|
|
2143
|
+
},
|
|
2144
|
+
"command.execute.before": async (input, output) => {
|
|
2145
|
+
try {
|
|
2146
|
+
const command = `${input.command ?? ""} ${input.arguments ?? ""}`.trim()
|
|
2147
|
+
const assessment = assessRiskyAkmCommand(command)
|
|
2148
|
+
if (!assessment) return
|
|
2149
|
+
output.parts = [{ type: "text", text: blockedCommandMessage(command, assessment) }]
|
|
2150
|
+
await emitWorkflowTelemetry(logClient, "warn", "akm.raw_cli.blocked", {
|
|
2151
|
+
sessionID: input.sessionID,
|
|
2152
|
+
toolName: String(input.command ?? "bash"),
|
|
2153
|
+
outcome: "blocked",
|
|
2154
|
+
reason: assessment.reason,
|
|
2155
|
+
command,
|
|
2156
|
+
category: assessment.category,
|
|
2157
|
+
})
|
|
2158
|
+
} catch {
|
|
2159
|
+
// Best-effort only.
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
1787
2162
|
"shell.env": async (_input, output) => {
|
|
1788
2163
|
try {
|
|
1789
2164
|
output.env.AKM_PROJECT = worktree
|
|
@@ -1839,6 +2214,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1839
2214
|
}, dedupe)
|
|
1840
2215
|
}
|
|
1841
2216
|
}
|
|
2217
|
+
await recordRetrospectiveFeedback(logClient, input.sessionID, text)
|
|
1842
2218
|
},
|
|
1843
2219
|
"tool.execute.after": async (input, output) => {
|
|
1844
2220
|
if (!input.tool.startsWith("akm_")) return
|
|
@@ -1874,8 +2250,9 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1874
2250
|
// Auto-feedback + session buffering: record every asset ref the tool
|
|
1875
2251
|
// touched so the stash ranking improves over time and so Stop/Compact
|
|
1876
2252
|
// has material to flush into a session summary memory.
|
|
1877
|
-
|
|
1878
|
-
|
|
2253
|
+
const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2254
|
+
noteRecentRefs(input.sessionID, refResult.refs)
|
|
2255
|
+
if (refResult.refs.length > 0 && input.sessionID) {
|
|
1879
2256
|
for (const ref of refResult.refs) {
|
|
1880
2257
|
addBufferEntry(input.sessionID, {
|
|
1881
2258
|
kind: "tool-ref",
|
|
@@ -1929,6 +2306,14 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1929
2306
|
}
|
|
1930
2307
|
},
|
|
1931
2308
|
tool: {
|
|
2309
|
+
akm_info: tool({
|
|
2310
|
+
description: "Show `akm info` output plus the installed akm-opencode plugin version and install location.",
|
|
2311
|
+
args: {},
|
|
2312
|
+
async execute() {
|
|
2313
|
+
const raw = await runCli(client as unknown as LogCapableClient, ["info"], { toolName: "akm_info" })
|
|
2314
|
+
return formatAkmInfoResponse(raw)
|
|
2315
|
+
},
|
|
2316
|
+
}),
|
|
1932
2317
|
akm_search: tool({
|
|
1933
2318
|
description: "Search your stash or the akm registry for scripts, skills, commands, agents, knowledge, memories, workflows, vaults, and wikis. Use source='registry' for installable community kits.",
|
|
1934
2319
|
args: {
|
|
@@ -1942,9 +2327,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1942
2327
|
.enum(["local", "stash", "registry", "both"])
|
|
1943
2328
|
.optional()
|
|
1944
2329
|
.describe("Search source. 'stash' searches local stash directories, 'registry' searches registries, and 'both' searches all sources. 'local' remains a backward-compatible alias for 'stash'."),
|
|
2330
|
+
include_proposed: tool.schema.boolean().optional().describe("Include proposed-quality results. Proposed assets are not curated until accepted."),
|
|
1945
2331
|
},
|
|
1946
|
-
async execute({ query, type, limit, source }) {
|
|
1947
|
-
|
|
2332
|
+
async execute({ query, type, limit, source, include_proposed }, context) {
|
|
2333
|
+
const raw = await runCli(
|
|
2334
|
+
client as unknown as LogCapableClient,
|
|
2335
|
+
createSearchArgs({ query, type, limit, source, includeProposed: include_proposed }),
|
|
2336
|
+
{ toolName: "akm_search", sessionID: context.sessionID, directory: context.directory },
|
|
2337
|
+
)
|
|
2338
|
+
return withProposedWarnings(raw)
|
|
1948
2339
|
},
|
|
1949
2340
|
}),
|
|
1950
2341
|
akm_show: tool({
|
|
@@ -1982,11 +2373,12 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1982
2373
|
name: tool.schema.string().optional().describe("Optional memory name."),
|
|
1983
2374
|
force: tool.schema.boolean().optional().describe("Overwrite an existing memory with the same name."),
|
|
1984
2375
|
},
|
|
1985
|
-
async execute({ content, name, force }) {
|
|
2376
|
+
async execute({ content, name, force }, context) {
|
|
1986
2377
|
const args = ["remember", content]
|
|
1987
2378
|
if (name) args.push("--name", name)
|
|
1988
2379
|
if (force) args.push("--force")
|
|
1989
|
-
|
|
2380
|
+
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2381
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_remember", sessionID: context.sessionID, directory: context.directory })
|
|
1990
2382
|
},
|
|
1991
2383
|
}),
|
|
1992
2384
|
akm_feedback: tool({
|
|
@@ -1996,10 +2388,20 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1996
2388
|
sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
|
|
1997
2389
|
note: tool.schema.string().optional().describe("Optional note to attach to the feedback."),
|
|
1998
2390
|
},
|
|
1999
|
-
async execute({ ref, sentiment, note }) {
|
|
2391
|
+
async execute({ ref, sentiment, note }, context) {
|
|
2000
2392
|
const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"]
|
|
2001
2393
|
if (note) args.push("--note", note)
|
|
2002
|
-
|
|
2394
|
+
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2395
|
+
const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback", sessionID: context.sessionID, directory: context.directory })
|
|
2396
|
+
await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
|
|
2397
|
+
sessionID: context.sessionID,
|
|
2398
|
+
toolName: "akm_feedback",
|
|
2399
|
+
assetRef: ref,
|
|
2400
|
+
outcome: "success",
|
|
2401
|
+
reason: sentiment,
|
|
2402
|
+
directory: context.directory,
|
|
2403
|
+
})
|
|
2404
|
+
return raw
|
|
2003
2405
|
},
|
|
2004
2406
|
}),
|
|
2005
2407
|
akm_curate: tool({
|
|
@@ -2009,10 +2411,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2009
2411
|
limit: tool.schema.number().optional().describe("Maximum number of curated matches to return. Defaults to 6."),
|
|
2010
2412
|
detail: tool.schema.enum(["summary", "normal", "full"]).optional().describe("Detail level for each match. Defaults to 'summary'."),
|
|
2011
2413
|
},
|
|
2012
|
-
async execute({ query, limit, detail }) {
|
|
2414
|
+
async execute({ query, limit, detail }, context) {
|
|
2013
2415
|
const args = ["curate", query, "--limit", String(limit ?? 6)]
|
|
2014
2416
|
if (detail) args.push("--detail", detail)
|
|
2015
|
-
|
|
2417
|
+
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2418
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_curate", sessionID: context.sessionID, directory: context.directory })
|
|
2016
2419
|
},
|
|
2017
2420
|
}),
|
|
2018
2421
|
akm_evolve: tool({
|
|
@@ -2555,6 +2958,89 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2555
2958
|
}
|
|
2556
2959
|
},
|
|
2557
2960
|
}),
|
|
2961
|
+
akm_proposal: tool({
|
|
2962
|
+
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'.",
|
|
2963
|
+
args: {
|
|
2964
|
+
action: tool.schema.enum(["list", "show", "diff", "accept", "reject"]).describe("Proposal subcommand."),
|
|
2965
|
+
id: tool.schema.string().optional().describe("Proposal id. Required for show/diff/accept/reject."),
|
|
2966
|
+
status: tool.schema.enum(["pending", "accepted", "rejected"]).optional().describe("Filter for action='list'."),
|
|
2967
|
+
reason: tool.schema.string().optional().describe("Required for action='reject'. Recorded with the archived proposal."),
|
|
2968
|
+
},
|
|
2969
|
+
async execute({ action, id, status, reason }) {
|
|
2970
|
+
const logMeta = { toolName: "akm_proposal" }
|
|
2971
|
+
switch (action) {
|
|
2972
|
+
case "list": {
|
|
2973
|
+
const args = ["proposal", "list"]
|
|
2974
|
+
if (status) args.push("--status", status)
|
|
2975
|
+
return runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
2976
|
+
}
|
|
2977
|
+
case "show": {
|
|
2978
|
+
if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='show'." })
|
|
2979
|
+
return runCli(client as unknown as LogCapableClient, ["proposal", "show", id], logMeta)
|
|
2980
|
+
}
|
|
2981
|
+
case "diff": {
|
|
2982
|
+
if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='diff'." })
|
|
2983
|
+
return runCli(client as unknown as LogCapableClient, ["proposal", "diff", id], logMeta)
|
|
2984
|
+
}
|
|
2985
|
+
case "accept": {
|
|
2986
|
+
if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='accept'. Confirm with the user before accepting." })
|
|
2987
|
+
return runCli(client as unknown as LogCapableClient, ["proposal", "accept", id], logMeta)
|
|
2988
|
+
}
|
|
2989
|
+
case "reject": {
|
|
2990
|
+
if (!id) return JSON.stringify({ ok: false, error: "'id' is required for action='reject'. Confirm with the user before rejecting." })
|
|
2991
|
+
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." })
|
|
2992
|
+
return runCli(client as unknown as LogCapableClient, ["proposal", "reject", id, "--reason", reason], logMeta)
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
},
|
|
2996
|
+
}),
|
|
2997
|
+
akm_reflect: tool({
|
|
2998
|
+
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).",
|
|
2999
|
+
args: {
|
|
3000
|
+
ref: tool.schema.string().optional().describe("[origin//]type:name ref to reflect on. Optional — when omitted the agent reflects on overall session signal."),
|
|
3001
|
+
task: tool.schema.string().optional().describe("Free-form task description guiding the reflection."),
|
|
3002
|
+
},
|
|
3003
|
+
async execute({ ref, task }) {
|
|
3004
|
+
const args = ["reflect"]
|
|
3005
|
+
if (ref) args.push(ref)
|
|
3006
|
+
if (task) args.push("--task", task)
|
|
3007
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_reflect" })
|
|
3008
|
+
},
|
|
3009
|
+
}),
|
|
3010
|
+
akm_propose: tool({
|
|
3011
|
+
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).",
|
|
3012
|
+
args: {
|
|
3013
|
+
type: tool.schema.enum(["skill", "command", "agent", "knowledge", "lesson", "script", "workflow", "wiki"]).describe("Asset type for the new proposal."),
|
|
3014
|
+
name: tool.schema.string().describe("Slug for the new asset (matches the standard ref grammar)."),
|
|
3015
|
+
task: tool.schema.string().describe("Required. Describes what the asset should do."),
|
|
3016
|
+
},
|
|
3017
|
+
async execute({ type, name, task }) {
|
|
3018
|
+
if (!task || !task.trim()) return JSON.stringify({ ok: false, error: "'task' is required for akm_propose." })
|
|
3019
|
+
const args = ["propose", type, name, "--task", task]
|
|
3020
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_propose" })
|
|
3021
|
+
},
|
|
3022
|
+
}),
|
|
3023
|
+
akm_distill: tool({
|
|
3024
|
+
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.",
|
|
3025
|
+
args: {
|
|
3026
|
+
ref: tool.schema.string().describe("[origin//]type:name ref to distill — most often memory:<name> or knowledge:<name>."),
|
|
3027
|
+
},
|
|
3028
|
+
async execute({ ref }) {
|
|
3029
|
+
if (!ref || !ref.trim()) return JSON.stringify({ ok: false, error: "'ref' is required for akm_distill." })
|
|
3030
|
+
return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
|
|
3031
|
+
},
|
|
3032
|
+
}),
|
|
3033
|
+
akm_setup: tool({
|
|
3034
|
+
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.",
|
|
3035
|
+
args: {
|
|
3036
|
+
force: tool.schema.boolean().optional().describe("Re-run detection even when agent.default is already configured."),
|
|
3037
|
+
},
|
|
3038
|
+
async execute({ force }) {
|
|
3039
|
+
const args = ["setup"]
|
|
3040
|
+
if (force) args.push("--force")
|
|
3041
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_setup" })
|
|
3042
|
+
},
|
|
3043
|
+
}),
|
|
2558
3044
|
akm_help: tool({
|
|
2559
3045
|
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.",
|
|
2560
3046
|
args: {
|
|
@@ -2583,6 +3069,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2583
3069
|
hints: topic ? lookupAkmHelpHint(topic) : [],
|
|
2584
3070
|
quickReference: AKM_HELP_QUICK_REFERENCE,
|
|
2585
3071
|
help: helpText,
|
|
3072
|
+
workflowTopics: [
|
|
3073
|
+
"proposal",
|
|
3074
|
+
"distill",
|
|
3075
|
+
"reflect",
|
|
3076
|
+
"propose",
|
|
3077
|
+
"lesson",
|
|
3078
|
+
"include-proposed",
|
|
3079
|
+
"llm-features",
|
|
3080
|
+
"vault-safety",
|
|
3081
|
+
],
|
|
2586
3082
|
})
|
|
2587
3083
|
},
|
|
2588
3084
|
}),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenCode plugin for AKM - search, show, and manage extension assets via the akm CLI, including
|
|
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",
|