Add gitdir backend, claude-science host, and sandbox dashboard gate; release 0.7.0 - #82
Conversation
The 0.6.2 migration wrapped every hook command in a `node -e` launcher
that calls `spawnSync(helper, {input})` with no timeout. The helper
(`evo-hook-drain`) hands off to the Python `evo-drain`, which inherits
the helper's stdout/stderr (`Stdio::inherit()`). node's `spawnSync` does
not return until those captured pipes reach EOF, i.e. until every
descendant holding them exits. If any descendant lingers, the launcher
blocks forever, so the host shows the PostToolUse hook "running" with a
spinner and the stuck node processes pile up (the reported "6 PostToolUse
hooks with loading animations"). It never blocks agent progress because
the host times the hook out, but the orphaned launcher keeps spinning.
The launcher's own contract is to "fail open", but failing open requires
bounding every blocking call. Fix:
- `spawnSync` now passes `timeout:2000` + `killSignal:'SIGKILL'`, so a
lingering/leaked descendant can no longer hang the hook. 2s is well
under the host's 60s hook timeout and comfortably above a real
sub-second drain.
- stdin is read only when fd 0 is a real pipe (`!tty.isatty(0)`), so an
interactive terminal never blocks `readFileSync(0)`.
Applied to both launcher sources: the shared `plugins/evo/hooks/hooks.json`
(Claude Code) and the Codex `_stable_hook_command()` generator. Adds a
regression test that spawns a helper whose grandchild outlives it on the
inherited stdio and asserts the hook still returns and fails open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpudqCLoPU7Y29rAKLw3Eg
Alpha patch carrying the PostToolUse hook hang fix (bounded spawnSync + stdin tty-guard so the node launcher fails open instead of spinning indefinitely). spawnSync timeout set to 2s. Bumps all version pins via scripts/bump-version.py and syncs npm/.
Some execution sandboxes deny creating any path named .git (files and directories alike), so the worktree backend's `git worktree add` and even `git init` fail with EPERM. The gitdir backend isolates each experiment with a relocated GIT_DIR under <run>/gitdirs/<exp> and a working tree that contains no .git, sharing the base repo's object store via objects/info/alternates so nothing is duplicated. Per-experiment isolation (independent HEAD/index/refs) matches the worktree backend. Registered in backends._construct_backend; selectable via `evo config backend gitdir` and `evo new --backend gitdir`. workspace_executor_for returns a GitDirExecutor that injects GIT_DIR/GIT_WORK_TREE, so the run and commit path resolves the repository without .git discovery. tests/unit/test_gitdir_backend.py uses real git (no mocks): allocate is .git-free and at the parent commit, objects share via alternates rather than copy, the executor commit path makes a real commit, two experiments stay isolated, and discard/sweep_orphans reclaim the workspace.
Extends the gitdir backend so evo runs end-to-end where `.git` creation is forbidden, not just per-experiment isolation. - `evo init --backend gitdir` (and any `--host claude-science` workspace) relocates the BASE repo off `.git`: it creates `.evo/basegit` via a relocated GIT_DIR, commits a baseline, and excludes `.evo/` from tracking. So init works without an existing `.git` and without creating one. - `core.maybe_apply_gitdir_env`, called at CLI startup, re-exports the base `GIT_DIR`/`GIT_WORK_TREE` for any workspace whose base is relocated (a `.evo/basegit` exists). Gated on relocation presence, NOT on `execution_backend`, so it is correct whether experiments run locally (gitdir) or on a remote provider (remote backend still bundles from the local base). No-op for normal `.git` workspaces, so worktree/pool/other hosts are unaffected. - `evo install claude-science` host adapter: sets the machine default `execution_backend=gitdir` (skills are published into the CS catalog by the in-session setup skill via host.skills; a CLI cannot write that catalog). Registered in both host lists. - Base relocation and execution backend are orthogonal: a claude-science workspace relocates its base regardless of where experiments execute, so `execution_backend=remote` (Modal/SSH) composes with a .git-free local base. tests/unit/test_gitdir_init.py: real-git tests for gitdir init (base relocated, baseline committed, nothing named .git, .evo excluded), the env hook (applies for relocated workspaces, no-op otherwise), and the claude-science adapter (install/doctor/uninstall). Verified end-to-end via the real CLI: init + new in a non-git dir with zero .git created.
Sandboxes (e.g. the Claude Science kernel) block the home directory, so ~/.evo can't be created and any user-default read/write fails with EPERM. global_evo_dir() now falls back to <workspace>/.evo/home when ~/.evo isn't writable, so evo works without a manually-set EVO_HOME and delegated child kernels resolve a writable home on their own without inheriting env.
init started the dashboard supervisor unconditionally. Relocated gitdir / claude-science workspaces run under a sandbox that forbids TCP socket binds, so the supervisor only crash-loops there. Gate the start on the relocate path and print a note pointing at `evo status` / `evo tree` instead.
There was a problem hiding this comment.
🚩 Telemetry always reports backend as "worktree" for init commands, now stale for gitdir
At plugins/evo/src/evo/cli.py:7436, props["backend"] = "worktree" is hardcoded in _telemetry_track_command for all init commands. With the new gitdir backend, this will misreport the backend for claude-science / gitdir-mode workspaces. The line is pre-existing and not in a changed hunk, but the PR makes it more wrong by adding a new backend that it doesn't account for. It should read something like props["backend"] = getattr(args, "backend", None) or "worktree" — or ideally pull the resolved backend from the workspace config.
Was this helpful? React with 👍 or 👎 to provide feedback.
| # In a workspace whose base repo is relocated off `.git` (gitdir mode), | ||
| # export the base GIT_DIR/GIT_WORK_TREE so every command's git calls resolve | ||
| # without a `.git`. No-op for normal `.git` workspaces. | ||
| try: | ||
| from .core import maybe_apply_gitdir_env | ||
| maybe_apply_gitdir_env() | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
🚩 Auto-registration silently skipped for gitdir workspaces due to call ordering
_maybe_auto_register() at plugins/evo/src/evo/cli.py:7471-7485 calls repo_root() which needs GIT_DIR for gitdir workspaces. But it runs BEFORE the new maybe_apply_gitdir_env() block at lines 7505-7512 that sets GIT_DIR. So in a gitdir workspace, repo_root() fails (no .git, no GIT_DIR yet), and auto-registration silently does nothing. The try/except is intentional (best-effort), so this is not a crash — but auto-registration never fires for gitdir workspaces. Swapping the two blocks would fix it.
Was this helpful? React with 👍 or 👎 to provide feedback.
- gitdir alternates: write the base object-store path with forward slashes. git's alternates parser doesn't resolve Windows backslash paths, so the shared object store went unlinked and the parent commit read as missing. - find_workspace_root: skip a .evo that is the global evo home ($EVO_HOME / ~/.evo). It shares the name but isn't a workspace; on Windows temp dirs live under ~, so an ambient ~/.evo was wrongly resolved as the workspace root.
…reachable The Windows CI 'parent commit not found' failure is opaque. Include the written alternates path, whether the base object store dir resolves, and git's stderr in the error so the root cause is visible.
Path.write_text() translates \n to \r\n on Windows. git's alternates parser reads the path literally and keeps the trailing \r, so it looked for 'objects\r' and the shared object store went unlinked (parent commit unfound). Write bytes with a bare LF. Assert no CR in the alternates file.
Adds a
.git-free execution path so evo runs in sandboxes that forbid.git(Claude Science's kernel), and cuts the 0.7.0 release.gitdirexecution backend (backends/gitdir.py): per-experiment worktrees with relocatedGIT_DIR/GIT_WORK_TREEand a shared object store via git alternates, no.gitanywhere. Registered in_construct_backend; selectable viaevo config backend gitdir/evo new --backend gitdir.evo init --host claude-science(or--backend gitdir) relocates the base repo off.git(.evo/basegit+ baseline commit).core.maybe_apply_gitdir_envre-applies the baseGIT_DIRon every later command. Orthogonal to execution: a relocated base composes withexecution_backend=remote(Modal/SSH).claude-sciencehost:evo install claude-sciencesets the machine-default backend togitdir; added toSUPPORTED_HOSTS.EVO_HOMEfallback: when~/.evois unwritable (sandbox), user-level state falls back to a workspace-local home so delegated child kernels resolve a writable home with no inherited env.initskips evo's Flask dashboard in relocated (sandbox) workspaces, where the sandbox forbids TCP binds.Other hosts unchanged (worktree default preserved; the env hook is a no-op without a relocated base). Real-git tests cover the backend, init/relocation, the host adapter, and the dashboard gate.