From citadel
Runs campaigns autonomously 24/7 by chaining Claude Code sessions via RemoteTrigger or local loop, stopping on budget exhaustion or completion. For unattended overnight operation.
npx claudepluginhub sethgammon/citadel --plugin citadelThis skill uses the workspace's default tool permissions.
**Use when:** running campaigns overnight or unattended -- chains sessions automatically until a ceiling or budget is hit.
Decomposes large development campaigns into phases, delegates to sub-agents, reviews outputs, and maintains persistent state across multiple sessions.
Manages multi-session autonomous agent tasks with progress checkpointing, failure recovery, task dependencies, and commands like /harness init/run/status/add. For long-running workflows across context windows.
Orchestrates autonomous loops of session-start → session-plan → wave-executor → session-end with kill-switches (SPIRAL, failed waves, carryover, max-hours, confidence). User-invoked via /autopilot; reads Mode-Selector for auto-execute decisions.
Share bugs, ideas, or general feedback.
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).
/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.
/daemon start (no --remote flag)daemon.json).RemoteTrigger. Leave chainTriggerId
and watchdogTriggerId as null in the state file.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)
/daemon start --remoteOnly when the user has explicitly passed --remote:
RemoteTrigger,
which counts against your 15 routine runs / 24h quota. A single overnight
daemon can exhaust it. Continue? (y/N)"| 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. |
Step 1: Validate prerequisites
.planning/ exists. If not: "No planning directory found. Run /do setup first."--campaign {slug} provided: read .planning/campaigns/{slug}.md.planning/campaigns/ (excluding completed/) for files with
status: active in frontmatter/archon first."--campaign flag: list them, ask user to specify$50--budget unlimited: set budget to Infinity, warn: "No budget cap. You will not
be protected from runaway costs. Monitor usage at your Anthropic dashboard."--budget {N}: parse as number, must be > 0--cost-per-session {N} provided: use that valueestimated_cost_per_loop field in frontmatter
(improve campaigns set this to 12): use that value$3Step 2: Check for existing daemon
.planning/daemon.json if it existsstatus: "running"):
/daemon stop first, then continueStep 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:
{
"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.
.planning/daemon.json. If missing or not running: "No daemon is running."status: stopped, stoppedAt, stopReason: user.daemon-stop event. Output: sessions completed, estimated spend, campaign 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.
.planning/daemon.jsonlog array, most recent first, formatted as:
[{timestamp}] Session #{N}: {status} -- {summary}
Phase: {phase} | Duration: {duration} | Est. cost: ${cost}
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
.planning/daemon.json"running" and not "paused-level-up" -- exit silently. The daemon was stopped.
"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).lastTickAt is within the last 2 minutes and lastTickStatus is
"running" -- another session is active. Exit silently.estimatedSpend >= budget -- stop the daemon:
status: "stopped", stopReason: "budget-exhausted"daemon-stop with reason budget-exhaustedstatus: "stopped", stopReason: "no-active-work"daemon-stop with reason no-active-workstatus: completed or status: failed -- stop the daemon:
status: "stopped", stopReason: "campaign-{status}"daemon-stop with reason campaign-completed or campaign-failedstatus: parked -- stop the daemon:
stopReason: "campaign-parked"status: level-up-pending -- pause the daemon (do not stop):
status: "paused-level-up", pauseReason: "Improve hit distribution saturation. Human approval required for level-up proposals."daemon-pause with reason level-up-pending"Paused: level-up triggered. Approve proposals at .planning/rubrics/{target}-proposals.md and set campaign status to active to resume."Step 2: Acquire lock
Update daemon.json:
lastTickAt: current ISO timestamplastTickStatus: "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:
Step 4: Record session
After /do continue returns (or the session is winding down):
completed, failed, parked, or
the campaign file no longer exists -- stop the daemon immediately:
status: "stopped", stopReason: "no-active-work",
stoppedAt: "{ISO timestamp}"daemon-stop with reason no-active-worksessionCount: increment by 1estimatedSpend: add costPerSessionlastTickStatus: "completed"log array:
{
"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.
Same as /daemon tick but with an additional check at Step 1:
After the standard gate checks pass, check whether the chain is alive:
lastTickAt from daemon.jsonlastTickAt is more than 2 * interval ago AND lastTickStatus is not "running":
"Watchdog: chain appears dead. Last tick at {lastTickAt}. Restarting chain."lastTickAt is recent (within 2 * interval): the chain is healthy. Exit silently.The daemon's primary continuation mechanism is the init-project.js SessionStart hook,
not RemoteTrigger prompt injection. On every session start, the hook:
.planning/daemon.jsonstatus: running: checks the lock (no overlap), budget (can afford), and campaign (still active)[daemon] Active daemon detected. Campaign: {slug}. Run: /do continue/do continueRemoteTrigger'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.
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}
*/30 * * * * cd ~/project && claude -p '/do continue'.planning/: "Run /do setup first."/archon once interactively to establish it./daemon start fresh.2 * interval./daemon tick called manually: works, gate checks apply. Warn it's internal.--budget {higher}."level-up-pending, set paused-level-up, keep watchdog alive for human-resume detection./do Tier 1 stop. All write stopReason: no-active-work.Always disclose, regardless of trust level:
/daemon stop."/daemon stop, no work is lost--budget unlimited -- no automatic cost protectionRed actions (unlimited budget) require explicit confirmation at ALL trust levels.
Before starting, verify daemon is warranted:
improve and no rubric exists: block -- rubric requires human approval firstRead trust level from harness.json:
unlimited to bypass)start: confirmation output, no HANDOFFstop: stop summary, no HANDOFFtick: no user output (headless); updates daemon.json, schedules or stopsstatus/log: output requested info