Skip to content

feat(plugins/db/postgres): PostgreSQL 16 provider#2

Merged
ikeikeikeike merged 1 commit into
mainfrom
feat/plugin-postgres
Jun 14, 2026
Merged

feat(plugins/db/postgres): PostgreSQL 16 provider#2
ikeikeikeike merged 1 commit into
mainfrom
feat/plugin-postgres

Conversation

@ikeikeikeike

Copy link
Copy Markdown
Member

Adds bough-plugin-postgres for per-worktree PostgreSQL 16 via services-flake.

Same lifecycle shape as bough-plugin-mysql: Up / ReadyCheck (pg_isready + TCP fallback) / Down / Cleanup / PortRangeDefault (50000-52999) / EnvVars (BOUGH_POSTGRES_HOST/PORT/SOCKET_DIR).

make test + golangci-lint run clean.

…es-flake

Mirror of the bough-plugin-mysql shape, swapped for postgresql_16:
  - Up: nix run --impure '.#postgres' -- up --tui=false, detached via
    Setsid so the WorktreeCreate hook returns before postgres is ready
  - ReadyCheck: pg_isready on PATH preferred, raw TCP DialTimeout
    fallback so the host does not require psql installed
  - Down: graceful nix run … down then lsof + SIGTERM/SIGKILL fallback
    + stray process-compose supervisor cleanup
  - Cleanup: rm -rf <datadir>
  - PortRange default 50000-52999 (out of mysql 42000-44999, prior
    bash-hook 33000-41999)
  - EnvVars: BOUGH_POSTGRES_HOST / _PORT / _SOCKET_DIR

The embedded flake.nix wraps services-flake's services.postgres
module, takes BOUGH_POSTGRES_PORT / _SOCKET_DIR / _DATADIR via
builtins.getEnv, and pins postgresql_16 against silent major bumps
from a nix flake update. socketDir set so the operator can dodge
macOS's 104-char sun_path limit.

cmd/bough-plugin-postgres stays four lines of plugin.Serve. The
real lifecycle lives in plugins/db/postgres.

.goreleaser.yaml gains a third build target + the archive id list
is extended so the v0.1.0+ tarballs ship bough + bough-plugin-mysql
+ bough-plugin-postgres in one archive per (os, arch).

Tests cover PortRangeDefault (default + override), EnvVars including
socket-dir default, deployFlake asset extraction (every fragment
the host integration relies on), Cleanup happy + empty-datadir
guard.
@ikeikeikeike
ikeikeikeike merged commit f1064d1 into main Jun 14, 2026
1 check passed
@ikeikeikeike
ikeikeikeike deleted the feat/plugin-postgres branch June 14, 2026 06:24
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
…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)
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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`.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
… 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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
ikeikeikeike added a commit that referenced this pull request Jun 21, 2026
…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.
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
…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)
ikeikeikeike added a commit that referenced this pull request Jul 3, 2026
fix: retrospective review fixes for merged PRs #1/#2/#3/#4/#5/#6/#7/#8/#14/#16/#17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant