Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 133 additions & 11 deletions packages/shepherd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ consumes: docs/eval/2026-07-06-shepherd-omp-adoption.md (adoption rationale)
# shepherd

[shepherd](https://github.com/shepherd-agents/shepherd) runtime substrate at
curated pin **`shepherd-ai` 0.2.1** — records agent runs as reversible
curated pin **`shepherd-ai` 0.3.0** — records agent runs as reversible
execution traces; sandboxed agents produce *retained outputs* reviewed with
`shepherd run changeset` and settled with `select`/`release`/`discard` before
anything touches the working tree. Task permissions are declared per-binding
`shepherd run changeset` and settled with `select`/`release`/`apply`/`discard`
before anything touches the working tree. Task permissions are declared per-binding
in the Python signature (`May[GitRepo, ReadOnly|ReadWrite]`) and enforced at
the native syscall jail (Linux Landlock; macOS Seatbelt).

Expand All @@ -22,7 +22,8 @@ shepherd doctor claude # core + jail + claude CLI/auth readiness (--probe = l
```

Not a harness: this package is the supervision layer next to the harnesses.
The sandboxed executor lane is **claude-only by design** in v0.2.x — the
The sandboxed executor lane is **claude-only by design** through v0.3.x
(re-verified against the 0.3.0 runtime-provider source) — the
retained-run provider accepts `static` (deterministic, keyless) or `claude`;
pointing it at pi/omp would be an upstream source fork, not configuration
(see the adoption eval in `docs/eval/`).
Expand All @@ -43,24 +44,132 @@ pointing it at pi/omp would be an upstream source fork, not configuration
- **Python API note:** `uv tool install` exposes the CLIs only. Demo scripts
(`agent_task.py` etc.) import `shepherd`/`shepherd_dialect`, so run them
with an interpreter that has `shepherd-ai` installed as a library
(`uv run --with "shepherd-ai==0.2.1" python agent_task.py`), not the bare
(`uv run --with "shepherd-ai==0.3.0" python agent_task.py`), not the bare
system `python3`.

## Placement semantics (why WSL2 works)

`placement="jail"` fails closed where the OS jail is unavailable; `"auto"`
degrades visibly to advisory. Upstream validates Linux Landlock only inside
containers, but the jail is self-contained (unprivileged Landlock, kernel
5.13+): on this repo's reference WSL2 host (kernel 6.6.87.2,
degrades visibly to advisory. As of 0.3.0 upstream executes grant enforcement
on both Linux (Landlock) and macOS (Seatbelt) — the 0.2.0 container-gated
validation caveat is retired. The jail remains self-contained (unprivileged
Landlock, kernel 5.13+): on this repo's reference WSL2 host (kernel 6.6.87.2,
`CONFIG_SECURITY_LANDLOCK=y`) `doctor` reports `native-jail: available` and
jailed runs execute. Always gate on `shepherd doctor claude`, not assumption.

## Operational runbook (live-agent runs)

The retained-run lifecycle from the quickstart demo scales to real agent work,
but the surface has three operational gaps the quickstart doesn't exercise.
All three were hit during a real multi-file Rust refactor run (2026-07-09,
reverie CER-1190, on 0.2.1); the fixes are mechanical and repeatable, and
every internal they rely on was re-probed as present and unchanged in 0.3.0
(`budget_seconds` default still 240, transport seam, recovery API,
readiness query).

### 1. Budget: the default 240 s is too short for refactors with compile checks

`workspace.run(runtime={"provider": "claude"})` constructs the
`ClaudeHeadlessProvider` with `budget_seconds=240` (hardcoded default). That's
enough for the donut demo but not for a multi-file edit + `cargo check` cycle
on a large workspace — the agent gets `BudgetExhausted` mid-flight and the run
fails. The budget is **not exposed** through the `runtime` dict (which only
carries `provider` and `model`); you patch it at the transport seam:

```python
from shepherd_dialect.workspace_control import runtime_provider as rp
from shepherd_dialect.providers import ClaudeHeadlessProvider

BUDGET_SECONDS = 900 # 15 min — enough for a multi-file refactor + cargo check
MAX_TURNS = 40

def _patched_transport(invocation):
return ClaudeHeadlessProvider(
provider_id=invocation.provider_id,
prompt=invocation.prompt,
model=invocation.model_name,
budget_seconds=BUDGET_SECONDS,
max_turns=MAX_TURNS,
)

rp._WORKSPACE_RUNTIME_PROVIDER_TRANSPORTS = rp._WorkspaceRuntimeProviderTransports(
claude=_patched_transport,
)
```

Set this **before** `workspace.run(...)`. The transport object is a frozen
dataclass holding one callable; replacing it is the only public-adjacent way
to raise the budget without a source fork. Revisit if upstream exposes
`budget_seconds` in the runtime plan.

### 2. Crashed runs orphan a scope that blocks readiness — `repair` won't fix it

A run that dies mid-flight (budget exhausted, host loss, wrong interpreter)
can leave an **orphaned scope ref** in `.vcscore/`. The next `workspace.run`
fails with `readiness blocked by run-XXXXXXXX`. `shepherd run repair`
reclaims orphaned *operation refs* only — it will report "Nothing to repair"
while the scope ref still blocks. The fix is the `VcsCoreApp` recovery API:

```python
uv run --with "shepherd-ai==0.3.0" python3 -c "
from vcs_core._app import VcsCoreApp, AppOpenMode
with VcsCoreApp.open_existing('.', mode=AppOpenMode.RECOVERY) as app:
print(app.archive_orphaned_scopes())
"
```

This is the failure mode the durable-patterns spec (OPS-482) targets for
automated recovery. Until that lands, `archive_orphaned_scopes()` is the
manual unblock. Gate on it: if `shepherd run repair` says "Nothing to
repair" but `workspace.run` still fails with `readiness blocked`, run the
snippet above.

### 3. `select` moves into vcs-core custody, not the working tree

`shepherd run select <run>` settles the output (state → `selected`) but does
**not** materialize the files into the working tree — it moves the changeset
into its "live parent world" inside `.vcscore/`. To get the files into the
tree for compile-check, read them from the retained changeset and write them
yourself:

```bash
shepherd run changeset <run> --read <path> > <path> # one file at a time
```

or batch-extract with a loop. The retained changeset is the source of truth;
`select`/`release`/`discard` are settlement actions, not filesystem
materialization — a gap inherent to the world/custody model. 0.3.0 adds a
fourth verb, `apply` (three-way-merge of a run's delta onto a workspace that
moved on, fail-closed on overlap); whether it also materializes the working
tree has NOT been live-exercised here yet — until verified, keep using the
changeset-extract flow below.

### Pre-flight checklist for a real agent run

1. `shepherd doctor claude --probe` — 9/9 including live auth round-trip.
2. Check readiness if the workspace has prior runs:
```python
uv run --with "shepherd-ai==0.3.0" python3 -c "
from vcs_core._query_readiness import evaluate_readiness, ReadinessRequest
r = evaluate_readiness('.vcscore', ReadinessRequest(command='shepherd.run'))
print(r.state, len(r.blockers or []))
"
```
If `blocked`, run the `archive_orphaned_scopes()` snippet from §2.
3. Patch the budget (§1) if the task involves edits + compile/test cycles.
4. After the run: `shepherd run show <run>` (enforcement, terminal status),
`shepherd run changeset --latest` (files), `--read <path>` (content).
5. To apply: extract files from the changeset (§3), `cargo check` / test,
then `git add` + commit. `select` is optional — it's a settlement record,
not the materialization step.

## Verified

CI-gated: install-script pin format + README/pin agreement (`tests/`),
shellcheck, ruff. Exercised live on WSL2 (2026-07-06, kernel 6.6.87.2):
shellcheck, ruff. Exercised live on WSL2 (0.2.1 on 2026-07-06, 0.3.0 bump on
2026-07-09; kernel 6.6.87.2):

- `install.sh` → `shepherd, version 0.2.1`, executables `shepherd` + `sp`.
- `install.sh` → `shepherd, version 0.3.0`, executables `shepherd` + `sp`.
- `shepherd doctor claude --json` → 8/8 ok, including `native-jail: available`
and `claude-auth` via `ANTHROPIC_API_KEY`.
- Offline quickstart (static provider, keyless) → run `retained`, changeset
Expand All @@ -71,6 +180,19 @@ shellcheck, ruff. Exercised live on WSL2 (2026-07-06, kernel 6.6.87.2):
`discard`. Caveat observed: a demo run that crashes mid-flight (e.g. wrong
interpreter) can leave the workspace `readiness blocked` for later runs —
a fresh `shepherd init` directory clears it.
- **Real agent refactor** (2026-07-09, reverie CER-1190): jailed agent ran
5.7 min with budget patched to 900 s / 40 turns, wrote a 10-file changeset
(multi-crate `tracing-subscriber` consolidation), changeset reviewed
file-by-file via `--read`, rustfmt-clean, all 4 sites replaced, `reveried`
untouched. Three operational gaps documented above were all hit and
resolved during this run.
- **0.3.0 bump verification** (2026-07-09): existing 0.2.1-era `.vcscore`
workspaces open cleanly under 0.3.0 (`doctor` Ready; `shepherd run list`
reads prior run history); offline quickstart green (run `retained`, settled
`released`). Known limitation unchanged: worktree adoption still hard-fails
on tracked symlinks and gitlinks (re-confirmed on 4 repos; OPS-524,
upstream `_workspace_external.py` raises on both).

Needs live exercise per-host: jail availability (kernel-dependent) and claude
auth — both reported by `shepherd doctor claude`.
auth — both reported by `shepherd doctor claude`. Not yet exercised on 0.3.0:
the jailed live-claude lane and the new `apply` settlement verb.
2 changes: 1 addition & 1 deletion packages/shepherd/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Idempotent. uv-native install from PyPI — never pipe remote scripts to a shell.
set -euo pipefail

SHEPHERD_VERSION="0.2.1"
SHEPHERD_VERSION="0.3.0"

command -v uv >/dev/null 2>&1 || { echo "uv not found — run scripts/bootstrap.sh first (proto provides uv)" >&2; exit 1; }

Expand Down
Loading