/followup, local future-check-in skill¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_followup_skill/
Why it exists¶
Forge does not use /schedule (cloud-side Anthropic routine): cloud sandbox cannot read ~/.forge-secrets/, cannot SSH to Console, has no access to local services or forge tooling, and burns subscription quota for a strictly-worse experience than running locally on Console (always-on, systemd, cron, notify, secrets, all forge scripts). /followup is the canonical "do this later" surface on Forge.
Architecture¶
- One persistent systemd timer (
forge-followup-dispatcher.timer) fires every minute on Console and runsforge_followup.py dispatch. - Dispatch reads
forge/data/followups.json, identifies pending entries whosescheduled_for_utc <= now, fires each viaclaude -p --model <model> --dangerously-skip-permissions. /followup createis a pure file write: appends an entry to the registry. No sudo, no daemon-reload, no per-job systemd unit. Schedule changes are atomic JSON writes.- Recurring jobs re-compute their next fire time inside the dispatcher and remain
status=pending. - One-shots flip to
status=successorstatus=failedafter firing and stay in the registry as history.
Files¶
| Path | Role |
|---|---|
forge/scripts/forge_followup.py |
CLI: create, list, show, run, cancel, dispatch |
forge/data/followups.json |
Registry, single source of truth for job state |
forge/memory/followups/<slug>-YYYY-MM-DD.md |
Per-fire output, mkdocs-served |
forge/scripts/systemd/forge-followup-dispatcher.timer |
systemd timer, fires every minute |
forge/scripts/systemd/forge-followup-dispatcher.service |
systemd service, runs dispatch |
forge/scripts/systemd/install-forge-followup.sh |
One-time installer (sudo) |
forge/.claude/skills/followup/SKILL.md |
User-invocable skill wrapper |
Registry schema¶
{
"followups": {
"<id>": {
"id": "<12-hex>",
"name": "doctrine-audit",
"slug": "doctrine-audit",
"prompt": "...",
"schedule_type": "once|recurring",
"schedule_spec": "in 1 week",
"recurring_spec": null,
"scheduled_for_utc": "2026-05-08T14:00:00+00:00",
"scheduled_for_local": "2026-05-08 09:00 CT",
"model": "claude-sonnet-4-6",
"model_alias": "sonnet",
"created_at_utc": "...",
"status": "pending|running|success|failed|cancelled",
"last_run_utc": null,
"last_status": null,
"last_output_file": null,
"last_duration_sec": null
}
}
}
Atomic writes use tempfile + os.replace.
Notify message format¶
On fire (success or failure), pushes through forge_notify.sh:
Subject: Followup ✓ done: doctrine-audit (or ✗ failed)
Priority: info on success, warning on failure
Body:
Status: ✓ done (47s, claude-sonnet-4-6)
Scheduled: 2026-05-01 07:45 CT (7d ago)
Fired: 2026-05-08 09:00 CT
Summary:
<one-line summary the agent emitted via the ##SUMMARY## tag>
Results: memory/followups/doctrine-audit-2026-05-08.md
Deep-dive: paste path into @forge_remote_bridge_bot
Doctrine compliance (Section 9.5)¶
- Robust over quick: queue-polled dispatcher survives reboots; atomic JSON writes; systemd unit with
Restart-safe service shape; no transient state lost on Console restart. - Fail loud: any non-zero exit from
claude -p, timeout, or empty output flips the job to failed and pushes awarningnotify with error excerpt. Output file is always written even on failure. - Idempotent: each job has a uuid; same-day re-runs overwrite the same output file path; recurring jobs re-arm by computing next fire from
now, not from the missed slot. - Test before claiming done: dispatcher verifies the output file exists and is non-empty before declaring
successin the notify. - Single source of truth:
data/followups.jsonis the only persistence; no per-job systemd units, no parallel state.
Time parsing¶
Supported <when> patterns (parsed in this order):
1. every <weekday>[ at] HH[:MM][am|pm] -> recurring
2. daily[ at] HH[:MM][am|pm] -> recurring
3. in N (min|h|d|w) -> one-shot relative
4. tomorrow [HH[:MM][am|pm]] -> one-shot, defaults to 9am
5. anything else -> dateutil fuzzy parse, CT-local if naive, must be future
Setup (one-time)¶
Installs the timer + service to /etc/systemd/system/ and enables. Idempotent; safe to re-run.
Limitations¶
- Minimum granularity is the dispatcher poll interval (1 minute). A job scheduled for 09:00:00 CT may fire 0-60s late.
- Long-running jobs (max 30 min by default, set in
RUN_TIMEOUT_SEC) tie up the dispatcher for that window. Concurrent due jobs run sequentially in one dispatch tick. AdjustTimeoutStartSecin the service unit if longer runs are needed. - Each fire bills subscription quota proportional to the prompt + reasoning. Use
haikufor trivial checks,sonnetfor normal work,opusonly for real judgment passes.
Related¶
- Section 9.5 doctrine (engineering standards)
feedback_subscription_over_api_for_automation.md(decision tree: procedural → /spawn → /followup)feedback_subscription_over_api_for_automation.md(cost decision tree)reference_notify.md(downstream notify pipeline)
[Claude Code]