Skip to content

fix: arrival-save plans to ~/.plannotator/plans/ on server startup#563

Open
backnotprop wants to merge 5 commits intomainfrom
fix/plan-save-storage
Open

fix: arrival-save plans to ~/.plannotator/plans/ on server startup#563
backnotprop wants to merge 5 commits intomainfrom
fix/plan-save-storage

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Closes #556.

Summary

  • Auto-save the plan to ~/.plannotator/plans/{slug}.md on 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.
  • Migrate planSave preferences (enabled flag + custom path) from browser cookies to ~/.plannotator/config.json so the server can honor them at arrival-save time. Migration is lazy, one-shot, and clears cookies only after the server confirms the write.
  • Preserve full precedence: PLANNOTATOR_PLAN_SAVE / PLANNOTATOR_PLAN_SAVE_ON_ARRIVAL env 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).
  • Validate customPath at the /api/config boundary (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.json is 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

  • Fresh user (no cookies, no config.json): submit a plan → ~/.plannotator/plans/{slug}.md exists immediately; approve → {slug}-approved.md lands alongside.
  • Legacy user with plannotator-save-path=/tmp/myplans cookie: open UI → config.json gains planSave.customPath: "/tmp/myplans", cookies cleared. Next plan's arrival save lands in /tmp/myplans/.
  • Legacy user with plannotator-save-enabled=false: after UI loads, config.json has enabled: false; next session's arrival save is skipped.
  • Settings UI: toggle enable/disable + change custom path → config.json reflects change, no cookies written; approve immediately after → snapshot uses the new values.
  • Env var: PLANNOTATOR_PLAN_SAVE_ON_ARRIVAL=false plannotator ... → no arrival save, regardless of config.
  • Env var: PLANNOTATOR_PLAN_SAVE=false → no approve/deny snapshot even if client body sends enabled: true.
  • Path-traversal guard: POST /api/config with planSave.customPath: "../../etc" returns 400 and leaves config.json unchanged.
  • Archive browser: open plannotator archive with customPath set in config.json → list and first-plan content both load from the custom directory, not defaults.
  • Pi server parity: bun run build:pi, submit plan → same behavior as Bun server.
  • Regression: full approve/deny flow with annotations — {slug}-approved.md content unchanged.

For provenance purposes, this PR was AI assisted.

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.
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.

No more plan file created in ~/.planotator/plan if I don't prove/deny

1 participant