Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
352c14a
proto: add file-layout rule for role-named split files
scrothers Jul 24, 2026
b06786a
proto: rename service-free leaf packages to types.proto/events.proto
scrothers Jul 24, 2026
e4378bd
proto: split service-bearing packages into slot files
scrothers Jul 24, 2026
2a51424
proto: promote slashcommand to its own plugin category
scrothers Jul 24, 2026
98c74fa
pluginruntime,statebackend,common: wire up slashcommand category
scrothers Jul 24, 2026
b79836e
docs: promote slashcommand to its own plugin category
scrothers Jul 24, 2026
43ec350
rules,site: six -> seven plugin categories sweep
scrothers Jul 24, 2026
a992668
specs: expand kernel-callback service with telemetry, config, event bus
scrothers Jul 24, 2026
c033cf3
proto: add trace/v1, metric/v1, extend kernel/v1 callback service
scrothers Jul 24, 2026
e433070
eventbus,telemetry: wildcard filters, span relay, dynamic metrics
scrothers Jul 24, 2026
33a41b4
kernelcallback: implement ExportSpans, RecordMetrics, GetTelemetryCon…
scrothers Jul 24, 2026
685390e
pkg/kernel: hand-written plugin-author SDK over KernelCallbackService
scrothers Jul 24, 2026
9380115
pkg/content: add ContentBlock builder layer
scrothers Jul 24, 2026
52ea893
pkg/render: add builder API and schema-version dispatch
scrothers Jul 24, 2026
54df34b
pkg/schema: add fluent builders for the tool-schema subset
scrothers Jul 24, 2026
61e10c3
pkg/config: add validating ConfigSchema/ConfigAttribute builder
scrothers Jul 24, 2026
3e7e1d3
pkg/kernel: add RunSession, CountTokens, Emit, ReadEvents, GetSession…
scrothers Jul 24, 2026
e50510c
pkg/telemetry: return public Provider interface from Bootstrap
scrothers Jul 24, 2026
84c1ab4
pkg/plugin: add shared plugin subprocess serving layer
scrothers Jul 24, 2026
a785e5f
Merge branch 'worktree-agent-a7a83025a177f319e' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
a57fded
Merge branch 'worktree-agent-a394bfdc498bd3b8e' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
24de56b
Merge branch 'worktree-agent-a5216c80908e55561' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
1f2f3a8
Merge branch 'worktree-agent-a194c2d02fa4dcd74' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
330062f
Merge branch 'worktree-agent-afc1becbc2836325d' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
cc90ded
pkg/tool: implement plugin-author SDK for tool provider category
scrothers Jul 24, 2026
46fa387
pkg/context: add hand-written provider SDK
scrothers Jul 24, 2026
3e62591
pkg/model: add hand-written model provider SDK
scrothers Jul 24, 2026
8fec696
pkg/memory: add memory provider plugin SDK
scrothers Jul 24, 2026
1ed44b4
pkg/widget: add hand-written widget provider SDK
scrothers Jul 24, 2026
a3bf97e
pkg/hook: add plugin-author SDK for HookSubscriberService
scrothers Jul 24, 2026
7a7969f
pkg/frontend: add hand-written frontend provider SDK
scrothers Jul 24, 2026
9858789
Merge branch 'worktree-agent-a544a0fba145b4883' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
6745e05
Merge branch 'worktree-agent-adb8e86885fac7581' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
13fff5e
Merge branch 'worktree-agent-ac098529a9884a40c' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
47a9bbe
Merge branch 'worktree-agent-a5e75a8a72837b441' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
ff1204a
Merge branch 'worktree-agent-ae5f486c958ef86f7' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
a9be83e
Merge branch 'worktree-agent-a6bfd8fddd5bd1438' into feat/pkg-plugin-sdk
scrothers Jul 24, 2026
8afd8eb
pkg/tool: fix package-name stutter (revive)
scrothers Jul 24, 2026
37df8ab
pkg/context: fix package-name stutter (revive)
scrothers Jul 24, 2026
caf9716
pkg/frontend: fix package-name stutter and lint findings
scrothers Jul 24, 2026
e152d08
pkg/slashcommand: implement the slash-command provider SDK
scrothers Jul 24, 2026
8cec16e
rules: carve out the pkg/ vs internal/ type boundary
scrothers Jul 24, 2026
405cb1d
pluginruntime: rebuild integration fixture on pkg/plugin+tool
scrothers Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
76 changes: 62 additions & 14 deletions .claude/rules/go-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,68 @@ api/ .proto sources — buf's module root (see buf.yaml).
pkg/ first-class, third-party-consumable Go integration —
the only thing a plugin author needs to import.
<category>/ pkg/model/, pkg/tool/, pkg/context/, pkg/memory/,
pkg/frontend/, pkg/widget/, plus pkg/kernel/ for the
kernel-callback service (docs/specifications/kernel-callbacks.md)
*.go hand-written ergonomic SDK: the thin, idiomatic Go
surface most plugin authors actually consume
pkg/frontend/, pkg/widget/, pkg/slashcommand/,
pkg/hook/ (the cross-category HookSubscriberService),
plus pkg/kernel/ for the kernel-callback service
(docs/specifications/kernel-callbacks.md)
*.go hand-written plugin-author SDK: domain types and
the author-facing interface(s), converted to/from
the generated wire types at the package boundary
(see "The pkg/ vs internal/ boundary" below)
proto/v1/*.pb.go buf-generated message + gRPC stubs. Never
hand-edited — see proto.md and plugin-runtime.md.
plugin/ the shared plugin-subprocess serving layer every
category SDK builds on — handshake, multi-service
muxing, the lazy kernel-callback handle, error
helpers. No proto/ subtree of its own.
render/, config/, schema/, content/ shared, cross-category builder
packages (RenderTree nodes, ConfigSchema,
the tool/model JSON-Schema subset, ContentBlock)
that every category SDK composes rather than
reimplementing per category.
docs/specifications/ protocol contracts (already exists, authoritative)
```

Nothing generated lives at the repo root. `pkg/<category>/` is deliberately
split in two: the `proto/v1/` subtree is 100% derived (`buf generate`
output), while the sibling `.go` files in `pkg/<category>/` are hand-written
and are where most plugin authors actually spend their time — a thin,
idiomatic wrapper over the generated stubs, not the stubs themselves.
and are where most plugin authors actually spend their time — the
plugin-author-facing SDK, not a pass-through to the generated stubs. See
"The pkg/ vs internal/ boundary" below for exactly what that SDK layer is
and isn't allowed to do.

## The pkg/ vs internal/ boundary

`pkg/<category>/`'s hand-written `.go` files (never `proto/v1/`) and
`internal/` sit on opposite sides of a deliberate asymmetry in how many Go
representations a wire message gets:

- **`pkg/<category>/` MAY define its own domain types** — plain Go structs
and enums shaped for how a plugin author actually thinks about the
category (e.g. `tool.Call`, `tool.Result`, `model.Spec`), converted
to/from the generated `pkg/<category>/proto/v1` message at the package
boundary (conventionally in a `convert.go`). This is a deliberate
ergonomics choice for the third-party-facing SDK: an author writing a
plugin should not have to hand-assemble `structpb.Struct` literals or
navigate a `oneof` wrapper type to implement one RPC. The conversion
layer is the SDK's job, not the author's.
- **`internal/` MUST consume the same `pkg/<category>/proto/v1` generated
types the wire actually carries, directly** — never a second, parallel
internal type that gets translated to and from the generated one. The
kernel-side client stub (the interface/driver-pattern code described
above) imports `pkg/<category>/proto/v1` (and, where convenient, the
`pkg/<category>` SDK wrapper) exactly as a third-party plugin author does
on the other end of the connection. There is exactly one Go
representation of each wire message on the kernel side — this is
unchanged and remains load-bearing for `internal/`.

A `pkg/<category>` domain type is real Go, not a wire type in disguise, but
it stays a *thin* wrapper in spirit: no business logic lives in `convert.go`
beyond validating the invariants the category's own spec states as MUST
(`internal/`'s domain logic — policy, plan/apply, cost — is not
duplicated here). If a `pkg/<category>` type starts accumulating behavior
beyond "shape the RPC ergonomically and validate what the spec requires,"
that behavior belongs in `internal/`, not the SDK.

`cmd/` binaries MUST stay thin: parse config, construct dependencies via
`internal/` constructors, call `Run`. If a `cmd/` file grows past simple
Expand All @@ -45,7 +93,7 @@ in a narrowly-named package that says what it does.

## Interfaces: the driver pattern

Every pluggable concern (each of the six provider categories, plus internal
Every pluggable concern (each of the seven provider categories, plus internal
swappable backends like the memory store) follows the same shape:

```
Expand Down Expand Up @@ -77,15 +125,15 @@ vector — backend-agnostic by design, see `docs/specifications/memory/README.md
is `internal/memory/` (interface) with
`internal/memory/drivers/{markdown,sqlite,vector}/`.

This applies to internal swappable components. The six *plugin* categories
themselves (model, tool, context, memory, frontend, widget) are out-of-process
via `hashicorp/go-plugin` — see `plugin-runtime.md` — but the kernel-side code
This applies to internal swappable components. The seven *plugin* categories
themselves (model, tool, context, memory, frontend, widget, slashcommand) are
out-of-process via `hashicorp/go-plugin` — see `plugin-runtime.md` — but the kernel-side code
that talks to them (the client stub, the registry, the cache) still follows
this same interface/driver shape internally.

The kernel-side client stub imports the same `pkg/<category>/proto/v1`
generated types (and, where convenient, the `pkg/<category>` SDK wrapper)
that a third-party plugin author imports on the other end of the connection.
There is exactly one Go representation of each wire message — the kernel
does not maintain a second, parallel internal type that gets translated to
and from the generated one.
that a third-party plugin author imports on the other end of the connection
— see "The pkg/ vs internal/ boundary" above for the full rule and the one
place a second Go representation *is* allowed (the plugin-author-facing
domain types inside `pkg/<category>` itself, never `internal/`).
39 changes: 27 additions & 12 deletions .claude/rules/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ dictated by the specs and MUST match exactly.
| Tool | `Invoke` | server-streaming | `docs/specifications/tool/protocol.md` |
| Frontend | `Attach` | **bidirectional** streaming | `docs/specifications/frontend/frontend-protocol.md` |
| Widget | `Attach` | server-streaming only | `docs/specifications/frontend/widget-protocol.md` |
| Slashcommand | `Invoke` | server-streaming | `docs/specifications/slashcommand/protocol.md` (same shape as Tool's `Invoke` — a direct-invoke command is a tool-shaped operation) |
| Kernel callback | `RunSession`, `CountTokens` | bidirectional (go-plugin's native plugin→kernel channel) | `docs/specifications/kernel-callbacks.md` |
| Kernel callback | `Subscribe` | server-streaming | `docs/specifications/kernel-callbacks.md#subscribe` (event-bus fan-out; see `docs/specifications/event-bus.md`) |
| Kernel callback | `ReadEvents` | server-streaming | `docs/specifications/kernel-callbacks.md#readevents` |

Frontend `Attach` and the kernel-callback channel are the **only** two
genuinely bidirectional RPCs in the whole protocol. Do not default a new RPC
to bidi streaming because it "might need it later" — pick the narrowest shape
the spec calls for.
genuinely bidirectional RPCs in the whole protocol — that channel's own
`Subscribe`/`ReadEvents` additions are server-streaming, not a second bidi
RPC on it. Do not default a new RPC to bidi streaming because it "might need
it later" — pick the narrowest shape the spec calls for.

- **A backend that has no real streaming to do still implements the
streaming RPC shape** and emits exactly one terminal message. Do not add a
Expand Down Expand Up @@ -75,15 +79,26 @@ kernel decide.
cancellation promptly (previous section) is what makes that deadline
actually bound wall-clock time instead of leaking a goroutine.

## The strong-typing rule and its one carve-out

`proto.md` bans `Any`/untyped `bytes`/loose maps as a general rule. The
Emit→Render→Paint payload (`docs/specifications/model/protocol.md#render`,
`docs/specifications/frontend/render-tree.md`) is the one deliberate exception: it is
opaque *by design* so a producer's payload format can evolve independently
of the kernel. Do not "fix" this by giving it a concrete message type — that
would defeat the plugin-independence the specs are built around. Every other
field stays strongly typed.
## The strong-typing rule and its carve-outs

`proto.md` bans `Any`/untyped `bytes`/loose maps as a general rule, with two
named exceptions — no others exist, and a third is not a shortcut to reach
for by analogy:

- The Emit→Render→Paint payload (`docs/specifications/model/protocol.md#render`,
`docs/specifications/frontend/render-tree.md`). Opaque *by design* so a
producer's payload format can evolve independently of the kernel. Do not
"fix" this by giving it a concrete message type — that would defeat the
plugin-independence the specs are built around.
- The event-bus `Publish`/`BusEvent` payload (`docs/specifications/event-bus.md`,
`docs/specifications/kernel-callbacks.md#publish`). Opaque for a different
reason: a third-party plugin's own event shape is unknowable to the kernel
ahead of time, the same reasoning that already applies to a category
provider's `Configure`-time config values. `payload_type` and
`schema_version` carry enough typing information for a *subscriber* to
decode it; the kernel itself never needs to.

Every other field stays strongly typed.

Similarly, `docs/specifications/configuration/`'s two type systems — HCL/`cty` for
provider config, a restricted JSON-Schema subset for tool I/O — are
Expand Down
10 changes: 9 additions & 1 deletion .claude/rules/logging-telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ external systems. If yes, it's pure domain, full stop.
`internal/telemetry.Provider`'s `ClientHandler()`/`ServerHandler()` into
its `grpc.WithStatsHandler`/`grpc.StatsHandler` options
(`internal/telemetry/grpchooks.go`). MUST NOT hand-roll a separate
trace-context propagation mechanism.
trace-context propagation mechanism. **This is unaffected by
`ExportSpans`/`RecordMetrics` relaying a plugin's finished spans/metrics
through the kernel** (`docs/specifications/observability.md#the-relay-model`)
— relay is a transport decision about where already-finished telemetry
data is *exported to*, not a second, competing mechanism for how an
in-flight call's trace context crosses the plugin boundary in the first
place. `traceparent` still propagates via the otelgrpc stats handlers
exactly as before; don't read the relay RPCs as license to also hand-roll
context propagation.
- Replay-path code (`docs/specifications/state-backend.md`) MUST select the
`noop` telemetry driver, unconditionally, no exceptions. Telemetry MUST
NOT persist `trace_id`/`span_id` into any table and MUST NOT recompute a
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/plugin-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ paths:

# Plugin runtime (`hashicorp/go-plugin`) conventions

Every one of the six plugin categories is a subprocess speaking gRPC over
Every one of the seven plugin categories is a subprocess speaking gRPC over
`hashicorp/go-plugin`. This file covers the lifecycle/runtime half; `grpc.md`
covers RPC shape and `proto.md` covers wire typing.

Expand Down
55 changes: 55 additions & 0 deletions .claude/rules/proto-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
paths:
- "**/*.proto"
---

# Protobuf file layout

`proto.md` governs syntax, typing, documentation, and versioning for what goes inside a proto file; this rule governs how a package's declarations are split across files within `api/pluggableharness/<category>/v1/`. It exists because `buf.yaml` sets `breaking: use: FILE` (see `proto.md`'s versioning section) — file layout becomes part of the frozen wire contract the moment the first `v*` tag is cut, so the split described here MUST be mechanical and repeatable rather than ad hoc, the same way `go-layout.md` fixes Go package shape before any Go file exists to match a glob against.

## The slot template

Every package directory uses a fixed set of role-named files, never the package-leaf basename:

| File | Holds | Present when |
|---|---|---|
| `service.proto` | The `service` block, nothing else | The package declares a service |
| `rpc_request.proto` | Every message in an rpc's input position, including empty ones (`message DescribeRequest {}`) | The package declares a service |
| `rpc_response.proto` | Every message in an rpc's **unary** return position | The service has at least one unary rpc |
| `events.proto` | Occurrence-shaped messages: anything flowing over a `stream` in either direction, plus oneof event envelopes and their variant payloads | The package has streamed rpcs or a standalone event/payload registry |
| `types.proto` | Domain messages and enums — capabilities, specs, records, refs, taxonomy enums | Almost every package |
| `errors.proto` | The category's `*Error` message and its `*ErrorCategory` enum | The package defines an error taxonomy |

A slot with nothing to hold is not created. A package with no service (e.g. `common`, `render`, `content`) collapses to a single `types.proto` — except a package whose entire purpose is a flat event/payload registry (e.g. `event.v1`), which uses `events.proto` instead of `types.proto` as its one file.

## Assignment is by role, never by name suffix

A message's slot is determined by where it appears on the wire, not by whether its name ends in `Request`/`Response`/`Event`. `pluggableharness.context.v1`'s `Contribute(ContextRequest) returns (ContextContribution)` puts `ContextRequest` in `rpc_request.proto` and `ContextContribution` in `rpc_response.proto` even though neither name carries the expected suffix. A message nested inside a response but never itself returned by an rpc (e.g. a per-item result embedded in a list response) belongs in `types.proto`, not `rpc_response.proto`.

A streamed return goes to `events.proto`, never `rpc_response.proto` — a server-streaming or bidirectional rpc's message flow is occurrence-shaped, not request/response-shaped. This applies uniformly to `stream` on either side of an rpc signature, per `grpc.md`'s streaming-shape table.

## Nested types and grouped declarations travel together

Nested messages, nested enums, and `reserved` statements always move with the message that owns them — they are never separated into a different file than their parent. A `oneof` wrapper and every one of its variant messages stay in the same file as a unit, even when that unit is large; splitting a oneof's variants away from its wrapper (or from each other) is never a valid cut, regardless of resulting file size.

## Exactly one package doc comment per package

`protoc-gen-go` copies the file-level comment block immediately above a `package` statement verbatim into the generated `.pb.go`'s package doc. With multiple files per proto package, only one file may carry that block, or multiple generated Go files end up claiming to be the package doc. The doc-comment file is `service.proto` for a service-bearing package, and the package's single collapsed file (`types.proto` or `events.proto`) for a service-free package — the proto analogue of a Go package's `doc.go`.

Every other file in the package gets a one-line purpose comment placed below its own `option go_package` line, as a comment detached from the `package` statement, so it never attaches to the package doc.

## Intra-package imports form a DAG

protoc requires an explicit `import` for any type referenced from another file, including a sibling file in the same package — there is no implicit same-package visibility as in Go. The intra-package import graph MUST stay acyclic; `buf build` rejects a cycle outright, the same hazard `proto.md` and `docs/specifications/model/data-types.md` already document at the cross-package level. Allowed direction, no back-edges:

```
types.proto ─┬─> errors.proto ─┬─> rpc_response.proto ─┐
└─────────────────┴─> events.proto ───────┼─> service.proto
└─> rpc_request.proto ─┘
```

Each file imports only the specific sibling and cross-package files whose types it actually references — never a broader import for convenience. Cross-package imports name a specific file (e.g. `import "pluggableharness/schema/v1/types.proto";`), never a package as a whole. The existing import-block ordering convention holds per file: the `google/protobuf/*` well-known-types group first, then a single alphabetized `pluggableharness/*` group.

## The layout is frozen at the first release tag

Once `breaking: use: FILE` starts being enforced against a real `v*` tag, a declaration may be added to an existing slot file or to a brand-new file, but an existing declaration may never move from one file to another — that is a file-level break under `FILE` even when the wire format and generated Go API are unchanged. Do not "clean up" a slot file's contents after the first release; if a slot has grown unwieldy, that is a `v2` package decision, not a same-version file reshuffle.
Loading
Loading