akm-opencode 0.7.2 → 0.7.5
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 +4 -4
- package/index.ts +590 -149
- 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
|
-
| `
|
|
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) |
|
|
51
|
-
| **`chat.message`** |
|
|
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
|
@@ -3,6 +3,11 @@ import { execFileSync, execSync, spawn } from "node:child_process"
|
|
|
3
3
|
import { readFileSync } from "node:fs"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
6
|
+
import { classifyFeedbackSignal, shouldSubmitAutomaticFeedback } from "../shared/feedback-signals"
|
|
7
|
+
import { appendCandidates, extractCandidatesFromText, getCandidateLogPath, readCandidates, updateCandidateStatus } from "../shared/memory-candidates"
|
|
8
|
+
import { appendMemoryEvent, getEventLogPath, readJsonl, type AkmMemoryEvent } from "../shared/memory-events"
|
|
9
|
+
import { shouldRecall } from "../shared/recall-policy"
|
|
10
|
+
import { redactObject, redactSecrets } from "../shared/redaction"
|
|
6
11
|
|
|
7
12
|
let resolvedAkmCommand = "akm"
|
|
8
13
|
const autoInstallPackageRef = process.env.AKM_PACKAGE_REF ?? "akm-cli@latest"
|
|
@@ -26,6 +31,8 @@ const AKM_RETROSPECTIVE_NEGATIVE_RE = createRetrospectiveNegativeRegex()
|
|
|
26
31
|
const AKM_EXPLICIT_CORRECTION_RE = createExplicitCorrectionRegex()
|
|
27
32
|
const PLUGIN_VERSION = readPackageVersion()
|
|
28
33
|
const PLUGIN_INSTALL_LOCATION = moduleDir
|
|
34
|
+
const OPENCODE_EVENT_LOG = getEventLogPath("opencode")
|
|
35
|
+
const OPENCODE_CANDIDATE_LOG = getCandidateLogPath("opencode")
|
|
29
36
|
|
|
30
37
|
// Per-session state that drives the compound-engineering loop.
|
|
31
38
|
// These maps are keyed by OpenCode sessionID.
|
|
@@ -37,6 +44,7 @@ const sessionContextEpoch = new Map<string, number>()
|
|
|
37
44
|
const sessionContextInjectedEpoch = new Map<string, number>()
|
|
38
45
|
const sessionCuratedVersion = new Map<string, number>()
|
|
39
46
|
const sessionCuratedInjectedVersion = new Map<string, number>()
|
|
47
|
+
const sessionRecallAudit = new Map<string, { shouldRecall: boolean; reason: string; query: string; injectedRefs: string[]; injectedChars: number; warnings: string[] }>()
|
|
40
48
|
type ParsedSemver = { core: [number, number, number]; prerelease: Array<string | number> | null }
|
|
41
49
|
type SessionBufferEntry = {
|
|
42
50
|
timestamp: string
|
|
@@ -71,7 +79,7 @@ const AKM_WORKFLOW_INSTRUCTION = [
|
|
|
71
79
|
"3. Record `akm_feedback` after the result is known.",
|
|
72
80
|
"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
81
|
"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.
|
|
82
|
+
"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
83
|
`7. ${PROPOSED_QUALITY_WARNING}`,
|
|
76
84
|
"8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
|
|
77
85
|
].join("\n")
|
|
@@ -225,42 +233,17 @@ function needsAgentSetup(message: string): boolean {
|
|
|
225
233
|
|
|
226
234
|
function addAgentSetupGuidance(message: string): string {
|
|
227
235
|
if (!needsAgentSetup(message)) return message
|
|
228
|
-
if (/\
|
|
229
|
-
return `${message}.
|
|
236
|
+
if (/\bakm init\b|\bakm_init\b|\bakm setup\b/i.test(message)) return message
|
|
237
|
+
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
238
|
}
|
|
231
239
|
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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> {
|
|
240
|
+
async function ensurePlatformAgentDefault(
|
|
241
|
+
client: LogCapableClient,
|
|
242
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
243
|
+
): Promise<boolean> {
|
|
261
244
|
const command = resolveAkmCommand()
|
|
262
245
|
if (typeof command !== "string") {
|
|
263
|
-
await writePluginLog(client, "warn", "AKM agent
|
|
246
|
+
await writePluginLog(client, "warn", "AKM agent default check skipped", {
|
|
264
247
|
subsystem: "akm",
|
|
265
248
|
trigger: meta.trigger,
|
|
266
249
|
sessionID: meta.sessionID,
|
|
@@ -270,59 +253,56 @@ async function runAgentSetup(client: LogCapableClient, meta: { directory?: strin
|
|
|
270
253
|
return false
|
|
271
254
|
}
|
|
272
255
|
|
|
273
|
-
const args = ["setup"]
|
|
274
|
-
if (meta.force) args.push("--force")
|
|
275
|
-
args.push("--format", "json")
|
|
276
|
-
|
|
277
256
|
try {
|
|
278
|
-
const
|
|
257
|
+
const currentRaw = execFileSync(command, ["config", "get", "agent.default", "--format", "json"], {
|
|
279
258
|
encoding: "utf8",
|
|
280
259
|
timeout: 30_000,
|
|
281
260
|
})
|
|
282
|
-
const parsed = safeJsonParse<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
261
|
+
const parsed = safeJsonParse<unknown>(currentRaw)
|
|
262
|
+
const current = typeof parsed === "string"
|
|
263
|
+
? parsed.trim()
|
|
264
|
+
: parsed && typeof parsed === "object"
|
|
265
|
+
? typeof (parsed as Record<string, unknown>).value === "string"
|
|
266
|
+
? ((parsed as Record<string, unknown>).value as string).trim()
|
|
267
|
+
: typeof (parsed as Record<string, unknown>)["agent.default"] === "string"
|
|
268
|
+
? ((parsed as Record<string, unknown>)["agent.default"] as string).trim()
|
|
269
|
+
: ""
|
|
270
|
+
: ""
|
|
271
|
+
if (current) return true
|
|
272
|
+
|
|
273
|
+
execFileSync(command, ["config", "set", "agent.default", meta.platform, "--format", "json"], {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
timeout: 30_000,
|
|
276
|
+
})
|
|
277
|
+
await writePluginLog(client, "info", "AKM agent default initialized", {
|
|
297
278
|
subsystem: "akm",
|
|
298
279
|
trigger: meta.trigger,
|
|
299
280
|
sessionID: meta.sessionID,
|
|
300
281
|
directory: meta.directory,
|
|
301
|
-
|
|
302
|
-
args,
|
|
303
|
-
stdout,
|
|
282
|
+
agentDefault: meta.platform,
|
|
304
283
|
})
|
|
305
284
|
return true
|
|
306
285
|
} catch (error: unknown) {
|
|
307
|
-
|
|
308
|
-
await writePluginLog(client, "warn", "AKM agent setup failed", {
|
|
286
|
+
await writePluginLog(client, "warn", "AKM agent default initialization failed", {
|
|
309
287
|
subsystem: "akm",
|
|
310
288
|
trigger: meta.trigger,
|
|
311
289
|
sessionID: meta.sessionID,
|
|
312
290
|
directory: meta.directory,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
error: message,
|
|
291
|
+
platform: meta.platform,
|
|
292
|
+
error: addAgentSetupGuidance(formatCliError(error)),
|
|
316
293
|
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
317
|
-
stderr: toLogString((error as { stderr?: unknown }).stderr) ??
|
|
294
|
+
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? "",
|
|
318
295
|
})
|
|
319
296
|
return false
|
|
320
297
|
}
|
|
321
298
|
}
|
|
322
299
|
|
|
323
|
-
async function ensureAgentSetup(
|
|
300
|
+
async function ensureAgentSetup(
|
|
301
|
+
client: LogCapableClient,
|
|
302
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
303
|
+
): Promise<boolean> {
|
|
324
304
|
if (agentSetupPromise) return agentSetupPromise
|
|
325
|
-
const promise =
|
|
305
|
+
const promise = ensurePlatformAgentDefault(client, meta)
|
|
326
306
|
const trackedPromise = promise.finally(() => {
|
|
327
307
|
if (agentSetupPromise === trackedPromise) agentSetupPromise = null
|
|
328
308
|
})
|
|
@@ -330,6 +310,74 @@ async function ensureAgentSetup(client: LogCapableClient, meta: { directory?: st
|
|
|
330
310
|
return trackedPromise
|
|
331
311
|
}
|
|
332
312
|
|
|
313
|
+
function isNotIndexedFeedbackError(message: string): boolean {
|
|
314
|
+
return /not in the current index|run ["'`]?akm index["'`]? and try again/i.test(message)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toLogString(value: unknown): string | undefined {
|
|
318
|
+
if (typeof value === "string") return value
|
|
319
|
+
if (value instanceof Buffer) return value.toString("utf8")
|
|
320
|
+
return undefined
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getExecStatus(error: unknown): number | null {
|
|
324
|
+
if (!error || typeof error !== "object" || !("status" in error)) return null
|
|
325
|
+
const status = (error as { status?: unknown }).status
|
|
326
|
+
return typeof status === "number" ? status : null
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
|
|
330
|
+
try {
|
|
331
|
+
const redacted = redactObject(extra)
|
|
332
|
+
await client.app.log({
|
|
333
|
+
query: typeof redacted.value.directory === "string" ? { directory: redacted.value.directory as string } : undefined,
|
|
334
|
+
body: {
|
|
335
|
+
service: "akm-opencode",
|
|
336
|
+
level,
|
|
337
|
+
message,
|
|
338
|
+
extra: redacted.value,
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
} catch {
|
|
342
|
+
// Avoid breaking the TUI if logging itself fails.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildEventScope(sessionID?: string, directory?: string, agent?: string) {
|
|
347
|
+
return {
|
|
348
|
+
user: process.env.AKM_USER_ID,
|
|
349
|
+
agent,
|
|
350
|
+
run: sessionID,
|
|
351
|
+
channel: process.env.AKM_CHANNEL,
|
|
352
|
+
project: directory,
|
|
353
|
+
repo: process.env.AKM_REPO,
|
|
354
|
+
branch: process.env.AKM_BRANCH,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function writeStructuredEvent(event: Omit<AkmMemoryEvent, "version" | "timestamp" | "harness">) {
|
|
359
|
+
return appendMemoryEvent(OPENCODE_EVENT_LOG, {
|
|
360
|
+
version: 1,
|
|
361
|
+
timestamp: nowIso(),
|
|
362
|
+
harness: "opencode",
|
|
363
|
+
...event,
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function logHookFailure(
|
|
368
|
+
client: LogCapableClient,
|
|
369
|
+
hook: string,
|
|
370
|
+
error: unknown,
|
|
371
|
+
extra?: Record<string, unknown>,
|
|
372
|
+
) {
|
|
373
|
+
await writePluginLog(client, "error", `AKM ${hook} hook failed`, {
|
|
374
|
+
subsystem: "hook",
|
|
375
|
+
hook,
|
|
376
|
+
error: formatCliError(error),
|
|
377
|
+
...extra,
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
333
381
|
function nowIso(): string {
|
|
334
382
|
return new Date().toISOString()
|
|
335
383
|
}
|
|
@@ -431,9 +479,20 @@ function runCurate(args: string[]): string | null {
|
|
|
431
479
|
return body || null
|
|
432
480
|
}
|
|
433
481
|
|
|
434
|
-
function
|
|
482
|
+
async function runCurateLogged(
|
|
483
|
+
client: LogCapableClient,
|
|
484
|
+
args: string[],
|
|
485
|
+
meta: CliLogMeta & { operation: string },
|
|
486
|
+
): Promise<string | null> {
|
|
487
|
+
return runCliSyncBestEffort(client, args, AKM_CURATE_TIMEOUT_MS, {
|
|
488
|
+
...meta,
|
|
489
|
+
subsystem: "curation",
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function runCurateForPrompt(client: LogCapableClient, text: string, sessionID?: string): Promise<string | null> {
|
|
435
494
|
if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
|
|
436
|
-
return
|
|
495
|
+
return runCurateLogged(client,
|
|
437
496
|
appendRunScopeArgs(
|
|
438
497
|
[
|
|
439
498
|
"--detail",
|
|
@@ -448,11 +507,12 @@ function runCurateForPrompt(text: string, sessionID?: string): string | null {
|
|
|
448
507
|
],
|
|
449
508
|
sessionID,
|
|
450
509
|
),
|
|
510
|
+
{ toolName: "chat.message", sessionID, operation: "prompt-curate" },
|
|
451
511
|
)
|
|
452
512
|
}
|
|
453
513
|
|
|
454
|
-
function runCurateForSession(sessionID: string): string | null {
|
|
455
|
-
return
|
|
514
|
+
async function runCurateForSession(client: LogCapableClient, sessionID: string): Promise<string | null> {
|
|
515
|
+
return runCurateLogged(client,
|
|
456
516
|
appendRunScopeArgs(
|
|
457
517
|
[
|
|
458
518
|
"--detail",
|
|
@@ -466,14 +526,17 @@ function runCurateForSession(sessionID: string): string | null {
|
|
|
466
526
|
],
|
|
467
527
|
sessionID,
|
|
468
528
|
),
|
|
529
|
+
{ toolName: "session.start", sessionID, operation: "session-curate" },
|
|
469
530
|
)
|
|
470
531
|
}
|
|
471
532
|
|
|
472
|
-
function runHintsForSession(): string | null {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
533
|
+
async function runHintsForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
|
|
534
|
+
return runCliSyncBestEffort(client, ["--format", "text", "-q", "hints"], AKM_CURATE_TIMEOUT_MS, {
|
|
535
|
+
toolName: "session.start",
|
|
536
|
+
sessionID,
|
|
537
|
+
subsystem: "hints",
|
|
538
|
+
operation: "session-hints",
|
|
539
|
+
})
|
|
477
540
|
}
|
|
478
541
|
|
|
479
542
|
function summarizeWorkflowList(value: unknown): string | null {
|
|
@@ -507,10 +570,15 @@ function summarizeWorkflowList(value: unknown): string | null {
|
|
|
507
570
|
return null
|
|
508
571
|
}
|
|
509
572
|
|
|
510
|
-
function runWorkflowSummaryForSession(): string | null {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
573
|
+
async function runWorkflowSummaryForSession(client: LogCapableClient, sessionID?: string): Promise<string | null> {
|
|
574
|
+
const raw = await runCliSyncBestEffort(client, ["--format", "json", "-q", "workflow", "list", "--active"], AKM_CURATE_TIMEOUT_MS, {
|
|
575
|
+
toolName: "session.start",
|
|
576
|
+
sessionID,
|
|
577
|
+
subsystem: "workflow",
|
|
578
|
+
operation: "active-workflow-summary",
|
|
579
|
+
})
|
|
580
|
+
if (!raw) return null
|
|
581
|
+
const parsed = parseMaybeJson(raw)
|
|
514
582
|
const summary = summarizeWorkflowList(
|
|
515
583
|
Array.isArray(parsed)
|
|
516
584
|
? parsed
|
|
@@ -547,14 +615,20 @@ function summarizeCuratorReportForContext(report: string): string {
|
|
|
547
615
|
return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
|
|
548
616
|
}
|
|
549
617
|
|
|
550
|
-
function getAkmStashDir(): string | undefined {
|
|
618
|
+
async function getAkmStashDir(client?: LogCapableClient): Promise<string | undefined> {
|
|
551
619
|
if (cachedAkmStashDir !== undefined) return cachedAkmStashDir || undefined
|
|
552
|
-
const
|
|
553
|
-
|
|
620
|
+
const raw = client
|
|
621
|
+
? await runCliSyncBestEffort(client, ["--format", "json", "-q", "config", "get", "stashDir"], AKM_CURATE_TIMEOUT_MS, {
|
|
622
|
+
toolName: "shell.env",
|
|
623
|
+
subsystem: "config",
|
|
624
|
+
operation: "get-stash-dir",
|
|
625
|
+
})
|
|
626
|
+
: runCurate(["--format", "json", "-q", "config", "get", "stashDir"])
|
|
627
|
+
if (!raw) {
|
|
554
628
|
cachedAkmStashDir = ""
|
|
555
629
|
return undefined
|
|
556
630
|
}
|
|
557
|
-
const parsed = parseMaybeJson(
|
|
631
|
+
const parsed = parseMaybeJson(raw)
|
|
558
632
|
if (typeof parsed === "string" && parsed.trim()) {
|
|
559
633
|
cachedAkmStashDir = parsed.trim()
|
|
560
634
|
return cachedAkmStashDir
|
|
@@ -568,7 +642,6 @@ function getAkmStashDir(): string | undefined {
|
|
|
568
642
|
}
|
|
569
643
|
}
|
|
570
644
|
}
|
|
571
|
-
const raw = result.stdout.trim()
|
|
572
645
|
cachedAkmStashDir = raw || ""
|
|
573
646
|
return cachedAkmStashDir || undefined
|
|
574
647
|
}
|
|
@@ -594,6 +667,19 @@ function safeJsonParse<T>(raw: string): T | undefined {
|
|
|
594
667
|
}
|
|
595
668
|
|
|
596
669
|
function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventType: string, extra: Record<string, unknown>) {
|
|
670
|
+
const mappedEvent: AkmMemoryEvent["event"] = eventType.includes("workflow_")
|
|
671
|
+
? eventType as AkmMemoryEvent["event"]
|
|
672
|
+
: eventType.includes("blocked")
|
|
673
|
+
? "safety_blocked"
|
|
674
|
+
: "workflow_step"
|
|
675
|
+
void writeStructuredEvent({
|
|
676
|
+
event: mappedEvent,
|
|
677
|
+
sessionId: typeof extra.sessionID === "string" ? extra.sessionID : undefined,
|
|
678
|
+
workflowRunId: typeof extra.runId === "string" ? extra.runId : undefined,
|
|
679
|
+
scope: buildEventScope(typeof extra.sessionID === "string" ? extra.sessionID : undefined, typeof extra.directory === "string" ? extra.directory : undefined, typeof extra.toolName === "string" ? extra.toolName : undefined),
|
|
680
|
+
input: redactObject(extra).value as Record<string, unknown>,
|
|
681
|
+
outcome: { status: level === "error" ? "failed" : level === "warn" ? "blocked" : "ok", warnings: typeof extra.reason === "string" ? [extra.reason] : undefined },
|
|
682
|
+
})
|
|
597
683
|
return writePluginLog(client, level, eventType, {
|
|
598
684
|
subsystem: "workflow-compliance",
|
|
599
685
|
eventType,
|
|
@@ -602,6 +688,29 @@ function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventT
|
|
|
602
688
|
})
|
|
603
689
|
}
|
|
604
690
|
|
|
691
|
+
async function runCliSyncBestEffort(
|
|
692
|
+
client: LogCapableClient,
|
|
693
|
+
args: string[],
|
|
694
|
+
timeoutMs: number,
|
|
695
|
+
meta: CliLogMeta & { subsystem: string; operation: string },
|
|
696
|
+
): Promise<string | null> {
|
|
697
|
+
const result = runCliSyncRaw(args, timeoutMs)
|
|
698
|
+
if (!result.ok) {
|
|
699
|
+
await writePluginLog(client, "warn", "AKM synchronous helper failed", {
|
|
700
|
+
subsystem: meta.subsystem,
|
|
701
|
+
operation: meta.operation,
|
|
702
|
+
toolName: meta.toolName,
|
|
703
|
+
sessionID: meta.sessionID,
|
|
704
|
+
directory: meta.directory,
|
|
705
|
+
args,
|
|
706
|
+
error: result.error,
|
|
707
|
+
})
|
|
708
|
+
return null
|
|
709
|
+
}
|
|
710
|
+
const body = result.stdout.trim()
|
|
711
|
+
return body || null
|
|
712
|
+
}
|
|
713
|
+
|
|
605
714
|
function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
|
|
606
715
|
if (!sessionID || refs.length === 0) return
|
|
607
716
|
const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
|
|
@@ -659,7 +768,7 @@ async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID:
|
|
|
659
768
|
sessionID,
|
|
660
769
|
})
|
|
661
770
|
const parsed = safeJsonParse<{ ok?: boolean }>(raw)
|
|
662
|
-
if (parsed?.ok
|
|
771
|
+
if (parsed?.ok === true) {
|
|
663
772
|
await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
|
|
664
773
|
sessionID,
|
|
665
774
|
toolName: "akm_feedback",
|
|
@@ -811,7 +920,7 @@ function rememberTextAsMemory(name: string, body: string): string | null {
|
|
|
811
920
|
execFileSync(command, ["--format", "json", "-q", "remember", "--name", name, "--force"], {
|
|
812
921
|
encoding: "utf8",
|
|
813
922
|
timeout: AKM_CURATE_TIMEOUT_MS * 2,
|
|
814
|
-
input: body,
|
|
923
|
+
input: redactSecrets(body).text,
|
|
815
924
|
})
|
|
816
925
|
return `memory:${name}`
|
|
817
926
|
} catch {
|
|
@@ -837,6 +946,13 @@ function captureSessionMemory(
|
|
|
837
946
|
}
|
|
838
947
|
|
|
839
948
|
const lines: string[] = []
|
|
949
|
+
lines.push("---")
|
|
950
|
+
lines.push("akm_memory_kind: session_checkpoint")
|
|
951
|
+
lines.push("harness: opencode")
|
|
952
|
+
lines.push(`session_id: ${sessionID}`)
|
|
953
|
+
lines.push(`reason: ${reason}`)
|
|
954
|
+
lines.push("---")
|
|
955
|
+
lines.push("")
|
|
840
956
|
lines.push(`# Session summary (${nowIso()})`)
|
|
841
957
|
lines.push(`Reason: ${reason}`)
|
|
842
958
|
lines.push(`Session: ${sessionID}`)
|
|
@@ -871,6 +987,26 @@ function captureSessionMemory(
|
|
|
871
987
|
}
|
|
872
988
|
|
|
873
989
|
if (isCheckpoint) {
|
|
990
|
+
void writeStructuredEvent({
|
|
991
|
+
event: "pre_compact_checkpoint",
|
|
992
|
+
sessionId: sessionID,
|
|
993
|
+
scope: buildEventScope(sessionID),
|
|
994
|
+
memory: { ref, reason, kind: "session_checkpoint" },
|
|
995
|
+
refs: [ref],
|
|
996
|
+
outcome: { status: "ok" },
|
|
997
|
+
})
|
|
998
|
+
const candidates = extractCandidatesFromText({ harness: "opencode", sessionId: sessionID, text: body, evidence: [ref, reason] })
|
|
999
|
+
if (candidates.length > 0) {
|
|
1000
|
+
appendCandidates(OPENCODE_CANDIDATE_LOG, candidates)
|
|
1001
|
+
void writeStructuredEvent({
|
|
1002
|
+
event: "candidate_extracted",
|
|
1003
|
+
sessionId: sessionID,
|
|
1004
|
+
scope: buildEventScope(sessionID),
|
|
1005
|
+
memory: { sourceRef: ref, count: candidates.length },
|
|
1006
|
+
refs: [ref],
|
|
1007
|
+
outcome: { status: "ok" },
|
|
1008
|
+
})
|
|
1009
|
+
}
|
|
874
1010
|
for (const entry of entries) {
|
|
875
1011
|
if (!entry.checkpointed) entry.checkpointed = true
|
|
876
1012
|
}
|
|
@@ -881,6 +1017,26 @@ function captureSessionMemory(
|
|
|
881
1017
|
|
|
882
1018
|
sessionFinalMemoryCaptured.add(sessionID)
|
|
883
1019
|
sessionBuffer.delete(sessionID)
|
|
1020
|
+
void writeStructuredEvent({
|
|
1021
|
+
event: "session_ended",
|
|
1022
|
+
sessionId: sessionID,
|
|
1023
|
+
scope: buildEventScope(sessionID),
|
|
1024
|
+
memory: { ref, reason, kind: "session_checkpoint" },
|
|
1025
|
+
refs: [ref],
|
|
1026
|
+
outcome: { status: "ok" },
|
|
1027
|
+
})
|
|
1028
|
+
const candidates = extractCandidatesFromText({ harness: "opencode", sessionId: sessionID, text: body, evidence: [ref, reason] })
|
|
1029
|
+
if (candidates.length > 0) {
|
|
1030
|
+
appendCandidates(OPENCODE_CANDIDATE_LOG, candidates)
|
|
1031
|
+
void writeStructuredEvent({
|
|
1032
|
+
event: "candidate_extracted",
|
|
1033
|
+
sessionId: sessionID,
|
|
1034
|
+
scope: buildEventScope(sessionID),
|
|
1035
|
+
memory: { sourceRef: ref, count: candidates.length },
|
|
1036
|
+
refs: [ref],
|
|
1037
|
+
outcome: { status: "ok" },
|
|
1038
|
+
})
|
|
1039
|
+
}
|
|
884
1040
|
return ref
|
|
885
1041
|
}
|
|
886
1042
|
|
|
@@ -1490,32 +1646,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1490
1646
|
} catch (error: unknown) {
|
|
1491
1647
|
let message = formatCliError(error)
|
|
1492
1648
|
if (["akm_reflect", "akm_propose"].includes(meta.toolName) && needsAgentSetup(message)) {
|
|
1493
|
-
|
|
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
|
-
}
|
|
1649
|
+
message = addAgentSetupGuidance(message)
|
|
1519
1650
|
}
|
|
1520
1651
|
message = addAgentSetupGuidance(message)
|
|
1521
1652
|
await writePluginLog(client, "error", "AKM command failed", {
|
|
@@ -2110,16 +2241,24 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2110
2241
|
const sid = extractSessionIdFromEvent(event) ?? extractSessionIdFromEvent((event as { properties?: unknown }).properties)
|
|
2111
2242
|
if (type === "session.created" || type === "session.updated") {
|
|
2112
2243
|
if (!sid) return
|
|
2244
|
+
writeStructuredEvent({
|
|
2245
|
+
event: "session_started",
|
|
2246
|
+
sessionId: sid,
|
|
2247
|
+
scope: buildEventScope(sid, directory),
|
|
2248
|
+
input: { type },
|
|
2249
|
+
outcome: { status: "ok" },
|
|
2250
|
+
})
|
|
2113
2251
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
2114
2252
|
if (type === "session.created") {
|
|
2115
2253
|
await ensureAgentSetup(logClient, {
|
|
2116
2254
|
directory,
|
|
2117
2255
|
sessionID: sid,
|
|
2118
2256
|
trigger: "session.created",
|
|
2257
|
+
platform: "opencode",
|
|
2119
2258
|
})
|
|
2120
2259
|
warmIndexInBackground()
|
|
2121
2260
|
if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
|
|
2122
|
-
const curated = runCurateForSession(sid)
|
|
2261
|
+
const curated = await runCurateForSession(logClient, sid)
|
|
2123
2262
|
if (curated) {
|
|
2124
2263
|
bumpCuratedVersion(sid)
|
|
2125
2264
|
sessionCurated.set(sid, curated)
|
|
@@ -2127,11 +2266,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2127
2266
|
}
|
|
2128
2267
|
}
|
|
2129
2268
|
if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
|
|
2130
|
-
const hints = runHintsForSession()
|
|
2269
|
+
const hints = await runHintsForSession(logClient, sid)
|
|
2131
2270
|
if (hints) sessionHints.set(sid, hints)
|
|
2132
2271
|
}
|
|
2133
2272
|
if (!sessionWorkflow.has(sid)) {
|
|
2134
|
-
sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
|
|
2273
|
+
sessionWorkflow.set(sid, await runWorkflowSummaryForSession(logClient, sid) ?? "")
|
|
2135
2274
|
}
|
|
2136
2275
|
const proposalSummary = await getPendingProposalCount(logClient, sid)
|
|
2137
2276
|
if (!proposalSummary.unsupported && proposalSummary.count > 0) {
|
|
@@ -2150,6 +2289,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2150
2289
|
})
|
|
2151
2290
|
await maybeIndexSessionMemory(logClient, sid, type, captured)
|
|
2152
2291
|
}
|
|
2292
|
+
if (type === "session.compacted") {
|
|
2293
|
+
writeStructuredEvent({
|
|
2294
|
+
event: "post_compact_summary",
|
|
2295
|
+
sessionId: sid,
|
|
2296
|
+
scope: buildEventScope(sid, directory),
|
|
2297
|
+
memory: { ref: captured ?? null, reason: type },
|
|
2298
|
+
outcome: { status: captured ? "ok" : "skipped" },
|
|
2299
|
+
})
|
|
2300
|
+
}
|
|
2153
2301
|
// Drop per-session state so a re-created session does not inherit
|
|
2154
2302
|
// stale hints/curation.
|
|
2155
2303
|
if (type === "session.deleted") {
|
|
@@ -2166,8 +2314,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2166
2314
|
sessionBuffer.delete(sid)
|
|
2167
2315
|
}
|
|
2168
2316
|
}
|
|
2169
|
-
} catch {
|
|
2170
|
-
|
|
2317
|
+
} catch (error: unknown) {
|
|
2318
|
+
await logHookFailure(logClient, "event", error)
|
|
2171
2319
|
}
|
|
2172
2320
|
},
|
|
2173
2321
|
// Stop is the closest analogue to Claude's Stop/SubagentStop — the user or
|
|
@@ -2188,8 +2336,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2188
2336
|
})
|
|
2189
2337
|
await maybeIndexSessionMemory(logClient, sid, "stop", captured)
|
|
2190
2338
|
}
|
|
2191
|
-
} catch {
|
|
2192
|
-
|
|
2339
|
+
} catch (error: unknown) {
|
|
2340
|
+
await logHookFailure(logClient, "stop", error)
|
|
2193
2341
|
}
|
|
2194
2342
|
},
|
|
2195
2343
|
// experimental.chat.system.transform is how OpenCode exposes the
|
|
@@ -2228,8 +2376,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2228
2376
|
sessionCuratedInjectedVersion.set(sid, curatedVersion)
|
|
2229
2377
|
}
|
|
2230
2378
|
}
|
|
2231
|
-
} catch {
|
|
2232
|
-
|
|
2379
|
+
} catch (error: unknown) {
|
|
2380
|
+
await logHookFailure(logClient, "experimental.chat.system.transform", error)
|
|
2233
2381
|
}
|
|
2234
2382
|
},
|
|
2235
2383
|
"tool.execute.before": async (input, output) => {
|
|
@@ -2244,9 +2392,19 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2244
2392
|
}
|
|
2245
2393
|
return
|
|
2246
2394
|
}
|
|
2395
|
+
if (input.tool === "akm_memory" && ["promote", "reject"].includes(String(args.action ?? "")) && !confirm) {
|
|
2396
|
+
output.args = {
|
|
2397
|
+
...args,
|
|
2398
|
+
__akmBlocked: `akm_memory action='${String(args.action)}' requires confirm:true because it mutates candidate or memory state.`,
|
|
2399
|
+
}
|
|
2400
|
+
return
|
|
2401
|
+
}
|
|
2247
2402
|
output.args = args
|
|
2248
|
-
} catch {
|
|
2249
|
-
|
|
2403
|
+
} catch (error: unknown) {
|
|
2404
|
+
await logHookFailure(logClient, "tool.execute.before", error, {
|
|
2405
|
+
toolName: input?.tool,
|
|
2406
|
+
sessionID: input?.sessionID,
|
|
2407
|
+
})
|
|
2250
2408
|
}
|
|
2251
2409
|
},
|
|
2252
2410
|
"permission.ask": async (input, output) => {
|
|
@@ -2274,8 +2432,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2274
2432
|
command,
|
|
2275
2433
|
category: assessment.category,
|
|
2276
2434
|
})
|
|
2277
|
-
} catch {
|
|
2278
|
-
|
|
2435
|
+
} catch (error: unknown) {
|
|
2436
|
+
await logHookFailure(logClient, "permission.ask", error, {
|
|
2437
|
+
sessionID: input?.sessionID,
|
|
2438
|
+
command: typeof input?.metadata?.command === "string" ? input.metadata.command : undefined,
|
|
2439
|
+
})
|
|
2279
2440
|
}
|
|
2280
2441
|
},
|
|
2281
2442
|
"command.execute.before": async (input, output) => {
|
|
@@ -2292,18 +2453,21 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2292
2453
|
command,
|
|
2293
2454
|
category: assessment.category,
|
|
2294
2455
|
})
|
|
2295
|
-
} catch {
|
|
2296
|
-
|
|
2456
|
+
} catch (error: unknown) {
|
|
2457
|
+
await logHookFailure(logClient, "command.execute.before", error, {
|
|
2458
|
+
sessionID: input?.sessionID,
|
|
2459
|
+
command: input?.command,
|
|
2460
|
+
})
|
|
2297
2461
|
}
|
|
2298
2462
|
},
|
|
2299
2463
|
"shell.env": async (_input, output) => {
|
|
2300
2464
|
try {
|
|
2301
2465
|
output.env.AKM_PROJECT = worktree
|
|
2302
2466
|
output.env.AKM_PLUGIN_VERSION = PLUGIN_VERSION
|
|
2303
|
-
const stashDir = getAkmStashDir()
|
|
2467
|
+
const stashDir = await getAkmStashDir(logClient)
|
|
2304
2468
|
if (stashDir) output.env.AKM_STASH_DIR = stashDir
|
|
2305
|
-
} catch {
|
|
2306
|
-
|
|
2469
|
+
} catch (error: unknown) {
|
|
2470
|
+
await logHookFailure(logClient, "shell.env", error)
|
|
2307
2471
|
}
|
|
2308
2472
|
},
|
|
2309
2473
|
"chat.message": async (input, output) => {
|
|
@@ -2319,13 +2483,53 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2319
2483
|
text: truncateLogText(text),
|
|
2320
2484
|
})
|
|
2321
2485
|
|
|
2322
|
-
// Compound-engineering loop: on every user message, curate the stash and
|
|
2323
|
-
// stash the result so experimental.chat.system.transform can inject it.
|
|
2324
2486
|
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
2325
|
-
const
|
|
2326
|
-
if (
|
|
2327
|
-
|
|
2328
|
-
|
|
2487
|
+
const decision = shouldRecall(text, { activeWorkflow: !!sessionWorkflow.get(input.sessionID), recentAssetFailure: retrospectiveState.get(input.sessionID)?.lastNegativeSignalAt != null })
|
|
2488
|
+
if (decision.shouldRecall) {
|
|
2489
|
+
const curated = await runCurateForPrompt(logClient, decision.query, input.sessionID)
|
|
2490
|
+
const refs = [...new Set((curated ?? "").match(/(?:[A-Za-z0-9@._+/-]+\/\/)?(?:skill|command|agent|knowledge|memory|lesson|script|workflow|vault|wiki):[A-Za-z0-9._/-]+/g) ?? [])]
|
|
2491
|
+
sessionRecallAudit.set(input.sessionID, {
|
|
2492
|
+
shouldRecall: true,
|
|
2493
|
+
reason: decision.reason,
|
|
2494
|
+
query: decision.query,
|
|
2495
|
+
injectedRefs: refs,
|
|
2496
|
+
injectedChars: curated?.length ?? 0,
|
|
2497
|
+
warnings: [],
|
|
2498
|
+
})
|
|
2499
|
+
writeStructuredEvent({
|
|
2500
|
+
event: "prompt_recall",
|
|
2501
|
+
sessionId: input.sessionID,
|
|
2502
|
+
scope: buildEventScope(input.sessionID, directory, input.agent),
|
|
2503
|
+
input: { promptPreview: text.slice(0, 280), query: decision.query, reason: decision.reason },
|
|
2504
|
+
refs,
|
|
2505
|
+
outcome: { status: curated ? "ok" : "skipped" },
|
|
2506
|
+
})
|
|
2507
|
+
if (curated) {
|
|
2508
|
+
sessionCurated.set(input.sessionID, curated)
|
|
2509
|
+
bumpCuratedVersion(input.sessionID)
|
|
2510
|
+
}
|
|
2511
|
+
} else {
|
|
2512
|
+
const hint = "Need more AKM context? Use `akm_search` or `akm_curate` before writing from scratch."
|
|
2513
|
+
sessionRecallAudit.set(input.sessionID, {
|
|
2514
|
+
shouldRecall: false,
|
|
2515
|
+
reason: decision.reason,
|
|
2516
|
+
query: decision.query,
|
|
2517
|
+
injectedRefs: [],
|
|
2518
|
+
injectedChars: 0,
|
|
2519
|
+
warnings: [],
|
|
2520
|
+
})
|
|
2521
|
+
writeStructuredEvent({
|
|
2522
|
+
event: "prompt_recall",
|
|
2523
|
+
sessionId: input.sessionID,
|
|
2524
|
+
scope: buildEventScope(input.sessionID, directory, input.agent),
|
|
2525
|
+
input: { promptPreview: text.slice(0, 280), query: decision.query, reason: decision.reason },
|
|
2526
|
+
outcome: { status: "skipped" },
|
|
2527
|
+
})
|
|
2528
|
+
const current = sessionCurated.get(input.sessionID) ?? ""
|
|
2529
|
+
if (!current.includes(hint)) {
|
|
2530
|
+
sessionCurated.set(input.sessionID, current ? `${current}\n\n${hint}` : hint)
|
|
2531
|
+
bumpCuratedVersion(input.sessionID)
|
|
2532
|
+
}
|
|
2329
2533
|
}
|
|
2330
2534
|
}
|
|
2331
2535
|
|
|
@@ -2401,6 +2605,14 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2401
2605
|
// has material to flush into a session summary memory.
|
|
2402
2606
|
const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2403
2607
|
noteRecentRefs(input.sessionID, refResult.refs)
|
|
2608
|
+
writeStructuredEvent({
|
|
2609
|
+
event: "tool_observation",
|
|
2610
|
+
sessionId: input.sessionID,
|
|
2611
|
+
scope: buildEventScope(input.sessionID, input.directory, input.tool),
|
|
2612
|
+
input: { tool: input.tool, callID: input.callID, args: input.args as Record<string, unknown>, output: parsed as Record<string, unknown> },
|
|
2613
|
+
refs: refResult.refs,
|
|
2614
|
+
outcome: { status: feedback === "negative" ? "failed" : "ok" },
|
|
2615
|
+
})
|
|
2404
2616
|
if (refResult.refs.length > 0 && input.sessionID) {
|
|
2405
2617
|
for (const ref of refResult.refs) {
|
|
2406
2618
|
addBufferEntry(input.sessionID, {
|
|
@@ -2446,10 +2658,40 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2446
2658
|
// memories do not accept feedback, and vault values never surface in
|
|
2447
2659
|
// JSON so automatic usage signals would be misleading.
|
|
2448
2660
|
if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
|
|
2449
|
-
const
|
|
2661
|
+
const directInput = Object.values(input.args as Record<string, unknown>).some((value) => typeof value === "string" && value.includes(ref))
|
|
2662
|
+
const signal = classifyFeedbackSignal({
|
|
2663
|
+
ref,
|
|
2664
|
+
polarity: feedback,
|
|
2665
|
+
harness: "opencode",
|
|
2666
|
+
sessionId: input.sessionID,
|
|
2667
|
+
directInput,
|
|
2668
|
+
note: `${note}; confidence=${directInput ? "0.65" : "0.25"}; source=${feedback === "positive" ? "tool_success" : "tool_failure"}`,
|
|
2669
|
+
})
|
|
2670
|
+
if (!shouldSubmitAutomaticFeedback(signal)) {
|
|
2671
|
+
writeStructuredEvent({
|
|
2672
|
+
event: "feedback_recorded",
|
|
2673
|
+
sessionId: input.sessionID,
|
|
2674
|
+
scope: buildEventScope(input.sessionID, input.directory, input.tool),
|
|
2675
|
+
refs: [ref],
|
|
2676
|
+
input: { source: signal.source, confidence: signal.confidence, note: signal.note },
|
|
2677
|
+
outcome: { status: "skipped", warnings: ["confidence below automatic submission threshold"] },
|
|
2678
|
+
})
|
|
2679
|
+
continue
|
|
2680
|
+
}
|
|
2681
|
+
const ok = queueFeedback(logClient, ref, feedback, signal.note, {
|
|
2450
2682
|
toolName: input.tool,
|
|
2451
2683
|
sessionID: input.sessionID,
|
|
2452
2684
|
}, dedupe)
|
|
2685
|
+
if (ok) {
|
|
2686
|
+
writeStructuredEvent({
|
|
2687
|
+
event: "feedback_recorded",
|
|
2688
|
+
sessionId: input.sessionID,
|
|
2689
|
+
scope: buildEventScope(input.sessionID, input.directory, input.tool),
|
|
2690
|
+
refs: [ref],
|
|
2691
|
+
input: { source: signal.source, confidence: signal.confidence, note: signal.note },
|
|
2692
|
+
outcome: { status: "ok" },
|
|
2693
|
+
})
|
|
2694
|
+
}
|
|
2453
2695
|
if (!ok) break
|
|
2454
2696
|
}
|
|
2455
2697
|
}
|
|
@@ -2552,6 +2794,32 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2552
2794
|
if (note) args.push("--note", note)
|
|
2553
2795
|
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2554
2796
|
const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_feedback", sessionID: context.sessionID, directory: context.directory })
|
|
2797
|
+
const parsed = safeJsonParse<{ ok?: boolean; error?: string }>(raw)
|
|
2798
|
+
if (parsed?.ok === false) {
|
|
2799
|
+
const error = parsed.error ?? "Unknown akm feedback error"
|
|
2800
|
+
if (isNotIndexedFeedbackError(error)) {
|
|
2801
|
+
await writePluginLog(logClient, "warn", "AKM feedback skipped", {
|
|
2802
|
+
subsystem: "feedback",
|
|
2803
|
+
toolName: "akm_feedback",
|
|
2804
|
+
sessionID: context.sessionID,
|
|
2805
|
+
directory: context.directory,
|
|
2806
|
+
ref,
|
|
2807
|
+
sentiment,
|
|
2808
|
+
reason: "ref_not_indexed",
|
|
2809
|
+
error,
|
|
2810
|
+
})
|
|
2811
|
+
await emitWorkflowTelemetry(logClient, "warn", "akm.feedback.skipped", {
|
|
2812
|
+
sessionID: context.sessionID,
|
|
2813
|
+
toolName: "akm_feedback",
|
|
2814
|
+
assetRef: ref,
|
|
2815
|
+
outcome: "skipped",
|
|
2816
|
+
reason: "ref not indexed",
|
|
2817
|
+
directory: context.directory,
|
|
2818
|
+
})
|
|
2819
|
+
return JSON.stringify({ ok: true, skipped: true, reason: "ref_not_indexed", ref, sentiment })
|
|
2820
|
+
}
|
|
2821
|
+
return raw
|
|
2822
|
+
}
|
|
2555
2823
|
await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
|
|
2556
2824
|
sessionID: context.sessionID,
|
|
2557
2825
|
toolName: "akm_feedback",
|
|
@@ -2563,6 +2831,108 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2563
2831
|
return raw
|
|
2564
2832
|
},
|
|
2565
2833
|
}),
|
|
2834
|
+
akm_memory: tool({
|
|
2835
|
+
description: "Audit AKM memory behavior, inspect pending candidates, and promote or reject candidates. Mutating actions require confirm:true.",
|
|
2836
|
+
args: {
|
|
2837
|
+
action: tool.schema.enum(["audit", "candidates", "promote", "reject", "checkpoint", "sync"]).describe("Memory action."),
|
|
2838
|
+
candidate_id: tool.schema.string().optional().describe("Candidate id for promote/reject."),
|
|
2839
|
+
reason: tool.schema.string().optional().describe("Reason for rejection."),
|
|
2840
|
+
scope: tool.schema.enum(["last-prompt", "session", "refs", "safety"]).optional().describe("Audit scope."),
|
|
2841
|
+
status: tool.schema.enum(["pending", "promoted", "rejected"]).optional().describe("Candidate status filter."),
|
|
2842
|
+
confirm: tool.schema.boolean().optional().describe("Must be true for mutate actions."),
|
|
2843
|
+
},
|
|
2844
|
+
async execute(input, context) {
|
|
2845
|
+
const blocked = blockedToolResponse(input as Record<string, unknown>)
|
|
2846
|
+
if (blocked) return blocked
|
|
2847
|
+
if (input.action === "audit") {
|
|
2848
|
+
const events = readJsonl<AkmMemoryEvent>(OPENCODE_EVENT_LOG)
|
|
2849
|
+
const sessionEvents = context.sessionID ? events.filter((event) => event.sessionId === context.sessionID) : events
|
|
2850
|
+
const recall = sessionRecallAudit.get(context.sessionID)
|
|
2851
|
+
const recentWrites = sessionEvents.filter((event) => ["durable_memory_written", "pre_compact_checkpoint", "session_ended"].includes(event.event)).slice(-5)
|
|
2852
|
+
const recentSafetyBlocks = sessionEvents.filter((event) => event.event === "safety_blocked").slice(-5)
|
|
2853
|
+
const refs = [...new Set(sessionEvents.flatMap((event) => event.refs ?? []))]
|
|
2854
|
+
const audit = input.scope === "last-prompt"
|
|
2855
|
+
? { lastRecall: recall ?? null }
|
|
2856
|
+
: input.scope === "refs"
|
|
2857
|
+
? { refs }
|
|
2858
|
+
: input.scope === "safety"
|
|
2859
|
+
? { recentSafetyBlocks }
|
|
2860
|
+
: input.scope === "session"
|
|
2861
|
+
? { lastRecall: recall ?? null, recentWrites, recentSafetyBlocks, refs }
|
|
2862
|
+
: { lastRecall: recall ?? null, recentWrites, recentSafetyBlocks, refs }
|
|
2863
|
+
return JSON.stringify({
|
|
2864
|
+
ok: true,
|
|
2865
|
+
audit,
|
|
2866
|
+
})
|
|
2867
|
+
}
|
|
2868
|
+
if (input.action === "candidates") {
|
|
2869
|
+
const candidates = readCandidates(OPENCODE_CANDIDATE_LOG)
|
|
2870
|
+
.filter((candidate) => !input.status || candidate.status === input.status)
|
|
2871
|
+
.filter((candidate) => !context.sessionID || candidate.sessionId === context.sessionID)
|
|
2872
|
+
return JSON.stringify({ ok: true, candidates })
|
|
2873
|
+
}
|
|
2874
|
+
if (input.action === "checkpoint") {
|
|
2875
|
+
const ref = captureSessionMemory(context.sessionID, "manual-checkpoint", { checkpoint: true })
|
|
2876
|
+
return JSON.stringify({ ok: true, ref })
|
|
2877
|
+
}
|
|
2878
|
+
if (input.action === "sync") {
|
|
2879
|
+
return JSON.stringify({ ok: true, synced: true, events: readJsonl<AkmMemoryEvent>(OPENCODE_EVENT_LOG).length, candidates: readCandidates(OPENCODE_CANDIDATE_LOG).length })
|
|
2880
|
+
}
|
|
2881
|
+
if (!input.candidate_id) return JSON.stringify({ ok: false, error: "'candidate_id' is required for promote/reject." })
|
|
2882
|
+
const candidates = readCandidates(OPENCODE_CANDIDATE_LOG)
|
|
2883
|
+
const candidate = candidates.find((entry) => entry.id === input.candidate_id)
|
|
2884
|
+
if (!candidate) return JSON.stringify({ ok: false, error: `Unknown candidate '${input.candidate_id}'.` })
|
|
2885
|
+
if (input.action === "reject") {
|
|
2886
|
+
const updated = updateCandidateStatus(OPENCODE_CANDIDATE_LOG, candidate.id, "rejected", input.reason)
|
|
2887
|
+
writeStructuredEvent({
|
|
2888
|
+
event: "candidate_rejected",
|
|
2889
|
+
sessionId: context.sessionID,
|
|
2890
|
+
scope: buildEventScope(context.sessionID, context.directory, context.agent),
|
|
2891
|
+
memory: { candidateId: candidate.id, reason: input.reason ?? null },
|
|
2892
|
+
outcome: { status: updated ? "ok" : "failed" },
|
|
2893
|
+
})
|
|
2894
|
+
return JSON.stringify({ ok: !!updated, candidate: updated ?? null })
|
|
2895
|
+
}
|
|
2896
|
+
if (input.action === "promote") {
|
|
2897
|
+
let rawResult: string | null = null
|
|
2898
|
+
if (candidate.recommendedAction === "remember") {
|
|
2899
|
+
rawResult = await runCli(client as unknown as LogCapableClient, ["remember", candidate.content, "--name", `candidate-${candidate.id}`, "--force", ...buildScopedArgs(context as unknown as Record<string, unknown>)], { toolName: "akm_memory", sessionID: context.sessionID, directory: context.directory })
|
|
2900
|
+
} else if (candidate.recommendedAction === "feedback" && candidate.targetRef) {
|
|
2901
|
+
rawResult = await runCli(client as unknown as LogCapableClient, ["feedback", candidate.targetRef, "--positive", "--note", candidate.content, ...buildScopedArgs(context as unknown as Record<string, unknown>)], { toolName: "akm_memory", sessionID: context.sessionID, directory: context.directory })
|
|
2902
|
+
} else if (candidate.recommendedAction === "distill" && candidate.targetRef) {
|
|
2903
|
+
rawResult = await runCli(client as unknown as LogCapableClient, ["distill", candidate.targetRef], { toolName: "akm_memory", sessionID: context.sessionID, directory: context.directory })
|
|
2904
|
+
} else if (candidate.recommendedAction === "propose") {
|
|
2905
|
+
rawResult = await runCli(client as unknown as LogCapableClient, ["propose", "knowledge", `candidate-${candidate.id}`, "--task", candidate.content], { toolName: "akm_memory", sessionID: context.sessionID, directory: context.directory })
|
|
2906
|
+
} else {
|
|
2907
|
+
return JSON.stringify({ ok: false, error: "Candidate recommendedAction is 'ignore'; reject it instead." })
|
|
2908
|
+
}
|
|
2909
|
+
const parsedResult = rawResult ? parseMaybeJson(rawResult) : null
|
|
2910
|
+
const resultOk = typeof parsedResult === "object" && parsedResult !== null && "ok" in parsedResult
|
|
2911
|
+
? parsedResult.ok !== false
|
|
2912
|
+
: !!rawResult
|
|
2913
|
+
if (!resultOk) {
|
|
2914
|
+
writeStructuredEvent({
|
|
2915
|
+
event: "candidate_promoted",
|
|
2916
|
+
sessionId: context.sessionID,
|
|
2917
|
+
scope: buildEventScope(context.sessionID, context.directory, context.agent),
|
|
2918
|
+
memory: { candidateId: candidate.id, recommendedAction: candidate.recommendedAction },
|
|
2919
|
+
outcome: { status: "failed", warnings: ["candidate promotion command did not succeed"] },
|
|
2920
|
+
})
|
|
2921
|
+
return JSON.stringify({ ok: false, candidate, result: parsedResult ?? rawResult })
|
|
2922
|
+
}
|
|
2923
|
+
const updated = updateCandidateStatus(OPENCODE_CANDIDATE_LOG, candidate.id, "promoted")
|
|
2924
|
+
writeStructuredEvent({
|
|
2925
|
+
event: "candidate_promoted",
|
|
2926
|
+
sessionId: context.sessionID,
|
|
2927
|
+
scope: buildEventScope(context.sessionID, context.directory, context.agent),
|
|
2928
|
+
memory: { candidateId: candidate.id, recommendedAction: candidate.recommendedAction },
|
|
2929
|
+
outcome: { status: updated ? "ok" : "failed" },
|
|
2930
|
+
})
|
|
2931
|
+
return JSON.stringify({ ok: !!updated, candidate: updated ?? null, result: parsedResult ?? rawResult })
|
|
2932
|
+
}
|
|
2933
|
+
return JSON.stringify({ ok: false, error: `Unsupported action '${String(input.action)}'.` })
|
|
2934
|
+
},
|
|
2935
|
+
}),
|
|
2566
2936
|
akm_curate: tool({
|
|
2567
2937
|
description: "Curate stash assets for a task or topic. Returns the top matches as a ranked list so the agent can inspect and use them.",
|
|
2568
2938
|
args: {
|
|
@@ -2744,6 +3114,24 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2744
3114
|
const model = parseModelHint(shown.modelHint)
|
|
2745
3115
|
const tools = parseToolPolicy(shown.toolPolicy)
|
|
2746
3116
|
|
|
3117
|
+
writeStructuredEvent({
|
|
3118
|
+
event: "subagent_started",
|
|
3119
|
+
sessionId: context.sessionID,
|
|
3120
|
+
scope: buildEventScope(context.sessionID, context.directory, context.agent),
|
|
3121
|
+
input: {
|
|
3122
|
+
ref: resolved.ref,
|
|
3123
|
+
stashAgent: shown.name,
|
|
3124
|
+
dispatchAgent: targetAgent,
|
|
3125
|
+
taskPrompt: task_prompt,
|
|
3126
|
+
advisoryToolPolicy: shown.toolPolicy ?? null,
|
|
3127
|
+
appliedToolPolicy: tools ?? null,
|
|
3128
|
+
modelHint: shown.modelHint ?? null,
|
|
3129
|
+
appliedModel: model ?? null,
|
|
3130
|
+
},
|
|
3131
|
+
refs: [resolved.ref],
|
|
3132
|
+
outcome: { status: "ok" },
|
|
3133
|
+
})
|
|
3134
|
+
|
|
2747
3135
|
const targetSession = await ensureTargetSessionID({
|
|
2748
3136
|
useSubtask,
|
|
2749
3137
|
context: { sessionID: context.sessionID, directory: context.directory },
|
|
@@ -2774,6 +3162,19 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2774
3162
|
})
|
|
2775
3163
|
if (!promptResponse.ok) return JSON.stringify(promptResponse)
|
|
2776
3164
|
|
|
3165
|
+
const childText = extractText(promptResponse.data.parts)
|
|
3166
|
+
const candidates = extractCandidatesFromText({ harness: "opencode", sessionId: targetSession.sessionID, text: childText, evidence: [resolved.ref, task_prompt] })
|
|
3167
|
+
if (candidates.length > 0) appendCandidates(OPENCODE_CANDIDATE_LOG, candidates)
|
|
3168
|
+
writeStructuredEvent({
|
|
3169
|
+
event: "subagent_completed",
|
|
3170
|
+
sessionId: context.sessionID,
|
|
3171
|
+
scope: buildEventScope(context.sessionID, context.directory, context.agent),
|
|
3172
|
+
input: { childSessionID: targetSession.sessionID, stashAgent: shown.name, dispatchAgent: targetAgent },
|
|
3173
|
+
memory: { parentSessionID: context.sessionID, childSessionID: targetSession.sessionID, candidatesCreated: candidates.length },
|
|
3174
|
+
refs: [resolved.ref],
|
|
3175
|
+
outcome: { status: "ok" },
|
|
3176
|
+
})
|
|
3177
|
+
|
|
2777
3178
|
return JSON.stringify({
|
|
2778
3179
|
ok: true,
|
|
2779
3180
|
ref: resolved.ref,
|
|
@@ -2783,7 +3184,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2783
3184
|
sessionID: targetSession.sessionID,
|
|
2784
3185
|
model,
|
|
2785
3186
|
tools,
|
|
2786
|
-
text:
|
|
3187
|
+
text: childText,
|
|
2787
3188
|
})
|
|
2788
3189
|
},
|
|
2789
3190
|
}),
|
|
@@ -2916,6 +3317,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2916
3317
|
})
|
|
2917
3318
|
return JSON.stringify({ ok: true, ref, shell: stdout.trim() })
|
|
2918
3319
|
} catch (error: unknown) {
|
|
3320
|
+
await writePluginLog(logClient, "error", "AKM vault load failed", {
|
|
3321
|
+
subsystem: "vault",
|
|
3322
|
+
toolName: "akm_vault",
|
|
3323
|
+
action: "load",
|
|
3324
|
+
ref,
|
|
3325
|
+
error: formatCliError(error),
|
|
3326
|
+
})
|
|
2919
3327
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
2920
3328
|
}
|
|
2921
3329
|
}
|
|
@@ -3023,6 +3431,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3023
3431
|
})
|
|
3024
3432
|
return stdout
|
|
3025
3433
|
} catch (error: unknown) {
|
|
3434
|
+
await writePluginLog(logClient, "error", "AKM wiki stash failed", {
|
|
3435
|
+
subsystem: "wiki",
|
|
3436
|
+
toolName: "akm_wiki",
|
|
3437
|
+
action: "stash",
|
|
3438
|
+
name,
|
|
3439
|
+
source,
|
|
3440
|
+
as_slug,
|
|
3441
|
+
error: formatCliError(error),
|
|
3442
|
+
})
|
|
3026
3443
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3027
3444
|
}
|
|
3028
3445
|
}
|
|
@@ -3082,14 +3499,18 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3082
3499
|
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='start'." })
|
|
3083
3500
|
const args = ["workflow", "start", ref]
|
|
3084
3501
|
if (params) args.push("--params", params)
|
|
3085
|
-
|
|
3502
|
+
const raw = await runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
3503
|
+
void emitWorkflowTelemetry(logClient, "info", "workflow_started", { toolName: "akm_workflow", ref, params: params ?? null })
|
|
3504
|
+
return raw
|
|
3086
3505
|
}
|
|
3087
3506
|
case "next": {
|
|
3088
3507
|
const picked = target ?? run_id ?? ref
|
|
3089
3508
|
if (!picked) return JSON.stringify({ ok: false, error: "'target', 'run_id', or 'ref' is required for action='next'." })
|
|
3090
3509
|
const args = ["workflow", "next", picked]
|
|
3091
3510
|
if (params) args.push("--params", params)
|
|
3092
|
-
|
|
3511
|
+
const raw = await runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
3512
|
+
void emitWorkflowTelemetry(logClient, "info", "workflow_next_loaded", { toolName: "akm_workflow", target: picked, params: params ?? null })
|
|
3513
|
+
return raw
|
|
3093
3514
|
}
|
|
3094
3515
|
case "complete": {
|
|
3095
3516
|
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='complete'." })
|
|
@@ -3098,7 +3519,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3098
3519
|
if (state) args.push("--state", state)
|
|
3099
3520
|
if (notes) args.push("--notes", notes)
|
|
3100
3521
|
if (evidence) args.push("--evidence", evidence)
|
|
3101
|
-
|
|
3522
|
+
const raw = await runCli(client as unknown as LogCapableClient, args, logMeta)
|
|
3523
|
+
const eventType = state === "blocked"
|
|
3524
|
+
? "workflow_step_blocked"
|
|
3525
|
+
: state === "failed"
|
|
3526
|
+
? "workflow_step_failed"
|
|
3527
|
+
: state === "skipped"
|
|
3528
|
+
? "workflow_step_skipped"
|
|
3529
|
+
: "workflow_step_completed"
|
|
3530
|
+
void emitWorkflowTelemetry(logClient, "info", eventType, { toolName: "akm_workflow", runId: run_id, step, notes: notes ?? null, evidence: evidence ?? null })
|
|
3531
|
+
return raw
|
|
3102
3532
|
}
|
|
3103
3533
|
case "status": {
|
|
3104
3534
|
const picked = target ?? run_id ?? ref
|
|
@@ -3130,12 +3560,20 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3130
3560
|
})
|
|
3131
3561
|
return JSON.stringify({ ok: true, template: stdout })
|
|
3132
3562
|
} catch (error: unknown) {
|
|
3563
|
+
await writePluginLog(logClient, "error", "AKM workflow template failed", {
|
|
3564
|
+
subsystem: "workflow",
|
|
3565
|
+
toolName: "akm_workflow",
|
|
3566
|
+
action: "template",
|
|
3567
|
+
error: formatCliError(error),
|
|
3568
|
+
})
|
|
3133
3569
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3134
3570
|
}
|
|
3135
3571
|
}
|
|
3136
3572
|
case "resume": {
|
|
3137
3573
|
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='resume'." })
|
|
3138
|
-
|
|
3574
|
+
const raw = await runCli(client as unknown as LogCapableClient, ["workflow", "resume", run_id], logMeta)
|
|
3575
|
+
void emitWorkflowTelemetry(logClient, "info", "workflow_resumed", { toolName: "akm_workflow", runId: run_id })
|
|
3576
|
+
return raw
|
|
3139
3577
|
}
|
|
3140
3578
|
}
|
|
3141
3579
|
},
|
|
@@ -3212,15 +3650,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3212
3650
|
return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
|
|
3213
3651
|
},
|
|
3214
3652
|
}),
|
|
3215
|
-
|
|
3216
|
-
description: "
|
|
3217
|
-
args: {
|
|
3218
|
-
|
|
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" })
|
|
3653
|
+
akm_init: tool({
|
|
3654
|
+
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.",
|
|
3655
|
+
args: {},
|
|
3656
|
+
async execute() {
|
|
3657
|
+
return runCli(client as unknown as LogCapableClient, ["init"], { toolName: "akm_init" })
|
|
3224
3658
|
},
|
|
3225
3659
|
}),
|
|
3226
3660
|
akm_help: tool({
|
|
@@ -3242,6 +3676,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3242
3676
|
timeout: 30_000,
|
|
3243
3677
|
}).toString().trim()
|
|
3244
3678
|
} catch (error: unknown) {
|
|
3679
|
+
await writePluginLog(logClient, "error", "AKM help command failed", {
|
|
3680
|
+
subsystem: "help",
|
|
3681
|
+
toolName: "akm_help",
|
|
3682
|
+
command: cliCommand,
|
|
3683
|
+
args: helpArgs,
|
|
3684
|
+
error: formatCliError(error),
|
|
3685
|
+
})
|
|
3245
3686
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3246
3687
|
}
|
|
3247
3688
|
return JSON.stringify({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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": [
|