fix: arrival-save plans to ~/.plannotator/plans/ on server startup#563
Open
backnotprop wants to merge 5 commits intomainfrom
Open
fix: arrival-save plans to ~/.plannotator/plans/ on server startup#563backnotprop wants to merge 5 commits intomainfrom
backnotprop wants to merge 5 commits intomainfrom
Conversation
Add planSave to PlannotatorConfig so the server can read user preferences at arrival-save time — before any HTTP request has arrived from the UI. resolvePlanSave follows the established resolveUseJina precedence (env > config > default). isSafeCustomPath rejects path-traversal segments at the /api/config boundary as a defense-in-depth measure against persisting a malicious customPath to disk. Refs #556. For provenance purposes, this commit was AI assisted.
- Write a plain {slug}.md to ~/.plannotator/plans/ on server startup
before the browser opens. Fixes #556 — users who exit Claude Code
without approving/denying now see a file land in the plans
directory they expect.
- Resolve planSave on approve/deny by merging body.planSave into
effective config and running resolvePlanSave, so
PLANNOTATOR_PLAN_SAVE / PLANNOTATOR_PLAN_SAVE_ON_ARRIVAL env vars
retain top precedence and a client default payload can't silently
re-enable saves an operator disabled at the environment level.
- Guard /api/config against persisting path-traversal segments to
config.json in all three Bun servers (plan, review, annotate).
- Test coexistence: arrival {slug}.md lives alongside -approved.md
and stays invisible to listArchivedPlans.
For provenance purposes, this commit was AI assisted.
Pi's storage/config modules are copied from packages/shared/ at build time via vendor.sh, so schema + resolver changes propagate for free. This commit updates the hand-written Pi server code: - Arrival save block in serverPlan.ts (same try/catch as Bun). - Approve/deny handlers use effective-config merge so env vars remain authoritative. - /api/config handlers (plan, review, annotate) reject unsafe customPath values. For provenance purposes, this commit was AI assisted.
Source of truth for plan-save preferences moves from browser cookies to ~/.plannotator/config.json, delivered to the client in the /api/plan response as serverConfig.planSave. - getPlanSaveSettings prefers serverConfig over legacy cookies, with a one-shot migration POST to /api/config that clears cookies only after the server confirms the write. - savePlanSaveSettings chains POSTs through a module-level promise queue so rapid changes (keystrokes in the custom-path input) land on the server in call order. - Settings.tsx takes a serverPlanSave prop and an onServerPlanSaveChange callback so mid-session changes propagate to App state — approve/deny and the archive browser see the new values without waiting for a reload. - Approve/deny bodies send customPath explicitly as string|null so a cleared path doesn't leave a stale config path winning the server's body-over-config merge. - useArchive.init marks hasFetched=true and App.tsx drops the redundant archive.fetchPlans() call that used to race with the initial setServerPlanSave update and clobber the archive list with defaults. Accepts a one-session discrepancy for legacy users upgrading with saves-disabled cookies: decision snapshots that session land in the default location alongside the arrival file, then config.json is authoritative forever after. Documented inline in planSave.ts. Refs #556. For provenance purposes, this commit was AI assisted.
For provenance purposes, this commit was AI assisted.
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.
Closes #556.
Summary
~/.plannotator/plans/{slug}.mdon server startup — before the browser opens — so users who quit Claude Code without clicking approve/deny still find the plan in the directory they expect.planSavepreferences (enabled flag + custom path) from browser cookies to~/.plannotator/config.jsonso the server can honor them at arrival-save time. Migration is lazy, one-shot, and clears cookies only after the server confirms the write.PLANNOTATOR_PLAN_SAVE/PLANNOTATOR_PLAN_SAVE_ON_ARRIVALenv vars > approve/deny body session override >config.json> defaults. Env vars now authoritatively win over the client body (previously a client default payload could silently re-enable saves an operator disabled at the environment level).customPathat the/api/configboundary (rejects..segments) as defense-in-depth.Backwards compatibility
Existing users keep their preferences — the first UI load after upgrade reads legacy cookies, POSTs them to
/api/config, and clears the cookies on success. From then on,config.jsonis authoritative.One-session transient window is accepted and documented inline in
planSave.ts: for a user who had saves disabled via cookies, the first post-upgrade session can produce harmless arrival + decision files in the default directory before migration fires. Same blast radius as the arrival-save trade-off itself — self-corrects next session.Test plan
~/.plannotator/plans/{slug}.mdexists immediately; approve →{slug}-approved.mdlands alongside.plannotator-save-path=/tmp/myplanscookie: open UI →config.jsongainsplanSave.customPath: "/tmp/myplans", cookies cleared. Next plan's arrival save lands in/tmp/myplans/.plannotator-save-enabled=false: after UI loads,config.jsonhasenabled: false; next session's arrival save is skipped.config.jsonreflects change, no cookies written; approve immediately after → snapshot uses the new values.PLANNOTATOR_PLAN_SAVE_ON_ARRIVAL=false plannotator ...→ no arrival save, regardless of config.PLANNOTATOR_PLAN_SAVE=false→ no approve/deny snapshot even if client body sendsenabled: true.POST /api/configwithplanSave.customPath: "../../etc"returns 400 and leaves config.json unchanged.plannotator archivewithcustomPathset in config.json → list and first-plan content both load from the custom directory, not defaults.bun run build:pi, submit plan → same behavior as Bun server.{slug}-approved.mdcontent unchanged.For provenance purposes, this PR was AI assisted.