Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agents/roadmaps/road-to-v1-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ complexity: heavy
> **Goal.** Plugin can connect to arbitrary MCP servers (per `.agent-settings.yml::mcp.servers`). All 136 agent-config commands are callable via slash-picker. Memories work (lokal + optional `@event4u/agent-memory` MCP backend). Hooks (sessionStart/End/Stop) respect agent-config hook system.

- [x] **T-1101 — MCP Client.** <!-- done 2026-05-30: packages/core/src/mcp/ — protocol.ts (zod JSON-RPC + MCP tool/result schemas, contentToText), transport.ts (McpTransport seam + StdioTransport spawning NDJSON over stdio, reusing llm/ndjson.ts), fake-transport.ts (deterministic in-memory test transport), client.ts (McpClient: initialize handshake + notifications/initialized, listTools, callTool, id-correlated requests, bounded init/request timeouts, fail-open markDead rejecting in-flight on transport death), registry.ts (McpToolRegistry: aggregate clients keyed by server-id, prefix `<server-id>:<tool>`, route callTool, map to ToolDefinition), manager.ts (McpManager: reads mcp.servers[], spawns+connects each fail-open, per-server degrade, dispose). agent-settings.ts gained mcp.servers schema (id rejects ':'). HAND-ROLLED, no @modelcontextprotocol/sdk → ADR-006 (no-native-deps law + ADR-003 NDJSON precedent; council codex/gpt-5.5+gemini-2.5-pro split, gemini direct). 25 mcp tests + 3 settings tests, full core suite 506 pass, task ci green. --> Hand-rolled minimal MCP stdio client (ADR-006, not `@modelcontextprotocol/sdk`). Reads `.agent-settings.yml::mcp.servers[]`, spawns each as subprocess, manages JSON-RPC connection. Tool registry includes MCP tools with prefix `<server-id>:<tool-name>`.
- [x] **T-1102 — agent-config MCP server consumption.** <!-- done 2026-05-30: packages/core/src/mcp/agent-config-client.ts — AgentConfigMcpClient typed convenience over McpClient (memoryLookup, chatHistoryRead, listSkills, skillRead, commandRead → flattened {text,isError}); DEFAULT_AGENT_CONFIG_SERVER = {id:'agent-config', command:'npx', args:['@event4u/agent-config','mcp']}. 3 tests with FakeTransport echo responder. The live connection at runtime is wired into the dispatcher in the IDE-runtime exit gate. LOCAL-WALKER SIBLING 2026-06-02 (ADR-050): the MCP `list_skills`/`skill_read` here is IDE-gated (MCP server lifecycle is native); the offline local-walker authority path now ships as the `configList {kind?,limit?}` protocol method on a new `ConfigHandler`, wiring the previously-dead `agent-config-walker.ts::indexByKind` to surface skills + rules (+ commands) the command palette (ADR-048) does not — `ConfigSummary {kind,name,description,path}`, `MAX_CONFIG_LIST_RESULTS=100` + total, +3 DTOs (64→67), `task ci` exit 0, `jetbrains:check` green. Mirrors `commandList` ↔ MCP `command_read`. The skill picker / rules viewer render stays IDE. --> Connect to the agent-config-shipped MCP server (`npx @event4u/agent-config mcp`). Use its tools (`memory_lookup`, `chat_history_read`, `list_skills`, `skill_read`, `command_read`) instead of our own filesystem readers for skill/rule/command content.
- [x] **T-1102 — agent-config MCP server consumption.** <!-- done 2026-05-30: packages/core/src/mcp/agent-config-client.ts — AgentConfigMcpClient typed convenience over McpClient (memoryLookup, chatHistoryRead, listSkills, skillRead, commandRead → flattened {text,isError}); DEFAULT_AGENT_CONFIG_SERVER = {id:'agent-config', command:'npx', args:['@event4u/agent-config','mcp']}. 3 tests with FakeTransport echo responder. The live connection at runtime is wired into the dispatcher in the IDE-runtime exit gate. LOCAL-WALKER SIBLING 2026-06-02 (ADR-050): the MCP `list_skills`/`skill_read` here is IDE-gated (MCP server lifecycle is native); the offline local-walker authority path now ships as the `configList {kind?,limit?}` protocol method on a new `ConfigHandler`, wiring the previously-dead `agent-config-walker.ts::indexByKind` to surface skills + rules (+ commands) the command palette (ADR-048) does not — `ConfigSummary {kind,name,description,path}`, `MAX_CONFIG_LIST_RESULTS=100` + total, +3 DTOs (64→67), `task ci` exit 0, `jetbrains:check` green. Mirrors `commandList` ↔ MCP `command_read`. The skill picker / rules viewer render stays IDE. READ SIBLING 2026-06-02 (ADR-052): `configList` shipped without a read half — the same half-contract the command council (ADR-048) rejected by requiring BOTH list+read. Now completed with `configRead {kind,name}` → `{kind,name,source,body}` on the same `ConfigHandler`, reading the body off the SAME cached walk (list/read can't disagree). LOCAL-ONLY (`ConfigSource='local'|'missing'`, no `mcp` — MCP has no `rule_read`); matches the DISPLAY name via a shared `displayName` helper. Council codex+gemini UNANIMOUS Q0–Q3=A. +2 DTOs (67→69), `task ci` exit 0 (core 1147), `jetbrains:check` green. Detail-pane render stays IDE. --> Connect to the agent-config-shipped MCP server (`npx @event4u/agent-config mcp`). Use its tools (`memory_lookup`, `chat_history_read`, `list_skills`, `skill_read`, `command_read`) instead of our own filesystem readers for skill/rule/command content.
- [~] **T-1103 — All 136 commands callable.** <!-- core done 2026-05-30: packages/core/src/commands/loader.ts loadCommandProcedure(name, {mcp?, localNodes}) — prefers agent-config MCP command_read, falls back to local walker body, reports missing; 5 tests. [~] not [x]: the slash-picker overlay rendering all commands + the actual invocation UX are webview/IDE-runtime surfaces (the picker filter pure-fn already exists from T-402); they bind to this loader but need a GUI session to verify. TRANSPORT WIRED 2026-06-02 (ADR-048, PR pending): the dead picker (`commandsToPickerItems`/`pickCommands`, T-402) + loader (`loadCommandProcedure`) are now exposed over two read-only protocol methods on a new `CommandHandler` (walk-once-cached `walkAgentConfig(cwd)`, the live collaborator from ADR-043): `commandList {query?}` (empty → all alphabetical, query → ranked; `total` = match count before a `MAX_COMMAND_LIST_RESULTS=100` cap — council Q1=B/Q2=A) + `commandRead {name}` (MCP-first/local-fallback body — Q1=B; core stays the resolution authority so the agent + palette agree and it works offline — Q5/Q6). `CommandSummary {name,description,path}` reuses the PickerItem shape (Q3=A); absolute path kept for IDE click-through (Q4=A); `requireCommands()` → `commands_not_configured`; fail-open walk. +5 DTOs (59→64), `jetbrains:check` BUILD SUCCESSFUL, +15 core tests incl. a buildCoreDispatcher live-wiring proof. STILL `[~]` — the overlay rendering, favourites, and invocation UX are IDE surfaces; an agent-config MCP client (so `commandRead` prefers it) is a documented follow-up. --> Slash-picker shows all commands from the agent-config tree (filtered by user-set favourites if Phase 0 Spike 0.5 recommended favourites). Each command's procedure is loaded via the MCP `command_read` tool at invocation time.
- [x] **T-1104 — Memories — local.** <!-- done 2026-05-30: packages/core/src/memory/local.ts — LocalMemoryStore over `.event4u-agent/memories/` in Markdown+YAML-frontmatter (name/description/metadata.type) + regenerated MEMORY.md index, NOT JSON: the roadmap's "JSON" wording yields to the real agent-config on-disk contract per ADR-006 (council unanimous). list/read/write/delete/regenerateIndex + serializeRecord; reads agent-config-authored files for external compat; kebab-name validation. 8 tests. --> `packages/core/src/memory/local.ts` reads/writes `.event4u-agent/memories/` in agent-config's md+frontmatter+MEMORY.md format (ADR-006, not JSON). Two memory types in MVP: `user` (long-lived) and `feedback` (workflow corrections).
- [x] **T-1105 — Memories — MCP backend.** <!-- done 2026-05-30: packages/core/src/memory/backend.ts — MemoryBackend iface; LocalMemoryBackend (filterRecords over the store), McpMemoryBackend (memory_lookup/memory_write via McpClient + best-effort parseMcpRecords), RoutingMemoryBackend (MCP-first lookup with local fallback on throw; writes always mirror locally so a later outage still serves). 9 tests. --> Optional: if `@event4u/agent-memory` MCP server configured, route memory_lookup/memory_write calls there. Local fallback if MCP server unreachable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,22 @@ data class ConfigListResponse(
val total: Int,
)

/** Load one artifact's body by (kind, name); kind is required as names are not unique. */
@Serializable
data class ConfigReadRequest(
val kind: String,
val name: String,
)

/** An artifact body read from the local walk index; source is local/missing (no mcp). */
@Serializable
data class ConfigReadResponse(
val kind: String,
val name: String,
val source: String,
val body: String,
)

/** Start an agentic chat turn (LLM + tool loop). maxIterations caps the loop; omitted = Core default. */
@Serializable
data class AgentTurnRequest(
Expand Down
119 changes: 119 additions & 0 deletions docs/adr/ADR-052-config-read-body-sibling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
adr: 052
title: Agent-Config Body Read — Completing The configList Contract With A Local-Only configRead {kind,name} Sibling (T-401)
status: Proposed (drafted 2026-06-02 — awaits user sign-off before flip to Accepted)
deciders: solo-dev (event4u team lead) — sign-off required
consulted: AI Council — codex-cli 0.134.0 + gemini-cli 0.41.2 (2026-06-02), both run serially per the documented stdin gotcha. UNANIMOUS Q0–Q3 = A across both members; Q4/Q5 traps reconciled in the Decision.
related: the read half of ADR-050 (configList). Mirrors the list+read PAIR that ADR-048 shipped for commands (`commandList` + `commandRead`); the command council had explicitly required BOTH because list-without-read is "half a contract". Reuses the `walkAgentConfig` collaborator already live for the rules loader (ADR-043), the command palette (ADR-048), and `configList` (ADR-050).
date: 2026-06-02
---

# ADR-052 — Agent-Config Body Read (configRead, T-401)

## Status

**Proposed** — awaits sign-off. One branch / one PR, committed in logical
chunks (protocol+codegen → core handler+wiring → tests → docs), preserving
minimal-safe-diff.

CI-verified locally: `task ci` exit 0 (lint, format, build, typecheck, test —
core 1147 pass / 1 skip, +15). `task jetbrains:check` BUILD SUCCESSFUL (the +2
Kotlin DTOs compile, 67 → 69). **No checkbox flip** — the skill picker / rules
viewer detail pane render remains an IDE surface; this is the headless body
data path those surfaces will call.

## Context

ADR-050 shipped `configList {kind?, limit?}` → lightweight `ConfigSummary`s for
the IDE's skill picker / rules viewer, but **without a read sibling**. The
command-palette council (ADR-048) had explicitly chosen BOTH `commandList` +
`commandRead` because "half a contract otherwise" — a list whose items the IDE
cannot open into a detail pane is incomplete. `configList` is exactly that same
half-contract for skills + rules + commands.

The `ConfigHandler` (ADR-050) walks the agent-config tree once and caches it;
every walked `ConfigNode` already carries `{kind, name, absPath, frontmatter,
body}` — the body is parsed from disk during the walk for **all three** kinds.
So the body the IDE needs already exists in the cached index; nothing new must
be read from disk. The agent-config MCP client (`AgentConfigMcpClient`) exposes
`skill_read` and `command_read` tools, but **no** `rule_read`.

## Decision

Add a read-only `configRead {kind, name}` → `{kind, name, source, body}`
protocol method on the existing `ConfigHandler`, reading the body straight off
the same cached walk `configList` groups.

Council (codex-cli + gemini-cli, both serial, UNANIMOUS Q0–Q3 = A):

- **Q0=A** — wire it now. Completes the `configList` contract; mirrors
`commandRead`; the IDE skill/rule detail pane needs the body; Core stays the
resolution authority.
- **Q1=A** — the request key is `{kind, name}`, not `{name}`. A name is **not**
unique across kinds (a skill and a command can share a slug), so `kind` is the
mandatory discriminator.
- **Q2=A** — **LOCAL-ONLY**. Read `node.body` from the cached walk; uniform
across all three kinds. An MCP-first path (like `commandRead`) would be
asymmetric — MCP has `skill_read`/`command_read` but no `rule_read` — and
would add an MCP dependency to a handler deliberately designed without one
(ADR-050 Q6). Hence `ConfigSourceSchema = 'local' | 'missing'` (no `mcp`
member, unlike `CommandSource`).
- **Q3=A** — response `{kind, name, source: 'local'|'missing', body}` mirrors
`commandRead`; a miss is `source: 'missing'` with an empty body (graceful, no
throw), consistent with `commandRead`.
- **Q4 — full body, uncapped.** codex: match `commandRead` (which returns the
full body uncapped); add a cap only behind evidence of an oversized-line
problem. gemini flagged NDJSON line length as the theoretical risk. Resolved
in favour of consistency with `commandRead`: a skill body is comparable in
size to a command procedure the transport already carries; a special-case cap
here would be an asymmetric surprise. (If a future skill body proves
pathological, a cap can land uniformly across both read methods.)
- **Q5 — traps, both satisfied by design.** codex: list and read must not
disagree after a file change → guaranteed, because `read` goes through the
**same** `this.nodes()` walk-once cache as `list`. gemini: never interpolate
`name` into a disk path (traversal) → guaranteed, because `read` matches
against the in-memory walked nodes and never touches the filesystem with the
request value.

One correctness refinement beyond the council: `read` matches on the artifact's
**display name** (`displayName` = a non-empty frontmatter `name`, else the file
slug) — i.e. exactly the `name` `configList` returned — not the raw file slug.
The shared `displayName` helper is now used by both `toSummary` (list) and
`read`, so the two can never key on different names.

## Consequences

- **+2 DTOs** (`ConfigReadRequest`, `ConfigReadResponse`) + `ConfigSource`
enum, 67 → 69 in codegen; +1 protocol method (`configRead`), behind the
existing `requireConfig()` → coded `config_not_configured` when absent.
- The skill picker / rules viewer detail pane now has a headless,
offline-capable body data path that does not depend on the agent-config MCP
server being up — Core stays the resolution authority, exactly as for
`configList` and `commandRead`.
- No behaviour change to any existing path; `list` is refactored only to share
the new `displayName` helper (byte-identical projection).
- The IDE render remains the last-mile surface; this ADR does not flip any
roadmap checkbox.

## Alternatives considered

- **Leave `configList` without a read sibling (Q0=B).** Rejected — it is the
documented half-contract; the IDE cannot open a listed artifact.
- **`{name}`-only key (Q1=B).** Rejected — names collide across kinds; the
result would be ambiguous.
- **MCP-first like `commandRead` (Q2=B).** Rejected — asymmetric (no
`rule_read`) and pulls an MCP dep into a local-only handler for no gain, since
the body is already on every walked node.
- **Cap the body length (Q4).** Deferred (YAGNI) — match `commandRead`'s
uncapped body; revisit uniformly if evidence of an oversized line appears.

## References

- `packages/core/src/config/handler.ts` — `ConfigHandler.read` + the shared
`displayName` helper.
- `packages/protocol/src/schema.ts` — `ConfigReadRequest/Response`,
`ConfigSource`.
- ADR-050 — the `configList` half this completes.
- ADR-048 — the `commandList` + `commandRead` PAIR this mirrors.
- ADR-043 — the `walkAgentConfig` live collaborator reused here.
- `agents/roadmaps/road-to-v1-0.md` T-401 — the registry data path.
Loading
Loading