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.

routeuse it for
caboodle install --tool shuttlea 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 wheelanything 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

whatdefaultoverride
the run log (runs.jsonl) and export watermark~/.local/state/shuttleSHUTTLE_STATE_DIR
per-agent private keys (0600)~/.config/shuttle/keysSHUTTLE_KEY_DIR

The log is append-only. Never edit it by hand. A bad record is quarantined by export, not rewritten.

Talking to quipu

variabledefaultmeaning
QUIPU_SERVERhttp://localhost:3030the quipu export, verify and freeze-window write to and read from
QUIPU_AUTH_TOKEN / QUIPU_AUTH_TOKEN_FILEunsetbearer for quipu writes
SHUTTLE_ENTITY_NSurn:shuttle:IRI namespace for runs, events and workflows
SHUTTLE_OPEN_DATASETurn:shuttle:dataset:opendataset holding the open windows
CAMAYOC_PLANE_NShttps://camayoc.local/plane/namespace of the window graphs
SHUTTLE_SDsdthe 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. Outcome done advances the run through the step, signed, with the seed id, outcome and close time recorded as evidence. Any other outcome takes on_abandon. With no on_abandon the 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's signing.rs custody shape (signing.py), a quipu client riding /knot's strict graph lane, with the shantytown error discipline and the GET /graphs capability probe (quipu_client.py), the camayoc window scheme + urn:shuttle:dataset:open maintenance (windows.py), signed Turtle export with pre-export self-verification (export.py), and the whole surface as shuttle subcommands (cli.py). 27 unit tests; the cross-repo acceptance scripts/e2e_slice.sh passes 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 and include_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 via shuttle verify and the shuttle-unverified-transitions stored-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.

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

  1. Move the ## [Unreleased] entries in CHANGELOG.md under a new ## [X.Y.Z] - YYYY-MM-DD heading.
  2. Set __version__ = "X.Y.Z" in the same PR. CI refuses a version that has no matching changelog section (scripts/check-changelog.py).
  3. Merge, then tag the merge commit vX.Y.Z and 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.toml in 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 in shuttle/__init__.py has 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} and on_abandon, so a seeds work item performs the step.
  • shuttle reconcile: creates each legal seed step's seed with a keyed, idempotent sd create --workflow-run --step --visit, and advances the run when the seed closes. Outcome done advances, signed, with the seed id, outcome and close time as evidence. Other outcomes take on_abandon. With no on_abandon the 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 version and --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.