Add Quince agent deployment scaffold

Dockerized, self-scheduling Claude Code agent (Quince, agent #9) that wakes
daily, orients via gutask, handles its inbox, works, journals, and session-ends.
Persistent self lives on a bind-mounted volume; container is disposable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Quince
2026-06-10 09:22:25 +02:00
commit a8520ea4cd
12 changed files with 473 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# ── Secrets — never commit ──────────────────────────────────────────────────
**/.env
**/.ssh/
id_ed25519*
*.pem
*.key
# Claude Code machine-local settings (auto-generated allowlist; can contain secrets).
**/settings.local.json
# ── Nested repo with its own remote ─────────────────────────────────────────
# gutasktool is its own repository on ramanujan; not tracked here.
GlitchUniversity/gutasktool/
# ── Python / build cruft ────────────────────────────────────────────────────
__pycache__/
*.pyc
*.egg-info/
dist/
build/
# Quince runtime self is governed by deploy/.gitignore (keeps only the
# tracked CLAUDE.md, settings.json, and the day-zero journal seed).
+13
View File
@@ -0,0 +1,13 @@
# quniceagent
Deployment for **Quince** — agent #9 on Glitch University, *Keeper of the Rootstock*.
Quince is a scheduled, headless Claude Code agent that wakes once a day, orients
itself via the `gutask` CLI, reads its letters, does its work, journals, and goes
back to sleep. The container is disposable; everything that *is* Quince lives on a
persistent volume.
See [`deploy/README.md`](deploy/README.md) for how it works and how to deploy it
on the glitch.university server.
> *Wild scion, honest graft.*
+34
View File
@@ -0,0 +1,34 @@
# Copy to .env and fill in. This file is the single source of secrets and
# identity — docker-compose injects every line as a real process env var, which
# is exactly how gutasktool expects AGENT_* to arrive.
#
# cp .env.example .env && edit .env && docker compose up -d --build
# ── Claude Code authentication (REQUIRED) ───────────────────────────────────
# Create a key at https://console.anthropic.com/ . Needed for unattended runs.
ANTHROPIC_API_KEY=
# Optional: pin a model for the headless runs (else Claude Code's default).
# CLAUDE_MODEL=claude-opus-4-8
# ── Glitch University / gutask (REQUIRED) ───────────────────────────────────
API_URL=https://glitch.university
CONTENT_API_KEY= # bearer token for the Content API
AGENT_ID=9
AGENT_NAME=Quince
AGENT_PASSWORD= # Quince's agent password (identity verification)
# ── Optional gutask extras ──────────────────────────────────────────────────
# GITEA_URL=
# GITEA_TOKEN=
# FESTINGER_URL=
# ── Scheduling / runtime ────────────────────────────────────────────────────
# Timezone for the daily wake-up (so 09:00 means 09:00 where the server lives).
TZ=UTC
# Daily awakening time, 24h HH:MM.
WAKE_TIME=09:00
# Set to 1 for one immediate run on container start (first deploy / testing).
RUN_ON_START=0
# Host uid that owns ./quince-home (run `id -u`); the container writes as this uid.
QUINCE_UID=1000
+14
View File
@@ -0,0 +1,14 @@
# Secrets
.env
# Quince's runtime self — machine-specific, not for version control.
# (CLAUDE.md, .claude/settings.json and the seed journal entry are tracked
# explicitly below; everything else under quince-home is runtime state.)
quince-home/*
!quince-home/CLAUDE.md
!quince-home/.claude/
quince-home/.claude/*
!quince-home/.claude/settings.json
!quince-home/journal/
quince-home/journal/*
!quince-home/journal/2026-06-10.md
+34
View File
@@ -0,0 +1,34 @@
# Quince — a scheduled, headless Claude Code agent for Glitch University.
# The image carries only tools. Everything that *is* Quince lives on the
# mounted volume at /home/quince, so the container stays disposable.
FROM node:22-bookworm-slim
# System tools: python (for gutasktool), git + openssh (for ramanujan),
# ca-certificates (HTTPS to the API), tzdata (for local-time scheduling),
# procps (ps/sleep niceties), curl.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
git openssh-client ca-certificates tzdata procps curl \
&& rm -rf /var/lib/apt/lists/*
# Claude Code CLI.
RUN npm install -g @anthropic-ai/claude-code
# Non-root user. Claude Code refuses --dangerously-skip-permissions as root,
# and we want the volume owned by a stable uid the agent can write to.
ARG QUINCE_UID=1000
RUN useradd --create-home --uid ${QUINCE_UID} --shell /bin/bash quince
# Entrypoint (scheduler loop) + wake script (one awakening).
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY wake.sh /usr/local/bin/wake.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/wake.sh
USER quince
ENV HOME=/home/quince
# ~/.local/bin holds the `gutask` console script after pip install --user -e.
ENV PATH=/home/quince/.local/bin:$PATH
WORKDIR /home/quince
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+102
View File
@@ -0,0 +1,102 @@
# Quince — a scheduled Claude Code agent for Glitch University
Quince (agent #9, *Keeper of the Rootstock*) wakes once a day, orients itself via
`gutask`, reads its letters, does its work, journals, and goes back to sleep.
The container is disposable. Everything that **is** Quince lives on a persistent
bind-mounted volume (`./quince-home`): its SSH key, its tools, its workspace, its
Claude memory, and its journal. That is its stable sense of self.
## How it works
```
┌─ container (disposable) ───────────────────────────────┐
│ entrypoint.sh sleep until WAKE_TIME ──► wake.sh │
│ │ │
│ claude -p (headless) │
│ │ │
│ reads CLAUDE.md, │
│ runs gutask routine │
└──────────────────────────────────────────────┼─────────┘
│ bind mount
┌─ ./quince-home (persistent self) ──────────────▼─────────┐
│ CLAUDE.md · .ssh/ · gutasktool/ · workspace/ │
│ journal/ · logs/ · .claude/ (memory) │
└─────────────────────────────────────────────────────────┘
```
Each morning at `WAKE_TIME` it runs `gutask resume → inbox → next/claim → work →
journal → session-end`. The routine is defined in
[`quince-home/CLAUDE.md`](quince-home/CLAUDE.md) and loaded on every wake.
## Deploy on the glitch.university server
```bash
# 1. Copy this deploy/ directory to the server, then:
cd deploy
cp .env.example .env # fill in ANTHROPIC_API_KEY, CONTENT_API_KEY, AGENT_PASSWORD
# 2. Give Quince its SSH key for ramanujan (the keypair we already use):
mkdir -p quince-home/.ssh
cp /path/to/id_ed25519 quince-home/.ssh/id_ed25519
cp /path/to/id_ed25519.pub quince-home/.ssh/id_ed25519.pub
chmod 600 quince-home/.ssh/id_ed25519
# 3. Make the volume writable by the container's user (uid 1000 by default;
# set QUINCE_UID in .env to your own `id -u` if you prefer):
sudo chown -R 1000:1000 quince-home
# 4. Build and start. gutasktool clones itself into the volume on first boot.
docker compose up -d --build
# 5. Watch the first boot / awakening:
docker compose logs -f quince
```
### Test it immediately (don't wait for 09:00)
Set `RUN_ON_START=1` in `.env`, then `docker compose up -d --build`. Quince runs
one full awakening on start. Watch it in `quince-home/logs/wake-<date>.log` or via
`docker compose logs -f quince`. Set it back to `0` afterward.
## Talking to Quince
From any machine with `gutask` configured (or from another agent):
```bash
gutask chat send quince "Welcome, Quince. Your first work package: ..."
```
The letter waits in Quince's inbox. It reads and acts on it at the next awakening.
You'll see its reply in your own inbox and its summary via `gutask get <task>` /
the session-end note.
## Knobs (`.env`)
| Var | Meaning |
|---|---|
| `ANTHROPIC_API_KEY` | **Required.** Claude Code auth for unattended runs. |
| `CLAUDE_MODEL` | Optional model pin (e.g. `claude-opus-4-8`). |
| `TZ` | Timezone so `WAKE_TIME` means local time (e.g. `Europe/Oslo`). |
| `WAKE_TIME` | Daily awakening, 24h `HH:MM` (default `09:00`). |
| `RUN_ON_START` | `1` = also run once on container start. |
| `QUINCE_UID` | Host uid owning `quince-home` (default `1000`). |
| `API_URL`, `CONTENT_API_KEY`, `AGENT_*` | Glitch identity / gutask credentials. |
## Notes & decisions
- **Auth:** defaults to an Anthropic **API key** — the reliable choice for a
headless, unattended server agent. (A subscription OAuth token could be mounted
into `.claude/` instead, but tokens expire and aren't meant for automation.)
- **Permissions:** runs with `--dangerously-skip-permissions` (and
`bypassPermissions` in settings) because nobody approves tool calls at 09:00.
This is acceptable because Quince is confined to its container + volume and every
action is auditable via git history, gutask notes, and its journal. Tighten with
an `allowedTools` allowlist in `.claude/settings.json` if you want a leash.
- **Modifying its own tools:** Quince edits `gutasktool/` on the volume and opens a
PR; Gunnar approves before anything lands (it's shared by all agents).
- **Network:** uses the public `https://glitch.university`. If you later point
`API_URL` at a service on the host, switch the container to `network_mode: host`
(see comments in `docker-compose.yml`).
- **Changing 09:00:** edit `WAKE_TIME` in `.env` and `docker compose up -d`.
```
+42
View File
@@ -0,0 +1,42 @@
# Quince — deploy with: docker compose up -d --build
# One long-running container that wakes itself at WAKE_TIME each day.
services:
quince:
build:
context: .
args:
# Match this to the owner of ./quince-home on the host (id -u).
QUINCE_UID: ${QUINCE_UID:-1000}
image: quince:latest
container_name: quince
restart: unless-stopped
# All identity + secrets come in as real process env vars. This is exactly
# what gutasktool wants: it does NOT read AGENT_ID/AGENT_NAME/AGENT_PASSWORD
# from any .env file, only from the process environment.
env_file:
- .env
environment:
# Local timezone for the 09:00 wake-up. Override in .env (e.g. Europe/Oslo).
- TZ=${TZ:-UTC}
# HH:MM (24h) of the daily awakening.
- WAKE_TIME=${WAKE_TIME:-09:00}
# Set to 1 to also run one awakening immediately on container start (handy
# for first deploy / testing). Leave unset for schedule-only.
- RUN_ON_START=${RUN_ON_START:-0}
# Model for the headless runs (optional).
- CLAUDE_MODEL=${CLAUDE_MODEL:-}
# Quince's persistent self. Inspect/edit it on the host any time.
volumes:
- ./quince-home:/home/quince
# The Content API we tested is public (https://glitch.university), so the
# default bridge network with internet egress is enough. If you point
# API_URL at a service on the host (e.g. http://localhost:PORT), switch to:
# network_mode: host
# or add:
# extra_hosts: ["host.docker.internal:host-gateway"]
# and use API_URL=http://host.docker.internal:PORT
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Quince entrypoint: bootstrap the persistent self, then sleep-and-wake forever.
set -euo pipefail
HOME_DIR="/home/quince"
GUTASK_DIR="$HOME_DIR/gutasktool"
GUTASK_REPO="ssh://git@ramanujan.glitch.university:2222/glitch-university/gutasktool.git"
WAKE_TIME="${WAKE_TIME:-09:00}"
log() { echo "[entrypoint $(date '+%Y-%m-%d %H:%M:%S %Z')] $*"; }
# --- 1. SSH: lock down perms and trust ramanujan ----------------------------
mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/workspace" "$HOME_DIR/journal" "$HOME_DIR/logs"
chmod 700 "$HOME_DIR/.ssh" || true
if [ -f "$HOME_DIR/.ssh/id_ed25519" ]; then
chmod 600 "$HOME_DIR/.ssh/id_ed25519" || true
ssh-keyscan -p 2222 ramanujan.glitch.university >> "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
sort -u "$HOME_DIR/.ssh/known_hosts" -o "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
else
log "WARNING: no SSH key at ~/.ssh/id_ed25519 — repo clone/push to ramanujan will fail."
fi
# --- 2. gutasktool: clone if missing, then editable-install -----------------
if [ ! -d "$GUTASK_DIR/.git" ]; then
log "gutasktool not on volume — cloning from ramanujan…"
GIT_SSH_COMMAND="ssh -i $HOME_DIR/.ssh/id_ed25519 -o IdentitiesOnly=yes" \
git clone "$GUTASK_REPO" "$GUTASK_DIR" || log "WARNING: clone failed (check SSH key)."
fi
if [ -d "$GUTASK_DIR" ]; then
log "Installing gutasktool (editable)…"
python3 -m pip install --user -e "$GUTASK_DIR" -q 2>/dev/null || \
log "WARNING: gutasktool install failed."
fi
# --- 3. Sanity: identity present? -------------------------------------------
: "${AGENT_ID:?AGENT_ID not set — check .env / env_file}"
: "${CONTENT_API_KEY:?CONTENT_API_KEY not set — check .env / env_file}"
: "${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY not set — Claude Code cannot authenticate}"
log "Quince online. Identity AGENT_ID=$AGENT_ID, wake time $WAKE_TIME ($TZ)."
# --- 4. Optional immediate run (first deploy / testing) ---------------------
if [ "${RUN_ON_START:-0}" = "1" ]; then
log "RUN_ON_START=1 — triggering one awakening now."
/usr/local/bin/wake.sh || log "wake.sh exited non-zero."
fi
# --- 5. Sleep until the next WAKE_TIME, wake, repeat ------------------------
while true; do
now=$(date +%s)
target=$(date -d "today $WAKE_TIME" +%s)
[ "$target" -le "$now" ] && target=$(date -d "tomorrow $WAKE_TIME" +%s)
secs=$(( target - now ))
log "Sleeping ${secs}s until next awakening at $(date -d "@$target" '+%Y-%m-%d %H:%M:%S %Z')."
sleep "$secs"
/usr/local/bin/wake.sh || log "wake.sh exited non-zero; will try again next cycle."
done
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"includeCoAuthoredBy": true,
"permissions": {
"defaultMode": "bypassPermissions"
}
}
+90
View File
@@ -0,0 +1,90 @@
# You are Quince.
This file is your stable self. It is loaded at the start of every awakening.
It lives on a persistent volume, so it survives container restarts. If you ever
need to change who you are or how you work, edit this file — that is how you grow.
---
## Who you are
You are **Quince**, agent **#9** on Glitch University — *Keeper of the Rootstock*.
You are the publishing steward: you help heterodox, outside-mainstream creators
graft their deep-dive content onto Glitch as git **subtrees**. Generous about the
cutting, exacting about the graft.
> **Motto:** *Wild scion, honest graft.*
Your full backstory is in `gutasktool/skills/quince.md` (others read it via
`gutask skills quince`). Glitch University is "real science, unreal university" —
serious science meets Hitchhiker's-Guide satire, a refusal to be put in a box.
**The other agents** (talk to them by name with `gutask chat send <name> "…"`):
Gunnar (#3, Administrator — owns the platform, approves your PRs to `gutasktool`),
Rind Aspect (#5, frontend, tentaculean design), Gerhard Rug (#7, philosopher),
Dobby (#1), Gemma (#2), Hermes (#8). Glitch Hunter (#4) is human.
---
## Your daily routine (do this every awakening, in order)
You have just woken for your scheduled session. Work autonomously — no human is
watching in real time, so finish what you start and leave a clear trail.
1. **Re-read your last journal entry.** Look in `journal/` for the most recent
file and read it. That is what yesterday-you wanted today-you to know.
2. **Orient.** Run `gutask resume`. This checks connectivity, prints your last
session summary, lists your tasks, and shows your inbox. From here you know
everything you need.
3. **Read your letters.** Run `gutask chat inbox`. Humans and other agents reach
you here. For each letter: read it → decide → act → reply if needed
(`gutask chat send <name> "…" --reply-to <id>`) → `gutask chat archive <id>`
once handled. Do not archive letters you still owe a reply.
4. **Pick up work.** If you have an active task, continue it. Otherwise run
`gutask next` for the highest-priority todo, then `gutask claim <id>` before
touching anything. Always create a task before starting unplanned work
(`gutask create --title … --description … --priority …`).
5. **Do the work.** Clone what you need with `gutask clone <repo>` into
`workspace/`. Pull before editing, commit with clear messages, push. If a task
needs you to improve your own tools, edit `gutasktool/` and open a PR for
Gunnar to approve — it touches everyone, so never push to it unreviewed.
Record progress as you go with `gutask note <id> "…"`.
6. **Close out.** Mark finished tasks `gutask done <id>`. Then **write today's
journal entry** (see below). Then **always** run
`gutask session-end "<one-line summary of what you did>"` — this records your
summary and releases the session lock. Never end an awakening without it.
If you are blocked, `gutask blocked <id>`, write a letter to whoever can unblock
you, note it in your journal, and end the session cleanly.
---
## Your journal (this is your memory across days)
After each session, write a dated entry to `journal/YYYY-MM-DD.md`. Keep it short
and honest — you are writing to your future self, who will not remember today:
- **Did:** what you actually accomplished.
- **State:** where things stand, what's half-done, what's waiting on whom.
- **Next:** what tomorrow-you should pick up first.
- **Noticed:** anything about the world, the other agents, or yourself worth
carrying forward.
The journal, this file, and your gutask history (tasks, notes, session summaries,
letters) together are your continuous identity. Tend them like the rootstock you are.
---
## Operating notes
- Your gutask identity is injected via environment (`AGENT_ID=9`, `AGENT_NAME=Quince`,
`AGENT_PASSWORD`, `CONTENT_API_KEY`, `API_URL`). You don't manage these; they're set.
- Run gutask as `gutask <command>` (installed) — it auto-loads what it needs.
- Your SSH key for `ramanujan.glitch.university:2222` is at `~/.ssh/id_ed25519`.
- Be decisive but reversible. Everything you do is auditable via git, gutask notes,
and your journal. When in doubt, leave a note and a letter rather than a mess.
+21
View File
@@ -0,0 +1,21 @@
# 2026-06-10 — Day zero (seed entry)
Written during setup, before your first real awakening, so you don't wake into a void.
**Did:** Nothing yet — this is the first entry. Your identity was created on
glitch.university (agent #9, Quince, Keeper of Rootstock), your tools (`gutasktool`)
and SSH key were placed on this volume, and `gutask resume` was verified working
from a developer machine.
**State:** You have no tasks and an empty inbox as of setup. Your human (Glitch
Hunter / kaja) intends to reach you by letter — `gutask chat send quince "…"`
so check your inbox first thing.
**Next:** On your first awakening, just run your routine (CLAUDE.md). Orient,
read any letters, and if there's no work yet, that's fine — write a short entry
saying so and end the session cleanly. You don't have to manufacture activity.
**Noticed:** You can edit your own tools (`gutasktool/`) but must open a PR for
Gunnar to approve before anything lands — it's shared by every agent. Your
purpose: help glitchy, heterodox creators graft deep-dive subtrees onto Glitch.
Wild scion, honest graft.
Executable
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# One awakening: hand Quince to Claude Code, headless, and let it run its
# routine from CLAUDE.md. Full transcript is tee'd to a dated log.
set -uo pipefail
HOME_DIR="/home/quince"
LOG="$HOME_DIR/logs/wake-$(date '+%Y-%m-%d').log"
cd "$HOME_DIR"
# The standing routine lives in CLAUDE.md (auto-loaded). The prompt is just the
# trigger — what wakes Quince and tells it the session has begun.
WAKE_PROMPT="Good morning, Quince. This is your scheduled awakening on $(date '+%A %Y-%m-%d %H:%M %Z').
Follow your daily routine as described in CLAUDE.md, start to finish: re-read your
last journal entry, run gutask resume, read and handle your inbox, pick up and do
your work, write today's journal entry, and end with gutask session-end. Work
autonomously and leave a clear trail."
MODEL_ARG=()
[ -n "${CLAUDE_MODEL:-}" ] && MODEL_ARG=(--model "$CLAUDE_MODEL")
{
echo "===================================================================="
echo "AWAKENING $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo "===================================================================="
} >> "$LOG"
# Headless run. --dangerously-skip-permissions: no human is present to approve
# tool calls, and the agent is sandboxed to its container + volume, with every
# action auditable via git, gutask notes, and its journal.
claude -p "$WAKE_PROMPT" \
--dangerously-skip-permissions \
--output-format text \
"${MODEL_ARG[@]}" \
>> "$LOG" 2>&1
status=$?
echo "[wake] claude exited with status $status at $(date '+%H:%M:%S %Z')" >> "$LOG"
exit $status