1934 lines
70 KiB
Markdown
1934 lines
70 KiB
Markdown
# 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.
|
||
|
||
```markdown
|
||
## ⛔ 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
|
||
`hasTakenDailyMedsToday` boolean AND a `dailyMedsTimestamp`. 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 both polls and dispatches. It polls the Gitea
|
||
notifications API every 15 seconds, triages each notification (checking
|
||
@-mentions and assignments), marks them as read, and spawns one isolated agent
|
||
session per actionable item via `openclaw cron add --session isolated`.
|
||
|
||
The poller also runs a secondary **label scan** every 2 minutes, checking all
|
||
watched repos for open issues/PRs with the `bot` label that are unassigned. This
|
||
catches cases where the agent chain broke — an agent timed out or crashed
|
||
without spawning the next agent. It also picks up newly-labeled issues that
|
||
didn't trigger a notification.
|
||
|
||
Key design decisions:
|
||
|
||
- **The poller IS the dispatcher.** No flag files, no heartbeat dependency. The
|
||
poller triages notifications and spawns agents directly.
|
||
- **Marks notifications as read immediately.** Prevents re-dispatch on the next
|
||
poll cycle.
|
||
- **Assigns the bot user at dispatch time.** Before spawning the agent, the
|
||
poller assigns the bot account to the issue via API. This prevents race
|
||
conditions — subsequent scans skip assigned issues. The spawned agent doesn't
|
||
need to claim ownership; it's already claimed.
|
||
- **Persistent dispatch tracking.** Dispatched issues are tracked in a JSON
|
||
file on disk (not just in memory), surviving poller restarts. Entries
|
||
auto-prune after 1 hour.
|
||
- **30-minute re-dispatch cooldown.** Safety net for broken agent chains. Normal
|
||
operation uses agent-to-agent chaining (each agent spawns the next), so the
|
||
poller only re-dispatches if the chain breaks.
|
||
- **Concurrency cap.** The poller checks how many agents are currently running
|
||
(`openclaw cron list`) and defers dispatch if the cap is reached.
|
||
- **Stale agent reaper.** Each scan cycle, kills agent sessions running longer
|
||
than 10 minutes. The `--timeout-seconds` flag isn't always enforced by
|
||
OpenClaw, so the poller handles cleanup itself.
|
||
- **`merge-ready` skip.** The label scan skips issues already labeled
|
||
`merge-ready` — those are in the human's court.
|
||
- **Template-based prompts.** The poller reads two workspace files (a dispatch
|
||
header with `{{variable}}` placeholders, and a workflow rules document),
|
||
concatenates them, substitutes variables, and passes the result as the
|
||
agent's `--message`. This keeps all instructions in version-controlled
|
||
workspace files with a single source of truth.
|
||
- **Zero dependencies.** Python stdlib only. Runs anywhere.
|
||
|
||
Response time: ~15–30s from notification to agent starting work.
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
Gitea notification poller + dispatcher.
|
||
|
||
Two polling loops:
|
||
1. Notification-based: detects new @-mentions and assignments, dispatches
|
||
agents for actionable notifications.
|
||
2. Label-based: periodically scans for issues/PRs with the 'bot' label
|
||
that are unassigned (available for work). Catches broken agent chains
|
||
and newly-labeled issues.
|
||
|
||
The poller assigns the bot user to the issue BEFORE spawning the agent,
|
||
preventing race conditions where multiple scans dispatch for the same issue.
|
||
|
||
Required env vars:
|
||
GITEA_URL - Gitea instance URL
|
||
GITEA_TOKEN - Gitea API token
|
||
|
||
Optional env vars:
|
||
POLL_DELAY - Seconds between notification polls (default: 15)
|
||
COOLDOWN - Seconds between dispatch batches (default: 30)
|
||
BOT_SCAN_INTERVAL - Seconds between label scans (default: 120)
|
||
MAX_CONCURRENT_AGENTS - Max simultaneous agents (default: 10)
|
||
REAP_AGE_SECONDS - Kill agents older than this (default: 600)
|
||
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"))
|
||
BOT_SCAN_INTERVAL = int(os.environ.get("BOT_SCAN_INTERVAL", "120"))
|
||
MAX_CONCURRENT_AGENTS = int(os.environ.get("MAX_CONCURRENT_AGENTS", "10"))
|
||
REAP_AGE_SECONDS = int(os.environ.get("REAP_AGE_SECONDS", "600"))
|
||
REDISPATCH_COOLDOWN = 1800 # 30 min safety net for broken agent chains
|
||
OPENCLAW_BIN = os.environ.get("OPENCLAW_BIN", "openclaw")
|
||
BOT_USER = os.environ.get("BOT_USER", "clawbot")
|
||
|
||
WORKSPACE = os.path.expanduser("~/.openclaw/workspace")
|
||
DISPATCH_HEADER = os.path.join(
|
||
WORKSPACE, "taskprompts", "how-to-handle-gitea-notifications.md"
|
||
)
|
||
WORKFLOW_DOC = os.path.join(
|
||
WORKSPACE, "taskprompts", "how-to-work-on-a-gitea-issue-or-pr.md"
|
||
)
|
||
DISPATCH_STATE_PATH = os.path.join(
|
||
os.path.dirname(os.path.abspath(__file__)), ".dispatch-state.json"
|
||
)
|
||
|
||
# Repos to watch for bot-labeled issues
|
||
WATCHED_REPOS = [
|
||
# "org/repo1",
|
||
# "org/repo2",
|
||
]
|
||
|
||
# Dispatch tracking (persisted to disk)
|
||
dispatched_issues: dict[str, float] = {}
|
||
|
||
|
||
def _load_dispatch_state() -> dict[str, float]:
|
||
try:
|
||
with open(DISPATCH_STATE_PATH) as f:
|
||
state = json.load(f)
|
||
now = time.time()
|
||
return {k: v for k, v in state.items() if now - v < 3600}
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
return {}
|
||
|
||
|
||
def _save_dispatch_state():
|
||
try:
|
||
with open(DISPATCH_STATE_PATH, "w") as f:
|
||
json.dump(dispatched_issues, f)
|
||
except OSError as e:
|
||
print(f"WARN: Could not save dispatch state: {e}", file=sys.stderr)
|
||
|
||
|
||
def gitea_api(method, path, data=None):
|
||
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: {method} {path}: {e}", file=sys.stderr, flush=True)
|
||
return None
|
||
|
||
|
||
def load_template() -> str:
|
||
"""Load dispatch header + workflow doc, concatenated."""
|
||
parts = []
|
||
for path in [DISPATCH_HEADER, WORKFLOW_DOC]:
|
||
try:
|
||
with open(path) as f:
|
||
parts.append(f.read())
|
||
except FileNotFoundError:
|
||
print(f"ERROR: File not found: {path}", file=sys.stderr)
|
||
sys.exit(1)
|
||
return "\n\n---\n\n".join(parts)
|
||
|
||
|
||
def render_template(template, repo_full, issue_number, title,
|
||
subject_type, reason):
|
||
return (
|
||
template
|
||
.replace("{{repo_full}}", repo_full)
|
||
.replace("{{issue_number}}", str(issue_number))
|
||
.replace("{{title}}", title)
|
||
.replace("{{subject_type}}", subject_type)
|
||
.replace("{{reason}}", reason)
|
||
.replace("{{gitea_url}}", GITEA_URL)
|
||
.replace("{{gitea_token}}", GITEA_TOKEN)
|
||
.replace("{{openclaw_bin}}", OPENCLAW_BIN)
|
||
.replace("{{bot_user}}", BOT_USER)
|
||
# Add your own variables here (e.g. git_channel)
|
||
)
|
||
|
||
|
||
def count_running_agents() -> int:
|
||
try:
|
||
result = subprocess.run(
|
||
[OPENCLAW_BIN, "cron", "list"],
|
||
capture_output=True, text=True, timeout=10,
|
||
)
|
||
return sum(1 for line in result.stdout.splitlines()
|
||
if "running" in line or "idle" in line)
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def spawn_agent(template, repo_full, issue_number, title,
|
||
subject_type, reason):
|
||
dispatch_key = f"{repo_full}#{issue_number}"
|
||
last = dispatched_issues.get(dispatch_key)
|
||
if last and (time.time() - last) < REDISPATCH_COOLDOWN:
|
||
return
|
||
|
||
if count_running_agents() >= MAX_CONCURRENT_AGENTS:
|
||
print(f" → Concurrency limit reached, deferring {dispatch_key}",
|
||
flush=True)
|
||
return
|
||
|
||
dispatched_issues[dispatch_key] = time.time()
|
||
|
||
# Assign bot user immediately to prevent races
|
||
gitea_api("PATCH", f"/repos/{repo_full}/issues/{issue_number}",
|
||
{"assignees": [BOT_USER]})
|
||
|
||
repo_short = repo_full.split("/")[-1]
|
||
job_name = f"gitea-{repo_short}-{issue_number}-{int(time.time())}"
|
||
msg = render_template(template, repo_full, issue_number, title,
|
||
subject_type, reason)
|
||
|
||
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:
|
||
_save_dispatch_state()
|
||
else:
|
||
dispatched_issues.pop(dispatch_key, None)
|
||
except Exception as e:
|
||
print(f"Spawn error: {e}", file=sys.stderr, flush=True)
|
||
dispatched_issues.pop(dispatch_key, None)
|
||
|
||
|
||
def is_actionable(notif):
|
||
"""Check if a notification warrants spawning an agent."""
|
||
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 for @-mentions in the latest comment
|
||
comments = gitea_api(
|
||
"GET", f"/repos/{repo_full}/issues/{number}/comments"
|
||
)
|
||
if comments:
|
||
last = comments[-1]
|
||
if last.get("user", {}).get("login") == BOT_USER:
|
||
return False, "own comment is latest", number
|
||
if f"@{BOT_USER}" in (last.get("body") or ""):
|
||
return True, "@-mentioned in comment", number
|
||
|
||
# Check for @-mention in issue body
|
||
body = issue.get("body", "") or ""
|
||
if f"@{BOT_USER}" in body:
|
||
return True, "@-mentioned in body", number
|
||
|
||
return False, "not mentioned", number
|
||
|
||
|
||
def scan_bot_labeled(template):
|
||
"""Scan for issues/PRs with 'bot' label that are unassigned."""
|
||
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"&labels=bot&sort=updated&limit=10",
|
||
) or []
|
||
for item in items:
|
||
number = str(item["number"])
|
||
dispatch_key = f"{repo_full}#{number}"
|
||
|
||
last = dispatched_issues.get(dispatch_key)
|
||
if last and (time.time() - last) < REDISPATCH_COOLDOWN:
|
||
continue
|
||
|
||
assignees = [
|
||
a.get("login", "") for a in item.get("assignees") or []
|
||
]
|
||
if BOT_USER in assignees:
|
||
continue
|
||
|
||
labels = [
|
||
l.get("name", "") for l in item.get("labels") or []
|
||
]
|
||
if "merge-ready" in labels:
|
||
continue
|
||
|
||
kind = "PR" if issue_type == "pulls" else "issue"
|
||
spawn_agent(
|
||
template, repo_full, number,
|
||
item.get("title", "")[:60],
|
||
"pull" if issue_type == "pulls" else "issue",
|
||
"bot label, unassigned",
|
||
)
|
||
|
||
|
||
def main():
|
||
global dispatched_issues
|
||
dispatched_issues = _load_dispatch_state()
|
||
|
||
if not GITEA_URL or not GITEA_TOKEN:
|
||
print("ERROR: GITEA_URL and GITEA_TOKEN required", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
template = load_template()
|
||
print(f"Poller started (poll={POLL_DELAY}s, cooldown={COOLDOWN}s, "
|
||
f"bot_scan={BOT_SCAN_INTERVAL}s, repos={len(WATCHED_REPOS)})",
|
||
flush=True)
|
||
|
||
seen_ids = set(
|
||
n["id"] for n in
|
||
(gitea_api("GET", "/notifications?status-types=unread") or [])
|
||
)
|
||
last_dispatch = 0
|
||
last_bot_scan = 0
|
||
|
||
while True:
|
||
time.sleep(POLL_DELAY)
|
||
now = time.time()
|
||
|
||
# --- Notification polling ---
|
||
notifs = gitea_api("GET", "/notifications?status-types=unread") or []
|
||
current_ids = {n["id"] for n in notifs}
|
||
new_ids = current_ids - seen_ids
|
||
if new_ids and now - last_dispatch >= COOLDOWN:
|
||
for n in [n for n in notifs if n["id"] in new_ids]:
|
||
nid = n.get("id")
|
||
if nid:
|
||
gitea_api("PATCH", f"/notifications/threads/{nid}")
|
||
is_act, reason, num = is_actionable(n)
|
||
if is_act:
|
||
repo = n["repository"]["full_name"]
|
||
title = n["subject"]["title"][:60]
|
||
stype = n["subject"].get("type", "").lower()
|
||
spawn_agent(template, repo, num, title, stype, reason)
|
||
last_dispatch = now
|
||
seen_ids = current_ids
|
||
|
||
# --- Bot label scan (less frequent) ---
|
||
if now - last_bot_scan >= BOT_SCAN_INTERVAL:
|
||
scan_bot_labeled(template)
|
||
last_bot_scan = now
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
Run it as a background service (launchd on macOS, systemd on Linux) with the env
|
||
vars set. It's intentionally simple — no frameworks, no async, no dependencies.
|
||
|
||
### 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:
|
||
|
||
```markdown
|
||
## 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_history` to 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:
|
||
|
||
1. **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.
|
||
|
||
2. **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 log` and `git checkout` to recover any
|
||
previous version.
|
||
|
||
3. **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.
|
||
|
||
```markdown
|
||
### 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](https://git.eeqj.de/sneak/prompts), a repo of standardized
|
||
policies that every project copies. The key document is
|
||
[REPO_POLICIES.md](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/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 check`** as a build step — if checks fail, the
|
||
Docker build fails, CI fails
|
||
- **`make check` depends on `fmt-check`, `lint`, and `test`** — all three must
|
||
pass
|
||
- **You can't skip any layer** — they're structurally linked
|
||
|
||
From
|
||
[REPO_POLICIES.md](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md):
|
||
|
||
> Every repo with software must have a root `Makefile` with these targets:
|
||
> `make test`, `make lint`, `make fmt` (writes), `make fmt-check` (read-only),
|
||
> `make check` (prereqs: `test`, `lint`, `fmt-check`), `make docker`, and
|
||
> `make hooks` (installs pre-commit hook).
|
||
|
||
> Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
|
||
> as a build step so the build fails if the branch is not green.
|
||
|
||
> Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
||
> runs `docker build .` on push. Since the Dockerfile already runs `make 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 types
|
||
> `make<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`, never `go 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 check` if local testing is possible, otherwise
|
||
> `make lint && make fmt-check`. The Makefile should provide a `make hooks`
|
||
> target to install the pre-commit hook.
|
||
|
||
Our PR checklist requires agents to install hooks after every clone:
|
||
|
||
```sh
|
||
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.yml` is standardized and must _NEVER_ be modified by an agent, only
|
||
> manually by the user.
|
||
|
||
This is enforced in our PR checklist:
|
||
|
||
```markdown
|
||
## 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](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md):
|
||
|
||
> Use platform-standard formatters: `black` for Python, `prettier` for
|
||
> JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
||
> two exceptions: four-space indents (except Go), and `proseWrap: always` for
|
||
> 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 for
|
||
> `make test` to be a no-op.
|
||
|
||
> `make test` must 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 -A` or `git add .`** — always stage files explicitly. Agents
|
||
love to `git 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 using agent-to-agent chaining. Each step
|
||
is handled by a separate, isolated agent session — the agent that writes code
|
||
never reviews it:
|
||
|
||
```markdown
|
||
## PR pipeline (every PR, no exceptions)
|
||
|
||
Worker agent → docker build . → push → label needs-review → spawn reviewer
|
||
Reviewer agent → review diff → PASS: docker build . → label merge-ready
|
||
→ FAIL: label needs-rework → spawn worker
|
||
Repeat until reviewer approves.
|
||
|
||
- docker build . is the ONLY authoritative check (runs make check inside)
|
||
- Never weaken tests/linters. Fix the code.
|
||
- Pre-existing failures are YOUR problem. Fix them as part of your PR.
|
||
```
|
||
|
||
The agent chain 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: build passes, code reviewed by a separate agent,
|
||
review feedback addressed, rebased. Anything less is still in the agent chain.
|
||
|
||
#### 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-check` in pre-commit hook
|
||
- Can't skip tests → `make check` depends on `make 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**](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/NEW_REPO_CHECKLIST.md)
|
||
— step-by-step when creating a repo from scratch
|
||
- [**EXISTING_REPO_CHECKLIST.md**](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/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](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/NEW_REPO_CHECKLIST.md)
|
||
walks through repo creation in five phases:
|
||
|
||
```markdown
|
||
# 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](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/EXISTING_REPO_CHECKLIST.md)
|
||
does the same for existing repos — check each item, fix gaps before starting
|
||
your actual task:
|
||
|
||
```markdown
|
||
# 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:
|
||
|
||
1. **Session starts** → read SOUL, USER, daily-context, today's daily file
|
||
2. **Message arrives** → check daily-context for state changes (sleep gap?
|
||
location change? meds overdue?), respond to the message, update state
|
||
3. **Heartbeat fires** → check inbox, projects, flights, sync workspace
|
||
4. **External event** (Gitea poller wake) → process notification, respond via
|
||
appropriate channel
|
||
5. **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
|
||
|
||
1. [Workspace Bootstrapping](#workspace-bootstrapping)
|
||
2. [Daily Context — The State File](#daily-context--the-state-file)
|
||
3. [Sitrep (Situation Report)](#sitrep-situation-report)
|
||
4. [Sleep Tracking](#sleep-tracking)
|
||
5. [Location & Timezone Tracking](#location--timezone-tracking)
|
||
6. [Medication Tracking](#medication-tracking)
|
||
7. [Flight & Travel Logging](#flight--travel-logging)
|
||
8. [Landing Checklist](#landing-checklist)
|
||
9. [Memory Architecture](#memory-architecture)
|
||
10. [Heartbeat Configuration](#heartbeat-configuration)
|
||
11. [Gitea Integration & Notification Polling](#gitea-integration--notification-polling)
|
||
12. [Sub-Agent Management](#sub-agent-management)
|
||
13. [Urgent Notifications via ntfy](#urgent-notifications-via-ntfy)
|
||
14. [Group Chat Behavior](#group-chat-behavior)
|
||
15. [Cron vs Heartbeat — When to Use Each](#cron-vs-heartbeat)
|
||
16. [Requirement Capture](#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):
|
||
|
||
```markdown
|
||
## 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`
|
||
|
||
```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:
|
||
|
||
```markdown
|
||
## 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
|
||
- `lastMessageFromUser` lets 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:
|
||
|
||
```markdown
|
||
### 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:
|
||
|
||
```markdown
|
||
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`
|
||
|
||
```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:
|
||
|
||
```markdown
|
||
### 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:
|
||
|
||
```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",
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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`
|
||
|
||
```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):
|
||
|
||
```markdown
|
||
## 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 rules
|
||
- `memory/medications-log-YYYY-MM.csv` — one row per ingestion, split by month
|
||
- `memory/checklist-medications.md` — pre-action verification checklist
|
||
|
||
### Log format: `memory/medications-log-YYYY-MM.csv`
|
||
|
||
```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`):
|
||
|
||
```markdown
|
||
# 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`):
|
||
|
||
```markdown
|
||
# 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:
|
||
|
||
```markdown
|
||
**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 segment
|
||
- `memory/location-log.csv` — one row per calendar day
|
||
- `memory/travel.md` — upcoming travel plans, booking status
|
||
- `memory/checklist-flights.md` — pre-action checklist
|
||
|
||
### Flight log format:
|
||
|
||
```csv
|
||
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):
|
||
|
||
```markdown
|
||
## 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`
|
||
|
||
```markdown
|
||
# 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:
|
||
|
||
```markdown
|
||
### 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.
|
||
|
||
```markdown
|
||
# 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
|
||
|
||
```markdown
|
||
## 🧠 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):
|
||
|
||
```markdown
|
||
### 🔄 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:
|
||
|
||
```markdown
|
||
### 📝 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:
|
||
|
||
```markdown
|
||
# 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`
|
||
|
||
```json
|
||
{
|
||
"lastChecks": {
|
||
"gitea": 1703280000,
|
||
"calendar": 1703260800,
|
||
"weather": null
|
||
},
|
||
"lastWeeklyDocsReview": "2026-02-24"
|
||
}
|
||
```
|
||
|
||
### Heartbeat vs Cron:
|
||
|
||
```markdown
|
||
**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):
|
||
|
||
```markdown
|
||
## 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:
|
||
|
||
```markdown
|
||
### 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)
|
||
|
||
- `docker build .` must pass. This is identical to CI and the only
|
||
authoritative check. 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.
|
||
- Rebase before and after committing
|
||
- Never self-review — each agent spawns a separate agent for review
|
||
```
|
||
|
||
---
|
||
|
||
## Urgent Notifications via ntfy
|
||
|
||
For time-sensitive alerts when chat messages might go unacknowledged (e.g.,
|
||
overdue medications):
|
||
|
||
```markdown
|
||
## 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:
|
||
|
||
```markdown
|
||
### 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:
|
||
|
||
```markdown
|
||
### 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:
|
||
|
||
```markdown
|
||
## 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:
|
||
|
||
```markdown
|
||
### 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
|
||
|
||
```markdown
|
||
## 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
|
||
|
||
```markdown
|
||
## 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._
|