akm-opencode 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/index.ts +244 -141
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,7 +36,7 @@ The plugin exposes **20 high-value tools**. Long-tail verbs (`add`, `save`, `imp
36
36
  | `akm_reflect` | Generate a reflection proposal via the configured agent CLI; output lands in the proposal queue only |
37
37
  | `akm_propose` | Generate a new-asset proposal via the configured agent CLI; the result is `quality:"proposed"` until accepted |
38
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` |
39
+ | `akm_init` | Initialize AKM's working stash directory and persist `stashDir` in config. This is the agent-safe initialization path; interactive `akm setup` is human-facing |
40
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 |
41
41
 
42
42
  ## Compound-engineering hooks
@@ -47,8 +47,8 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
47
47
 
48
48
  | Event | What happens |
49
49
  | --- | --- |
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. |
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. |
50
+ | **`session.created`** (event hook) | Sets `agent.default` to `opencode` when missing, 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 start with relevant stash context. |
51
+ | **`chat.message`** | Records user feedback/memory intent and appends a short reminder to use `akm_search` / `akm_curate` when more stash context is needed. It does not auto-run AKM CLI lookups on every message. |
52
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. |
53
53
  | **`tool.execute.before`** (`akm_*` tools) | Blocks destructive or sensitive operations until `confirm:true` is provided. |
54
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. |
@@ -61,7 +61,7 @@ fails silently when `akm` is not on PATH — the TUI is never affected.
61
61
 
62
62
  | Variable | Default | Purpose |
63
63
  | --- | --- | --- |
64
- | `AKM_AUTO_CURATE` | `1` | Set to `0` to disable automatic `akm curate` on user messages. |
64
+ | `AKM_AUTO_CURATE` | `1` | Set to `0` to disable automatic `akm curate` on user messages. Session start no longer auto-curates. |
65
65
  | `AKM_AUTO_FEEDBACK` | `1` | Set to `0` to disable automatic `akm feedback` on tool success/failure. |
66
66
  | `AKM_AUTO_HINTS` | `1` | Set to `0` to skip injecting `akm hints` at session start. |
67
67
  | `AKM_AUTO_MEMORY` | `1` | Set to `0` to disable automatic session-summary memories. |
package/index.ts CHANGED
@@ -71,7 +71,7 @@ const AKM_WORKFLOW_INSTRUCTION = [
71
71
  "3. Record `akm_feedback` after the result is known.",
72
72
  "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.",
73
73
  "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`.",
74
- "6. Run `akm_setup` once on a fresh machine to detect installed agent CLIs and persist `agent.default`. Reflect/propose require it.",
74
+ "6. Use `akm_init` when you need to create the working stash or persist `stashDir`. Do not use `akm setup` from an agent; it is interactive and human-facing.",
75
75
  `7. ${PROPOSED_QUALITY_WARNING}`,
76
76
  "8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
77
77
  ].join("\n")
@@ -225,42 +225,17 @@ function needsAgentSetup(message: string): boolean {
225
225
 
226
226
  function addAgentSetupGuidance(message: string): string {
227
227
  if (!needsAgentSetup(message)) return message
228
- if (/\bakm_setup\b|\bakm setup\b/i.test(message)) return message
229
- return `${message}. Run akm_setup to detect an installed agent CLI and persist agent.default. If none is installed yet, install one of: opencode, claude, codex, gemini, aider.`
228
+ if (/\bakm init\b|\bakm_init\b|\bakm setup\b/i.test(message)) return message
229
+ return `${message}. Agents should not run akm setup because it is interactive. The plugin should set agent.default to the current platform when it is missing; if interactive configuration is still needed, ask the user to run akm setup manually. Use akm_init for agent-safe stash initialization.`
230
230
  }
231
231
 
232
- function toLogString(value: unknown): string | undefined {
233
- if (typeof value === "string") return value
234
- if (value instanceof Buffer) return value.toString("utf8")
235
- return undefined
236
- }
237
-
238
- function getExecStatus(error: unknown): number | null {
239
- if (!error || typeof error !== "object" || !("status" in error)) return null
240
- const status = (error as { status?: unknown }).status
241
- return typeof status === "number" ? status : null
242
- }
243
-
244
- async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
245
- try {
246
- await client.app.log({
247
- query: typeof extra.directory === "string" ? { directory: extra.directory } : undefined,
248
- body: {
249
- service: "akm-opencode",
250
- level,
251
- message,
252
- extra,
253
- },
254
- })
255
- } catch {
256
- // Avoid breaking the TUI if logging itself fails.
257
- }
258
- }
259
-
260
- async function runAgentSetup(client: LogCapableClient, meta: { directory?: string; sessionID?: string; trigger: string; force?: boolean }): Promise<boolean> {
232
+ async function ensurePlatformAgentDefault(
233
+ client: LogCapableClient,
234
+ meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
235
+ ): Promise<boolean> {
261
236
  const command = resolveAkmCommand()
262
237
  if (typeof command !== "string") {
263
- await writePluginLog(client, "warn", "AKM agent setup skipped", {
238
+ await writePluginLog(client, "warn", "AKM agent default check skipped", {
264
239
  subsystem: "akm",
265
240
  trigger: meta.trigger,
266
241
  sessionID: meta.sessionID,
@@ -270,59 +245,56 @@ async function runAgentSetup(client: LogCapableClient, meta: { directory?: strin
270
245
  return false
271
246
  }
272
247
 
273
- const args = ["setup"]
274
- if (meta.force) args.push("--force")
275
- args.push("--format", "json")
276
-
277
248
  try {
278
- const stdout = execFileSync(command, args, {
249
+ const currentRaw = execFileSync(command, ["config", "get", "agent.default", "--format", "json"], {
279
250
  encoding: "utf8",
280
251
  timeout: 30_000,
281
252
  })
282
- const parsed = safeJsonParse<{ ok?: boolean; error?: string }>(stdout)
283
- if (parsed?.ok === false) {
284
- await writePluginLog(client, "warn", "AKM agent setup reported an error", {
285
- subsystem: "akm",
286
- trigger: meta.trigger,
287
- sessionID: meta.sessionID,
288
- directory: meta.directory,
289
- command,
290
- args,
291
- stdout,
292
- error: parsed.error ?? "Unknown akm setup error",
293
- })
294
- return false
295
- }
296
- await writePluginLog(client, "info", "AKM agent setup completed", {
253
+ const parsed = safeJsonParse<unknown>(currentRaw)
254
+ const current = typeof parsed === "string"
255
+ ? parsed.trim()
256
+ : parsed && typeof parsed === "object"
257
+ ? typeof (parsed as Record<string, unknown>).value === "string"
258
+ ? ((parsed as Record<string, unknown>).value as string).trim()
259
+ : typeof (parsed as Record<string, unknown>)["agent.default"] === "string"
260
+ ? ((parsed as Record<string, unknown>)["agent.default"] as string).trim()
261
+ : ""
262
+ : ""
263
+ if (current) return true
264
+
265
+ execFileSync(command, ["config", "set", "agent.default", meta.platform, "--format", "json"], {
266
+ encoding: "utf8",
267
+ timeout: 30_000,
268
+ })
269
+ await writePluginLog(client, "info", "AKM agent default initialized", {
297
270
  subsystem: "akm",
298
271
  trigger: meta.trigger,
299
272
  sessionID: meta.sessionID,
300
273
  directory: meta.directory,
301
- command,
302
- args,
303
- stdout,
274
+ agentDefault: meta.platform,
304
275
  })
305
276
  return true
306
277
  } catch (error: unknown) {
307
- const message = addAgentSetupGuidance(formatCliError(error))
308
- await writePluginLog(client, "warn", "AKM agent setup failed", {
278
+ await writePluginLog(client, "warn", "AKM agent default initialization failed", {
309
279
  subsystem: "akm",
310
280
  trigger: meta.trigger,
311
281
  sessionID: meta.sessionID,
312
282
  directory: meta.directory,
313
- command,
314
- args,
315
- error: message,
283
+ platform: meta.platform,
284
+ error: addAgentSetupGuidance(formatCliError(error)),
316
285
  stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
317
- stderr: toLogString((error as { stderr?: unknown }).stderr) ?? message,
286
+ stderr: toLogString((error as { stderr?: unknown }).stderr) ?? "",
318
287
  })
319
288
  return false
320
289
  }
321
290
  }
322
291
 
323
- async function ensureAgentSetup(client: LogCapableClient, meta: { directory?: string; sessionID?: string; trigger: string; force?: boolean }): Promise<boolean> {
292
+ async function ensureAgentSetup(
293
+ client: LogCapableClient,
294
+ meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
295
+ ): Promise<boolean> {
324
296
  if (agentSetupPromise) return agentSetupPromise
325
- const promise = runAgentSetup(client, meta)
297
+ const promise = ensurePlatformAgentDefault(client, meta)
326
298
  const trackedPromise = promise.finally(() => {
327
299
  if (agentSetupPromise === trackedPromise) agentSetupPromise = null
328
300
  })
@@ -330,6 +302,52 @@ async function ensureAgentSetup(client: LogCapableClient, meta: { directory?: st
330
302
  return trackedPromise
331
303
  }
332
304
 
305
+ function isNotIndexedFeedbackError(message: string): boolean {
306
+ return /not in the current index|run ["'`]?akm index["'`]? and try again/i.test(message)
307
+ }
308
+
309
+ function toLogString(value: unknown): string | undefined {
310
+ if (typeof value === "string") return value
311
+ if (value instanceof Buffer) return value.toString("utf8")
312
+ return undefined
313
+ }
314
+
315
+ function getExecStatus(error: unknown): number | null {
316
+ if (!error || typeof error !== "object" || !("status" in error)) return null
317
+ const status = (error as { status?: unknown }).status
318
+ return typeof status === "number" ? status : null
319
+ }
320
+
321
+ async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
322
+ try {
323
+ await client.app.log({
324
+ query: typeof extra.directory === "string" ? { directory: extra.directory } : undefined,
325
+ body: {
326
+ service: "akm-opencode",
327
+ level,
328
+ message,
329
+ extra,
330
+ },
331
+ })
332
+ } catch {
333
+ // Avoid breaking the TUI if logging itself fails.
334
+ }
335
+ }
336
+
337
+ async function logHookFailure(
338
+ client: LogCapableClient,
339
+ hook: string,
340
+ error: unknown,
341
+ extra?: Record<string, unknown>,
342
+ ) {
343
+ await writePluginLog(client, "error", `AKM ${hook} hook failed`, {
344
+ subsystem: "hook",
345
+ hook,
346
+ error: formatCliError(error),
347
+ ...extra,
348
+ })
349
+ }
350
+
333
351
  function nowIso(): string {
334
352
  return new Date().toISOString()
335
353
  }
@@ -431,9 +449,20 @@ function runCurate(args: string[]): string | null {
431
449
  return body || null
432
450
  }
433
451
 
434
- function runCurateForPrompt(text: string, sessionID?: string): string | null {
452
+ async function runCurateLogged(
453
+ client: LogCapableClient,
454
+ args: string[],
455
+ meta: CliLogMeta & { operation: string },
456
+ ): Promise<string | null> {
457
+ return runCliSyncBestEffort(client, args, AKM_CURATE_TIMEOUT_MS, {
458
+ ...meta,
459
+ subsystem: "curation",
460
+ })
461
+ }
462
+
463
+ async function runCurateForPrompt(client: LogCapableClient, text: string, sessionID?: string): Promise<string | null> {
435
464
  if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
436
- return runCurate(
465
+ return runCurateLogged(client,
437
466
  appendRunScopeArgs(
438
467
  [
439
468
  "--detail",
@@ -448,11 +477,12 @@ function runCurateForPrompt(text: string, sessionID?: string): string | null {
448
477
  ],
449
478
  sessionID,
450
479
  ),
480
+ { toolName: "chat.message", sessionID, operation: "prompt-curate" },
451
481
  )
452
482
  }
453
483
 
454
- function runCurateForSession(sessionID: string): string | null {
455
- return runCurate(
484
+ async function runCurateForSession(client: LogCapableClient, sessionID: string): Promise<string | null> {
485
+ return runCurateLogged(client,
456
486
  appendRunScopeArgs(
457
487
  [
458
488
  "--detail",
@@ -466,14 +496,17 @@ function runCurateForSession(sessionID: string): string | null {
466
496
  ],
467
497
  sessionID,
468
498
  ),
499
+ { toolName: "session.start", sessionID, operation: "session-curate" },
469
500
  )
470
501
  }
471
502
 
472
- function runHintsForSession(): string | null {
473
- const result = runCliSyncRaw(["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS)
474
- if (!result.ok) return null
475
- const body = result.stdout.trim()
476
- return body || null
503
+ async function runHintsForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
504
+ return runCliSyncBestEffort(client, ["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS, {
505
+ toolName: "session.start",
506
+ sessionID,
507
+ subsystem: "hints",
508
+ operation: "session-hints",
509
+ })
477
510
  }
478
511
 
479
512
  function summarizeWorkflowList(value: unknown): string | null {
@@ -507,10 +540,15 @@ function summarizeWorkflowList(value: unknown): string | null {
507
540
  return null
508
541
  }
509
542
 
510
- function runWorkflowSummaryForSession(): string | null {
511
- const result = runCliSyncRaw(["--format", "json", "-q", "workflow", "list", "--active"], AKM_CURATE_TIMEOUT_MS)
512
- if (!result.ok) return null
513
- const parsed = parseMaybeJson(result.stdout)
543
+ async function runWorkflowSummaryForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
544
+ const raw = await runCliSyncBestEffort(client, ["--format", "json", "-q", "workflow", "list", "--active"], AKM_CURATE_TIMEOUT_MS, {
545
+ toolName: "session.start",
546
+ sessionID,
547
+ subsystem: "workflow",
548
+ operation: "active-workflow-summary",
549
+ })
550
+ if (!raw) return null
551
+ const parsed = parseMaybeJson(raw)
514
552
  const summary = summarizeWorkflowList(
515
553
  Array.isArray(parsed)
516
554
  ? parsed
@@ -547,14 +585,20 @@ function summarizeCuratorReportForContext(report: string): string {
547
585
  return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
548
586
  }
549
587
 
550
- function getAkmStashDir(): string | undefined {
588
+ async function getAkmStashDir(client?: LogCapableClient): Promise<string | undefined> {
551
589
  if (cachedAkmStashDir !== undefined) return cachedAkmStashDir || undefined
552
- const result = runCliSyncRaw(["--format", "json", "-q", "config", "get", "stashDir"], AKM_CURATE_TIMEOUT_MS)
553
- if (!result.ok) {
590
+ const raw = client
591
+ ? await runCliSyncBestEffort(client, ["--format", "json", "-q", "config", "get", "stashDir"], AKM_CURATE_TIMEOUT_MS, {
592
+ toolName: "shell.env",
593
+ subsystem: "config",
594
+ operation: "get-stash-dir",
595
+ })
596
+ : runCurate(["--format", "json", "-q", "config", "get", "stashDir"])
597
+ if (!raw) {
554
598
  cachedAkmStashDir = ""
555
599
  return undefined
556
600
  }
557
- const parsed = parseMaybeJson(result.stdout)
601
+ const parsed = parseMaybeJson(raw)
558
602
  if (typeof parsed === "string" && parsed.trim()) {
559
603
  cachedAkmStashDir = parsed.trim()
560
604
  return cachedAkmStashDir
@@ -568,7 +612,6 @@ function getAkmStashDir(): string | undefined {
568
612
  }
569
613
  }
570
614
  }
571
- const raw = result.stdout.trim()
572
615
  cachedAkmStashDir = raw || ""
573
616
  return cachedAkmStashDir || undefined
574
617
  }
@@ -602,6 +645,29 @@ function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventT
602
645
  })
603
646
  }
604
647
 
648
+ async function runCliSyncBestEffort(
649
+ client: LogCapableClient,
650
+ args: string[],
651
+ timeoutMs: number,
652
+ meta: CliLogMeta & { subsystem: string; operation: string },
653
+ ): Promise<string | null> {
654
+ const result = runCliSyncRaw(args, timeoutMs)
655
+ if (!result.ok) {
656
+ await writePluginLog(client, "warn", "AKM synchronous helper failed", {
657
+ subsystem: meta.subsystem,
658
+ operation: meta.operation,
659
+ toolName: meta.toolName,
660
+ sessionID: meta.sessionID,
661
+ directory: meta.directory,
662
+ args,
663
+ error: result.error,
664
+ })
665
+ return null
666
+ }
667
+ const body = result.stdout.trim()
668
+ return body || null
669
+ }
670
+
605
671
  function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
606
672
  if (!sessionID || refs.length === 0) return
607
673
  const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
@@ -659,7 +725,7 @@ async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID:
659
725
  sessionID,
660
726
  })
661
727
  const parsed = safeJsonParse<{ ok?: boolean }>(raw)
662
- if (parsed?.ok !== false) {
728
+ if (parsed?.ok === true) {
663
729
  await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
664
730
  sessionID,
665
731
  toolName: "akm_feedback",
@@ -1490,32 +1556,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
1490
1556
  } catch (error: unknown) {
1491
1557
  let message = formatCliError(error)
1492
1558
  if (["akm_reflect", "akm_propose"].includes(meta.toolName) && needsAgentSetup(message)) {
1493
- const setupOk = await ensureAgentSetup(client, {
1494
- directory: meta.directory,
1495
- sessionID: meta.sessionID,
1496
- trigger: `${meta.toolName}:retry`,
1497
- force: true,
1498
- })
1499
- if (setupOk) {
1500
- try {
1501
- const stdout = execFileSync(command, fullArgs, {
1502
- encoding: "utf8",
1503
- timeout: 60_000,
1504
- })
1505
- await writePluginLog(client, "info", "AKM command recovered after setup", {
1506
- subsystem: "akm",
1507
- toolName: meta.toolName,
1508
- sessionID: meta.sessionID,
1509
- directory: meta.directory,
1510
- command,
1511
- args: fullArgs,
1512
- })
1513
- return recordSuccess(stdout)
1514
- } catch (retryError: unknown) {
1515
- error = retryError
1516
- message = formatCliError(retryError)
1517
- }
1518
- }
1559
+ message = addAgentSetupGuidance(message)
1519
1560
  }
1520
1561
  message = addAgentSetupGuidance(message)
1521
1562
  await writePluginLog(client, "error", "AKM command failed", {
@@ -2116,10 +2157,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2116
2157
  directory,
2117
2158
  sessionID: sid,
2118
2159
  trigger: "session.created",
2160
+ platform: "opencode",
2119
2161
  })
2120
2162
  warmIndexInBackground()
2121
2163
  if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
2122
- const curated = runCurateForSession(sid)
2164
+ const curated = await runCurateForSession(logClient, sid)
2123
2165
  if (curated) {
2124
2166
  bumpCuratedVersion(sid)
2125
2167
  sessionCurated.set(sid, curated)
@@ -2127,11 +2169,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2127
2169
  }
2128
2170
  }
2129
2171
  if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
2130
- const hints = runHintsForSession()
2172
+ const hints = await runHintsForSession(logClient, sid)
2131
2173
  if (hints) sessionHints.set(sid, hints)
2132
2174
  }
2133
2175
  if (!sessionWorkflow.has(sid)) {
2134
- sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
2176
+ sessionWorkflow.set(sid, await runWorkflowSummaryForSession(logClient, sid) ?? "")
2135
2177
  }
2136
2178
  const proposalSummary = await getPendingProposalCount(logClient, sid)
2137
2179
  if (!proposalSummary.unsupported && proposalSummary.count > 0) {
@@ -2166,8 +2208,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2166
2208
  sessionBuffer.delete(sid)
2167
2209
  }
2168
2210
  }
2169
- } catch {
2170
- // Lifecycle hooks must never throw into the TUI.
2211
+ } catch (error: unknown) {
2212
+ await logHookFailure(logClient, "event", error)
2171
2213
  }
2172
2214
  },
2173
2215
  // Stop is the closest analogue to Claude's Stop/SubagentStop — the user or
@@ -2188,8 +2230,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2188
2230
  })
2189
2231
  await maybeIndexSessionMemory(logClient, sid, "stop", captured)
2190
2232
  }
2191
- } catch {
2192
- // Best-effort only.
2233
+ } catch (error: unknown) {
2234
+ await logHookFailure(logClient, "stop", error)
2193
2235
  }
2194
2236
  },
2195
2237
  // experimental.chat.system.transform is how OpenCode exposes the
@@ -2228,8 +2270,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2228
2270
  sessionCuratedInjectedVersion.set(sid, curatedVersion)
2229
2271
  }
2230
2272
  }
2231
- } catch {
2232
- // Never break the turn because of a transform failure.
2273
+ } catch (error: unknown) {
2274
+ await logHookFailure(logClient, "experimental.chat.system.transform", error)
2233
2275
  }
2234
2276
  },
2235
2277
  "tool.execute.before": async (input, output) => {
@@ -2245,8 +2287,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2245
2287
  return
2246
2288
  }
2247
2289
  output.args = args
2248
- } catch {
2249
- // Never break tool execution from the pre-hook.
2290
+ } catch (error: unknown) {
2291
+ await logHookFailure(logClient, "tool.execute.before", error, {
2292
+ toolName: input?.tool,
2293
+ sessionID: input?.sessionID,
2294
+ })
2250
2295
  }
2251
2296
  },
2252
2297
  "permission.ask": async (input, output) => {
@@ -2274,8 +2319,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2274
2319
  command,
2275
2320
  category: assessment.category,
2276
2321
  })
2277
- } catch {
2278
- // Best-effort only.
2322
+ } catch (error: unknown) {
2323
+ await logHookFailure(logClient, "permission.ask", error, {
2324
+ sessionID: input?.sessionID,
2325
+ command: typeof input?.metadata?.command === "string" ? input.metadata.command : undefined,
2326
+ })
2279
2327
  }
2280
2328
  },
2281
2329
  "command.execute.before": async (input, output) => {
@@ -2292,18 +2340,21 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2292
2340
  command,
2293
2341
  category: assessment.category,
2294
2342
  })
2295
- } catch {
2296
- // Best-effort only.
2343
+ } catch (error: unknown) {
2344
+ await logHookFailure(logClient, "command.execute.before", error, {
2345
+ sessionID: input?.sessionID,
2346
+ command: input?.command,
2347
+ })
2297
2348
  }
2298
2349
  },
2299
2350
  "shell.env": async (_input, output) => {
2300
2351
  try {
2301
2352
  output.env.AKM_PROJECT = worktree
2302
2353
  output.env.AKM_PLUGIN_VERSION = PLUGIN_VERSION
2303
- const stashDir = getAkmStashDir()
2354
+ const stashDir = await getAkmStashDir(logClient)
2304
2355
  if (stashDir) output.env.AKM_STASH_DIR = stashDir
2305
- } catch {
2306
- // Best-effort only.
2356
+ } catch (error: unknown) {
2357
+ await logHookFailure(logClient, "shell.env", error)
2307
2358
  }
2308
2359
  },
2309
2360
  "chat.message": async (input, output) => {
@@ -2319,12 +2370,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2319
2370
  text: truncateLogText(text),
2320
2371
  })
2321
2372
 
2322
- // Compound-engineering loop: on every user message, curate the stash and
2323
- // stash the result so experimental.chat.system.transform can inject it.
2373
+ // Session-start curation already seeded context. On later user messages,
2374
+ // inject only a small reminder instead of firing more automatic AKM CLI calls.
2324
2375
  if (AKM_AUTO_CURATE && input.sessionID) {
2325
- const curated = runCurateForPrompt(text, input.sessionID)
2326
- if (curated) {
2327
- sessionCurated.set(input.sessionID, curated)
2376
+ const hint = "Need more AKM context? Use `akm_search` or `akm_curate` before writing from scratch."
2377
+ const current = sessionCurated.get(input.sessionID) ?? ""
2378
+ if (!current.includes(hint)) {
2379
+ sessionCurated.set(input.sessionID, current ? `${current}\n\n${hint}` : hint)
2328
2380
  bumpCuratedVersion(input.sessionID)
2329
2381
  }
2330
2382
  }
@@ -2552,6 +2604,32 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2552
2604
  if (note) args.push("--note", note)
2553
2605
  args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
2554
2606
  const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback", sessionID: context.sessionID, directory: context.directory })
2607
+ const parsed = safeJsonParse<{ ok?: boolean; error?: string }>(raw)
2608
+ if (parsed?.ok === false) {
2609
+ const error = parsed.error ?? "Unknown akm feedback error"
2610
+ if (isNotIndexedFeedbackError(error)) {
2611
+ await writePluginLog(logClient, "warn", "AKM feedback skipped", {
2612
+ subsystem: "feedback",
2613
+ toolName: "akm_feedback",
2614
+ sessionID: context.sessionID,
2615
+ directory: context.directory,
2616
+ ref,
2617
+ sentiment,
2618
+ reason: "ref_not_indexed",
2619
+ error,
2620
+ })
2621
+ await emitWorkflowTelemetry(logClient, "warn", "akm.feedback.skipped", {
2622
+ sessionID: context.sessionID,
2623
+ toolName: "akm_feedback",
2624
+ assetRef: ref,
2625
+ outcome: "skipped",
2626
+ reason: "ref not indexed",
2627
+ directory: context.directory,
2628
+ })
2629
+ return JSON.stringify({ ok: true, skipped: true, reason: "ref_not_indexed", ref, sentiment })
2630
+ }
2631
+ return raw
2632
+ }
2555
2633
  await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
2556
2634
  sessionID: context.sessionID,
2557
2635
  toolName: "akm_feedback",
@@ -2916,6 +2994,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
2916
2994
  })
2917
2995
  return JSON.stringify({ ok: true, ref, shell: stdout.trim() })
2918
2996
  } catch (error: unknown) {
2997
+ await writePluginLog(logClient, "error", "AKM vault load failed", {
2998
+ subsystem: "vault",
2999
+ toolName: "akm_vault",
3000
+ action: "load",
3001
+ ref,
3002
+ error: formatCliError(error),
3003
+ })
2919
3004
  return JSON.stringify({ ok: false, error: formatCliError(error) })
2920
3005
  }
2921
3006
  }
@@ -3023,6 +3108,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
3023
3108
  })
3024
3109
  return stdout
3025
3110
  } catch (error: unknown) {
3111
+ await writePluginLog(logClient, "error", "AKM wiki stash failed", {
3112
+ subsystem: "wiki",
3113
+ toolName: "akm_wiki",
3114
+ action: "stash",
3115
+ name,
3116
+ source,
3117
+ as_slug,
3118
+ error: formatCliError(error),
3119
+ })
3026
3120
  return JSON.stringify({ ok: false, error: formatCliError(error) })
3027
3121
  }
3028
3122
  }
@@ -3130,6 +3224,12 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
3130
3224
  })
3131
3225
  return JSON.stringify({ ok: true, template: stdout })
3132
3226
  } catch (error: unknown) {
3227
+ await writePluginLog(logClient, "error", "AKM workflow template failed", {
3228
+ subsystem: "workflow",
3229
+ toolName: "akm_workflow",
3230
+ action: "template",
3231
+ error: formatCliError(error),
3232
+ })
3133
3233
  return JSON.stringify({ ok: false, error: formatCliError(error) })
3134
3234
  }
3135
3235
  }
@@ -3212,15 +3312,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
3212
3312
  return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
3213
3313
  },
3214
3314
  }),
3215
- akm_setup: tool({
3216
- 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.",
3217
- args: {
3218
- force: tool.schema.boolean().optional().describe("Re-run detection even when agent.default is already configured."),
3219
- },
3220
- async execute({ force }) {
3221
- const args = ["setup"]
3222
- if (force) args.push("--force")
3223
- return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_setup" })
3315
+ akm_init: tool({
3316
+ description: "Initialize AKM's working stash directory and persist `stashDir` in config. This is the agent-safe initialization path; do not use interactive `akm setup` from tools.",
3317
+ args: {},
3318
+ async execute() {
3319
+ return runCli(client as unknown as LogCapableClient, ["init"], { toolName: "akm_init" })
3224
3320
  },
3225
3321
  }),
3226
3322
  akm_help: tool({
@@ -3242,6 +3338,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
3242
3338
  timeout: 30_000,
3243
3339
  }).toString().trim()
3244
3340
  } catch (error: unknown) {
3341
+ await writePluginLog(logClient, "error", "AKM help command failed", {
3342
+ subsystem: "help",
3343
+ toolName: "akm_help",
3344
+ command: cliCommand,
3345
+ args: helpArgs,
3346
+ error: formatCliError(error),
3347
+ })
3245
3348
  return JSON.stringify({ ok: false, error: formatCliError(error) })
3246
3349
  }
3247
3350
  return JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-opencode",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
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": [