Getting Started¶
Page status: release-ready Source state: checked-example Applies to: Shepherd v0.3.0 Owner: @docs-system-owner (TBD) Validation: docs_src/quickstart/test_world_hero.py
Quickstart. This is the path that runs on the installed package, offline. For the mental model, see the concepts. For exact APIs, see the reference.
Run one task, get its work back as a retained output — a proposal held beside your files — inspect it, and settle it. Deterministic, keyless, no network.
Install¶
Shepherd requires Python 3.11+. OS-level grant enforcement is executed on both macOS (Seatbelt) and Linux (Landlock); Windows is unsupported — use WSL. (Platform notes)
Initialize a workspace¶
Shepherd runs inside an initialized workspace. Lead with shepherd init:
Run¶
Save this as hero.py in that directory and run python hero.py:
import shepherd as sp
# A task is a signature + docstring: the contract a sandboxed agent fulfils.
# The `repo` parameter is the whole permission surface — a writable workspace
# handle. Narrow it with sp.May[sp.GitRepo, sp.ReadOnly] when writes aren't needed.
@sp.task
def write_note(repo: sp.GitRepo, topic: str, output_path: str, output_text: str) -> None:
"""Write one note about `topic` into the repository."""
with sp.open(".") as workspace: # run `shepherd init` here first
workspace.tasks.register(write_note)
run = workspace.run(
write_note,
repo=workspace.git_repo(),
topic="shepherd",
output_path="NOTE.txt",
output_text="Hello from a Shepherd retained output.\n",
runtime={"provider": "static"}, # deterministic, offline; "claude" = live agent
)
output = run.output() # a proposal, held to one side
print(list(output.changeset().changed_paths)) # what it wants to change
print(output.read_text("NOTE.txt"), end="") # read it before deciding
output.select() # record your decision — or .discard()
What happens, in order:
@sp.taskdeclares the task: a signature plus docstring — the contract a provider-run agent fulfils. Therepo: sp.GitRepoparameter is a writable workspace handle: the signature is the permission surface, and nothing else authorizes the write. (Usesp.May[sp.GitRepo, sp.ReadOnly]when a task should only read.)with sp.open(".") as workspace:opens the initialized workspace and closes it on exit. (On an uninitialized directory it raises — runshepherd initfirst.)workspace.tasks.register(write_note)registers the task object directly — no source strings, no separate id.workspace.run(write_note, repo=..., topic=..., ...)executes it as a retained run, passing the task's own arguments as keywords.provider: "static"is the deterministic offline provider, so this run is reproducible and free;"claude"runs a live sandboxed agent instead (needs theclaudeCLI and auth).- The work does not touch your files. It lands as a retained output; you
read its changeset and contents first, then settle it —
select(),apply(),release(), ordiscard()— exactly once.
Output¶
Executed against the shipped 0.3.0 release (this exact transcript is what the page's test asserts):
Inspect the record¶
Every run leaves a durable trace you can read back from the CLI:
shepherd run list
shepherd run show --latest
shepherd run trace --latest --events
shepherd run changeset --latest
You can also fetch this same demo in script form with
shepherd demo write quickstart > quickstart_demo.py, and a live-agent
variant with shepherd demo write agent-task (see the
README).
If it fails¶
WorkspaceControlErrorfromsp.open(".")— the directory is not an initialized Shepherd workspace. Runshepherd initthere first.- Ran the script twice in the same directory? The second run refuses at
settlement with
InvalidRepositoryStateError— the workspace already carries the first run's selected state, and re-settling an identical result fails closed. Start from a fresh directory when repeating the walkthrough. - Looking for
with sp.workspace(model=...): my_task(...)? That ambient direct-call shape is on the roadmap, not in this release — nothing serves that call yet, and a task that declares repository access refuses the ambient call outright (AmbientWorldAccessRefused), pointing you here. Retained runs, as above, are the shipped path.
Upgrading from 0.2.x¶
0.3.0 modernizes the task syntax; three changes matter when upgrading:
- One behavior removal. An unannotated parameter named
repois no longer treated as a workspace handle — handles are keyed on the annotation only. Annotate it (repo: sp.GitRepo) to keep it a handle, or pass an ordinary value throughargs={"repo": ...}whenrepois genuinely data. register_source(...)is no longer the way to define a task by hand. Write@sp.taskand register the function object:workspace.tasks.register(fn). The source-text form remains for machine-generated task code.- Task arguments pass as keywords.
workspace.run(task, repo=h, topic="x")replaces theargs={...}dict;args={...}still works and is the escape hatch when a task parameter shares a name with a run option.
Next¶
- Grant a task repo access — read-only / read-write grants per bound repository, enforced at the OS under a jailed placement.
- Concepts: Tasks — the mental model.
- Roadmap — what ships today vs. what is ahead.