Operational reference for running, seeding, and validating Copse. The rules that apply to every
change stay in AGENTS.md; this guide holds environment-specific mechanics that are
useful only when that workflow needs them.
Copse is an Electron desktop app with no backend service. make run is the normal entry point: it
runs check-node, content-addresses dependency and build inputs, verifies the complete dist/ tree,
and then starts the app. It is safe to repeat and does the minimum work needed even after branch
switches or edits with preserved mtimes. make build stops after syncing the build, and make clean
drops dist/ and the dev-sync fingerprints.
The standard scripts live in package.json; use dev, build, start, typecheck, lint,
format:check, test, test:e2e, and check rather than recreating their behavior. Reach for
dev over make run when you want watch mode rather than a single build.
The repo pins the Node 24 LTS release in .nvmrc and requires Node >=24 plus
pnpm (packageManager: pnpm@10.34.5; enable with corepack enable). Both nvm
and fnm read .nvmrc. Tooling under scripts/*.mts uses native TypeScript type
stripping.
Installs use pnpm’s default isolated linker with package-import-method=auto
(.npmrc: prefer clone, then hardlink, then copy) so packages are symlinked
through node_modules/.pnpm while store bytes are shared when the filesystem
allows. Agent-prepared worktrees route Corepack, the pnpm store, Electron downloads and extracted
runtime, and gortex through fixed directories under ~/.copse/cache/. The read-only
preflight_worktree reports the current project's package-manager and declared setup readiness.
Pass its planFingerprint to one approved prepare_worktree call to install locked dependencies
and run the setup declared in .copse/worktree-preparation.json. This repository declares its
Electron/ChromeDriver/native/gortex steps there; other projects need none of those artifacts.
Python projects with pyproject.toml and uv.lock automatically use locked uv workspace sync;
they need an installed compatible Python and uv, but no Copse declaration. Preflight leaves the
project and shared caches read-only, using disposable scratch for manager bookkeeping.
See project worktree preparation.
Cursor Cloud setup normally installs the pinned version through .cursor/cloud-setup.sh. If an
older executable still shadows it, activate the repo version:
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
nvm install
nvm use
export PATH="$HOME/.nvm/versions/node/v$(cat .nvmrc)/bin:$PATH"With fnm, run fnm install && fnm use from the repository root instead.
Confirm with node -v before debugging a tooling failure.
The Cloud VM exposes a VNC desktop on DISPLAY=:1, so launch the app with
DISPLAY=:1 npm run dev. Prefer WebdriverIO for repeatable evidence; use VNC to debug layout or
runtime behavior.
Known environment behavior:
-
A first
npm run devcan transiently readdist/main/index.jswhile the initial bundle is being written. Runnpm run buildonce first or restart the watcher if it reports a startup syntax error. -
Headless GPU and D-Bus initialization errors are usually benign when the app still renders. An explicit Electron launch may use
--disable-gputo quiet them. -
The VNC screen blanker can cover a healthy app after roughly ten seconds without a real X input event. Pointer warps do not reset it. For a recording, an unbound real key event keeps it awake:
while true; do DISPLAY=:1 xdotool key F15; sleep 0.5; done
No provider key is needed to exercise the core loop. When neither ANTHROPIC_API_KEY nor
OPENAI_API_KEY is set and COPSE_PANEL_MOCK_LLM is unset, Copse falls back to
MockLLMProvider (src/shared/llm/mock-provider.ts). It echoes the user message and issues one
list_dir call on its first turn. Set COPSE_PANEL_MOCK_LLM=1 to force it when credentials exist.
Development and test builds support two one-shot mock directives in a user message:
[[mcp:<tool> {json}]]selects a tool call.[[mock:delay_ms <n>]]delays the response.
They are gated behind __COPSE_TEST_DIRECTIVES__. npm run build:release sets COPSE_RELEASE=1,
dead-code eliminates the parser, and fails if a directive marker survives, so packaged apps do not
ship this test language.
For a multi-turn e2e, register an ordered regex-to-tool/text script through
window.__copseE2e.setMockScript([…]). Keep the script next to the natural-language prompts in the
spec; tests/e2e/mock-script-multiturn.e2e.ts is the reference. Reserve inline directives for
one-shot steering.
Everything Copse persists lives under one root, ~/.copse/ (COPSE_DIR moves the whole profile):
| Path | Contents |
|---|---|
user-data/config.json |
projects, activeProjectId, workspace root, pack settings, usage ledger |
user-data/settings.json |
settings, including encrypted API keys |
user-data/ (rest) |
mcp.json, tools/, browser profiles, gortex/ semantic index |
workspace/<projectId>/<threadId>/ |
threads, tasks, decision log, deferred approvals |
worktrees/ |
Copse-managed Git worktrees |
knowledge/, long-tasks/, roadmap-review/, pack-tool-snapshots/, hooks.json |
per-feature stores |
Electron's userData used to default to <appData>/copse-panel (~/Library/Application Support/
on macOS), which split the profile across two unrelated directories. app-init.ts now points it at
~/.copse/user-data/ and migrates a legacy directory across on first launch;
COPSE_PANEL_USER_DATA still pins an exact directory and skips migration.
pnpm run dev (and make run-dev) launches against ~/.copse-dev instead, so the watch loop keeps
its own persistent threads, settings, and plugins — and its own Electron single-instance lock, which
is what lets a dev build and make run be open at the same time. Setting COPSE_DIR or
COPSE_PANEL_USER_DATA yourself disables that default, including setting COPSE_DIR=~/.copse to
reproduce something against the everyday profile.
To bypass the native “Open Folder” dialog, pre-seed a projects entry and activeProjectId.
Chat threads do not live in config.json. They use the filesystem-native store at
~/.copse/workspace/<projectId>/<threadId>/, with meta.json, append-only events.jsonl, OKF
messages/*.md, and blobs/*. COPSE_WORKSPACE_DIR overrides just that root. The e2e harness and
unit tests point it at a disposable directory.
Use writeSeedConfig in tests/e2e/helpers/seed-config.ts to seed e2e state. It translates any
threads:<projectId> fixture array into the native thread directories; writing that key directly to
config.json no longer works. The thread store is mounted read-only into agent read tools for past
thread references. See thread-store-format.md.
The post-edit hook runs scripts/hook-file-check.mts through the Copse, Cursor, and Claude hook
configs. It auto-applies oxfmt and reports type-unaware ESLint findings. A reported rewrite makes
the agent's prior view stale, so re-read that file. Type-aware rules and tsc remain part of
npm run check.
While iterating, use a path/base-name/glob filter or the test oracle:
npm test -- thread-store
npm run oracle -- --explain
npm run oracle -- --run unit
npm run oracle -- --run e2eA filter matching zero tests is an error. Trust an oracle subset only at HIGH confidence; LOW
lists blind spots and broad calls for the full tier. Always run npm run check before committing.
See testing-strategy.md for the complete tier and CI policy.
Prefer the ordinary remote e2e loop when a cloud host is configured:
npm run e2e:remote -- run --detach
npm run e2e:remote -- wait <run-id>Results land in .tmp/remote-e2e/runs/<run-id>/. With COPSE_CI_REGISTRY, e2e:remote up pulls a
pre-baked image. Use local test:e2e for macOS-specific behavior, where no remote host is available,
or when a skill requires an on-machine display. For native GUI behavior or authenticated real-agent
runs, use the isolated workflow in remote-agent-demo-debugging.md.
Every visible change needs the smallest focused browser or Electron spec that seeds the target state, asserts its DOM behavior, and saves a screenshot. Do not substitute a manual VNC glance.
A typical Electron fixture flow is:
- Run
npm run build. - Seed the app through
tests/e2e/helpers/seed-config.tswith a project and active project id. - Launch with
COPSE_PANEL_MOCK_LLM=1 ANTHROPIC_API_KEY= OPENAI_API_KEY=when the state needs the agent loop but not a real model. - Run the focused spec, for example
npm run test:e2e -- --spec tests/e2e/tool-display-live-mock.e2e.ts. - Inspect the resulting image under
tests/e2e/screenshots/.
The test oracle defines screenshot ownership. The CI run remains read-only and attaches changed
renders as an immutable artifact. After a successful same-repository run, a trusted follow-up puts
those PNGs on a bot-owned branch, opens a child PR into the source branch, and posts its link on the
parent PR. Review GitHub's image diffs there, then merge that child PR (or enable auto-merge) to apply
the accepted references. A source-head change supersedes the child PR. Forks and promotion PRs whose
source is an integration branch use the downloadable artifact and a manual commit because automation
must not write to those branches. If a real visual change is not mapped, apply the
update-screenshots label to render the complete reference set, then remove the label after the
review PR is created. Local filtering is implemented by
scripts/lib/screenshot-scope.mts; fixture determinism and tier selection are documented in
testing-strategy.md.