Skip to content

Nightly Caretaker + file-driven scheduler#212

Draft
pseay-imbue wants to merge 28 commits into
mainfrom
preston/nightly-agent
Draft

Nightly Caretaker + file-driven scheduler#212
pseay-imbue wants to merge 28 commits into
mainfrom
preston/nightly-agent

Conversation

@pseay-imbue

Copy link
Copy Markdown

Adds a self-managing scheduled-task system and a nightly Caretaker agent that quietly keeps a workspace healthy. Pairs with the minds-side PR (imbue-ai/mngr preston/nightly-agent: blinking tab + timezone capture).

What's in here

  • libs/scheduler — a thin, supervised file-driven scheduler with offline catch-up (a task missed while the machine was off runs once on next boot; multiple misses coalesce). One readable, agent-editable runtime/scheduled_tasks.toml; per-task state at runtime/scheduler/state.toml. scheduler run|list|add|remove|show.
  • Wiring[program:scheduler] under supervisord; bootstrap seeds the default schedule + dirs on first boot (the nightly Caretaker task at 3 AM).
  • Caretaker[create_templates.caretaker] + the caretaker skill. A singleton, once-a-night agent created lazily on first run. Consent-gated (runtime/caretaker/preferences.toml); first run is a cheap survey + a pre-prepared, user-friendly welcome (clean, user-facing-only messaging); later runs scan logs (via check-app-errors), read the prior log, and propose/apply fixes. run_caretaker.sh is the idempotent wake (create / message-idle / wrap-up-busy).
  • Skillsmanage-scheduled-tasks, check-app-errors; CLAUDE.md nudge to check /var/log/supervisor/ after editing a service.
  • system_interface — auto-created agents (the Caretaker) open as their own tab in the workspace, blinking in the accent color until opened.

Testing

  • Scheduler: 27 unit tests, pyright + ruff clean, meta-ratchets pass.
  • Verified end-to-end in a live workspace: scheduler runs, schedule seeds, the Caretaker is created with the right labels and posts its clean welcome, the blinking tab appears.

Draft — open for review alongside the paired minds PR.

🤖 Generated with Claude Code

pseay-imbue and others added 10 commits June 25, 2026 12:24
Phase 3 of the Caretaker/scheduler feature:
- manage-scheduled-tasks skill: query/edit runtime/scheduled_tasks.toml via
  the scheduler CLI, with the [[task]] schema, cron syntax, and catch-up
  semantics.
- check-app-errors skill: survey supervisorctl status and scan
  /var/log/supervisor logs for errors; reusable by chat agents and the
  Caretaker's nightly scan.
- CLAUDE.md: remind agents to check /var/log/supervisor after building or
  editing a service, pointing at check-app-errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the file-driven scheduler as a supervised service and seed its
default schedule on first boot.

- supervisord.conf: add [program:scheduler] (uv run scheduler run),
  mirroring [program:app-watcher]'s knobs, logging to
  /var/log/supervisor/scheduler-{stdout,stderr}.log.
- pyproject.toml: register the scheduler lib (libs/scheduler/) as a
  workspace member, dependency, and uv source.
- bootstrap/manager.py: on first boot, seed runtime/scheduled_tasks.toml
  (iff absent) with the single nightly Caretaker task per the frozen
  contract, and create runtime/scheduler/ + runtime/caretaker/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce libs/scheduler: a thin, supervised task scheduler that runs
recurring shell commands on a cron schedule with offline catch-up (a task
missed during downtime runs once on the next boot; coalesced).

- data_types: ScheduledTask + TaskRunState (frozen models)
- engine: pure due/overdue/coalesce logic, timezone-aware, unit-testable
- schedule_file/state: tomlkit round-trip for runtime/scheduled_tasks.toml
  and atomic runtime/scheduler/state.toml
- runner: non-blocking daemon loop (launch/reap, running guard, boot tick)
- cli: `scheduler run|list|add|remove|show` console script

27 unit tests pass; pyright + ruff clean. Workspace registration of the
lib lives in phase 2 (root pyproject.toml) to keep file ownership disjoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerated after merging phase 1 (libs/scheduler) and phase 2 (workspace
registration). Adds the scheduler workspace package and its croniter dep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Caretaker is a singleton, once-a-night agent that scans the workspace's
service logs for problems, reviews its previous run, and proposes (or, with
the user's recorded permission, applies) fixes -- always explained in plain,
non-technical, user-experience terms.

- .mngr/settings.toml: [create_templates.caretaker] (claude agent oriented by
  an appended system prompt to run the caretaker skill)
- .agents/skills/caretaker/SKILL.md: the nightly routine (clear, incremental
  logging, consent-gated scan/fix, first-run welcome, prune to 30 logs)
- scripts/run_caretaker.sh: idempotent wake -- create if absent, message if
  idle, wrap-up message if mid-run (singleton, found by the caretaker=true
  label; created with auto_created=true so minds shows it as a new tab)
- scripts/preferences.py: get/set/show for runtime/caretaker/preferences.toml
  (auto_scan, auto_fix, fix_scope, introduced)
- references/welcome-message.md: the canonical first-run welcome text

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Satisfies test_meta_ratchets' requirement that every project ship a
test_*_ratchets.py. Counts: one while-true and one time.sleep (the daemon
tick loop); zero builtin-exception raises, broad excepts, or relative imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nightly Caretaker (any agent labelled auto_created/caretaker) now opens
as its own tab in the workspace chat UI and blinks in the accent color until
the user opens it -- the real "new tab" surface (minds sidebar tabs are
per-workspace is_primary agents, so a same-host sub-agent like the Caretaker
can never be a sidebar tab; it belongs here, inside the workspace).

DockviewWorkspace.ts: surfaceAutoCreatedAgents() auto-opens an inactive tab
for each newly-seen auto_created agent (once ever, tracked in localStorage so
a closed tab isn't reopened); createCustomTab() adds a .dv-tab-new blink class
until the tab's first activation, then marks it seen (localStorage).
style.css: the tab-new-pulse accent animation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Caretaker was narrating its internal routine into the chat (status lines,
"first run", etc.), which read as unclear and oddly formatted. Tighten the
skill so the chat is strictly user-facing:

- Add a "How you talk to the user" section: chat = warm, plain, non-technical;
  do work silently via tools; keep reasoning in thinking and working notes in
  the run-log file, never the chat; end with a single clean message.
- First run delivers a pre-prepared, verbatim "Hi, I'm your Caretaker..."
  welcome (references/welcome-message.md), mirroring the main chat's /welcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pseay-imbue and others added 18 commits June 25, 2026 18:13
The Caretaker self-invoking "/clear" never cleared anything (it only emits
text). Make mngr the one that clears: on re-wake, send "/clear" to the agent's
stdin, settle briefly, then send the caretaking run trigger. First creation
skips the clear (nothing to clear) so the welcome is the first thing the user
sees. Busy Caretakers still get a graceful wrap-up message, then the same
mngr-driven clear + run sequence on restart.

Remove the agent-self-clear step from SKILL.md; the routine now starts at
opening the run log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unopened auto-created (Caretaker) tab now flashes in this workspace's own
accent color instead of the fixed green, and uses a sharp flash-then-fade
animation so it is much harder to miss.

The server injects the primary agent's hex `color` label as a
`system-interface-accent-color` meta tag (mirroring the existing agent-id tag),
the frontend reads it at bootstrap and publishes it as a `--workspace-accent`
CSS variable, and the `.dv-tab-new` animation snaps bright in that accent then
eases out to the normal idle tab look each cycle. Falls back to `--color-accent`
when no per-workspace accent is configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- system_interface: replace "surface once ever" with re-surfacing on each fresh
  run. A new run is the rising edge of the Caretaker's activity; if the user does
  not already have the tab open, re-open it (inactive) and reset the flash so it
  draws attention again. An already-open tab is left untouched.
- run_caretaker.sh: the first-creation trigger is now a welcome-only nudge, so
  day 1 shows just the welcome; the recurring "It's time for your caretaking run"
  message only appears from day 2 onward (after mngr's /clear).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tab keyed surfacing on activity_state (THINKING/TOOL_RUNNING), but the
Caretaker's lifecycle is RUNNING while activity is often idle/null, so the tab
never popped up. Switch to a reliable run-key: the caretaker_run label that
mngr bumps on every wake. The UI surfaces (and re-flashes) the tab when the key
changes -- including first sight -- so it pops up reliably and re-appears each
run, while leaving an already-open tab alone. run_caretaker.sh sets/bumps
caretaker_run on create and re-wake. Welcome heading is now "Hi, I'm a Caretaker
for your Mind."

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sage

Mirror the initial chat's creation (mngr create ... --message /welcome): the
Caretaker is created with --message /caretaker-welcome, a new verbatim skill
that emits a fixed welcome and runs no routine. So the first thing the user sees
is the canned welcome, delivered the same way as the main chat's -- not an agent
improvising while running its routine. The caretaker skill is now routine-only
(scan/fix on later nights) and records consent when the user answers; the old
first-run/introduced/welcome path and welcome-message.md are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Flash the whole .dv-tab clickable region (via :has) instead of just the
  inner label, so the new-tab indicator covers the entire tab.
- Format the /caretaker-welcome message with bold headings for the greeting
  and the two setup questions.
- Hide the /caretaker-welcome trigger user-message in the chat, the same way
  the main chat hides its /welcome trigger (isHiddenUserMessage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mngr observe --discovery-only events carry no labels (the discovery
certified_data omits them), so the observe-snapshot handlers were
overwriting the label-bearing entries from the initial list_agents
discovery with label-less ones. The web UI keys tab auto-surfacing on
the caretaker/auto_created labels and hides the is_primary services
agent by label, so a label-less snapshot meant the Caretaker tab never
opened and is_primary filtering only worked by luck.

Read each agent's labels from its local data.json (which mngr writes on
create) when the discovery event lacks them. Falls back to empty labels
for agents with no local state dir (e.g. tracked on a remote host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Welcome shows only on the very first run, gated on a persistent
  'introduced' preference, so it never reappears on later nights.
- Each later run retires the old Caretaker and creates a fresh agent
  (clean chat). An in-session /clear starts a new Claude session id the
  system interface isn't watching, so it never actually cleared the UI;
  a brand-new agent is the only reliable clear.
- The in-workspace UI now prunes the retired Caretaker's dead tab when
  the agent disappears, scoped to auto-created agents so a user's own
  chat tab is never at risk.
- The welcome now asks whether to take a first look right now; if the
  user says yes, the Caretaker runs its routine on the first day instead
  of waiting for the nightly schedule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stop overriding the tab label's text color during the flash. The faded
keyframe forced color: var(--color-text-secondary), which did not match
the tab's actual resting color in every state. Now only the wrapper
background animates (accent -> transparent) behind the label, so between
flashes the label is exactly the gray dockview would give it normally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Caretaker skill now makes clear it is chatting directly in its own tab:
  address the user as 'you', never prefix/label messages (no '@user:'),
  never narrate channel choice or bookkeeping, always plain language. Its
  closing summary is written straight as its response rather than routed
  through send-user-message (whose inline fallback added the '@user:'
  prefix and a 'no telegram, so...' narration).
- The run now starts with a short hello before any work, worded from the
  user's saved preferences, so the user always sees the Caretaker greet
  them and say what it is about to do.
- Later runs are triggered by a hidden '/caretaker' slash command (invokes
  the routine) instead of a visible nudge; hidden in the chat UI like
  '/caretaker-welcome'.
- caretaker-welcome skill: output only the message, with no 'I was asked
  to output the following' preamble.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Caretaker is now a single persistent agent invoked via /caretaker on
every run. The skill detects the first-ever run (introduced=false AND no
prior runtime/caretaker/*.md logs); on first run it sends the welcome
verbatim and sets introduced=true, otherwise it runs the nightly routine.
Removes the separate caretaker-welcome skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eduler

Researched whether a well-established library (APScheduler, schedule,
croniter, Celery beat, systemd timers, anacron) could replace the custom
libs/scheduler. Conclusion: keep ours -- no library cleanly covers the
combination of cron schedules, file-driven TOML config, a supervisord-hosted
Python service, and offline catch-up of missed runs from on-disk state.
APScheduler 3.x is closest but needs non-default config and adds a
TOML<->jobstore reconciliation layer (net complexity increase). Documented
the analysis and a contingency migration plan in
libs/scheduler/SCHEDULER_LIBRARY_EVALUATION.md. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ally clears

The nightly Caretaker is now a single persistent agent that mngr clears and
re-triggers each run, instead of being destroyed and recreated. run_caretaker.sh
creates the Caretaker once (first message /caretaker) and on later runs bumps its
run key, sends /clear, then sends /caretaker again -- the idempotent skill
self-detects first vs later run. Removed the destroy/recreate path, the
introduced gating, and the welcome-vs-run branching.

To make /clear actually clear the rendered chat, the system interface now renders
only the sessions at or after the most recent /clear boundary in an agent's
session history (previously it merged every session, so a cleared chat still
showed the old transcript with the new one appended). compact/resume/startup
sources remain continuations and never reset the view.

Dropped the retired /caretaker-welcome trigger from the hidden-message list in the
frontend (only /welcome and /caretaker remain hidden).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After mngr sends /clear to re-trigger the Caretaker, the cleared session
opens with the raw '/clear' command and Claude Code's
'<local-command-caveat>' message. Neither is a real user turn, so hide
both in isHiddenUserMessage -- the cleared chat now starts at the
Caretaker's own first message. Extends the message-classification test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant