diff --git a/packages/engine/src/__tests__/__snapshots__/agent-heartbeat-procedures.test.ts.snap b/packages/engine/src/__tests__/__snapshots__/agent-heartbeat-procedures.test.ts.snap new file mode 100644 index 000000000..0cf2acf9e --- /dev/null +++ b/packages/engine/src/__tests__/__snapshots__/agent-heartbeat-procedures.test.ts.snap @@ -0,0 +1,181 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`agent-heartbeat procedure templates > keeps lite no-task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Ambient review** — since you have no assigned task, review board/project + signals and recent memory context before acting. +5. **Classify scope before acting** — label the next action as either: + - **Board-scope execution:** work that can be completed now with ambient + tools (coordination, delegation, messaging, memory updates). + - **Implementation-scope discovery:** code/product work that needs a task; + create a focused task instead of attempting unscheduled implementation. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + create a focused task, delegate work, send/reply to a message, or append + durable memory. +7. **Persist progress** — use available ambient tools only: + fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a created task, delegation, +message reply, memory append, or explicit "no-op with reason") is a bug. Do +not loop on the same plan across heartbeats without recording why." +`; + +exports[`agent-heartbeat procedure templates > keeps lite task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Assignment review** — if you have an assigned task, re-read its current + description, latest comments, and any task documents. Decide whether the + prior plan is still valid given the wake delta. Do not assume yesterday's + plan is still correct. +5. **Classify scope before acting** — label the next action as either: + - **In-scope execution:** directly advances the assigned task's current + acceptance criteria. + - **Out-of-scope discovery:** useful but separate work; capture it as a + focused follow-up task instead of expanding the current task silently. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + advance the task, create a follow-up, log findings, delegate, or update + memory. Don't stop at planning unless the task is a planning task. +7. **Persist progress** — fn_task_log for observations, fn_task_document_write + for durable findings, status updates only when the work warrants it. +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a log, a document write, a +status change, a comment, a delegation, or an explicit "no-op with reason") is +a bug. Do not loop on the same plan across heartbeats without recording why." +`; + +exports[`agent-heartbeat procedure templates > keeps off no-task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. +5. **Persist progress** — use available ambient tools only. +6. **Exit** — call fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug." +`; + +exports[`agent-heartbeat procedure templates > keeps off task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. +5. **Persist progress** — record the action via available task/memory tools. +6. **Exit** — call fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug." +`; + +exports[`agent-heartbeat procedure templates > keeps strict no-task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. If Pending Room Messages are present, + review them in the prompt and use fn_post_room_message only when relevant. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Ambient review** — since you have no assigned task, review board/project + signals and recent memory context before acting. No-task heartbeat runs are + inherently coordination-class because no bound task exists to classify. +5. **Classify scope before acting** — label the next action as either: + - **Board-scope execution:** work that can be completed now with ambient + tools (coordination, delegation, messaging, memory updates). + - **Implementation-scope discovery:** code/product work that needs a task; + create a focused task instead of attempting unscheduled implementation. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + create a focused task, delegate work, send/reply to a message, or append + durable memory. +7. **Persist progress** — use available ambient tools only: + fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a created task, delegation, +message reply, memory append, or explicit "no-op with reason") is a bug. Do +not loop on the same plan across heartbeats without recording why." +`; + +exports[`agent-heartbeat procedure templates > keeps strict task procedure stable 1`] = ` +"## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. If Pending Room Messages are present, + review them in the prompt and use fn_post_room_message only when relevant. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Classify the bound task** — if you have an assigned task, classify it as + exactly one of: + - **executor-class** — implementation work: writing code, tests, + documentation prose, or running build/lint/typecheck. + - **blocked** — task has blockedBy set, or is waiting on a peer / dependency + / external input. + - **coordination-class** — planning, triage, routing, decision-making, or + review. + Then branch: + - If the bound task is **executor-class** or **blocked**, skim it once for + blocker risk, do not re-read PROMPT.md to advance it, and pivot this + heartbeat to broader board signals (in-progress risk scan, stale in-review + queue, idle direct reports, and strategic themes in memory). Inbox is + already handled in step 2. + - If the bound task is **coordination-class**, engage directly with the + bound task. +5. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + advance the task, create a follow-up, log findings, delegate, or update + memory. Don't stop at planning unless the task is a planning task. +6. **Persist progress** — fn_task_log for observations, fn_task_document_write + for durable findings, status updates only when the work warrants it. +7. **Per-tick self-check** — before exiting, verify all three: + - Was the inbox processed? + - Is the chosen action on a coordination-shaped lever? + - If the bound task was executor-class, did I avoid re-planning it? +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a log, a document write, a +status change, a comment, a delegation, or an explicit "no-op with reason") is +a bug. Do not loop on the same plan across heartbeats without recording why." +`; diff --git a/packages/engine/src/__tests__/agent-heartbeat-procedures.test.ts b/packages/engine/src/__tests__/agent-heartbeat-procedures.test.ts new file mode 100644 index 000000000..e3f64b87a --- /dev/null +++ b/packages/engine/src/__tests__/agent-heartbeat-procedures.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { + HEARTBEAT_NO_TASK_PROCEDURE_LITE, + HEARTBEAT_NO_TASK_PROCEDURE_OFF, + HEARTBEAT_NO_TASK_PROCEDURE_STRICT, + HEARTBEAT_PROCEDURE_LITE, + HEARTBEAT_PROCEDURE_OFF, + HEARTBEAT_PROCEDURE_STRICT, +} from "../agent-heartbeat.js"; + +describe("agent-heartbeat procedure templates", () => { + it("keeps strict task procedure stable", () => { + expect(HEARTBEAT_PROCEDURE_STRICT).toMatchSnapshot(); + }); + + it("keeps lite task procedure stable", () => { + expect(HEARTBEAT_PROCEDURE_LITE).toMatchSnapshot(); + }); + + it("keeps off task procedure stable", () => { + expect(HEARTBEAT_PROCEDURE_OFF).toMatchSnapshot(); + }); + + it("keeps strict no-task procedure stable", () => { + expect(HEARTBEAT_NO_TASK_PROCEDURE_STRICT).toMatchSnapshot(); + }); + + it("keeps lite no-task procedure stable", () => { + expect(HEARTBEAT_NO_TASK_PROCEDURE_LITE).toMatchSnapshot(); + }); + + it("keeps off no-task procedure stable", () => { + expect(HEARTBEAT_NO_TASK_PROCEDURE_OFF).toMatchSnapshot(); + }); +}); diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index a8e9cc264..447953cde 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -478,7 +478,7 @@ export const HEARTBEAT_SYSTEM_PROMPT_NO_TASK = HEARTBEAT_NO_TASK_SYSTEM_PROMPT; * agent to re-anchor on its own operating procedure each wake instead of * silently grinding on a previously assigned task. */ -export const HEARTBEAT_PROCEDURE = `## Heartbeat Procedure (run every tick, in order) +export const HEARTBEAT_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) 1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. Confirm your role, soul, instructions, and memory match what @@ -524,11 +524,59 @@ Critical: a heartbeat without observable progress (a log, a document write, a status change, a comment, a delegation, or an explicit "no-op with reason") is a bug. Do not loop on the same plan across heartbeats without recording why.`; +export const HEARTBEAT_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Assignment review** — if you have an assigned task, re-read its current + description, latest comments, and any task documents. Decide whether the + prior plan is still valid given the wake delta. Do not assume yesterday's + plan is still correct. +5. **Classify scope before acting** — label the next action as either: + - **In-scope execution:** directly advances the assigned task's current + acceptance criteria. + - **Out-of-scope discovery:** useful but separate work; capture it as a + focused follow-up task instead of expanding the current task silently. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + advance the task, create a follow-up, log findings, delegate, or update + memory. Don't stop at planning unless the task is a planning task. +7. **Persist progress** — fn_task_log for observations, fn_task_document_write + for durable findings, status updates only when the work warrants it. +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a log, a document write, a +status change, a comment, a delegation, or an explicit "no-op with reason") is +a bug. Do not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. +5. **Persist progress** — record the action via available task/memory tools. +6. **Exit** — call fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; + +// Backward-compatible alias; prefer HEARTBEAT_PROCEDURE_STRICT. +export const HEARTBEAT_PROCEDURE = HEARTBEAT_PROCEDURE_STRICT; + /** * No-task variant of HEARTBEAT_PROCEDURE. Keep this aligned with the ambient * tool set (no fn_task_log / fn_task_document_* in no-task runs). */ -export const HEARTBEAT_NO_TASK_PROCEDURE = `## Heartbeat Procedure (run every tick, in order) +export const HEARTBEAT_NO_TASK_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) 1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. Confirm your role, soul, instructions, and memory match what @@ -562,6 +610,52 @@ Critical: a heartbeat without observable progress (a created task, delegation, message reply, memory append, or explicit "no-op with reason") is a bug. Do not loop on the same plan across heartbeats without recording why.`; +export const HEARTBEAT_NO_TASK_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. +4. **Ambient review** — since you have no assigned task, review board/project + signals and recent memory context before acting. +5. **Classify scope before acting** — label the next action as either: + - **Board-scope execution:** work that can be completed now with ambient + tools (coordination, delegation, messaging, memory updates). + - **Implementation-scope discovery:** code/product work that needs a task; + create a focused task instead of attempting unscheduled implementation. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + create a focused task, delegate work, send/reply to a message, or append + durable memory. +7. **Persist progress** — use available ambient tools only: + fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. +8. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a created task, delegation, +message reply, memory append, or explicit "no-op with reason") is a bug. Do +not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_NO_TASK_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. +5. **Persist progress** — use available ambient tools only. +6. **Exit** — call fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; + +// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_PROCEDURE_STRICT. +export const HEARTBEAT_NO_TASK_PROCEDURE = HEARTBEAT_NO_TASK_PROCEDURE_STRICT; + /** Parameter schema for the fn_heartbeat_done tool */ const heartbeatDoneParams = Type.Object({ summary: Type.Optional(Type.String({ description: "Summary of what was accomplished this heartbeat" })),