feat(plan): add pluggable Storage interface (parity with todo toolset) - #3239
Merged
Conversation
Introduce plan.Storage with a default FilesystemStorage, a WithStorage option, and a per-instance New(opts...) constructor so embedders can inject their own backend and get isolation instead of the process-wide singleton. The revision bump moves into Storage.Upsert so backends can make it atomic; the filesystem default keeps today's read-modify-write (last-writer-wins). The handlers delegate to Storage instead of touching files directly, and Describe() reports the backend via fmt.Stringer. CreateToolSet() still returns the process-wide singleton, so standalone and CLI behavior is unchanged: atomic temp+rename writes, name validation, and unreadable-file warnings all live in FilesystemStorage. Tests cover the default filesystem backend and a custom in-memory backend through the interface (conformance suite), plus handler-level coverage for custom-backend injection, nil-list normalization, backend-error propagation, and proof that the revision bump is owned by Storage. Closes #3237
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The PR correctly refactors the plan toolset to match the todo pluggable-storage pattern. The drafter examined the diff for concurrency issues, nil-pointer risks, revision-bump ownership, interface conformance, and behavior regressions. The verifier confirmed all raised concerns are either pre-existing or non-regressions.
Areas checked:
- ✅ Mutex move from
ToolSet→FilesystemStorage— all four methods (Get,Upsert,List,Delete) acquires.mucorrectly; no unguarded path introduced - ✅ Singleton path (
CreateToolSet/sync.OnceValue) — unchanged and still serializes all default-path agents - ✅ Multi-instance serialization — independently-created
New()instances were never cross-serialized in the old code either; behavior is unchanged - ✅ Revision bump moved into
FilesystemStorage.Upsert— no double-bump or missing bump - ✅
WithStorage(nil)panic guard — consistent withtodopattern - ✅
var _ Storage = (*FilesystemStorage)(nil)compile-time interface assertion present - ✅
TestStorage_Conformanceruns bothFilesystemStorageand in-memory backend through all four methods - ✅ Error propagation from Storage methods through all four handlers verified
No confirmed bugs introduced by this PR.
dgageot
approved these changes
Jun 25, 2026
10 tasks
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
Brings the
planbuiltin toolset to parity withtodo: storage is now apluggable
Storageinterface with a default filesystem backend, an injectionseam, and per-instance construction instead of a process-wide-only singleton.
Acceptance criteria
plan.Storageinterface + default filesystem implStorageinterface;FilesystemStorageplan.WithStorage(...)injects a custom backendWithStorage(Storage) Option, panics on nil (matchestodo)CreateToolSet()preservedNew(opts ...Option);CreateToolSet()still returns thesync.OnceValuesingletonStorage.UpsertFilesystemStorage.UpsertTestStorage_Conformanceruns both backends over all four methodsInterface
Behavior preservation
write_plan/read_plan/list_plans/delete_plan) delegate toStoragerather than callingload/savedirectly.FilesystemStorageretains the original atomic write (temp file + rename),namePatternvalidation, missing-vs-corrupt distinction, corrupt-plan-still-deletable, filename-as-authoritative-key, and lazy directory creation on first write.ToolSettoFilesystemStorage; the shared singleton still routes every operation through one backend instance, so in-process collaboration is serialized exactly as before.Describe()reports the backend viafmt.Stringer, so the filesystem case still rendersplan(dir=...).Tests
TestStorage_ConformanceoverFilesystemStorageand an in-memory backendWithStorage;WithStorage(nil)panicsListnormalizes to"plans":[]; backend errors surface asIsErroron every handlerAll four handlers and
Describeare at 100% statement coverage. Remaining uncovered lines inFilesystemStorageare OS-error paths (MkdirAll / ReadFile / Remove failures) that require fault injection and were uncovered before this change.Design choices open to revision
These follow the issue text and the
todopattern, but can be changed if a different shape is preferred:New(dir string)becomesNew(opts ...Option), a breaking signature change to an exported constructor (mirrorstodo.New). If keepingNew(dir string)is preferred, a separate constructor can be added instead.todoexportsMemoryTodoStorage(its default). An exportedplan.MemoryStoragecan be added if embedders want an off-the-shelf ephemeral backend.FilesystemStorage(the issue lists it under the filesystem implementation), so custom backends are free to define their own key rules. If validation should be guaranteed for all backends, it can move up into the handler.Validation
go build ./...: cleango test ./pkg/tools/builtin/plan/... ./pkg/teamloader/...: passgolangci-lint run ./pkg/tools/builtin/plan/...: cleanCloses #3237