Skip to content

feat(cli,backend): auto-detect nix/docker when backend is omitted#8

Merged
ikeikeikeike merged 1 commit into
mainfrom
feat/L5b-auto-detect-backend
Jun 17, 2026
Merged

feat(cli,backend): auto-detect nix/docker when backend is omitted#8
ikeikeikeike merged 1 commit into
mainfrom
feat/L5b-auto-detect-backend

Conversation

@ikeikeikeike

Copy link
Copy Markdown
Member

Summary

  • Adds internal/backend.Detect() — probes nix (with nix-command + flakes enabled) first, then docker info, else returns ErrNoBackend with a message naming the YAML knob to bypass.
  • Wires it into bough create: runs at most once per invocation, only when at least one databases[].backend is empty. Result is injected via extras["backend"], preserving the plugin contract unchanged.
  • Explicit YAML backend: nix / backend: docker still short-circuits detection — operator intent wins.
  • README roadmap + quick-start YAML comment updated to describe the landed auto-detect.

Test plan

  • nix develop .#ci -c go build ./...
  • nix develop .#ci -c go test ./... (incl. 4 new tests in internal/backend)
  • nix develop .#ci -c golangci-lint run ./internal/backend/... ./internal/cli/... (0 issues in touched scope; pre-existing lint debt in plugins/db/*/docker.go belongs to Λ-5c)
  • Manual: drop backend: from a real .worktree-isolation.yaml, run bough create, observe [bough] backend: auto-detected nix (or docker) on stderr and the plugin Up taking the right code path

ikeikeikeike added a commit that referenced this pull request Jun 17, 2026
CI on main has been red since v0.2.1 because the Λ-5a Docker backend
landed three classes of lint debt that local `make format-changed`
never caught (errcheck excludes ${io.Closer} family but not the moby
SDK's *client.Client.Close, and unused helpers slipped past
`go vet`):

- defer cli.Close() / defer rc.Close() in 4 plugins: wrap each in a
  `_ = ...Close()` thunk so errcheck stops complaining without losing
  the cleanup semantics
- removeIfExists in 4 plugin docker.go files: dead code since the
  Λ-5d hardening replaced it with the upOrReuse / start-fail-cleanup
  pair, but the function definitions stayed behind
- (*Provider).dockerCleanup in 4 plugin docker.go files: dead code
  because each plugin's public Cleanup() goes through the nix code
  path's os.RemoveAll directly; the docker variant was never wired

`os` import auto-pruned by goimports in mysql / postgres / redis
docker.go after dockerCleanup removal (elasticsearch keeps os for
its MkdirAll + chown helper).

Local verification:

  golangci-lint run ./...   0 issues
  go test ./...             all packages pass

Unblocks PR #8 (Λ-5b auto-detect) and PR #9 (Λ-5c dockerutil pkg)
both of which inherited these warnings from main.
… empty

bough has shipped a hybrid backend selector (nix vs docker) since
v0.2-alpha, but the YAML required an explicit `backend:` per engine —
operators upgrading from v0.1.x kept hitting the nix path even on
docker-only hosts because the host would inject an empty string into
Extras and the plugin would silently fall through to its nix code path
and explode at `nix run` time with an unhelpful error.

This change wires a new internal/backend package whose `Detect()` runs
once per `bough create` invocation:

  1. `nix` on PATH AND `nix-command` + `flakes` enabled in active nix
     config (via `nix config show experimental-features`) → "nix"
  2. `docker info` exits 0 → "docker"
  3. neither → error naming the YAML knob the operator can use to
     bypass detection

Detection is cached across the dbCfg loop so a 4-database YAML still
costs at most one probe. Explicit `backend: nix` / `backend: docker`
in YAML short-circuits detection entirely — operator intent always
wins. The contract with the plugin layer (`Extras["backend"] ==
"docker"`) is unchanged, so the four DB plugins need no companion edit.

Roadmap row for v0.2.0 reworded to reflect that auto-detect is now the
default when `backend:` is omitted, and the inline YAML comment in the
quick-start example matches.

Tests cover the contract (returns nix|docker or ErrNoBackend), the
tie-break (nix wins when both are reachable), and the LookPath
fast-path. Host-dependent tests skip cleanly on CI hosts that lack one
or both backends.
@ikeikeikeike
ikeikeikeike force-pushed the feat/L5b-auto-detect-backend branch from 93c168b to 2ad4966 Compare June 17, 2026 01:22
@ikeikeikeike
ikeikeikeike merged commit d3391dc into main Jun 17, 2026
1 check passed
@ikeikeikeike
ikeikeikeike deleted the feat/L5b-auto-detect-backend branch June 17, 2026 01:27
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
… 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 28, 2026
feat(eval): confidence demotion + Sonnet GATE5 judge + obs archive (ECC #4/#8/#9)
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