feat(v0.6.0): external memory + capability compile + MCP server + signing#23
Merged
Conversation
…nd 4 priority A12) Round 4 external review (AI #2) proposed widening the protocol-level feature-negotiation surface so the host coordinator can route Query / Store / fallback decisions per backend without hard-coding plugin identity. v0.5 shipped 5 capability flags; v0.6 adds 11 more — TemporalQuery / MetadataFilter / NamespaceIsolation / SoftDelete / BulkImport / DedupeKey / SourceEventID / TTL / EventualConsistency plus int32 MaxBatchSize and MaxQueryTokens limits. The expansion is additive — v0.5 plugin binaries continue to advertise only the original 5 flags (proto field numbers 1-6); the new fields default to the proto zero values, which the host treats as "feature not supported". No wire break. SQLite reference-fallback now advertises its honest values: SoftDelete + BulkImport + DedupeKey + SourceEventID = true, since v0.5.1 made each of those real (Forget flips state, Import restores rows, Store consults DedupeKey + SourceEventID for idempotency). MaxQueryTokens = 4000 mirrors the host validator default. mem0 / Graphiti will light up the SemanticQuery / VectorSearch / NamespaceIsolation / TTL / EventualConsistency cluster when their plugins land in Ν-1.1 / Ν-1.3. Touched: memory.proto + types.go + server.go + client.go + sqlite.go. Regenerated memory.pb.go + memory_grpc.pb.go via `make proto`.
…s + 4-line plugin.Serve)
v0.6 introduces the official mem0 adapter. This commit lays down
the skeleton:
- plugins/memory/mem0/mem0.go: Provider struct, Capabilities
(mem0's correct advertise — SemanticQuery / VectorSearch /
NamespaceIsolation / TTL / EventualConsistency / MetadataFilter
/ BulkExport / BulkImport all true; DedupeKey / SourceEventID
false because the host computes idempotency tokens), Health
(static), and stubbed Store / Query / Forget / Export / Import
that return errNotYetImplemented until Ν-1.1c (namespace
mapping) and Ν-1.1d (HTTP client adapter) wire the real REST
calls.
- plugins/memory/mem0/CONTRACT.md: pins the contract that makes
the plugin swappable.
- Scope translation: repo → user_id, worktree → session_id
(round 4 AI #2).
- Fallback policy: Query Read fallback only, Store fail-fast
(round 4 AI #1 + #2 split-brain Blocker 1 mitigation).
- Cache: 30 s TTL, LRU 512, Query-only, invalidated on Store /
Forget / Import (round 4 AI #1 + #2).
- Capabilities matrix mapping each advertised flag to its
rationale.
- cmd/bough-plugin-memory-mem0/main.go: the four-line plugin.Serve
entry the host spawns when .bough.yaml memory_backends declares
kind: mem0. Reads endpoint / api_key / namespace / timeout from
the BOUGH_MEMORY_MEM0_* env block.
Build clean and golangci-lint 0 issues across the new package and
cmd entry.
…ession_id) Round 4 AI #2's mem0-layered namespace lands here. bough's three- tier Scope translates into mem0's namespace pair: global → user_id = global/<user@host> repo → user_id = repo/<repo_hash>/<root_hash> worktree → user_id = repo/<repo_hash>/<root_hash> session_id = worktree/<worktree_id> The repo / worktree split puts the long-lived identity on user_id and the short-lived per-branch identity on session_id, matching mem0's memory-types layering (user / session / agent / run). Two worktrees of the same repo share the user namespace but stay queryable per branch through session_id. v0.6 limitation: the wire Scope carries only RepoName + WorktreeID, so both hashes derive from RepoName (and WorktreeID for the worktree root hash). v0.6.x will extend memapi.Scope with optional Remote Fingerprint + RootFingerprint so the hashes match docs/NAMESPACE_ MAPPING.md exactly (sha256(remote URL) + sha256(absolute path)). The current derivation still cleanly isolates two repos with different names. BOUGH_MEMORY_MEM0_NAMESPACE multi-tenant prefix is honored for every scope level. namespace_test.go covers global / repo / worktree user_id shape, shared repo prefix across repo + worktree of the same RepoName, disjoint user_ids for two different repos, multi-tenant prefix application, machineID stability, and hashShort idempotency. 8 tests pass; lint 0 issues.
…Export / Import + helpers) Round 4 AI #1 + #2 split-brain Blocker 1 lands as policy in this commit. The mem0 plugin's six stub methods now route through the real mem0 v1 REST API while keeping the host's fallback decision read-only: - doJSON (http.go) is the single HTTP entry every per-RPC method uses. No transparent retry — round 4 AI #1 flagged retry on writes as risky without native idempotency. The host computes DedupeKey + SourceEventID, but mem0 does not honor them, so the plugin advertises false for both and Store fails fast. - Store packs every Instinct field into mem0 metadata so an Export -> Import cycle round-trips even though mem0's primary surface is just a memory string. The mem0 event tag (ADD / UPDATE / NONE) drives the WasUpsert response. - Query applies MinConfidence and MaxResults host-side so the budget aggregator never sees rows below the cap. - Forget url.PathEscapes the memory id so user-supplied ids do not break the path. - Export emits the same YAML / JSONL shape as the sqlite reference-fallback so Import is backend-agnostic. - Import reuses an inlined copy of sqlite's parser; v0.6.x will lift the parsers into pkg/memorywire/ once a second plugin needs them. - Health stays static — the host's first real RPC after Health surfaces a misconfigured endpoint, so we don't duplicate the probe. http_test.go (httptest.Server, 9 cases) covers the wire shape: authentication headers, content-type, error decoding with upstream body, Store metadata round-trip, mem0 event semantics, Query min- confidence filter, Forget url-escape, and an end-to-end Export -> Import YAML round trip. Full conformance e2e arrives in Ν-1.2. 17 tests pass (8 namespace + 9 http); lint 0 issues.
…U 512 + scope invalidate) Round 4 AI #1 + #2 both flagged a cache as essential for absorbing duplicate Query traffic across one bough invocation, with two guard rails: it must be Query-only (Store / Forget / Import never read or write entries) and Store / Forget / Import must invalidate every cached entry for the touched scope so a follow-up Query never returns a row mem0 no longer holds. cache.go implements both rules: - queryCache holds entries keyed by (scope, term, max_results, max_tokens, min_confidence) so distinct queries never alias. - TTL is 30 s — short enough that mem0 updates from another bough process land within one query round, long enough to amortise duplicate calls inside a single CLI invocation. - LRU caps at 512 entries; oldest evicts when full. - clock is injectable so TTL behaviour can be unit-tested. - invalidateScope drops every key targeting the given Scope. mem0.go hooks the cache without breaking the existing tests: - Provider gains a cache field; New() initialises it. - Query consults cache.get first, falls through to mem0 search on miss, cache.put on the way out. - Store and Forget call cache.invalidateScope after a successful upstream call. Import re-uses Store so invalidation propagates automatically. cache_test.go (6 cases) covers get/put hit+miss, TTL expiry via the injectable clock, LRU eviction at cap+1, invalidateScope dropping target entries while siblings survive, plus an end-to-end TestProviderQuery_CacheHit (server hit once across two identical Query calls) and TestProviderStore_InvalidatesCache (Store flips the next Query back to the live wire path). 23 mem0 tests pass (8 namespace + 9 http + 6 cache); lint 0 issues.
…brain mitigation) v0.5.1 plumbed an instinct.fallback_on_error flag into the coordinator's Query path but always passed nil for the fallback backend (round 4 AI #1 + #2 Blocker 1: split-brain). v0.6's mem0 plugin gives us a real "non-SQLite primary" to test against, so this commit lights up the second-backend slot. internal/cli/instinct_memory_helpers.go: - discoverFallbackSQLite spawns a separate bough-plugin-memory- sqlite subprocess regardless of cfg.Instinct.DefaultMemoryBackend. Reads the SQLite path from cfg.MemoryBackends[kind=sqlite].path so an operator who wants a dedicated fallback file can point it at .bough/memory/fallback.db while the primary mem0 keeps writing to mem0. - loadInstinctCoordinator now spawns the fallback when fallback_on_error is true AND the primary kind != "sqlite". A SQLite primary remains its own fallback (= no second process, no shared DB file). The close func disposes both subprocesses so the host's lifecycle code stays one-call clean. The coordinator's Query path (v0.5.1) does the actual fallback; nothing here changes the policy. AI #1 + #2 mandated that Store / Forget / Import never fall back to SQLite (that would split-brain the dataset), and mem0's plugin advertises DedupeKey / SourceEventID = false so the coordinator never retries the write — Store fails loud, as the CONTRACT.md prescribes. Build clean across the repo, all unit tests pass, lint 0 issues.
Round 4 priority A: end-to-end conformance for the bough mem0 plugin without depending on the real mem0 cloud. Two new files: - conformance/memory/mock_mem0/main.go: a single binary that starts an in-process HTTP mock of mem0's v1 REST API on a random local port, wires the bough mem0 plugin to that endpoint, and serves the plugin as a bough MemoryBackend over gRPC. The mock honours metadata.bough_id so the conformance suite's Forget(inst.ID) lookup works (real mem0 generates its own UUIDs but the mock obeys the host) and implements soft-delete via metadata.bough_state="forgotten" so the Forget -> Export -> Import round trip in lifecycle.go succeeds — the bough mem0 plugin's Capabilities advertise SoftDelete=true, and the mock now backs that contract. - conformance/memory/mem0_test.go: TestMem0_Conformance compiles mock_mem0 to a temp binary and runs the full MemoryBackend conformance suite against it. Mirrors the existing TestSelf entry that drives the in-tree mock_plugin. All three sub-tests pass (Lifecycle / Bloat / Concurrency); lint 0 issues. Real mem0 cloud is never reached, so the suite is hermetic and CI-friendly.
…deferral) Round 4 AI #2 deferred the Graphiti binary to v0.6.x: the sidecar lifecycle (graph DB + Graphiti server + LLM extraction) does not fit cleanly into v0.6.0 alongside mem0 + CapabilityCompiler + MCP server + signing. v0.6.0 ships the skeleton instead: - examples/memory-plugin-graphiti-skeleton/README.md: adapter guide covering the v0.6.x shape (Provider struct + Capabilities cluster Graphiti will advertise + namespace mapping using Graphiti's group_id concept). - examples/memory-plugin-graphiti-skeleton/docker-compose.yml: Neo4j 5.13 + getzep/graphiti:latest with healthchecks so developers can stand up a local Graphiti stack with one `docker compose up -d` (requires OPENAI_API_KEY for Graphiti's entity extraction). docs/EXTERNAL_MEMORY_BACKENDS.md gets a status matrix bumped to v0.5.x / v0.6.0 / v0.6.x columns, the mem0 section now documents the cache + namespace mapping + split-brain fallback policy that shipped in Ν-1.1, and the Graphiti section explains why the binary lives in v0.6.x as a separate GoReleaser archive. No Go code changes; docs + examples only.
… registry + compiler + emitter interface) Round 4 priority A3 (= 7 group metadata + Checksum) + A13 (= Emitter interface lifted into plugins/capability/api/) land in this commit. v0.6 keeps the gRPC contract at the v0.5 wire (= 17 fields + Payload escape hatch) so plugin authors building against v0.5 stay compatible; the new groups round-trip through Payload until v0.7 graduates them to proto fields alongside the MemoryBackend v2 bump. pkg/schema/capability.go: - CapabilityArtifact gains Target / Invocation / Contract / Validation / Provenance struct groups + Checksum string. - Target.Format keys the emitter registry; Target.Host + MCPKind tune the output for the agent runtime. - Provenance carries source_ref / tree_sha / generated_by per round 3 priority B + round 4 supply-chain note. - CanonicalBytes + ComputeChecksum produce a deterministic sha256 hash of every field except Stability / CreatedAt / Checksum so the CLI can short-circuit Compile when the artifact on disk already matches (= AI #1 idempotency). plugins/capability/api/emitter.go (new): - Emitter interface (Format / Emit) lifted into the public api package so v0.6.x can graduate emitters into gRPC plugin slots without rewriting the registry. internal/capability/registry.go (new): - Format → Emitter map with thread-safe Register / Lookup / Formats. internal/capability/compiler.go (new): - NewCompiler / Compile orchestration mirrors internal/instinct/ coordinator.go shape: walk instincts × kinds × targets, stamp Checksum, dispatch through the registry, gather Artifacts + Emissions. DryRun=true materialises artifacts but skips emit. internal/capability/compiler_test.go (new, 5 cases): - Registry register / lookup / formats / unknown error - Compile happy path (= 1 instinct × 1 kind × 2 targets fires twice and stamps checksums) - Compile DryRun=true skips emit - Compile unknown target surfaces stable error - Compile emit failure propagates with target + artifact context - CanonicalBytes deterministic across calls All tests pass; lint 0 issues.
… claude-skill + mcp) Round 4 priority A2 + A6 land here: agent-skill is the default target (= bough is host-neutral OSS, gh skill is the cross-runtime distribution surface), claude-skill produces SKILL.md when the operator opts into a Claude-specific layout, and the mcp emitter ships read-only first per round 4 AI #2's MCP scope discipline. internal/export/agent_skill.go: - AgentSkillEmitter renders gh skill style markdown with full frontmatter (name / description / version / kind / host / source_ref / tree_sha / generated_by / checksum / state_changing). Steps / Constraints / Evidence / Prerequisites sections follow the gh skill body convention. internal/export/claude_skill.go: - ClaudeSkillEmitter renders an Anthropic Agent Skills SKILL.md with minimal frontmatter (the matcher only reads name + description). Provenance + checksum still ride along so the same supply-chain story applies. internal/export/mcp.go: - MCPEmitter dispatches on Target.MCPKind: tool / resource / prompt. tools default state_changing=false (read-only first); the CLI flips to true only when --allow-write is passed. Spec version pinned to 2025-11-25 in every payload as the protocol-version advertise that round 4 AI #2 mandated. internal/export/registry.go: - DefaultRegistry returns a populated *capability.Registry holding the three builtin emitters. CLI bootstrap (Ν-1.6) calls this. internal/export/emitter_test.go (6 cases): frontmatter shape, filename suffix, MCP kind dispatch (tool / resource / prompt), provenance round-trip, $schema pin, and DefaultRegistry composition. All tests pass; lint 0 issues.
… install/lint stubs)
Round 4 priority A2: default target is agent-skill (host-neutral
gh skill style); --profile claude-code switches into the Anthropic
SKILL.md layout, --to claude-skill / mcp pick a different format
outright.
Subcommands:
- compile : harvest instincts via the coordinator, build a
CompileRequest, dispatch through the v0.6 emitter
registry, write to --out-dir or print to stdout.
- list : enumerate the registered emitter formats.
- preview : DryRun=true; print artifacts as JSON without emit.
- install : stub, lands in v0.6.x.
- lint : stub, lands in v0.6.x (validation.probes recorded
but not executed by v0.6.0).
Shared compileOpts struct + bindCompileFlags helper keeps the
compile / preview wiring DRY so v0.6.x can extend the flag set
in one place. harvestInstincts loads every active row in scope by
default; --instinct-id <id> filters to the explicit set and
surfaces a stable error when an id is missing. writeEmissions
either mkdirs --out-dir + writes per-emission files or prints
bytes to stdout when --out-dir is empty.
NewRootCmd gains newCapabilityCmd() in cli.go so `bough
capability ...` is registered at startup.
Build clean, internal/cli tests pass, lint 0 issues.
…0, MCP 2025-11-25) Round 4 AI #2 + AI #1: v0.6.0 ships an MCP server companion binary that exposes bough's memory subsystem to MCP clients (Claude Desktop, Cursor, etc.) as a read-only surface. State-changing methods refuse with codeWriteForbidden until v0.6.x adds the --allow-write CLI flag. cmd/bough-mcp-server/main.go (~100 lines): - discoverSQLite spawns bough-plugin-memory-sqlite as the v0.6 memory source (mem0 / Graphiti backends land with --backend in v0.6.x). - WatchStdin starts the round 4 AI #1 zombie guard: stdin EOF fires Shutdown which kills the SQLite subprocess, releasing the DB file lock before Claude Desktop can spawn the next instance. - Run loops on stdio JSON-RPC; errors trigger Shutdown so we never leave the backend subprocess running. internal/mcp/types.go (~140 lines): - jsonrpcRequest / jsonrpcResponse / jsonrpcError envelopes. - Error code constants including codeWriteForbidden (-32001) for the read-only refusal path. - InitializeResult + ServerCapabilities + BoughMCPCapabilities carries the round 4 priority A7 vendor block so clients can programmatically probe spec_version / read_only / state_changing_tools. - ToolDefinition / ToolCallResult / ResourceDescriptor / ResourcesReadResult: the v0.6 wire types. internal/mcp/server.go (~270 lines): - Server holds the MemoryBackend client + close func + the shut atomic.Int32 that drives Graceful Shutdown. - Run dispatches newline-framed JSON-RPC; dispatchLine routes initialize / tools/list / tools/call / resources/list / resources/read / prompts/list / shutdown. - handleInitialize advertises MCPSpecVersion = "2025-11-25" plus the BoughMCPServer block. - handleToolsList ships only memory.query on v0.6. - handleToolsCall refuses memory.store / memory.forget / memory.promote with codeWriteForbidden + a v0.6.x message; memory.query routes to MemoryBackend.Query and renders rows as text content. - handleResourcesList / handleResourcesRead expose bough://memory/scopes only. - Shutdown is idempotent; post-shutdown requests return an internal error so the host drain cleanly. internal/mcp/server_test.go (6 cases): initialize advertise, tools/list shape, write-tool refusal with codeWriteForbidden, memory.query round trip, resources/list shape, Shutdown idempotency + post-shutdown refusal. Build clean, all tests pass, lint 0 issues.
…I + AcceptedSignatureSchemes field) Round 4 priority A9 + A10 + A11 land in this commit as the v0.6 opt-in path: bough now ships a verify CLI + the config surface the v0.6.x enforcement flip will read from. internal/config/config.go: - InstinctPluginSecurity gains AcceptedSignatureSchemes []string, the two-system signing surface. Defaults parse as ["cosign", "minisign"] so plugin authors can pick either flow; an enterprise operator can narrow the slice to enforce a single supply-chain story. - Doc comment updated to reflect Ν-1.8 starting to consume RequireSigned (= v0.5 was warn-only). internal/pluginsign/verifier.go (new): - Verify dispatches on req.Scheme (cosign | minisign) and shells out to the canonical CLI tools (`cosign verify-blob --bundle`, `minisign -V -m -x -p`). We never reimplement the cryptography — we inherit sigstore's / minisign's track record. - Result.ToolMissing=true signals the verifier binary was not on PATH; v0.6.0 keeps this fail-open (operator who set require_signed without installing the tools sees a clear message rather than a hard refusal). v0.6.x adds strict mode. - Defaults: SigPath = <binary>.bundle for cosign (GoReleaser keyless output), <binary>.minisig for minisign. internal/cli/plugin_verify.go (new): - bough plugins verify <binary> [--scheme cosign|minisign] [--signature <path>] [--pubkey <path>] (round 4 priority A10). - Wires pluginsign.Verify and translates the result into a single ✓-prefixed success line, a NOTICE for tool-missing, or a non- zero exit for verify failures. internal/cli/plugins_list.go: AddCommand the verify subcommand. Build clean, internal/cli + internal/config tests pass, lint 0 issues. Actual signing of the bough binaries themselves wires into GoReleaser in Ν-1.11.
Two new conformance packages give downstream callers a hermetic entry into the v0.6 capability + MCP surfaces. Both follow the existing conformance/memory pattern: a Config + Run(t, cfg) helper that plugin authors invoke from their own _test.go. conformance/capability: - Run drives Dispatch / DryRun / Checksum sub-tests against the caller-supplied emitter slice. The default `recordingEmitter` lets the suite verify "one instinct × one kind × N targets" produces N artifacts + N emissions, that DryRun=true skips emit, and that ComputeChecksum is deterministic across regenerations. - TestSelf wires the three v0.6 builtin emitters (agent-skill, claude-skill, mcp) from export.DefaultRegistry so the in-tree emitter set is exercised by `go test ./...`. conformance/mcp: - Run spins up the in-process bough-mcp-server over an io.Pipe pair, then walks the wire surface as a real client would: initialize advertises read_only=true + state_changing_tools= false + spec_version="2025-11-25"; tools/list ships exactly one tool (memory.query); state-changing tool names refuse with codeWriteForbidden (-32001); resources/list exposes bough://memory/scopes; shutdown is idempotent and post-shutdown dispatches refuse cleanly. - TestSelf uses the default fakeBackend so `go test ./...` exercises every method without spawning a binary or touching a real backend. All conformance packages pass; lint 0 issues. The round 4 AI #1 zombie sim (= stdin EOF triggers Shutdown which kills the backend subprocess) lives in cmd/bough-mcp-server/main.go and the server unit tests; the conformance suite stays focused on protocol correctness.
…ROADMAP + CHANGELOG + README Three new docs cover the v0.6 surfaces that landed in Ν-1.4 → Ν-1.8; ROADMAP + CHANGELOG bump to reflect what shipped, and the README gets a "Capability compiler + MCP (v0.6+)" section so the entry point story is up-front. docs/CAPABILITY_COMPILER.md: - Schema description (12 + 7 group + Checksum) with field-by-field rationale. - Dispatch loop diagram (instincts × kinds × targets). - Target matrix (agent-skill default, claude-skill via --profile, mcp via Target.MCPKind). - CLI examples and the extension story for v0.6.x plugin authors. docs/MCP_SERVER.md: - Read-only first contract (codeWriteForbidden for state-changing tools). - BoughMCPServer capability block (spec_version, read_only, state_changing_tools). - Zombie guard story (stdin EOF → Shutdown → SQLite subprocess killed). - Claude Desktop wiring snippet. docs/SIGNING.md: - cosign + minisign scheme matrix. - Configuration block + bough plugins verify CLI examples. - Fail-open behaviour today, strict mode for v0.6.x. - v0.6.0 → v0.6.x → v0.7 → v0.8+ timeline. docs/ROADMAP.md: v0.6.0 row ✅ with the priority A items each checkmark traces back to. CHANGELOG.md: v0.6.0 entry summarising every Ν-1.x phase with round 4 attribution. README.md: new "Capability compiler + MCP (v0.6+)" section after the v0.5 Instincts section so the entry story stays linear.
…cosign keyless signing) Two new binaries (round 4 priority A1 + A6) and the Sigstore cosign keyless signing flow (round 4 priority A9) land in the release pipeline. builds: - bough-plugin-memory-mem0: ./cmd/bough-plugin-memory-mem0, pure-Go (CGO_ENABLED=0) so it cross-compiles to the same 4 OS-arch matrix every other bough plugin uses. - bough-mcp-server: ./cmd/bough-mcp-server, same matrix. Ships in the same release archive as the SQLite reference-fallback it spawns at runtime. archives.ids gains both binaries so the default tarball (bough_<ver>_<os>_<arch>.tar.gz) now contains 8 binaries (host + 5 plugins + 1 mcp server + 1 mem0 adapter). signs (round 4 priority A9, A11): - cosign-keyless signs every archive with Sigstore via the GitHub Actions OIDC flow (no pre-provisioned private key needed). COSIGN_EXPERIMENTAL=1 enables the keyless path; the workflow must be run with id-token: write. - Signatures land alongside each archive as <archive>.sig + <archive>.pem so `bough plugins verify` can validate them. Snapshot builds skip signing because local `goreleaser release --snapshot` does not have OIDC context. Graphiti is intentionally NOT in this matrix — round 4 AI #2 deferred the Graphiti binary to v0.6.x as a separate GoReleaser archive so the v0.6.0 install footprint stays bounded. All cmd packages build clean; the actual signed release runs in Ν-1.12 once the v0.6.0 tag is pushed.
…em0 namespace Round 4 review #23 surfaced six findings that block v0.6.0 ship. This commit clears all six in-place; remaining medium / low items land in follow-up commits. #1 internal/cli/capability.go: runCompile passed `currentScope(nil, ...)`, but currentScope dereferences cfg.Repositories on the default path → nil pointer panic on every `bough capability compile/preview`. Use an explicit Scope{Level: worktree, WorktreeID: "default"} literal until --scope flags land in v0.6.x. #2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go: WatchStdin and Server.Run both Read from os.Stdin concurrently, so the watchdog stole bytes the JSON-RPC scanner needed. The ZOMBIE guard also only triggered on io.EOF, so wrapped errors left the goroutine spinning. Both go away by deleting WatchStdin entirely and letting Server.Run's bufio.Scanner detect EOF — main.go's `defer server.Shutdown()` handles the subprocess teardown. #4 + #5 internal/pluginsign/verifier.go + internal/cli/plugin_verify.go: verifyCosign defaulted SigPath to ".bundle" but .goreleaser.yaml writes ".sig", and the command was missing the --certificate-identity / --certificate-oidc-issuer flags cosign 2.x requires for keyless verify. Default SigPath now prefers .sig (with .bundle fallback for legacy artifacts), CertPath defaults to the GoReleaser .pem companion, and the Request grows CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the CLI exposes as --cert-identity / --cert-identity-regexp / --cert-issuer. PreRunE now validates --pubkey is set for minisign and --cert-identity (or regexp) is set for cosign BEFORE shelling out so the operator sees an Args error rather than a phantom verify failure. #6 internal/mcp/server.go: handleResourcesRead built Scope{Level: level} with no RepoName/WorktreeID, then asked the backend for a count. sqlite's scopeID returns "/" for that shape, so the filter never matched a real worktree row and the resource reported `worktree: 0` even when worktree rows existed. The honest fix is to stop returning a faithful count the host cannot guarantee: readScopesResource now lists the three scope tiers with descriptions and points clients at memory.query for counts (v0.6.x will add an explicit count API once Server learns about cfg.Repositories). #7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0 worktree case derived rootHash from `RepoName + "/" + WorktreeID`, so two worktrees of the same repo got different user_ids and the documented user-tier sharing did not hold. Use hashShort(RepoName) for both segments; branch identity continues to live entirely in session_id. Build clean, all unit + conformance tests pass, lint 0 issues.
… propagation / cache gen / cache namespace / Export pagination #10 instinctToMetadata + writeInstinct skip zero-value time.Time so a freshly constructed Instinct does not serialise a -6795364578871 epoch into mem0 metadata or the JSONL export (which would round-trip back as a garbage "year 0001" timestamp). #11 The YAML / JSONL Export now carries dedupe_key + source_event_id alongside each row, and Import's parser returns an importRow struct that preserves both tokens. The host's Store-by-dedupe contract therefore survives sqlite Export → mem0 Import round trips end-to-end. #12 queryCache gains a monotonic invalidation generation. Query captures gen before the HTTP roundtrip and hands it to put; put silently drops the write when invalidateScope has bumped gen in the interim. A concurrent Store / Forget / Import can no longer race a slow Query and pin a stale response in cache for cacheTTL. #13 cacheKey gains a namespace field, populated via the new Provider.cacheKeyFor method, so two tenants sharing one Provider with different namespaces never share cache entries even when Scope shape matches. #15 Export now paginates: it loops GET /api/v1/memories/?page=N until a less-than-page-size batch comes back. Long scopes (>100 instincts) no longer silently truncate at the first page. Build clean, plugins/memory/mem0 unit tests pass.
…guard + signing docs honest Three review #23 follow-ups (#9, #14, #16) collected into one commit because they share the same theme: undo the v0.6 plan's overreach in the host-side CLI surface and the docs that backed it. #16 — `bough instinct export` was a Μ-1.12 stub that returned "not yet wired". The CLI advertised the command in `--help` though, so an operator who tried it hit a dead end with no fix-it pointer. Wire it through Coordinator.Export (new method, mirrors the Import flow: delegates to backend.Export, emits an audit event keyed `export`). Same scope-inference shape as newInstinctPromoteCmd so the two commands see the same default scope. #14 — internal/cli/capability.writeEmissions was joining emitter-supplied filenames straight into outDir. v0.6.0 ships three first-party emitters that all produce safe filenames, but the emitter registry is plugin-extensible from v0.6.x, and a buggy or malicious emitter returning `../../etc/foo` would have landed bytes outside the operator's --out-dir. Add the standard archive/tar-style traversal guard: filepath.Clean → absolute-or-".." reject → filepath.Rel(outDir) verify the canonical path stays inside. #9 — internal/config/config.go and docs/SIGNING.md both claimed v0.6 consumes `require_signed: true` as a spawn-time refuse-to-spawn gate. The Ν-1.8 implementation actually shipped the `bough plugin verify` CLI only; the spawn-time gate lands in v0.6.x per the four-stage timeline already documented in docs/SIGNING.md "Timeline" table. Revert the config docstring and the SIGNING.md "Configuration" para so the contract bough advertises matches what it enforces. All three change unit + conformance tests still pass: go test ./conformance/... → 6 packages, all ok go test ./internal/... → all ok go test -tags=conformance ./plugins/memory/sqlite/... → ok
… caps gating + mock hard-delete Review #23 #8 surfaced a contract divergence: the mem0 plugin advertised Capabilities.SoftDelete=true while real mem0 cloud's DELETE /api/v1/memories/<id>/ hard-deletes the row. The in-tree conformance suite shipped CI-green because the mock_mem0 server faked soft-delete in the wire layer (metadata.bough_state="forgotten" gated by mockStore.search), masking the production divergence. Make the mem0 plugin honest, then make the conformance suite caps- aware so a backend that says SoftDelete=false still passes the suite without faking it. plugins/memory/mem0/mem0.go Capabilities.SoftDelete: true → false, with an updated docstring explaining the hard-delete semantics and pointing at the conformance gate. plugins/memory/mem0/CONTRACT.md `soft_delete` row: true → false, with the reason ("mem0 cloud hard-deletes on DELETE…; the conformance suite gates its Export- after-Forget assertion on this flag"). conformance/memory/lifecycle.go - Store#2 WasUpsert assertion gated on caps.DedupeKey (mem0 honestly advertises DedupeKey=false: no native dedupe primitive). - Reorder so Export → Import round-trip runs BEFORE Forget, so every backend exercises the round-trip regardless of soft-delete semantics. - Forget runs on every backend; the post-Forget Query asserts the row stops being queryable (common contract). - The post-Forget Export-still-returns-the-row assertion is gated on caps.SoftDelete so honest hard-deleting backends pass. conformance/memory/mock_mem0/main.go - softDelete → hardDelete: delete(s.rows, id) directly, mirroring real mem0 cloud's DELETE semantic. - search/list bough_state filter removed: nothing soft-deletes through this path any more. - Header comment expanded to record the divergence #8 surfaced and why the mock now matches production. Verified locally: go test ./conformance/memory/... → TestSelf + TestMem0_Conformance pass go test -tags=conformance \ BOUGH_CONFORMANCE_MEMORY_PLUGIN_BIN=dist/bough-plugin-memory-sqlite \ ./plugins/memory/sqlite/... → pass (caps.SoftDelete=true path)
…view #23 #2/#3) The existing conformance/mcp suite wires Server.Run through io.Pipe, which CI-greened the pre-fix WatchStdin design where a second goroutine on os.Stdin would have raced the bufio.Scanner inside Run. Review #23 #2/#3 surfaced that gap; the WatchStdin function itself is gone, but the in-process suite still cannot regress-test the production stdio path. This test spawns the real bough-mcp-server binary (and its bough-plugin-memory-sqlite dependency, built into a shared temp dir so the server's discoverSQLite finds it via PATH) and drives the MCP initialize handshake through actual stdin/stdout pipes: 1. Build both binaries into t.TempDir(). 2. Start cmd/bough-mcp-server with PATH=tempdir + sqlite db path. 3. Write one JSON-RPC initialize frame; read exactly one response. A WatchStdin-style second reader would steal the bytes and ReadBytes would time out. 4. Close stdin → Server.Run's bufio.Scanner returns false → main returns → defer server.Shutdown() reaps the SQLite plugin. If the SQLite subprocess outlives the server, cmd.Wait blocks past 10s and the test fires. The build cost (~10s for two binaries) is paid once per test run. The test runs without tags so the CI memory job already covers it; operators running `go test ./...` locally get coverage without configuring anything extra. Verified locally: go test -run TestSubprocessLifecycle ./conformance/mcp/... PASS: TestSubprocessLifecycle (1.65s)
golangci-lint errcheck flagged conformance/mcp/subprocess_test.go
line 153: os.RemoveAll(dir) on the build-failure cleanup path
discarded the error. The discard is intentional (= the temp dir
goes away with the test process), so prefix with `_ =` to satisfy
the linter without changing behaviour.
Verified locally:
nix develop .#ci -c golangci-lint run ./conformance/mcp/...
0 issues.
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
…em0 namespace Round 4 review #23 surfaced six findings that block v0.6.0 ship. This commit clears all six in-place; remaining medium / low items land in follow-up commits. #1 internal/cli/capability.go: runCompile passed `currentScope(nil, ...)`, but currentScope dereferences cfg.Repositories on the default path → nil pointer panic on every `bough capability compile/preview`. Use an explicit Scope{Level: worktree, WorktreeID: "default"} literal until --scope flags land in v0.6.x. #2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go: WatchStdin and Server.Run both Read from os.Stdin concurrently, so the watchdog stole bytes the JSON-RPC scanner needed. The ZOMBIE guard also only triggered on io.EOF, so wrapped errors left the goroutine spinning. Both go away by deleting WatchStdin entirely and letting Server.Run's bufio.Scanner detect EOF — main.go's `defer server.Shutdown()` handles the subprocess teardown. #4 + #5 internal/pluginsign/verifier.go + internal/cli/plugin_verify.go: verifyCosign defaulted SigPath to ".bundle" but .goreleaser.yaml writes ".sig", and the command was missing the --certificate-identity / --certificate-oidc-issuer flags cosign 2.x requires for keyless verify. Default SigPath now prefers .sig (with .bundle fallback for legacy artifacts), CertPath defaults to the GoReleaser .pem companion, and the Request grows CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the CLI exposes as --cert-identity / --cert-identity-regexp / --cert-issuer. PreRunE now validates --pubkey is set for minisign and --cert-identity (or regexp) is set for cosign BEFORE shelling out so the operator sees an Args error rather than a phantom verify failure. #6 internal/mcp/server.go: handleResourcesRead built Scope{Level: level} with no RepoName/WorktreeID, then asked the backend for a count. sqlite's scopeID returns "/" for that shape, so the filter never matched a real worktree row and the resource reported `worktree: 0` even when worktree rows existed. The honest fix is to stop returning a faithful count the host cannot guarantee: readScopesResource now lists the three scope tiers with descriptions and points clients at memory.query for counts (v0.6.x will add an explicit count API once Server learns about cfg.Repositories). #7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0 worktree case derived rootHash from `RepoName + "/" + WorktreeID`, so two worktrees of the same repo got different user_ids and the documented user-tier sharing did not hold. Use hashShort(RepoName) for both segments; branch identity continues to live entirely in session_id. Build clean, all unit + conformance tests pass, lint 0 issues.
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
… propagation / cache gen / cache namespace / Export pagination #10 instinctToMetadata + writeInstinct skip zero-value time.Time so a freshly constructed Instinct does not serialise a -6795364578871 epoch into mem0 metadata or the JSONL export (which would round-trip back as a garbage "year 0001" timestamp). #11 The YAML / JSONL Export now carries dedupe_key + source_event_id alongside each row, and Import's parser returns an importRow struct that preserves both tokens. The host's Store-by-dedupe contract therefore survives sqlite Export → mem0 Import round trips end-to-end. #12 queryCache gains a monotonic invalidation generation. Query captures gen before the HTTP roundtrip and hands it to put; put silently drops the write when invalidateScope has bumped gen in the interim. A concurrent Store / Forget / Import can no longer race a slow Query and pin a stale response in cache for cacheTTL. #13 cacheKey gains a namespace field, populated via the new Provider.cacheKeyFor method, so two tenants sharing one Provider with different namespaces never share cache entries even when Scope shape matches. #15 Export now paginates: it loops GET /api/v1/memories/?page=N until a less-than-page-size batch comes back. Long scopes (>100 instincts) no longer silently truncate at the first page. Build clean, plugins/memory/mem0 unit tests pass.
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
…guard + signing docs honest Three review #23 follow-ups (#9, #14, #16) collected into one commit because they share the same theme: undo the v0.6 plan's overreach in the host-side CLI surface and the docs that backed it. #16 — `bough instinct export` was a Μ-1.12 stub that returned "not yet wired". The CLI advertised the command in `--help` though, so an operator who tried it hit a dead end with no fix-it pointer. Wire it through Coordinator.Export (new method, mirrors the Import flow: delegates to backend.Export, emits an audit event keyed `export`). Same scope-inference shape as newInstinctPromoteCmd so the two commands see the same default scope. #14 — internal/cli/capability.writeEmissions was joining emitter-supplied filenames straight into outDir. v0.6.0 ships three first-party emitters that all produce safe filenames, but the emitter registry is plugin-extensible from v0.6.x, and a buggy or malicious emitter returning `../../etc/foo` would have landed bytes outside the operator's --out-dir. Add the standard archive/tar-style traversal guard: filepath.Clean → absolute-or-".." reject → filepath.Rel(outDir) verify the canonical path stays inside. #9 — internal/config/config.go and docs/SIGNING.md both claimed v0.6 consumes `require_signed: true` as a spawn-time refuse-to-spawn gate. The Ν-1.8 implementation actually shipped the `bough plugin verify` CLI only; the spawn-time gate lands in v0.6.x per the four-stage timeline already documented in docs/SIGNING.md "Timeline" table. Revert the config docstring and the SIGNING.md "Configuration" para so the contract bough advertises matches what it enforces. All three change unit + conformance tests still pass: go test ./conformance/... → 6 packages, all ok go test ./internal/... → all ok go test -tags=conformance ./plugins/memory/sqlite/... → ok
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
… caps gating + mock hard-delete Review #23 #8 surfaced a contract divergence: the mem0 plugin advertised Capabilities.SoftDelete=true while real mem0 cloud's DELETE /api/v1/memories/<id>/ hard-deletes the row. The in-tree conformance suite shipped CI-green because the mock_mem0 server faked soft-delete in the wire layer (metadata.bough_state="forgotten" gated by mockStore.search), masking the production divergence. Make the mem0 plugin honest, then make the conformance suite caps- aware so a backend that says SoftDelete=false still passes the suite without faking it. plugins/memory/mem0/mem0.go Capabilities.SoftDelete: true → false, with an updated docstring explaining the hard-delete semantics and pointing at the conformance gate. plugins/memory/mem0/CONTRACT.md `soft_delete` row: true → false, with the reason ("mem0 cloud hard-deletes on DELETE…; the conformance suite gates its Export- after-Forget assertion on this flag"). conformance/memory/lifecycle.go - Store#2 WasUpsert assertion gated on caps.DedupeKey (mem0 honestly advertises DedupeKey=false: no native dedupe primitive). - Reorder so Export → Import round-trip runs BEFORE Forget, so every backend exercises the round-trip regardless of soft-delete semantics. - Forget runs on every backend; the post-Forget Query asserts the row stops being queryable (common contract). - The post-Forget Export-still-returns-the-row assertion is gated on caps.SoftDelete so honest hard-deleting backends pass. conformance/memory/mock_mem0/main.go - softDelete → hardDelete: delete(s.rows, id) directly, mirroring real mem0 cloud's DELETE semantic. - search/list bough_state filter removed: nothing soft-deletes through this path any more. - Header comment expanded to record the divergence #8 surfaced and why the mock now matches production. Verified locally: go test ./conformance/memory/... → TestSelf + TestMem0_Conformance pass go test -tags=conformance \ BOUGH_CONFORMANCE_MEMORY_PLUGIN_BIN=dist/bough-plugin-memory-sqlite \ ./plugins/memory/sqlite/... → pass (caps.SoftDelete=true path)
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
…view #23 #2/#3) The existing conformance/mcp suite wires Server.Run through io.Pipe, which CI-greened the pre-fix WatchStdin design where a second goroutine on os.Stdin would have raced the bufio.Scanner inside Run. Review #23 #2/#3 surfaced that gap; the WatchStdin function itself is gone, but the in-process suite still cannot regress-test the production stdio path. This test spawns the real bough-mcp-server binary (and its bough-plugin-memory-sqlite dependency, built into a shared temp dir so the server's discoverSQLite finds it via PATH) and drives the MCP initialize handshake through actual stdin/stdout pipes: 1. Build both binaries into t.TempDir(). 2. Start cmd/bough-mcp-server with PATH=tempdir + sqlite db path. 3. Write one JSON-RPC initialize frame; read exactly one response. A WatchStdin-style second reader would steal the bytes and ReadBytes would time out. 4. Close stdin → Server.Run's bufio.Scanner returns false → main returns → defer server.Shutdown() reaps the SQLite plugin. If the SQLite subprocess outlives the server, cmd.Wait blocks past 10s and the test fires. The build cost (~10s for two binaries) is paid once per test run. The test runs without tags so the CI memory job already covers it; operators running `go test ./...` locally get coverage without configuring anything extra. Verified locally: go test -run TestSubprocessLifecycle ./conformance/mcp/... PASS: TestSubprocessLifecycle (1.65s)
3 tasks
ikeikeikeike
commented
Jul 16, 2026
| cmd := &cobra.Command{ | ||
| Use: "plugins", | ||
| Short: "List engine plugins discoverable on PATH", | ||
| Short: "List and verify bough plugin binaries", |
Member
Author
There was a problem hiding this comment.
Retrospective review: this commit changed newPluginsCmd's Short text to "List and verify bough plugin binaries" and wired newPluginsVerifyCmd() alongside newPluginsListCmd(). The verify subcommand (internal/cli/plugin_verify.go) and its AddCommand wiring were deleted wholesale by the later v0.9.0 reset (commit eee8a3d), but this Short string was left behind — so bough plugins --help kept advertising a "verify" capability that no longer exists, contradicting docs/SIGNING.md's (correct) "There is no bough plugins verify subcommand today". Fixed in PR #108, which restores the Short text to match the single wired list subcommand and adds a regression test asserting the two can't drift apart again.
ikeikeikeike
added a commit
that referenced
this pull request
Jul 16, 2026
fix: retrospective review fixes for merged PR #23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Round 4 external review (June 2026) scoped v0.6.0 to mem0 first-class + capability compile + read-only MCP + signing scaffolding。 Graphiti は v0.6.x で別 archive (round 4 AI #2 deferral)。
bough-plugin-memory-mem0) = HTTP REST + 30s TTL cache + namespace mapping + Read-only fallback to SQLite (= split-brain 防止、 round 4 AI Initial implementation #1 + feat(plugins/db/postgres): PostgreSQL 16 provider #2)bough capability compileCLI (= compile / list / preview / install + lint stub)bough-mcp-servercompanion binary (= stdio JSON-RPC、 MCP 2025-11-25 pin、 stdin EOF zombie guard)bough plugins verifyCLI + GoReleaser cosign keyless (= round 4 priority A9-A11)Test plan
go test ./...全 green (= unit + conformance memory/capability/mcp 全部)golangci-lint0 issuesRefs: v0.5.1 ship 済 = #22、 round 4 synthesis = `~/.claude/notes/bough-v06-syntheses-round1.md`