shuttle
The workflow engine of the quipu stack: every transition signed, every window frozen.
Shuttle has no daemon, no queue, no server and no dashboard. A local JSONL log is the hot path, and quipu is the governed record. Runs move through their workflow's declared steps. Every transition is signed by the agent that performed it (per-agent ed25519). The append-only history is exported into quipu's time-windowed operational graphs, where a completed window is deep-frozen whole into a read-only archive that stays queryable.
Consumers read the graph, never shuttle.
agents / crews (advance runs; each step signed)
│
▼
shuttle (state machine + JSONL outbox)
│ export: signed Turtle, idempotent batches
▼
quipu (windowed graphs · deep-frozen archives)
│
▼
shantytown · creel · NeuralAmplifier (read the graph, never shuttle)
- camayoc owns the vocabulary: the
workflow slice (
WorkflowRun,TransitionEvent, …). - caboodle installs and verifies shuttle as a stack member.
- seeds work items can perform a step (see Seed steps).
A run, end to end
shuttle keys init agent-a --workflow triage
# prints the public key and registration Turtle. A HUMAN loads it into the
# dataKind=identity graph; shuttle never registers its own keys.
shuttle define examples/triage.json
shuttle start triage run-1 --agent agent-a
shuttle advance run-1 --step claim --agent agent-a # signed
shuttle advance run-1 --step work --agent agent-a
shuttle advance run-1 --step finish --agent agent-a # terminal
shuttle status
shuttle export # drain the log into quipu windows
shuttle verify run-1 # re-verify signatures FROM the graph
shuttle freeze-window 2026-07 # archive a completed window
Everything up to status is local. export, verify and freeze-window
talk to quipu.
Install and operate
Install
Shuttle is a Python package (3.11+) with one dependency, cryptography, used
for ed25519 only. Each release publishes a wheel, an sdist and
checksums.txt.
| route | use it for |
|---|---|
caboodle install --tool shuttle | a stack box. Installs the release wheel whose SHA-256 caboodle was reviewed with, then caboodle verify --tool shuttle runs a real run round trip in a throwaway HOME. |
| the release wheel | anything else: check it against checksums.txt, then pipx install shuttle-<version>-py3-none-any.whl. |
pip install -e . | development. |
shuttle version prints one line, shuttle <version>. Deploy tooling parses
the second field, and CI asserts the shape, so treat it as an interface.
Where state lives
| what | default | override |
|---|---|---|
the run log (runs.jsonl) and export watermark | ~/.local/state/shuttle | SHUTTLE_STATE_DIR |
| per-agent private keys (0600) | ~/.config/shuttle/keys | SHUTTLE_KEY_DIR |
The log is append-only. Never edit it by hand. A bad record is quarantined
by export, not rewritten.
Talking to quipu
| variable | default | meaning |
|---|---|---|
QUIPU_SERVER | http://localhost:3030 | the quipu export, verify and freeze-window write to and read from |
QUIPU_AUTH_TOKEN / QUIPU_AUTH_TOKEN_FILE | unset | bearer for quipu writes |
SHUTTLE_ENTITY_NS | urn:shuttle: | IRI namespace for runs, events and workflows |
SHUTTLE_OPEN_DATASET | urn:shuttle:dataset:open | dataset holding the open windows |
CAMAYOC_PLANE_NS | https://camayoc.local/plane/ | namespace of the window graphs |
SHUTTLE_SD | sd | the seeds binary reconcile runs |
A SHACL refusal from quipu is reported as a refusal. Shuttle never treats it as a silent success.
Checking an install
shuttle version
caboodle verify --tool shuttle # define -> start -> advance -> status, hermetic
Seed steps
A transition can be performed by a seeds work item instead of by hand:
{"step": "review", "from": "open", "to": "reviewed",
"seed": {"title": "Review the change", "labels": ["review"]},
"on_abandon": "reject"}
shuttle reconcile --agent <you> (on a timer, or by hand) does two things for
every open run:
- For each seed step legal from the run's state, it runs
sd create --workflow-run <run> --step <step> --visit <n>. seeds derives the id from that key, so repeating the create returns the same seed. A run that re-enters the state gets a new visit and a new seed. - It reads the seed with
sd show --json. Outcomedoneadvances the run through the step, signed, with the seed id, outcome and close time recorded as evidence. Any other outcome takeson_abandon. With noon_abandonthe run is FLAGGED (exit 1) and never advanced silently.
seeds knows nothing about workflows beyond the link field. A missed tick costs
one tick, not a stuck run. SHUTTLE_SD names the sd binary; where seeds
stores its work is sd's own configuration.
Design: shuttle — signed runs, windowed export, freezable history
Implementation status (2026-08-24): ✅ v1 built: CLI + library, the full vertical slice. Pure state machine (
shuttle/model.py), append-only JSONL outbox with a forward-only export watermark (state.py), per-agent ed25519 signing in quipu'ssigning.rscustody shape (signing.py), a quipu client riding/knot's strict graph lane, with the shantytown error discipline and theGET /graphscapability probe (quipu_client.py), the camayoc window scheme +urn:shuttle:dataset:openmaintenance (windows.py), signed Turtle export with pre-export self-verification (export.py), and the whole surface asshuttlesubcommands (cli.py). 27 unit tests; the cross-repo acceptancescripts/e2e_slice.shpasses against quipu main (2026-08-24): keys → human registration → define → start → three signed advances → export → hot query + verify → freeze → identical rows via the frozen dataset andinclude_kinds→ signatures verifying against the frozen window. Deliberately NOT built (v1 deferrals, on the tracker): an HTTP/MCP server (agents invoke the CLI; quipu is the meeting point), quipu write-gate signature enforcement (quipu-8cc — unverifiable transitions are detectable from day one viashuttle verifyand theshuttle-unverified-transitionsstored-query pattern), key rotation ceremony, and a consumer path in NeuralAmplifier (its v1 stub exports).
Status: the stack had both halves of a workflow loop and no engine: shantytown pulls governed workflows from quipu's transaction log, and NeuralAmplifier pushes decision episodes in. Shuttle is the engine between them — it owns runs, signs every transition, and makes quipu the shared, governed, freezable record.
1. What shuttle is not
No daemon, no queue, no broker, no server. The hot path is a local
append-only JSONL log (the NeuralAmplifier DecisionLog precedent); export
drains it into quipu at-least-once with idempotent episode names. Consumers
never talk to shuttle — they read the graph. This keeps shantytown's
no-daemon doctrine intact and makes quipu the single meeting point.
2. Append-only, stated as a producer rule
Shantytown measured that quipu triple-level /retract deletes nothing, so
workflow state CANNOT be a mutated status field. Shuttle is append-only by
construction: aegis:TransitionEvents are the truth; aegis:currentState
is re-asserted per transition as a derived convenience read by latest
valid_from. The fold (model.fold_transitions) replays every event
through the same validator that admitted it, so a corrupt or reordered log
is a loud error, never a quietly wrong state.
3. Signing — agent-specific, freeze-proof
Every transition is signed by the performing agent's ed25519 key over the canonical message
shuttle-transition-v1|{run_iri}|{step}|{from}|{to}|{at}|{agent}
which is re-derivable from the exported facts alone. Custody mirrors quipu
src/signing.rs: host files, 0600, auto-generated, never regenerated.
Public keys are registered by a human as aegis:VerifierRegistration
facts in a dataKind=identity graph — never frozen, so signatures inside a
frozen window stay verifiable; shuttle never registers its own keys, keeping
the trust root human-owned.
Verification happens three times, deliberately: at advance (the signature
is created from validated inputs), at export (fail fast on key drift or a
tampered log — an unverifiable transition refuses to export), and at
shuttle verify (the consumer-side check, from the graph, against the
identity graph's registrations — works across cold composition unchanged).
4. Windows and freeze
Runs land in the window graph of the month they started
({ns}/window/shuttle/runs/{YYYY-MM}, the camayoc scheme, pinned by tests
in both repos), so freezing a window never splits a run. ensure_window
registers-and-labels or neither (operational/fresh/soleRecord), and
maintains urn:shuttle:dataset:open — the dataset consumers put in FROM,
because explicit scope is the fix for the silent-zero-rows hazard.
shuttle freeze-window calls quipu's hash-verified freeze and drops the
window from the open dataset; urn:quipu:dataset:frozen and
include_kinds:["archive"] carry it from there.
5. The import seam
shuttle import-run ingests a foreign transitions JSONL (NeuralAmplifier's
workflow_export shape) as a complete run, signed by the importing
agent — the importer attests the mapping, since the origin system never
signed shuttle messages. That distinction is honest and queryable:
competency Q6 asks who performed a transition, and for an imported run the
answer is the importer.
6. Related
- quipu
docs/design/graph-kinds-and-deep-freeze.md— the archive half. - camayoc
docs/design/workflow-and-archive.md+competency/workflow-and-archive.md— the vocabulary and the questions it owes. - shantytown
shuttle_runs— the consumer stub.
Releasing
The version is single-sourced from shuttle/__init__.py (__version__).
pyproject.toml reads it, so a tag, the built wheel and shuttle version
cannot disagree.
- Move the
## [Unreleased]entries inCHANGELOG.mdunder a new## [X.Y.Z] - YYYY-MM-DDheading. - Set
__version__ = "X.Y.Z"in the same PR. CI refuses a version that has no matching changelog section (scripts/check-changelog.py). - Merge, then tag the merge commit
vX.Y.Zand push the tag.
The release workflow then:
- refuses a tag that disagrees with
__version__; - runs the suite;
- builds the wheel and sdist, installs the wheel clean and checks it reports the tagged version;
- publishes the release with
checksums.txt, using that version's changelog section as the release notes.
After a release
- The unattended actuator on the fleet host installs the new release on its next tick (checksum-verified, command-tested, atomically promoted).
- caboodle installs only the version its member manifest pins. Move the pin
with
caboodle bump-member members/shuttle.tomlin a caboodle PR.
Changelog
All notable changes to shuttle. The format follows Keep a Changelog, and shuttle uses semantic versioning. Each release's notes are its section here; CI refuses a version with no section (see Releasing).
Unreleased
Added
CHANGELOG.md, and a CI check that the version inshuttle/__init__.pyhas a section here. The release lane publishes that section as the release notes.- An mdBook (
book.toml,docs/) published to GitHub Pages.
0.3.0 - 2026-10-01
Added
- Seed steps: a transition may declare
seed = {title, labels}andon_abandon, so a seeds work item performs the step. shuttle reconcile: creates each legal seed step's seed with a keyed, idempotentsd create --workflow-run --step --visit, and advances the run when the seed closes. Outcomedoneadvances, signed, with the seed id, outcome and close time as evidence. Other outcomes takeon_abandon. With noon_abandonthe run is flagged (exit 1), never advanced silently. Reconcile holds an exclusive lock on the state dir.
0.2.0 - 2026-09-05
The first version that can be deployed unattended.
Added
shuttle versionand--version, printing one parseable line (shuttle <version>). Deploy tooling reads it.- A tag-triggered release lane: it refuses a tag that disagrees with the
package, runs the suite, installs the built wheel clean and checks its
version, and publishes the wheel, sdist and
checksums.txt.
Changed
- The version is single-sourced from
shuttle/__init__.py.
Fixed
- Export declares each window's workflows in that window, so runs in a month after their workflow was defined are no longer refused by SHACL, and one bad record is quarantined instead of stopping the whole drain.
[0.1.0] - 2026-08-24
Never published as a release. The hand-installed first build: signed runs, windowed export into quipu, and freezable history.