Sem descrição

clanker 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
src 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
test 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
usm-scripts 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
.gitignore 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
.usmignore 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
MANIFEST.usm 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
README.md 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás
meson.build 4cf9855680 feat: ClankerSaddle — Spry web app orchestrating saddle daemons with projects, tasks, dispatch, terminal viewer há 1 mês atrás

README.md

clanker-saddle

ClankerSaddle is an LLM task orchestration platform: a Statum + Spry web application that queues work for humans and dispatches it to saddle daemons — one-container agents that work on real git checkouts through bash sessions with full auditability. You design tasks during the day; the agent works while you sleep.

Architecture

Browser ←→ clanker-saddle (Spry app: SQLite queue, Statum UI, dispatcher)
                          ↕ HTTP (poll-based, 5s ticks)
              saddle daemons (Vala: bash session + LLM loop, port 7100)
                          ↕ curl
              any OpenAI-compatible chat-completions endpoint

The web app owns projects, tasks, dependencies and the saddle registry. Its dispatcher pairs ready tasks (dependencies done, priority first) with idle saddles, pushes each assignment to the daemon's POST /start (including the LLM endpoint config), then polls GET /output and GET /status every five seconds to mirror the agent's bash history into session_commands and to project its saddle comment/subtask/done events onto the task. Blocking questions flow back the same way: the daemon's question appears on the task page, and the human's answer is pushed to the daemon's POST /answer on the next tick.

Component Lives Role
Web app src/ pages, actions, services, dispatcher, saddle API
Saddle daemon ../saddle bash PTY-free session, LLM loop, control server
Mock LLM ../saddle/test/mock_llm.py scripted endpoint for testing

Orchestration behaviour to know:

Setting Meaning
SADDLE_LLM_BASE_URL LLM endpoint sent with every assignment; unset pauses dispatch
SADDLE_LLM_API_KEY / SADDLE_LLM_MODEL Optional credentials/model for the above
SADDLE_PROJECTS_ROOT Checkouts are addressed as root/<project-name> inside the saddle container (default /workspace)

Quick start (local)

export WS_PREFIX="$HOME/.local"
export PKG_CONFIG_PATH="$WS_PREFIX/lib64/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
export XDG_DATA_DIRS="$WS_PREFIX/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export LD_LIBRARY_PATH="$WS_PREFIX/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"

meson setup builddir && ninja -C builddir

Run the app, a saddle daemon and a mock LLM in three shells (the mock's port must be reachable from the daemon; project checkouts live under SADDLE_PROJECTS_ROOT):

export SPRY_SEED_ADMIN_USERNAME=admin SPRY_SEED_ADMIN_PASSWORD=change-me
export SADDLE_LLM_BASE_URL=http://127.0.0.1:9999 SADDLE_LLM_MODEL=mock
export SADDLE_PROJECTS_ROOT=/tmp/clanker-e2e/workspace
./builddir/clanker-saddle 8080

../saddle/builddir/src/saddle --port 7100

python3 ../saddle/test/mock_llm.py --port 9999

Browse to http://localhost:8080, log in with the seeded admin account, register the saddle on /saddles with host localhost:7100, and create your first project and task. spry dev wraps the first shell (rebuild + restart on save); web-config.json (from spry keys) keeps sessions alive across restarts and is never packaged or baked into images.

The database is ./clanker-saddle.sqlite, or SPRY_DB_PATH when set.

Docker setup

Two multi-stage Alpine images build everything from source against musl (see ../Dockerfile.saddle and ../Dockerfile.clanker-saddle):

podman build -f Dockerfile.clanker-saddle -t localhost/clanker-saddle:0.1 ..
podman build -f Dockerfile.saddle -t localhost/saddle:0.1 ..

podman network create clanker-net
podman run -d --name clanker-web --userns=keep-id --network clanker-net \
    -p 8080:8080 -v "$PWD/data:/var/lib/clanker-saddle:Z" \
    -e SPRY_SEED_ADMIN_USERNAME=admin -e SPRY_SEED_ADMIN_PASSWORD=change-me \
    -e SADDLE_LLM_BASE_URL=http://127.0.0.1:9999 \
    -e SADDLE_PROJECTS_ROOT=/workspace \
    localhost/clanker-saddle:0.1
podman run -d --name clanker-saddle --userns=keep-id --network clanker-net \
    -p 7100:7100 -v "$PWD/workspace:/workspace:Z" \
    localhost/saddle:0.1

Register the saddle with its network address (clanker-saddle:7100) — the web app talks to the daemon container-to-container, while the LLM base URL is dialled by the daemon itself (a mock can run inside the saddle container via podman exec -d clanker-saddle …, reachable at http://127.0.0.1:9999). Put the SQLite store on a volume (SPRY_DB_PATH defaults to /var/lib/clanker-saddle/clanker-saddle.sqlite) and mount git checkouts under /workspace. On SELinux hosts the bind mounts need :Z, and rootless podman needs --userns=keep-id for the mounts to be writable. For durable sessions mount web-config.json read-only and point ASTRALIS_CONFIG_PATH at it; otherwise the app runs with ephemeral keys.

Creating your first project and task

  1. Project — /projects → name, git URL, default branch. The checkout must exist at $SADDLE_PROJECTS_ROOT/<name> on the saddle's filesystem (clone it into the workspace volume; the agent does not clone). Link related projects as co-developed (agent may modify) or reference (read-only) from the project page.
  2. Task — /tasks → project, title, description, priority (1 is highest), optionally a design task (explore/plan/question only). Chain work with dependencies: a task waits until everything it depends on is done.
  3. Saddle — /saddles (admin) → name plus the daemon's host:port. Within five seconds the dispatcher assigns the highest-priority ready task to an idle saddle and the agent starts working.

Tasks can also arrive from the agent itself: saddle subtask lines create child tasks, and push-style daemons may report through POST /api/saddle/{id}/update (output, comment, block, subtask, done, status) and collect answers from GET /api/saddle/{id}/poll.

Monitoring agent progress

  • /tasks — live queue: status chips (queued → assigned → working → blocked → done/failed), dependency states and ready flags.
  • /tasks/{id} — the control room; it refreshes every three seconds while a saddle works: comments thread (yours and the agent's), blocks, subtasks, session history, and the agent's final summary.
  • /tasks/{id}/terminal — the full bash session log: every command the agent ran with its combined output and exit code, plus its commentary lines (# …) and saddle events.
  • / — dashboard: saddle cards, queue summary, recent activity.

Answering block questions

When an agent needs a human decision it runs saddle block "<question>"; the task flips to blocked and the question appears on its page with an answer form. Type the answer, submit — the dispatcher delivers it to the daemon within five seconds, the agent resumes with the answer in its conversation, and the task returns to working. Answers are delivered exactly once per question.

Integration testing

test/integration_test.py drives the whole loop against a running app + daemon pair: explore task, dependency gating, block + answer, and the audit trail (26 checks, Statum-style PASS/FAIL). Host run:

mkdir -p /tmp/clanker-e2e/workspace && cd /tmp/clanker-e2e/workspace \
    && git init demo && cd demo && git commit --allow-empty -m one \
    && git commit --allow-empty -m two

WEB=http://localhost:8080 SADDLE=http://localhost:7100 \
WORKSPACE=/tmp/clanker-e2e/workspace \
DB_PATH=/tmp/clanker-e2e/web/clanker-saddle.sqlite \
    python3 test/integration_test.py

For a container run point WEB/SADDLE at the published ports, pass SADDLE_HOST=clanker-saddle:7100, the container's SQLite file as DB_PATH, and start the in-container mock via MOCK_CMD='podman exec -d clanker-saddle python3 /usr/libexec/saddle/mock_llm.py …'.

Deployment (USM)

MANIFEST.usm + usm-scripts/ package the app as bin:clanker-saddle (usm manifest package), published to the Web-Stack repository:

usm manifest package
cd ../web-stack-repo && usm repository add ../clanker-saddle-0.1.usmc

web-config.json is never packaged (.usmignore); keys are mounted at runtime, never baked into images.