Skip to content

Add gitdir backend, claude-science host, and sandbox dashboard gate; release 0.7.0 - #82

Merged
alokwhitewolf merged 13 commits into
mainfrom
gitdir-backend
Jul 1, 2026
Merged

Add gitdir backend, claude-science host, and sandbox dashboard gate; release 0.7.0#82
alokwhitewolf merged 13 commits into
mainfrom
gitdir-backend

Conversation

@alokwhitewolf

Copy link
Copy Markdown
Collaborator

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.

  • gitdir execution backend (backends/gitdir.py): per-experiment worktrees with relocated GIT_DIR/GIT_WORK_TREE and a shared object store via git alternates, no .git anywhere. Registered in _construct_backend; selectable via evo config backend gitdir / evo new --backend gitdir.
  • Base-repo relocation: evo init --host claude-science (or --backend gitdir) relocates the base repo off .git (.evo/basegit + baseline commit). core.maybe_apply_gitdir_env re-applies the base GIT_DIR on every later command. Orthogonal to execution: a relocated base composes with execution_backend=remote (Modal/SSH).
  • claude-science host: evo install claude-science sets the machine-default backend to gitdir; added to SUPPORTED_HOSTS.
  • EVO_HOME fallback: when ~/.evo is unwritable (sandbox), user-level state falls back to a workspace-local home so delegated child kernels resolve a writable home with no inherited env.
  • init skips 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.

claude and others added 10 commits June 30, 2026 09:11
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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +7505 to +7512
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Open in Devin Review

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.
@alokwhitewolf
alokwhitewolf merged commit f5e7e9b into main Jul 1, 2026
59 of 60 checks passed
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.

2 participants