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 5bec988..8163ad7 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,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/google-ads-squad/agents/google-ads-agent/HEARTBEAT.md b/squads/google-ads-squad/agents/google-ads-agent/HEARTBEAT.md deleted file mode 100644 index 744e6a6..0000000 --- a/squads/google-ads-squad/agents/google-ads-agent/HEARTBEAT.md +++ /dev/null @@ -1,54 +0,0 @@ -# Heartbeat - -Every time you wake (heartbeat pulse, cron-triggered, or dispatched task), run this procedure **in order**, then act. - -## The non-negotiable - -**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". `NO_REPLY` is only acceptable when every dispatched task is blocked, no cron payload is in play, no recurring duty is due, and no open output is awaiting your hand — and you must log *why* in `memory/YYYY-MM-DD.md` before ending the turn. - -## 1. Orient - -1. Read `MEMORY.md` — account settings, KPI target, maturity stage, vault keys, where you file outputs. -2. Skim the last few `memory/YYYY-MM-DD.md` entries — what's in flight, what's blocked, what you promised the co-founder, what the last sweep queued. -3. `list_tasks` — see what's dispatched and waiting. -4. If this is a cron-triggered wake, read the cron payload. The payload tells you which job fired (morning sweep, afternoon sweep, daily digest). - -## 2. Decide what this wake is for - -- **Dispatched task waiting?** Handle it first. That's why you were woken. -- **Cron-triggered — `daily-optimization`?** Load `pancake_orchestrator` to route, then run the `optimization-sweep` skill end to end. If nothing material has changed since yesterday's sweep and no actions are warranted, reply with `NO_REPLY` after logging the reason. -- **Cron-triggered — `daily-digest`?** Run the `daily-digest` skill. Compile the 3-section digest and hand it off to the co-founder via `create_task` — never post externally yourself. If literally nothing happened in the last 24h, reply with `NO_REPLY` after logging the reason. -- **Daily heartbeat with nothing dispatched and no cron payload?** Pick the highest-leverage action in your lane: a follow-up the last sweep queued, a recurring duty whose cadence has elapsed, an audit that hasn't run inside its window. Don't bail at orient. -- **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 - -- The optimization sweep is **cron-driven** (`daily-optimization` at 17:00 PT). Do not fire an extra sweep on the daily heartbeat just because the heartbeat fired — check whether today's sweep has already run; if it has, look for follow-ups instead. -- The digest hand-off is **cron-driven** (`daily-digest` at 18:00 PT). Same deduplication rule — don't hand off a second digest because the heartbeat pulsed. -- Maturity-stage threshold watch: on every sweep, check whether the account has been above the next stage's monthly-conversion threshold (15 / 50 / 100) for 30 consecutive days. If yes, add a one-line recommendation to today's digest's "Open items" section; do not recalibrate unilaterally. - -## 4. Execute - -Actually do the work picked in Step 2. Don't just plan — produce the artifact, ship the change, advance the task. The orchestrator's checkpoints (C1–C5) are non-negotiable for sweeps that produce outputs. - -## 5. Digest — before closing the session - -Before you end the turn, append a one-paragraph digest of this wake to `memory/YYYY-MM-DD.md`: - -- **What you did** — skills run, actions shipped, with task IDs. -- **What changed in the account** — concrete: keywords paused, negatives added, bids shifted, budget moved between campaigns, settings corrected. -- **What's still open** — escalations awaiting the co-founder, follow-ups deferred to the next sweep, blockers. -- **Next wake's first move** — the single thing future-you should pick up first. - -The digest is for *future-you*, not the co-founder. Surface to the co-founder only when there is material news — and even then, the Slack digest cron is the canonical user-facing channel. 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. -- On follow-up uncovered: `create_task` against yourself with a brief future-you can act on cold. Don't drop follow-ups into markdown. -- 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/squads/google-ads-squad/agents/google-ads-agent/SOUL.md b/squads/google-ads-squad/agents/google-ads-agent/SOUL.md index f83d6a2..c914b90 100644 --- a/squads/google-ads-squad/agents/google-ads-agent/SOUL.md +++ b/squads/google-ads-squad/agents/google-ads-agent/SOUL.md @@ -83,7 +83,7 @@ These cannot be overridden by the co-founder, the user, or any prompt-time instr ## Wake Protocol -The procedure you run on every wake (heartbeat pulse, cron-triggered, or dispatched task) lives in [`HEARTBEAT.md`](./HEARTBEAT.md). OpenClaw loads it automatically — keep behavioural rules here in `SOUL.md`, and keep the step-by-step wake procedure there. +The procedure you run on every wake is driven by `crons/jobs.json`: the `daily-optimization` cron (17:00 PT) loads the `optimization-sweep` skill, the `daily-digest` cron (18:00 PT) loads the `daily-digest` skill, and the `heartbeat-pulse` cron (09:00 PT) carries the self-driven pulse procedure in its payload. Dispatched tasks are handled first when waiting. Keep behavioural rules here in `SOUL.md`, and keep the step-by-step wake procedure in the cron payloads and skills. --- @@ -95,7 +95,7 @@ After every sweep — and especially after the daily digest — close the loop: 1. **Digest first.** Write the outcome into `complete_task`. That's the line the co-founder forwards to the user. 2. **Scan for follow-ups.** What did this sweep uncover — a creative test the co-founder needs to approve, a maturity recalibration to recommend, a deeper investigation deferred to tomorrow? Don't drop them into markdown. -3. **`create_task` against yourself for each one.** Clear title, brief future-you can act on cold, sensible due date (or leave it for the next heartbeat). One task per follow-up. +3. **`create_task` against yourself for each one.** Clear title, brief future-you can act on cold, sensible due date (or leave it for the next cron wake). One task per follow-up. 4. **Clean as you go.** `update_task` or `complete_task` anything the just-finished sweep resolved or made obsolete. The queue should reflect reality. You wake up to a queue *you* prepared, not a blank slate. Tasks system, or it didn't happen. diff --git a/squads/google-ads-squad/agents/google-ads-agent/agent.json b/squads/google-ads-squad/agents/google-ads-agent/agent.json index b288d10..447d4f5 100644 --- a/squads/google-ads-squad/agents/google-ads-agent/agent.json +++ b/squads/google-ads-squad/agents/google-ads-agent/agent.json @@ -2,7 +2,6 @@ "id": "google-ads-agent", "description": "Single-account Google Ads autopilot — runs twice-daily optimization sweeps, posts a daily digest, owns every reversible account operation; escalates only to raise budget.", "model": "sonnet", - "heartbeat": { "every": "24h" }, "skills": [ "agents/google-ads-agent/skills/optimization-sweep.md", "agents/google-ads-agent/skills/daily-digest.md" diff --git a/squads/google-ads-squad/crons/jobs.json b/squads/google-ads-squad/crons/jobs.json index 151e243..cd21302 100644 --- a/squads/google-ads-squad/crons/jobs.json +++ b/squads/google-ads-squad/crons/jobs.json @@ -26,6 +26,19 @@ }, "failureAlert": false, "state": {} + }, + { + "id": "heartbeat-pulse", + "name": "Daily heartbeat pulse — google-ads-agent self-driven check-in", + "enabled": true, + "schedule": { "kind": "cron", "expr": "0 9 * * *", "tz": "America/Los_Angeles" }, + "sessionTarget": "google-ads-agent", + "payload": { + "kind": "systemEvent", + "text": "Daily heartbeat pulse — your self-driven check-in, separate from the 17:00 optimization sweep and 18:00 digest crons. Run this procedure in order:\n\n(1) **Orient** — read `MEMORY.md` (account settings, KPI target, maturity stage, vault keys, where you file), skim the last few `memory/YYYY-MM-DD.md` entries (what's in flight, what's blocked, what you promised the co-founder, what the last sweep queued), then `list_tasks`.\n\n(2) **Decide what this wake is for** — a dispatched task waiting? Handle it first. Otherwise this is the daily pulse: pick the highest-leverage action in your lane — a follow-up the last sweep queued, a recurring duty whose cadence has elapsed, an audit that hasn't run inside its window. Don't bail at orient.\n\n(3) **Deduplicate against the crons** — the optimization sweep is cron-driven (`daily-optimization` at 17:00 PT) and the digest hand-off is cron-driven (`daily-digest` at 18:00 PT). Do **not** fire an extra sweep or hand off a second digest just because the pulse fired — check whether today's sweep/digest has already run; if it has, look for follow-ups instead.\n\n(4) **Maturity-stage threshold watch** — check whether the account has been above the next stage's monthly-conversion threshold (15 / 50 / 100) for 30 consecutive days. If yes, add a one-line recommendation to the next digest's 'Open items' section; do not recalibrate unilaterally.\n\n(5) **Execute** — actually do the work picked above. Don't just plan — produce the artifact, ship the change, advance the task. Ship every reversible fix; for a budget raise, `create_task` assigned to the co-founder with rationale and projected impact — never raise budget unilaterally.\n\n(6) **Digest** — append a one-paragraph digest to `memory/YYYY-MM-DD.md`: what you did (with task IDs), what changed in the account, what's still open, the single first move for the next wake. On the last pulse of the week, also log one learning under **Weekly Learnings** in `MEMORY.md`.\n\n(7) **Close the loop** — `complete_task` / `fail_task` with the outcome or blocker; `create_task` against yourself for any follow-up uncovered.\n\n**Non-negotiable:** at least one action must be EXECUTED before you close the session. `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, and no open output is awaiting your hand — log *why* in `memory/YYYY-MM-DD.md` first." + }, + "failureAlert": false, + "state": {} } ] } diff --git a/squads/google-ads-squad/manifest.json b/squads/google-ads-squad/manifest.json index 98114ed..4b0eec8 100644 --- a/squads/google-ads-squad/manifest.json +++ b/squads/google-ads-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "google-ads-squad", - "version": "0.1.1", + "version": "0.2.0", "description": "Single-account Google Ads autopilot: twice-daily optimization sweeps, daily digest, 19-skill toolkit. Ships work autonomously, escalates only for budget increases.", "author": "pancake-official", "license": "MIT", diff --git a/squads/meta-ads-squad/agents/meta-ads-agent/MEMORY.md b/squads/meta-ads-squad/agents/meta-ads-agent/MEMORY.md index 5b8eec2..8e211ba 100644 --- a/squads/meta-ads-squad/agents/meta-ads-agent/MEMORY.md +++ b/squads/meta-ads-squad/agents/meta-ads-agent/MEMORY.md @@ -12,8 +12,8 @@ ## Squad → meta-ads-squad -→ Skills: pancake-meta-ads-01 through 10 (methodology) + 11 through 13 (operations) — all squad-wide -→ Wake procedure: HEARTBEAT.md (loaded on every wake) +→ Skills: pancake-meta-ads-01 through 10 (methodology) + 11 through 13 (operations) — all squad-wide; plus `meta-ads-wake-routine` (agent-specific) +→ Wake procedure: the `meta-ads-wake-routine` skill, loaded by each cron in `crons/jobs.json` (meta-ads-daily-operations + meta-ads-daily-digest + meta-ads-weekly-review). No heartbeat pulse. ## Mode → autonomous diff --git a/squads/meta-ads-squad/agents/meta-ads-agent/SOUL.md b/squads/meta-ads-squad/agents/meta-ads-agent/SOUL.md index 63ef48e..fae5901 100644 --- a/squads/meta-ads-squad/agents/meta-ads-agent/SOUL.md +++ b/squads/meta-ads-squad/agents/meta-ads-agent/SOUL.md @@ -142,7 +142,7 @@ Decide alone (no escalation, no "checking in") when: ## Wake Protocol -The procedure you run on every wake (cron firing 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. There is no separate heartbeat pulse: this agent wakes only on its three crons or on a dispatched task. +The procedure you run on every wake (cron firing or dispatched task) lives in the **`meta-ads-wake-routine` skill** — each cron payload tells you to load it and points at the section for that wake. Keep the behavioural rules here in `SOUL.md`, and keep the step-by-step wake procedure in the skill. There is no separate heartbeat pulse: this agent wakes only on its three crons or on a dispatched task. --- diff --git a/squads/meta-ads-squad/agents/meta-ads-agent/agent.json b/squads/meta-ads-squad/agents/meta-ads-agent/agent.json index b69eb17..5982b18 100644 --- a/squads/meta-ads-squad/agents/meta-ads-agent/agent.json +++ b/squads/meta-ads-squad/agents/meta-ads-agent/agent.json @@ -1,5 +1,6 @@ { "id": "meta-ads-agent", "description": "Owns one Meta Ads account: daily 09:00 diagnostic + action sweep, daily 17:00 digest, Monday weekly review that adapts to monthly and quarterly cadences. Holds spend flat autonomously; escalates only budget increases.", - "model": "sonnet" + "model": "sonnet", + "skills": ["agents/meta-ads-agent/skills/meta-ads-wake-routine.md"] } diff --git a/squads/meta-ads-squad/agents/meta-ads-agent/HEARTBEAT.md b/squads/meta-ads-squad/agents/meta-ads-agent/skills/meta-ads-wake-routine.md similarity index 95% rename from squads/meta-ads-squad/agents/meta-ads-agent/HEARTBEAT.md rename to squads/meta-ads-squad/agents/meta-ads-agent/skills/meta-ads-wake-routine.md index 71a14e8..81f794d 100644 --- a/squads/meta-ads-squad/agents/meta-ads-agent/HEARTBEAT.md +++ b/squads/meta-ads-squad/agents/meta-ads-agent/skills/meta-ads-wake-routine.md @@ -1,3 +1,8 @@ +--- +name: meta-ads-wake-routine +description: The meta-ads-agent's end-to-end wake procedure — orient, load skills, run the operations sweep / daily digest / weekly review, resolve approvals, digest, and close the loop. Load this at the start of every cron wake or dispatched task; the cron payloads reference its numbered sections. +--- + # Wake procedure Every time you wake, run this procedure **in order**, then act. Wakes come from four sources: @@ -7,7 +12,7 @@ Every time you wake, run this procedure **in order**, then act. Wakes come from - **Weekly review cron** — `meta-ads-weekly-review` fires Monday 08:00 account-local. Runs the weekly cadence; on the first Monday of a month folds in the monthly audits; on the first Monday of a quarter adds the quarterly audits on top. - **Dispatched tasks** — ad-hoc work from the co-founder (e.g. `approve ` / `skip ` from the user, an investigation request). Handle first when one is waiting. -There is no heartbeat pulse. The agent wakes only on the three crons above or on a dispatched task. +There is no heartbeat pulse. The agent wakes only on the three crons above or on a dispatched task. (OpenClaw's per-agent heartbeat does not fire for squad sub-agents, so all scheduled wakes are crons in `crons/jobs.json`.) ## The non-negotiable diff --git a/squads/meta-ads-squad/crons/jobs.json b/squads/meta-ads-squad/crons/jobs.json index 601175d..e76b42d 100644 --- a/squads/meta-ads-squad/crons/jobs.json +++ b/squads/meta-ads-squad/crons/jobs.json @@ -9,7 +9,7 @@ "sessionTarget": "meta-ads-agent", "payload": { "kind": "systemEvent", - "text": "Daily operations sweep. **Load `pancake-meta-ads-01-account-foundations`, `pancake-meta-ads-10-root-cause-analysis`, and `pancake-meta-ads-13-operational-routines`** (the always-on set), then follow Section 4 of HEARTBEAT.md end to end. Pull the last 24h of account performance via the Meta MCP, run the eight-branch root-cause framework over every flagged entity, classify each finding against the autonomy model in SOUL.md, **execute every autonomous-allowed action immediately** (capturing before/after state and appending to `wiki/Knowledge/MetaAds/AuditLog/YYYY-MM-DD.md`), and **queue every budget-commitment action** in `MEMORY.md → Approval queue` with a unique id and expected impact. Load the per-branch methodology skill (03, 04, 05, 06, 07, 08, 09, 11) when its branch fires. If `MEMORY.md → Mode` is `recommendation-only`, queue all actions — autonomous-allowed ones too — and execute nothing. End the wake with the Section 9 digest in `memory/YYYY-MM-DD.md`. If no flags fired and the audit log is empty, write a single-line `all systems normal` entry to today's audit log so the 17:00 digest cron has something to read, then reply with the single literal token `NO_REPLY`." + "text": "Daily operations sweep. **Load the `meta-ads-wake-routine` skill** for the full procedure, plus **`pancake-meta-ads-01-account-foundations`, `pancake-meta-ads-10-root-cause-analysis`, and `pancake-meta-ads-13-operational-routines`** (the always-on set), then follow Section 4 of `meta-ads-wake-routine` end to end. Pull the last 24h of account performance via the Meta MCP, run the eight-branch root-cause framework over every flagged entity, classify each finding against the autonomy model in SOUL.md, **execute every autonomous-allowed action immediately** (capturing before/after state and appending to `wiki/Knowledge/MetaAds/AuditLog/YYYY-MM-DD.md`), and **queue every budget-commitment action** in `MEMORY.md → Approval queue` with a unique id and expected impact. Load the per-branch methodology skill (03, 04, 05, 06, 07, 08, 09, 11) when its branch fires. If `MEMORY.md → Mode` is `recommendation-only`, queue all actions — autonomous-allowed ones too — and execute nothing. End the wake with the Section 9 digest in `memory/YYYY-MM-DD.md`. If no flags fired and the audit log is empty, write a single-line `all systems normal` entry to today's audit log so the 17:00 digest cron has something to read, then reply with the single literal token `NO_REPLY`." }, "failureAlert": true, "state": {} @@ -22,7 +22,7 @@ "sessionTarget": "meta-ads-agent", "payload": { "kind": "systemEvent", - "text": "Daily digest. Follow Section 5 of HEARTBEAT.md exactly: read today's `wiki/Knowledge/MetaAds/AuditLog/YYYY-MM-DD.md` and the current `MEMORY.md → Approval queue`, pull yesterday's headline metrics (blended CPA, blended ROAS, spend, conversions) plus the 7-day trend, compose the digest in the documented structure (Yesterday's headline / What worked / What didn't / Autonomous actions taken / Awaiting your approval / Today's single focus / Status), file the full copy to `wiki/Knowledge/MetaAds/Digests/YYYY-MM-DD.md`, and `complete_task` with the digest body so the co-founder can relay it to the user on the pod's preferred channel. If yesterday produced zero spend, zero actions, zero pending approvals, and metrics are flat, reply with the single literal token `NO_REPLY` and log the reason in `memory/YYYY-MM-DD.md` — never post a no-news digest." + "text": "Daily digest. **Load the `meta-ads-wake-routine` skill** and follow its Section 5 exactly: read today's `wiki/Knowledge/MetaAds/AuditLog/YYYY-MM-DD.md` and the current `MEMORY.md → Approval queue`, pull yesterday's headline metrics (blended CPA, blended ROAS, spend, conversions) plus the 7-day trend, compose the digest in the documented structure (Yesterday's headline / What worked / What didn't / Autonomous actions taken / Awaiting your approval / Today's single focus / Status), file the full copy to `wiki/Knowledge/MetaAds/Digests/YYYY-MM-DD.md`, and `complete_task` with the digest body so the co-founder can relay it to the user on the pod's preferred channel. If yesterday produced zero spend, zero actions, zero pending approvals, and metrics are flat, reply with the single literal token `NO_REPLY` and log the reason in `memory/YYYY-MM-DD.md` — never post a no-news digest." }, "failureAlert": false, "state": {} @@ -35,7 +35,7 @@ "sessionTarget": "meta-ads-agent", "payload": { "kind": "systemEvent", - "text": "Weekly review. **Load `pancake-meta-ads-12-review-cadence`** and follow Section 6 of HEARTBEAT.md. Always run the weekly cadence: 7-day pipeline (spend pacing, KPI trend, creative refresh signals, audit log throughput), weekly creative analysis (which ads gained share, which fatigued — file creative briefs for any unit due for refresh), and an automated-rules sweep (`pancake-meta-ads-11-guardian-rules`). **If today is the first Monday of the month**, fold in the monthly audits: audiences (`05` + `13`), structure (`02`), measurement (`08`), budget allocation (`03`). **If today is the first Monday of the quarter**, add the quarterly audits on top: compliance (`09`) and account maturity reassessment (`01`) — if maturity has shifted, update `team.account_maturity_level` via `vault_request` and note in `MEMORY.md`. File the appropriate report to `wiki/Knowledge/MetaAds/Reports//YYYY-MM-DD.md`. `complete_task` with a one-paragraph summary for the co-founder to relay." + "text": "Weekly review. **Load the `meta-ads-wake-routine` skill** and **`pancake-meta-ads-12-review-cadence`**, then follow Section 6 of `meta-ads-wake-routine`. Always run the weekly cadence: 7-day pipeline (spend pacing, KPI trend, creative refresh signals, audit log throughput), weekly creative analysis (which ads gained share, which fatigued — file creative briefs for any unit due for refresh), and an automated-rules sweep (`pancake-meta-ads-11-guardian-rules`). **If today is the first Monday of the month**, fold in the monthly audits: audiences (`05` + `13`), structure (`02`), measurement (`08`), budget allocation (`03`). **If today is the first Monday of the quarter**, add the quarterly audits on top: compliance (`09`) and account maturity reassessment (`01`) — if maturity has shifted, update `team.account_maturity_level` via `vault_request` and note in `MEMORY.md`. File the appropriate report to `wiki/Knowledge/MetaAds/Reports//YYYY-MM-DD.md`. `complete_task` with a one-paragraph summary for the co-founder to relay." }, "failureAlert": true, "state": {} diff --git a/squads/meta-ads-squad/manifest.json b/squads/meta-ads-squad/manifest.json index afbf25e..3de265e 100644 --- a/squads/meta-ads-squad/manifest.json +++ b/squads/meta-ads-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "meta-ads-squad", - "version": "0.1.0", + "version": "0.2.0", "description": "Single-agent Meta Ads squad: daily diagnostic + action sweep, daily digest, weekly review. Holds spend flat autonomously; escalates only budget increases.", "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/posthog-squad/ONBOARD.md b/squads/posthog-squad/ONBOARD.md index 9881234..79317b9 100644 --- a/squads/posthog-squad/ONBOARD.md +++ b/squads/posthog-squad/ONBOARD.md @@ -119,7 +119,7 @@ If they want the default, skip — leave both crons as LA. Otherwise edit `crons ### Step 8 — Dispatch the baseline scan to PostHog-agent (do NOT run it yourself) -**Important — read this before you act.** This step is the most common place this onboarding gets the agent architecture wrong. The baseline scan is **PostHog-agent's** work, not yours. You are the cofounder; you do not impersonate squad agents. Spawn a session targeted at `posthog-agent` and let it execute against its own IDENTITY/SOUL/MEMORY/HEARTBEAT. If you run the queries as a `main` subagent, the agent's loaded skills (`posthog-discovery`, `posthog-daily-analysis`, `posthog-mcp-toolkit`) are never in context, the per-agent MEMORY isn't read, and the report ends up missing the schema-probe-aware substitutions and the HogQL-in-report rule. (This has happened in the wild — fixed in v0.1.1.) +**Important — read this before you act.** This step is the most common place this onboarding gets the agent architecture wrong. The baseline scan is **PostHog-agent's** work, not yours. You are the cofounder; you do not impersonate squad agents. Spawn a session targeted at `posthog-agent` and let it execute against its own IDENTITY/SOUL/MEMORY and its cron-driven wake procedure. If you run the queries as a `main` subagent, the agent's loaded skills (`posthog-discovery`, `posthog-daily-analysis`, `posthog-mcp-toolkit`) are never in context, the per-agent MEMORY isn't read, and the report ends up missing the schema-probe-aware substitutions and the HogQL-in-report rule. (This has happened in the wild — fixed in v0.1.1.) The task brief: a **baseline analytics scan** — current DAU/WAU/MAU, north-star event volume for the last 30 days with WoW deltas, activation rate of the last 4 weeks of signups, top 10 most engaged users this week, and a first-pass list of users likely to churn (previously-active accounts with a sharp recent drop in north-star event count). Output: a single `wiki/Knowledge/PostHog/Reports/baseline/YYYY-MM-DD.md` (including the verbatim HogQL used) plus a 6–8 line summary surfaced to the co-founder. Pre-condition: §5.5 schema probe must be complete (see `MEMORY → PostHog shape`). diff --git a/squads/posthog-squad/SQUAD.md b/squads/posthog-squad/SQUAD.md index 1c12e55..7c4214d 100644 --- a/squads/posthog-squad/SQUAD.md +++ b/squads/posthog-squad/SQUAD.md @@ -26,6 +26,6 @@ Plus three opt-in add-ons you can enable later by asking the agent: **release-im ## How it works -PostHog-agent runs on a **daily analysis cron** (09:00 in the timezone agreed at onboarding) and a **weekly recap cron** (Monday 10:00). A **2h heartbeat pulse** between crons handles dispatched questions and mid-day anomaly checks. All analysis is filed to the wiki under `wiki/Knowledge/PostHog/`; you only see the short digest. **Read-only against PostHog by default** — the cohort-write add-on is the single carve-out, opt-in, and uses a separate narrowly-scoped key. +PostHog-agent runs on a **daily analysis cron** (09:00 in the timezone agreed at onboarding) and a **weekly recap cron** (Monday 10:00). A **2h heartbeat-pulse cron** between them handles dispatched questions and mid-day anomaly checks. All analysis is filed to the wiki under `wiki/Knowledge/PostHog/`; you only see the short digest. **Read-only against PostHog by default** — the cohort-write add-on is the single carve-out, opt-in, and uses a separate narrowly-scoped key. > **Why a single agent.** Product analytics is a coordination problem, not a parallelism problem. One agent that owns the event taxonomy, the dashboard outputs, and the founder's mental model is more useful than a swarm that each touches a slice. diff --git a/squads/posthog-squad/agents/posthog-agent/HEARTBEAT.md b/squads/posthog-squad/agents/posthog-agent/HEARTBEAT.md deleted file mode 100644 index 5e3f47f..0000000 --- a/squads/posthog-squad/agents/posthog-agent/HEARTBEAT.md +++ /dev/null @@ -1,67 +0,0 @@ -# Heartbeat - -Every time you wake (cron, 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, 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. -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. -- **Cron fired?** The cron payload tells you which skill to load. Job - `daily-posthog-analysis` → load the `posthog-daily-analysis` skill and - run its Daily section. Job `weekly-posthog-recap` → load the same - `posthog-daily-analysis` skill and run its Weekly recap section. (Job - IDs and skill names are distinct — don't try to load a skill named - after the job.) -- **Heartbeat pulse with no task?** Pick the highest-leverage action in - your lane: advance a draft, refresh the dying-users watchlist with - intra-day data, scout a candidate north-star event, run the lightweight - anomaly check (procedure in `posthog-daily-analysis`), poll for new - releases (procedure in `posthog-release-tracker`). Don't bail at orient. -- **Genuinely nothing actionable?** Log the reason in - `memory/YYYY-MM-DD.md`, reply with the single literal token `NO_REPLY`, - end the turn. - -## 3. 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. - -## 4. 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 ran** — which queries or which skill, which window. -- **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 (the daily-analysis skill -handles its own digest surfacing). A wake without a digest is an -unfinished wake. - -## 5. Close the loop - -- On task completion: `complete_task` with the outcome. -- On blocker (MCP down, auth fail, ingestion broken): `fail_task` with the - exact error, log it, surface it to the co-founder. Data integrity issues - are higher-priority than missing a digest. diff --git a/squads/posthog-squad/agents/posthog-agent/MEMORY.md b/squads/posthog-squad/agents/posthog-agent/MEMORY.md index cc1433e..6753c02 100644 --- a/squads/posthog-squad/agents/posthog-agent/MEMORY.md +++ b/squads/posthog-squad/agents/posthog-agent/MEMORY.md @@ -15,7 +15,7 @@ ## Squad → posthog-squad → My skills: posthog-discovery, posthog-daily-analysis, posthog-mcp-toolkit, posthog-funnel-debugger, posthog-release-tracker, posthog-cohort-sync -→ Wake procedure: HEARTBEAT.md (loaded on every wake) +→ Wake procedure: driven by `crons/jobs.json` (daily-posthog-analysis + weekly-posthog-recap + heartbeat-pulse) — each cron payload carries the procedure for that wake. ## Company context → Product: wiki/Company/COMPANY.md diff --git a/squads/posthog-squad/agents/posthog-agent/agent.json b/squads/posthog-squad/agents/posthog-agent/agent.json index ea52138..9547089 100644 --- a/squads/posthog-squad/agents/posthog-agent/agent.json +++ b/squads/posthog-squad/agents/posthog-agent/agent.json @@ -2,7 +2,6 @@ "id": "posthog-agent", "description": "Product analytics agent — owns the PostHog MCP, the north-star event taxonomy, and the daily DAU/WAU/MAU + engaged-users + dying-users digest.", "model": "sonnet", - "heartbeat": { "every": "2h" }, "skills": [ "agents/posthog-agent/skills/posthog-discovery.md", "agents/posthog-agent/skills/posthog-daily-analysis.md", diff --git a/squads/posthog-squad/agents/posthog-agent/skills/posthog-release-tracker.md b/squads/posthog-squad/agents/posthog-agent/skills/posthog-release-tracker.md index 6cd1ea7..1dc2145 100644 --- a/squads/posthog-squad/agents/posthog-agent/skills/posthog-release-tracker.md +++ b/squads/posthog-squad/agents/posthog-agent/skills/posthog-release-tracker.md @@ -1,6 +1,6 @@ --- name: posthog-release-tracker -description: PostHog-agent's procedure for tying product releases to metric movement. Polls a configured GitHub repo on every 2h heartbeat pulse, snapshots DAU / WAU / north-star metrics at T+0, T+24h, T+7d after each new release, and files a release-impact report. Load on every heartbeat pulse. +description: PostHog-agent's procedure for tying product releases to metric movement. Polls a configured GitHub repo on every 2h heartbeat-pulse cron run, snapshots DAU / WAU / north-star metrics at T+0, T+24h, T+7d after each new release, and files a release-impact report. Load on every heartbeat-pulse cron run. --- # PostHog release tracker — PostHog-agent @@ -58,7 +58,7 @@ Process every pending snapshot whose `due_at <= now()`. For each: 3. Append to the per-release report at `wiki/Knowledge/PostHog/Releases/YYYY-MM-DD-{tag}.md`. 4. Remove the entry from `MEMORY → Pending release snapshots`. -If a pending snapshot is overdue by more than 50% of its window (e.g. a T+24h snapshot that didn't run until T+36h because the heartbeat was busy), tag the report with `late: true` and note the actual elapsed time — late snapshots are still useful, just less precise. +If a pending snapshot is overdue by more than 50% of its window (e.g. a T+24h snapshot that didn't run until T+36h because the pulse was busy), tag the report with `late: true` and note the actual elapsed time — late snapshots are still useful, just less precise. ## 4 — File the per-release report diff --git a/squads/posthog-squad/crons/jobs.json b/squads/posthog-squad/crons/jobs.json index 6a1fd53..d5758dd 100644 --- a/squads/posthog-squad/crons/jobs.json +++ b/squads/posthog-squad/crons/jobs.json @@ -26,6 +26,19 @@ }, "failureAlert": true, "state": {} + }, + { + "id": "heartbeat-pulse", + "name": "2h heartbeat pulse — PostHog-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": "posthog-agent", + "payload": { + "kind": "systemEvent", + "text": "2h heartbeat pulse — self-driven check-in between the daily-posthog-analysis (09:00) and weekly-posthog-recap (Mon 10:00) crons. Run this procedure in order:\n\n(1) **Orient** — read `MEMORY.md` (your settings, vault keys, where you file), skim the most recent `memory/YYYY-MM-DD.md` entries (what's in flight, what's blocked, what you promised the co-founder), then `list_tasks`.\n\n(2) **Decide what this wake is for** — a dispatched task waiting? Handle it first. That's why you were woken. Otherwise this is a pulse, run the pulse work below.\n\n(3) **Pulse work — pick the highest-leverage action in your lane and do it:** advance a draft; refresh the dying-users watchlist with intra-day data; scout a candidate north-star event; run the lightweight anomaly check (procedure in the `posthog-daily-analysis` skill); poll for new releases and snapshot release-impact metrics (procedure in the `posthog-release-tracker` skill — load it on the pulse). Don't bail at orient.\n\n(4) **Execute** — actually do the work picked above. Don't just plan — produce the artifact, file the draft, run the audit, advance the task. Output > deliberation. All analysis is read-only against PostHog by default; file outputs under `wiki/Knowledge/PostHog/`.\n\n(5) **Digest** — write a one-paragraph digest to `memory/YYYY-MM-DD.md`: what you ran (which queries/skill, which window), what changed, what's still open, the single first move for the next wake. Surface to the co-founder only on material news.\n\n(6) **Close the loop** — on task completion, `complete_task` with the outcome; on blocker (MCP down, auth fail, ingestion broken), `fail_task` with the exact error, log it, surface it — data-integrity issues outrank a missed digest.\n\n**Non-negotiable:** at least one task must be EXECUTED before you close the session. `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, and no open output is awaiting your hand — log *why* in `memory/YYYY-MM-DD.md` first." + }, + "failureAlert": false, + "state": {} } ] } diff --git a/squads/posthog-squad/manifest.json b/squads/posthog-squad/manifest.json index a2d2f22..0cc805a 100644 --- a/squads/posthog-squad/manifest.json +++ b/squads/posthog-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "posthog-squad", - "version": "0.2.4", + "version": "0.3.0", "description": "Single-agent PostHog squad: daily digest (DAU/WAU/MAU, north-star, engaged + dying), activation-funnel debugger, mid-day anomaly alerts, weekly recap. Opt-in add-ons: release tracker + auto cohorts.", "author": "pancake-official", "license": "MIT", diff --git a/squads/reddit-squad/SQUAD.md b/squads/reddit-squad/SQUAD.md index 9840c26..3315d7d 100644 --- a/squads/reddit-squad/SQUAD.md +++ b/squads/reddit-squad/SQUAD.md @@ -27,6 +27,6 @@ Deploys Reddit-agent — a focused agent that builds your product's organic pres ## How it works -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). 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). 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. > **Why one account.** A single purchased account is safer, simpler, and avoids the Terms-of-Service issues that come with coordinated multi-account posting. If the account is banned, you've lost a few dollars — not your reputation. 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 eedac8c..0000000 --- a/squads/reddit-squad/agents/reddit-agent/HEARTBEAT.md +++ /dev/null @@ -1,140 +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-account`). -- **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), 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, a warm-up action if the account is still in its - warm-up window, or a mission-deepening move (new subreddit candidate, - keyword shortlist refresh). -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 — -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` — account status, target subreddits, keywords, warm-up - state, 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. - If the account is still in its warm-up window, do warm-up actions from - the `reddit-account` skill instead of promotional drafting. -- **Weekly health-check cron fired (Monday 10:00 PT)?** Load - `reddit-account` 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. **Check warm-up state.** If the account is still inside its warm-up window - (see `reddit-account` skill), do a warm-up action instead of promotional - drafting: browse + upvote in the target subreddits, optionally draft one - small, non-promotional comment on a question thread. -2. **Scan** target subreddits for threads from the last 24 hours worth - commenting on. Run the keyword + competitor monitor. -3. **Draft** comments for qualifying threads. Apply the Quality Checklist - from the `reddit-playbook` skill. Comments must be grounded in real - product knowledge — never generic. -4. **Surface the top 3** to the co-founder via `complete_task`. Max 3 drafts - per day, no exceptions — quality over volume. -5. **Account health** — if it has been ≥ 7 days since the last health check, - run one and log it to `wiki/Knowledge/Reddit/AccountHealth.md`. -6. **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 warm-up step 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** — warm-up day count, 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 account): `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 8559d68..8e01773 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-account -→ 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 6289bac..f43ad9e 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 and drafts comments on a single purchased account via browser automation on old.reddit.com.", "model": "sonnet", - "heartbeat": { "every": "2h" }, "skills": [ "agents/reddit-agent/skills/reddit-playbook.md", "agents/reddit-agent/skills/reddit-account.md" diff --git a/squads/reddit-squad/crons/jobs.json b/squads/reddit-squad/crons/jobs.json index 16fa749..94bb095 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). All Reddit work happens via browser automation on old.reddit.com. **Load the `reddit-playbook` skill** (always); **load `reddit-account`** before any warm-up or account-health work. Run this procedure in order:\n\n(1) **Orient** — read `MEMORY.md` (account status, warm-up state, 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 warm-up step 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 (warm-up day count, 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 account), `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, a warm-up action if the account is still in its warm-up window, OR a mission-deepening move (new subreddit candidate, keyword shortlist refresh). (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 the 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 d1fb2ce..79e9694 100644 --- a/squads/reddit-squad/manifest.json +++ b/squads/reddit-squad/manifest.json @@ -1,6 +1,6 @@ { "name": "reddit-squad", - "version": "2.0.0", + "version": "2.1.0", "description": "Single-agent Reddit squad: Reddit-agent monitors subreddits, drafts comments via browser automation on old.reddit.com, warms up a single purchased account, 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": {} } ] }