Skip to content

v0.2.1: hardening (resume idempotency + orphan cleanup + port collision)#7

Merged
ikeikeikeike merged 1 commit into
mainfrom
feat/v0.2-hardening
Jun 16, 2026
Merged

v0.2.1: hardening (resume idempotency + orphan cleanup + port collision)#7
ikeikeikeike merged 1 commit into
mainfrom
feat/v0.2-hardening

Conversation

@ikeikeikeike

Copy link
Copy Markdown
Member

Summary

Three concrete repro-validated fixes surfaced by auba dogfood PDCA:

  1. gitwt AddOrAttach idempotency — skip silently if dst is already
    a registered worktree. Mirrors threecorp
    scripts/worktree-create.sh:46-50. Makes
    claude --worktree F-X --resume <session> safe to re-invoke.

  2. plugin dockerUp idempotency + start-fail cleanupupOrReuse
    returns early when a bough-<engine>-<port> container is already
    running; if ContainerStart errors after ContainerCreate, the
    just-created container is force-removed so a retry is not blocked
    by a stale name.

  3. Port-collision actionable error — macOS Docker Desktop's vpnkit
    hides 0.0.0.0 publishes from net.Listen, so we instead catch the
    daemon's "port is already allocated" string and rewrap with
    docker ps --filter publish=<port> as the next step.

Repro tests (all pass)

  • bough create F-idem × 3: 48 s / 12 s / 13 s (resume = no-op, all
    containers reused, no errors)
  • mysql smoke vs probe-mysql on 43501: actionable error, no orphan
  • mysql smoke after probe stop: 42 s clean cycle

Test plan

  • go build ./... succeeds in nix develop .#ci
  • go test ./... passes
  • 3 repro scenarios above verified manually

Follow-ups (Λ-5b / others)

  • Hybrid backend selector + auto-detect (Λ-5b)
  • Pull upOrReuse / isPortFree / lookupByName into a shared
    pkg/dockerutil/ package (per-plugin duplication noted in 5a)

Three concrete repro-validated fixes surfaced by auba dogfood PDCA:

1. gitwt AddOrAttach: skip silently if `dst` is already a registered
   worktree of `repoPath`. Mirrors threecorp
   scripts/worktree-create.sh:46-50 "skip if worktree already exists".
   Makes `claude --worktree F-X --resume <session>` safe to re-invoke
   — every WorktreeCreate fire after the first becomes a no-op
   instead of failing with "branch already exists" + "worktree already
   checked out" combined error.

2. plugin dockerUp: idempotency via upOrReuse. If a container with
   the deterministic name `bough-<engine>-<port>` is already running,
   Up returns success without recreating. If it exists but is
   stopped, the stale container is force-removed before the fresh
   create. Eliminates the 30s teardown-then-recreate penalty on
   every resume.

3. plugin dockerUp Start failure cleanup. The just-created container
   is force-removed if ContainerStart returns an error, so a retry
   is not blocked by a stale name and the operator does not see a
   confusing "name already in use" downstream. macOS Docker
   Desktop's vpnkit hides 0.0.0.0 publishes from `net.Listen`, so we
   also detect the daemon's "port is already allocated" string and
   rewrap with `docker ps --filter publish=<port>` as the actionable
   next step.

Verified via 3 repro scenarios:

- `bough create F-idem` × 3 in a row:
    1st cold     48 s
    2nd resume   12 s (all sub-repos "attached", all containers reused)
    3rd resume   13 s (same)
  No errors, deterministic ports preserved across all three calls.

- mysql smoke against a probe-mysql on the same host port (43501):
    Up returns
    "mysql docker: host port 43501 is already published by another
    container — `docker ps --filter publish=43501` to find it; raw: …"
    in <1 s instead of timing out on the daemon. No `bough-mysql-43501`
    container is left behind.

- mysql smoke after the probe is stopped:
    Up 0.55 s + ReadyCheck 8.9 s + Down 30 s = 42 s clean cycle, no
    orphan.
@ikeikeikeike
ikeikeikeike merged commit 513c82d into main Jun 16, 2026
1 check failed
@ikeikeikeike
ikeikeikeike deleted the feat/v0.2-hardening branch June 16, 2026 18:23
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
…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 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