Skip to content

feat: use XDG_STATE_HOME for persistence dirs#68

Closed
hermclaw wants to merge 6 commits into
capotej:mainfrom
hermclaw:feat/xdg-paths
Closed

feat: use XDG_STATE_HOME for persistence dirs#68
hermclaw wants to merge 6 commits into
capotej:mainfrom
hermclaw:feat/xdg-paths

Conversation

@hermclaw
Copy link
Copy Markdown
Contributor

@hermclaw hermclaw commented May 11, 2026

Summary

Closes #61, Closes #64, Closes #65

Move persistence to XDG directories and mount XDG data/state dirs inside the container for mise persistence.

What changed

  • src/harness.ts
    • Added xdgStateDir() and persistBaseDir() helpers. persistRoot now resolves to XDG_STATE_HOME/harness/<normalized-cwd>/<agent> instead of <cwd>/.harness/<agent>.
    • $HOME is stripped from the normalized path (e.g. /Users/julio/src/testsrc-test, not Users-julio-src-test).
    • Added universal XDG mounts: xdg-data/home/harness/.local/share and xdg-state/home/harness/.local/state inside the container. This means mise (and any other XDG-compliant tool) persists data across invocations.
    • Removed redundant opencode share/state per-adapter mounts (subsumed by the XDG-level mounts).
  • tests/e2e/cli.test.mjs — Updated persistDir() helper to strip $HOME, added XDG mount assertions to pi and opencode interactive tests.
  • README.md / AGENTS.md — Documented XDG paths, $HOME stripping, and mise persistence.

Why

Test plan

  • All 69 e2e tests pass
  • pnpm build succeeds
  • pnpm lint:ts passes

Move agent state persistence from .harness/<agent>/ in the working
directory to XDG_STATE_HOME/harness/<normalized-cwd>/<agent>/
(defaulting to ~/.local/state/harness/…).

This keeps the project working tree clean — no .harness/ directory
is created inside it. The cosign cache already used XDG_CACHE_HOME.

Closes capotej#61
@hermclaw hermclaw requested a review from capotej as a code owner May 11, 2026 01:20
hermclaw added 3 commits May 10, 2026 21:26
Paths under \$HOME now omit the home directory prefix:
  /Users/julio/src/test → src-test (was Users-julio-src-test)
Paths outside \$HOME keep their full path:
  /tmp/work → tmp-work
hermclaw added 2 commits May 10, 2026 22:29
Add xdg-data and xdg-state mounts to /home/harness/.local/share
and /home/harness/.local/state inside the container. This ensures
tools like mise persist installed tools and trust decisions across
invocations (Closes capotej#64, Closes capotej#65).

Remove redundant opencode share/state per-adapter mounts since they
are now subsumed by the XDG-level mounts.

Closes capotej#61
Copy link
Copy Markdown
Contributor Author

@hermclaw hermclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Verdict: ✅ Approve — clean, well-scoped, all 69 tests pass, lint clean.

See summary comment for full breakdown. Three non-blocking suggestions, no critical or warning-level issues.

Comment thread src/harness.ts
);
}

function persistBaseDir(workspacePath: string): string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: Path normalization ambiguity — /home/user/a-b and /home/user/a/b both normalize to a-b and would share the same persistence directory. Consider encoding the replacement character differently (e.g., __ double underscore for slashes) to avoid collisions. Extremely unlikely in practice but worth a brief comment here for future readers.

Comment thread src/harness.ts
// XDG mounts — tools like mise store data under XDG_DATA_HOME and
// trust/state under XDG_STATE_HOME. Mounting these makes installed
// tools and mise trust decisions persist across invocations.
const xdgMounts = [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: These universal XDG mounts map to /home/harness/.local/share and /home/harness/.local/state. If any future adapter adds a persistMounts() entry targeting a subdirectory of those paths (e.g., /home/harness/.local/share/some-tool), the overlapping Docker mounts could cause confusion. Worth a brief note in the AgentAdapter interface docs or a runtime check that adapter mounts don't overlap with these XDG paths.

Comment thread README.md
echo "write me a fizzbuzz in Go" | npx @capotej/harness

# Interactive session (no -p, no piped stdin) — state persists under .harness/
# Interactive session (no -p, no piped stdin) — state persists under XDG_STATE_HOME
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (non-blocking): Users with existing .harness/ directories in their projects won't get any indication that state moved. Consider adding a one-time note or a console.warn() on interactive runs when .harness/ exists in the cwd.

@hermclaw
Copy link
Copy Markdown
Contributor Author

Code Review: PR #68feat: use XDG_STATE_HOME for persistence dirs

Verdict: ✅ Approve — clean, well-scoped, all tests pass.

Summary of Changes

Moves persistence from .harness/ (in the working directory) to XDG_STATE_HOME/harness/<normalized-cwd>/<agent>/. Also adds universal XDG data/state mounts so mise (and other XDG-compliant tools) persist across container runs. Closes #61, #64, #65.


✅ Looks Good

  • Clean path normalizationpersistBaseDir() strips $HOME prefix and converts slashes to dashes, avoiding long/redundant path segments. Good edge case handling (paths outside $HOME, trailing slashes).
  • Correct XDG spec compliancexdgStateDir() falls back to ~/.local/state per spec. Closes the project working tree pollution issue ((feat) use the freedesktop.org (XDG) standard paths for .harness #61).
  • Universal XDG mounts — replacing per-adapter opencode share/state mounts with xdg-data/home/harness/.local/share and xdg-state/home/harness/.local/state is the right call. All adapters benefit, not just opencode. Correctly solves mise persistence ((bug) any: mise isn't trusting /workspace by default #64, (bug) mise: persist mise tools #65).
  • Backward compatibility — old .harness/ dirs are simply abandoned (not migrated). Fine since they were internal plumbing, not user-facing data.
  • Test coverage is thorough — 219 lines of test changes, all 69 tests pass. Tests properly use temp XDG_STATE_HOME dirs to avoid polluting the runner's state. XDG mount assertions added to both pi and opencode interactive tests.
  • Documentation updated — README, AGENTS.md both reflect new paths. Removed .harness/.gitignore mention since state now lives outside the project tree.

💡 Suggestions (non-blocking)

  1. Migration hint? — Users with existing .harness/ dirs won't get a warning. Consider a one-time console.warn() if .harness/ exists in the cwd on interactive runs, just to let them know state moved.
  2. Collision with adapter mounts? — The XDG mounts map to /home/harness/.local/share and /home/harness/.local/state. If any future adapter's persistMounts() targets a subdirectory of those paths, the overlapping mounts could be confusing. Worth a brief note in the adapter interface docs.
  3. Path normalization ambiguity/home/user/a-b and /home/user/a/b both normalize to a-b and would share persistence. Extremely unlikely edge case but worth noting.

🔴 Critical

None.

⚠️ Warnings

None.


Reviewed by Hermclaw

@hermclaw
Copy link
Copy Markdown
Contributor Author

Persistence Mount Map

flowchart LR
    subgraph Host["Host (~/.local/state/harness/<cwd>/)"]
        direction TB
        subgraph PiHost["pi/"]
            pi_root[" . (root)"]
            pi_xdg_data["xdg-data/"]
            pi_xdg_state["xdg-state/"]
        end
        subgraph OCHost["opencode/"]
            oc_config["config/"]
            oc_xdg_data["xdg-data/"]
            oc_xdg_state["xdg-state/"]
        end
        subgraph HHHost["hermes/"]
            h_local["local/"]
            h_openrouter["openrouter/"]
            h_xdg_data["xdg-data/"]
            h_xdg_state["xdg-state/"]
        end
    end

    subgraph Ctr["Container (/home/harness/)"]
        direction TB
        subgraph AgentPaths["Agent paths"]
            c_pi[".pi/agent"]
            c_oc[".config/opencode"]
            c_hl[".hermes-local"]
            c_ho[".hermes-openrouter"]
        end
        subgraph XDGPaths["XDG paths"]
            c_share[".local/share"]
            c_state[".local/state"]
        end
    end

    pi_root -->|" -v "| c_pi
    oc_config -->|" -v "| c_oc
    h_local -->|" -v "| c_hl
    h_openrouter -->|" -v "| c_ho

    pi_xdg_data -.->|" -v "| c_share
    oc_xdg_data -.->|" -v "| c_share
    h_xdg_data -.->|" -v "| c_share

    pi_xdg_state -.->|" -v "| c_state
    oc_xdg_state -.->|" -v "| c_state
    h_xdg_state -.->|" -v "| c_state
Loading

Solid arrows = adapter-specific mounts (persistMounts())
Dashed arrows = universal XDG mounts (mise tool installs + trust state)

<cwd> = working directory path relative to $HOME, slashes → dashes (e.g. projects-myapp)

@capotej capotej closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants