Each comment from the review session is listed below, analyzed against the current codebase, and followed by concrete implementation recommendations.
Current state: The kasper directory is hardcoded to .opencode/kasper/ under cwd (index.ts:122 — join(cwd, ".opencode", "kasper")). The global config dir (~/.config/opencode/) is resolved via resolveGlobalOpencodeDir() but only used for the global config file and default config generation, not for the state/log storage.
Analysis:
- This is a valid feature request for CI and shared project setups — multiple instances on the same codebase would share state
- Thread/multi-process safety is already handled: file locks with stale detection (30s), atomic writes (temp+rename), and debounced flushes (2s). A shared directory would inherit these protections automatically.
- The concern about AGENTS.md being local is valid — the kasper state tracks global aggregates, but improvements modify the local
AGENTS.md. If two project copies share kasper state, the improvements queue would reflect both — but the AGENTS.md mutations would only apply locally. This is inconsistent.
Recommendation: Add a state_dir config key (optional, defaults to .opencode/kasper/). Users can point it at a shared location. Document the caveat: improvements applied automatically will only modify the AGENTS.md in the current working copy. State-level aggregates and improvement tracking would be shared.
Current state: Config reloads every 5 seconds via setInterval (index.ts:202). Only acts if JSON-serialized config differs.
Analysis:
- 5s is reasonable for a CLI tool — the overhead is one JSON file read per interval, negligible
- File system watchers (
fs.watch) on Windows are unreliable (false positives, crashers with nested dirs); on Windows specifically, polling is safer - The current approach is already efficient — it reads the config on each interval and compares. Failed reads are non-fatal
- Moving to 60s would delay config hot-reloads unacceptably (e.g., user changes threshold and waits a full minute)
Recommendation: Keep 5s polling. Add a config_poll_interval_ms config key (default 5000) so users who want longer intervals can set it. Do NOT use fs.watch on Windows — the polling approach is battle-tested.
Current state:
session.createdstores agent name and type inagentRegistry(index.ts:668), withagentType: parentID ? "subagent" : "primary"parentToChildrenmap andsessionParentsmap track parent-child relationships (index.ts:687-693)- Subagent sessions get their own
PendingEvalsincechat.messageuser events create one if absent (index.ts:790) pollAndEvaluate()evaluates subagents via the polling loop — there is no filter to skip subagent sessions
Analysis:
- Subagents ARE evaluated. The guard at the top of
pollAndEvaluate()only checksdeletedSessionsandsessionsEvaluated— it does not checkagentType === "subagent" - This means subagent sessions contribute their own scores to the aggregate
- Parent-child relationship IS tracked via
parentToChildren/sessionParentsmaps, but this information is NOT used during evaluation or scoring
Recommendation: Consider whether subagent evaluations are meaningful. A subagent's "user instruction" is whatever the primary agent delegated to it — scoring it against that may not reflect user intent. Options:
- Filter subagents from auto-evaluation in the
pollAndEvaluate()handler by checkingagentInfo?.agentType === "subagent" - Keep evaluating but mark subagent scores with a
subagentflag in the score card to exclude them from primary agent aggregates
Current state: The session.created handler detects subagents via parentID (index.ts:679) and registers them in parentToChildren / sessionParents maps. Subagents can have their own parentID, so chains (primary → subagent A → subagent B) are tracked correctly.
Analysis:
- The data structure supports arbitrary nesting
- BUT: there is no UI or state feature that exposes the nesting (e.g., no tree view, no per-chain scoring)
- The
sessionParentsmap has a 1000-entry limit with partial cleanup — if a chain is very deep, tracking could degrade - The
parent_idfield is stored in state on recordSession (state.ts:135), so relationships survive restarts
Recommendation: This is already handled structurally. Add a /kasper tree command that visualizes the parent-child relationships of recent sessions. Add a limit to the chain depth (e.g., track at most 3 levels) to prevent unbounded map growth.
Current state: Yes, detail_level is wired through correctly:
config.detail_leveldefaults to"standard"(types.ts—DEFAULT_CONFIG)buildEvalPrompt()readsthis.config.detail_level ?? "standard"(scorer.ts:284)- Three behaviors:
- minimal: tool names only (
- toolName), no args/results; no AGENTS.md or agent prompt content in prompt; context note only - standard: tool args + 500-char results; AGENTS.md + agent prompt included
- thorough: tool args + 2000-char results; AGENTS.md + agent prompt included
- minimal: tool names only (
- Minimal mode drastically reduces token usage — only tool names, no config content
Analysis: Fully implemented and functional. No issues.
Recommendation: None. Add a note in the README about estimated token savings per level if useful.
Current state: The buildEvalPrompt() method DOES label sections with markdown headers:
## Agent(if agent name present)## User's Instruction## Agent's Response## Tools Used(minimal) /## Tool Calls Made(standard/thorough)## Current Agent Prompt(non-minimal)## Current AGENTS.md Content(non-minimal)## User Guidance (evaluation focus)(if guidance set)## Note(if compacted)
Analysis: This is already implemented. The scorer prompt clearly segments each input type.
Recommendation: Already done. No changes needed.
Current state: The AgentPromptManager only reads/writes from the project directory:
- Path:
.opencode/agents/{agentName}.md(agent-prompts.ts:30-35) - No global fallback or merge
The AgentsMdManager reads from project-root AGENTS.md with fallback to .opencode/AGENTS.md.
SDK verified: client.app.agents() returns Array<Agent> where each Agent has prompt?: string — the authoritative effective prompt (file + inline config merged). Global config at ~/.config/opencode/ also has agents/ directory per opencode convention. Both client.app.agents() and client.config.get() work.
Analysis:
- There is no global agent prompt loading. If a user has a global
~/.config/opencode/agents/build.md, the plugin won't see it client.app.agents()would return the effective prompt without file I/O — includes inline config overrides- OpenCode itself supports global agent prompts in
~/.config/opencode/agents/
Recommendation:
- Change
AgentPromptManager.read()to fall back to the global~/.config/opencode/agents/{name}.mdif the project-level file doesn't exist - At evaluation time, call
ctx.client.app.agents()to get the authoritative agent prompt (SDK-verified), fall back to file-read
SDK verified: Yes — client.app.agents() returns Array<Agent> with prompt?: string per agent. Config has agent?: { [key: string]: AgentConfig } which can set prompt inline. Agent prompt files at .opencode/agents/{name}.md are the file-based convention. All three sources exist.
Current state: The plugin reads agent prompts only from disk files via AgentPromptManager. Inline config prompts from opencode.json are NOT captured via file reads. The client.app.agents() API (SDK-verified) would return the authoritative effective prompt.
Recommendation: At evaluation time, call ctx.client.app.agents() to get the effective prompt (SDK-verified available). This captures inline config + file content. Fall back to file-read if the API call fails.
SDK verified:
experimental.chat.system.transformhook exists — receives{ sessionID?, model }and mutatesoutput: { system: string[] }— this IS the full system prompt array (AGENTS.md + agent prompt + skill context + framework instructions)UserMessage.system?: stringfield exists in both v1 and v2 — system overrides per user messageclient.session.messages()returns message history, whereUserMessagehassystem?: stringclient.session.init()exists — creates AGENTS.md, confirming it's fundamental to sessions
Current state: The plugin does NOT use experimental.chat.system.transform. The scorer reads AGENTS.md from disk at evaluation time — this is the current state, not what was active during the session. No system prompt capture happens at session start.
Analysis: AGENTS.md IS part of the system prompt for all agents/subagents. The experimental.chat.system.transform hook exposes the full system: string[] array at runtime. Retroactive viewing is possible via UserMessage.system from client.session.messages().
Recommendation: Use experimental.chat.system.transform to capture the system prompt at session start. Store it in PendingEval. At evaluation time, hash-compare with the current AGENTS.md — if they differ, note it in the scoring prompt ("AGENTS.md changed mid-session"). This provides full prompt provenance.
Current state:
- Each session is evaluated independently (
runEvaluationper polling tick viapollAndEvaluate()) considerImprovement()checks if a weakness has been seen ≥min_observations_for_updatetimes across ALL sessions via the state store's aggregate data- Time-decayed weighting (
weakness_decay_days) is applied during aggregate recalculation - Each evaluation suggests improvements based on that single session's score card, but the decision to queue/apply is based on the aggregate pattern frequency
SDK verified: No scoring/evaluation API exists in the SDK — client.session.evaluate() does NOT exist. The plugin's approach (creating ephemeral child sessions for scoring) is the correct and only viable path. There's no batch-analysis API either.
Analysis:
- The aggregation is cross-session — weaknesses are tracked globally and per-agent across all sessions
- The actual suggestion text comes from the latest score card's
suggested_*_updatefield, not from a batch analysis - This means the suggestion quality depends entirely on the LLM's per-session judgment, not on cross-session pattern synthesis
Recommendation: The current approach is correct. Optionally add a "batch review" mode that sends the last N low-scoring sessions to the LLM together for a synthesized improvement suggestion. Add a config key batch_suggestion_size (default 1, meaning per-session).
Current state:
- Each opencode instance runs its own copy of the plugin in-process
- State is stored on disk at
{cwd}/.opencode/kasper/state.json - Cross-process merging is implemented (
state.ts:339—mergeExternalState()):- Acquires file lock before writing
- Reads current on-disk state
- Merges external sessions, improvements, rejected patterns
- Deduplicates improvements by
idfield - Atomic writes (temp file + rename)
- Debounced writes (2s)
Analysis:
- Two instances on the same project will share state via the same
state.jsonfile - The merging logic is active —
mergeExternalState()runs on every flush - BUT: evaluation work is NOT deduplicated across instances.
sessionsEvaluatedis in-memory (Set<string>), not persisted. Instance restart means it could re-evaluate sessions another instance already scored
Recommendation:
- Persist
sessionsEvaluatedto the state file (small — just session IDs) - Add a "last evaluated by" field to session records so instances can skip sessions already scored by another instance
SDK verified:
experimental.chat.system.transformhook exists — captures system prompt at chat time, can store in PendingEvalclient.session.messages()returns message history includingUserMessage.system?: string— provides retroactive system context- Session events:
pollAndEvaluate()replaces the oldsession.idletrigger — sessions are polled at 10s intervals viasetInterval.session.createdfires with{ info: Session }(v1) or{ sessionID, info: Session }(v2) - All plugin hooks verified functional:
"chat.message","tool.execute.after","command.execute.before"are separate named hooks (not dispatched througheventhandler) client.session.delete()takes{ sessionID }(v2) or{ path: { id } }(v1), returnsbooleanclient.config.get()returns fullConfig,client.config.update()exists (notpatch)
| # | Fix | Effort | Impact |
|---|---|---|---|
| 11 | Persist sessionsEvaluated to state file |
Small | Prevents duplicate LLM calls across restarts + instances |
| 7 | Global agent prompt fallback in AgentPromptManager |
Small | Captures global agent prompts, not just project ones |
| 8 | Use client.app.agents() API for authoritative agent prompts |
Medium | Eliminates file I/O races, captures inline config prompts |
| # | Fix | Effort | Impact |
|---|---|---|---|
| 1 | Add state_dir config key for shared kasper state |
Medium | Multi-instance / CI support |
| 12 | Hash AGENTS.md at session start, detect mid-session changes | Small | Better evaluation accuracy |
| 12 | Capture system prompt via experimental.chat.system.transform hook |
Medium | Full prompt provenance for scoring |
| 10 | Optional batch suggestion mode (N sessions → one improvement) | Large | Higher quality LLM suggestions |
| 4 | Add /kasper tree command for session hierarchy |
Small | Visibility into subagent chains |
| # | Fix | Effort | Impact |
|---|---|---|---|
| 2 | Add config_poll_interval_ms config key |
Trivial | User control over reload frequency |
| 11 | Add "last evaluated by instance" field to session records | Small | Cross-instance dedup metadata |
| # | Why |
|---|---|
| 5 | detail_level is already fully implemented and working |
| 6 | Prompt sections are already clearly labeled with markdown headers |
All SDK APIs referenced in this review were verified against @opencode-ai/sdk types (v1 and v2).
| SDK API | Status | Used by plugin? |
|---|---|---|
client.app.agents() → Array<Agent> with prompt?: string |
EXISTS | No — plugin could use this for authoritative agent prompts |
client.config.get() → Config |
EXISTS | No — plugin reads config from JSONC files only |
client.session.create() |
EXISTS | Yes — for ephemeral scoring sessions |
client.session.prompt() |
EXISTS | Yes — for scoring LLM call |
client.session.delete() |
EXISTS | Yes — cleanup scoring sessions |
client.session.messages() |
EXISTS | Yes — manual historical evaluation |
client.session.init() |
EXISTS | No |
client.session.evaluate() |
DOES NOT EXIST | Plugin's child-session scoring is the only viable path |
experimental.chat.system.transform hook |
EXISTS | No — key gap for prompt provenance |
experimental.session.compacting hook |
EXISTS | Yes — injects score context |
"chat.message" named hook |
EXISTS | Yes — accumulates PendingEval |
"tool.execute.after" named hook |
EXISTS | Yes — records tool calls |
"command.execute.before" named hook |
EXISTS | Yes — intercepts /kasper commands |
"session.created" named hook |
EXISTS (dynamic dispatch) | Yes — registers agent info |
session.idle SSE event |
EXISTS | Deprecated — poll-based evaluation via pollAndEvaluate() replaces idle-event trigger |
UserMessage.system?: string |
EXISTS | No — could provide retroactive system context |
Key finding: The plugin already uses all available hooks correctly. The gaps are (a) not calling client.app.agents() for authoritative prompts, (b) not using experimental.chat.system.transform for prompt provenance, and (c) not persisting sessionsEvaluated for cross-instance dedup. These are the three most impactful fixes.