plugins/claude-code-hermit/skills/hermit-routines/SKILL.md
Schedules routines via one persistent Monitor subprocess (zero-token skips); CronCreate fallback where Monitor is unavailable. heartbeat-restart stays a CronCreate re-arm anchor.
npx skillsauth add gtapps/claude-code-hermit hermit-routinesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Register and manage scheduled routines. Where the Monitor tool is available, all enabled routines except heartbeat-restart run from ONE persistent Monitor subprocess that decides eligibility outside the session — a skipped fire costs zero model tokens. heartbeat-restart stays a CronCreate re-arm anchor: its daily fire re-invokes load (re-arming the monitor) and its skill re-arms the heartbeat monitor. Where Monitor is unavailable (Bedrock/Google Cloud Agent Platform/Foundry, DISABLE_TELEMETRY/CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC), load falls back to per-routine CronCreates.
/claude-code-hermit:hermit-routines load register/reconcile: monitor mode if available, else CronCreate diff-register
/claude-code-hermit:hermit-routines load --reset unconditional reset: tear down + recreate everything
/claude-code-hermit:hermit-routines run <ids> [internal] ROUTINE_DUE handler — invoked by the monitor's notification
/claude-code-hermit:hermit-routines list list configured routines from config.json
/claude-code-hermit:hermit-routines status show monitor/anchor state (or CronCreate registrations in fallback mode)
/claude-code-hermit:hermit-routines stop [id] stop the monitor (or a specific fallback-mode CronCreate)
/claude-code-hermit:hermit-routines stop --all stop everything
Called automatically by hermit-start.ts on always-on launches. Can also be called manually to apply config changes mid-session.
Resolve the plugin root path: derive it from this skill's Base directory, which the harness injects into the invocation context as <plugin_root>/skills/hermit-routines. Strip the trailing /skills/hermit-routines to get pluginRoot. This works in both installed and --plugin-dir modes. ($CLAUDE_PLUGIN_ROOT is NOT a Bash env var at runtime — evaluating it in Bash always returns empty. The braced ${CLAUDE_PLUGIN_ROOT} form is text-substituted in skill markdown only in installed mode. Neither is reliable here — always use the Base-directory derivation.) The resolved pluginRoot must be baked into the Monitor command and into any CronCreate-delivered prompt at registration — it is not available inside either subprocess or cron-delivered prompts.
Validate pluginRoot before proceeding. If pluginRoot is empty, or either of <pluginRoot>/scripts/routines.ts (the log-event/precheck/cron-registry verbs all live in it) or <pluginRoot>/scripts/routine-monitor.sh does not exist (test -f — pass the bare file path, never a path with a verb appended), abort load immediately — do not register/delete anything — and log one line: Routine load aborted: plugin scripts not found at "<pluginRoot>". No routines registered or reset.
Read config.routines, filter enabled: true, split into the anchor (id === "heartbeat-restart") and everyone else ("scheduled routines").
Empty scheduled-routine set does not early-exit — an operator disabling every routine mid-session must not leave a live monitor/anchor/crons behind. Reconcile: TaskStop any routine-monitor task (ignore not-found), delete state/routine-monitor-liveness.json and state/routine-monitor.runtime.json, run the anchor-only planner leg (Step 3's --ids heartbeat-restart, which with nothing else enabled emits DELETE: for every tracked id including the anchor if it too is disabled) and execute its deletes, then log "No enabled routines in config." and stop.
Monitor path (tried first, unless a prior load in this deployment already fell back — see Step 3-F note):
state/routine-monitor.runtime.json; if it has a task_id, TaskStop it (ignore not-found). TaskList → TaskStop any remaining task with description routine-monitor (orphan sweep). Delete state/routine-monitor-liveness.json. Do NOT delete state/routine-schedule.json on an ordinary re-arm — the anchor invokes load daily, and wiping the cursor would silently lose any pending mark, including one deferred by an in-progress session. Delete it only on a first transition into monitor mode (runtime file was absent, or mode was croncreate-fallback) or on load --reset (below).<abs-project-dir> as the session's current absolute working directory (the project root), then call the Monitor tool — description: "routine-monitor" (reserved slot), command: "bash <pluginRoot>/scripts/routine-monitor.sh 60 <abs-project-dir>/.claude-code-hermit", timeout_ms: 86400000 (schema-required boilerplate on a persistent monitor — it does not expire on this deadline; the daily anchor re-arm exists to recover from monitor death and session restarts, not a timeout), persistent: true. The state path must be absolute; $PWD triggers Claude Code's simple_expansion approval even when the shipped command permission is present.state/routine-monitor-liveness.json exists. Absent → the subprocess likely failed to spawn (seccomp/nested-userns) — TaskStop the task just registered and go to Step 3-F.state/routine-monitor.runtime.json: {"description":"routine-monitor","task_id":"<id>","command":"<cmd>","interval":60,"started_at":"<ISO now>","mode":"monitor"}. If there are zero scheduled routines but the anchor is enabled, skip registering the Monitor and record "routines":0 in the runtime file instead — go straight to anchor registration below.bun <pluginRoot>/scripts/routines.ts cron-registry plan .claude-code-hermit <pluginRoot> --ids heartbeat-restart. Execute its DELETE:/CREATE: lines exactly as Step 3-F's fallback flow does below — the DELETE: lines are what sweep any formerly-tracked non-anchor CronCreate; no separate sweep step needed. Commit with bun <pluginRoot>/scripts/routines.ts cron-registry commit .claude-code-hermit <pluginRoot> "<succeeded-csv>" --ids heartbeat-restart (--ids must match on both calls — commit replans internally). Skip the WAKESPREAD advisory in monitor mode (meaningless over one routine).mode just changed to monitor): also call CronList and CronDelete every entry whose prompt contains [hermit-routine: except [hermit-routine:heartbeat-restart] — covers live crons from an in-process upgrade that the mirror no longer tracks (duplicate-fire hazard).Routines: monitor registered (<N> scheduled), anchor re-armed. or the routines:0/first-transition variants.Step 3-F — fallback (Monitor unavailable, registration failed, or liveness-verify failed): run the CronCreate flow below against the full enabled set (scheduled routines + anchor), then write state/routine-monitor.runtime.json: {"mode":"croncreate-fallback","started_at":"<ISO now>"}.
CronCreate flow (also used verbatim by load --reset's fallback branch):
bun <pluginRoot>/scripts/routines.ts cron-registry plan .claude-code-hermit <pluginRoot> (append --force for load --reset). Parse stdout: DELETE:<id>, CREATE:<id>|<schedule>, WARN:<id>|<reason>, KEEP:<n>, optional trailing WAKESPREAD:<distinct>|<max>|<loneliest>.
SKIP|<reason> (planner fell open, e.g. corrupt config.json): abort, log Routine load aborted: cron planner failed — <reason>. No routines registered.DELETE:/CREATE: lines (only KEEP:<n>, optional WAKESPREAD:): already current — log Routines unchanged: <n> current, 0 registered. (plus the wake-spread line if present); stop. No CronList, no CronCreate, no CronDelete this run.DELETE: lines: call CronList once, CronDelete each entry whose prompt contains [hermit-routine:<id>] (skip silently if absent).CREATE:<id>|<schedule> line, build the prompt (Shared execution semantics, below) using <schedule> as-is (already tz-shifted). Call CronCreate: cron: <schedule>, recurring: true, durable: false, prompt: <resolved>. Per-routine error isolation: if CronCreate throws for one routine, record the failure and continue — track which ids succeeded.bun <pluginRoot>/scripts/routines.ts cron-registry commit .claude-code-hermit <pluginRoot> "<succeeded-csv>" (append --force for load --reset).Routines registered: <N> ok, <M> failed, <K> kept[, <W> tz-warned]. List failures/warnings on their own lines. If WAKESPREAD:<distinct>|<max>|<loneliest> was present: WARN: wake spread — <distinct> distinct 30-min wake windows (max <max>); consider clustering: <loneliest> (advisory only).load --reset: the unconditional escape hatch for suspected drift. Monitor mode: unconditionally TaskStop/orphan-sweep any routine-monitor task, CronList → CronDelete every live [hermit-routine:*] entry (anchor included), delete state/routine-schedule.json (deliberate baseline reset — the ordinary re-arm's "preserve the cursor" rule doesn't apply here), then re-register per Step 3 with --force appended to both the anchor plan and commit calls. Fallback mode: Step 3's planner call gets --force (every enabled routine becomes CREATE, no DELETEs) — --reset also sweeps unconditionally first: CronList, then CronDelete every [hermit-routine: entry. Step 3's commit call gets --force too. Both modes need --force on commit as well as plan: an unforced commit replan would carry stale registered_at forward, undoing the reset's clock.
Used both by the fallback CronCreate prompt (built at CREATE: time) and by the run <ids> handler (below) — one definition, two callers.
One shared template for both run_during_waiting (rdw) values — routines.ts precheck takes rdw as an argument and consults the waiting-check and the binding pause flag internally. Default run_during_waiting is false when the field is absent.
Model-override substitution. Read the routine's optional model field. First, if id === "heartbeat-restart", treat model as absent regardless of its value — its re-arm append must run in the session, so it is never dispatched to a subagent. Then: if model is absent/null, invoke /<skill> directly. If set to a non-null <model>, dispatch instead: resolve <abs-project-dir> as the session's current absolute working directory (the project root) and dispatch the skill via the Agent tool: subagent_type "general-purpose", model "<model>", prompt "The hermit project is at <abs-project-dir>; its state lives in <abs-project-dir>/.claude-code-hermit/. Invoke the skill /<skill> to completion, following its instructions exactly, and resolve any project-relative .claude-code-hermit/ reads/writes against <abs-project-dir> — pass the absolute <abs-project-dir>/.claude-code-hermit path to any hermit script the skill runs rather than relying on your cwd. Return only a one-line status." Language clause: when config.language is set, append one more sentence to that prompt — All operator-facing prose you produce (channel messages, push notifications, report text) must be written in <language>. — substituting the configured language; append nothing when it is null. No script call: the dispatching session already holds the language in its Operator Preferences context. This anchors the subagent's own project-relative state paths without constraining where else it may work — a dispatched skill that legitimately changes directory for unrelated work (e.g. a custom routine touching a sibling repo) is unaffected, since .claude-code-hermit/ always lives under <abs-project-dir> regardless of the subagent's cwd at invocation time. The Agent runs in isolated context and returns only a one-line status; the precheck call, the fired log line, and any heartbeat-restart/reflect_after appends stay in the session turn at the session model.
Base execution, one routine, <delivery> = cron-create (fallback prompt) or monitor (run handler):
Run: bun <pluginRoot>/scripts/routines.ts precheck <id> <rdw> <delivery>
If the output is SKIP, stop. If PROCEED, then invoke /<skill> (or dispatch per the model-override rule above). After it completes, run:
bun <pluginRoot>/scripts/routines.ts log-event <id> fired <delivery>
Replace <pluginRoot>, <id>, <rdw> (true/false; default false), and <skill> (passed verbatim to the slash invocation — claude-code-hermit:brief --morning becomes /claude-code-hermit:brief --morning).
Special case — heartbeat-restart: append Then invoke /claude-code-hermit:hermit-routines load to re-arm the routine monitor (and the anchor CronCreate). after the trailing fired log line. Daily re-arm via this routine is what keeps the monitor and, in fallback mode, the routine CronCreates from reaching the 7-day auto-expiry.
reflect_after: true: append after the trailing fired log line (and after the heartbeat-restart append if both apply). Skip when skill is claude-code-hermit:reflect — chaining reflect after reflect is a config foot-gun.
Then, only if `routines.ts precheck` returned PROCEED (not SKIP), run <pluginRoot>/scripts/reflect-precheck.ts .claude-code-hermit <pluginRoot> --quick. If its first output line is exactly `EMPTY`, do not invoke reflect. Otherwise (a `RUN|<hash>` line) invoke /claude-code-hermit:reflect --quick --precheck-verdict '<that full line>'.
Special case — skill is exactly claude-code-hermit:reflect: reflect's body should not load on days with nothing to reflect on. Replace the invoke clause with:
run <pluginRoot>/scripts/reflect-precheck.ts .claude-code-hermit <pluginRoot>. If its first output line is exactly `EMPTY`, do not invoke reflect; fall through to the `fired` log line. Otherwise (a `RUN|<phases-json>` line) invoke /claude-code-hermit:reflect --precheck-verdict '<that full line>'.
Runs the precheck once; an EMPTY day never loads the reflect skill body. Does not apply to --scheduled-checks invocations (no cadence precheck) — --quick gets its own via the reflect_after append above.
The ROUTINE_DUE notification handler — invoked when the monitor emits ROUTINE_DUE [hermit-routine:<id>] .... Parse the bracketed ids. For each, look up the routine in config.routines and execute per Shared execution semantics above with <delivery> = monitor. Ids no longer present in config are skipped silently.
Show configured routines from config.json (not the live view — that's status).
config.routines. If empty: "No routines configured."Routines (config.json):
# ID Schedule Skill RDW RA Model Status
1. heartbeat-restart 0 4 * * * claude-code-hermit:heartbeat start true false - enabled
2. weekly-review 0 23 * * 0 claude-code-hermit:weekly-review false false - disabled
RA is true when reflect_after: true. Model is the model value if set, otherwise -.
state/routine-monitor.runtime.json.
mode, started_at, interval, and state/routine-monitor-liveness.json's last_peek_at. Call CronList filtered to [hermit-routine: — in steady state only [hermit-routine:heartbeat-restart] should appear; more means "legacy CronCreates still active — run load to clean up."CronList filtered to [hermit-routine:, displayed as:
Active routine CronCreates:
ID CRON-ID SCHEDULE
heartbeat-restart 4e007cf4 0 4 * * *
(Extract id from the [hermit-routine:<id>] prefix.) If none: "No active routine CronCreates. Run load to register."/claude-code-hermit:hermit-routines load."Monitor mode:
stop or stop --all (no id, or --all): TaskStop the monitor task, clear state/routine-monitor.runtime.json, CronDelete the anchor. Log: "Stopped routine monitor and anchor."stop <id> (id ≠ heartbeat-restart): routines share one subprocess — do not stop it. Reply: "Routines share one monitor subprocess — to stop just <id>, set enabled: false on that entry in config.json and run load."stop heartbeat-restart: CronDelete the anchor alone (the monitor, if any, keeps running).Fallback mode (unchanged):
stop <id>: CronList, find the [hermit-routine:<id>] entry, CronDelete it (or "not active").stop (no id): CronList filtered to [hermit-routine:*] — 0 active: report; 1 active: stop without asking; 2+: list and ask (or --all).stop --all: CronDelete every [hermit-routine:*] entry.in_progress with no open operator turn no longer starves routines.^[A-Za-z0-9._-]{1,64}$ (enforced by validate-config.ts) — ids travel through bracket markers, --ids CSVs, and JSONL rows.hermit-settings routines invokes load after writing config; hand-edited config.json needs a manual load.hermit-start.ts calls load only on always-on launches.model (optional) runs a routine's skill in an isolated-context subagent at the named model (opus/sonnet/haiku) — returns only a one-line status, so skip it on routines whose value is the rich chat output. Ignored on heartbeat-restart (re-arm must run in the session). Validated by scripts/validate-config.ts.config.timezone to machine-local (CronCreate only knows machine time) at minute granularity — half-hour/45-minute zones (Kolkata, Adelaide, Kathmandu) work. Null config.timezone passes through unchanged. Monitor mode needs no shift — routines.ts due evaluates directly in config.timezone.load; heartbeat-restart's daily reload self-corrects within 24h. On the transition day, one fallback-mode fire may land at the wrong hour. Inexpressible-after-shift schedules pass through unchanged with a WARN: line.durable: false. CronCreates die with the session; re-registered on every always-on launch.heartbeat-restart. CC's recurring-task expiry is a hard 7-day cliff, reset only by re-creating. The diff planner re-registers any routine whose age crosses a conservative threshold even with unchanged config; heartbeat-restart's daily load is what crosses it. Disable it in fallback mode and routine CronCreates expire after 7 days.state/cron-registry.json is a derived mirror, never hand-edited — --reset, a missing/corrupt mirror, or a .boot-id mismatch all fall back to treating every enabled routine as needing registration.tools
Composes and delivers the daily fitness brief — a forward-looking morning read (readiness + today's plan) or a backward-looking evening read (today's training, or an earned-rest note, + tomorrow's setup) — in the operator's configured voice. Invoke with /claude-code-fitness-hermit:fitness-brief --morning|--evening|--slot <name>. Becomes the plugin's two daily beats — the morning Strava connectivity check and the evening activity sync, RPE binding, and Run deep-dive.
development
Renew the hermit's long-lived Claude login token over the channel, before it expires. Relays a one-time sign-in link to the operator, takes the code back, installs the new token, and restarts. Activates on messages like 'relogin', 'renew my login', 'reauth', 'the login is expiring', or when doctor's credential-expiry check flags setup-token.
development
Synthesizes the past 7 days of archived briefs into a weekly digest — top stories, emerging vs faded themes, category activity, and per-source performance built from archive frontmatter. Delivers to the operator's configured channel and archives a weekly note. Designed as a weekly routine. Invoke with /feed-hermit:weekly-digest.
development
Manage developing story arcs tracked across briefs — add, resolve, and list active arcs in compiled/story-arcs-*.md. Arc Watch keywords drive the feed-brief arc-tagging enrichment. Invoke with /feed-hermit:story-arcs add|resolve|list.