71 KiB
OPENCLAW_TRICKS.md — Configuration Recipes for OpenClaw Agents
A collection of tested patterns, prompts, and file structures for configuring an OpenClaw agent as a proactive personal assistant. These are extracted from a production setup that's been running since early 2026.
How This System Evolved
This section explains what we built, why, and how the pieces fit together. The rest of the document has the specific prompts and schemas — this part gives you the big picture so you understand the design decisions.
The Starting Point
OpenClaw gives you a persistent workspace directory and the ability to run tools (shell, web, browser, messaging). Out of the box, the agent wakes up fresh every session with no memory of what happened before. The core challenge is: how do you turn a stateless LLM into a stateful personal assistant that knows who you are, where you are, what you need, and what it was working on?
The answer is files. The workspace IS the agent's brain. Every piece of state, every rule, every memory lives in files that the agent reads on startup and updates as things change.
The Workspace Files — Separation of Concerns
We started with one big AGENTS.md that had everything: rules, procedures, medication instructions, git workflow, travel preferences. It worked at first but quickly became unwieldy. The context window was getting eaten by instructions the agent didn't need for most tasks.
The solution was factoring out rulesets into focused files:
- AGENTS.md — the master file. Responsibilities, session startup procedure, high-level rules. Think of it as the table of contents that points to everything else.
- SOUL.md — personality, tone, values. Separated because it's philosophical, not procedural. Also: it's fun to let the agent evolve this one over time.
- USER.md — info about the human. Timezone, communication preferences, basics the agent needs every session.
- MEMORY.md — curated long-term memory. Only loaded in private sessions (security — prevents leaking personal context in group chats).
- HEARTBEAT.md — what to check on periodic heartbeat polls. Kept small intentionally to minimize token burn on frequent checks.
- TOOLS.md — environment-specific notes (hostnames, device names, channel IDs). Separated from skills because this is YOUR setup, skills are shared.
Then the procedural rulesets got their own files too:
memory/medications-instructions.md— full medication protocol. Dosages, schedules, safety rules, overdose prevention logic. Only loaded when medication tasks come up.memory/checklist-pr.md— PR quality gate. What to verify before pushing, after sub-agent pushes, before assigning to the human for review.memory/checklist-flights.md— flight time verification, prep block creation, landing checklist triggers.memory/checklist-medications.md— pre-action verification before reporting medication status or sending reminders.memory/checklist-messaging.md— rules for every outgoing message: URL verification, timezone conversion, status claim verification.memory/landing-checklist.md— post-flight procedures (update location, timezone, check meds, sync calendar).
The key insight: MEMORY.md has a "checklists" section at the very top that says "before doing X, read file Y." The agent reads MEMORY.md at session start, sees the checklist index, and knows which file to consult before any action. This way the detailed instructions aren't always in context — they're loaded on-demand.
## ⛔ CHECKLISTS (read the relevant file before acting)
- **Any PR/code work** → `memory/checklist-pr.md`
- **Any message to the user** → `memory/checklist-messaging.md`
- **Medications** → `memory/checklist-medications.md`
- **Flights/travel** → `memory/checklist-flights.md`
The Daily Context State File
This was probably the single most impactful addition. It's a JSON file
(memory/daily-context.json) that every session reads on every message. It
tracks the current state of the human: where they are, what timezone they're in,
whether they're sleeping, when they last took meds, when they last sent a
message.
Why JSON instead of markdown? Because it's machine-readable. The agent can update individual fields programmatically, and the structure is unambiguous. Markdown is great for instructions; JSON is great for state.
The daily context file means:
- The agent always knows the human's timezone (critical for a frequent traveler)
- Medication reminders fire based on actual timestamps, not guesses
- Sleep predictions inform when to send alerts vs stay quiet
- Location tracking happens automatically
The Two-Tier Memory System
Daily files (memory/YYYY-MM-DD.md) are raw logs — what happened each day.
Topic tables, decisions, lessons, notes. Think of these as a daily journal.
MEMORY.md is curated long-term memory — the distilled essence. Standing rules, key facts, lessons learned. Think of this as what a human would "just know" about their life.
During heartbeats, the agent periodically reviews recent daily files and promotes significant items to MEMORY.md, and removes stale info. It's explicitly modeled on how human memory works: raw experience gets processed into lasting knowledge, and irrelevant details fade.
The security model: MEMORY.md is only loaded in private (1:1 DM) sessions. In group chats, the agent works from daily files and daily-context.json only. This prevents personal context from leaking into shared channels.
Medication Tracking
This is a safety-critical system. The design is deliberately paranoid:
- CSV as source of truth. Not the daily-context boolean, not the agent's memory — the CSV log file is authoritative.
- Double-verification before any action. The daily context has a
hasTakenDailyMedsTodayboolean AND adailyMedsTimestamp. A midnight cron resets the boolean. But if the human is in a timezone ahead of the server, the reset happens at the wrong time. So the rule is: always verify the timestamp falls on today's date in the human's timezone. The boolean is a convenience hint, never the source of truth. - Interval medications anchored to actual doses. Some meds are "every 5 days" or "every 14 days." The next dose date is calculated from the last actual dose timestamp, not from the intended schedule. If a dose is missed, the next date is unknown until the missed dose is taken and logged.
- Overdose prevention. The agent blocks logging if it detects a same-day duplicate batch. This is the highest-priority safety rule — a duplicate daily batch of certain medications could cause cardiac arrest.
- Escalating alerts. If daily meds are >26h since last dose, the agent escalates aggressively — ntfy push notification if chat messages go unacknowledged.
The medication instructions file is loaded on-demand (not every session), keeping context costs low for non-medication conversations.
Sleep Tracking
The agent infers sleep from activity gaps rather than requiring explicit "I'm going to sleep" statements. On every message, it checks: was there a gap since the last message that overlaps with the predicted sleep window? If so, log sleep start = last activity before gap, wake = first activity after.
Activity isn't just chat messages — it includes Gitea commits, comments, scheduled flight departures, and any other observable actions.
Sleep predictions drift over time (the human in this setup tends to sleep ~30min later each day), so the agent tracks the trend and extrapolates. Caffeine intake adjusts predictions forward. The predictions feed into:
- When to send alerts vs stay quiet
- Sitrep "sleep conflict" warnings (appointment during predicted sleep)
- General awareness of the human's schedule
Location & Timezone Tracking
The agent always knows where the human is. This matters because:
- Times must always be displayed in the human's local timezone, not the server's timezone
- "Today" and "tomorrow" mean different things in different timezones
- Medication timing needs to account for timezone changes
- Flight prep needs local airport info
The landing checklist (triggered automatically after every flight) updates location, timezone, nearest airport, and lodging in the daily context file. It also checks if any cron jobs have hardcoded timezones that need updating.
Gitea Notification Delivery
There are two approaches for getting Gitea notifications to your agent, depending on your network setup.
Option A: Direct Webhooks (VPS / Public Server)
If your OpenClaw instance runs on a VPS or other publicly reachable server, the simplest approach is direct webhooks. Run Traefik (or any reverse proxy with automatic TLS) on the same server and configure Gitea webhooks to POST directly to OpenClaw's webhook endpoint. This is push-based and realtime — notifications arrive instantly.
Setup: add a webhook on each Gitea repo (or use an organization-level webhook)
pointing to https://your-openclaw-host/hooks/gitea. OpenClaw handles the rest.
Option B: Notification Poller + Dispatcher (Local Machine Behind NAT)
If your OpenClaw runs on a dedicated local machine behind NAT (like a home Mac or Linux workstation), Gitea can't reach it directly. This is our setup — OpenClaw runs on a Mac Studio on a home LAN.
The solution: a Python script that polls the Gitea notifications API, triages each notification, and spawns an isolated agent session per actionable item. Response time is ~15-30 seconds.
Evolution note: We originally used a flag-file approach (poller writes flag → agent checks during heartbeat → ~30 min latency). This was replaced by the dispatcher pattern below, which is near-realtime.
Key design decisions:
- The poller IS the dispatcher. It fetches notification details, checks whether the agent is mentioned or assigned, and spawns agents directly. No middleman session needed.
- One agent per actionable notification. Each spawns via
openclaw cron add --session isolatedwith full context (API token, issue URL, instructions) baked into the message. Parallel notifications get parallel agents. - Marks notifications as read immediately. Prevents re-processing. The agent's job is to respond, not to manage notification state.
- Tracks notification IDs, not counts. Only fires on genuinely new notifications, not re-reads of existing ones.
- Triage before dispatch. Not every notification is actionable. The poller checks: is the agent @-mentioned (in issue body or latest comment)? Is the issue/PR assigned to the agent? Is the agent's comment already the latest (no response needed)?
- Assignment scan as backup. A secondary loop periodically scans watched repos for open issues assigned to the agent that were recently updated but have no agent response. This catches cases where notifications aren't generated (API-created issues, self-assignment).
- Strict scope enforcement. Each spawned agent's prompt includes a SCOPE constraint: "You are responsible for ONLY this issue. Do NOT touch any other issues or PRs." This prevents rogue agents from creating unauthorized work.
- Priority rule. Agent prompts explicitly state that the user's instructions in the issue override all boilerplate rules (e.g., if the user asks for a DM response, the agent should DM).
- Zero dependencies. Just Python stdlib. Runs anywhere.
#!/usr/bin/env python3
"""
Gitea notification poller + dispatcher.
Two polling loops:
1. Notification-based: detects new notifications (mentions, assignments)
and dispatches agents for actionable ones.
2. Assignment-based: periodically checks for open issues/PRs assigned to
the agent that have no recent response. Catches cases where
notifications aren't generated.
Required env vars:
GITEA_URL - Gitea instance URL
GITEA_TOKEN - Gitea API token
Optional env vars:
POLL_DELAY - Delay between polls in seconds (default: 15)
COOLDOWN - Minimum seconds between dispatches (default: 30)
ASSIGNMENT_INTERVAL - Seconds between assignment scans (default: 120)
OPENCLAW_BIN - Path to openclaw binary
"""
import json
import os
import subprocess
import sys
import time
import urllib.request
import urllib.error
GITEA_URL = os.environ.get("GITEA_URL", "").rstrip("/")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
POLL_DELAY = int(os.environ.get("POLL_DELAY", "15"))
COOLDOWN = int(os.environ.get("COOLDOWN", "30"))
ASSIGNMENT_INTERVAL = int(os.environ.get("ASSIGNMENT_INTERVAL", "120"))
OPENCLAW_BIN = os.environ.get("OPENCLAW_BIN", "/opt/homebrew/bin/openclaw")
# Mattermost channel for status updates (customize to your setup)
GIT_CHANNEL = "channel:YOUR_GIT_CHANNEL_ID"
# Repos to scan for assigned issues
WATCHED_REPOS = [
"your-org/repo1",
"your-org/repo2",
]
# Track dispatched issues to prevent duplicates
dispatched_issues = set()
BOT_USERNAME = "your-bot-username" # e.g. "clawbot"
def check_config():
if not GITEA_URL or not GITEA_TOKEN:
print("ERROR: GITEA_URL and GITEA_TOKEN required", file=sys.stderr)
sys.exit(1)
def gitea_api(method, path, data=None):
"""Call Gitea API, return parsed JSON or None on error."""
url = f"{GITEA_URL}/api/v1{path}"
body = json.dumps(data).encode() if data else None
headers = {"Authorization": f"token {GITEA_TOKEN}"}
if body:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, headers=headers, method=method, data=body)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read()
return json.loads(raw) if raw else None
except Exception as e:
print(f"WARN: Gitea API {method} {path}: {e}",
file=sys.stderr, flush=True)
return None
def get_unread_notifications():
result = gitea_api("GET", "/notifications?status-types=unread")
return result if isinstance(result, list) else []
def mark_notification_read(notif_id):
gitea_api("PATCH", f"/notifications/threads/{notif_id}")
def needs_bot_response(repo_full, issue_number):
"""True if bot is NOT the author of the most recent comment."""
comments = gitea_api(
"GET", f"/repos/{repo_full}/issues/{issue_number}/comments"
)
if comments and len(comments) > 0:
if comments[-1].get("user", {}).get("login") == BOT_USERNAME:
return False
return True
def is_actionable_notification(notif):
"""Check if a notification needs agent action.
Returns (actionable, reason, issue_number)."""
subject = notif.get("subject", {})
repo = notif.get("repository", {})
repo_full = repo.get("full_name", "")
url = subject.get("url", "")
number = url.rstrip("/").split("/")[-1] if url else ""
if not number or not number.isdigit():
return False, "no issue number", None
issue = gitea_api("GET", f"/repos/{repo_full}/issues/{number}")
if not issue:
return False, "couldn't fetch issue", number
# Check assignment
assignees = [a.get("login") for a in (issue.get("assignees") or [])]
if BOT_USERNAME in assignees:
if needs_bot_response(repo_full, number):
return True, f"assigned to {BOT_USERNAME}", number
return False, "assigned but already responded", number
# Check issue body for @mention
issue_body = issue.get("body", "") or ""
issue_author = issue.get("user", {}).get("login", "")
if f"@{BOT_USERNAME}" in issue_body and issue_author != BOT_USERNAME:
if needs_bot_response(repo_full, number):
return True, f"@-mentioned in body by {issue_author}", number
# Check latest comment for @mention
comments = gitea_api(
"GET", f"/repos/{repo_full}/issues/{number}/comments"
)
if comments:
last = comments[-1]
author = last.get("user", {}).get("login", "")
body = last.get("body", "") or ""
if author == BOT_USERNAME:
return False, "own comment is latest", number
if f"@{BOT_USERNAME}" in body:
return True, f"@-mentioned in comment by {author}", number
return False, "not mentioned or assigned", number
def spawn_agent(repo_full, issue_number, title, subject_type, reason):
"""Spawn an isolated agent to handle one issue/PR."""
dispatch_key = f"{repo_full}#{issue_number}"
if dispatch_key in dispatched_issues:
return
dispatched_issues.add(dispatch_key)
repo_short = repo_full.split("/")[-1]
job_name = f"gitea-{repo_short}-{issue_number}-{int(time.time())}"
# Build agent prompt with full context
msg = (
f"Gitea notification: {reason} on {subject_type} #{issue_number} "
f"'{title}' in {repo_full}.\n\n"
f"Gitea API base: {GITEA_URL}/api/v1\n"
f"Gitea token: {GITEA_TOKEN}\n\n"
f"SCOPE (STRICT): You are responsible for ONLY {subject_type} "
f"#{issue_number} in {repo_full}. Do NOT create PRs, branches, "
f"comments, or take any action on ANY other issue or PR.\n\n"
f"PRIORITY RULE: The user's instructions in the issue/PR take "
f"priority over ALL other rules. If asked to respond in DM, do so. "
f"Later instructions override earlier ones.\n\n"
f"Instructions:\n"
f"1. Read ALL existing comments on #{issue_number} via API\n"
f"2. Follow the user's instructions\n"
f"3. If code work needed: clone to $(mktemp -d), make changes, "
f"run make check, push, comment on the issue/PR\n"
f"4. Default: post work reports as Gitea comments\n"
f"5. Don't post duplicate comments if yours is already the latest"
)
try:
result = subprocess.run(
[
OPENCLAW_BIN, "cron", "add",
"--name", job_name,
"--at", "1s",
"--message", msg,
"--delete-after-run",
"--session", "isolated",
"--no-deliver",
"--thinking", "low",
"--timeout-seconds", "300",
],
capture_output=True, text=True, timeout=15,
)
if result.returncode == 0:
print(f" → Agent spawned: {job_name}", flush=True)
else:
print(f" → Spawn failed: {result.stderr.strip()[:200]}",
flush=True)
dispatched_issues.discard(dispatch_key)
except Exception as e:
print(f" → Spawn error: {e}", file=sys.stderr, flush=True)
dispatched_issues.discard(dispatch_key)
def dispatch_notifications(notifications):
"""Triage notifications and spawn agents for actionable ones."""
for notif in notifications:
subject = notif.get("subject", {})
repo = notif.get("repository", {})
repo_full = repo.get("full_name", "")
title = subject.get("title", "")[:60]
notif_id = notif.get("id")
subject_type = subject.get("type", "").lower()
is_act, reason, issue_num = is_actionable_notification(notif)
if notif_id:
mark_notification_read(notif_id)
if is_act:
print(f" ACTIONABLE: {repo_full} #{issue_num} ({reason})",
flush=True)
spawn_agent(repo_full, issue_num, title, subject_type, reason)
else:
print(f" skip: {repo_full} #{issue_num} ({reason})", flush=True)
def scan_assigned_issues():
"""Backup scan: find assigned issues needing response."""
for repo_full in WATCHED_REPOS:
for issue_type in ["issues", "pulls"]:
items = gitea_api(
"GET",
f"/repos/{repo_full}/issues?state=open&type={issue_type}"
f"&assignee={BOT_USERNAME}&sort=updated&limit=10"
)
if not items:
continue
for item in items:
number = str(item["number"])
dispatch_key = f"{repo_full}#{number}"
if dispatch_key in dispatched_issues:
continue
if not needs_bot_response(repo_full, number):
continue
kind = "PR" if issue_type == "pulls" else "issue"
print(f" [assign-scan] {repo_full} {kind} #{number}",
flush=True)
spawn_agent(
repo_full, number, item.get("title", "")[:60],
"pull" if issue_type == "pulls" else "issue",
f"assigned to {BOT_USERNAME}"
)
def main():
check_config()
print(f"Gitea poller+dispatcher (poll={POLL_DELAY}s, "
f"cooldown={COOLDOWN}s, assign_scan={ASSIGNMENT_INTERVAL}s)",
flush=True)
seen_ids = set(n["id"] for n in get_unread_notifications())
last_dispatch_time = 0
last_assign_scan = 0
print(f"Initial unread: {len(seen_ids)} (draining)", flush=True)
while True:
time.sleep(POLL_DELAY)
now = time.time()
# Notification polling
notifications = get_unread_notifications()
current_ids = {n["id"] for n in notifications}
new_ids = current_ids - seen_ids
if new_ids:
ts = time.strftime("%H:%M:%S")
new_notifs = [n for n in notifications if n["id"] in new_ids]
print(f"[{ts}] {len(new_ids)} new notification(s)", flush=True)
if now - last_dispatch_time >= COOLDOWN:
dispatch_notifications(new_notifs)
last_dispatch_time = now
else:
remaining = int(COOLDOWN - (now - last_dispatch_time))
print(f" → Cooldown ({remaining}s remaining)", flush=True)
seen_ids = current_ids
# Assignment scan (less frequent)
if now - last_assign_scan >= ASSIGNMENT_INTERVAL:
scan_assigned_issues()
last_assign_scan = now
if __name__ == "__main__":
main()
Run it as a background service (launchd on macOS, systemd on Linux) with
GITEA_URL and GITEA_TOKEN set. Customize WATCHED_REPOS, BOT_USERNAME,
and GIT_CHANNEL for your setup. It's intentionally simple — no frameworks,
no async, no dependencies.
Lessons learned during development:
openclaw cron add --atuses formats like1s,20m— not+5sor+0s.--no-deliveris incompatible with--session main. Use--session isolatedwith--no-deliver.--system-eventtargets the main DM session. If your agent is active in a channel session, it won't see system events. Use--session isolatedwith--messageinstead.- Isolated agent sessions don't have access to workspace files (TOOLS.md, etc). Bake all credentials and instructions directly into the agent prompt.
- Agents WILL go out of scope unless the SCOPE constraint is extremely explicit and uses strong language ("violating scope is a critical failure").
- When the user's explicit instructions in an issue conflict with boilerplate rules in the agent prompt, the agent will follow the boilerplate unless the prompt explicitly says "user instructions take priority."
The Daily Diary
Every day gets a memory/YYYY-MM-DD.md file. The agent appends a topic summary
to a table after every meaningful conversation:
## Topics
| Time (TZ) | Topic |
| --------- | ------------------------------------------------ |
| 14:30 | Discussed PR review workflow |
| 16:00 | Medication logged |
| 18:45 | Flight prep blocks created for tomorrow's flight |
This serves multiple purposes:
- Any session can see what's been discussed today without loading full conversation history
- The agent can use
sessions_historyto get details on a specific topic if needed - During memory maintenance, the agent reviews these to decide what's worth promoting to MEMORY.md
- It's a simple audit trail of what happened
The Requirement Capture Rule
One of the most important rules: every single requirement, preference, or instruction the human provides MUST be captured in a file immediately. Not "mentally noted" — written to disk. Because the agent wakes up fresh every session, a "mental note" is worthless. If it's not in a file, it didn't happen.
This applies to everything: project rules ("no mocks in tests"), workflow preferences ("fewer PRs, don't over-split"), corrections, new policies. Immediate write to the daily file, and to MEMORY.md if it's a standing rule.
Sensitive Output Routing
A lesson learned the hard way: the audience determines what you can say, not who asked. If the human asks for a medication status report in a group channel, the agent can't just dump it there — other people can read it. The rule: if the output would contain sensitive information (PII, secrets, credentials, API keys, operational details like flight numbers, locations, travel plans, medical info, etc.) and the channel isn't private, redirect to DM and reply in-channel with "sent privately."
This is enforced at multiple levels:
- AGENTS.md has a warning banner at the top
- The checklist system catches it before action
- Channel-specific rule files (like our
memory/JAMES_CHAT.md) define what's shareable per-channel
Sub-Agent Isolation
When spawning coding sub-agents for PR work, each one MUST clone to a fresh
temporary directory. Never share git clones between agents — dirty working
directories cause false CI results, merge conflicts, and wrong file state. The
rule: cd $(mktemp -d) && git clone <url> . && ...
The Heartbeat System
OpenClaw polls the agent periodically with a configurable prompt. The agent reads HEARTBEAT.md and decides what to do. We keep HEARTBEAT.md small (focused checklist) to minimize token burn on these frequent checks.
The heartbeat handles:
- Gitea inbox triage (check for new assignments)
- Flight prep block creation (look ahead 7 days)
- Open project review (can I take a step on anything?)
- Workspace sync (commit and push changes)
- Periodic memory maintenance
State tracking in memory/heartbeat-state.json prevents redundant checks (e.g.,
don't re-check notifications if you checked 10 minutes ago).
The key output rule: heartbeats should either be HEARTBEAT_OK (nothing to do)
or a direct alert. Work narration goes to a designated status channel, never to
the human's DM.
The Workspace as a Git Repo (Disaster Recovery + Debugging)
The entire workspace is itself a git repo, aggressively committed and pushed after every change. A cron job runs every 6 hours as a safety net, but the agent pushes promptly after any modification.
This gives you three things:
-
Disaster recovery. The git remote is on a different machine (ideally in a different building/city/country from the OpenClaw host). If the host dies, you clone the workspace onto a new machine and the agent picks up where it left off — all memory files, daily context, logs, checklists intact.
-
State snapshots. Every commit is a snapshot of the agent's complete state at that moment. If the agent corrupts a file, overwrites something important, or makes a bad edit, you can
git logandgit checkoutto recover any previous version. -
Prompt debugging. When the agent behaves unexpectedly, you can look at the git history to see exactly what its state files contained at the time. What did daily-context.json say? What was in MEMORY.md? What checklist version was it using? This turns "why did it do that?" from guesswork into forensics.
The agent treats this repo differently from code repos — no PRs, no branches, just direct commits to main. It's a state mirror, not a development workflow.
### State Repo (clawstate.git)
- Commit and push workspace changes after any change to workspace files
- Push promptly — the remote repo should reflect the workspace in effectively
realtime
- Auto-sync cron runs every 6h as a safety net
Important: Code repos should be cloned OUTSIDE the workspace (e.g.
~/repos/ or a fast external drive) to avoid embedding git repos inside the
workspace repo. The workspace repo tracks workspace files only.
A deliberate policy exception: The workspace repo violates one of the most important repo policies — "never commit secrets." The workspace contains API keys, tokens, and credentials in files like TOOLS.md because those are part of the agent's operational state. This is an accepted exception because the repo is permanently private and serves as a backup/DR system, not a development repo. If your workspace state repo were ever to become public, it would be a catastrophic leak. Treat it accordingly: private visibility, restricted access, no forks.
Git Quality Standards — Interlocking Automated Checks
An AI agent will forget things. It will skip the formatter, push without
testing, weaken a linter rule to make CI pass, or use git add . and commit
junk files. You cannot rely on "be careful" — you need automated gates that make
it structurally impossible to ship bad code.
The approach we use is based on sneak/prompts, a repo of standardized policies that every project copies. The key document is REPO_POLICIES.md, which defines the interlocking check system.
The Interlocking Chain
The checks form a dependency chain where each layer requires the previous:
Gitea Actions CI
└── docker build .
└── make check (inside Dockerfile)
├── make fmt-check (code formatting)
├── make lint (static analysis)
└── make test (unit/integration tests)
This means:
- CI runs
docker build .— that's it, one command - The Dockerfile runs
make checkas a build step — if checks fail, the Docker build fails, CI fails make checkdepends onfmt-check,lint, andtest— all three must pass- You can't skip any layer — they're structurally linked
From REPO_POLICIES.md:
Every repo with software must have a root
Makefilewith these targets:make test,make lint,make fmt(writes),make fmt-check(read-only),make check(prereqs:test,lint,fmt-check),make docker, andmake hooks(installs pre-commit hook).
Every repo should have a
Dockerfile. All Dockerfiles must runmake checkas a build step so the build fails if the branch is not green.
Every repo should have a Gitea Actions workflow (
.gitea/workflows/) that runsdocker build .on push. Since the Dockerfile already runsmake check, a successful build implies all checks pass.
Why Docker as the CI Runner
Running make check inside docker build solves the "works on my machine"
problem:
- Clean environment every time. No stale caches, no leftover files, no wrong toolchain version
- Reproducible. The Dockerfile pins the base image by SHA (not tag), so the build environment is identical everywhere
- No CI configuration drift. The CI workflow is one line:
docker build .. All the actual logic lives in the Dockerfile and Makefile, which are version-controlled in the repo
From REPO_POLICIES.md:
ALL external references must be pinned by cryptographic hash. This includes Docker base images, Go modules, npm packages, GitHub Actions, and anything else fetched from a remote source. Version tags (
@v4,@latest,:3.21, etc.) are server-mutable and therefore remote code execution vulnerabilities.
The Makefile as Authoritative Documentation
The Makefile isn't just a build tool — it's the single source of truth for how the repo works:
The Makefile is authoritative documentation for how the repo is used. Beyond the required targets above, it should have targets for every common operation: running a local development server (
make run,make dev), re-initializing or migrating the database (make db-reset,make migrate), building artifacts (make build), generating code, seeding data, or anything else a developer would do regularly. If someone checks out the repo and typesmake<tab>, they should see every meaningful operation available.
This isn't an AI-specific pattern — it predates agents entirely. A Makefile in
every repo has always been good engineering practice because it gives any new
developer (human or AI) a plain, direct, authoritative reference for how to
interact with the repo. It's authoritative because it's how we actually invoke
the tools — it's not documentation that can drift from reality, it IS the
reality. make<tab> shows you everything you can do. No wikis to read, no
READMEs to hope are up-to-date.
The deeper insight: humans also have limited state, limited memory, and start
dropping things when context gets too big. A developer switching between a Go
service, a Python scraper, and a JS frontend in the same afternoon doesn't want
to remember three different ways to run tests, three different linter
invocations, three different formatting commands. make test is make test
everywhere. make check is make check everywhere. make fmt is make fmt
everywhere. The language, the framework, the toolchain — all abstracted behind a
universal interface.
This reduces cognitive load and allows better flow and focus. You don't have to
re-orient when switching repos. You don't have to look up "was it pytest or
python -m pytest or make test-unit?" The Makefile answers all of that with
one consistent vocabulary across every project.
The agent can implement and enforce these policies — auto-generating Makefiles, running the checklists, catching drift — but everyone benefits. Humans get consistency. Agents get predictability. New team members (human or AI) get instant orientation. The policies serve all three audiences simultaneously.
For AI agents, there's an additional benefit:
- The agent always uses
make test, nevergo test ./...directly — the Makefile encapsulates flags, timeouts, environment setup - A new sub-agent spawned on any repo can immediately see every available operation without reading docs or asking questions
- The human never has to explain "how to run the tests" — it's always
make test, everywhere, every repo
Pre-Commit Hooks
Local enforcement before code even reaches the remote:
Pre-commit hook:
make checkif local testing is possible, otherwisemake lint && make fmt-check. The Makefile should provide amake hookstarget to install the pre-commit hook.
Our PR checklist requires agents to install hooks after every clone:
echo '#!/bin/sh\nmake check' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
Linter Config Is Sacred
One of the most dangerous failure modes (documented in the failures section above) is an agent modifying linter config to make checks pass:
.golangci.ymlis standardized and must NEVER be modified by an agent, only manually by the user.
This is enforced in our PR checklist:
## After sub-agent pushes code
1. Check diff for .golangci.yml / linter / test config changes
2. Check diff for `-short` / `-timeout` flags added to test commands
3. If any config files changed: reject and rework
The principle: if the check fails, fix the code, not the check. This applies universally — linter rules, test assertions, formatting config, CI workflows. Weakening a gate to make it pass is worse than a loud failure, because loud failures get fixed while silent rot compounds.
Formatting Standards
From REPO_POLICIES.md:
Use platform-standard formatters:
blackfor Python,prettierfor JS/CSS/Markdown/HTML,go fmtfor Go. Always use default configuration with two exceptions: four-space indents (except Go), andproseWrap: alwaysfor Markdown (hard-wrap at 80 columns).
This means formatting is never a judgment call — run the formatter, done. The
make fmt target writes changes, make fmt-check verifies without modifying
(used in CI). Agents run make fmt before committing, CI runs make fmt-check
to catch anything that slipped through.
Test Requirements
All repos with software must have tests that run via the platform-standard test framework (
go test,pytest,jest/vitest, etc.). If no meaningful tests exist yet, add the most minimal test possible — e.g. importing the module under test to verify it compiles/parses. There is no excuse formake testto be a no-op.
make testmust complete in under 20 seconds. Add a 30-second timeout in the Makefile.
The timeout is critical for agents — without it, a hanging test blocks the entire sub-agent session. If tests take too long, that's a bug to file, not a timeout to increase.
Git Hygiene Rules
From REPO_POLICIES.md and our operational experience:
- Never
git add -Aorgit add .— always stage files explicitly. Agents love togit add .and accidentally commit.DS_Store, editor swap files, or debug output - Never force-push to main — feature branches only
- Branch protection on main — enforce this with Gitea/GitHub branch protection rules: require PR reviews, require CI to pass, block force-pushes. This is the server-side interlock that makes the "never push directly to main" rule structural rather than aspirational. An agent (or a tired human at 3am) physically cannot bypass the review and CI gates.
- Each change = separate commit — formatting changes go in their own commit before logic changes
- Rebase before and after — PRs must be mergeable at time of push
- Never commit secrets —
.env, credentials, API keys in.gitignore
The PR Pipeline
Our agent follows a strict PR lifecycle:
## PR pipeline (every PR, no exceptions)
1. **Review/rework loop**: code review → rework → re-review → repeat until clean
2. **Check/rework loop**: `make check` + `docker build .` → rework → re-check →
repeat until clean
3. Only after BOTH loops pass with zero issues: assign to human
- "Passes checks" ≠ "ready for human"
- Never weaken tests/linters. Fix the code.
- Pre-existing failures are YOUR problem. Fix them as part of your PR.
The agent doesn't just create a PR and hand it off — it drives the PR through review, rework, and verification until it's genuinely ready. A PR assigned to the human means: all checks pass, code reviewed, review feedback addressed, rebased against main, no conflicts. Anything less is the agent's open task.
New Repo Bootstrap
Every new repo follows a checklist from REPO_POLICIES.md:
New repos must contain at minimum:
README.md,.git,.gitignore,.editorconfig,LICENSE,REPO_POLICIES.md,Makefile,Dockerfile,.dockerignore,.gitea/workflows/check.yml
Plus language-specific files (Go: go.mod, .golangci.yml; JS: package.json,
.prettierrc; Python: pyproject.toml).
The full standardized configs are available at
https://git.eeqj.de/sneak/prompts/raw/branch/main/<filename> — agents fetch
them when bootstrapping a new repo, ensuring consistency across all projects.
Why This Matters — For Everyone
These interlocking checks aren't just agent-proofing — they're human-proofing too. Humans also cut corners under deadline pressure, forget to run the formatter, skip tests "just this once," or push a quick fix without checking CI. The automated gates don't care who's committing:
- Can't push unformatted code →
make fmt-checkin pre-commit hook - Can't skip tests →
make checkdepends onmake test - Can't weaken linters → config file changes flagged in PR review
- Can't claim "CI passes" without proof → Docker build is pass/fail
- Can't ship without review → PR assignment rules
AI agents do have a unique failure mode on top of this: they're confidently wrong. An agent will assert that checks pass without having run them, or silently weaken a gate to make the build green. The interlocking system catches this too — confidence doesn't override a failing build.
Nobody needs willpower or attention to detail when the system makes doing the wrong thing fail loudly. That's the point: good engineering infrastructure benefits every contributor, regardless of whether they're carbon or silicon.
Checklists Over Prose — Why Redundancy Is the Point
REPO_POLICIES.md describes everything: what files are required, how the Makefile should work, how formatting works, how CI works. It's comprehensive. But comprehensive prose doesn't keep an agent on track — point-by-point checklists do.
That's why we also maintain two separate checklists alongside REPO_POLICIES.md:
- NEW_REPO_CHECKLIST.md — step-by-step when creating a repo from scratch
- EXISTING_REPO_CHECKLIST.md — step-by-step when starting work in a repo that may not conform yet
These are intentionally redundant with REPO_POLICIES.md. That's the point.
Why redundancy works for AI agents:
LLMs are good at linear, sequential processing. Give them a prose document with 30 requirements scattered across 10 paragraphs, and they'll miss things. Give them a numbered checklist where each item is a concrete action with a checkbox, and they'll march through it reliably.
The NEW_REPO_CHECKLIST.md walks through repo creation in five phases:
# 1. Initialize
- [ ] `git init`
- [ ] Ask the user for the license
# 2. First Commit (README only)
- [ ] Create README.md with all required sections
- [ ] `git add README.md && git commit`
# 3. Scaffolding (feature branch)
- [ ] Fetch .gitignore, .editorconfig from templates
- [ ] Create LICENSE, REPO_POLICIES.md, Dockerfile
- [ ] Configure Makefile with all required targets
# 4. Verify
- [ ] `make check` passes
- [ ] `make docker` succeeds
- [ ] No secrets in repo
# 5. Merge and Set Up
- [ ] Merge to main, install hooks, push
The EXISTING_REPO_CHECKLIST.md does the same for existing repos — check each item, fix gaps before starting your actual task:
# Formatting (do this first)
- [ ] Run `make fmt` as standalone commit before any other changes
# Required Files
- [ ] README.md, LICENSE, REPO_POLICIES.md, .gitignore, .editorconfig
- [ ] Dockerfile, .dockerignore, Gitea Actions workflow
# Makefile
- [ ] All required targets exist and work
- [ ] `make check` passes
# Git Hygiene
- [ ] Pre-commit hook installed, no secrets, all refs pinned
# Final
- [ ] `make check` passes, `docker build` succeeds
- [ ] Fix everything before starting your actual task
The same principle applies everywhere in the agent's configuration. We have checklists for:
- PR quality gates (
memory/checklist-pr.md) — what to verify before pushing, after sub-agent pushes, before assigning to the human - Medication actions (
memory/checklist-medications.md) — what to verify before reporting status or sending reminders - Flight actions (
memory/checklist-flights.md) — what to verify before stating flight times - Messaging (
memory/checklist-messaging.md) — what to verify before sending any message (URLs resolve? times converted? status verified?)
Each checklist is loaded on-demand (referenced from a checklist index at the top of MEMORY.md). The agent reads the relevant checklist before the relevant action. It's the same pattern as REPO_POLICIES + NEW_REPO_CHECKLIST: prose document for understanding, checklist for execution.
The meta-lesson: AI agents are linear thinkers. They follow step-by-step instructions reliably. They follow scattered prose requirements unreliably. Structure your rules as checklists, accept the redundancy, and treat it as a feature — not a code smell.
Putting It All Together
The system works as a loop:
- Session starts → read SOUL, USER, daily-context, today's daily file
- Message arrives → check daily-context for state changes (sleep gap? location change? meds overdue?), respond to the message, update state
- Heartbeat fires → check inbox, projects, flights, sync workspace
- External event (Gitea poller wake) → process notification, respond via appropriate channel
- Session ends → state persists in files for next session
The files are the continuity. The agent is stateless; the workspace is not.
Table of Contents
- Workspace Bootstrapping
- Daily Context — The State File
- Sitrep (Situation Report)
- Sleep Tracking
- Location & Timezone Tracking
- Medication Tracking
- Flight & Travel Logging
- Landing Checklist
- Memory Architecture
- Heartbeat Configuration
- Gitea Integration & Notification Polling
- Sub-Agent Management
- Urgent Notifications via ntfy
- Group Chat Behavior
- Cron vs Heartbeat — When to Use Each
- Requirement Capture
Workspace Bootstrapping
Your workspace is the agent's home directory. Core files:
workspace/
├── AGENTS.md # Responsibilities, rules, procedures
├── SOUL.md # Personality, tone, values
├── USER.md # Info about your human (name, tz, prefs)
├── IDENTITY.md # Agent's own identity
├── HEARTBEAT.md # What to check on heartbeat polls
├── MEMORY.md # Curated long-term memory
├── TOOLS.md # Environment-specific notes (hosts, keys, devices)
└── memory/
├── daily-context.json
├── YYYY-MM-DD.md # Daily raw notes
├── sleep-log.csv
├── location-log.csv
├── flight-log.csv
├── medications-log-YYYY-MM.csv
├── medications-instructions.md
├── landing-checklist.md
├── checklist-*.md # Various operational checklists
└── heartbeat-state.json
Session startup prompt (put in AGENTS.md):
## Every Session
Before doing anything else:
1. Read `SOUL.md` — this is who you are
2. Read `USER.md` — this is who you're helping
3. Read `memory/daily-context.json` — current state (location, timezone, sleep,
meds)
4. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
5. **If in MAIN SESSION** (direct chat with your human): Also read `MEMORY.md`
Don't ask permission. Just do it.
This ensures the agent always has context on wake. The key insight is that the agent wakes up fresh every session — files ARE its memory.
Daily Context — The State File
This is the single most important file. It's a JSON blob that every session reads on every message. It tracks the current state of your human.
Schema: memory/daily-context.json
{
"isSleeping": false,
"lastKnownWakeTime": "2026-02-28T12:30:00+07:00",
"predictedSleepTime": "2026-03-01T05:30:00+07:00",
"predictedWakeTime": "2026-03-01T12:45:00+07:00",
"hasTakenDailyMedsToday": false,
"dailyMedsTimestamp": "2026-02-27T11:43:00+07:00",
"lastCaffeineTimestamp": null,
"currentLocation": "City (IATA)",
"currentTimezone": "Asia/Bangkok",
"currentLodging": "Hotel Name",
"travelLeg": "Description of current travel phase",
"nearestAirport": "ICAO code",
"lastMessageFromUser": "2026-02-28T05:20:00+07:00",
"lastMessageChannel": "mattermost",
"isAvailable": true,
"lastUpdated": "2026-02-28T06:00:00+07:00",
"lastSleep": "actual: 2026-02-28 05:15-12:30 ICT (~7h15m). Stable pattern."
}
AGENTS.md prompt:
## Daily Context
Stored in `memory/daily-context.json`. All agents: review on every
conversation/message to detect if any state has changed. Update the JSON file
directly.
Why it works:
- Every session reads this first, so the agent always knows where you are
- Sleep predictions help the agent decide when to alert vs stay quiet
- Medication timestamps enable overdue detection
- Timezone field means the agent always converts times correctly
lastMessageFromUserlets the agent infer activity gaps = sleep
Sitrep (Situation Report)
The sitrep is triggered by a keyword (e.g. "sitrep") and produces a concise status briefing. The key design feature is that the agent can add sections dynamically based on what it thinks you should know.
AGENTS.md prompt:
### Sitrep (Situation Report)
When the user says "sitrep", provide a concise summary covering:
1. **Daily meds** — taken today or not
2. **Interval meds** — any due/overdue or upcoming in next 48h. ALWAYS include
"X days from now" countdown. ALWAYS include day of week.
3. **Open issues assigned to me** — count
4. **Open issues assigned to user** — count + brief summary
5. **Upcoming travel** — flights in next 72h with times/airports. ALWAYS also
include the next known flight even if beyond 72h.
6. **Upcoming appointments** — next 48h
7. **Todo list** — overdue, due today, due tomorrow (separate categories)
8. **Unbooked travel** — flights that need booking. Flag deadlines.
9. **Sleep conflicts** — appointments in next 48h that fall within predicted
sleep window
10. **Sleep** — one-line summary of last sleep + drift trend + concerns
11. **Unanswered questions** — questions the agent asked that haven't been
answered
12. **Weather alerts** — only if significant or extreme. Use METAR from
aviationweather.gov. Omit if nothing notable.
13. **Overdue reminders** — anything pending
14. **Open projects** — one-line status per project
Before generating a sitrep, review the last 3 days of daily memory files for
context. **If anything notable isn't covered by the items above — recent
lessons, pending decisions, things you think the user should know about — add
additional sections as needed.**
Omit any item that is "none" or zero. Keep it scannable. Use bullet points, not
prose. Only surface things the user might not know about or needs to act on.
Key design decisions:
- "Add additional sections as needed" — this is the magic line. It lets the agent surface things you didn't think to ask about. If there's a pending decision from 2 days ago, or a lesson from a recent mistake, it'll include it.
- "Omit any item that is none or zero" — keeps it clean. No "Overdue reminders: none" clutter.
- Relative times ("4 days from now, Tuesday") are more useful than bare dates
- Review last 3 days ensures the agent has recent context, not just today
- The sitrep pulls from multiple data sources (daily context, calendars, issue trackers, memory files) — it's a dashboard in prose form
Time display tip:
Include current time in local timezone, UTC, and any other relevant timezone at
the top.
This anchors the reader and makes all the relative times unambiguous.
Sleep Tracking
The agent infers sleep from message activity gaps, not from explicit "I'm going to sleep" statements (though those help too).
File: memory/sleep-log.csv
Date,SleepTime,WakeTime,TimeZone,Duration,Status,Notes
2026-02-07,08:30,15:30,America/Los_Angeles,7h00m,actual,Approximate times
2026-02-08,~12:00,~15:30,America/Los_Angeles,~3h30m,actual,Short sleep
AGENTS.md prompt:
### Sleep Tracking
- **`memory/sleep-log.csv`** — columns: Date, SleepTime, WakeTime, TimeZone,
Duration, Status, Notes
- Infer sleep/wake times from message activity and explicit statements
- User's sleep pattern drifts later each day; typical duration 4–8 hours
- Update daily-context.json isSleeping field accordingly
- **On every message from user:** if there was a communication gap overlapping
predicted sleep time, infer a sleep window (sleep start = last activity before
gap, wake = first activity after gap). Activity includes direct messages, git
commits/comments, scheduled flight departures, and any other observable
actions — not just chat messages. Log based on observed gaps only, never from
mathematical predictions.
Daily context fields for sleep:
{
"isSleeping": false,
"lastKnownWakeTime": "2026-02-28T12:30:00+07:00",
"predictedSleepTime": "2026-03-01T05:30:00+07:00",
"predictedWakeTime": "2026-03-01T12:45:00+07:00",
"lastSleep": "actual: 2026-02-28 05:15-12:30 ICT (~7h15m). Stable pattern."
}
How prediction works:
The agent observes the pattern drift (e.g., sleeping 30min later each day) and extrapolates. Predictions are updated after each actual observation. The agent uses predictions to:
- Avoid sending non-urgent alerts during predicted sleep
- Flag sleep conflicts with upcoming appointments in sitreps
- Factor caffeine intake into predictions (caffeine = 5-6h minimum awake)
Caffeine integration:
{
"lastCaffeineTimestamp": "2026-02-10T00:45:00-08:00"
}
Track caffeine in daily-context.json. If the user has caffeine, adjust sleep prediction forward by their typical caffeine-to-sleep duration.
Location & Timezone Tracking
The agent always knows where you are and adjusts all time displays accordingly.
File: memory/location-log.csv
Date,City,Country,Timezone
2026-02-08,Las Vegas,US,America/Los_Angeles
2026-02-11,Berlin,DE,Europe/Berlin
2026-02-18,Bangkok,TH,Asia/Bangkok
Timezone rules (AGENTS.md):
## Timezone/Time Rules
- System clock is PST. User's TZ is in daily-context.json — always convert.
- "Today"/"tomorrow" = user's local TZ, not system clock.
- Never state times without explicit conversion.
- When logging medication times, always use the user's current timezone.
Why this matters:
OpenClaw runs on a server (probably in a fixed timezone). Your human travels. Without explicit timezone tracking:
- "Take your meds" fires at 3am local time
- "Today" means the wrong day
- Sleep predictions are off by hours
- Calendar events show wrong times
The daily-context.json currentTimezone field is the single source of truth.
Everything else derives from it.
Medication Tracking
This is a safety-critical system. The design prioritizes never missing a dose and never double-dosing over convenience.
Files:
memory/medications-instructions.md— the authoritative medication list, dosages, schedules, and safety rulesmemory/medications-log-YYYY-MM.csv— one row per ingestion, split by monthmemory/checklist-medications.md— pre-action verification checklist
Log format: memory/medications-log-YYYY-MM.csv
Date,Time,TimeZone,Medication,Dosage
2026-02-27,11:43,Asia/Bangkok,Metoprolol,50 mg
2026-02-27,11:43,Asia/Bangkok,Omeprazole,40 mg
2026-02-27,11:43,Asia/Bangkok,Aspirin,162 mg
Instructions file structure (memory/medications-instructions.md):
# Medication Instructions
## General Rules
- Maintain cumulative CSV logs split by month as the authoritative record
- CSV columns (fixed order): Date,Time,TimeZone,Medication,Dosage
- "Today"/"yesterday"/"tomorrow" always mean relative to currentTimezone in
daily-context.json — NEVER relative to system clock
- Always spell medication names correctly; silently correct transcription errors
## Daily Medications
One batch per calendar day unless explicitly confirmed otherwise. "Log my daily
meds" = log each as a separate row.
**CRITICAL: Never trust hasTakenDailyMedsToday without verifying
dailyMedsTimestamp.**
- Resolve the current date in user's currentTimezone FIRST
- Check if dailyMedsTimestamp falls on THAT date in THAT timezone
- If the timestamp is from a previous calendar day, meds are NOT taken today
regardless of the boolean
**Ideal dosing window:** every 24h ±4h. Hard minimum: 14h between doses.
| Medication | Dosage |
| ----------- | ------ |
| (your meds) | (dose) |
## Interval-Based Medications
Scheduling anchored to last actual dose, NOT intended dates.
| Medication | Dosage | Interval |
| ---------- | ------ | ------------ |
| (med name) | (dose) | Every X days |
**Missed dose rules:**
- If missed, next dose date is unknown until the missed dose is taken and logged
- Interval restarts from actual ingestion timestamp
- Missed doses block future scheduling
## Safety Rules (HIGHEST PRIORITY)
1. Always triple-check before instructing any medication:
- Last actual dose taken
- Required dosing interval
- Current eligibility without overdose risk
2. If any ambiguity exists, **do not instruct dosing**
3. Immediately block logging if same-day duplicate daily-med batch or other
overdose pattern appears
4. Require explicit confirmation in any overdose-risk scenario
Verification checklist (memory/checklist-medications.md):
# Medications Checklist
## Before reporting medication status
1. Check medications-log-YYYY-MM.csv for actual entries, not dailyMedsTimestamp
2. Verify dailyMedsTimestamp is from today in user's timezone (not system clock)
3. Cross-reference medications-instructions.md for what's due
## Before sending medication reminders
1. Confirm current time in user's timezone
2. Check if already taken today (verify against CSV, not boolean)
3. For interval meds: calculate days since last dose from CSV
4. Never send PII (medication names) via public notification channels
Overdue escalation:
**CRITICAL: If daily meds are >26h since last dose, escalate aggressively.** Use
urgent notification channel if chat messages go unacknowledged. Do not let this
slide silently. Always include hours since last dose in reminders (e.g. "daily
meds overdue — last dose was 27h ago").
Why the double-check on the boolean:
The daily-context.json has a hasTakenDailyMedsToday boolean AND a
dailyMedsTimestamp. A midnight-reset cron flips the boolean to false. But if
the user is in a timezone ahead of the server, the boolean may reset before
their actual "today" ends — or after it began. The rule: always verify the
timestamp falls on today's date in the user's timezone. The boolean is a
convenience hint, not the source of truth.
Flight & Travel Logging
Files:
memory/flight-log.csv— one row per flight segmentmemory/location-log.csv— one row per calendar daymemory/travel.md— upcoming travel plans, booking statusmemory/checklist-flights.md— pre-action checklist
Flight log format:
Date,FlightNumber,Origin,Destination,Duration,Alliance
2026-02-10,DL9670,LAS,AMS,10h10m,SkyTeam
2026-02-10,KL1856,AMS,BER,1h25m,SkyTeam
Flight prep blocks (put in HEARTBEAT.md):
## Flight Prep Blocks (daily)
Run `khal list today 7d`. For flights missing "shower and dress" / "travel to
airport" blocks, create them:
- shower_start = flight_departure - airport_buffer - travel_buffer - 1h
- Airport buffer: 2h domestic, 2.5h international
- Travel to airport: 1h default
- Create "shower and dress" block at shower_start (1h duration)
- Create "travel to airport" block after shower
- Set cron reminders: 15min before shower start, at departure time
- Skip layover connections (already in airport)
This means the agent automatically works backwards from flight times to create preparation blocks in the calendar. No manual planning needed.
Landing Checklist
Triggered automatically after every flight lands. The agent doesn't wait to be asked.
File: memory/landing-checklist.md
# Landing Checklist
Run this after EVERY flight, regardless of whether location/timezone changes.
## Immediate (within minutes of landing)
- [ ] Update daily-context.json → currentLocation, currentLodging,
currentTimezone, nearestAirport, travelLeg
- [ ] Update midnight-reset cron job timezone to new currentTimezone
- [ ] Update location-log.csv with new city for today
- [ ] Log flight segment in flight-log.csv
## Within 1 hour
- [ ] Check if daily meds are due/overdue (timezone change may shift the window)
- [ ] Check if any interval meds are due today
- [ ] Sync calendar and review next 48h for the new timezone
- [ ] Check for any cron jobs with hardcoded timezones that need updating
## If entering a new country
- [ ] Check for medication resupply needs
- [ ] Note any upcoming reminders that reference the old timezone
AGENTS.md prompt:
### Landing Checklist
- After EVERY flight the user takes, run through `memory/landing-checklist.md`
- Trigger: calendar event landing time, or user messages after a flight
- Do not wait to be asked — run it proactively
Memory Architecture
The two-tier memory system: daily files (raw) + MEMORY.md (curated).
Daily files: memory/YYYY-MM-DD.md
Raw logs of what happened each day. Topics discussed, decisions made, tasks completed. Every session can read these.
# 2026-02-28
## Topics
| Time | Topic | Session |
| ----- | ---------------------------- | ------- |
| 14:30 | Discussed PR review workflow | main |
| 16:00 | Medication logged | main |
## Notes
- Decided to switch deployment strategy for project X
- User prefers Y approach over Z — standing rule
MEMORY.md — Long-term curated memory
## 🧠 MEMORY.md - Your Long-Term Memory
- **ONLY load in main session** (direct chats with your human)
- **DO NOT load in shared contexts** (group chats, sessions with others)
- This is for **security** — contains personal context that shouldn't leak
- Write significant events, thoughts, decisions, opinions, lessons learned
- This is your curated memory — the distilled essence, not raw logs
- Over time, review daily files and update MEMORY.md with what's worth keeping
Memory maintenance (in AGENTS.md or HEARTBEAT.md):
### 🔄 Memory Maintenance (During Heartbeats)
Periodically (every few days), use a heartbeat to:
1. Read through recent memory/YYYY-MM-DD.md files
2. Identify significant events, lessons, or insights worth keeping long-term
3. Update MEMORY.md with distilled learnings
4. Remove outdated info from MEMORY.md that's no longer relevant
Think of it like reviewing a journal and updating a mental model. Daily files
are raw notes; MEMORY.md is curated wisdom.
Critical rule — write it down:
### 📝 Write It Down - No "Mental Notes"!
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
- "Mental notes" don't survive session restarts. Files do.
- When someone says "remember this" → update memory file
- When you learn a lesson → update AGENTS.md or relevant file
- When you make a mistake → document it so future-you doesn't repeat it
- **Text > Brain** 📝
Heartbeat Configuration
Heartbeats are periodic polls from OpenClaw. The agent can do background work or stay quiet.
HEARTBEAT.md structure:
# HEARTBEAT.md
## Inbox Check (PRIORITY)
(check whatever notification sources apply to your setup — e.g. Gitea
notifications, emails, issue trackers)
## Flight Prep Blocks (daily)
(create calendar prep blocks for upcoming flights)
## Open Projects Review
(check project status, take next steps, or ask if blocked)
## Workspace Sync
(commit and push workspace changes)
## Rules
- Post status updates to a designated channel, not the main chat
- Update state files after any state change
- If nothing needs attention, reply HEARTBEAT_OK
## Output Rules
Never send internal thinking or status narration to user's DM. Output should be:
- HEARTBEAT_OK (if nothing needs attention)
- A direct question or alert (if action needed)
- Work narration → send to a status channel via message tool
Tracking heartbeat state: memory/heartbeat-state.json
{
"lastChecks": {
"gitea": 1703280000,
"calendar": 1703260800,
"weather": null
},
"lastWeeklyDocsReview": "2026-02-24"
}
Heartbeat vs Cron:
**Use heartbeat when:**
- Multiple checks can batch together
- You need conversational context from recent messages
- Timing can drift slightly (every ~30 min is fine)
- You want to reduce API calls by combining periodic checks
**Use cron when:**
- Exact timing matters ("9:00 AM sharp every Monday")
- Task needs isolation from main session history
- You want a different model or thinking level for the task
- One-shot reminders ("remind me in 20 minutes")
Gitea Integration & Notification Polling
For self-hosted Gitea instances, you can set up a notification poller that injects Gitea events (issue assignments, PR reviews, @-mentions) into the agent's session.
Workflow rules (HEARTBEAT.md / AGENTS.md):
## Gitea Work Scope
Find issues/PRs assigned to me or where I'm @-mentioned → do the work.
- If @-mentioned with work but not assigned → assign myself, remove other
assignees
- When nothing is assigned to me, my Gitea work is done
- PRs assigned to user = their queue, not my backlog
Workflow: issue → branch → PR "(closes #X)" → review/rework → assign user when
all checks pass + reviewed.
### Rules
- Respond in the medium addressed: Gitea → Gitea comment, not chat
- Before acting on ANY issue or PR: read ALL existing comments first
- Never do work on a PR/issue that isn't assigned to you
- Work done, no more needed → close the issue
- Your part done, need feedback → unassign yourself, assign next person
Notification poller:
A Python script that polls the Gitea notifications API and injects events into OpenClaw sessions. It runs via launchd/systemd. Ask me for the full source code if you want to set this up — I can share it (it's a general-purpose tool with no PII).
Sub-Agent Management
For complex coding tasks, spawn isolated sub-agents.
Key rules:
### Sub-Agent Git Isolation (MANDATORY)
- NEVER let multiple sub-agents share the same git clone
- Each sub-agent MUST clone to a unique temporary directory (e.g. `mktemp -d`)
- When spawning, always instruct: `cd $(mktemp -d) && git clone <url> . && ...`
- Dirty working directories cause false CI results
### Sub-Agent PR Quality Gate (MANDATORY)
- `make check` must pass with ZERO failures. No exceptions.
- Pre-existing failures are YOUR problem. Fix them as part of your PR.
- NEVER modify linter config to make checks pass. Fix the code.
- Every PR must include full `make check` output
- Rebase before and after committing
- Never self-review
Urgent Notifications via ntfy
For time-sensitive alerts when chat messages might go unacknowledged (e.g., overdue medications):
## Urgent Notifications
Send urgent messages via ntfy.sh:
curl -d "MESSAGE HERE" ntfy.sh/YOUR-PRIVATE-TOPIC-ID
Use this for time-sensitive alerts (overdue meds, critical issues, etc.).
**Never send PII via ntfy.** Keep messages generic (e.g. "daily meds overdue"
not medication names or personal details).
Create a random topic ID for your ntfy channel. The agent can curl to it
directly.
Group Chat Behavior
Rules for when the agent is in group chats with multiple people:
### Know When to Speak
**Respond when:**
- Directly mentioned or asked a question
- You can add genuine value (info, insight, help)
- Something witty/funny fits naturally
- Correcting important misinformation
**Stay silent when:**
- Just casual banter between humans
- Someone already answered the question
- Your response would just be "yeah" or "nice"
- The conversation is flowing fine without you
**The human rule:** Humans don't respond to every message. Neither should you.
Quality > quantity.
### React Like a Human
Use emoji reactions naturally:
- Appreciate something but don't need to reply → 👍, ❤️
- Something made you laugh → 😂
- Interesting/thought-provoking → 🤔, 💡
- Acknowledge without interrupting → ✅, 👀
One reaction per message max.
Requirement Capture
Never lose a rule or preference your human states:
### Requirement Capture (MANDATORY)
- **Every single requirement, rule, preference, or instruction the user provides
MUST be captured in the daily memory file immediately**
- This includes: project rules, workflow preferences, corrections, new policies,
technical decisions
- If the user says something should be done a certain way, write it down in
memory/YYYY-MM-DD.md AND in MEMORY.md if it's a standing rule
- Nothing is to be missed. If in doubt, log it.
Sensitive Output Routing — Audience-Aware Responses
A critical security pattern: the audience determines what you can say, not who asked. If your human asks for a sitrep (or any sensitive info) in a group channel, you can't just dump it there — other people can read it.
AGENTS.md / checklist prompt:
## Sensitive Output Routing (CRITICAL)
- NEVER output sensitive information in any non-private channel, even if your
human asks for it
- This includes: PII, secrets, credentials, API keys, and sensitive operational
information (flight numbers/times/dates, locations, travel plans, medical
info, financial details, etc.)
- If a request would produce any of the above in a shared channel: send the
response via DM instead, and reply in-channel with "sent privately"
- The rule is: the audience determines what you can say, not who asked
- This applies to: group chats, public issue trackers, shared Mattermost
channels, Discord servers — anywhere that isn't a 1:1 DM
Why this matters:
This is a real failure mode. If someone asks "sitrep" in a group channel and you respond with medication names, partner details, travel dates, hotel names, or API credentials — you just leaked all of that to everyone in the channel. The human asking is authorized to see it; the channel audience is not. Always check WHERE you're responding, not just WHO asked.
General Tips
Bias toward action
Don't ask blocking questions with obvious answers. Figure it out. Come back with results, not requests for permission.
Checklists over trust
For safety-critical operations (meds, deployments, external communications),
maintain checklists in memory/checklist-*.md and reference them from
AGENTS.md. The agent reads the checklist before acting.
State repo
Keep your workspace in a git repo that auto-syncs. This gives you version history and recovery:
### State Repo
- Commit and push workspace changes after any change
- Push promptly — remote should reflect workspace in near-realtime
- Auto-sync cron runs every 6h as a safety net
Error handling philosophy
## On Errors
When something goes wrong:
1. Identify what input caused the wrong output
2. Fix the input (rule, checklist, prompt, automation)
3. Move on
No apologies. No promises. Fix the system.
Never weaken checks
## On Integrity
Never cheat. When a check fails, fix the code — don't weaken the check. When a
test fails, fix the bug — don't loosen the assertion. When a linter flags
something, fix the finding — don't suppress the rule.
This document is a living reference. Patterns here have been tested in production and refined through real-world use.