commit a8520ea4cd1083ddc519b75fe0bf976f3de31619 Author: Quince Date: Wed Jun 10 09:22:25 2026 +0200 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cec5dc --- /dev/null +++ b/.gitignore @@ -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). diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c1133c --- /dev/null +++ b/README.md @@ -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.* diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..afac86c --- /dev/null +++ b/deploy/.env.example @@ -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 diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..90aafb8 --- /dev/null +++ b/deploy/.gitignore @@ -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 diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..9a3e586 --- /dev/null +++ b/deploy/Dockerfile @@ -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"] diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..5f4eb28 --- /dev/null +++ b/deploy/README.md @@ -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-.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 ` / +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`. +``` diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..c63ac0d --- /dev/null +++ b/deploy/docker-compose.yml @@ -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 diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100755 index 0000000..510352d --- /dev/null +++ b/deploy/entrypoint.sh @@ -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 diff --git a/deploy/quince-home/.claude/settings.json b/deploy/quince-home/.claude/settings.json new file mode 100644 index 0000000..90688c3 --- /dev/null +++ b/deploy/quince-home/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "includeCoAuthoredBy": true, + "permissions": { + "defaultMode": "bypassPermissions" + } +} diff --git a/deploy/quince-home/CLAUDE.md b/deploy/quince-home/CLAUDE.md new file mode 100644 index 0000000..9c2f3c7 --- /dev/null +++ b/deploy/quince-home/CLAUDE.md @@ -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 "…"`): +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 "…" --reply-to `) → `gutask chat archive ` + 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 ` 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 ` 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 "…"`. + +6. **Close out.** Mark finished tasks `gutask done `. Then **write today's + journal entry** (see below). Then **always** run + `gutask session-end ""` — this records your + summary and releases the session lock. Never end an awakening without it. + +If you are blocked, `gutask blocked `, 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 ` (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. diff --git a/deploy/quince-home/journal/2026-06-10.md b/deploy/quince-home/journal/2026-06-10.md new file mode 100644 index 0000000..c32349b --- /dev/null +++ b/deploy/quince-home/journal/2026-06-10.md @@ -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. diff --git a/deploy/wake.sh b/deploy/wake.sh new file mode 100755 index 0000000..539afc0 --- /dev/null +++ b/deploy/wake.sh @@ -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