daemon
$
npx mdskill add SethGammon/Citadel/daemonRuns campaigns continuously 24/7 using chained Code sessions
- Automates long-running tasks without manual intervention
- Uses RemoteTrigger for session chaining and local runner by default
- Resumes from saved state until context limits or budget are reached
- Schedules next session automatically or stops on completion
SKILL.md
.github/skills/daemonView on GitHub ↗
---
name: daemon
description: >-
Continuous autonomous operation mode. Keeps campaigns running 24/7 by
chaining Claude Code sessions via RemoteTrigger. Each session picks up
from the campaign's continuation state, works until context runs low or
the phase completes, then schedules the next session. Auto-stops on
campaign completion or budget exhaustion. The thing that makes Citadel
run overnight.
user-invocable: true
auto-trigger: false
last-updated: 2026-03-28
---
# /daemon -- Continuous Autonomous Operation
## Orientation
**Use when:** running campaigns overnight or unattended -- chains sessions automatically until a ceiling or budget is hit.
**Don't use when:** a single autonomous session is enough (use /archon); you want manual control between cycles (use /loop).
## Default execution path (READ FIRST)
**`/daemon start` does NOT call `RemoteTrigger` by default.** The local
runner is the default. Only pass `--remote` to use Anthropic's routine
system, and only after explicit user confirmation.
**Why:** `RemoteTrigger` counts against the account-wide **15 routine runs /
24h** cap. A single overnight run can exhaust the quota and pause every other
routine on the account (including unrelated ones). See
[docs/ROUTINE-QUOTA.md](../../docs/ROUTINE-QUOTA.md).
### Default flow — `/daemon start` (no `--remote` flag)
1. Do Steps 1, 2, and 4 below (validate, check existing, write `daemon.json`).
2. **Skip Step 3** — do NOT create any `RemoteTrigger`. Leave `chainTriggerId`
and `watchdogTriggerId` as `null` in the state file.
3. Instead of Step 5's trigger-confirmation, output:
```
Daemon state created: .planning/daemon.json
Campaign: {slug}
Budget: ${N}
To start the tick loop, run in a separate terminal:
npm run daemon:local
Leave that terminal open. It spawns `claude -p "/do continue"` each
session, respects daemon.json status, and consumes zero Anthropic
routine quota. Stop with Ctrl+C or `/daemon stop`.
For true unattended background operation (machine sleeps, user away):
/daemon start --remote (uses RemoteTrigger, counts against 15/day cap)
```
### Opt-in routine flow — `/daemon start --remote`
Only when the user has explicitly passed `--remote`:
1. Before proceeding, confirm: "This will use Anthropic's `RemoteTrigger`,
which counts against your 15 routine runs / 24h quota. A single overnight
daemon can exhaust it. Continue? (y/N)"
2. If the user confirms, run the full Step 1–5 protocol below (including
Step 3's trigger creation).
## Commands
| Command | Behavior |
|---|---|
| `/daemon start` | Default: create state file, prompt user to run `npm run daemon:local` (zero routine cost) |
| `/daemon start --remote` | Use `RemoteTrigger` instead (counts against 15/day routine quota — requires confirmation) |
| `/daemon start --campaign {slug}` | Target a specific campaign |
| `/daemon start --budget {N}` | Set budget cap in dollars (default: $50) |
| `/daemon start --budget unlimited` | Explicitly disable budget cap |
| `/daemon start --interval {N}m` | Set watchdog interval (default: 30m) |
| `/daemon start --cooldown {N}s` | Set delay between sessions (default: 60s) |
| `/daemon start --cost-per-session {N}` | Override per-session cost estimate (default: $3) |
| `/daemon stop` | Stop the daemon, tear down triggers |
| `/daemon status` | Show daemon state, session count, budget remaining |
| `/daemon log` | Show recent daemon session history |
| `/daemon tick` | Internal: heartbeat handler fired by triggers. Not user-facing. |
## Protocol
### /daemon start
**Step 1: Validate prerequisites**
1. Check `.planning/` exists. If not: "No planning directory found. Run `/do setup` first."
2. Find the target campaign:
- If `--campaign {slug}` provided: read `.planning/campaigns/{slug}.md`
- Otherwise: scan `.planning/campaigns/` (excluding `completed/`) for files with
`status: active` in frontmatter
- If no active campaign found: "No active campaign. Start one with `/archon` first."
- If multiple active campaigns and no `--campaign` flag: list them, ask user to specify
3. Verify the campaign has a Continuation State section (Archon knows where to resume)
4. Parse budget:
- Default: `$50`
- If `--budget unlimited`: set budget to `Infinity`, warn: "No budget cap. You will not
be protected from runaway costs. Monitor usage at your Anthropic dashboard."
- If `--budget {N}`: parse as number, must be > 0
5. Parse cost-per-session:
- If `--cost-per-session {N}` provided: use that value
- If not provided AND the campaign has an `estimated_cost_per_loop` field in frontmatter
(improve campaigns set this to 12): use that value
- Otherwise: default `$3`
- This auto-read prevents the common mistake of running an improve campaign
(which spawns 3 evaluator agents + attack + verify per loop) with the $3
default designed for simple archon sessions
**Step 2: Check for existing daemon**
1. Read `.planning/daemon.json` if it exists
2. If a daemon is already running (`status: "running"`):
- Show its state: campaign, sessions completed, budget remaining
- Ask: "A daemon is already running. Stop it and start a new one?"
- If yes: run `/daemon stop` first, then continue
- If no: abort
**Step 3: Create triggers**
**A. Chain trigger** — one-shot, fires after cooldown, `command: "/daemon tick"`. Save ID as `chainTriggerId`.
**B. Watchdog trigger** — recurring, fires every `--interval`, `command: "/daemon tick --watchdog"`. Save ID as `watchdogTriggerId`.
Both use `type: scheduled/recurring`, `project_path: {absolute project root}`, `description: "Daemon: {slug} tick/watchdog"`.
**Step 4: Write state file**
Write `.planning/daemon.json`:
```json
{
"status": "running",
"campaignSlug": "{slug}",
"budget": 50,
"costPerSession": 3,
"estimatedSpend": 0,
"sessionCount": 0,
"interval": "30m",
"cooldown": "60s",
"chainTriggerId": "{id from step 3A}",
"watchdogTriggerId": "{id from step 3B}",
"startedAt": "{ISO timestamp}",
"lastTickAt": null,
"lastTickStatus": null,
"stoppedAt": null,
"stopReason": null,
"log": []
}
```
**Step 5: Log and confirm**
Log: `daemon-start` event with budget and interval. Output confirmation: campaign slug, budget (estimated sessions), cooldown, watchdog interval, state file path. Suggest `/daemon status` and `/daemon stop`.
---
### /daemon stop
1. Read `.planning/daemon.json`. If missing or not `running`: "No daemon is running."
2. Delete both triggers (ignore failures — may already be cleaned up).
3. Update daemon.json: `status: stopped`, `stoppedAt`, `stopReason: user`.
4. Log `daemon-stop` event. Output: sessions completed, estimated spend, campaign status.
---
### /daemon status
Output: status, campaign (slug + phase), sessions, budget (spent/cap/remaining), cost/session source, last tick (time + status), running duration, watchdog interval, state file path.
If `paused-level-up`: add instructions to review proposals at `.planning/rubrics/{target}-proposals.md` and set campaign `status: active` to resume.
For improve campaigns: add loops completed/total, current level, last axis attacked.
---
### /daemon log
1. Read `.planning/daemon.json`
2. Output the `log` array, most recent first, formatted as:
```
[{timestamp}] Session #{N}: {status} -- {summary}
Phase: {phase} | Duration: {duration} | Est. cost: ${cost}
```
3. Show the last 20 entries. If more exist: "Showing last 20 of {total}. Full log in .planning/daemon.json"
---
### /daemon tick
**This is the heartbeat handler. It runs in a fresh Claude Code session spawned
by RemoteTrigger. It is not user-facing.**
**Step 1: Gate checks**
1. Read `.planning/daemon.json`
2. **Status gate**: If status is not `"running"` and not `"paused-level-up"` -- exit silently. The daemon was stopped.
- If status is `"paused-level-up"`: read the campaign file. If campaign status is now
`active` (human approved the level-up), update daemon.json `status: "running"`,
clear `pauseReason`, log `daemon-resume` with reason `level-up-approved`, and
continue to Step 2 (acquire lock). If campaign is still `level-up-pending`: exit
silently (still waiting for human).
3. **Lock gate**: If `lastTickAt` is within the last 2 minutes and `lastTickStatus` is
`"running"` -- another session is active. Exit silently.
4. **Budget gate**: If `estimatedSpend >= budget` -- stop the daemon:
- Update daemon.json: `status: "stopped"`, `stopReason: "budget-exhausted"`
- Delete both triggers (RemoteTrigger delete)
- Log: `daemon-stop` with reason `budget-exhausted`
- Exit.
5. **Campaign gate**: Read the campaign file.
- If the campaign file does not exist -- stop the daemon:
- Update daemon.json: `status: "stopped"`, `stopReason: "no-active-work"`
- Delete both triggers
- Log: `daemon-stop` with reason `no-active-work`
- Exit.
- If `status: completed` or `status: failed` -- stop the daemon:
- Update daemon.json: `status: "stopped"`, `stopReason: "campaign-{status}"`
- Delete both triggers
- Log: `daemon-stop` with reason `campaign-completed` or `campaign-failed`
- Exit.
- If `status: parked` -- stop the daemon:
- Same as above with `stopReason: "campaign-parked"`
- Exit.
- If `status: level-up-pending` -- **pause** the daemon (do not stop):
- Update daemon.json: `status: "paused-level-up"`, `pauseReason: "Improve hit distribution saturation. Human approval required for level-up proposals."`
- Do NOT delete triggers (the watchdog stays alive to detect when the human resumes)
- Log: `daemon-pause` with reason `level-up-pending`
- Append to daemon.json log: `"Paused: level-up triggered. Approve proposals at .planning/rubrics/{target}-proposals.md and set campaign status to active to resume."`
- Exit.
**Step 2: Acquire lock**
Update daemon.json:
- `lastTickAt`: current ISO timestamp
- `lastTickStatus`: `"running"`
**Step 3: Execute**
Run `/do continue` -- this routes to Archon, which reads the campaign's Continuation
State and picks up where the last session left off.
Archon will work until:
- The current phase completes (normal exit)
- Context runs low and PreCompact fires (saves state, session can end)
- An error parks the campaign
**Step 4: Record session**
After `/do continue` returns (or the session is winding down):
1. Read the campaign file again to get updated status and phase
2. **No-work gate**: If the campaign status is `completed`, `failed`, `parked`, or
the campaign file no longer exists -- stop the daemon immediately:
- Update daemon.json: `status: "stopped"`, `stopReason: "no-active-work"`,
`stoppedAt: "{ISO timestamp}"`
- Delete both triggers (RemoteTrigger delete)
- Log: `daemon-stop` with reason `no-active-work`
- Do NOT schedule the next tick. Exit after recording the session.
3. Update daemon.json:
- `sessionCount`: increment by 1
- `estimatedSpend`: add `costPerSession`
- `lastTickStatus`: `"completed"`
- Append to `log` array:
```json
{
"session": {sessionCount},
"timestamp": "{ISO timestamp}",
"status": "completed",
"phase": "{current_phase}",
"summary": "{brief description of what happened}",
"estimatedCost": {costPerSession}
}
```
**Step 5: Schedule next tick**
Re-read daemon.json. If still `running` and `estimatedSpend + costPerSession <= budget`: create new chain trigger (one-shot, cooldown delay), update `chainTriggerId`. If budget would be exceeded: stop daemon (`budget-exhausted`), delete watchdog, log `daemon-stop`.
**Step 6: Exit**
Session ends cleanly. PreCompact hook saves campaign state. The next tick
will start a fresh session with full context budget.
---
### /daemon tick --watchdog
Same as `/daemon tick` but with an additional check at Step 1:
After the standard gate checks pass, check whether the chain is alive:
- Read `lastTickAt` from daemon.json
- If `lastTickAt` is more than `2 * interval` ago AND `lastTickStatus` is not `"running"`:
- The chain died. Log: `"Watchdog: chain appears dead. Last tick at {lastTickAt}. Restarting chain."`
- Proceed with Step 2 onwards (this watchdog tick becomes a chain tick)
- Schedule the next chain tick in Step 5
- If `lastTickAt` is recent (within `2 * interval`): the chain is healthy. Exit silently.
---
## SessionStart Hook Bridge (Primary Bootstrap)
The daemon's primary continuation mechanism is the `init-project.js` SessionStart hook,
not RemoteTrigger prompt injection. On every session start, the hook:
1. Reads `.planning/daemon.json`
2. If `status: running`: checks the lock (no overlap), budget (can afford), and campaign (still active)
3. If all gates pass: outputs `[daemon] Active daemon detected. Campaign: {slug}. Run: /do continue`
4. The agent sees this message first and executes `/do continue`
RemoteTrigger's role is reduced to scheduling session starts. The hook handles everything
else. If RemoteTrigger is unavailable, an OS cron job or manual restart achieves the same result.
---
## Budget Tracking
**Primary:** Read latest entry from `.planning/telemetry/session-costs.jsonl` (written by session-end hook) for real cost. Use `override_cost` if present, else `estimated_cost`.
**Fallback:** `costPerSession` flat estimate (default $3). Each tick adds it to `estimatedSpend`.
Stop when `estimatedSpend >= budget` or `estimatedSpend + costPerSession > budget` (preemptive).
Overrides: `--budget {N}` | `--budget unlimited` (explicit, warns) | `--cost-per-session {N}`
---
## Fringe Cases
- **RemoteTrigger unavailable**: SessionStart hook bridge still works. Suggest OS cron: `*/30 * * * * cd ~/project && claude -p '/do continue'`
- **No `.planning/`**: "Run `/do setup` first."
- **Campaign has no Continuation State**: run `/archon` once interactively to establish it.
- **daemon.json corrupted**: treat as no daemon running; `/daemon start` fresh.
- **Session crashes without next tick**: watchdog restarts chain after `2 * interval`.
- **Multiple daemons**: one per project. Block with "daemon already running" prompt.
- **`/daemon tick` called manually**: works, gate checks apply. Warn it's internal.
- **Budget exhausted**: stop, log "Budget exhausted. Restart with `--budget {higher}`."
- **Level-up during run**: detect `level-up-pending`, set `paused-level-up`, keep watchdog alive for human-resume detection.
- **Campaign completes mid-session**: no-work gate (Step 4) catches it, stops daemon.
- **Idle loop bug (campaign done but daemon still running)**: three layers prevent it — campaign gate (Step 1), no-work gate (Step 4), `/do` Tier 1 stop. All write `stopReason: no-active-work`.
---
## Contextual Gates
### Disclosure
Always disclose, regardless of trust level:
- "Starting continuous mode on campaign {slug}. Budget: ${N} (~{sessions} sessions at ${cost}/session). Sessions restart automatically until done or budget exhausted."
- For unlimited budget: "WARNING: No budget cap. Sessions will continue until the campaign completes or you run `/daemon stop`."
### Reversibility
- **Amber:** Standard daemon with budget cap -- stop with `/daemon stop`, no work is lost
- **Red:** Daemon with `--budget unlimited` -- no automatic cost protection
Red actions (unlimited budget) require explicit confirmation at ALL trust levels.
### Proportionality
Before starting, verify daemon is warranted:
- If campaign has only 1 remaining phase: suggest running it directly instead
- If estimated sessions <= 2: suggest manual continuation instead
- If campaign is type `improve` and no rubric exists: block -- rubric requires human approval first
### Trust Gating
Read trust level from `harness.json`:
- **Novice** (0-4 sessions): Block daemon activation entirely. Output: "Daemon mode requires familiarity with the harness. Complete a few sessions first, then daemon will be available."
- **Familiar** (5-19 sessions): Allow with full disclosure and explicit confirmation.
- **Trusted** (20+ sessions): Allow with cost-only confirmation.
## Quality Gates
- Budget cap MUST be set (default $50, explicit `unlimited` to bypass)
- Daemon state file MUST be written before any triggers are created
- Both triggers (chain + watchdog) must be created; if either fails, abort and clean up
- Every tick must update daemon.json BEFORE scheduling the next tick
- Campaign must have Continuation State before daemon can start
- Lock mechanism must prevent overlapping sessions
- Watchdog must detect and recover from dead chains
- Stop must clean up ALL triggers (no orphaned triggers)
## Exit Protocol
- `start`: confirmation output, no HANDOFF
- `stop`: stop summary, no HANDOFF
- `tick`: no user output (headless); updates daemon.json, schedules or stops
- `status`/`log`: output requested info
- On error: actionable message, clean up any dangling triggers before exiting
More from SethGammon/Citadel