akm-opencode 0.7.1 → 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.
- package/README.md +4 -4
- package/index.ts +473 -188
- 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
|
@@ -53,6 +53,7 @@ const sessionSuccessfulAssetTouchCount = new Map<string, number>()
|
|
|
53
53
|
const pendingProposalSummaryCache = new Map<string, { count: number; expiresAt: number; unsupported?: boolean }>()
|
|
54
54
|
const retrospectiveState = new Map<string, { recentRefs: string[]; lastNegativeSignalAt?: number }>()
|
|
55
55
|
let cachedAkmStashDir: string | undefined
|
|
56
|
+
let agentSetupPromise: Promise<boolean> | null = null
|
|
56
57
|
|
|
57
58
|
// Asset-ref grammar matching the stash skill: [origin//]type:name.
|
|
58
59
|
// We validate normalized tokens individually instead of running a global regex
|
|
@@ -70,7 +71,7 @@ const AKM_WORKFLOW_INSTRUCTION = [
|
|
|
70
71
|
"3. Record `akm_feedback` after the result is known.",
|
|
71
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.",
|
|
72
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`.",
|
|
73
|
-
"6.
|
|
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.",
|
|
74
75
|
`7. ${PROPOSED_QUALITY_WARNING}`,
|
|
75
76
|
"8. Never accept or reject proposals, push saves, remove sources, or access vault values without explicit user approval.",
|
|
76
77
|
].join("\n")
|
|
@@ -218,6 +219,93 @@ function formatCliError(error: unknown): string {
|
|
|
218
219
|
return error instanceof Error ? error.message : String(error)
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
function needsAgentSetup(message: string): boolean {
|
|
223
|
+
return /(agent commands are disabled|agent not configured|no [`'"]?agent[`'"]? block in config\.json)/i.test(message)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function addAgentSetupGuidance(message: string): string {
|
|
227
|
+
if (!needsAgentSetup(message)) return message
|
|
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
|
+
}
|
|
231
|
+
|
|
232
|
+
async function ensurePlatformAgentDefault(
|
|
233
|
+
client: LogCapableClient,
|
|
234
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
235
|
+
): Promise<boolean> {
|
|
236
|
+
const command = resolveAkmCommand()
|
|
237
|
+
if (typeof command !== "string") {
|
|
238
|
+
await writePluginLog(client, "warn", "AKM agent default check skipped", {
|
|
239
|
+
subsystem: "akm",
|
|
240
|
+
trigger: meta.trigger,
|
|
241
|
+
sessionID: meta.sessionID,
|
|
242
|
+
directory: meta.directory,
|
|
243
|
+
error: command.error,
|
|
244
|
+
})
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const currentRaw = execFileSync(command, ["config", "get", "agent.default", "--format", "json"], {
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
timeout: 30_000,
|
|
252
|
+
})
|
|
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", {
|
|
270
|
+
subsystem: "akm",
|
|
271
|
+
trigger: meta.trigger,
|
|
272
|
+
sessionID: meta.sessionID,
|
|
273
|
+
directory: meta.directory,
|
|
274
|
+
agentDefault: meta.platform,
|
|
275
|
+
})
|
|
276
|
+
return true
|
|
277
|
+
} catch (error: unknown) {
|
|
278
|
+
await writePluginLog(client, "warn", "AKM agent default initialization failed", {
|
|
279
|
+
subsystem: "akm",
|
|
280
|
+
trigger: meta.trigger,
|
|
281
|
+
sessionID: meta.sessionID,
|
|
282
|
+
directory: meta.directory,
|
|
283
|
+
platform: meta.platform,
|
|
284
|
+
error: addAgentSetupGuidance(formatCliError(error)),
|
|
285
|
+
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
286
|
+
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? "",
|
|
287
|
+
})
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function ensureAgentSetup(
|
|
293
|
+
client: LogCapableClient,
|
|
294
|
+
meta: { directory?: string; sessionID?: string; trigger: string; platform: string },
|
|
295
|
+
): Promise<boolean> {
|
|
296
|
+
if (agentSetupPromise) return agentSetupPromise
|
|
297
|
+
const promise = ensurePlatformAgentDefault(client, meta)
|
|
298
|
+
const trackedPromise = promise.finally(() => {
|
|
299
|
+
if (agentSetupPromise === trackedPromise) agentSetupPromise = null
|
|
300
|
+
})
|
|
301
|
+
agentSetupPromise = trackedPromise
|
|
302
|
+
return trackedPromise
|
|
303
|
+
}
|
|
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
|
+
|
|
221
309
|
function toLogString(value: unknown): string | undefined {
|
|
222
310
|
if (typeof value === "string") return value
|
|
223
311
|
if (value instanceof Buffer) return value.toString("utf8")
|
|
@@ -246,6 +334,20 @@ async function writePluginLog(client: LogCapableClient, level: LogLevel, message
|
|
|
246
334
|
}
|
|
247
335
|
}
|
|
248
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
|
+
|
|
249
351
|
function nowIso(): string {
|
|
250
352
|
return new Date().toISOString()
|
|
251
353
|
}
|
|
@@ -347,9 +449,20 @@ function runCurate(args: string[]): string | null {
|
|
|
347
449
|
return body || null
|
|
348
450
|
}
|
|
349
451
|
|
|
350
|
-
function
|
|
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> {
|
|
351
464
|
if (!text || text.length < AKM_CURATE_MIN_CHARS) return null
|
|
352
|
-
return
|
|
465
|
+
return runCurateLogged(client,
|
|
353
466
|
appendRunScopeArgs(
|
|
354
467
|
[
|
|
355
468
|
"--detail",
|
|
@@ -364,11 +477,12 @@ function runCurateForPrompt(text: string, sessionID?: string): string | null {
|
|
|
364
477
|
],
|
|
365
478
|
sessionID,
|
|
366
479
|
),
|
|
480
|
+
{ toolName: "chat.message", sessionID, operation: "prompt-curate" },
|
|
367
481
|
)
|
|
368
482
|
}
|
|
369
483
|
|
|
370
|
-
function runCurateForSession(sessionID: string): string | null {
|
|
371
|
-
return
|
|
484
|
+
async function runCurateForSession(client: LogCapableClient, sessionID: string): Promise<string | null> {
|
|
485
|
+
return runCurateLogged(client,
|
|
372
486
|
appendRunScopeArgs(
|
|
373
487
|
[
|
|
374
488
|
"--detail",
|
|
@@ -382,14 +496,17 @@ function runCurateForSession(sessionID: string): string | null {
|
|
|
382
496
|
],
|
|
383
497
|
sessionID,
|
|
384
498
|
),
|
|
499
|
+
{ toolName: "session.start", sessionID, operation: "session-curate" },
|
|
385
500
|
)
|
|
386
501
|
}
|
|
387
502
|
|
|
388
|
-
function runHintsForSession(): string | null {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
})
|
|
393
510
|
}
|
|
394
511
|
|
|
395
512
|
function summarizeWorkflowList(value: unknown): string | null {
|
|
@@ -423,10 +540,15 @@ function summarizeWorkflowList(value: unknown): string | null {
|
|
|
423
540
|
return null
|
|
424
541
|
}
|
|
425
542
|
|
|
426
|
-
function runWorkflowSummaryForSession(): string | null {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
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)
|
|
430
552
|
const summary = summarizeWorkflowList(
|
|
431
553
|
Array.isArray(parsed)
|
|
432
554
|
? parsed
|
|
@@ -463,14 +585,20 @@ function summarizeCuratorReportForContext(report: string): string {
|
|
|
463
585
|
return `${report.slice(0, AKM_CURATOR_CONTEXT_MAX_CHARS).trimEnd()}\n\n[truncated for context]`
|
|
464
586
|
}
|
|
465
587
|
|
|
466
|
-
function getAkmStashDir(): string | undefined {
|
|
588
|
+
async function getAkmStashDir(client?: LogCapableClient): Promise<string | undefined> {
|
|
467
589
|
if (cachedAkmStashDir !== undefined) return cachedAkmStashDir || undefined
|
|
468
|
-
const
|
|
469
|
-
|
|
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) {
|
|
470
598
|
cachedAkmStashDir = ""
|
|
471
599
|
return undefined
|
|
472
600
|
}
|
|
473
|
-
const parsed = parseMaybeJson(
|
|
601
|
+
const parsed = parseMaybeJson(raw)
|
|
474
602
|
if (typeof parsed === "string" && parsed.trim()) {
|
|
475
603
|
cachedAkmStashDir = parsed.trim()
|
|
476
604
|
return cachedAkmStashDir
|
|
@@ -484,7 +612,6 @@ function getAkmStashDir(): string | undefined {
|
|
|
484
612
|
}
|
|
485
613
|
}
|
|
486
614
|
}
|
|
487
|
-
const raw = result.stdout.trim()
|
|
488
615
|
cachedAkmStashDir = raw || ""
|
|
489
616
|
return cachedAkmStashDir || undefined
|
|
490
617
|
}
|
|
@@ -518,6 +645,29 @@ function emitWorkflowTelemetry(client: LogCapableClient, level: LogLevel, eventT
|
|
|
518
645
|
})
|
|
519
646
|
}
|
|
520
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
|
+
|
|
521
671
|
function noteRecentRefs(sessionID: string | undefined, refs: string[]) {
|
|
522
672
|
if (!sessionID || refs.length === 0) return
|
|
523
673
|
const state = retrospectiveState.get(sessionID) ?? { recentRefs: [] }
|
|
@@ -575,7 +725,7 @@ async function recordRetrospectiveFeedback(client: LogCapableClient, sessionID:
|
|
|
575
725
|
sessionID,
|
|
576
726
|
})
|
|
577
727
|
const parsed = safeJsonParse<{ ok?: boolean }>(raw)
|
|
578
|
-
if (parsed?.ok
|
|
728
|
+
if (parsed?.ok === true) {
|
|
579
729
|
await emitWorkflowTelemetry(client, "info", "akm.feedback.recorded", {
|
|
580
730
|
sessionID,
|
|
581
731
|
toolName: "akm_feedback",
|
|
@@ -678,6 +828,21 @@ function queueFeedback(
|
|
|
678
828
|
stdio: "ignore",
|
|
679
829
|
},
|
|
680
830
|
)
|
|
831
|
+
child.on("exit", (code, signal) => {
|
|
832
|
+
if ((typeof code === "number" && code !== 0) || signal) {
|
|
833
|
+
void writePluginLog(client, "warn", "AKM auto-feedback failed", {
|
|
834
|
+
subsystem: "feedback",
|
|
835
|
+
toolName: meta.toolName,
|
|
836
|
+
sessionID: meta.sessionID,
|
|
837
|
+
directory: meta.directory,
|
|
838
|
+
ref,
|
|
839
|
+
sentiment,
|
|
840
|
+
error: signal
|
|
841
|
+
? `akm feedback exited via signal ${signal}`
|
|
842
|
+
: `akm feedback exited with code ${code}`,
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
})
|
|
681
846
|
child.on("error", (error) => {
|
|
682
847
|
void writePluginLog(client, "warn", "AKM auto-feedback failed", {
|
|
683
848
|
subsystem: "feedback",
|
|
@@ -1314,11 +1479,7 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1314
1479
|
const fullArgs = args.includes("--format") ? [...args] : [...args, "--format", "json"]
|
|
1315
1480
|
const proposalId = args[0] === "proposal" && typeof args[2] === "string" ? args[2] : null
|
|
1316
1481
|
|
|
1317
|
-
|
|
1318
|
-
const stdout = execFileSync(command, fullArgs, {
|
|
1319
|
-
encoding: "utf8",
|
|
1320
|
-
timeout: 60_000,
|
|
1321
|
-
})
|
|
1482
|
+
const recordSuccess = async (stdout: string): Promise<string> => {
|
|
1322
1483
|
await writePluginLog(client, "info", "AKM command completed", {
|
|
1323
1484
|
subsystem: "akm",
|
|
1324
1485
|
toolName: meta.toolName,
|
|
@@ -1384,8 +1545,20 @@ async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta
|
|
|
1384
1545
|
})
|
|
1385
1546
|
}
|
|
1386
1547
|
return stdout
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
const stdout = execFileSync(command, fullArgs, {
|
|
1552
|
+
encoding: "utf8",
|
|
1553
|
+
timeout: 60_000,
|
|
1554
|
+
})
|
|
1555
|
+
return recordSuccess(stdout)
|
|
1387
1556
|
} catch (error: unknown) {
|
|
1388
|
-
|
|
1557
|
+
let message = formatCliError(error)
|
|
1558
|
+
if (["akm_reflect", "akm_propose"].includes(meta.toolName) && needsAgentSetup(message)) {
|
|
1559
|
+
message = addAgentSetupGuidance(message)
|
|
1560
|
+
}
|
|
1561
|
+
message = addAgentSetupGuidance(message)
|
|
1389
1562
|
await writePluginLog(client, "error", "AKM command failed", {
|
|
1390
1563
|
subsystem: "akm",
|
|
1391
1564
|
toolName: meta.toolName,
|
|
@@ -1980,9 +2153,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1980
2153
|
if (!sid) return
|
|
1981
2154
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
1982
2155
|
if (type === "session.created") {
|
|
2156
|
+
await ensureAgentSetup(logClient, {
|
|
2157
|
+
directory,
|
|
2158
|
+
sessionID: sid,
|
|
2159
|
+
trigger: "session.created",
|
|
2160
|
+
platform: "opencode",
|
|
2161
|
+
})
|
|
1983
2162
|
warmIndexInBackground()
|
|
1984
2163
|
if (AKM_AUTO_CURATE && !sessionCurated.has(sid)) {
|
|
1985
|
-
const curated = runCurateForSession(sid)
|
|
2164
|
+
const curated = await runCurateForSession(logClient, sid)
|
|
1986
2165
|
if (curated) {
|
|
1987
2166
|
bumpCuratedVersion(sid)
|
|
1988
2167
|
sessionCurated.set(sid, curated)
|
|
@@ -1990,11 +2169,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
1990
2169
|
}
|
|
1991
2170
|
}
|
|
1992
2171
|
if (AKM_AUTO_HINTS && !sessionHints.has(sid)) {
|
|
1993
|
-
const hints = runHintsForSession()
|
|
2172
|
+
const hints = await runHintsForSession(logClient, sid)
|
|
1994
2173
|
if (hints) sessionHints.set(sid, hints)
|
|
1995
2174
|
}
|
|
1996
2175
|
if (!sessionWorkflow.has(sid)) {
|
|
1997
|
-
sessionWorkflow.set(sid, runWorkflowSummaryForSession() ?? "")
|
|
2176
|
+
sessionWorkflow.set(sid, await runWorkflowSummaryForSession(logClient, sid) ?? "")
|
|
1998
2177
|
}
|
|
1999
2178
|
const proposalSummary = await getPendingProposalCount(logClient, sid)
|
|
2000
2179
|
if (!proposalSummary.unsupported && proposalSummary.count > 0) {
|
|
@@ -2029,8 +2208,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2029
2208
|
sessionBuffer.delete(sid)
|
|
2030
2209
|
}
|
|
2031
2210
|
}
|
|
2032
|
-
} catch {
|
|
2033
|
-
|
|
2211
|
+
} catch (error: unknown) {
|
|
2212
|
+
await logHookFailure(logClient, "event", error)
|
|
2034
2213
|
}
|
|
2035
2214
|
},
|
|
2036
2215
|
// Stop is the closest analogue to Claude's Stop/SubagentStop — the user or
|
|
@@ -2051,8 +2230,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2051
2230
|
})
|
|
2052
2231
|
await maybeIndexSessionMemory(logClient, sid, "stop", captured)
|
|
2053
2232
|
}
|
|
2054
|
-
} catch {
|
|
2055
|
-
|
|
2233
|
+
} catch (error: unknown) {
|
|
2234
|
+
await logHookFailure(logClient, "stop", error)
|
|
2056
2235
|
}
|
|
2057
2236
|
},
|
|
2058
2237
|
// experimental.chat.system.transform is how OpenCode exposes the
|
|
@@ -2091,8 +2270,8 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2091
2270
|
sessionCuratedInjectedVersion.set(sid, curatedVersion)
|
|
2092
2271
|
}
|
|
2093
2272
|
}
|
|
2094
|
-
} catch {
|
|
2095
|
-
|
|
2273
|
+
} catch (error: unknown) {
|
|
2274
|
+
await logHookFailure(logClient, "experimental.chat.system.transform", error)
|
|
2096
2275
|
}
|
|
2097
2276
|
},
|
|
2098
2277
|
"tool.execute.before": async (input, output) => {
|
|
@@ -2108,8 +2287,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2108
2287
|
return
|
|
2109
2288
|
}
|
|
2110
2289
|
output.args = args
|
|
2111
|
-
} catch {
|
|
2112
|
-
|
|
2290
|
+
} catch (error: unknown) {
|
|
2291
|
+
await logHookFailure(logClient, "tool.execute.before", error, {
|
|
2292
|
+
toolName: input?.tool,
|
|
2293
|
+
sessionID: input?.sessionID,
|
|
2294
|
+
})
|
|
2113
2295
|
}
|
|
2114
2296
|
},
|
|
2115
2297
|
"permission.ask": async (input, output) => {
|
|
@@ -2137,8 +2319,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2137
2319
|
command,
|
|
2138
2320
|
category: assessment.category,
|
|
2139
2321
|
})
|
|
2140
|
-
} catch {
|
|
2141
|
-
|
|
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
|
+
})
|
|
2142
2327
|
}
|
|
2143
2328
|
},
|
|
2144
2329
|
"command.execute.before": async (input, output) => {
|
|
@@ -2155,154 +2340,180 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2155
2340
|
command,
|
|
2156
2341
|
category: assessment.category,
|
|
2157
2342
|
})
|
|
2158
|
-
} catch {
|
|
2159
|
-
|
|
2343
|
+
} catch (error: unknown) {
|
|
2344
|
+
await logHookFailure(logClient, "command.execute.before", error, {
|
|
2345
|
+
sessionID: input?.sessionID,
|
|
2346
|
+
command: input?.command,
|
|
2347
|
+
})
|
|
2160
2348
|
}
|
|
2161
2349
|
},
|
|
2162
2350
|
"shell.env": async (_input, output) => {
|
|
2163
2351
|
try {
|
|
2164
2352
|
output.env.AKM_PROJECT = worktree
|
|
2165
2353
|
output.env.AKM_PLUGIN_VERSION = PLUGIN_VERSION
|
|
2166
|
-
const stashDir = getAkmStashDir()
|
|
2354
|
+
const stashDir = await getAkmStashDir(logClient)
|
|
2167
2355
|
if (stashDir) output.env.AKM_STASH_DIR = stashDir
|
|
2168
|
-
} catch {
|
|
2169
|
-
|
|
2356
|
+
} catch (error: unknown) {
|
|
2357
|
+
await logHookFailure(logClient, "shell.env", error)
|
|
2170
2358
|
}
|
|
2171
2359
|
},
|
|
2172
2360
|
"chat.message": async (input, output) => {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2361
|
+
try {
|
|
2362
|
+
const text = extractText(output.parts).trim()
|
|
2363
|
+
if (!text) return
|
|
2364
|
+
await writePluginLog(logClient, "info", "AKM user feedback recorded", {
|
|
2365
|
+
subsystem: "feedback",
|
|
2366
|
+
actor: "user",
|
|
2367
|
+
sessionID: input.sessionID,
|
|
2368
|
+
messageID: input.messageID,
|
|
2369
|
+
agent: input.agent,
|
|
2370
|
+
text: truncateLogText(text),
|
|
2371
|
+
})
|
|
2183
2372
|
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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.
|
|
2375
|
+
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
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)
|
|
2380
|
+
bumpCuratedVersion(input.sessionID)
|
|
2381
|
+
}
|
|
2191
2382
|
}
|
|
2192
|
-
}
|
|
2193
2383
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2384
|
+
// Track explicit memory intents so capture-memory has something durable
|
|
2385
|
+
// to flush when the session ends.
|
|
2386
|
+
if (/\b(remember|memory|memories)\b/i.test(text)) {
|
|
2387
|
+
addBufferEntry(input.sessionID, {
|
|
2388
|
+
kind: "memory-intent",
|
|
2389
|
+
note: truncateLogText(text, 500),
|
|
2390
|
+
})
|
|
2391
|
+
}
|
|
2202
2392
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2393
|
+
if (input.sessionID && AKM_AUTO_FEEDBACK && AKM_RETROSPECTIVE_FEEDBACK_RE.test(text)) {
|
|
2394
|
+
const recentRefs = (sessionBuffer.get(input.sessionID) ?? [])
|
|
2395
|
+
.filter((entry) => entry.kind === "tool-ref" && !!entry.ref)
|
|
2396
|
+
.map((entry) => entry.ref!)
|
|
2397
|
+
.filter((ref, index, refs) => !ref.startsWith("memory:") && !ref.startsWith("vault:") && refs.indexOf(ref) === index)
|
|
2398
|
+
.slice(-3)
|
|
2399
|
+
const dedupe = new Set<string>()
|
|
2400
|
+
for (const ref of recentRefs) {
|
|
2401
|
+
queueFeedback(logClient, ref, "positive", "opencode retrospective: user confirmed it worked", {
|
|
2402
|
+
toolName: "chat.message",
|
|
2403
|
+
sessionID: input.sessionID,
|
|
2404
|
+
}, dedupe)
|
|
2405
|
+
}
|
|
2215
2406
|
}
|
|
2407
|
+
await recordRetrospectiveFeedback(logClient, input.sessionID, text)
|
|
2408
|
+
} catch (error: unknown) {
|
|
2409
|
+
await writePluginLog(logClient, "error", "AKM chat.message hook failed", {
|
|
2410
|
+
subsystem: "hook",
|
|
2411
|
+
hook: "chat.message",
|
|
2412
|
+
sessionID: input?.sessionID,
|
|
2413
|
+
messageID: input?.messageID,
|
|
2414
|
+
agent: input?.agent,
|
|
2415
|
+
error: formatCliError(error),
|
|
2416
|
+
})
|
|
2216
2417
|
}
|
|
2217
|
-
await recordRetrospectiveFeedback(logClient, input.sessionID, text)
|
|
2218
2418
|
},
|
|
2219
2419
|
"tool.execute.after": async (input, output) => {
|
|
2220
|
-
|
|
2420
|
+
try {
|
|
2421
|
+
if (!input.tool.startsWith("akm_")) return
|
|
2221
2422
|
|
|
2222
|
-
|
|
2223
|
-
|
|
2423
|
+
const parsed = parseToolOutput(output.output)
|
|
2424
|
+
if (!parsed) return
|
|
2224
2425
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2426
|
+
const feedback = classifyToolFeedback(parsed)
|
|
2427
|
+
if (feedback) {
|
|
2428
|
+
await writePluginLog(logClient, feedback === "negative" ? "warn" : "info", "AKM system feedback recorded", {
|
|
2429
|
+
subsystem: "feedback",
|
|
2430
|
+
actor: "system",
|
|
2431
|
+
feedback,
|
|
2432
|
+
toolName: input.tool,
|
|
2433
|
+
sessionID: input.sessionID,
|
|
2434
|
+
callID: input.callID,
|
|
2435
|
+
title: output.title,
|
|
2436
|
+
error: typeof (parsed as { error?: unknown }).error === "string" ? (parsed as { error?: string }).error : undefined,
|
|
2437
|
+
})
|
|
2438
|
+
}
|
|
2238
2439
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2440
|
+
const memoryRefs = extractMemoryRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2441
|
+
if (memoryRefs.length > 0) {
|
|
2442
|
+
await writePluginLog(logClient, "info", "AKM memory usage recorded", {
|
|
2443
|
+
subsystem: "memory",
|
|
2444
|
+
toolName: input.tool,
|
|
2445
|
+
sessionID: input.sessionID,
|
|
2446
|
+
callID: input.callID,
|
|
2447
|
+
refs: memoryRefs,
|
|
2448
|
+
})
|
|
2449
|
+
}
|
|
2249
2450
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2451
|
+
// Auto-feedback + session buffering: record every asset ref the tool
|
|
2452
|
+
// touched so the stash ranking improves over time and so Stop/Compact
|
|
2453
|
+
// has material to flush into a session summary memory.
|
|
2253
2454
|
const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2254
2455
|
noteRecentRefs(input.sessionID, refResult.refs)
|
|
2255
2456
|
if (refResult.refs.length > 0 && input.sessionID) {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
})
|
|
2263
|
-
}
|
|
2264
|
-
if (feedback === "positive") {
|
|
2265
|
-
sessionSuccessfulAssetTouchCount.set(
|
|
2266
|
-
input.sessionID,
|
|
2267
|
-
(sessionSuccessfulAssetTouchCount.get(input.sessionID) ?? 0) + 1,
|
|
2268
|
-
)
|
|
2269
|
-
const checkpointRef = maybeCheckpointSessionMemory(input.sessionID)
|
|
2270
|
-
if (checkpointRef) {
|
|
2271
|
-
await writePluginLog(logClient, "info", "AKM checkpoint memory captured", {
|
|
2272
|
-
subsystem: "memory",
|
|
2273
|
-
actor: "system",
|
|
2274
|
-
sessionID: input.sessionID,
|
|
2275
|
-
reason: "checkpoint",
|
|
2276
|
-
ref: checkpointRef,
|
|
2457
|
+
for (const ref of refResult.refs) {
|
|
2458
|
+
addBufferEntry(input.sessionID, {
|
|
2459
|
+
kind: "tool-ref",
|
|
2460
|
+
toolName: input.tool,
|
|
2461
|
+
ref,
|
|
2462
|
+
status: feedback ?? "unknown",
|
|
2277
2463
|
})
|
|
2278
2464
|
}
|
|
2465
|
+
if (feedback === "positive") {
|
|
2466
|
+
sessionSuccessfulAssetTouchCount.set(
|
|
2467
|
+
input.sessionID,
|
|
2468
|
+
(sessionSuccessfulAssetTouchCount.get(input.sessionID) ?? 0) + 1,
|
|
2469
|
+
)
|
|
2470
|
+
const checkpointRef = maybeCheckpointSessionMemory(input.sessionID)
|
|
2471
|
+
if (checkpointRef) {
|
|
2472
|
+
await writePluginLog(logClient, "info", "AKM checkpoint memory captured", {
|
|
2473
|
+
subsystem: "memory",
|
|
2474
|
+
actor: "system",
|
|
2475
|
+
sessionID: input.sessionID,
|
|
2476
|
+
reason: "checkpoint",
|
|
2477
|
+
ref: checkpointRef,
|
|
2478
|
+
})
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2279
2481
|
}
|
|
2280
|
-
}
|
|
2281
2482
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2483
|
+
if (
|
|
2484
|
+
AKM_AUTO_FEEDBACK
|
|
2485
|
+
&& feedback
|
|
2486
|
+
&& input.tool !== "akm_feedback"
|
|
2487
|
+
&& refResult.refs.length > 0
|
|
2488
|
+
) {
|
|
2489
|
+
const dedupe = new Set<string>()
|
|
2490
|
+
const feedbackRefs = feedback === "positive"
|
|
2491
|
+
? refResult.refs
|
|
2492
|
+
: refResult.refs.filter((ref) => !refResult.positiveOnlyRefs.includes(ref))
|
|
2493
|
+
const note = feedback === "positive"
|
|
2494
|
+
? `opencode auto: ${input.tool} succeeded`
|
|
2495
|
+
: `opencode auto: ${input.tool} failed`
|
|
2496
|
+
for (const ref of feedbackRefs) {
|
|
2497
|
+
// Memories and vault refs are not first-class feedback targets —
|
|
2498
|
+
// memories do not accept feedback, and vault values never surface in
|
|
2499
|
+
// JSON so automatic usage signals would be misleading.
|
|
2500
|
+
if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
|
|
2501
|
+
const ok = queueFeedback(logClient, ref, feedback, note, {
|
|
2502
|
+
toolName: input.tool,
|
|
2503
|
+
sessionID: input.sessionID,
|
|
2504
|
+
}, dedupe)
|
|
2505
|
+
if (!ok) break
|
|
2506
|
+
}
|
|
2305
2507
|
}
|
|
2508
|
+
} catch (error: unknown) {
|
|
2509
|
+
await writePluginLog(logClient, "error", "AKM tool.execute.after hook failed", {
|
|
2510
|
+
subsystem: "hook",
|
|
2511
|
+
hook: "tool.execute.after",
|
|
2512
|
+
toolName: input?.tool,
|
|
2513
|
+
sessionID: input?.sessionID,
|
|
2514
|
+
callID: input?.callID,
|
|
2515
|
+
error: formatCliError(error),
|
|
2516
|
+
})
|
|
2306
2517
|
}
|
|
2307
2518
|
},
|
|
2308
2519
|
tool: {
|
|
@@ -2393,6 +2604,32 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2393
2604
|
if (note) args.push("--note", note)
|
|
2394
2605
|
args.push(...buildScopedArgs(context as unknown as Record<string, unknown>))
|
|
2395
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
|
+
}
|
|
2396
2633
|
await emitWorkflowTelemetry(logClient, "info", "akm.feedback.recorded", {
|
|
2397
2634
|
sessionID: context.sessionID,
|
|
2398
2635
|
toolName: "akm_feedback",
|
|
@@ -2482,16 +2719,27 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2482
2719
|
description: "Read compact text summaries of the parent session's messages so a dispatched AKM subagent can inherit upstream context.",
|
|
2483
2720
|
args: {},
|
|
2484
2721
|
async execute(_input, context) {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2722
|
+
try {
|
|
2723
|
+
const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
|
|
2724
|
+
if (!parent.ok) return JSON.stringify(parent)
|
|
2725
|
+
const messages = await sdkClient.session.messages({
|
|
2726
|
+
path: { id: parent.parentID },
|
|
2727
|
+
query: { directory: context.directory },
|
|
2728
|
+
})
|
|
2729
|
+
if (messages.error || !messages.data) {
|
|
2730
|
+
return JSON.stringify({ ok: false, error: "Failed to read parent session messages." })
|
|
2731
|
+
}
|
|
2732
|
+
return JSON.stringify(summarizeSessionMessages(parent.parentID, messages.data))
|
|
2733
|
+
} catch (error: unknown) {
|
|
2734
|
+
await writePluginLog(logClient, "error", "AKM parent session read failed", {
|
|
2735
|
+
subsystem: "session",
|
|
2736
|
+
toolName: "akm_parent_messages",
|
|
2737
|
+
sessionID: context.sessionID,
|
|
2738
|
+
directory: context.directory,
|
|
2739
|
+
error: formatCliError(error),
|
|
2740
|
+
})
|
|
2492
2741
|
return JSON.stringify({ ok: false, error: "Failed to read parent session messages." })
|
|
2493
2742
|
}
|
|
2494
|
-
return JSON.stringify(summarizeSessionMessages(parent.parentID, messages.data))
|
|
2495
2743
|
},
|
|
2496
2744
|
}),
|
|
2497
2745
|
akm_session_messages: tool({
|
|
@@ -2500,23 +2748,35 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2500
2748
|
session_id: tool.schema.string().describe("OpenCode session ID to inspect."),
|
|
2501
2749
|
},
|
|
2502
2750
|
async execute({ session_id }, context) {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2751
|
+
try {
|
|
2752
|
+
const parent = await getParentSessionID(sdkClient, context.sessionID, context.directory)
|
|
2753
|
+
const allowedSessionIDs = new Set<string>([context.sessionID])
|
|
2754
|
+
if (parent.ok) allowedSessionIDs.add(parent.parentID)
|
|
2755
|
+
if (context.agent !== "akm-curator" && !allowedSessionIDs.has(session_id)) {
|
|
2756
|
+
return JSON.stringify({
|
|
2757
|
+
ok: false,
|
|
2758
|
+
error: "akm_session_messages only allows arbitrary session IDs for the akm-curator agent. Use akm_parent_messages for parent context.",
|
|
2759
|
+
})
|
|
2760
|
+
}
|
|
2761
|
+
const messages = await sdkClient.session.messages({
|
|
2762
|
+
path: { id: session_id },
|
|
2763
|
+
query: { directory: context.directory },
|
|
2764
|
+
})
|
|
2765
|
+
if (messages.error || !messages.data) {
|
|
2766
|
+
return JSON.stringify({ ok: false, error: `Failed to read messages for session '${session_id}'.` })
|
|
2767
|
+
}
|
|
2768
|
+
return JSON.stringify(summarizeSessionMessages(session_id, messages.data))
|
|
2769
|
+
} catch (error: unknown) {
|
|
2770
|
+
await writePluginLog(logClient, "error", "AKM session messages read failed", {
|
|
2771
|
+
subsystem: "session",
|
|
2772
|
+
toolName: "akm_session_messages",
|
|
2773
|
+
sessionID: context.sessionID,
|
|
2774
|
+
directory: context.directory,
|
|
2775
|
+
targetSessionID: session_id,
|
|
2776
|
+
error: formatCliError(error),
|
|
2510
2777
|
})
|
|
2511
|
-
}
|
|
2512
|
-
const messages = await sdkClient.session.messages({
|
|
2513
|
-
path: { id: session_id },
|
|
2514
|
-
query: { directory: context.directory },
|
|
2515
|
-
})
|
|
2516
|
-
if (messages.error || !messages.data) {
|
|
2517
2778
|
return JSON.stringify({ ok: false, error: `Failed to read messages for session '${session_id}'.` })
|
|
2518
2779
|
}
|
|
2519
|
-
return JSON.stringify(summarizeSessionMessages(session_id, messages.data))
|
|
2520
2780
|
},
|
|
2521
2781
|
}),
|
|
2522
2782
|
akm_agent: tool({
|
|
@@ -2734,6 +2994,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2734
2994
|
})
|
|
2735
2995
|
return JSON.stringify({ ok: true, ref, shell: stdout.trim() })
|
|
2736
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
|
+
})
|
|
2737
3004
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
2738
3005
|
}
|
|
2739
3006
|
}
|
|
@@ -2841,6 +3108,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2841
3108
|
})
|
|
2842
3109
|
return stdout
|
|
2843
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
|
+
})
|
|
2844
3120
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
2845
3121
|
}
|
|
2846
3122
|
}
|
|
@@ -2948,6 +3224,12 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2948
3224
|
})
|
|
2949
3225
|
return JSON.stringify({ ok: true, template: stdout })
|
|
2950
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
|
+
})
|
|
2951
3233
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
2952
3234
|
}
|
|
2953
3235
|
}
|
|
@@ -3030,15 +3312,11 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3030
3312
|
return runCli(client as unknown as LogCapableClient, ["distill", ref], { toolName: "akm_distill" })
|
|
3031
3313
|
},
|
|
3032
3314
|
}),
|
|
3033
|
-
|
|
3034
|
-
description: "
|
|
3035
|
-
args: {
|
|
3036
|
-
|
|
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" })
|
|
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" })
|
|
3042
3320
|
},
|
|
3043
3321
|
}),
|
|
3044
3322
|
akm_help: tool({
|
|
@@ -3060,6 +3338,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3060
3338
|
timeout: 30_000,
|
|
3061
3339
|
}).toString().trim()
|
|
3062
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
|
+
})
|
|
3063
3348
|
return JSON.stringify({ ok: false, error: formatCliError(error) })
|
|
3064
3349
|
}
|
|
3065
3350
|
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.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": [
|