|
|
1 月之前 | |
|---|---|---|
| src | 1 月之前 | |
| test | 1 月之前 | |
| usm-scripts | 1 月之前 | |
| .gitignore | 1 月之前 | |
| .usmignore | 1 月之前 | |
| MANIFEST.usm | 1 月之前 | |
| README.md | 1 月之前 | |
| meson.build | 1 月之前 |
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.
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) |
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.
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.
/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./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./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.
/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.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.
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 …'.
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.