feat(core): default run storage outside the project#55
Merged
Conversation
Screenshots, logs, manifests, and evidence journals now default to ~/.pickforge/picklab/projects/<projectId>/runs/ instead of <project>/.picklab/runs/, so a default run never appears in `git status`. PICKLAB_HOME now defaults to ~/.pickforge/picklab (was ~/.picklab); the old default is still read non-destructively as a fallback for existing global config, agent state, and sessions. - Add a single storage resolver (resolveRunStorage) for home, project-local, and custom modes, with a stable per-project id derived from the canonical project path. Route run creation, the run catalog, and active-evidence-pointer resolution in core, CLI, and MCP through it. - openRunCatalog layers the resolved primary root with a read-only legacy project-local fallback root, so pre-existing project-local runs stay discoverable without migration. - Add PICKLAB_STORAGE_MODE / PICKLAB_STORAGE_PATH env overrides and a project/global config `storage` field. - Add a non-destructive legacy read fallback (resolveReadablePath, per-entry) for global config, agent state, and sessions across the ~/.picklab -> ~/.pickforge/picklab default-root change; picklab doctor flags a detected legacy home. - Update README/SECURITY docs and record the default change in docs/releases/UNRELEASED.md. Closes #34
readAgentsState previously only checked the new PICKLAB_HOME default, so "is this agent registered?" silently forgot pre-#34 ~/.picklab state instead of falling back to it non-destructively, unlike global config and sessions.
pruneFinalizedEvidenceRuns computed retention over the full merged run catalog, which under home/custom mode includes the read-only legacy project-local fallback root added for discovery. Once more than `keep` finalized runs existed across both roots, it would rm -rf the oldest ones regardless of which root they lived in, deleting pre-existing legacy runs it never owned and never migrated. Reproduced live: 15 legacy + 10 new runs, keep=5 deleted 5 legacy dirs. Scope both the candidate set and the active-pointer bookkeeping to entries whose rootDir matches the resolved primary storage root; the legacy fallback root is read-only by contract. Also corrects the docstring, which still claimed no finalization producer called this function (finalizeActiveEvidenceRun has called it automatically since this feature shipped).
…oject paths Project-committed .picklab/config.json could select storage.mode "custom" with an arbitrary path, and that config travels with `git clone` — a hostile or careless repo could silently redirect artifact writes (screenshots, which may carry secrets) to any absolute path with zero prompt. Only the user-owned global config or the PICKLAB_STORAGE_MODE / PICKLAB_STORAGE_PATH env override may now select "custom" (and supply its path). Project config may still select "project-local" (blast radius already scoped to itself) or "home". A project config requesting "custom" is not an error: it falls back to global config's mode, then "home", so a cloned repo with a hostile or misconfigured project config never bricks `picklab`. The rejection is reported on the resolved result (`rejectedProjectCustom`) and surfaced by `picklab doctor` as a warning. Also reject a custom path that equals or is nested inside the project directory — otherwise it just reintroduces project pollution under a different name. Implemented by reading the global and project config layers separately (config.ts's new loadConfigLayers) instead of resolving storage from loadConfig's already-merged blob, since the merge alone can't tell which layer a value came from.
…le one destroySessionRecord removed only the path resolveReadablePath would pick for a read (new home if present, else legacy). A session created under the legacy home and later updated writes to the new home on every update but never touches the stale legacy copy, so once both exist, destroying it only deleted the new-home copy — resolveReadablePath then picked the surviving legacy file on the next read, resurrecting the "destroyed" session via getSession / listSessions. Remove both the new-home and legacy paths unconditionally (force); either or both may not exist.
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
Implements #34 under the workspace storage policy:
homemode:~/.pickforge/picklab/projects/<slug-sha256:16>/runs/(stable id from the canonical project path);project-localrestores.picklab/runs/opt-in;customrequires an absolute, non-project-contained pathresolveRunStorage: config layers +PICKLAB_STORAGE_MODE/PICKLAB_STORAGE_PATHenv, env winning) used by runs, evidence, CLI artifacts, and MCP tools/resourcesPICKLAB_HOMEdefault moves~/.picklab→~/.pickforge/picklabwith per-file read fallback, session dir-merge, writes always to the new home;picklab doctorwarns on a detected legacy home.picklab/config.jsoncan no longer selectcustomstorage (user-owned global config or env only; falls back to home + doctorstorage-custom-rejectedwarn), and custom paths inside the project are rejectedCloses #34. Follow-up filed: #54 (pre-existing write-path symlink verification, exposure reduced by this PR).
Validation
Local review: serious-backend risk class — Opus 4.8 (data-safety), Sonnet 5 (operational), Sonnet 5 (security/trust); adjudicated P0+P1+3×P2 all fixed; targeted fix-verification clean. KISS keep.