Cistern
Cistern is an agentic workflow orchestrator. It routes units of work called droplets through configurable pipelines called aqueducts, where each stage is a cataractae handled by an LLM-powered agent.
Vocabulary
| Term | Meaning |
|---|---|
| Droplet | Atomic unit of work — always say "droplet", never "task/item/ticket" |
| Cistern | The reservoir — droplets queue here before processing |
| Aqueduct | Named pipeline (e.g., virgo, marcia, julia, appia) |
| Cataractae | A stage within an aqueduct (implement → review → qa → delivery) |
| Castellarius | The overseer daemon — routes droplets, manages pipelines |
| Recirculate | Send a droplet back for revision |
| Drought | Idle state — maintenance hooks run here |
| Filtration | LLM refinement that sharpens a rough idea into well-specified droplets before they enter the pipeline |
Never say "drop/item/task/ticket/issue" for work units — always droplet.
Installation
- CLI:
~/go/bin/ct - Config:
~/.cistern/cistern.yaml - DB:
~/.cistern/cistern.db - Log:
~/.cistern/castellarius.log - Dashboard:
http://192.168.0.138:5737(ttyd) - Build:
cd ~/cistern && COMMIT=$(git rev-parse --short HEAD) && PATH="/usr/local/go/bin:$PATH" go build -ldflags "-X main.version=${COMMIT} -X main.commit=${COMMIT}" -o ~/go/bin/ct ./cmd/ct/
Repos configured
| Repo | Prefix | Aqueducts |
|---|---|---|
| cistern | ci- |
virgo, marcia |
| ScaledTest | st- |
julia, appia |
| PortfolioWebsite | pw- |
anio |
| lobsterdog | ld- |
claudia, palatinus, traiana |
Aqueduct configs live ONLY in ~/.cistern/cistern.yaml and ~/source/cistern/aqueduct/aqueduct.yaml. The git_sync drought hook copies the pipeline definition from each repo's origin/main into ~/.cistern/aqueduct/, overwriting local edits — but since lobsterdog, ScaledTest, and PortfolioWebsite don't have their own aqueduct.yaml, they all share the cistern repo's. Never create aqueduct/aqueduct.yaml in lobsterdog, ScaledTest, or PortfolioWebsite — it was specifically removed from lobsterdog (ca958e9). To add or change aqueduct names, edit only ~/.cistern/cistern.yaml (names, cataractae count, max_cataractae) and restart Castellarius.
Worktree Rule
Never edit ~/cistern directly. That's the primary clone — touching it corrupts all agent worktrees.
All manual Cistern work goes in the primary clone at ~/cistern:
cd ~/cistern
git checkout -B lobsterdog-work origin/main # Always sync before starting
ScaledTest worktree: ~/.cistern/sandboxes/ScaledTest/lobsterdog
⚠️ Do NOT use ~/.cistern/sandboxes/cistern/ for manual work — that entire directory is owned by the Castellarius pipeline. A lobsterdog worktree there was removed 2026-04-02; placing anything inside the sandbox root risks collision with the pipeline.
Pipeline
implement → review → qa → security-review → docs → delivery
| Priority | Meaning |
|---|---|
| 1 | Critical — maximum scrutiny (security review included) |
| 2 | Standard — default scrutiny |
| 3 | Low — minimal scrutiny |
Adding a Droplet
⛔ Always get explicit confirmation before filing any droplet.
Direct — when requirements are already clear:
ct droplet add \
--title "Short imperative description" \
--repo <repo-name> \
--priority 2 \
--description "What, why, acceptance criteria"
Filtered — for non-trivial or exploratory work:
Filtration is a thinking tool, not a filing tool. It refines ideas into clear specs — filing is always done separately with ct droplet add.
⚠️ Filtration pitfalls:
- Do not set provider API keys before running
ct filter— ct filter uses the opencode CLI, which manages its own credentials. Setting API keys in the environment may cause authentication conflicts. - Run
ct filterfrom~/cistern— it uses--allowedToolsto read codebase context. Running from another directory gives the agent no context. - Don't use
--descriptionfor long text — pass the title only; provide full context in the first interactive turn instead. --fileand--repoflags are removed — they no longer exist onct filter
Troubleshooting ct filter failures:
- "Session not found" error: Caused by
OPENCODE_SERVER_USERNAME,OPENCODE_SERVER_PASSWORD,OPENCODE_PID, orOPENCODEenv vars being set. These cause opencode to connect to an existing server session instead of creating a new one.ct filterautomatically unsets these vars in the subprocess environment. If runningopencode runmanually, unset them:unset OPENCODE_SERVER_USERNAME OPENCODE_SERVER_PASSWORD OPENCODE_PID OPENCODE - Empty response: The
opencode run --format jsoncommand must produce NDJSON output on stdout. If stderr shows errors but stdout is empty, check that--dangerously-skip-permissionsis included and the model is valid - Timeout (10 min default): Increase with
CT_FILTER_TIMEOUTenv var (in seconds):CT_FILTER_TIMEOUT=600 ct filter --title "..."
Step 1 — Start (from ~/cistern, no provider API keys exported):
cd ~/cistern
ct filter --title "Rough idea"
Step 2 — Resume with answers:
ct filter --resume <session-id> "answers and context..."
Step 3 — When spec is approved, file manually:
# File each droplet explicitly, wiring deps with --depends-on
ct droplet add --title "First droplet" --repo <repo> --priority 2 \
--description "..."
ct droplet add --title "Second droplet" --repo <repo> --priority 2 \
--description "..." --depends-on <first-id>
Rules:
- Never use
ct droplet add --filter— fires-and-forgets, no conversation ct filter --fileandct filter --repoare removed flags — they don't exist- Minimum 3 rounds. Keep going past 3 until the spec is unambiguous — every cataracta (implement, reviewer, QA, delivery) should be able to read CONTEXT.md and have the same understanding of what needs to change, with no guessing about scope, file locations, or acceptance criteria. Stop when the spec is concrete, not when the count hits a number.
- After each round, present the updated spec as a numbered list with dependencies stated in plain text (e.g. "Droplet 2 requires droplet 1 to be delivered first")
- After each session, give a recommendation: ready to file, or needs more passes? Say why.
- Get explicit "yes" before filing any droplet
- File follow-up droplets with
--depends-on <id>rather than injecting notes into flowing work
Telegram buttons during filtration
When running on Telegram (channel=telegram), use inline buttons at decision points instead of waiting for typed responses. Send buttons with:
CHAT_ID=8569372105
openclaw message send --channel telegram --target "$CHAT_ID" \
--message "<summary of current spec>" \
--buttons '<buttons-json>'
After each filtration round (present the numbered spec, then):
[ [{"text":"✅ Ready to file","callback_data":"filter:file"},
{"text":"🔄 Another round","callback_data":"filter:continue"}],
[{"text":"❌ Cancel","callback_data":"filter:cancel"}] ]
Button click responses arrive as callback_data: filter:file etc. Map them:
filter:file→ file each droplet manually withct droplet add, wiring--depends-onexplicitly; confirm each ID after filingfilter:continue→ ask what to refine, do another roundfilter:cancel→ confirm cancellation, do not file
Key Commands
# Status
ct status
ct droplet list
ct droplet list --repo <repo>
ct droplet show <id>
# Manage
ct droplet restart <id> # Restart from current cataractae
ct droplet restart <id> --cataractae delivery # Re-enter at a specific cataractae
ct droplet pool <id>
ct droplet cancel <id> --reason "..."
ct droplet note <id> "..."
ct droplet deps <id> --add <dep-id>
# Daemon
ct castellarius start/status
journalctl --user -u cistern-castellarius -f
systemctl --user restart cistern-castellarius
# Cataractae
ct cataractae list
ct cataractae generate
# Dashboard (restart after rebuild)
systemctl --user restart cistern-web.service
Infrastructure
- Castellarius: systemd user service
cistern-castellarius.service(Restart=always) - Auth: Opencode CLI manages its own credentials — no provider API keys needed in the service environment
start-castellarius.shjust runsexec ct castellarius start— no credential setup- Web dashboard: port 5737, managed by
cistern-web.service - Self-restart: git_sync drought hook + binary mtime detection → os.Exit(0) → systemd restarts
Troubleshooting
Run ct doctor first — it checks daemon status and common config issues automatically.
| Symptom | Check |
|---|---|
| Castellarius not running | systemctl --user status cistern-castellarius → start it |
| Castellarius hung/log quiet | ct doctor flags it automatically; manual check: cat ~/.cistern/castellarius.health (shows last tick time + poll interval) |
| Sessions failing auth | ct doctor checks agent CLI auth automatically; also verify environment variables are correct for the configured provider |
| Droplet stuck | ct droplet show <id> — check notes; ct droplet restart <id> |
| Logs | journalctl --user -u cistern-castellarius -f or cat ~/.cistern/castellarius.log |
| Dashboard stale after rebuild | systemctl --user restart cistern-web.service |
| Binary out of date | Rebuild: cd ~/cistern && COMMIT=$(git rev-parse --short HEAD) && PATH="/usr/local/go/bin:$PATH" go build -ldflags "-X main.version=${COMMIT} -X main.commit=${COMMIT}" -o ~/go/bin/ct ./cmd/ct/ |