Skip to main content

hausfold-scruff (Python SDK)

A thin Python client over the scruff binary — the worktree-lifecycle substrate for parallel coding agents. scruff has no daemon, so this SDK shells out to it (asyncio.create_subprocess_exec + --json, watch --json for a live NDJSON stream).

Async-first: watch() is naturally a stream. A sync script can still call every method via asyncio.run(...).

Import name is scruff; the package on PyPI is hausfold-scruff.

Install

pip install hausfold-scruff
# or: uv add hausfold-scruff

For local development against this repo instead: pip install -e sdk/python.

scruff itself must be on PATH, or pass ScruffClientOptions(bin="/path/to/scruff").

Two shapes of usage

Programmatic. Every ScruffClient method except the two ending in _interactive captures the child's stdout and returns — safe to call from a server with many concurrent sessions.

import asyncio
from scruff import ScruffClient

async def main() -> None:
    scruff = ScruffClient()

    envelope = await scruff.list()
    for lane in envelope.lanes:
        # occupied/dirty are `bool | None`: None means "not determined",
        # never coerce it to False.
        print(lane.name, lane.state, lane.occupied)

    # Create a lane WITHOUT attaching an agent to it — the primitive an
    # orchestrator wants. child/spawn only ever print the new path.
    lane_dir = await scruff.child("/path/to/some-repo", "task-42")
    # ...now launch YOUR OWN agent process against lane_dir.

asyncio.run(main())
# Live updates instead of polling — created/parked/resumed/reaped/changed.
async for line in scruff.watch():
    if line.kind == "created" and line.lane is not None:
        notify_ui(line.lane)

# Or scoped to the one lane this session holds — no hello/ready framing,
# and nothing about anybody else's lanes.
async for event in scruff.watch_lane(lane_dir):
    if event.kind == "reaped":
        end_session()

Interactive. new_interactive / resume_interactive inherit the calling process's stdio, so when scruff execs the configured agent client (claude, codex, opencode, pi) it takes over the real terminal — same as running scruff new by hand — and control returns to you when that session ends.

# A terminal app, run in an actual TTY:
await scruff.new_interactive("task-42")
# ... the agent owned the screen; you're back here when it exits.

Do not call new_interactive from a server — scruff new execs the agent client unconditionally, without checking for a TTY, so piped stdio blocks forever. Use resume() instead: it detects piped stdout and prints the reopen command as text rather than exec'ing.

Holding a session open: leases

scruff's sweep (reap) needs to know a checkout is in use. On a human's machine, lsof answers that; a server has no pane or shell cwd'd anywhere, so it says so itself with a lease:

lease = await scruff.lease(lane_dir)  # refreshes on an interval, < the 90s TTL
# ... serve the session ...
await lease.release()

Pass pid= instead when the lease should track a real local process — the OS then drops it the instant that pid dies, no refresh loop needed.

A lease can only save a lane from reap, never condemn one — "nobody leased it" isn't proof nobody's there.

scruff.lease(...) is a coroutine, so it can await the first heartbeat before returning: a failure to take the lease raises immediately instead of surfacing on the next refresh or release call.

watch() cleanup

watch() returns an async generator; stop consuming (break, or .aclose()) to kill the underlying process. Async generators aren't guaranteed to close promptly when they go out of scope — wrap long-lived use in contextlib.aclosing() to tear down the subprocess deterministically:

from contextlib import aclosing

async with aclosing(scruff.watch()) as stream:
    async for line in stream:
        ...

Types for a frontend

scruff.types has no runtime dependencies beyond the standard library. Import just the dataclasses if you're modeling the same wire shape elsewhere:

from scruff import ScruffLane, WatchEvent

What's NOT here yet

hook create/hook remove have no wrapper — shell out via run() if you need them. Types are hand-ported from the Go structs, not generated; if scruff's JSON shape drifts from this file, that's a bug here.

Testing

tests/fake-scruff.sh stands in for the real binary so tests don't need a Go build.

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
mypy src

Release files for hausfold-scruff 1.3.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hausfold-scruff 1.3.3
File Size Uploaded
hausfold_scruff-1.3.3.tar.gz 14.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hausfold-scruff 1.3.3
File Interpreter ABI Platform
hausfold_scruff-1.3.3-py3-none-any.whl Python 3 none any Details

Total release size: 30.1 kB

Release files / hausfold_scruff-1.3.3.tar.gz

Download URL hausfold_scruff-1.3.3.tar.gz
Size 14.3 kB
Tags Source
SHA-256 checksum
How to use checksums
ae9bedcd53a4105e520f00192bad170aa6380d89638204168b5454ad3a20f6cd
BLAKE2b-256 checksum
How to use checksums
2243a5268c7e09ae569c4fd3f82258d95aa15c639c0663b39e83c35670bee313
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / hausfold_scruff-1.3.3-py3-none-any.whl

Download URL hausfold_scruff-1.3.3-py3-none-any.whl
Size 15.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
179448547c48ff8cdeb70dc14f54fd51c9e703d4df6c9c7df0cc52aac57e7ce6
BLAKE2b-256 checksum
How to use checksums
aad343376bad47c54daac670e16a939aec349b559aec7a35d0a3c7afcba4da96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release history Release notifications | RSS feed

1.6.1

2 release files

1.6.0

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.7

2 release files

1.3.6

2 release files

1.3.5

2 release files

1.3.4

2 release files

This release

1.3.3 This release

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page