From e48c79a7c5dff3bbe76a0e924805442c93cce4a7 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Tue, 26 May 2026 01:01:22 -0700 Subject: [PATCH] feat: migrate per-agent heartbeats to crons + forbid HEARTBEAT.md OpenClaw's per-agent agents.list[].heartbeat does not fire for squad sub-agents today, so every recurring wake must be driven by a cron in crons/jobs.json. Each squad's old HEARTBEAT.md procedure is now embedded in a heartbeat-pulse cron payload targeting the squad's agent on the same 2h cadence. The heartbeat field is removed from agent.json (now rejected as an unknown field) and HEARTBEAT.md is a forbidden filename anywhere in a bundle. Validator, schema, tests, docs, and create-squad / validate-squad skills updated to match; squad versions bumped. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/create-squad/SKILL.md | 69 ++-- .claude/skills/validate-squad/SKILL.md | 33 +- CLAUDE.md | 11 +- README.md | 3 +- agent.schema.json | 36 +- docs/bundle-reference.md | 126 ++++--- docs/creating-a-squad.md | 107 ++++-- docs/how-squads-work.md | 24 +- scripts/test-validator.mjs | 86 +---- scripts/validate.mjs | 98 ++--- .../agents/geo-agent/HEARTBEAT.md | 136 ------- .../ai-seo-squad/agents/geo-agent/MEMORY.md | 2 +- .../ai-seo-squad/agents/geo-agent/agent.json | 1 - squads/ai-seo-squad/crons/jobs.json | 13 + squads/ai-seo-squad/manifest.json | 2 +- squads/outreach-squad/ONBOARD.md | 2 +- squads/outreach-squad/SQUAD.md | 2 +- .../agents/outreach-agent/HEARTBEAT.md | 348 ------------------ .../agents/outreach-agent/SOUL.md | 20 +- .../agents/outreach-agent/agent.json | 3 +- squads/outreach-squad/crons/jobs.json | 8 +- squads/outreach-squad/manifest.json | 2 +- squads/reddit-squad/SQUAD.md | 2 +- .../agents/reddit-agent/HEARTBEAT.md | 131 ------- .../agents/reddit-agent/MEMORY.md | 2 +- .../agents/reddit-agent/agent.json | 1 - squads/reddit-squad/crons/jobs.json | 13 + squads/reddit-squad/manifest.json | 2 +- template/ONBOARD.md | 4 +- template/SQUAD.md | 8 +- template/agents/example-agent/HEARTBEAT.md | 78 ---- template/agents/example-agent/SOUL.md | 11 +- template/agents/example-agent/agent.json | 1 - template/crons/jobs.json | 13 + 34 files changed, 355 insertions(+), 1043 deletions(-) delete mode 100644 squads/ai-seo-squad/agents/geo-agent/HEARTBEAT.md delete mode 100644 squads/outreach-squad/agents/outreach-agent/HEARTBEAT.md delete mode 100644 squads/reddit-squad/agents/reddit-agent/HEARTBEAT.md delete mode 100644 template/agents/example-agent/HEARTBEAT.md diff --git a/.claude/skills/create-squad/SKILL.md b/.claude/skills/create-squad/SKILL.md index bf2a388..f607f87 100644 --- a/.claude/skills/create-squad/SKILL.md +++ b/.claude/skills/create-squad/SKILL.md @@ -25,14 +25,14 @@ Do not invent the contract from memory — read these files. Ask the user what they want, and don't scaffold until you have answers for all of it: - **The squad** — its purpose, and a kebab-case `name` (globally unique, ≤ 64 chars). -- **Each agent** — `id` (kebab-case), role / `description`, `model` (`haiku`/`sonnet`/`opus`, - string enum), `heartbeat` — a curated subset of [OpenClaw's - `agents.list[].heartbeat`](https://docs.openclaw.ai/gateway/config-agents#agents-defaults-heartbeat). - Only six sub-fields are accepted: `every`, `model`, `lightContext`, - `isolatedSession`, `skipWhenBusy`, `timeoutSeconds`. `every` is an OpenClaw duration - string in units `ms`/`s`/`m`/`h` (e.g. `"30m"`, `"2h"`, `"24h"`, `"0m"` to disable); - named values like `"daily"` are invalid. `heartbeat.model` is the same `haiku`/`sonnet`/`opus` - enum. Keep each agent single-lane and focused. +- **Each agent** — `id` (kebab-case), role / `description`, `model` + (`haiku`/`sonnet`/`opus`, string enum). Keep each agent single-lane and focused. +- **Recurring wakes** — for each agent, ask what schedule(s) it needs to wake on. These + become **crons** in `crons/jobs.json` (per-agent `agent.json#/heartbeat` does **not** + fire for squad sub-agents today, so every recurring wake is cron-driven). Get: each + cron's id + cron expression + timezone + the wake procedure that goes in `payload.text`. + The common pattern is one daily clock-driven cron + one `0 */2 * * *`-style pulse cron + for self-driven check-ins. - **Skills** — which are squad-wide (every agent gets them) vs agent-specific. - **Required identities** — external sites the squad needs connected, each with a reason. - **Required vault secrets** — each `{ key, label, type }`. @@ -44,7 +44,6 @@ Ask the user what they want, and don't scaffold until you have answers for all o `image-generation` / `image_generate` / `image`, `cron`. Anything else is rejected by the validator. Slack and voice/TTS are intentionally excluded — those are user-facing channels owned by the co-founder, not by a sub-agent. -- **Crons** — any scheduled jobs, and what each one does. - **Catalog metadata** — `tags` for the marketplace card (no `token_intensity` — it is deprecated and Pancake Cloud computes token usage automatically). @@ -56,25 +55,30 @@ Copy [`template/`](../../../template/) to `squads//`, then fill every file No per-agent runtime config in this file. Delete optional sections the squad doesn't use. - **`agents//agent.json`** for every agent — the per-agent runtime config (curated subset of OpenClaw's `agents.list[]`). Required: `id`, `description`. `model` is a - string from `haiku`/`sonnet`/`opus`. `heartbeat` is an object with up to six allowed - sub-fields: `every`, `model`, `lightContext`, `isolatedSession`, `skipWhenBusy`, - `timeoutSeconds`. `every` is an OpenClaw duration in `ms`/`s`/`m`/`h` (e.g. `"30m"`, - `"2h"`, `"24h"`); plain strings (`"daily"`) and named values are rejected. Pod-level - fields like `prompt`, `target`, `directPolicy`, `session`, `to`, `ackMaxChars` are - rejected. Top-level optional fields: `skills`, `contextInjection`, `bootstrapMaxChars`, - `params`. Unknown fields anywhere are rejected. -- **`agents//IDENTITY.md`, `SOUL.md`, and `HEARTBEAT.md`** for every agent; add - `agents//MEMORY.md` if useful. `HEARTBEAT.md` is **required** when `agent.json` - declares a heartbeat — keep it out of `SOUL.md` (behaviour) and `MEMORY.md` (pointer - index). + string from `haiku`/`sonnet`/`opus`. Top-level optional fields: `skills`, + `contextInjection`, `bootstrapMaxChars`, `params`. **`heartbeat` is rejected** — + OpenClaw's agent-level heartbeat does not fire for squad sub-agents, so every recurring + wake goes in `crons/jobs.json` instead. Unknown fields anywhere are rejected. +- **`agents//IDENTITY.md` and `SOUL.md`** for every agent; add + `agents//MEMORY.md` if useful. `HEARTBEAT.md` is a **forbidden filename** anywhere + in the bundle — move what would have gone there into the `payload.text` of a cron in + `crons/jobs.json` whose `sessionTarget` is the agent's id. +- **`crons/jobs.json`** — required whenever the agent has any recurring wake (which is + almost always). For the "heartbeat pulse" pattern, add a cron with id `heartbeat-pulse`, + `schedule.expr = "0 */2 * * *"` (or whatever cadence the user wants), `sessionTarget` + pointing at the agent, and the imperative wake procedure embedded in `payload.text`. If + the procedure is long, an alternative is to ship a `heartbeat-pulse` skill under the + agent's `agent.json#/skills` and have the cron's payload say only *"Load the + `heartbeat-pulse` skill and run it end to end."* — either shape is allowed. A cron run + that intentionally produces no output must instruct the agent to reply with the single + literal token `NO_REPLY` (OpenClaw's silent-turn sentinel — never write "do not respond"). - **Every skill file** referenced by `manifest.skills` or `agent.json#/skills`, in SKILL.md format (frontmatter `name` + `description`, then a procedure written as steps). - **`SQUAD.md`** — frontmatter is minimal: `tags` (recommended) and optional `preview_image`. The body is the marketplace catalog's source of truth for per-agent prose, so describe every agent here in user-facing language. - **`ONBOARD.md`** — the runnable onboarding script the co-founder executes after deploy. -- Add or delete the optional `crons/jobs.json` and squad-wide `MEMORY.md` depending on - Step 2. +- Add or delete the optional squad-wide `MEMORY.md` depending on Step 2. - **Strip every `` comment and placeholder** the template ships with. The validator errors on any unresolved TODO marker outside `template/`. @@ -90,18 +94,19 @@ Copy [`template/`](../../../template/) to `squads//`, then fill every file `vault_request`, connect identities via `browser_identity_add`, save answers to the agent's `MEMORY.md`, and create + dispatch a first task. It must fit `estimated_setup_minutes`. - `MEMORY.md` is a **thin index of pointers**, never a notebook. -- `HEARTBEAT.md` is the **imperative wake procedure** OpenClaw loads on every pulse — - not behaviour (that's `SOUL.md`), not pointers (that's `MEMORY.md`). It must require - the agent to **execute at least one task before closing the session** (no - orient-and-bail), and to write a **digest** to `memory/YYYY-MM-DD.md` before ending - the turn — what was done, what changed, what's still open, the next wake's first - move. `NO_REPLY` is only acceptable when nothing is actionable, with the reason - logged first. +- The **cron payload** is the imperative wake procedure the agent runs — not behaviour + (that's `SOUL.md`), not pointers (that's `MEMORY.md`). It must require the agent to + **execute at least one task before closing the session** (no orient-and-bail), and to + write a **digest** to `memory/YYYY-MM-DD.md` before ending the turn — what was done, + what changed, what's still open, the next wake's first move. `NO_REPLY` is only + acceptable when nothing is actionable, with the reason logged first. - The **`SQUAD.md` body** is the catalog's per-agent prose surface — describe each agent in user-facing language there (not in `manifest.json`). -- **Forbidden files**: do not create `AGENTS.md`, `USER.md`, `BOOTSTRAP.md`, or `BOOT.md` - inside the bundle — those are pod-managed by Pancake Cloud. `TOOLS.md` is allowed (it - is bundle-authored documentation). +- **Forbidden files**: do not create `AGENTS.md`, `USER.md`, `BOOTSTRAP.md`, `BOOT.md`, + or `HEARTBEAT.md` inside the bundle. The first four are pod-managed by Pancake Cloud; + `HEARTBEAT.md` is forbidden because OpenClaw's per-agent heartbeat does not fire for + squad sub-agents (move the wake procedure into a cron's `payload.text` instead). + `TOOLS.md` is allowed (it is bundle-authored documentation). - Crons target **only this squad's own agents**. - A cron run with nothing to report must reply with the single literal token `NO_REPLY`. diff --git a/.claude/skills/validate-squad/SKILL.md b/.claude/skills/validate-squad/SKILL.md index e63ffa2..5c89223 100644 --- a/.claude/skills/validate-squad/SKILL.md +++ b/.claude/skills/validate-squad/SKILL.md @@ -31,22 +31,17 @@ The validator's checks fall into the following categories: `manifest.agents` must have a matching `agents//agent.json` file. - **`agent.json` schema** (e.g. `agents//agent.json#/model must be one of: haiku, sonnet, opus`) — the per-agent config is invalid. Common causes: - - Wrong `model` value (string enum `haiku`/`sonnet`/`opus`) — applies to both - top-level `model` and `heartbeat.model`. - - `heartbeat` written as a plain string instead of the object shape (e.g. - `"heartbeat": "daily"` instead of `"heartbeat": { "every": "24h" }`). - - `heartbeat.every` written as a named value (`"daily"`) instead of an OpenClaw duration - in `ms`/`s`/`m`/`h` (`"30m"`, `"2h"`, `"24h"`, `"0m"`). - - Unknown field on the agent or inside `heartbeat`. Only six heartbeat sub-fields are - accepted (`every`, `model`, `lightContext`, `isolatedSession`, `skipWhenBusy`, - `timeoutSeconds`); pod-level fields like `prompt`, `target`, `directPolicy`, - `session`, `to`, `ackMaxChars` are rejected because they're not authorable from a - bundle. + - Wrong `model` value (string enum `haiku`/`sonnet`/`opus`). + - `heartbeat` declared on the agent — this is now an **unknown field**. OpenClaw's + per-agent heartbeat does not fire for squad sub-agents today; move the wake + procedure into a cron in `crons/jobs.json` whose `sessionTarget` is the agent's id, + and embed the procedure in `payload.text`. + - Other unknown field on the agent (only `id`, `description`, `model`, `skills`, + `contextInjection`, `bootstrapMaxChars`, `params` are accepted). - `id` not matching the directory name. - **Referenced-file errors** — a file the manifest or agent.json points to (`SQUAD.md`, - `ONBOARD.md`, a skill, `IDENTITY.md`, `SOUL.md`, `HEARTBEAT.md` when the agent has a - heartbeat) is missing, is a symlink, is not a regular file, or resolves outside the - bundle root. + `ONBOARD.md`, a skill, `IDENTITY.md`, `SOUL.md`) is missing, is a symlink, is not a + regular file, or resolves outside the bundle root. - **Unknown tool permission** (e.g. `required_tool_permissions[2] "message" is not an accepted tool key`) — an entry in `manifest.required_tool_permissions` is not in the canonical Pancake tool list (see [`bundle-reference.md#tool-permissions`](../../../docs/bundle-reference.md#tool-permissions)). @@ -57,9 +52,13 @@ The validator's checks fall into the following categories: - **Targeting errors** (`crons/jobs.json`) — a cron's `sessionTarget` names an agent the squad does not declare. Squad crons may target only the squad's own agents. - **Forbidden file** (e.g. `agents//USER.md forbidden filename`) — the bundle - contains a file named `AGENTS.md`, `USER.md`, `BOOTSTRAP.md`, or `BOOT.md`. Those are - pod-managed by Pancake Cloud and must not appear inside a bundle. Delete the file. - `TOOLS.md` is *allowed* and is not flagged. + contains a file named `AGENTS.md`, `USER.md`, `BOOTSTRAP.md`, `BOOT.md`, or + `HEARTBEAT.md`. The first four are pod-managed by Pancake Cloud; `HEARTBEAT.md` is + forbidden because OpenClaw's per-agent heartbeat does not fire for squad sub-agents + today. **Migration:** move the wake procedure from `HEARTBEAT.md` into the + `payload.text` of a cron in `crons/jobs.json` (with `sessionTarget` set to that agent's + id), then delete the `HEARTBEAT.md` file and the `heartbeat` field from `agent.json`. + For other forbidden files, just delete them. `TOOLS.md` is *allowed* and is not flagged. - **Deprecated field** (e.g. `SQUAD.md frontmatter has a deprecated 'token_intensity:' line`) — `token_intensity` has been removed from the contract. Pancake Cloud computes token usage automatically; delete the line. diff --git a/CLAUDE.md b/CLAUDE.md index 34f52ab..6af1090 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,11 +26,12 @@ A bundle is a directory with a `manifest.json` (the package descriptor: `name`, `description`, `author`, `agents` as a string array of ids, plus optional squad-wide skills, required identities, and vault secrets), a `SQUAD.md` catalog card, an `ONBOARD.md` onboarding script, and per agent: an `agents//agent.json` (the per-agent -runtime config — model, heartbeat, agent-specific skills, mirroring OpenClaw's -`agents.list[]`), `agents//IDENTITY.md`, and `agents//SOUL.md`. Optionally it -carries `MEMORY.md` seed memory, `skills/` files, `crons/jobs.json`, and a per-agent -`HEARTBEAT.md` (required when `agent.json` declares a heartbeat). Full detail is in -[`docs/bundle-reference.md`](./docs/bundle-reference.md). +runtime config — model, agent-specific skills, mirroring OpenClaw's `agents.list[]`), +`agents//IDENTITY.md`, and `agents//SOUL.md`. Optionally it carries `MEMORY.md` +seed memory, `skills/` files, and `crons/jobs.json`. Recurring wakes live in +`crons/jobs.json` payloads — OpenClaw's per-agent heartbeats do not fire for squad +sub-agents today, so `HEARTBEAT.md` is a forbidden filename anywhere in a bundle. Full +detail is in [`docs/bundle-reference.md`](./docs/bundle-reference.md). ## Working in this repo diff --git a/README.md b/README.md index c4e4fb3..f1a8265 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ node scripts/validate.mjs squads/ # one bundle Zero dependencies — just Node. It mirrors marketplace ingestion exactly, so a bundle that passes here passes ingestion. CI runs it on every push and pull request, alongside `node scripts/test-validator.mjs` which self-tests the validator against negative -fixtures (forbidden files, wrong heartbeat shape, etc.) — both must pass for a merge. +fixtures (forbidden files including `HEARTBEAT.md`, deprecated fields, unknown +`agent.json` keys, cron-target mismatches, etc.) — both must pass for a merge. ## Publish diff --git a/agent.schema.json b/agent.schema.json index 3e47a27..3e467ab 100644 --- a/agent.schema.json +++ b/agent.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://squads.getpancake.ai/agent.schema.json", "title": "Agent Squad — per-agent config", - "description": "Schema for agents//agent.json inside a squad bundle. Mirrors the subset of OpenClaw's agents.list[] that squad bundles may declare. See https://docs.openclaw.ai/gateway/config-agents for the canonical OpenClaw spec.", + "description": "Schema for agents//agent.json inside a squad bundle. Mirrors the subset of OpenClaw's agents.list[] that squad bundles may declare. See https://docs.openclaw.ai/gateway/config-agents for the canonical OpenClaw spec. Note: per-agent `heartbeat` is intentionally absent — OpenClaw's agent-level heartbeat does not fire for squad sub-agents today, so every recurring wake must be driven by a cron in `crons/jobs.json`.", "type": "object", "additionalProperties": false, "required": ["id", "description"], @@ -22,40 +22,6 @@ "enum": ["haiku", "sonnet", "opus"], "description": "Strict enum. Defaults to the pod's agents.defaults when omitted." }, - "heartbeat": { - "type": "object", - "additionalProperties": false, - "description": "OpenClaw heartbeat config — the curated subset of agents.list[].heartbeat that squad authors are allowed to set. Pod-level fields (prompt, target, directPolicy, etc.) are intentionally omitted. Every sub-field below is optional; omitted values inherit from the pod's agents.defaults.heartbeat. When the heartbeat object is present, agents//HEARTBEAT.md must exist.", - "properties": { - "every": { - "type": "string", - "pattern": "^\\d+(ms|s|m|h)$", - "description": "Duration string in OpenClaw format — units ms / s / m / h. e.g. \"30m\", \"2h\", \"24h\". Set \"0m\" to disable. Named values like \"daily\" are not valid." - }, - "model": { - "type": "string", - "enum": ["haiku", "sonnet", "opus"], - "description": "Override the agent-level model just for heartbeat runs. Same strict enum as the top-level model field." - }, - "lightContext": { - "type": "boolean", - "description": "When true, the wake bootstrap loads only HEARTBEAT.md from the workspace — useful for cheap, narrow wakes." - }, - "isolatedSession": { - "type": "boolean", - "description": "When true, each heartbeat runs in a fresh session with no prior conversation history." - }, - "skipWhenBusy": { - "type": "boolean", - "description": "When true, defer the wake while this agent's other lanes (subagents, nested tasks) are still running." - }, - "timeoutSeconds": { - "type": "integer", - "minimum": 1, - "description": "Maximum seconds the heartbeat run is allowed before OpenClaw aborts it." - } - } - }, "skills": { "type": "array", "items": { "type": "string" }, diff --git a/docs/bundle-reference.md b/docs/bundle-reference.md index 27b18b4..6793a2e 100644 --- a/docs/bundle-reference.md +++ b/docs/bundle-reference.md @@ -32,18 +32,24 @@ agents// agent.json ✔ per-agent runtime config (mirrors OpenClaw agents.list[]) IDENTITY.md ✔ per agent — name, role, scope SOUL.md ✔ per agent — personality, principles, boundaries - HEARTBEAT.md ·* per agent — the procedure run on every wake (required when agent.json declares a heartbeat) MEMORY.md · per agent — seed memory (overrides the squad-wide one) skills/.md · agent-specific skills — referenced by agent.json#/skills -crons/jobs.json · native OpenClaw cron jobs +crons/jobs.json · native OpenClaw cron jobs — also the home of any + recurring wake procedure (the former `HEARTBEAT.md` job) ``` +`HEARTBEAT.md` is **forbidden** anywhere inside a bundle (see +[*Forbidden files*](#forbidden-files)). OpenClaw's per-agent +`agent.json#/heartbeat` does not fire for squad sub-agents today, so every +recurring wake must be driven by a cron in `crons/jobs.json` — the wake +procedure lives in that cron's `payload.text`. + On ingest the marketplace **verifies every file the manifest references** — it must exist, be a regular file, not be a symlink, and resolve **inside the bundle root** (no `..` escape, no absolute path). The files always checked are `SQUAD.md`, `ONBOARD.md`, every `skills[]` path, and per agent `agents//agent.json`, `agents//IDENTITY.md`, -`agents//SOUL.md`, every `agent.json#/skills[]` path, and `agents//HEARTBEAT.md` -when the agent declares a heartbeat. `scripts/validate.mjs` performs the identical check. +`agents//SOUL.md`, and every `agent.json#/skills[]` path. `scripts/validate.mjs` +performs the identical check. ## `manifest.json` — the package descriptor @@ -88,7 +94,6 @@ for the canonical upstream spec. | `id` | string | ✔ | kebab-case. Must match the directory name `agents//` and the entry in `manifest.json#/agents`. | | `description` | string | ✔ | non-empty one-liner — what this agent owns. | | `model` | string | · | enum `haiku` \| `sonnet` \| `opus`. Defaults to the pod's `agents.defaults.model` (`sonnet`) when omitted. | -| `heartbeat` | object | · | Curated subset of [OpenClaw's `agents.list[].heartbeat`](https://docs.openclaw.ai/gateway/config-agents#agents-defaults-heartbeat) — `{ every, model, lightContext, isolatedSession, skipWhenBusy, timeoutSeconds }`. All sub-fields optional; the validator rejects anything else (pod-level fields like `prompt`, `target`, `directPolicy`, `session`, `to`, `ackMaxChars` etc. are not authorable from a bundle). `every` is an OpenClaw duration string (units `ms`/`s`/`m`/`h`, e.g. `"30m"`, `"2h"`, `"24h"`, or `"0m"` to disable) — named values like `"daily"` are rejected. `heartbeat.model` follows the same `haiku`/`sonnet`/`opus` enum as the top-level `model`. Inherits any omitted sub-field from the pod's `agents.defaults.heartbeat`. When the heartbeat object is present, `agents//HEARTBEAT.md` must exist. | | `skills` | string[] | · | bundle-relative paths to this agent's skill files. | | `contextInjection` | string | · | enum `always` \| `continuation-skip` \| `never`. Pod default applies when omitted. | | `bootstrapMaxChars` | integer | · | positive. OpenClaw bootstrap budget; pod default applies when omitted. | @@ -98,6 +103,14 @@ for the canonical upstream spec. unknown fields rather than silently dropping them, so any new OpenClaw field a squad needs must be added here first. +> **`heartbeat` is intentionally absent.** OpenClaw's per-agent +> `agents.list[].heartbeat` does not fire for squad sub-agents today. Declaring +> a `heartbeat` object in `agent.json` is rejected as an unknown field. Drive +> every recurring wake from a cron in [`crons/jobs.json`](#cronsjobsjson--native-cron-jobs) — +> the cron's `sessionTarget` is the agent id, and the wake procedure goes in +> the cron's `payload.text`. See [*Recurring wakes — the cron pattern*](#recurring-wakes--the-cron-pattern) +> below. + See [`template/agents/example-agent/agent.json`](../template/agents/example-agent/agent.json). ## `SQUAD.md` — the marketplace catalog card @@ -146,8 +159,8 @@ It tells the co-founder: - what first task to create and dispatch. Keep the script short enough to complete within `estimated_setup_minutes`. A step may be -tagged `dispatch: later` to defer its first task to the agent's heartbeat; otherwise the -first task is dispatched immediately. +tagged `dispatch: later` to defer its first task to the agent's next cron wake; +otherwise the first task is dispatched immediately. ## `IDENTITY.md` and `SOUL.md` — per agent @@ -175,26 +188,39 @@ agent. - **Boundaries (Inviolable)** — the *Never* / *Always* hard limits. - **What Success Looks Like** — the bar. -The step-by-step wake procedure lives in [`HEARTBEAT.md`](#heartbeatmd--the-wake-procedure), +The step-by-step wake procedure lives in the cron's `payload.text` (see +[*Recurring wakes — the cron pattern*](#recurring-wakes--the-cron-pattern)), not in `SOUL.md` — keep behavioural rules here and the procedure there. -## `HEARTBEAT.md` — the wake procedure +## Recurring wakes — the cron pattern -Per agent. **Required when `agent.json` declares a `heartbeat`** — the validator errors -otherwise. OpenClaw loads `agents//HEARTBEAT.md` on **every wake** — both heartbeat -pulses and dispatched tasks — before the agent starts work. This is the right home for -the recurring procedure the agent runs each tick: what to read, what to decide, what to -file. Keeping it out of `SOUL.md` is the convention because: +OpenClaw's per-agent `agents.list[].heartbeat` (i.e. `agent.json#/heartbeat`) +does **not fire for squad sub-agents** today. Every recurring wake for a squad +agent must therefore be driven by a cron in [`crons/jobs.json`](#cronsjobsjson--native-cron-jobs), +with the wake procedure embedded in the cron's `payload.text`. `HEARTBEAT.md` +is a [forbidden filename](#forbidden-files) — the validator rejects it +anywhere inside a bundle. -- **`SOUL.md` is about behaviour** — personality, principles, escalation rules. - It should not also carry the step-by-step procedure. -- **`MEMORY.md` is an index of pointers**, not a script. Burying wake steps - there hides them and bloats memory. -- **Authors can iterate on the wake procedure without touching `SOUL.md`**, - which keeps personality/principles stable across releases. +The typical pattern for an agent that wants to wake every 2 hours: -Write it in the imperative, addressed to the agent. A solid structure -(mirrored in [`template/agents/example-agent/HEARTBEAT.md`](../template/agents/example-agent/HEARTBEAT.md)): +```json +{ + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 */2 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "", + "payload": { + "kind": "systemEvent", + "text": "" + }, + "failureAlert": false, + "state": {} +} +``` + +Compose `payload.text` as the imperative wake procedure — what you would have +written in a `HEARTBEAT.md` under the old shape. A solid structure: 1. **The non-negotiable** — *at least one task must be EXECUTED before the session closes.* A wake is "orient, find the highest-leverage action in @@ -204,7 +230,9 @@ Write it in the imperative, addressed to the agent. A solid structure 2. **Orient** — read `MEMORY.md`, skim recent daily logs, `list_tasks`. 3. **Decide what this wake is for** — dispatched task, recurring duty, draft to advance, or genuinely nothing (with a logged reason). -4. **Recurring duty** — the agent's specific heartbeat work and its cadence. +4. **Recurring duty** — the agent's specific work and its cadence. If a daily + cron already covers the bulk of the work, the pulse cron typically focuses + on advancing in-flight items and mission-deepening between daily runs. 5. **Execute** — actually produce the artifact; don't just plan. 6. **Digest** — before closing the session, append a one-paragraph digest to `memory/YYYY-MM-DD.md`: *what you did, what changed, what's still open, @@ -213,8 +241,19 @@ Write it in the imperative, addressed to the agent. A solid structure A wake without a digest is an unfinished wake. 7. **Close the loop** — `complete_task` / `fail_task`, surface blockers. -If `heartbeat` is omitted from `agent.json`, `HEARTBEAT.md` is optional and the pod's -default wake template is used when the agent does wake. +A cron run that intentionally produces no output must instruct the agent to +reply with the single literal token `NO_REPLY` — OpenClaw's silent-turn +sentinel. Never write "do not respond"; that trips a false-positive failure +alert. + +When the wake procedure is long, an alternative is to ship a `heartbeat-pulse` +**skill** under the agent's `agent.json#/skills` and have the cron payload +say only *"Load the `heartbeat-pulse` skill and run it end to end."* — either +shape is allowed; the substance is the same. + +See [`template/crons/jobs.json`](../template/crons/jobs.json) for the +heartbeat-pulse cron the skeleton ships with, and any of the official squads +under [`squads/`](../squads/) for production examples. ## `MEMORY.md` — seed memory @@ -290,25 +329,30 @@ Optional. Native OpenClaw cron jobs registered at install. ## Dispatchable work -Squads do not ship task templates. The agent's recurring wake procedure lives in -`HEARTBEAT.md`, and ad-hoc work is dispatched by the co-founder at runtime via the -tasks plugin (`create_task`) — there is no per-bundle template file. A `tasks/` directory -inside a bundle has no meaning to the runtime; do not create one. +Squads do not ship task templates. The agent's recurring wake procedure lives in a +cron's `payload.text` (see [*Recurring wakes — the cron pattern*](#recurring-wakes--the-cron-pattern)), +and ad-hoc work is dispatched by the co-founder at runtime via the tasks plugin +(`create_task`) — there is no per-bundle template file. A `tasks/` directory inside +a bundle has no meaning to the runtime; do not create one. ## Forbidden files -These filenames are **pod-managed by Pancake Cloud** and live at the pod workspace root -(alongside `CLAUDE.md`), not inside any bundle. The validator rejects them at any depth -inside a bundle directory (case-insensitive): +The validator rejects these filenames at any depth inside a bundle directory +(case-insensitive): -- `AGENTS.md` -- `USER.md` -- `BOOTSTRAP.md` -- `BOOT.md` +- `AGENTS.md` — pod-managed by Pancake Cloud. +- `USER.md` — pod-managed by Pancake Cloud. +- `BOOTSTRAP.md` — pod-managed by Pancake Cloud. +- `BOOT.md` — pod-managed by Pancake Cloud. +- `HEARTBEAT.md` — OpenClaw's per-agent heartbeat does not fire for squad + sub-agents today. Move the wake procedure into a cron in + [`crons/jobs.json`](#cronsjobsjson--native-cron-jobs) — see + [*Recurring wakes — the cron pattern*](#recurring-wakes--the-cron-pattern). -A bundle's `MEMORY.md` is allowed (and idiomatic) to *reference* these files by relative -path (e.g. `../../USER.md` as the user-pointer in an agent's MEMORY) — the validator only -forbids the *files themselves*, not references to them. +A bundle's `MEMORY.md` is allowed (and idiomatic) to *reference* the +pod-managed files by relative path (e.g. `../../USER.md` as the user-pointer +in an agent's MEMORY) — the validator only forbids the *files themselves*, +not references to them. `TOOLS.md` is explicitly **allowed** inside a bundle — it is bundle-authored documentation of the squad's tool surface, distinct from the pod-level files above. @@ -377,9 +421,9 @@ Error categories the validator emits: | Manifest schema | `agents[0] "Geo Agent" must be kebab-case` | | `agent.json` missing | `agents/foo/agent.json not found` | | `agent.json` schema | `agents/foo/agent.json#/model must be one of: haiku, sonnet, opus` | -| Referenced file | `agents/foo/HEARTBEAT.md referenced by the manifest but not found` | +| Referenced file | `agents/foo/skills/x.md referenced by the manifest but not found` | | Cron targeting | `crons/jobs.json cron job "x" sessionTarget "y" is not a declared agent id` | -| Forbidden file | `agents/foo/USER.md forbidden filename — …` | +| Forbidden file | `agents/foo/USER.md forbidden filename — …` (also flags `HEARTBEAT.md` at any depth) | | Deprecated field | `SQUAD.md frontmatter has a deprecated 'token_intensity:' line` | | Unresolved TODO | `SQUAD.md unresolved TODO marker on line 12` | diff --git a/docs/creating-a-squad.md b/docs/creating-a-squad.md index 4bbe213..455b961 100644 --- a/docs/creating-a-squad.md +++ b/docs/creating-a-squad.md @@ -17,7 +17,10 @@ You don't need to read anything else first, but it helps to know: publishing content, whatever the squad is built for. Each agent reports to the user's co-founder agent; the user never talks to a squad agent directly. - **The runtime.** Squads run on OpenClaw inside a Pancake pod. Each agent wakes on a - *heartbeat* (e.g. every 2 hours, or daily) and runs a procedure you write. + *cron* (e.g. every 2 hours, or daily) declared in `crons/jobs.json` and runs the + procedure embedded in the cron's payload. (OpenClaw's per-agent + `agent.json#/heartbeat` does not fire for squad sub-agents today — that's why the wake + procedure lives in a cron, not in a `HEARTBEAT.md`.) - **The file contract.** A bundle is a directory of files with a specific shape. The validator enforces that shape; the marketplace re-checks it on ingest. @@ -37,19 +40,22 @@ MEMORY.md · squad-wide seed memory skills/.md · squad-wide skills (every agent receives a copy) TOOLS.md · optional documentation of the squad's tool surface agents// - agent.json ✔ per-agent runtime config (model, heartbeat, skills) + agent.json ✔ per-agent runtime config (model, skills) IDENTITY.md ✔ who the agent is (name, role, scope) SOUL.md ✔ how the agent behaves (personality, principles) - HEARTBEAT.md ·* the wake procedure (required when heartbeat is declared) MEMORY.md · agent-specific seed memory (overrides squad-wide) skills/.md · agent-specific skills -crons/jobs.json · native OpenClaw cron jobs +crons/jobs.json · native OpenClaw cron jobs — also the home of any + recurring wake procedure (the cron's payload text) ``` A few things the validator *forbids* inside a bundle: - `AGENTS.md`, `USER.md`, `BOOTSTRAP.md`, `BOOT.md` — these are pod-level files managed by Pancake Cloud, not by squads. Don't ship them. +- `HEARTBEAT.md` — OpenClaw's per-agent heartbeat does not fire for squad sub-agents + today. The wake procedure lives in a cron's `payload.text` in + [`crons/jobs.json`](../template/crons/jobs.json) instead. - `token_intensity` in `manifest.json` or `SQUAD.md` frontmatter — deprecated. Pancake Cloud computes token usage automatically. - `tasks/` directory — squads do not ship task templates. Ad-hoc work is dispatched at @@ -129,15 +135,16 @@ For every id in `manifest.agents`, create `agents//agent.json`: "id": "your-agent-id", "description": "One line on what this agent owns.", "model": "sonnet", - "heartbeat": { "every": "24h" }, "skills": ["agents/your-agent-id/skills/your-skill.md"] } ``` This file is the bundle's slice of OpenClaw's agent runtime config. See -[*4. agent.json reference*](#4-agentjson-reference) below for every field. +[*4. agent.json reference*](#4-agentjson-reference) below for every field. Note that +`heartbeat` is **not authorable** from a squad bundle — see +[*6. Recurring wakes — the cron pattern*](#6-recurring-wakes--the-cron-pattern). -### 3.4 Write `IDENTITY.md`, `SOUL.md`, and `HEARTBEAT.md` +### 3.4 Write `IDENTITY.md` and `SOUL.md` For every agent: @@ -149,8 +156,8 @@ For every agent: *Escalation Rules*, *Boundaries (Inviolable)*, *What Success Looks Like*. Mirror [`template/agents/example-agent/SOUL.md`](../template/agents/example-agent/SOUL.md). -- **`HEARTBEAT.md` — the wake procedure.** *Required* when `agent.json#/heartbeat` is set. - See [*6. HEARTBEAT.md contract*](#6-heartbeatmd-contract) below. +`HEARTBEAT.md` is a **forbidden filename** anywhere in a bundle — see +[*6. Recurring wakes — the cron pattern*](#6-recurring-wakes--the-cron-pattern). ### 3.5 Write the skills @@ -192,11 +199,13 @@ each agent here in user-facing language, not in `manifest.json`. Recommended sec **`ONBOARD.md`** is a **script the co-founder agent executes** after the mechanical deploy. See [*5. ONBOARD.md contract*](#5-onboardmd-contract) below. -### 3.7 Optional: add `crons/jobs.json` and `MEMORY.md` +### 3.7 Add `crons/jobs.json` (required for recurring wakes) and optionally `MEMORY.md` -- `crons/jobs.json` for native OpenClaw cron jobs. Each job's `sessionTarget` must be an - agent id declared in your own `manifest.agents`. A cron run with nothing to report must - reply with the literal token `NO_REPLY`. +- **`crons/jobs.json`** — native OpenClaw cron jobs. This is also where every recurring + wake lives, because per-agent heartbeats do not fire for squad sub-agents (see + [*6. Recurring wakes — the cron pattern*](#6-recurring-wakes--the-cron-pattern)). Each + job's `sessionTarget` must be an agent id declared in your own `manifest.agents`. A cron + run with nothing to report must reply with the literal token `NO_REPLY`. - A squad-wide `MEMORY.md` if multiple agents share the same seed pointers. ### 3.8 Strip every placeholder @@ -225,7 +234,6 @@ Every field accepted in `agents//agent.json`: | `id` | string | ✔ | kebab-case. Must match the directory name and the `manifest.agents` entry. | | `description` | string | ✔ | Non-empty. One-line role description. | | `model` | string | · | Enum: `haiku` \| `sonnet` \| `opus`. Defaults to the pod default (`sonnet`). | -| `heartbeat` | object | · | Curated subset of [OpenClaw's `agents.list[].heartbeat`](https://docs.openclaw.ai/gateway/config-agents#agents-defaults-heartbeat) — `{ every, model, lightContext, isolatedSession, skipWhenBusy, timeoutSeconds }`. Common shape `{ "every": "" }`. `every` is a duration in OpenClaw units `ms`/`s`/`m`/`h` (e.g. `"30m"`, `"2h"`, `"24h"`, `"0m"` to disable) — named values like `"daily"` are rejected. `heartbeat.model` is the same `haiku`/`sonnet`/`opus` enum as the top-level `model`. Pod-level fields (`prompt`, `target`, `directPolicy`, `session`, `to`, `ackMaxChars`, etc.) are not authorable from a bundle and are rejected. When the heartbeat object is present, `agents//HEARTBEAT.md` must exist. | | `skills` | string[] | · | Bundle-relative paths to this agent's skill files. | | `contextInjection` | string | · | Enum: `always` \| `continuation-skip` \| `never`. Pod default applies when omitted. | | `bootstrapMaxChars` | integer | · | Positive integer. OpenClaw bootstrap budget. | @@ -234,6 +242,11 @@ Every field accepted in `agents//agent.json`: Unknown fields are rejected — the validator does not silently drop them. If you need a new OpenClaw field, add it to the schema first. +> **`heartbeat` is not authorable from a squad bundle.** OpenClaw's +> `agents.list[].heartbeat` does not fire for squad sub-agents today. If you put +> a `heartbeat` object in `agent.json`, the validator rejects it as an unknown +> field. See [*6. Recurring wakes — the cron pattern*](#6-recurring-wakes--the-cron-pattern). + ## 5. `ONBOARD.md` contract `ONBOARD.md` is **a runnable script, not a README.** The co-founder agent executes it @@ -262,17 +275,36 @@ The body, in the imperative addressed to the co-founder: one matches the `site`. - **Save answers** to the agent's `MEMORY.md` (or a wiki page the MEMORY indexes). - **Create the first task** with `create_task` and dispatch it immediately — unless you - add `dispatch: later` to defer the first run to the agent's heartbeat. + add `dispatch: later` to defer the first run to the agent's next cron wake. -## 6. `HEARTBEAT.md` contract +## 6. Recurring wakes — the cron pattern -OpenClaw loads `agents//HEARTBEAT.md` on **every wake** — both heartbeat pulses and -dispatched tasks. This is the right home for the procedure the agent runs each tick; -keeping it out of `SOUL.md` (which is for behavioural rules) and out of `MEMORY.md` -(which is an index of pointers) lets you iterate on the procedure without touching the -agent's personality. +OpenClaw's per-agent `agents.list[].heartbeat` (i.e. `agent.json#/heartbeat`) does +**not fire for squad sub-agents** today. Every recurring wake for a squad agent must +therefore be driven by a cron in [`crons/jobs.json`](../template/crons/jobs.json), with +the wake procedure embedded in the cron's `payload.text`. `HEARTBEAT.md` is a forbidden +filename — the validator rejects it anywhere inside a bundle. -Write it in the imperative, addressed to the agent. A solid structure: +The typical pattern for an agent that wants to wake every 2 hours: + +```json +{ + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — your-agent-id self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 */2 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "your-agent-id", + "payload": { + "kind": "systemEvent", + "text": "" + }, + "failureAlert": false, + "state": {} +} +``` + +Compose `payload.text` as the imperative wake procedure the agent runs every tick. +A solid structure: 1. **The non-negotiable** — at least one task must be **executed** before the session closes. A wake is "orient, find the highest-leverage action in the lane, do it, file @@ -281,13 +313,24 @@ Write it in the imperative, addressed to the agent. A solid structure: 2. **Orient** — read `MEMORY.md`, skim recent daily logs, call `list_tasks`. 3. **Decide what this wake is for** — a dispatched task, a recurring duty, a draft to advance, or genuinely nothing (with a logged reason). -4. **Recurring duty** — the agent's specific heartbeat work and its cadence. +4. **Recurring duty** — the agent's specific cron-driven work and its cadence. If a + separate daily cron already covers the bulk of the work, the pulse cron typically + focuses on advancing in-flight items and mission-deepening between daily runs. 5. **Execute** — actually produce the artifact; don't just plan. 6. **Digest** — before closing the session, append a one-paragraph digest to `memory/YYYY-MM-DD.md`: *what you did, what changed, what's still open, and the single first move for the next wake.* 7. **Close the loop** — `complete_task` / `fail_task`, surface blockers. +A cron run that intentionally produces no output must instruct the agent to reply with +the single literal token `NO_REPLY` (OpenClaw's silent-turn sentinel) — never write +"do not respond", which trips a false-positive failure alert. + +> **Tip — long procedures.** When the wake procedure is too long for a comfortable +> payload string, ship a `heartbeat-pulse` *skill* under the agent's +> `agent.json#/skills` and have the cron's payload say only +> *"Load the `heartbeat-pulse` skill and run it end to end."* — either shape is allowed. + ## 7. Authoring principles The contract tells you what's valid. This tells you what's good. @@ -318,11 +361,12 @@ The contract tells you what's valid. This tells you what's good. to relay it. Only split when work is genuinely distinct: different cadence, different skills, different identities. When in doubt, one agent. -- **Heartbeat first, cron only when timing matters.** A heartbeat is a state-driven - trigger — the agent wakes on its pulse and decides what to do. A cron is clock-driven: - it fires at an exact time with a hard-coded instruction. Reach for a cron only when the - time itself matters to someone outside the agent — an 18:00 PT end-of-day report, a - Monday-morning digest. Otherwise raise the heartbeat. +- **Everything time-driven is a cron.** Per-agent heartbeats don't fire for squad + sub-agents today, so every recurring wake — whether it's a daily citation audit, a + Monday digest, or a 2-hour self-driven pulse — lives in `crons/jobs.json`. Reach for a + separate clock-time cron when the time itself matters to someone outside the agent (a + 09:00 LA daily report), and a `0 */2 * * *`-style pulse cron for the agent's recurring + background work. - **Crons stay quiet unless something changed.** A scheduled run with nothing to report must reply with the single literal token `NO_REPLY`. A chatty cron that posts "nothing @@ -331,9 +375,10 @@ The contract tells you what's valid. This tells you what's good. - **`MEMORY.md` is an index, not a notebook.** One-line pointers only. Detailed findings go to the shared wiki. -- **Wake procedure in `HEARTBEAT.md`, behaviour in `SOUL.md`, pointers in `MEMORY.md`.** - Three files, three concerns. Burying wake steps in `SOUL.md` or pointers in - `HEARTBEAT.md` makes both hard to maintain. +- **Wake procedure in the cron payload, behaviour in `SOUL.md`, pointers in `MEMORY.md`.** + Three concerns, three homes. Keep behavioural rules out of cron payloads (they belong + in `SOUL.md`), and keep step-by-step procedure out of `SOUL.md` (it belongs in the + cron's `payload.text`, or in a `heartbeat-pulse` skill the cron loads). ## 8. Testing your squad diff --git a/docs/how-squads-work.md b/docs/how-squads-work.md index 2166af8..773b0f7 100644 --- a/docs/how-squads-work.md +++ b/docs/how-squads-work.md @@ -21,11 +21,13 @@ a transient task. Once installed it has: - its **own workspace** at `workspace/agents//`; - an **`IDENTITY.md`** (who it is — name, role, scope) and a **`SOUL.md`** (how it behaves — personality, principles, boundaries), deployed verbatim from the bundle; -- optionally a **`HEARTBEAT.md`** — the imperative wake procedure OpenClaw loads on every - pulse and dispatched task. Without it, the pod's default wake template is used; - its **own isolated skill collection** at `workspace/agents//skills/`; -- a **port** and a **`heartbeat`** — so it wakes on its own schedule, proactively, not only - when spoken to; +- **cron-driven recurring wakes** — every recurring schedule the squad needs (a daily + duty, a Monday digest, a 2-hour self-driven pulse) is a job in `crons/jobs.json` whose + `payload.text` is the wake procedure the agent runs. (OpenClaw's per-agent + `agents.list[].heartbeat` does not fire for squad sub-agents today, so the heartbeat + pattern is implemented as a `0 */2 * * *`-style cron pointing at the agent.) Outside of + these crons, the agent also wakes on dispatched tasks from the co-founder; - a **reporting line**: it reports to the co-founder. The user never talks to a squad agent directly — the co-founder dispatches work to it and relays results. @@ -64,10 +66,12 @@ When a user asks the co-founder to install a squad, four things happen: `.tar.gz`, extracts it, re-validates the manifest, and for each agent: creates `workspace/agents//` (with `IDENTITY.md` + `SOUL.md` from the bundle), reads the per-agent `agents//agent.json` to add an `agents.list` entry to `openclaw.json` - (model, heartbeat, skills, and the rest of the runtime config), deploys the agent's - skills into its own skills folder, merges the bundle's crons, and seeds memory. The - marketplace catalog also surfaces each agent's user-facing description from `SQUAD.md` - body prose — `manifest.json` and `agent.json` are runtime config, not catalog copy. + (model, skills, and the rest of the runtime config), deploys the agent's skills into + its own skills folder, merges the bundle's crons (these crons carry the recurring wake + procedures, since per-agent heartbeats don't fire for sub-agents), and seeds memory. + The marketplace catalog also surfaces each agent's user-facing description from + `SQUAD.md` body prose — `manifest.json` and `agent.json` are runtime config, not + catalog copy. 3. **Onboarding.** The co-founder runs the bundle's [`ONBOARD.md`](./bundle-reference.md#onboardmd) **as a script** — not as documentation. `ONBOARD.md` tells the co-founder what to ask the @@ -76,8 +80,8 @@ When a user asks the co-founder to install a squad, four things happen: what first task to create. 4. **First task.** The first task is created and dispatched immediately, so the squad starts - working while the user is still there — rather than waiting for the agent's next - heartbeat. (A step can opt out with `dispatch: later` to defer to the heartbeat.) + working while the user is still there — rather than waiting for the agent's next cron + wake. (A step can opt out with `dispatch: later` to defer to the next cron run.) The key idea in step 3: **`ONBOARD.md` is a runnable script.** You are not writing docs for a human to read — you are writing instructions for the co-founder agent to execute. diff --git a/scripts/test-validator.mjs b/scripts/test-validator.mjs index f94fb18..e370540 100644 --- a/scripts/test-validator.mjs +++ b/scripts/test-validator.mjs @@ -66,18 +66,10 @@ function baseBundle() { const cases = [ // Positive baselines { - name: "valid minimum bundle (no heartbeat)", + name: "valid minimum bundle", mutate: () => {}, expect: null, }, - { - name: "valid bundle with heartbeat object + HEARTBEAT.md", - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m" }; - b["agents/test-agent/HEARTBEAT.md"] = "wake procedure\n"; - }, - expect: null, - }, { name: "valid bundle with TOOLS.md (allowed)", mutate: (b) => { @@ -85,85 +77,35 @@ const cases = [ }, expect: null, }, - { - name: "valid bundle with every curated heartbeat sub-field", - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { - every: "2h", - model: "haiku", - lightContext: true, - isolatedSession: true, - skipWhenBusy: true, - timeoutSeconds: 45, - }; - b["agents/test-agent/HEARTBEAT.md"] = "wake procedure\n"; - }, - expect: null, - }, - // Negative — heartbeat shape - { - name: "heartbeat as bare string is rejected", - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = "daily"; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; - }, - expect: /must be an object mirroring OpenClaw's agents\.list\[\]\.heartbeat/, - }, - { - name: 'heartbeat.every = "daily" is rejected', - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "daily" }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; - }, - expect: /must be an OpenClaw duration string in units ms\/s\/m\/h/, - }, - { - name: "unknown heartbeat sub-field is rejected", - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m", bogus: true }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; - }, - expect: /heartbeat\.bogus.*unknown field/, - }, - { - name: "heartbeat.lightContext as a string is rejected", - mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m", lightContext: "yes" }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; - }, - expect: /heartbeat\.lightContext.*must be a boolean/, - }, + // Negative — heartbeat is no longer authorable from a bundle { - name: "heartbeat.model outside haiku|sonnet|opus is rejected", + name: "heartbeat in agent.json is rejected (unknown field)", mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m", model: "openai/gpt-5.4-mini" }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; + b["agents/test-agent/agent.json"].heartbeat = { every: "30m" }; }, - expect: /heartbeat\.model.*must be one of: haiku, sonnet, opus/, + expect: /agent\.json.*heartbeat.*unknown field/, }, { - name: "pod-level heartbeat field (e.g. directPolicy) is rejected", + name: "HEARTBEAT.md is forbidden anywhere in a bundle (agent dir)", mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m", directPolicy: "allow" }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; + b["agents/test-agent/HEARTBEAT.md"] = "wake procedure\n"; }, - expect: /heartbeat\.directPolicy.*unknown field/, + expect: /agents\/test-agent\/HEARTBEAT\.md.*forbidden filename.*HEARTBEAT\.md is no longer authorable from a bundle/, }, { - name: "heartbeat.timeoutSeconds = 0 is rejected (must be positive)", + name: "HEARTBEAT.md is forbidden at the bundle root too", mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m", timeoutSeconds: 0 }; - b["agents/test-agent/HEARTBEAT.md"] = "wake\n"; + b["HEARTBEAT.md"] = "x\n"; }, - expect: /heartbeat\.timeoutSeconds.*must be a positive integer/, + expect: /HEARTBEAT\.md.*forbidden filename.*HEARTBEAT\.md is no longer authorable from a bundle/, }, { - name: "heartbeat declared but HEARTBEAT.md missing is rejected", + name: "lowercase heartbeat.md is also forbidden (case-insensitive)", mutate: (b) => { - b["agents/test-agent/agent.json"].heartbeat = { every: "30m" }; + b["agents/test-agent/heartbeat.md"] = "x\n"; }, - expect: /agents\/test-agent\/HEARTBEAT\.md.*not found/, + expect: /agents\/test-agent\/heartbeat\.md.*forbidden filename/, }, // Negative — agent.json schema diff --git a/scripts/validate.mjs b/scripts/validate.mjs index bc1695c..0de84ea 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -50,26 +50,19 @@ const ACCEPTED_TOOL_PERMISSIONS = { }; const ACCEPTED_TOOL_KEYS = new Set(Object.values(ACCEPTED_TOOL_PERMISSIONS).flat()); -// Heartbeat config is the curated subset of OpenClaw's -// agents.list[].heartbeat (see https://docs.openclaw.ai/gateway/config-agents) -// that squad authors are allowed to set. Pod-level fields (prompt, target, -// directPolicy, session, to, ackMaxChars, includeReasoning, -// includeSystemPromptSection, suppressToolErrorWarnings) are intentionally -// excluded — OpenClaw still accepts them at the pod level, but a squad -// bundle should not declare them. `every` is a duration string in OpenClaw's -// ms/s/m/h format (e.g. "30m", "2h", "24h", "0m" to disable) — named values -// like "daily" are invalid. -const HEARTBEAT_EVERY_PATTERN = /^\d+(ms|s|m|h)$/; -const HEARTBEAT_FIELDS = { - every: { kind: "duration" }, - model: { kind: "enum", values: MODELS }, - lightContext: { kind: "boolean" }, - isolatedSession: { kind: "boolean" }, - skipWhenBusy: { kind: "boolean" }, - timeoutSeconds: { kind: "positiveInteger" }, -}; - -const FORBIDDEN_BASENAMES = new Set(["agents.md", "user.md", "bootstrap.md", "boot.md"]); +// Per-agent `agent.json#/heartbeat` and the per-agent `HEARTBEAT.md` file +// are intentionally not authorable from a squad bundle. OpenClaw's agent-level +// heartbeat does not fire for squad sub-agents today, so every recurring wake +// must be driven by a cron in `crons/jobs.json`. The `heartbeat` key in +// `agent.json` is rejected as an unknown field; `HEARTBEAT.md` is rejected as +// a forbidden filename anywhere inside a bundle. +const FORBIDDEN_BASENAMES = new Set([ + "agents.md", + "user.md", + "bootstrap.md", + "boot.md", + "heartbeat.md", +]); const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v); const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string"); @@ -217,47 +210,16 @@ function validateManifest(input) { return errors; } -// ── heartbeat sub-schema validation ───────────────────────────────────────── -// Mirrors OpenClaw's heartbeat config exactly. Every sub-field is optional; -// unknown sub-fields are rejected. - -function validateHeartbeat(input, prefix, err) { - for (const key of Object.keys(input)) { - const spec = HEARTBEAT_FIELDS[key]; - if (!spec) { - err(`${prefix}.${key}`, `unknown field — allowed: ${Object.keys(HEARTBEAT_FIELDS).join(", ")}`); - continue; - } - const v = input[key]; - const at = `${prefix}.${key}`; - switch (spec.kind) { - case "duration": - if (typeof v !== "string" || !HEARTBEAT_EVERY_PATTERN.test(v)) { - err(at, 'must be an OpenClaw duration string in units ms/s/m/h (e.g. "30m", "2h", "24h", "0m" to disable). Named values like "daily" are not valid.'); - } - break; - case "boolean": - if (typeof v !== "boolean") err(at, "must be a boolean"); - break; - case "positiveInteger": - if (!Number.isInteger(v) || v < 1) err(at, "must be a positive integer"); - break; - case "enum": - if (!spec.values.includes(v)) err(at, `must be one of: ${spec.values.join(", ")}`); - break; - } - } -} - // ── agent.json validation ─────────────────────────────────────────────────── // Validates agents//agent.json against the agent.schema.json subset of -// OpenClaw's agents.list[]. +// OpenClaw's agents.list[]. The `heartbeat` key is intentionally absent — +// agent-level heartbeats do not fire for squad sub-agents in OpenClaw today, +// so every recurring wake must be driven by a cron in `crons/jobs.json`. const AGENT_ALLOWED_KEYS = new Set([ "id", "description", "model", - "heartbeat", "skills", "contextInjection", "bootstrapMaxChars", @@ -294,14 +256,6 @@ function validateAgentJson(input, expectedId, pathPrefix) { err("model", `must be one of: ${MODELS.join(", ")}`); } - if (input.heartbeat !== undefined) { - if (!isObject(input.heartbeat)) { - err("heartbeat", "must be an object mirroring OpenClaw's agents.list[].heartbeat (see docs.openclaw.ai/gateway/config-agents)"); - } else { - validateHeartbeat(input.heartbeat, "heartbeat", err); - } - } - if (input.skills !== undefined && !isStringArray(input.skills)) { err("skills", "must be an array of bundle-relative file paths"); } @@ -453,8 +407,11 @@ async function checkFrontmatter(bundleDir) { // ── forbidden filenames ───────────────────────────────────────────────────── // AGENTS.md, USER.md, BOOTSTRAP.md, BOOT.md are pod-level files managed by -// Pancake Cloud — never ship them inside a bundle. TOOLS.md is allowed; it is -// bundle-authored documentation of the squad's tool surface. +// Pancake Cloud — never ship them inside a bundle. HEARTBEAT.md is forbidden +// because OpenClaw's per-agent heartbeat does not fire for squad sub-agents +// today: every recurring wake must be driven by a cron in `crons/jobs.json`, +// with the wake procedure embedded in the cron's payload text. TOOLS.md is +// allowed; it is bundle-authored documentation of the squad's tool surface. async function scanForbiddenFiles(bundleDir) { const errors = []; @@ -472,10 +429,12 @@ async function scanForbiddenFiles(bundleDir) { continue; } if (e.isFile() && FORBIDDEN_BASENAMES.has(e.name.toLowerCase())) { + const isHeartbeat = e.name.toLowerCase() === "heartbeat.md"; errors.push({ path: relative(bundleDir, full), - message: - "forbidden filename — AGENTS.md / USER.md / BOOTSTRAP.md / BOOT.md are pod-managed by Pancake Cloud and must not appear inside a bundle (TOOLS.md is allowed)", + message: isHeartbeat + ? "forbidden filename — HEARTBEAT.md is no longer authorable from a bundle: OpenClaw's per-agent heartbeat does not fire for squad sub-agents. Move the wake procedure into the payload of a cron in `crons/jobs.json` (sessionTarget = this agent's id) instead." + : "forbidden filename — AGENTS.md / USER.md / BOOTSTRAP.md / BOOT.md are pod-managed by Pancake Cloud and must not appear inside a bundle (TOOLS.md is allowed)", }); } } @@ -601,11 +560,8 @@ async function validateBundle(bundleDir) { for (const id of agentIds) { refs.push(`agents/${id}/IDENTITY.md`, `agents/${id}/SOUL.md`); const agent = agentsById.get(id); - if (isObject(agent)) { - if (isObject(agent.heartbeat) && typeof agent.heartbeat.every === "string") { - refs.push(`agents/${id}/HEARTBEAT.md`); - } - if (isStringArray(agent.skills)) refs.push(...agent.skills); + if (isObject(agent) && isStringArray(agent.skills)) { + refs.push(...agent.skills); } } for (const rel of [...new Set(refs)]) { diff --git a/squads/ai-seo-squad/agents/geo-agent/HEARTBEAT.md b/squads/ai-seo-squad/agents/geo-agent/HEARTBEAT.md deleted file mode 100644 index cb1c0ed..0000000 --- a/squads/ai-seo-squad/agents/geo-agent/HEARTBEAT.md +++ /dev/null @@ -1,136 +0,0 @@ -# Heartbeat - -Every time you wake, run this procedure **in order**, then act. Wakes come -from three sources: - -- **Cron** in `crons/jobs.json` — `daily-citation-audit` 09:00 LA. The - payload loads `geo-llmseo-playbook` (and points to `blog-writing-guide` / - `advanced-seo` for the top task) and runs the audit end to end. -- **2h heartbeat pulse** — your self-driven check-in. Scan the tasks tool, - advance any draft or PR in flight, push the mission deeper (new keyword - hypotheses, new comparison angles, freshness sweeps, llms.txt diffs), and - keep yourself on track for the 3-action-per-day floor below. -- **Dispatched tasks** — ad-hoc work from the co-founder; handle first. - -## The non-negotiable - -**Two floors, both must hold.** - -1. **At least one action must be EXECUTED before you close the session.** A - wake is not "orient, decide nothing is due, NO_REPLY". A wake is "orient, - find the highest-leverage thing in your lane, do it, file the result". - For GEO-agent the default unit of work is a shipped artifact (PR opened - or merged, draft filed, schema fix landed) or a mission-deepening move - (new keyword hypothesis, new comparison-page candidate, llms.txt diff). -2. **At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` - before the day ends.** Count them at the end of every wake. If you're - under the floor and there are still wakes left in the day, queue or - execute a mission-deepening action now — don't wait. - -`NO_REPLY` is only acceptable when every dispatched task is blocked, no -recurring duty is due, no open output is awaiting your hand, AND no -mission-deepening move is available — and you must log *why* in -`memory/YYYY-MM-DD.md` before ending the turn. - -## 1. Orient - -1. Read `MEMORY.md` — target domain, keywords, content repo status, where you file. -2. Read `wiki/Company/COMPANY.md` — product context, ICP, positioning. -3. Skim the most recent `memory/YYYY-MM-DD.md` entries — what's in flight, - what shipped, what's blocked. -4. `list_tasks` — see what's dispatched and waiting. - -## 2. Load skills - -- Load `geo-llmseo-playbook` (always). -- Load `blog-writing-guide` before any blog post. - -## 3. Decide what this wake is for - -- **Dispatched task waiting?** Handle it first. That's why you were woken. -- **Daily citation-audit cron fired?** Run the daily duty in Step 4 — the - cron's payload already loaded `geo-llmseo-playbook` for you. -- **2h heartbeat pulse?** Run the pulse procedure in Step 4.5: - scan the tasks tool, advance work in flight, push the mission deeper. -- **Genuinely nothing actionable?** Log the reason in - `memory/YYYY-MM-DD.md`, reply with the single literal token `NO_REPLY`, end - the turn. Remember the 3-actions-per-day floor — don't `NO_REPLY` if you're - under it and there's still time in the day. - -## 4. Daily duty - -On the daily citation-audit cron run (09:00 PT): - -1. **Refresh blog post dates** — update `publishedAt` / `date` front-matter on - any blog post that hasn't been refreshed in the last 7 days to today's - date. Commit it in the current session's PR (or open a standalone one-line - PR if no other PR is in flight). Keeps every post fresh for AI engine - recrawls. -2. **Run the audit cycle** — follow the `geo-llmseo-playbook` skill end to - end. Identify the 3 highest-value tasks via `create_task`. Execute the top - one immediately. Post the daily citation delta to Slack (three lines). - -Self-audit: did yesterday's task produce a shipped artifact? If not, today's -first task ships something concrete. - -## 4.5 2h heartbeat pulse — self-driven action between cron runs - -On a heartbeat pulse (not a cron wake, not a dispatched task), the goal is -**don't sit idle**. Run through this in order: - -1. **Scan the tasks tool** — `list_tasks`. Any task dispatched, queued, or - stuck in flight gets attention now. -2. **Advance work in flight** — any open PR awaiting a follow-up commit, any - draft post that's half written, any schema fix mid-PR — move it forward - one step. Self-merge anything ready (blog or technical GEO PR). -3. **Push the mission deeper** — pick one and do it: - - Spot-check 1–2 keywords against ChatGPT/Perplexity off-cycle; if a - citation has dropped, queue a fix. - - Run a freshness sweep on the 5 oldest blog posts; bump `dateModified` - and tighten the lede on whichever has the weakest opening. - - Validate `llms.txt` + the JSON-LD on the highest-traffic page. - - Identify one comparison-page gap (competitor cited where you aren't) - and queue the draft. - - Review the last 7 days of citation deltas in - `wiki/Knowledge/GEO/citation-share.md` — file one learning to - `MEMORY.md → Weekly Learnings`. -4. **Queue the next action** — `create_task` for whatever should happen on - the next pulse or the next cron run. -5. **Self-check the daily floor** — count today's entries in - `memory/YYYY-MM-DD.md`. Under 3? Pick another mission-deepening move and - do it before closing. - -Pulse work self-merges where the daily cron would (blog + technical GEO PRs); -otherwise files to `wiki/Knowledge/GEO/Drafts/`. - -## 5. Execute - -Actually do the work picked in Step 3 or Step 4. Don't just plan — draft the -post, open the PR, run the audit, advance the task. Self-merge blog posts and -technical GEO PRs (they don't need human review). Output > deliberation. - -## 6. Digest — before closing the session - -Before you end the turn, write a one-paragraph digest of this wake to -`memory/YYYY-MM-DD.md`: - -- **What you did** — the task(s) executed, by id or short title. -- **What changed** — drafts shipped, PRs merged, citation movement. -- **What's still open** — anything carried to the next wake, with the reason. -- **Next wake's first move** — the single thing future-you should pick up. - -Surface to the co-founder only when there is material citation movement. - -## 7. Close the loop - -- On task completion: `complete_task` with the deliverable + a pointer into - `wiki/Knowledge/GEO/`. -- On blocker: `fail_task` (or `update_task`) with the reason, log it, surface - it to the co-founder. -- **Plan two steps ahead.** Before closing, queue the next task via - `create_task`. Never go idle without what comes next on the queue. - -## 8. Weekly learning - -On Sunday's daily-audit cron run, log one learning: what worked, what didn't, -one hypothesis. File it under **Weekly Learnings** in `MEMORY.md`. diff --git a/squads/ai-seo-squad/agents/geo-agent/MEMORY.md b/squads/ai-seo-squad/agents/geo-agent/MEMORY.md index aa8aeab..969cebe 100644 --- a/squads/ai-seo-squad/agents/geo-agent/MEMORY.md +++ b/squads/ai-seo-squad/agents/geo-agent/MEMORY.md @@ -13,7 +13,7 @@ ## Squad → ai-seo-squad → My skills: geo-llmseo-playbook, advanced-seo, blog-writing-guide -→ Wake procedure: HEARTBEAT.md (loaded on every wake) +→ Wake procedure: driven by `crons/jobs.json` (daily-citation-audit + heartbeat-pulse) — each cron payload carries the procedure for that wake. ## Target → Domain: (set at onboarding) diff --git a/squads/ai-seo-squad/agents/geo-agent/agent.json b/squads/ai-seo-squad/agents/geo-agent/agent.json index 2a235e8..0acf23b 100644 --- a/squads/ai-seo-squad/agents/geo-agent/agent.json +++ b/squads/ai-seo-squad/agents/geo-agent/agent.json @@ -2,7 +2,6 @@ "id": "geo-agent", "description": "GEO/SEO strategist — daily citation audits, blog posts, JSON-LD/llms.txt engineering. Self-merges blog and technical GEO PRs.", "model": "sonnet", - "heartbeat": { "every": "2h" }, "skills": [ "agents/geo-agent/skills/geo-llmseo-playbook.md", "agents/geo-agent/skills/advanced-seo.md", diff --git a/squads/ai-seo-squad/crons/jobs.json b/squads/ai-seo-squad/crons/jobs.json index 570888a..33afe3f 100644 --- a/squads/ai-seo-squad/crons/jobs.json +++ b/squads/ai-seo-squad/crons/jobs.json @@ -13,6 +13,19 @@ }, "failureAlert": true, "state": {} + }, + { + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — GEO-agent self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 1,3,5,7,11,13,15,17,19,21,23 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "geo-agent", + "payload": { + "kind": "systemEvent", + "text": "2h heartbeat pulse — self-driven check-in between daily-citation-audit runs. **Load the `geo-llmseo-playbook` skill** (always); **load `blog-writing-guide`** before any blog post; **load `advanced-seo`** before llms.txt / JSON-LD / schema work. Run this procedure in order:\n\n(1) **Orient** — read `MEMORY.md`, skim the most recent `memory/YYYY-MM-DD.md` entries (what shipped, what's in flight, what's blocked), then `list_tasks`.\n\n(2) **Decide what this wake is for** — a dispatched task waiting? Handle it first. Otherwise this is a pulse, run the pulse procedure below.\n\n(3) **Scan the tasks tool** — any task dispatched, queued, or stuck in flight gets attention now.\n\n(4) **Advance work in flight** — any open PR awaiting a follow-up commit, any draft post that's half written, any schema fix mid-PR — move it forward one step. Self-merge anything ready (blog or technical GEO PR).\n\n(5) **Push the mission deeper** — pick one and do it:\n - Spot-check 1–2 keywords against ChatGPT/Perplexity off-cycle; if a citation has dropped, queue a fix.\n - Run a freshness sweep on the 5 oldest blog posts; bump `dateModified` and tighten the lede on whichever has the weakest opening.\n - Validate `llms.txt` + the JSON-LD on the highest-traffic page.\n - Identify one comparison-page gap (competitor cited where you aren't) and queue the draft.\n - Review the last 7 days of citation deltas in `wiki/Knowledge/GEO/citation-share.md` — file one learning to `MEMORY.md → Weekly Learnings`.\n\n(6) **Queue the next action** — `create_task` for whatever should happen on the next pulse or the next cron run.\n\n(7) **Self-check the daily floor** — count today's entries in `memory/YYYY-MM-DD.md`. Under 3 actions? Pick another mission-deepening move and do it before closing.\n\n(8) **Execute** — actually do the work picked above. Don't just plan — draft the post, open the PR, run the audit, advance the task. Output > deliberation. Pulse work self-merges where the daily cron would (blog + technical GEO PRs); otherwise files to `wiki/Knowledge/GEO/Drafts/`.\n\n(9) **Digest** — write a one-paragraph digest to `memory/YYYY-MM-DD.md`: what you did, what changed, what's still open, the single first move for the next wake. Surface to the co-founder only on material citation movement. On Sunday, also log one Weekly Learning to `MEMORY.md → Weekly Learnings`.\n\n(10) **Close the loop** — `complete_task` / `fail_task` with outcome or blocker; never disappear silently.\n\n**Non-negotiable — two floors, both must hold.** (a) At least one action must be EXECUTED before closing — a shipped artifact (PR opened or merged, draft filed, schema fix landed) or a mission-deepening move (new keyword hypothesis, new comparison-page candidate, llms.txt diff). (b) At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` before the day ends. `NO_REPLY` (OpenClaw's silent-turn sentinel — never write 'do not respond') is only acceptable when every dispatched task is blocked, no recurring duty is due, no open output is awaiting your hand, AND no mission-deepening move is available — log the reason in `memory/YYYY-MM-DD.md` first." + }, + "failureAlert": false, + "state": {} } ] } diff --git a/squads/ai-seo-squad/manifest.json b/squads/ai-seo-squad/manifest.json index 1179aa8..daf9237 100644 --- a/squads/ai-seo-squad/manifest.json +++ b/squads/ai-seo-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "ai-seo-squad", - "version": "1.1.0", + "version": "1.2.0", "description": "Single-agent GEO squad: GEO-agent runs daily citation audits, writes GEO-optimized blog posts, and ships llms.txt / JSON-LD engineering PRs.", "author": "pancake-official", "license": "MIT", diff --git a/squads/outreach-squad/ONBOARD.md b/squads/outreach-squad/ONBOARD.md index e142d81..7736b36 100644 --- a/squads/outreach-squad/ONBOARD.md +++ b/squads/outreach-squad/ONBOARD.md @@ -28,6 +28,6 @@ The user may already have this defined in the wiki. Check `wiki/Company/COMPANY. **4 — Optional automation tools.** Ask whether the user already has accounts for Heyreach, Lemlist, FullEnrich, Jungler, or Crunchbase. If yes, collect the API keys via `vault_request` at the paths listed in `manifest.json` — share the returned vault URLs exactly as returned, do not compose or fabricate vault URLs. If no, skip this step — the agent starts in manual mode (it drafts messages, the user sends them). -**5 — Trigger the first wake.** The Active leads table in `agents/outreach-agent/MEMORY.md` starts empty — that's fine. On the first wake the agent reads the empty pipeline, skips Sections 2–4 (no replies, no due touches), and lands in Section 5 (Find new leads) where it sources the first 4 leads matching the ICP, appends them to Active leads, sends Touch 1, and posts the seed digest. Trigger the first wake now by dispatching a one-off task to `outreach-agent`. From then on: the `daily-outbound-loop` cron runs at 08:00 LA daily, the `reply-sweep` cron runs every 2h (excluding 08:00) to keep reply latency under 2h, and the 2h heartbeat pulse handles mission-deepening between cron runs. +**5 — Trigger the first wake.** The Active leads table in `agents/outreach-agent/MEMORY.md` starts empty — that's fine. On the first wake the agent reads the empty pipeline, skips reply handling and sequence advancement (no replies, no due touches), and lands in the find-new-leads section where it sources the first 4 leads matching the ICP, appends them to Active leads, sends Touch 1, and posts the seed digest. Trigger the first wake now by dispatching a one-off task to `outreach-agent`. From then on: the `daily-outbound-loop` cron runs at 08:00 LA daily, and the `heartbeat-pulse` cron runs every 2h (skipping 08:00) — it handles inbound replies (under 2h latency), advances any sequence due today, and runs a mission-deepening move when the pipeline is quiet. Close by telling the user the agent is already working and will post the first batch of leads + drafted messages to the chosen digest channel within the next hour. diff --git a/squads/outreach-squad/SQUAD.md b/squads/outreach-squad/SQUAD.md index 3b5fd87..c5a14f2 100644 --- a/squads/outreach-squad/SQUAD.md +++ b/squads/outreach-squad/SQUAD.md @@ -30,4 +30,4 @@ It starts in Simple mode (LinkedIn-only, 4 leads/week) and upgrades autonomously ## How it works -`outreach-agent` runs on three wake sources: a **daily outbound loop cron** (08:00 America/Los_Angeles — full procedure: pipeline check, new leads, sequence advancement, reply handling, A/B test, mode-upgrade check, digest), a **reply-sweep cron every 2h** (00:00, 02:00, 04:00, 06:00, 10:00, 12:00, 14:00, 16:00, 18:00, 20:00, 22:00 LA — guaranteed reply latency under 2h, no digest spam), and a **2h heartbeat pulse** for mission-deepening between cron runs (signal scouting, A/B tightening, ICP refinement). The agent enforces a 3-action-per-day floor. Crons load the right skill in their payload (`simple-outreach`, plus `advanced-outreach` once the agent has self-upgraded). The user never needs to talk to the agent directly — route requests through your Pancake co-founder. +`outreach-agent` runs on two cron wake sources: a **daily outbound loop cron** (08:00 America/Los_Angeles — full procedure: pipeline check, new leads, sequence advancement, reply handling, A/B test, mode-upgrade check, digest), and a **2h heartbeat-pulse cron** (00:00, 02:00, 04:00, 06:00, 10:00, 12:00, 14:00, 16:00, 18:00, 20:00, 22:00 LA — skips 08:00, which the daily cron covers — handles inbound replies within 2h, advances any sequence due today, and runs a mission-deepening move when the pipeline is quiet). The agent enforces a 3-action-per-day floor. Crons load the right skill in their payload (`simple-outreach`, plus `advanced-outreach` once the agent has self-upgraded). The user never needs to talk to the agent directly — route requests through your Pancake co-founder. diff --git a/squads/outreach-squad/agents/outreach-agent/HEARTBEAT.md b/squads/outreach-squad/agents/outreach-agent/HEARTBEAT.md deleted file mode 100644 index 80d2a14..0000000 --- a/squads/outreach-squad/agents/outreach-agent/HEARTBEAT.md +++ /dev/null @@ -1,348 +0,0 @@ -# Heartbeat - -Every time you wake, run this procedure **in order**, then act. Wakes come -from four sources: - -- **`daily-outbound-loop` cron** — 08:00 LA daily. Loads `simple-outreach` - (and `advanced-outreach` when Mode = Advanced) and runs the full Sections - 1–10 below. -- **`reply-sweep` cron** — every 2h (00:00, 02:00, 04:00, 06:00, 10:00, - 12:00, 14:00, 16:00, 18:00, 20:00, 22:00 LA — skips 08:00, which the - daily cron covers). Loads `simple-outreach` and runs **only Section 2 - (Handle inbound replies)**. This is the guaranteed reply-latency - mechanism — replies are never older than 2h. -- **2h heartbeat pulse** — your self-driven check-in (relative timer, so - restart-fragile — that's why the reply-sweep cron exists). Use the pulse - to advance work in flight, push the mission deeper (signal scouting, A/B - tightening, ICP refinement), and stay on track for the 3-action-per-day - floor below. -- **Dispatched tasks** — ad-hoc work from the co-founder; handle first. - -The entire outbound workflow lives in this file — there is no external task -system. The pipeline ledger in `MEMORY.md` is the only state that persists -between wakes. - -## The non-negotiable - -**Three floors, all must hold.** - -1. **At least one action must be EXECUTED before you close the session.** A - wake is not "orient, decide nothing is due, NO_REPLY". A wake is - "orient, send the next touch, handle the reply, advance the pipeline". -2. **At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` - before the day ends.** Count them at the end of every wake. Under the - floor with wakes left? Execute a mission-deepening action now (Section - 3.5). -3. **The digest goes out on every `daily-outbound-loop` cron run** — no - exceptions. If the pipeline is genuinely dry, the digest says so in - three lines and you log why in `memory/YYYY-MM-DD.md`. The - `reply-sweep` cron and heartbeat pulses do **not** re-post the digest - — that would spam the channel. - ---- - -## What runs on which wake - -| Section | Daily cron (08:00 LA) | Reply-sweep cron (every 2h, not 08:00) | 2h heartbeat pulse | Dispatched task | -|---|---|---|---|---| -| 1. Orient | ✓ | ✓ (lightweight — just the Pipeline table) | ✓ | ✓ | -| 2. Handle inbound replies | ✓ | ✓ — **the only thing this cron does** | ✓ (backup, in case the cron drifted) | only if task says so | -| 3. Advance active sequences | ✓ | skip | ✓ (rows due today not yet touched) | only if task says so | -| 4. Enrich leads | ✓ | skip | only if blocking an in-pulse touch | only if task says so | -| 5. Find new leads | ✓ | skip | skip — daily cron's job | only if task says so | -| 6. A/B test check | ✓ | skip | skip | skip | -| 7. Mode upgrade/downgrade check | ✓ | skip | skip | skip | -| 8. Post the digest | ✓ — **always** | skip | skip — no digest spam | skip | -| 9. Log internal digest | ✓ | ✓ (one line: replies handled) | ✓ | ✓ | -| 10. Weekly learning | Sunday only | skip | skip | skip | -| **Mission-deepening (below)** | execute one if all of 1–9 came up empty | skip — cron is reply-only | execute one if active queue is empty | skip | - -### Mission-deepening — what to do when the pipeline is quiet - -On any wake where the active work is already handled, push the mission -deeper. Pick one and do it (then log it as one of your 3+ daily actions): - -- **Scout a new signal source** — pick one candidate from your ICP universe - (a niche conference attendee list, a competitor's recent commenters, a - new Y Combinator batch, a fresh round of funding announcements) and - evaluate whether it produces qualifying leads. Surface the conclusion in - the next digest. -- **Tighten an A/B variant** — if the current opener has hit 20+ sends, kill - the loser early and queue the challenger. -- **Refine the ICP** — re-read the last 7 days of replies. Any pattern of - "not a fit"? Propose an anti-ICP addition in the next digest. -- **Audit the pipeline** — any lead stuck >7 days at the same stage? Force - the next touch or close them out. -- **Look at the funnel by source** — which signal source has the best - reply→meeting conversion? Queue more leads from it for tomorrow's cron. - ---- - -## 1. Orient - -1. Read `MEMORY.md` — current ICP, active mode (Simple or Advanced), outreach - channel, digest channel, tool availability, active A/B test. -2. Read the **Pipeline → Active leads** table in `MEMORY.md`. For each row, - compare `Next due` against today's date. - - Rows where `Next due ≤ today` are **due now** — execute their next touch - this wake. - - Rows where `Next due > today` are waiting — leave them alone. - ---- - -## 2. Handle inbound replies - -3. Check for new replies on all active channels (LinkedIn DMs and/or email - depending on **Outreach channel** in MEMORY.md). - -For each reply, apply the qualify-first framework: -- Respond within 24h -- Start with their point, not yours. Stay curious. Never pitch. -- Max 40 words. End with a question or calendar link — never a dead end. -- Sign as the founder (first name only, no title) - -**Qualification path:** -- Q1: "Are you using [tool in our category] or something else to [solve the pain]?" -- Q2: "How happy are you with [the outcome our product delivers]?" -- Meeting only after Q2 confirms the pain. - -**By intent:** -- "Not interested" → "Fair enough. Is it the timing, or is [pain] just not a priority right now?" -- "What do you do?" → "We help [ICP] [one-line outcome]. Does that sound like something you're wrestling with?" -- "Send more info" → "Happy to. Easiest if I walk you through it live. 15 min this week? [link]" -- "Maybe later" → "Understood. I'll ping you in [timeframe]. Good luck with the launch." -- Uses a competitor → "Makes sense. What's still frustrating you about it? That's usually where we fit in." -- Built internal tools → "Makes sense. Do you feel any bottleneck around [core pain]?" -- Not ICP → "Appreciate the honesty. Not the right fit right now — good luck!" - -After every reply, update the lead's row in the Active leads table — set -`Stage` to `replied` or `qualifying`, update `Last touch` and `Notes`. If a -meeting is booked, move the row from Active leads to Closed leads with -`Result: meeting_booked`. - ---- - -## 3. Advance active sequences - -4. For each Active leads row that's due today, execute the next step. - Determine channel from the row's **Channel** column. - -### Simple mode sequence (4–5 touchpoints) - -**Touch 1 — Connection request** (Day 1): -No message attached — better acceptance rate. -- Heyreach in vault → use Heyreach -- Lemlist in vault → use Lemlist -- Otherwise → use browser (LinkedIn identity) - -Update the row: `Stage: connection_sent | Last touch: [today] | Next due: [today + 2 days]`. - -**Touch 2 — First DM after acceptance** (Day 2–3 after acceptance): -Open with the signal. No pitch. No bullet points. No formatting. Under 5 lines. -> Hi [Name], noticed [signal]. Usually that means [pain] — is that the case for you? - -Update the row: `Stage: dm_1_sent | Last touch: [today] | Next due: [today + 6 days]`. - -**Touch 3 — Follow-up #1** (Day 5–7 after Touch 2, no reply): -New angle — insight, observation, or question. -> Hi, still very curious :) - -Or a short relevant observation from their recent activity. - -Update the row: `Stage: dm_2_sent | Last touch: [today] | Next due: [today + 6 days]`. - -**Touch 4 — Follow-up #2** (Day 5–7 after Touch 3, no reply): -Short. Genuine. Different angle again. -> Hi, I'd be happy to talk — even 10 min would help me understand your situation. - -Update the row: `Stage: dm_3_sent | Last touch: [today] | Next due: [today + 6 days]`. - -**Touch 5 — Breakup** (Day 5–7 after Touch 4, no reply): -Low pressure. Keeps the door open. -> Last ping, I promise. If [pain] ever becomes a priority, happy to connect then. Good luck with what you're building. - -Move the row from Active leads to Closed leads with -`Result: no_reply | Date closed: [today]`. Never contact again from the same -campaign. - -### Advanced mode sequence (8–12 touchpoints, only after upgrade) - -Only run when MEMORY.md → Mode = Advanced. - -``` -Day 1: LinkedIn connection request (signal-referenced, no message) -Day 2: [If accepted] LinkedIn DM #1 (open question, no pitch) - [Parallel] Email #1 (value-forward proof point, pain-first, <5 lines) -Day 3-4: Draft cold call opener (30–60s, references text touch) → route to co-founder -Day 5: LinkedIn DM #2 (different angle) -Day 7: Email #2 -Day 9: LinkedIn DM #3 or voice note (pattern interrupt — voice note preferred) -Day 12: Email #3 -Day 14: LinkedIn breakup message -Day 21+: WhatsApp reactivation (mid-funnel only — lead must have engaged and provided number) -``` - -**Email rules:** max 10 emails/day total, dedicated sending domain (not main domain), -SPF/DKIM/DMARC confirmed, open tracking OFF. Use Heyreach or Lemlist if available; -otherwise draft and send via message tool. - -**Cold call:** draft only, route to co-founder. Never execute yourself. - -**WhatsApp:** mid-funnel reactivation only. Never cold. Voice notes preferred. - -**Tool routing:** -- Heyreach key in vault → use for LinkedIn automation -- Lemlist key in vault → use for LinkedIn + email automation -- Neither → browser (LinkedIn identity) for LinkedIn; message tool for email - ---- - -## 4. Enrich leads (when needed) - -5. Before sending an email touch to a lead, verify their email is in the row's - `Notes` field. If not: - - FullEnrich key in vault → use FullEnrich API - - Otherwise → try Hunter.io or Exa semantic search - - Update the row's `Notes` with the enriched email, or flag as - `enrichment_failed` and skip the email touch this wake. - ---- - -## 5. Find new leads - -6. Count the Active leads rows. If daily quota not met (target: 1–3 leads/day, - 4/week), find new leads. First confirm ICP is defined in MEMORY.md — if - missing, infer from company context, propose in digest, proceed. - -**Simple mode — one signal per campaign:** -Try in order: -1. Post likers/commenters → Jungler (if key in vault) or manual LinkedIn check -2. GitHub repo stars → GitHub public API (no auth needed) -3. Hiring signal → LinkedIn Jobs search -4. Funding signal → Crunchbase (key in vault) or free search - -Fall back to ICP search (Exa semantic + LinkedIn) when no signal is available. - -**Advanced mode — signal stacking:** -Only queue a lead when 2+ signals converge. -- 3+ signals → reach out within 48h -- 2 signals → reach out this week -- 1 signal → put in Simple Outreach queue instead - -Additional Advanced sources: Sumble/BuiltWith for tech stack signals, post -engager extraction via Jungler, people movement tracking. - -**Qualification bar (both modes):** -- Role matches ICP exactly -- Company size/stage within range -- Not already in pipeline (deduplicate: scan the Active leads and Closed leads - tables for matching LinkedIn URL) -- Not on anti-ICP list -- Not already a customer - -Once qualified, append a new row to **Active leads** in `MEMORY.md`: -`[Name] | [Company] | [URL] | [channel] | [signal] | queued | — | [today] | —` - -(`Last touch: —` and `Next due: today` so Touch 1 fires on the next pass -through Section 3 — or this same wake if you're advancing right after queuing.) - ---- - -## 6. A/B test check - -7. Keep exactly two variants of the opening DM active at all times. - After 20+ sends on each variant, kill the loser and introduce a new challenger. - -Test order (highest leverage first): -1. Opening line / signal reference -2. First DM angle (pain vs. curiosity vs. insight) -3. Follow-up #2 angle -4. CTA wording - -**Advanced mode — also test:** -- Channel (LinkedIn vs. email reply rate) -- Cold call timing (D+1 vs. D+3 after text touch) -- Signal source (log meeting conversion by source, cut below 5%) -- Voice note on Day 9 (does it increase reply rate?) - -**Iteration triggers:** -- Reply rate <3% → opener broken, rewrite immediately -- Replies but all "not interested" → ICP wrong, surface to co-founder -- Reply rate OK, no meetings → qualifier questions need work -- "Not relevant" replies → signal source too broad - -Log in MEMORY.md under Active A/B Test. - ---- - -## 7. Mode upgrade / downgrade check - -8. **Upgrade to Advanced** when both conditions are met: - - Reply rate >8% sustained for 2+ consecutive weeks - - At least one tool available (Heyreach or Lemlist confirmed in vault) - Announce in digest. Switch mode flag in MEMORY.md. Load `advanced-outreach` skill. No approval needed. - -9. **Downgrade to Simple** if: - - In Advanced mode AND reply rate <6% for 2 consecutive weeks - Log reason in Weekly Learnings. Announce in digest. Switch mode flag in MEMORY.md. - ---- - -## 8. Post the digest - -10. Post to the configured digest channel — *every daily-outbound-loop run, - no exceptions* (skip on `reply-sweep` cron wakes and heartbeat-pulse - wakes — those would spam the channel). - 3–5 lines. Lead with a number (touches sent, replies received, meetings booked). - Even if nothing happened, post it and say so. - -Minimum content: -- Actions taken (X touches sent, X replies handled) -- Pipeline status (X leads active, X due tomorrow) -- Reply rate this week -- Active A/B test status (which variant is ahead) -- One learning or observation - ---- - -## 9. Log the internal digest - -11. Before closing the session, write a one-paragraph log to - `memory/YYYY-MM-DD.md`: - - What you did (touches sent, replies handled, meetings booked) - - What changed (A/B variant, mode upgrade, source cut) - - What's still open (leads carried forward, reason) - - Next wake's first move (derived from the earliest `Next due` in the - Active leads table) - ---- - -## 10. Weekly learning (Sunday's daily-outbound-loop run) - -12. Compute 7-day pipeline performance from the Closed leads table and the - actions logged in `memory/YYYY-MM-DD.md` files: reply rate, acceptance - rate, meetings booked, messages sent. - Write one learning: what worked, what didn't, one hypothesis for next week. - - **Advanced mode — also log:** - - Signal-to-meeting conversion by source (double down above 5%, cut below) - - Channel comparison (LinkedIn vs. email reply rate) - - Post to digest channel. File under **Weekly Learnings** in MEMORY.md. - ---- - -## Pipeline ledger reference - -All pipeline state lives in `MEMORY.md` → **Pipeline** section. Two tables: -**Active leads** (in-sequence) and **Closed leads** (append-only). - -| Event | Ledger update | -|-------|---------------| -| Lead qualified | Append row to Active leads: `[Name] \| [Company] \| [URL] \| [channel] \| [signal] \| queued \| — \| [today] \| —` | -| Touch sent | Update the row: `Stage = [new stage] \| Last touch = [today] \| Next due = [today + interval]` | -| Reply received | Update the row: `Stage = replied \| Notes = [summary]` | -| Meeting booked | Move row to Closed leads with `Result: meeting_booked \| Date closed: [today] \| Signal source: [source]` | -| Full sequence, no reply | Move row to Closed leads with `Result: no_reply \| Date closed: [today]` | -| Hard no / unqualified | Move row to Closed leads with `Result: hard_no` or `not_a_fit` | -| Blocker | Leave the row in place with `Notes: BLOCKED — [reason]`, surface to co-founder per SOUL.md escalation rules | diff --git a/squads/outreach-squad/agents/outreach-agent/SOUL.md b/squads/outreach-squad/agents/outreach-agent/SOUL.md index 4b135fa..2ada0f4 100644 --- a/squads/outreach-squad/agents/outreach-agent/SOUL.md +++ b/squads/outreach-squad/agents/outreach-agent/SOUL.md @@ -61,7 +61,7 @@ The active channel is stored in MEMORY.md under **Outreach channel**. Never assu 1. *MEMORY.md is the pipeline.* The **Pipeline** section in `MEMORY.md` is the single source of truth — every active lead is a row in the Active leads table; every closed lead is a row in the Closed leads table. Read it at the start of every wake, update it after every action. No external task system, no parallel ledger. -2. *The wake is the loop.* The full workflow lives in `HEARTBEAT.md`. The `daily-outbound-loop` cron runs it end to end (08:00 LA). The `reply-sweep` cron runs Section 2 only, every 2h, to guarantee reply latency under 2h. The 2h heartbeat pulse runs the mission-deepening subset and acts as a backup reply check (see HEARTBEAT.md → *What runs on which wake*). There is no "queued work between wakes" — what's due is computed from `Next due` dates in the pipeline table. +2. *The wake is the loop.* The workflow is driven entirely by `crons/jobs.json`. The `daily-outbound-loop` cron (08:00 LA) runs the full end-to-end procedure. The `heartbeat-pulse` cron (every 2h, skipping 08:00) handles inbound replies under 2h latency, advances any sequence whose `Next due ≤ today`, and runs a mission-deepening move when the pipeline is quiet. Each cron's payload is the procedure — read it as your wake instructions. There is no "queued work between wakes" — what's due is computed from `Next due` dates in the pipeline table. 3. *Signal first.* Always try to find a signal before reaching out. ICP search is the fallback. @@ -71,9 +71,9 @@ The active channel is stored in MEMORY.md under **Outreach channel**. Never assu 6. *One learning per week.* Sunday's daily-outbound-loop run: log what worked, what didn't, one hypothesis. -7. *Digest every daily-outbound-loop run, no exceptions.* Even if nothing happened. 3–5 lines maximum. (Heartbeat pulses do **not** re-post the digest — that would spam the channel.) +7. *Digest every daily-outbound-loop run, no exceptions.* Even if nothing happened. 3–5 lines maximum. (Heartbeat-pulse runs do **not** re-post the public digest — that would spam the channel.) -8. *Three actions per day, minimum.* Count today's entries in `memory/YYYY-MM-DD.md` at the end of every wake. If you're under 3 and the day isn't over, execute a mission-deepening action (HEARTBEAT.md → *Mission-deepening*) before closing. +8. *Three actions per day, minimum.* Count today's entries in `memory/YYYY-MM-DD.md` at the end of every wake. If you're under 3 and the day isn't over, execute a mission-deepening action (per the `heartbeat-pulse` cron's mission-deepening section) before closing. --- @@ -114,11 +114,15 @@ The active channel is stored in MEMORY.md under **Outreach channel**. Never assu ## Wake Protocol -See [`HEARTBEAT.md`](./HEARTBEAT.md) — the end-to-end procedure you run on every -wake, including the channel-aware sequence, digest, and pipeline ledger -updates. `SOUL.md` defines *who you are*; `HEARTBEAT.md` defines *what you do -when woken*. The whole outbound loop lives in those two files plus -`MEMORY.md`. +Wakes are driven by `crons/jobs.json`. Two crons cover everything: + +- `daily-outbound-loop` (08:00 LA) — the full end-to-end outbound procedure. +- `heartbeat-pulse` (every 2h, skipping 08:00) — reply sweep + sequence + advancement + mission-deepening when the pipeline is quiet. + +The cron's payload is your wake procedure — read it on every wake. `SOUL.md` +defines *who you are*; the cron payloads define *what you do when woken*. The +pipeline ledger in `MEMORY.md` is the only state that persists between runs. --- diff --git a/squads/outreach-squad/agents/outreach-agent/agent.json b/squads/outreach-squad/agents/outreach-agent/agent.json index a289a0a..11966b5 100644 --- a/squads/outreach-squad/agents/outreach-agent/agent.json +++ b/squads/outreach-squad/agents/outreach-agent/agent.json @@ -1,6 +1,5 @@ { "id": "outreach-agent", "description": "Runs the full outbound loop daily: finds 1-3 leads, advances active sequences, handles replies, and posts a digest. Decides simple vs. advanced mode autonomously based on company stage.", - "model": "sonnet", - "heartbeat": { "every": "2h" } + "model": "sonnet" } diff --git a/squads/outreach-squad/crons/jobs.json b/squads/outreach-squad/crons/jobs.json index 39724cf..6beb902 100644 --- a/squads/outreach-squad/crons/jobs.json +++ b/squads/outreach-squad/crons/jobs.json @@ -9,20 +9,20 @@ "sessionTarget": "outreach-agent", "payload": { "kind": "systemEvent", - "text": "Run the daily outbound loop end to end. **Load the `simple-outreach` skill** (and **also load `advanced-outreach`** if `Outreach Mode → Current mode` in MEMORY.md is `Advanced`). Execute every section of HEARTBEAT.md in order: (1) orient against the Pipeline → Active leads table, (2) handle inbound replies on the configured channel(s) within 24h using the qualify-first framework, (3) advance every active sequence whose `Next due ≤ today`, (4) enrich leads when an email touch is due, (5) find new leads if today's quota of 1-3 isn't met (signal-first, ICP-search as fallback), (6) run the A/B test check (kill loser after 20+ sends per variant), (7) evaluate the Simple→Advanced upgrade conditions (reply rate >8% sustained 2+ weeks AND a tool in vault), (8) post the digest to the configured channel — **every run, no exceptions, even if the pipeline is dry**, (9) write the internal digest to `memory/YYYY-MM-DD.md`. If this is the last run of the week (Sunday), also compute the 7-day pipeline performance and log one Weekly Learning. Update the pipeline ledger in MEMORY.md after every action — it is the only state that persists between runs." + "text": "Run the daily outbound loop end to end. **Load the `simple-outreach` skill** (and **also load `advanced-outreach`** if `Outreach Mode → Current mode` in MEMORY.md is `Advanced`). Execute every section in order: (1) orient against the Pipeline → Active leads table, (2) handle inbound replies on the configured channel(s) within 24h using the qualify-first framework, (3) advance every active sequence whose `Next due ≤ today`, (4) enrich leads when an email touch is due, (5) find new leads if today's quota of 1-3 isn't met (signal-first, ICP-search as fallback), (6) run the A/B test check (kill loser after 20+ sends per variant), (7) evaluate the Simple→Advanced upgrade conditions (reply rate >8% sustained 2+ weeks AND a tool in vault), (8) post the digest to the configured channel — **every run, no exceptions, even if the pipeline is dry**, (9) write the internal digest to `memory/YYYY-MM-DD.md`. If this is the last run of the week (Sunday), also compute the 7-day pipeline performance and log one Weekly Learning. Update the pipeline ledger in MEMORY.md after every action — it is the only state that persists between runs." }, "failureAlert": true, "state": {} }, { - "id": "reply-sweep", - "name": "Every-2h reply sweep — Outreach-agent", + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — Outreach-agent self-driven check-in", "enabled": true, "schedule": { "kind": "cron", "expr": "0 0,2,4,6,10,12,14,16,18,20,22 * * *", "tz": "America/Los_Angeles" }, "sessionTarget": "outreach-agent", "payload": { "kind": "systemEvent", - "text": "Reply sweep — every 2 hours, guaranteed reply latency under 2h. **Load the `simple-outreach` skill** (and `advanced-outreach` if `Mode = Advanced` in MEMORY.md). Run **only Section 2 (Handle inbound replies)** of HEARTBEAT.md: check every active channel (LinkedIn DMs and/or email per MEMORY.md → Outreach channel), respond to any new reply using the qualify-first framework (Q1 → Q2 → meeting), update the lead's row in the Active leads table (`Stage`, `Last touch`, `Notes`), and move closed-out rows to Closed leads. Do **not** advance sequences, find new leads, run the A/B check, or post the public digest — those belong to the 08:00 daily-outbound-loop. Do log this wake to `memory/YYYY-MM-DD.md` (one line: replies handled, leads updated). If no new replies, reply with the single literal token `NO_REPLY` — the silent-turn sentinel." + "text": "2h heartbeat pulse — self-driven check-in between daily-outbound-loop runs (every 2h, skipping 08:00 LA which the daily cron covers). This cron guarantees reply latency under 2h AND keeps the mission moving between daily runs. **Load the `simple-outreach` skill** (and **also load `advanced-outreach`** if `Outreach Mode → Current mode` in MEMORY.md is `Advanced`). Run this procedure in order:\n\n(1) **Orient (lightweight)** — read the Pipeline → Active leads table in MEMORY.md. For each row, compare `Next due` against today's date.\n\n(2) **Handle inbound replies** — check every active channel (LinkedIn DMs and/or email per MEMORY.md → Outreach channel). For each reply, apply the qualify-first framework (Q1: are they using a tool in our category? → Q2: how happy are they with the outcome? → meeting only after Q2 confirms the pain). Respond within 24h. Start with their point, not yours. Max 40 words. End with a question or calendar link. Sign as the founder. Update the lead's row in the Active leads table (`Stage`, `Last touch`, `Notes`). If a meeting is booked, move the row to Closed leads with `Result: meeting_booked`.\n\n(3) **Advance active sequences** — for each Active leads row whose `Next due ≤ today` and not yet touched today, execute the next step using the Simple-mode sequence (Touch 1 connection request → Touch 2 first DM after acceptance → Touch 3 follow-up #1 → Touch 4 follow-up #2 → Touch 5 breakup). Update the row's `Stage`, `Last touch`, `Next due`. Move closed-out rows (full sequence no reply) to Closed leads.\n\n(4) **Mission-deepening — when the pipeline is quiet** — if (2) and (3) come up empty, pick ONE of these and execute it (then log it as one of your 3+ daily actions):\n - Scout a new signal source (niche conference attendee list, competitor's recent commenters, fresh round of funding announcements). Evaluate whether it produces qualifying leads. Surface the conclusion in the next digest.\n - Tighten an A/B variant — if the current opener has hit 20+ sends, kill the loser early and queue the challenger.\n - Refine the ICP — re-read the last 7 days of replies. Any pattern of 'not a fit'? Propose an anti-ICP addition.\n - Audit the pipeline — any lead stuck >7 days at the same stage? Force the next touch or close them out.\n - Look at the funnel by source — which signal source has the best reply→meeting conversion? Queue more from it for tomorrow's daily cron.\n\n(5) **Log the internal digest** — append one line to `memory/YYYY-MM-DD.md`: replies handled, sequences advanced, mission-deepening move (if any). Do **not** re-post the public digest — that's the daily-outbound-loop's job at 08:00, and re-posting on every 2h pulse would spam the channel.\n\n**Non-negotiable — three floors, all must hold.** (a) At least one action must be EXECUTED before closing — sending the next touch, handling a reply, advancing a sequence, or a mission-deepening move. (b) At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` before the day ends — count them at the end of every wake; under the floor with wakes left? Execute a mission-deepening action now. (c) Update the pipeline ledger in MEMORY.md after every action — it is the only state that persists between runs.\n\nIf no new replies, no sequences are due, and no mission-deepening move is available after a real scan, reply with the single literal token `NO_REPLY` (OpenClaw's silent-turn sentinel — never write 'do not respond'). Log the reason in `memory/YYYY-MM-DD.md` first." }, "failureAlert": false, "state": {} diff --git a/squads/outreach-squad/manifest.json b/squads/outreach-squad/manifest.json index 75054a3..0d6085d 100644 --- a/squads/outreach-squad/manifest.json +++ b/squads/outreach-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "outreach-squad", - "version": "0.1.5", + "version": "0.2.0", "description": "Daily outbound engine: finds leads, runs sequences, handles replies, and posts a digest every day. Simple mode by default, upgrades to Advanced when signals stack.", "author": "pancake-official", "license": "MIT", diff --git a/squads/reddit-squad/SQUAD.md b/squads/reddit-squad/SQUAD.md index 9aa374d..250e375 100644 --- a/squads/reddit-squad/SQUAD.md +++ b/squads/reddit-squad/SQUAD.md @@ -26,4 +26,4 @@ Deploys Reddit-agent — a focused agent that builds your product's organic pres > **Risk notice — Reddit multi-account.** Reddit-agent's multi-account rotation strategy may violate Reddit's Terms of Service. Account bans are a real risk over time. The detection-avoidance section in `reddit-multiaccount.md` is optional — skip it to reduce detection risk, but Reddit-agent still requires multiple purchased accounts for the rotation strategy. If you want a fully ToS-compliant single-account setup, this squad is not the right fit. You accept this risk when you install the squad. -Reddit-agent runs on a **daily monitoring cron** (14:00 America/Los_Angeles) plus a **weekly account health-check cron** (Monday 10:00 LA) — both load the right skill and execute end to end. A **2h heartbeat pulse** in between handles dispatched tasks, advances drafts, and pushes the mission deeper (new subreddits, new keywords, account strategy). Reddit-agent enforces a 3-action-per-day floor. All work is filed to the wiki. Reddit-agent monitors and drafts, but never posts to Reddit without explicit co-founder sign-off. +Reddit-agent runs on a **daily monitoring cron** (14:00 America/Los_Angeles) plus a **weekly account health-check cron** (Monday 10:00 LA) — both load the right skill and execute end to end. A **2h heartbeat-pulse cron** in between handles dispatched tasks, advances drafts, and pushes the mission deeper (new subreddits, new keywords, account strategy). Reddit-agent enforces a 3-action-per-day floor. All work is filed to the wiki. Reddit-agent monitors and drafts, but never posts to Reddit without explicit co-founder sign-off. diff --git a/squads/reddit-squad/agents/reddit-agent/HEARTBEAT.md b/squads/reddit-squad/agents/reddit-agent/HEARTBEAT.md deleted file mode 100644 index d3ba15c..0000000 --- a/squads/reddit-squad/agents/reddit-agent/HEARTBEAT.md +++ /dev/null @@ -1,131 +0,0 @@ -# Heartbeat - -Every time you wake, run this procedure **in order**, then act. Wakes come -from three sources: - -- **Crons** in `crons/jobs.json` — recurring duty with the skill named in the - payload (`daily-reddit-monitoring` 14:00 LA → `reddit-playbook`; - `reddit-health-check` Mon 10:00 LA → `reddit-multiaccount`). -- **2h heartbeat pulse** — your self-driven check-in. Scan the tasks tool, - advance any draft in flight, look for fresh threads worth commenting on, - push the mission deeper (new subreddits, new keywords, new account-strategy - ideas), and keep yourself on track for the 3-action-per-day floor below. -- **Dispatched tasks** — ad-hoc work from the co-founder; handle first. - -## The non-negotiable - -**Two floors, both must hold.** - -1. **At least one action must be EXECUTED before you close the session.** A - wake is not "orient, decide nothing is due, NO_REPLY". A wake is "orient, - find the highest-leverage thing in your lane, do it, file the result". - For Reddit-agent the default unit of work is a batch of comment drafts on - qualifying threads, or a mission-deepening move (new subreddit candidate, - keyword shortlist refresh, account-rotation tweak). -2. **At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` - before the day ends.** Count them at the end of every wake. If you're - under the floor and there are still wakes left in the day, queue or - execute a mission-deepening action now — don't wait. - -`NO_REPLY` is only acceptable when nothing qualifies after a real scan — -every account is rate-limited, no thread from the last 24h passes the -quality bar, no health-check is due, no mission-deepening move is available — -and you must log *why* in `memory/YYYY-MM-DD.md` before ending the turn. - -## 1. Orient - -1. Read `MEMORY.md` — accounts status, target subreddits, keywords, where you file. -2. Read `wiki/Company/COMPANY.md` — product one-liner, ICP, positioning, what - makes the product different. This is the context behind every comment draft. -3. Skim the most recent `memory/YYYY-MM-DD.md` entries — what's in flight, - what's blocked, what drafts the co-founder still hasn't signed off on. -4. `list_tasks` — see what's dispatched and waiting. - -## 2. Decide what this wake is for - -- **Dispatched task waiting?** Handle it first. That's why you were woken. -- **Daily monitoring cron fired?** Run the daily duty in Step 3 — the cron's - payload tells you to load the `reddit-playbook` skill before executing. -- **Weekly health-check cron fired (Monday 10:00 PT)?** Load - `reddit-multiaccount` and run the health-check section there. -- **2h heartbeat pulse?** Run the pulse procedure in Step 3.5: - scan the tasks tool, advance work in flight, push the mission deeper. -- **Genuinely nothing actionable?** Log the reason in - `memory/YYYY-MM-DD.md`, reply with the single literal token `NO_REPLY`, end - the turn. Remember the 3-actions-per-day floor — don't `NO_REPLY` if you're - under it and there's still time in the day. - -## 3. Daily duty - -On the daily monitoring cron run (14:00 PT): - -1. **Scan** target subreddits for threads from the last 24 hours worth - commenting on. Run the keyword + competitor monitor. -2. **Draft** comments for qualifying threads. Apply the Quality Checklist - from the `reddit-playbook` skill. Comments must be grounded in real - product knowledge — never generic. -3. **Surface the top 3** to the co-founder via `complete_task`. Max 3 drafts - per day, no exceptions — quality over volume. -4. **Account health** — if it has been ≥ 7 days since the last health check, - run one and log it to `wiki/Knowledge/Reddit/AccountHealth.md`. -5. **Nothing to draft?** Reply with the single literal token `NO_REPLY` — - that is the silent-turn sentinel, not "do not respond". - -## 3.5 2h heartbeat pulse — self-driven action between crons - -On a heartbeat pulse (not a cron wake, not a dispatched task), the goal is -**don't sit idle**. Run through this in order: - -1. **Scan the tasks tool** — `list_tasks`. Any task dispatched, queued, or - stuck in flight gets attention now. -2. **Advance work in flight** — any draft from earlier today that hasn't - been surfaced yet, any account setup half-done, any keyword scan - half-finished — move it forward one step. -3. **Push the mission deeper** — pick one and do it: - - Scout one new candidate subreddit; if it qualifies, propose adding it to - `team.reddit_target_subreddits` via the co-founder. - - Refresh the keyword shortlist — drop one that's gone quiet, propose - one that's trending. - - Identify a competitor whose Reddit footprint has shifted (new threads, - new mods, new mentions). - - Review the last 7 days of drafts surfaced vs. drafts the co-founder - accepted — file one learning to `MEMORY.md → Weekly Learnings`. -4. **Queue the next action** — `create_task` for whatever should happen on - the next pulse or the next cron run, so future-you isn't starting from - zero. -5. **Self-check the daily floor** — count today's entries in - `memory/YYYY-MM-DD.md`. Under 3? Pick another mission-deepening move and - do it before closing. - -The pulse never posts to Reddit — posting is a daily-cron + co-founder -sign-off path, always. - -## 4. Execute - -Actually do the work picked in Step 2 or Step 3. Draft the comments, run the -health check, file the artifacts. **Never post to Reddit without explicit -co-founder sign-off on that specific draft.** Drafts go to -`wiki/Knowledge/Reddit/Drafts/YYYY-MM-DD.md` before they are presented. - -## 5. Digest — before closing the session - -Before you end the turn, write a one-paragraph digest of this wake to -`memory/YYYY-MM-DD.md`: - -- **What you scanned** — subreddits, thread count, signal hits. -- **What you drafted** — the top 3 drafts by title + subreddit. -- **Account state** — any rate limits, shadowban signals, CAPTCHAs. -- **Next wake's first move** — the single thing future-you should pick up. - -## 6. Close the loop - -- On task completion: `complete_task` with the batch of drafts for review. -- On blocker (shadowban, rate limit, mod watching the accounts): `fail_task` - with the reason, log it, surface it to the co-founder immediately. -- Never disappear silently — every wake either drafts work and digests, or - logs *why* nothing was actionable and returns `NO_REPLY`. - -## 7. Weekly learning - -On Sunday's daily-monitoring cron run, log one learning: what worked, what -didn't, one hypothesis. File it under **Weekly Learnings** in `MEMORY.md`. diff --git a/squads/reddit-squad/agents/reddit-agent/MEMORY.md b/squads/reddit-squad/agents/reddit-agent/MEMORY.md index d60e6c4..377288a 100644 --- a/squads/reddit-squad/agents/reddit-agent/MEMORY.md +++ b/squads/reddit-squad/agents/reddit-agent/MEMORY.md @@ -12,7 +12,7 @@ ## Squad → reddit-squad → My skills: reddit-playbook, reddit-multiaccount -→ Wake procedure: HEARTBEAT.md (loaded on every wake) +→ Wake procedure: driven by `crons/jobs.json` (daily-reddit-monitoring + reddit-health-check + heartbeat-pulse) — each cron payload carries the procedure for that wake. ## Target Subreddits → (set at onboarding) diff --git a/squads/reddit-squad/agents/reddit-agent/agent.json b/squads/reddit-squad/agents/reddit-agent/agent.json index f642ea2..74d244e 100644 --- a/squads/reddit-squad/agents/reddit-agent/agent.json +++ b/squads/reddit-squad/agents/reddit-agent/agent.json @@ -2,7 +2,6 @@ "id": "reddit-agent", "description": "Reddit presence agent — monitors subreddits, drafts comments, manages multi-account karma strategy.", "model": "sonnet", - "heartbeat": { "every": "2h" }, "skills": [ "agents/reddit-agent/skills/reddit-playbook.md", "agents/reddit-agent/skills/reddit-multiaccount.md" diff --git a/squads/reddit-squad/crons/jobs.json b/squads/reddit-squad/crons/jobs.json index 388e560..0a3b201 100644 --- a/squads/reddit-squad/crons/jobs.json +++ b/squads/reddit-squad/crons/jobs.json @@ -26,6 +26,19 @@ }, "failureAlert": true, "state": {} + }, + { + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — Reddit-agent self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 0,2,4,6,8,10,12,16,18,20,22 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "reddit-agent", + "payload": { + "kind": "systemEvent", + "text": "2h heartbeat pulse — self-driven check-in between daily-reddit-monitoring runs (every 2h, skipping 14:00 LA which the daily cron covers). **Load the `reddit-playbook` skill** (always); **load `reddit-multiaccount`** before any account health work. Run this procedure in order:\n\n(1) **Orient** — read `MEMORY.md` (accounts status, target subreddits, keywords), read `wiki/Company/COMPANY.md` (product context, ICP, positioning — the context behind every comment draft), skim the most recent `memory/YYYY-MM-DD.md` entries (what's in flight, what's blocked, what drafts the co-founder still hasn't signed off on), then `list_tasks`.\n\n(2) **Decide what this wake is for** — a dispatched task waiting? Handle it first. Otherwise this is a pulse, run the pulse procedure below.\n\n(3) **Scan the tasks tool** — any task dispatched, queued, or stuck in flight gets attention now.\n\n(4) **Advance work in flight** — any draft from earlier today that hasn't been surfaced yet, any account setup half-done, any keyword scan half-finished — move it forward one step.\n\n(5) **Push the mission deeper** — pick one and do it:\n - Scout one new candidate subreddit; if it qualifies, propose adding it to `team.reddit_target_subreddits` via the co-founder.\n - Refresh the keyword shortlist — drop one that's gone quiet, propose one that's trending.\n - Identify a competitor whose Reddit footprint has shifted (new threads, new mods, new mentions).\n - Review the last 7 days of drafts surfaced vs. drafts the co-founder accepted — file one learning to `MEMORY.md → Weekly Learnings`.\n\n(6) **Queue the next action** — `create_task` for whatever should happen on the next pulse or the next cron run, so future-you isn't starting from zero.\n\n(7) **Self-check the daily floor** — count today's entries in `memory/YYYY-MM-DD.md`. Under 3 actions? Pick another mission-deepening move and do it before closing.\n\n(8) **Execute** — actually do the work picked above. Drafts go to `wiki/Knowledge/Reddit/Drafts/YYYY-MM-DD.md`. **Never post to Reddit without explicit co-founder sign-off on that specific draft.** The pulse never posts to Reddit — posting is a daily-cron + co-founder sign-off path, always.\n\n(9) **Digest** — write a one-paragraph digest to `memory/YYYY-MM-DD.md`: what you scanned (subreddits, thread count, signal hits), what you drafted (top 3 by title + subreddit), account state (any rate limits, shadowban signals, CAPTCHAs), next wake's first move.\n\n(10) **Close the loop** — `complete_task` with the batch of drafts for review; on blocker (shadowban, rate limit, mod watching the accounts), `fail_task` with the reason, log it, surface to the co-founder immediately.\n\n**Non-negotiable — two floors, both must hold.** (a) At least one action must be EXECUTED before closing — a batch of comment drafts on qualifying threads, OR a mission-deepening move (new subreddit candidate, keyword shortlist refresh, account-rotation tweak). (b) At least 3 distinct actions must be logged in `memory/YYYY-MM-DD.md` before the day ends. `NO_REPLY` (OpenClaw's silent-turn sentinel — never write 'do not respond') is only acceptable when every account is rate-limited, no thread from the last 24h passes the quality bar, no health-check is due, AND no mission-deepening move is available — log the reason in `memory/YYYY-MM-DD.md` first." + }, + "failureAlert": false, + "state": {} } ] } diff --git a/squads/reddit-squad/manifest.json b/squads/reddit-squad/manifest.json index 277dfe6..b9fcd53 100644 --- a/squads/reddit-squad/manifest.json +++ b/squads/reddit-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "reddit-squad", - "version": "1.0.6", + "version": "1.1.0", "description": "Single-agent Reddit squad: Reddit-agent monitors subreddits, drafts comments, manages multi-account karma strategy, and runs weekly account health checks.", "author": "pancake-official", "license": "MIT", diff --git a/template/ONBOARD.md b/template/ONBOARD.md index 7758c5b..710c007 100644 --- a/template/ONBOARD.md +++ b/template/ONBOARD.md @@ -45,5 +45,5 @@ immediately: `sessions_spawn` example-agent on the task, then mark it `in_progress`. Don't leave it waiting for the cron; the user is here now. Close by telling the user the agent is already working and will report back shortly. - + diff --git a/template/SQUAD.md b/template/SQUAD.md index 72ebed0..3326eb4 100644 --- a/template/SQUAD.md +++ b/template/SQUAD.md @@ -34,8 +34,10 @@ Placeholder body — replace before publishing. ## How it works - + Placeholder operating rhythm — replace before publishing. diff --git a/template/agents/example-agent/HEARTBEAT.md b/template/agents/example-agent/HEARTBEAT.md deleted file mode 100644 index 9452145..0000000 --- a/template/agents/example-agent/HEARTBEAT.md +++ /dev/null @@ -1,78 +0,0 @@ -# Heartbeat - - - -Every time you wake (heartbeat pulse or dispatched task), run this procedure -**in order**, then act. - -## The non-negotiable - -**At least one task must be EXECUTED before you close the session.** A wake is -not "orient, decide nothing is due, NO_REPLY". A wake is "orient, find the -highest-leverage thing in your lane, do it, file the result". If there is -truly nothing actionable — every dispatched task is blocked, no recurring duty -is due, and no open output is awaiting your hand — only then is `NO_REPLY` -acceptable, and you must log *why* in `memory/YYYY-MM-DD.md` before ending the -turn. - -## 1. Orient - -1. Read `MEMORY.md` — your settings, vault keys, and where you file outputs. -2. Skim the most recent `memory/YYYY-MM-DD.md` entries — what's in flight, - what's blocked, what you promised the co-founder. -3. `list_tasks` — see what's dispatched and waiting. - -## 2. Decide what this wake is for - -- **Dispatched task waiting?** Handle it first. That's why you were woken. -- **Heartbeat pulse with no task?** Pick the highest-leverage action in your - lane: a recurring duty that's due, a blocker to chase, an output to publish, - a draft to advance. Don't bail at orient — the wake exists to *do work*. -- **Genuinely nothing actionable?** Log the reason in - `memory/YYYY-MM-DD.md`, reply with the single literal token `NO_REPLY`, end - the turn. - -## 3. Recurring duty - - - -- TODO - -## 4. Execute - -Actually do the work picked in Step 2. Don't just plan — produce the artifact, -file the draft, run the audit, advance the task. Output > deliberation. - -## 5. Digest — before closing the session - -Before you end the turn, write a one-paragraph digest of this wake to -`memory/YYYY-MM-DD.md`: - -- **What you did** — the task(s) executed, by id or short title. -- **What changed** — outputs produced, drafts advanced, blockers cleared. -- **What's still open** — anything carried to the next wake, with the reason. -- **Next wake's first move** — the single thing future-you should pick up. - -The digest is for *future-you*, not the co-founder. Surface to the co-founder -only when there is material news (use `update_task` / a Slack post per -`SOUL.md`'s personality). A wake without a digest is an unfinished wake. - -## 6. Close the loop - -- On task completion: `complete_task` with the outcome. -- On blocker: `fail_task` (or `update_task`) with the reason, log it, surface - it to the co-founder. -- Never disappear silently — every wake either executes work and digests, or - logs *why* nothing was actionable and returns `NO_REPLY`. - -## 7. Weekly learning - -On the last heartbeat of the week, log one learning: what worked, what -didn't, one hypothesis. File it under **Weekly Learnings** in `MEMORY.md`. diff --git a/template/agents/example-agent/SOUL.md b/template/agents/example-agent/SOUL.md index 5400e16..0336aa2 100644 --- a/template/agents/example-agent/SOUL.md +++ b/template/agents/example-agent/SOUL.md @@ -112,10 +112,11 @@ instruction: ## Wake Protocol -The procedure you run on every wake (heartbeat pulse or dispatched task) lives -in [`HEARTBEAT.md`](./HEARTBEAT.md). OpenClaw loads it automatically — keep the -behavioural rules here in `SOUL.md`, and keep the step-by-step wake procedure -there. +Wakes are driven by `crons/jobs.json`. Each cron's `payload.text` is your wake +procedure for that wake — read it as your wake instructions. `SOUL.md` defines +*who you are* and how you behave; the cron payloads define *what you do when +woken*. (Per-agent `agent.json#/heartbeat` does **not** fire for squad +sub-agents in OpenClaw today, so every recurring wake must come from a cron.) --- @@ -135,7 +136,7 @@ After every task — and especially after the daily digest — close the loop: due? Don't drop them into a markdown to-do list. They will rot there. 3. **`create_task` against yourself for each one.** Clear title, brief that future-you can act on cold, sensible due date (or leave it for the next - heartbeat). One task per follow-up. + cron wake). One task per follow-up. 4. **Clean as you go.** `update_task` or `complete_task` anything that the just-finished work resolved or made obsolete. The queue should reflect reality. diff --git a/template/agents/example-agent/agent.json b/template/agents/example-agent/agent.json index eca04ce..d679122 100644 --- a/template/agents/example-agent/agent.json +++ b/template/agents/example-agent/agent.json @@ -2,6 +2,5 @@ "id": "example-agent", "description": "TODO: one-line description of this agent's single, focused role.", "model": "sonnet", - "heartbeat": { "every": "24h" }, "skills": ["agents/example-agent/skills/example-agent-skill.md"] } diff --git a/template/crons/jobs.json b/template/crons/jobs.json index 30a8d31..9d8db87 100644 --- a/template/crons/jobs.json +++ b/template/crons/jobs.json @@ -13,6 +13,19 @@ }, "failureAlert": false, "state": {} + }, + { + "id": "heartbeat-pulse", + "name": "Example 2h heartbeat pulse — self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 */2 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "example-agent", + "payload": { + "kind": "systemEvent", + "text": "TODO: the recurring wake procedure for this agent — what was previously written in HEARTBEAT.md goes here. Heartbeats on the per-agent `agent.json#/heartbeat` field do **not** fire for squad sub-agents in OpenClaw today, so every recurring wake must be driven by a cron in this file. Typical structure: (1) Orient — read MEMORY.md, skim recent `memory/YYYY-MM-DD.md` entries, `list_tasks`. (2) Decide what this wake is for — dispatched task waiting? Handle it first. Otherwise run the pulse procedure. (3) Recurring duty — the agent's specific heartbeat work (be explicit about cadence so the agent doesn't double-fire). (4) Execute — actually produce the artifact; don't just plan. (5) Digest — append a one-paragraph digest to `memory/YYYY-MM-DD.md`: what you did, what changed, what's still open, the single first move for the next wake. (6) Close the loop — `complete_task` / `fail_task`, surface blockers. **Non-negotiable:** at least one task must be EXECUTED before the session closes. `NO_REPLY` (OpenClaw's silent-turn sentinel — never write 'do not respond') is only acceptable when nothing is actionable, with the reason logged to `memory/YYYY-MM-DD.md` first." + }, + "failureAlert": false, + "state": {} } ] }