akm-opencode 0.7.3 → 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/index.ts +354 -16
- package/package.json +1 -1
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
|
|
@@ -320,13 +328,14 @@ function getExecStatus(error: unknown): number | null {
|
|
|
320
328
|
|
|
321
329
|
async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
|
|
322
330
|
try {
|
|
331
|
+
const redacted = redactObject(extra)
|
|
323
332
|
await client.app.log({
|
|
324
|
-
query: typeof
|
|
333
|
+
query: typeof redacted.value.directory === "string" ? { directory: redacted.value.directory as string } : undefined,
|
|
325
334
|
body: {
|
|
326
335
|
service: "akm-opencode",
|
|
327
336
|
level,
|
|
328
337
|
message,
|
|
329
|
-
extra,
|
|
338
|
+
extra: redacted.value,
|
|
330
339
|
},
|
|
331
340
|
})
|
|
332
341
|
} catch {
|
|
@@ -334,6 +343,27 @@ async function writePluginLog(client: LogCapableClient, level: LogLevel, message
|
|
|
334
343
|
}
|
|
335
344
|
}
|
|
336
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
|
+
|
|
337
367
|
async function logHookFailure(
|
|
338
368
|
client: LogCapableClient,
|
|
339
369
|
hook: string,
|
|
@@ -637,6 +667,19 @@ function safeJsonParse<T>(raw: string): T | undefined {
|
|
|
637
667
|
}
|
|
638
668
|
|
|
639
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
|
+
})
|
|
640
683
|
return writePluginLog(client, level, eventType, {
|
|
641
684
|
subsystem: "workflow-compliance",
|
|
642
685
|
eventType,
|
|
@@ -877,7 +920,7 @@ function rememberTextAsMemory(name: string, body: string): string | null {
|
|
|
877
920
|
execFileSync(command, ["--format", "json", "-q", "remember", "--name", name, "--force"], {
|
|
878
921
|
encoding: "utf8",
|
|
879
922
|
timeout: AKM_CURATE_TIMEOUT_MS * 2,
|
|
880
|
-
input: body,
|
|
923
|
+
input: redactSecrets(body).text,
|
|
881
924
|
})
|
|
882
925
|
return `memory:${name}`
|
|
883
926
|
} catch {
|
|
@@ -903,6 +946,13 @@ function captureSessionMemory(
|
|
|
903
946
|
}
|
|
904
947
|
|
|
905
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("")
|
|
906
956
|
lines.push(`# Session summary (${nowIso()})`)
|
|
907
957
|
lines.push(`Reason: ${reason}`)
|
|
908
958
|
lines.push(`Session: ${sessionID}`)
|
|
@@ -937,6 +987,26 @@ function captureSessionMemory(
|
|
|
937
987
|
}
|
|
938
988
|
|
|
939
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
|
+
}
|
|
940
1010
|
for (const entry of entries) {
|
|
941
1011
|
if (!entry.checkpointed) entry.checkpointed = true
|
|
942
1012
|
}
|
|
@@ -947,6 +1017,26 @@ function captureSessionMemory(
|
|
|
947
1017
|
|
|
948
1018
|
sessionFinalMemoryCaptured.add(sessionID)
|
|
949
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
|
+
}
|
|
950
1040
|
return ref
|
|
951
1041
|
}
|
|
952
1042
|
|
|
@@ -2151,6 +2241,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2151
2241
|
const sid = extractSessionIdFromEvent(event) ?? extractSessionIdFromEvent((event as { properties?: unknown }).properties)
|
|
2152
2242
|
if (type === "session.created" || type === "session.updated") {
|
|
2153
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
|
+
})
|
|
2154
2251
|
if (!sessionContextEpoch.has(sid)) sessionContextEpoch.set(sid, 0)
|
|
2155
2252
|
if (type === "session.created") {
|
|
2156
2253
|
await ensureAgentSetup(logClient, {
|
|
@@ -2192,6 +2289,15 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2192
2289
|
})
|
|
2193
2290
|
await maybeIndexSessionMemory(logClient, sid, type, captured)
|
|
2194
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
|
+
}
|
|
2195
2301
|
// Drop per-session state so a re-created session does not inherit
|
|
2196
2302
|
// stale hints/curation.
|
|
2197
2303
|
if (type === "session.deleted") {
|
|
@@ -2286,6 +2392,13 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2286
2392
|
}
|
|
2287
2393
|
return
|
|
2288
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
|
+
}
|
|
2289
2402
|
output.args = args
|
|
2290
2403
|
} catch (error: unknown) {
|
|
2291
2404
|
await logHookFailure(logClient, "tool.execute.before", error, {
|
|
@@ -2370,14 +2483,53 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2370
2483
|
text: truncateLogText(text),
|
|
2371
2484
|
})
|
|
2372
2485
|
|
|
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
2486
|
if (AKM_AUTO_CURATE && input.sessionID) {
|
|
2376
|
-
const
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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
|
+
}
|
|
2381
2533
|
}
|
|
2382
2534
|
}
|
|
2383
2535
|
|
|
@@ -2453,6 +2605,14 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2453
2605
|
// has material to flush into a session summary memory.
|
|
2454
2606
|
const refResult = extractToolRefs(input.tool, input.args as Record<string, unknown>, parsed)
|
|
2455
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
|
+
})
|
|
2456
2616
|
if (refResult.refs.length > 0 && input.sessionID) {
|
|
2457
2617
|
for (const ref of refResult.refs) {
|
|
2458
2618
|
addBufferEntry(input.sessionID, {
|
|
@@ -2498,10 +2658,40 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2498
2658
|
// memories do not accept feedback, and vault values never surface in
|
|
2499
2659
|
// JSON so automatic usage signals would be misleading.
|
|
2500
2660
|
if (ref.startsWith("memory:") || ref.startsWith("vault:")) continue
|
|
2501
|
-
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, {
|
|
2502
2682
|
toolName: input.tool,
|
|
2503
2683
|
sessionID: input.sessionID,
|
|
2504
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
|
+
}
|
|
2505
2695
|
if (!ok) break
|
|
2506
2696
|
}
|
|
2507
2697
|
}
|
|
@@ -2641,6 +2831,108 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2641
2831
|
return raw
|
|
2642
2832
|
},
|
|
2643
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
|
+
}),
|
|
2644
2936
|
akm_curate: tool({
|
|
2645
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.",
|
|
2646
2938
|
args: {
|
|
@@ -2822,6 +3114,24 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2822
3114
|
const model = parseModelHint(shown.modelHint)
|
|
2823
3115
|
const tools = parseToolPolicy(shown.toolPolicy)
|
|
2824
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
|
+
|
|
2825
3135
|
const targetSession = await ensureTargetSessionID({
|
|
2826
3136
|
useSubtask,
|
|
2827
3137
|
context: { sessionID: context.sessionID, directory: context.directory },
|
|
@@ -2852,6 +3162,19 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2852
3162
|
})
|
|
2853
3163
|
if (!promptResponse.ok) return JSON.stringify(promptResponse)
|
|
2854
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
|
+
|
|
2855
3178
|
return JSON.stringify({
|
|
2856
3179
|
ok: true,
|
|
2857
3180
|
ref: resolved.ref,
|
|
@@ -2861,7 +3184,7 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
2861
3184
|
sessionID: targetSession.sessionID,
|
|
2862
3185
|
model,
|
|
2863
3186
|
tools,
|
|
2864
|
-
text:
|
|
3187
|
+
text: childText,
|
|
2865
3188
|
})
|
|
2866
3189
|
},
|
|
2867
3190
|
}),
|
|
@@ -3176,14 +3499,18 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3176
3499
|
if (!ref) return JSON.stringify({ ok: false, error: "'ref' is required for action='start'." })
|
|
3177
3500
|
const args = ["workflow", "start", ref]
|
|
3178
3501
|
if (params) args.push("--params", params)
|
|
3179
|
-
|
|
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
|
|
3180
3505
|
}
|
|
3181
3506
|
case "next": {
|
|
3182
3507
|
const picked = target ?? run_id ?? ref
|
|
3183
3508
|
if (!picked) return JSON.stringify({ ok: false, error: "'target', 'run_id', or 'ref' is required for action='next'." })
|
|
3184
3509
|
const args = ["workflow", "next", picked]
|
|
3185
3510
|
if (params) args.push("--params", params)
|
|
3186
|
-
|
|
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
|
|
3187
3514
|
}
|
|
3188
3515
|
case "complete": {
|
|
3189
3516
|
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='complete'." })
|
|
@@ -3192,7 +3519,16 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3192
3519
|
if (state) args.push("--state", state)
|
|
3193
3520
|
if (notes) args.push("--notes", notes)
|
|
3194
3521
|
if (evidence) args.push("--evidence", evidence)
|
|
3195
|
-
|
|
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
|
|
3196
3532
|
}
|
|
3197
3533
|
case "status": {
|
|
3198
3534
|
const picked = target ?? run_id ?? ref
|
|
@@ -3235,7 +3571,9 @@ export const AkmPlugin: Plugin = async ({ client, worktree, directory }) => {
|
|
|
3235
3571
|
}
|
|
3236
3572
|
case "resume": {
|
|
3237
3573
|
if (!run_id) return JSON.stringify({ ok: false, error: "'run_id' is required for action='resume'." })
|
|
3238
|
-
|
|
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
|
|
3239
3577
|
}
|
|
3240
3578
|
}
|
|
3241
3579
|
},
|
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": [
|