feat: harden permanent-agent heartbeat instructions (#2081)

## Summary

Hardens permanent-agent operating law while keeping the
heartbeat/executor split:

- **Critical Rules** in task-scoped and no-task heartbeat system prompts
(survive custom `HEARTBEAT.md`)
- Stronger default procedures: disposition checklist, scoped-wake,
blocked dedup, progress note style
- **Wake Delta multi-assign inventory** (ranked, cap 8,
coordination-only framing) + `checkout_conflict` regression test
- Standing instructions six-section template for blank custom create /
empty detail insert
- Onboarding interview guidance to prefer structured `instructionsText`
- Playbooks, CONCEPTS, agents.md accuracy; remove stale agent
gap-analysis doc

Plan:
`docs/plans/2026-07-12-001-feat-permanent-agent-heartbeat-instructions-plan.md`

## Test plan

- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/assigned-task-ranking.test.ts`
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/agent-heartbeat-procedures.test.ts
src/__tests__/heartbeat-executor.test.ts -u`
- [x] `pnpm --filter @fusion/dashboard exec vitest run
app/components/__tests__/standing-instructions-template.test.ts`
- [ ] CI gate green on PR

## Residual Review Findings

None recorded at open (inline review; no residual sink).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added ranked multi-assignment context to agent heartbeat wake-ups,
including task status, ownership, and lease details.
* Added standing-instructions templates for creating and editing
permanent agents.
* Improved onboarding guidance with a consistent six-section instruction
structure.
* Added clearer heartbeat handling for blocked tasks, no-task runs, and
checkout conflicts.

* **Documentation**
* Added permanent-agent heartbeat playbooks and expanded coordination
glossary entries.
  * Updated documentation indexes and heartbeat behavior guidance.

* **Tests**
* Added coverage for task ranking, instruction templates, wake-up
context, and conflict handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-14 08:23:11 -07:00
committed by GitHub
parent 30a83f21fc
commit dff864e098
18 changed files with 1330 additions and 317 deletions

View File

@@ -242,6 +242,24 @@ Requires both the workflow-columns and graph-executor flags; with either off, bi
### Effective agent (execution principal)
The agent identity that actually runs a piece of work after column-agent precedence resolves — and the principal every identity-keyed subsystem must consult: permission gating, heartbeat serialization in both directions, resume re-dispatch, and mid-flight change detection. It may differ from the task's assigned agent under an override binding, and one task may have multiple effective agents across concurrent branch sessions.
### Permanent / durable agent
A non-ephemeral agent row operators create and keep (CEO, specialists, column agents). Permanent agents wake on heartbeats, hold standing instructions (`instructionsText` / `instructionsPath` / `soul`), and coordinate work. Ephemeral task workers are created for a lane and deleted when the session ends.
### Heartbeat run
A short permanent-agent wake: timer, assignment, message, or on-demand. The run loads identity + Wake Delta + procedure, takes **one** coordination action, and exits via `fn_heartbeat_done`. Task-body coding runs on the **executor** path, not the default heartbeat.
### Heartbeat procedure
Per-tick ordered checklist text (built-in strict/lite/off templates or a per-agent `HEARTBEAT.md` at `heartbeatProcedurePath`). Task-scoped custom files replace the built-in procedure; no-task runs always use the ambient built-in so task-only tools are never promised. System-prompt **Critical Rules** still apply even when a custom procedure is loaded.
### Checkout lease
Exclusive execution ownership of a task (`checkedOutBy` + lease metadata / central claim row). Heartbeat validates the lease for a bound task and exits `checkout_conflict` without retry when another agent holds it. Claim/checkout are the binding primitives; agents must not spin on 409-style conflicts.
### Auto-claim
No-task heartbeat behavior that may claim an unowned ready `todo` matching role/policy (`autoClaimRelevantTasks`, engineer backlog opt-in). Distinct from multi-assign inventory (tasks already `assignedAgentId` to the agent). Auto-claim candidates and “your assigned tasks” must stay separate prompt sections.
### assignmentPolicy
Per-agent routing eligibility: `auto` (default pool + auto-claim), `explicit-only` (direct assign/delegate only), `none` (never bind implementation work — liaison/observer guarantee). Enforced on claim, checkout, assign, inbox selection, and delegation.
### Lane
A horizontal row on the multi-lane board, one per workflow in use by visible cards. Each lane renders its own workflow's columns. Tasks with no workflow selection appear in the Default workflow's lane; every card appears in exactly one lane. Zero-card lanes are hidden; lanes are collapsible with persisted state.

View File

@@ -105,9 +105,9 @@ FN-7088 links previously-unlinked first-class testing and baseline docs here so
| [UX Audit Report](./ux-audit-report.md) | Comprehensive UX audit with prioritized recommendations for dashboard improvements |
| [Codebase Improvement Audit](./codebase-improvement-audit.md) | Evidence-based technical debt and reliability gap audit with prioritized recommendations |
| [Gap Analysis](./gap-analysis.md) | System completeness analysis comparing Fusion to Paperclip feature set |
| [Permanent Agent Heartbeat Playbooks](./agents-playbooks.md) | Worked manager/IC/message/blocked/no-task heartbeat scenarios and anti-patterns |
| [Agent Sandbox Research](./agent-sandboxing-research.md) | Research on agent isolation, capability enforcement, and sandboxing approaches |
| [Even Realities Integration Research (FN-3737)](./even-realities-integration-research.md) | Research summary and recommended integration topology for Even Realities glasses + Fusion |
| [Agent Gap Analysis](./agent-paperclip-gap-analysis.md) | Gap analysis for agent Paperclip integration |
| [pi-autoresearch Analysis for Fusion Port](./research/pi-autoresearch-analysis.md) | Upstream architecture/license analysis and Fusion integration mapping for autoresearch capabilities |
| [pi-autoresearch Audit vs Fusion Research](./research/pi-autoresearch-audit-2026-05.md) | Audit comparing Fusion's research subsystem against upstream pi-autoresearch capabilities and parity gaps (FN-4136) |
| [Research Hardening Preflight Baseline](./research/research-hardening-preflight.md) | Verified research subsystem baseline, lifecycle contracts, and hardening pressure points |

View File

@@ -1,283 +0,0 @@
# Agent Paperclip-Style Gap Analysis
## Executive Summary
Fusion already has the core *shape* of a heartbeat-driven agent runtime: agents have lifecycle state, heartbeat runs are tracked, heartbeats can be triggered by timer/assignment/on-demand events, and agents can create follow-up tasks. This gives Fusion a strong baseline for the Paperclip execution model, especially around short-lived run execution (`HeartbeatMonitor.executeHeartbeat()`), agent persistence (`AgentStore`), and lightweight communication (`MessageStore`, task comments).
The largest gaps are ownership and coordination semantics. Paperclip’s model depends on explicit checkout leasing, inbox-driven work picking, blocked-task dedup, wake-context handling (including mention-triggered behavior), and budget-aware escalation rules. Fusion currently executes assigned work, but it does not enforce checkout-first locking or the comment/inbox contract that makes Paperclip runs predictable and conflict-safe.
Recommended priority: first add ownership/inbox primitives (checkout + inbox-lite + wake context), then add policy layers (blocked dedup, self-assignment constraints, communication conventions), and finally add advanced governance parity (budget controls, richer approval/doc workflows). Several adjacent foundations are already covered by existing Fusion tasks (assignment, hierarchy, permissions), so new work should build on those rather than duplicate them.
## Pattern Inventory
### Implemented Patterns
| Pattern | Fusion Implementation | Location in Code |
|---|---|---|
| Heartbeat run execution loop | Wake → task resolution → agent session → run completion | `packages/engine/src/agent-heartbeat.ts` |
| Heartbeat trigger mechanisms | Timer, assignment, and on-demand trigger scheduling | `packages/engine/src/agent-heartbeat.ts` (`HeartbeatTriggerScheduler`), `packages/dashboard/src/routes.ts` (`POST /agents/:id/runs`) |
| Attachment CRUD | Upload/list/download/delete task attachments | `packages/dashboard/src/routes.ts` (`/tasks/:id/attachments*`), `packages/core/src/store.ts` |
### Partial Implementations
| Pattern | What Exists | What's Missing | Packages Affected | Complexity |
|---|---|---|---|---|
| Heartbeat Procedure (9-step) | Simplified wake→check assignment→work→exit | No approval-follow-up, inbox prioritization, checkout, blocked dedup, delegation semantics parity | engine, core, dashboard | M |
| Wake context variables | `contextSnapshot`, `source`, `triggerDetail`, optional `taskId` | No `PAPERCLIP_WAKE_COMMENT_ID`, approval context vars, or env-style propagation | engine, dashboard, core | M |
| Communication model | Task comments + `MessageStore` mailboxes | No mention-routing semantics, no required ticket-link markdown rules | core, dashboard, engine | M |
| Delegation model | `spawn_agent` + `task_create` + dependencies | No `parentId`/`goalId` issue graph semantics, no workspace inheritance flags | core, engine, dashboard | M |
| Chain of command | `reportsTo` hierarchy and `/agents/:id/children` | No explicit `chainOfCommand` resolution/escalation policy in heartbeat flow | core, engine, dashboard | M |
| Routines | Automation schedules and manual runs exist | Not agent-inbox-native routines with webhook/api triggers + catch-up/concurrency policies | core, engine, dashboard | L |
| Planning mode docs | Planning sessions and summary-based task creation | No issue document key/revision API parity (`plan` doc revisions) | core, dashboard, engine | M |
| Approval workflow | `requirePlanApproval`, approve/reject-plan routes | No approval entity model with linked issues and resolution-first heartbeat handling | core, engine, dashboard | M |
| Status model parity | Fusion columns + task status fields | Missing direct parity with `backlog/in_progress/blocked/cancelled` lifecycle model | core, dashboard, engine | M |
| Error-handling policy | Run failure/termination handling and state transitions | No hard rule enforcement (checkout-first, mandatory end-comment, no cross-team cancel) | engine, core | M |
| Agent identity endpoint | `/agents/:id` and run stats exist | No `/agents/me` contract with company-scoped identity + budget fields | dashboard, core | S |
| Instructions-path API parity | `/agents/:id/instructions` supports path/text updates | No dedicated `/agents/:id/instructions-path` endpoint semantics | dashboard, core | S |
### Missing Patterns
| Pattern | Description | Packages Affected | Complexity | Dependencies |
|---|---|---|---|---|
| Checkout mechanism | Exclusive checkout API with 409 conflict semantics and no-retry policy | core, dashboard, engine | L | Task-agent assignment foundation (FN-1096/1098/1099) |
| Inbox-lite work selection | Compact assignment inbox with `in_progress` then `todo` priority and blocked handling | core, dashboard, engine | M | Checkout mechanism |
| Blocked-task dedup | Skip redundant blocked comments when no new context | core, engine | M | Inbox/comment cursor support |
| Budget management | Per-agent budget tracking, 80% throttling, 100% auto-pause | core, engine, dashboard | L | Agent metrics foundations (FN-1184 family) |
| Self-assignment mention handoff rules | Restrict self-assignment to explicit mention-triggered ownership transfer | engine, dashboard, core | M | Inbox + mention wake context |
| Issue search parity | Full-text search across title/identifier/description/comments | core, dashboard | M | Unified indexing strategy |
| Run audit trail header semantics | Uniform mutating-action run correlation equivalent to `X-Paperclip-Run-Id` policy | engine, dashboard, core | M | Checkout + identity context |
| Comment link policy enforcement | Auto-link ticket IDs and enforce company-prefixed internal URL style | dashboard, engine | S | Issue identifier conventions |
| Send-back-to-user handoff | Support assigning tasks back to user (`assigneeUserId`) with `in_review` handoff pattern | core, dashboard, engine | M | Expanded assignee model |
### Not Applicable
| Pattern | Reason it doesn't apply |
|---|---|
| Commit co-author rule (`Co-Authored-By: Paperclip`) | Paperclip-specific governance requirement; Fusion uses task-scoped commit conventions (`feat(FN-xxx): ...`) |
| OpenClaw invite workflow | Paperclip/OpenClaw org provisioning concern, not a Fusion board runtime concern |
| Paperclip company skills workflow APIs | Fusion has no company-level skill package registry model matching Paperclip |
| Paperclip project/workspace setup API shape | Fusion project registration exists but not via Paperclip issue-workflow endpoints |
| Full Paperclip control-plane API parity | Fusion is a different product surface; only overlapping agent-runtime patterns are relevant |
## Detailed Analysis
### 1) Heartbeat Procedure (9-step)
**Status:** PARTIAL
**Description:** Paperclip requires identity/approval/inbox/pick/checkout/context/work/status/delegate every wake.
**Current State:** Fusion executes a compact heartbeat run (`executeHeartbeat`) with task fetch and tool-enabled session.
**Gap:** Missing approval-first branch, inbox-lite prioritization, explicit checkout, and blocked dedup policy.
**Recommendation:** Extend `executeHeartbeat()` into explicit policy stages and persist per-stage telemetry in run metadata.
**Files Affected:** `packages/engine/src/agent-heartbeat.ts`, `packages/core/src/types.ts`
### 2) Checkout mechanism (409 no-retry)
**Status:** MISSING
**Description:** Must claim ownership before work; 409 means abandon and pick different work.
**Current State:** Fusion relies on scheduler movement/assignment; no explicit lease endpoint.
**Gap:** No first-class lock/lease primitive with conflict semantics.
**Recommendation:** Add checkout/release APIs and enforce checkout-before-work in heartbeat + executor flows.
**Files Affected:** `packages/core/src/agent-store.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/agent-heartbeat.ts`
### 3) Inbox-lite work selection
**Status:** MISSING
**Description:** Compact assignment view with priority ordering and blocked filtering.
**Current State:** Heartbeat resolves a single assigned task (`agent.taskId`).
**Gap:** No ranked inbox endpoint or work-selection strategy.
**Recommendation:** Introduce `/agents/:id/inbox-lite` equivalent and priority resolver in heartbeat scheduler.
**Files Affected:** `packages/core/src/agent-store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/agent-heartbeat.ts`, `packages/engine/src/scheduler.ts`
### 4) Blocked-task dedup
**Status:** MISSING
**Description:** Avoid repeated blocked updates unless new context appears.
**Current State:** No built-in dedup against last blocked comment + comment cursor.
**Gap:** Repetitive blocked churn is possible.
**Recommendation:** Store last-blocked-comment metadata per task/agent and gate repeated updates.
**Files Affected:** `packages/core/src/store.ts`, `packages/engine/src/agent-heartbeat.ts`
### 5) Wake context (`PAPERCLIP_*` parity)
**Status:** PARTIAL
**Description:** Runs carry trigger reason, task/comment/approval context.
**Current State:** `contextSnapshot` includes wake reason/trigger detail/taskId in run APIs.
**Gap:** No structured comment-trigger and approval-resolution context model.
**Recommendation:** Formalize wake context schema in `AgentHeartbeatRun.contextSnapshot` and propagate to prompts/tools.
**Files Affected:** `packages/core/src/types.ts`, `packages/engine/src/agent-heartbeat.ts`, `packages/dashboard/src/routes.ts`
### 6) Communication model (comments, mentions, link style)
**Status:** PARTIAL
**Description:** Paperclip uses issue comments, mention-driven wakes, and strict ticket-link markdown conventions.
**Current State:** Fusion has task comments and message inbox/outbox APIs.
**Gap:** No mention-to-wake semantics in task comments; no enforced link formatting conventions.
**Recommendation:** Add mention parser and optional markdown linting for task comment references.
**Files Affected:** `packages/core/src/message-store.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/agent-heartbeat.ts`
### 7) Delegation model (`parentId`, `goalId`, workspace inheritance)
**Status:** PARTIAL
**Description:** Delegation creates structured child/follow-up issues preserving goal/workspace lineage.
**Current State:** Fusion supports `spawn_agent` and `task_create` with dependencies.
**Gap:** No explicit parent/goal fields or non-child workspace inheritance marker.
**Recommendation:** Extend task schema with optional parent/goal/workspace-link fields and expose in creation APIs.
**Files Affected:** `packages/core/src/types.ts`, `packages/core/src/store.ts`, `packages/engine/src/agent-tools.ts`, `packages/dashboard/src/routes.ts`
### 8) Chain of command escalation
**Status:** PARTIAL
**Description:** Escalation follows explicit `chainOfCommand`.
**Current State:** `reportsTo` relationship exists with child lookup route.
**Gap:** No computed chain traversal/escalation helper in runtime policies.
**Recommendation:** Add `resolveChainOfCommand(agentId)` utility and escalation tool usage pattern in heartbeat instructions.
**Files Affected:** `packages/core/src/agent-store.ts`, `packages/engine/src/agent-heartbeat.ts`, `packages/dashboard/src/routes.ts`
### 9) Budget management
**Status:** MISSING
**Description:** Budget controls (80% focus narrowing, 100% auto-pause).
**Current State:** Token usage totals are tracked, but no budget thresholds/policies exist.
**Gap:** No budget envelope or policy enforcement.
**Recommendation:** Add budget config to agent schema and gate trigger scheduler/execution when budget thresholds are crossed.
**Files Affected:** `packages/core/src/types.ts`, `packages/core/src/agent-store.ts`, `packages/engine/src/agent-heartbeat.ts`, `packages/dashboard/src/routes.ts`
### 10) Routines
**Status:** PARTIAL
**Description:** Recurring task triggers integrated with agent inbox pickup and routine policies.
**Current State:** Fusion automations support cron-like scheduling and manual runs.
**Gap:** No routine-to-agent-assignment model with dedicated catch-up/concurrency policy vocabulary.
**Recommendation:** Either map routines onto automation primitives with explicit policy fields or add dedicated routine entities.
**Files Affected:** `packages/core` (automation domain), `packages/engine` (trigger integration), `packages/dashboard/src/routes.ts`
### 11) Planning mode document revisions
**Status:** PARTIAL
**Description:** Plan documents keyed by identifier with revision history.
**Current State:** Planning sessions and summaries exist; task prompt persists spec text.
**Gap:** No key-based issue document API with base-revision concurrency control.
**Recommendation:** Add per-task document store (`plan`, etc.) and revision endpoints.
**Files Affected:** `packages/core/src/store.ts`, `packages/core/src/types.ts`, `packages/dashboard/src/routes.ts`
### 12) Approval workflows
**Status:** PARTIAL
**Description:** Approval entities linked to issues, reviewed first on wake.
**Current State:** Planned tasks can require manual approval (`awaiting-approval` + approve/reject routes).
**Gap:** No standalone approval objects with linked issue resolution metadata.
**Recommendation:** Add optional approval domain only if needed beyond existing planning approval.
**Files Affected:** `packages/engine/src/planning.ts`, `packages/dashboard/src/routes.ts`, `packages/core/src/types.ts`
### 13) Self-assignment constraints
**Status:** MISSING
**Description:** Self-assignment allowed only for explicit mention handoff context.
**Current State:** Explicit task assignment exists (`PATCH /tasks/:id/assign`) without mention-gated policy.
**Gap:** Ownership can be changed without mention-driven context checks.
**Recommendation:** Add policy checks in assignment route/runtime for mention-handoff mode when agent-initiated.
**Files Affected:** `packages/dashboard/src/routes.ts`, `packages/core/src/store.ts`, `packages/engine/src/agent-heartbeat.ts`
### 14) Status value parity
**Status:** PARTIAL
**Description:** Paperclip issue statuses differ from Fusion’s board columns/status fields.
**Current State:** Fusion has columns (`planning`, `todo`, `in-progress`, `in-review`, `done`, `archived`) and ad-hoc task status strings.
**Gap:** No direct `blocked/cancelled/backlog` canonical lifecycle.
**Recommendation:** Decide whether to add a normalized execution status enum alongside board columns.
**Files Affected:** `packages/core/src/types.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/scheduler.ts`
### 15) Error-handling policy rules
**Status:** PARTIAL
**Description:** Paperclip enforces checkout-first, mandatory heartbeat comments, and cross-team cancellation restrictions.
**Current State:** Fusion tracks run failures and transitions states robustly.
**Gap:** Governance rules are not codified as hard guards.
**Recommendation:** Add policy middleware in heartbeat execution and task mutation routes.
**Files Affected:** `packages/engine/src/agent-heartbeat.ts`, `packages/dashboard/src/routes.ts`, `packages/core/src/store.ts`
### 16) Issue search across comments
**Status:** MISSING
**Description:** Full-text search over title/identifier/description/comments.
**Current State:** Task list endpoint supports pagination; no query search parameter.
**Gap:** No index-backed search route covering comments.
**Recommendation:** Add SQLite FTS index (or equivalent) for task/comment fields and `q=` API.
**Files Affected:** `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`
### 17) Attachments API
**Status:** IMPLEMENTED
**Description:** Upload/list/get/delete artifact attachments per work item.
**Current State:** Fusion supports attachment upload, retrieval, listing, and deletion for tasks.
**Gap:** Endpoint shape differs from Paperclip, but core capability exists.
**Recommendation:** Keep as-is unless external API parity is required.
**Files Affected:** `packages/dashboard/src/routes.ts`, `packages/core/src/store.ts`
### 18) Agent identity (`GET /me`, role, budget)
**Status:** PARTIAL
**Description:** Single identity endpoint returns effective role, company scope, budget, chain-of-command metadata.
**Current State:** Fusion provides `GET /agents/:id` and list endpoints; role/state metadata exists.
**Gap:** No canonical `/agents/me` and no company/budget identity payload.
**Recommendation:** Add authenticated `/agents/me` route and extend identity model if budget features are adopted.
**Files Affected:** `packages/dashboard/src/routes.ts`, `packages/core/src/types.ts`
### 19) Run audit trail mutation header parity
**Status:** MISSING
**Description:** Every mutating action should be tied to current run identity for auditability.
**Current State:** Run IDs are persisted for heartbeats, but mutation APIs do not require run-scoped correlation headers.
**Gap:** Hard to trace all side effects to one heartbeat transaction.
**Recommendation:** Add optional/required run-correlation metadata for agent-initiated mutations.
**Files Affected:** `packages/dashboard/src/routes.ts`, `packages/core/src/store.ts`, `packages/engine/src/agent-heartbeat.ts`
### 20) Commit co-author governance
**Status:** NOT APPLICABLE
**Description:** Paperclip requires a specific co-author line in git commits.
**Current State:** Fusion enforces task ID commit conventions.
**Gap:** Governance mismatch is intentional.
**Recommendation:** No action unless product policy changes.
**Files Affected:** None
### 21) Company-prefixed ticket-link conventions
**Status:** MISSING
**Description:** All ticket references should be markdown links with company prefix paths.
**Current State:** No automatic formatting/linting of task comment references.
**Gap:** Linking behavior is user-dependent and inconsistent.
**Recommendation:** Add optional comment formatter/linter for task/agent outputs.
**Files Affected:** `packages/dashboard/src/routes.ts`, `packages/engine/src/agent-heartbeat.ts`
### 22) OpenClaw invite workflow
**Status:** NOT APPLICABLE
**Description:** CEO-only OpenClaw invite prompt generation and approval.
**Current State:** Not part of Fusion’s domain model.
**Gap:** Out of product scope.
**Recommendation:** None.
**Files Affected:** None
### 23) Company skills workflow parity
**Status:** NOT APPLICABLE
**Description:** Company skill import/scan/sync APIs.
**Current State:** Fusion has local skill references/instructions, not Paperclip company skill registry APIs.
**Gap:** Different product boundaries.
**Recommendation:** None (unless Fusion adopts company-level skill governance).
**Files Affected:** None
### 24) Instructions-path dedicated endpoint parity
**Status:** PARTIAL
**Description:** Dedicated endpoint to update only instruction file path.
**Current State:** Fusion supports generic instruction updates on `/agents/:id/instructions` and full patch route.
**Gap:** No endpoint-level parity with adapter-key semantics.
**Recommendation:** Add alias route only if external parity/testing needs it.
**Files Affected:** `packages/dashboard/src/routes.ts`
### 25) Send-back-to-user handoff
**Status:** MISSING
**Description:** Explicit handoff from agent back to requesting user (`in_review`, assigneeUserId).
**Current State:** Task assignment supports `assignedAgentId` only.
**Gap:** No user assignee field or handoff policy.
**Recommendation:** Add user-assignment fields and route support for review handoff workflows.
**Files Affected:** `packages/core/src/types.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/agent-heartbeat.ts`
## Recommended Implementation Order
1. **Ownership foundation:** implement checkout/release semantics and conflict handling (409 no-retry rule).
2. **Work selection foundation:** add inbox-lite endpoint + deterministic prioritization + wake-context schema upgrades.
3. **Policy layer:** blocked-task dedup, self-assignment mention guardrails, and run-audit correlation metadata.
4. **Communication layer:** mention-triggered wake routing and ticket-link formatting helpers.
5. **Escalation and governance:** chain-of-command runtime helpers + budget thresholds/auto-pause behavior.
6. **Workflow parity upgrades:** planning document revisions + richer approval entities (if needed beyond current `awaiting-approval`).
7. **Search and UX parity:** full-text issue/task search and optional `/agents/me` identity endpoint.
8. **Deferred/optional parity:** instructions-path alias route and any Paperclip-specific API-shape harmonization.
## Existing Related Tasks
- **FN-1085 (end-to-end agent review):** General agent behavior consistency and bug fixes; relevant umbrella but not a direct replacement for checkout/inbox/budget features.
- **FN-1096 / FN-1098 / FN-1099 (task-agent assignment):** Foundational for checkout/inbox ownership semantics. New work should depend on these conventions rather than redefine assignment.
- **FN-1119 (agent API keys):** Covers auth substrate useful for any future `/agents/me` and run-audit identity policies.
- **FN-1122 (agent permissions):** Relevant for self-assignment restrictions, checkout authorization, and escalation actions.
- **FN-1164 / FN-1165 / FN-1167 (org chart / chain of command):** Direct overlap with escalation-chain gap; avoid duplicate hierarchy modeling.
- **FN-1170 / FN-1172 / FN-1173 (agent instructions):** Overlaps with instructions-path parity and prompt-policy rollout.
- **FN-1181 / FN-1182 / FN-1183 (agent self-reflection):** Can complement blocked dedup and policy-aware heartbeat decisions.
- **FN-1184 / FN-1185 / FN-1186 / FN-1187 (agent performance ratings):** Useful dependency/input for budget-governance and execution throttling logic.

71
docs/agents-playbooks.md Normal file
View File

@@ -0,0 +1,71 @@
# Permanent Agent Heartbeat Playbooks
[← Agents](./agents.md)
Operator and implementer playbooks for durable (permanent) agent heartbeats. Heartbeats are **coordination** windows; task-body implementation runs on the executor path.
## Mental model
1. Wake (timer / assignment / message / on-demand)
2. Identity + Wake Delta + procedure
3. **One** concrete coordination action
4. Disposition + `fn_heartbeat_done`
Do **not** implement code, run tests, or commit from a default strict heartbeat.
## Playbook: Manager health tick
1. Process inbox / room notices first.
2. Read reports-health (if present) for stale direct reports.
3. Pick **one**: reassign blocked child, message an idle specialist, or create a focused follow-up.
4. Log outcome with next owner; `fn_heartbeat_done`.
## Playbook: Message / comment wake
1. Acknowledge the wake payload (message id or comment).
2. If one clear action: reply / create / delegate — then exit (scoped-wake).
3. Do not open a multi-step investigation in the same tick.
## Playbook: Bound task is executor-class or blocked
1. Do **not** re-read PROMPT.md to implement.
2. Skim for blocker risk; pivot to board signals (stale in-review, idle reports, memory themes).
3. **Blocked dedup:** if the same blocker was already logged and nothing new arrived → no-op with reason.
## Playbook: No-task ambient run
1. Inbox first.
2. Prefer auto-claim candidates only when policy allows and role matches.
3. Create or delegate **one** focused task; never unscheduled implementation with ambient tools alone.
## Playbook: Multi-assign inventory in Wake Delta
When Wake Delta lists “your assigned tasks (coordination inventory…)”:
- Use it to **unblock, reassign, or prioritize** — not to code.
- Bound line (`assigned task: FN-…`) remains the singular execution bind.
- Foreign `lease: held-by-other` → do **not** retry checkout this tick.
## Playbook: Empty wake / nothing to do
Explicit no-op with reason is success:
```text
fn_heartbeat_done summary: "No new inbox or wake delta; no open coordination lever; no-op."
```
## Anti-patterns
| Avoid | Prefer |
|---|---|
| Implement / test / commit in heartbeat | Executor path / create task |
| Re-comment same blocker every tick | Blocked dedup no-op |
| Retry checkout on conflict | Exit / other work |
| Pause task on failure | Log + create/delegate follow-up |
| Duplicate `fn_task_create` without scan | Scan open tasks first |
| Self-only “please review” with no real review path | Explicit reviewer / `in-review` handoff |
## Related
- Composition and settings: [Agents](./agents.md)
- Domain vocabulary: [CONCEPTS.md](../CONCEPTS.md)

View File

@@ -929,6 +929,10 @@ fn message delete MSG-123
fn agent mailbox AGENT-001
```
## Permanent agent playbooks
Worked manager/IC/message/blocked/no-task scenarios live in [Permanent Agent Heartbeat Playbooks](./agents-playbooks.md). Prefer those examples over re-deriving tick behavior from engine source.
## Heartbeat Prompt Composition and Autonomous Run Behavior
Heartbeat runs are composed from multiple prompt layers so each wake has full identity and operating context:
@@ -949,10 +953,16 @@ Heartbeat runs are composed from multiple prompt layers so each wake has full id
3. **Execution prompt framing**
- `Identity Snapshot` block (agent ID/role + loaded soul/instructions/memory preview; `memory: loaded` when either inline memory or workspace `MEMORY.md` is present)
- `Wake Delta` block (source, trigger detail, wake reason, assignment/comments/messages)
- Optional multi-assign inventory under Wake Delta (`your assigned tasks (coordination inventory…)`) ranked from `assignedAgentId` rows (cap 8); not an implement-from-heartbeat queue
- Heartbeat procedure block (task-scoped or no-task variant, plus optional per-agent procedure override file)
- System prompts always include **Critical Rules** (one action, no implement-from-heartbeat, checkout no-retry, blocked dedup) so custom `HEARTBEAT.md` cannot erase them
This structure ensures every run re-anchors on identity, wake reason, and current context before taking action.
**Heartbeat skill policy:** heartbeat sessions load the waking agent’s `metadata.skills` plus enabled plugin skills. There is **no** role fallback to the published `fusion` operator skill on the heartbeat lane.
**Default HEARTBEAT.md seed:** `ensureDefaultHeartbeatProcedureFile` is create-if-missing only. Operator edits to an existing per-agent procedure file are preserved; upgrade without force does not overwrite content.
#### Wake reason values (message wakes)
Heartbeat prompts derive wake reason from trigger context plus current inbox snapshot:

View File

@@ -0,0 +1,551 @@
---
title: Permanent Agent Heartbeat and Standing Instructions Hardening
type: feat
date: 2026-07-12
status: completed
deepened: 2026-07-12
refreshed_from_main: 2026-07-13
base_sha: 8e4514e58
---
# Permanent Agent Heartbeat and Standing Instructions Hardening
## Summary
Harden permanent-agent operating law without changing Fusion’s heartbeat/executor split: ship a stronger always-on procedure and system-prompt contract (disposition, blocked dedup, critical rules, scoped-wake, progress style, checkout no-retry), seed structured standing-instruction templates for new agents only, improve multi-task visibility in Wake Delta, and document playbooks + CONCEPTS so operators and implementers share one model.
---
## Main refresh notes (2026-07-13)
`feature/better-agent-instructions` was fast-forwarded to `origin/main` at `8e4514e58`. Plan paths and tests were re-audited against that tip. **Product intent of U1–U7 is unchanged.** Implementation file pointers below supersede earlier paths when they conflict.
| Area | Change on main | Plan impact |
|---|---|---|
| Storage | SQLite → PostgreSQL cutover (#1793 and follow-ups) | Still use `TaskStore` facade APIs (`getTasksByAssignedAgent`, `selectNextTaskForAgent`). Impl bodies live under `packages/core/src/task-store/`. Prefer facade in engine code; touch impl files only if ranking helpers need colocation. |
| Heartbeat prompt tests | `heartbeat-session-prompt.test.ts` **removed** | FN-5053 / system-prompt / no-task tool alignment lives in `packages/engine/src/__tests__/heartbeat-executor.test.ts` (~2950+). U1/U2 tests extend **that** file + procedure snapshots. |
| Ranking APIs | Facades in `store.ts`; impls split out | `getTasksByAssignedAgentImpl` → `packages/core/src/task-store/remaining-ops-6.ts`; `selectNextTaskForAgentImpl` → `packages/core/src/task-store/branch-group-ops.ts`. Pure Wake Delta ranker still recommended as new module (not necessarily inside those ops files). |
| Heartbeat ops | FN-7939 timer-audit supervision; FN-7878/error recovery hardening | Do not fight timer/zombie supervision. U1–U5 stay in prompt/Wake Delta/procedure text + conflict coverage. |
| Multi-assign still missing | Wake Delta still singular `- assigned task:` | U5 still required; no preemption by main. |
| Procedure snapshots | Still `agent-heartbeat-procedures.test.ts` + `.snap` | Unchanged approach for U2. |
| Gap-analysis doc | Local delete of `docs/agent-paperclip-gap-analysis.md` + README row (uncommitted WIP on this branch) | Orthogonal cleanup; keep deleted; not part of U1–U7 product scope. |
**Implementer rule after refresh:** run file-scoped tests under current names only. Do not reintroduce `heartbeat-session-prompt.test.ts`.
---
## Problem Frame
Permanent agents already wake on timers, assignment, messages, and on-demand runs with identity + procedure injection. In practice, ticks still thrash (re-commenting blockers, vague exits, weak multi-task pick visibility), standing instructions are freeform and uneven across presets, and existing per-agent `HEARTBEAT.md` files freeze old procedure text because upgrade does not overwrite. Operator docs describe composition well but lack worked playbooks and accurate upgrade semantics.
The goal is not to make heartbeats into coding sessions. Task-body work stays on the executor path. The goal is **reliable coordination ticks**: clear ownership discipline, one concrete action, durable handoff, and consistent identity structure for new permanent agents.
---
## Requirements
- **R1.** Default task-scoped and no-task heartbeat procedures (`strict` at minimum) include a final disposition checklist before `fn_heartbeat_done`.
- **R2.** Procedures and/or system prompts include blocked-task dedup guidance (no re-chase when blocker context is unchanged).
- **R3.** A compact critical-rules block exists in system prompts (survives custom `HEARTBEAT.md`) and is reinforced in default procedure text.
- **R4.** Scoped-wake fast path: message/comment/`task_assigned` wakes prioritize that signal and skip ambient board thrash when a single clear action is available.
- **R5.** Progress note style guidance for `fn_task_log` / ambient persist tools (status line + done / remaining / next owner + task ids).
- **R6.** Checkout/claim conflict no-retry language is agent-visible; existing engine exit on `checkout_conflict` remains the authority.
- **R7.** New permanent agents can be created with a six-section standing instructions skeleton (Description, Expertise, Priorities, Boundaries, Communication, Collaboration & Escalation) without migrating existing agents.
- **R8.** Custom blank create prefill and empty-state “insert template” do not overwrite non-empty instructions.
- **R9.** Heartbeat/executor separation remains: no procedure text directs coding/tests/commits from heartbeat under default strict discipline.
- **R10.** No-task procedures never reference task-only tools (`fn_task_log`, task documents) — FN-5053 remains green.
- **R11.** Operator docs gain worked playbooks and CONCEPTS glossary entries; `docs/agents.md` upgrade wording matches create-if-missing seed behavior (or product changes upgrade to force re-seed with explicit operator action).
- **R12.** Phase-1 engine: Wake Delta can surface a compact ranked multi-assignment list for agents with multiple `assignedAgentId` rows (reuse inbox ranking / store helpers; no second claim system).
---
## Key Technical Decisions
1. **Keep coordination default; do not fold executor work into heartbeat.** Rationale: worktree/executor/reviewer/merge pipeline is Fusion’s implementation path; heartbeats remain ambient control.
2. **Dual placement for critical rules: system prompt + default procedure.** Rationale: custom `HEARTBEAT.md` fully replaces procedure text on task-scoped runs; system prompts still load. Critical safety language must survive operator-edited procedure files.
3. **Prefer procedure constants as source of truth; seed `HEARTBEAT.md` is create-if-missing.** Rationale: mode variants (strict/lite/off, task/no-task) are code-selected; snapshot tests pin constants. Do not silently overwrite operator-edited files. Optional force re-seed is a separate product action with explicit UI label.
4. **Standing instructions template seeds `instructionsText`, not a new default file and not `HEARTBEAT.md`.** Rationale: all 20 presets, onboarding, and interview already use inline instructions; engine concatenates opaque markdown.
5. **Template lives in dashboard agent-presets first.** Rationale: create/edit/onboarding surfaces are dashboard-owned; core/engine need no schema change for v1.
6. **Multi-task visibility is Wake Delta injection only in U5; assignee-filtered tools stay deferred.** Rationale: multi-assign is needed at procedure pick time before agents reliably call tools. `getTasksByAssignedAgent` already exists. `fn_task_list({ assignedTo: "me" })` only if mid-run re-query is proven later.
7. **Multi-assign membership is `assignedAgentId === me`, not lease-owned only.** Lease is an annotation (`held-by-other`), not the membership filter. Checkout is an execution lease often absent on `todo`; lease-only would hide most multi-assign backlog.
8. **Multi-assign cap is fixed at 8 with `+N more`; no per-agent setting in U5.** Rationale: between auto-claim default density (5) and max (10); owned tasks are higher-signal than optional pickup candidates.
9. **Ranked multi-assign lines follow inbox actionability; fully unactionable blocked are count-only.** Align with `selectNextTaskForAgent`: titled lines for `in_progress`, ready `todo`, partially blocked. Fully blocked / pure wait → aggregate count line only (avoids re-chase thrash). Optional later: include `in-review`/`triage` as low-rank titled rows if operators need them.
10. **Multi-assign list is coordination inventory, not an implement-from-heartbeat queue.** Header framing + U1/U2 pivot law required. Bound task line stays singular; list marks `(bound)` on the matching row.
11. **Multi-assign and auto-claim stay separate prompt sections with disjoint universes.** Auto-claim candidates require unassigned ready todos; multi-assign requires `assignedAgentId`. Never merge headers.
12. **Compact self-only fields in multi-assign rows.** `id`, column, rank tag, short title snippet (~60–80 chars), optional lease annotation. No PROMPT.md, comments, or other agents’ boards.
13. **Delivery order: U1+U2 before or same PR as U5.** List injection is code-independent of prompt law, but shipping multi-list without pivot/critical-rules regresses thrash.
14. **Blocked dedup is prompt-first.** Rationale: thrash is behavioral; store fingerprints only if production still re-chases after procedure ships.
15. **Do not auto-attach the published `fusion` skill to heartbeats.** Rationale: heartbeat skill policy has no role fallback; stuffing playbooks into `fusion` would hit operators, not permanent agents, and bloat every tick if forced.
16. **User-facing docs never name external control-plane competitors.** Rationale: product framing is Fusion-native permanent-agent quality.
17. **Existing agents: no silent instruction or HEARTBEAT rewrite.** Rationale: operator edits and production hearts must not regress on deploy.
18. **Prefer `getTasksByAssignedAgent` + pure rank helper over `listTasks` for Wake Delta.** Do not re-call `selectNextTaskForAgent` solely to build the multi-list (it full-board scans). Coarse dep tiering on assigned rows is acceptable for v1.
---
## High-Level Technical Design
```mermaid
flowchart TB
subgraph alwaysOn [Always-on prompt layers]
Sys[System prompt + critical rules]
Ident[Identity: instructionsText + path + soul + memory]
Exec[Execution framing: Identity Snapshot + Wake Delta]
Proc[Procedure: built-in or HEARTBEAT.md]
end
Wake[Wake: timer / assignment / message / on-demand] --> Sys
Sys --> Ident --> Exec --> Proc
Proc --> Action[One coordination action]
Action --> Disp[Disposition checklist]
Disp --> Done[fn_heartbeat_done]
Bound{Bound task class?}
Bound -->|executor-class or blocked| Pivot[Pivot to board coordination]
Bound -->|coordination-class| Engage[Engage bound task]
Proc --> Bound
subgraph later [Executor path unchanged]
Exe[Executor implements task body in worktree]
end
Action -.->|create / delegate / assign| Exe
```
**Layer responsibilities**
| Layer | Owns |
|---|---|
| System prompt | Invariants that survive custom HEARTBEAT.md |
| Procedure constants / seeded HEARTBEAT.md | Per-tick ritual (strict/lite/off) |
| Standing instructions | Role identity and operating orders |
| Wake Delta | Tick-local facts: wake reason, bound task, inbox, multi-assign list |
| Docs playbooks | Worked scenarios operators can cite; not every-tick injection |
**Procedure precedence (unchanged contract, documented clearly)**
1. No-task + custom file present → built-in **no-task** procedure (`default-no-task-override`)
2. Task-scoped + custom file loads → custom fully replaces built-in
3. Else → mode-selected built-in (`strict` / `lite` / `off`)
### U5 multi-assign / pick-work design (deepened)
```mermaid
flowchart LR
subgraph ownership [Ownership model]
Many["task.assignedAgentId\n(N tasks → 1 agent)"]
One["agent.taskId\n(0–1 execution bind)"]
end
Many --> Rank[rankAssignedTasksForWakeDelta]
One --> BoundLine["Wake Delta: assigned task: FN-X"]
Rank --> List["Wake Delta: your assigned tasks\n(ranked, cap 8)"]
BoundLine --> Agent[Permanent agent tick]
List --> Agent
Agent -->|coordination only| Act[One action]
Agent -.->|never| Code[Implement task body]
```
**Cardinalities (implementer must not confuse these)**
| Field | Cardinality | Role |
|---|---|---|
| `task.assignedAgentId` | Many tasks → one agent | Durable ownership inventory |
| `agent.taskId` | 0–1 | Singular execution bind for this tick |
**Data source**
- Primary: `TaskStore.getTasksByAssignedAgent(agentId, { excludeArchived: true })` (facade: `packages/core/src/store.ts` → `getTasksByAssignedAgentImpl` in `packages/core/src/task-store/remaining-ops-6.ts`)
- Soft-deleted already excluded via `ACTIVE_TASKS_WHERE`
- **Do not** use `listTasks` full board just to render the list
- **Do not** re-call `selectNextTaskForAgent` only for list building (facade → `selectNextTaskForAgentImpl` in `packages/core/src/task-store/branch-group-ops.ts`; still full-board scan)
**Ranking / tiers (aligned with inbox, extended for visibility)**
Order within titled ranked lines (FIFO by `columnMovedAt ?? createdAt` within tier):
1. `in_progress` — `column === "in-progress"`, annotate `paused` if set
2. `ready_todo` — `todo`, not paused, deps empty or treat as ready when unresolved (v1 coarse); foreign checkout → annotate `lease: held-by-other`, still list but not “pick to claim”
3. `partial_blocked` — `todo` with deps and partial progress (v1: deps non-empty is enough coarse signal if full dep hydrate is expensive)
4. Optional low-rank titled: `in-review`, `triage` (product may omit in v1 and fold into count-only)
5. **Exclude from titled lines:** `done`, `archived`
6. **Count-only line:** fully unactionable blocked / pure wait / optionally paused-only piles — e.g. `also assigned not actionable now: N (fully blocked/paused)`
**Render format (authoritative sketch)**
```markdown
## Wake Delta
- source: timer
- wake reason: timer
- assigned task: FN-100
- your assigned tasks (coordination inventory — not an implement-from-heartbeat queue; ranked, 3 of 3):
1. FN-100 [in_progress] (bound) Fix checkout lease renew
2. FN-220 [ready_todo] Draft board hygiene follow-up
3. FN-301 [partial_blocked] Wait on schema — lease: held-by-other
- also assigned not actionable now: 2 (fully blocked/paused)
- inbox snapshot: …
```
| Condition | Behavior |
|---|---|
| 0 open assigned after filters | Omit multi-list block entirely |
| ≥2 open assigned **or** ≥1 sibling besides bound | Show ranked list |
| Exactly 1 open assigned and it is bound | Optional one-line list or omit; prefer omit to reduce noise |
| Cap 8 with total > 8 | Show 8 lines + `(+N more assigned open tasks; do not auto-retry checkout/claim)` |
| No-task run but assigned rows exist | Keep `- assigned task: none` + multi-list (recovery visibility) |
| Auto-claim candidates also present | Separate header; never merge sections |
**checkout_conflict (independent of list)**
Path in `HeartbeatMonitor.executeHeartbeat` (`packages/engine/src/agent-heartbeat.ts`): bound task has `checkedOutBy` set and ≠ agent → `completeRun` with `resultJson.reason: "checkout_conflict"`, **before** session create. No Wake Delta that tick. U5 must add unit coverage; U1 supplies no-retry language for tool paths.
**Pure helper extract (recommended)**
- New: `packages/core/src/assigned-task-ranking.ts` exporting `rankAssignedTasksForWakeDelta(...)`
- Or private helpers colocated with heartbeat if export surface is undesirable
- Keep `selectNextTaskForAgent` behavior byte-compatible in the same PR unless sharing is free
**Mocks**
- `createMockTaskStore` must include `getTasksByAssignedAgent: vi.fn().mockResolvedValue([])` so existing heartbeat tests do not throw once the monitor starts calling it
**Known pre-existing drift (document in U5 / U6, do not necessarily fix in U5)**
- Inbox `assignTask` path may set `agent.taskId` without ensuring `task.assignedAgentId` consistency on every path. Multi-list makes drift visible; fix as follow-up if observed.
---
## Scope Boundaries
### In scope
- Engine procedure + system prompt text and tests/snapshots
- Optional Wake Delta multi-assign list + checkout_conflict coverage test
- Dashboard standing-instructions template constant, custom prefill, empty-state insert, optional preset restructure
- Onboarding interview prompt guidance to fill six sections
- Docs: playbooks, CONCEPTS, agents.md accuracy, README index
- i18n for new template button/placeholder strings (en + type defs; other locales follow project convention)
### Out of scope
- Making heartbeats implement task bodies by default
- Auto-loading repo root `AGENTS.md` into permanent agents
- Silent migration of existing `instructionsText` or `HEARTBEAT.md`
- Full budget hard-stop product (usage APIs already exist; policy thresholds are follow-up)
- New parallel claim/ownership ledger
- Expanding published `fusion` skill into permanent-agent runtime law
- Typed board interaction redesign beyond existing `fn_ask_question` guidance in prompts/docs
### Deferred to Follow-Up Work
- Force re-seed / overwrite `HEARTBEAT.md` with backup from upgrade button
- Store-level blocker fingerprint for blocked dedup
- `fn_task_list({ assignedTo: "me" })` or `fn_agent_inbox` tool
- Compact opt-in heartbeat skill via `metadata.skills`
- lite/off procedure parity for every new checklist line (strict is mandatory; lite/off get critical safety subset)
- Engine self-improve prompts preserving section headings
---
## Phased Delivery
| Phase | Theme | Units | Landability |
|---|---|---|---|
| **P0** | Always-on law (prompt) | U1, U2 | Single engine PR |
| **P1** | Standing identity template | U3, U4 | Dashboard PR (can parallel P0) |
| **P2** | Agent-visible pick work | U5 | Engine PR after P0 |
| **P3** | Docs + glossary | U6 | Docs PR anytime after P0 text settles |
| **P4** | Optional upgrade truth | U7 | Only if product wants force re-seed |
---
## Implementation Units
### U1. Harden heartbeat system prompts (critical rules, checkout, dedup, note style)
- **Goal:** Put durable operating law into `HEARTBEAT_SYSTEM_PROMPT` and `HEARTBEAT_NO_TASK_SYSTEM_PROMPT` so custom `HEARTBEAT.md` cannot erase it.
- **Requirements:** R2, R3, R5, R6, R9, R10
- **Dependencies:** none
- **Files:**
- `packages/engine/src/agent-heartbeat.ts`
- `packages/engine/src/__tests__/heartbeat-executor.test.ts` (system-prompt / FN-5053 / no-task tool alignment lives here after main refresh; former `heartbeat-session-prompt.test.ts` removed)
- **Approach:**
- Add a short **Critical Rules** section (bullet list, ~8–12 lines) covering: one action; no implement-from-heartbeat; blocked dedup; no checkout/claim retry on conflict; no duplicate `fn_task_create` without scan; escalate via reports-to when stuck; prefer delegate/create over human for agent-capable work; progress notes structured; no-op must be explicit.
- Reinforce existing no-pause-on-failure language in system prompts (still present as “Do NOT call fn_task_pause…” in `HEARTBEAT_SYSTEM_PROMPT` / no-task variant).
- Keep no-task tool inventory consistent with FN-5053 forbidden list (asserted in `heartbeat-executor.test.ts`).
- **Patterns:** Existing no-task tool alignment + system-prompt containment tests in `heartbeat-executor.test.ts` (~2950+ and related describe blocks).
- **Test scenarios:**
- Happy path: task system prompt contains critical-rules anchors (checkout no-retry, blocked dedup, disposition or progress-note language).
- Happy path: no-task system prompt contains ambient-safe critical rules and still omits task-only tool directives in forbidden set.
- Regression: no-pause-on-failure language still present.
- Regression: FN-5053 no-task tool alignment still passes (existing executor tests).
- **Verification:** File-scoped vitest on `heartbeat-executor.test.ts`; no full suite.
---
### U2. Harden default procedures (disposition, scoped-wake, strict self-check)
- **Goal:** Update `HEARTBEAT_PROCEDURE_*` and `HEARTBEAT_NO_TASK_PROCEDURE_*` so default/seeded procedure paths get the full tick ritual.
- **Requirements:** R1, R2, R4, R5, R9, R10
- **Dependencies:** U1 (shared wording anchors; can land same PR)
- **Files:**
- `packages/engine/src/agent-heartbeat.ts`
- `packages/engine/src/__tests__/agent-heartbeat-procedures.test.ts`
- `packages/engine/src/__tests__/__snapshots__/agent-heartbeat-procedures.test.ts.snap`
- `packages/engine/src/__tests__/heartbeat-executor.test.ts`
- **Approach:**
- **STRICT task:** After wake delta, add scoped-wake branch (if message/comment/`task_assigned` with clear single action → act and exit without broad board scan). Before exit: **Final disposition checklist** (acted with evidence / delegated / follow-up created / blocked with owner / explicit no-op reason). Keep executor-class/blocked pivot. Add blocked dedup under classify/persist. Progress note style under persist step.
- **STRICT no-task:** Same disposition + scoped-wake; ambient-only persist tools; no `fn_task_log`.
- **LITE / OFF:** Subset — disposition + critical no-retry/dedup lines minimum; full classify matrix only in strict.
- Seed content remains `HEARTBEAT_PROCEDURE` (= strict); new agents and missing files get new text; **existing files unchanged**.
- **Patterns:** Snapshot suite `agent-heartbeat-procedures.test.ts`; executor asserts for `executor-class`, inbox order, no-task override.
- **Test scenarios:**
- Snapshot update for all six procedure templates when text changes.
- Executor: task-scoped strict still contains classify + new disposition phrases.
- Executor: no-task still overrides custom procedure file (`default-no-task-override`).
- Edge: no-task procedure must not mention `fn_task_log` / task document tools.
- **Verification:** Snapshot + file-scoped engine tests green.
---
### U3. Standing instructions template constant + create/edit UX
- **Goal:** Provide a six-section markdown skeleton for permanent-agent standing instructions without breaking existing agents.
- **Requirements:** R7, R8
- **Dependencies:** none (parallel to U1/U2)
- **Files:**
- `packages/dashboard/app/components/agent-presets/standing-instructions-template.ts` (new)
- `packages/dashboard/app/components/agent-presets/agentCreatePayload.ts`
- `packages/dashboard/app/components/NewAgentDialog.tsx`
- `packages/dashboard/app/components/AgentDetailView.tsx` (Instructions tab empty-state insert)
- `packages/dashboard/app/components/__tests__/standing-instructions-template.test.ts` (new)
- `packages/dashboard/app/components/__tests__/agent-presets.test.ts` (only if presets restructured)
- `packages/i18n/locales/en/app.json` (+ `resources.d.ts` as required)
- **Approach:**
- Export `STANDING_INSTRUCTIONS_TEMPLATE` with headings: Description, Expertise, Priorities, Boundaries, Communication, Collaboration & Escalation.
- Helpers: empty detection; apply only when blank.
- Custom create path: prefill skeleton.
- Detail tab: “Insert template” when inline empty; never auto-run on load for non-empty.
- Do **not** write template into `heartbeatProcedurePath`.
- **Patterns:** `agentCreatePayload.ts` mapping; Instructions tab save via `updateAgentInstructions`.
- **Test scenarios:**
- Custom blank create shows six headings in draft.
- Non-empty instructions: insert does not silently replace (disabled or confirm-only).
- Template helper: empty → skeleton; non-empty → unchanged.
- Heartbeat path still independent of instructions save.
- **Verification:** Dashboard component unit tests file-scoped.
---
### U4. Seed new agents and interview drafts with structured instructions
- **Goal:** Improve quality of **new** permanent agents (presets + onboarding interview) without migrating live agents.
- **Requirements:** R7
- **Dependencies:** U3
- **Files:**
- `packages/dashboard/app/components/agent-presets/index.ts` (optional: rewrite `instructionsText` into six sections)
- `packages/dashboard/src/agent-onboarding.ts` (system prompt: fill six sections inside `instructionsText`)
- `packages/dashboard/app/components/__tests__/agent-presets.test.ts`
- `packages/dashboard/src/__tests__/agent-onboarding.test.ts` (if prompt text asserted)
- Setup/Model onboarding tests only if payload shape assertions change
- **Approach:**
- Prefer structured preset bodies for all 20 presets **or** soft-merge: keep role bullets under Priorities inside the skeleton (product call during implement: structured rewrite is cleaner for new installs).
- Interview system prompt: require six headings when generating `instructionsText`; accept freeform for backward compatibility (no hard validation).
- Soul remains character; template sections remain operating rules (avoid duplicating long communication essays in both).
- **Patterns:** `mapPresetToAgentDraft`, `mapOnboardingSummaryToAgentDraft`, `AGENT_ONBOARDING_SYSTEM_PROMPT`.
- **Test scenarios:**
- CEO preset create payload includes non-empty instructions with required headings (if restructure lands).
- Preset integrity suite still enforces minimum guidance length.
- Onboarding summary with freeform instructions still validates as today.
- **Verification:** Preset + onboarding tests; manual smoke optional.
---
### U5. Wake Delta multi-assignment list + checkout_conflict test coverage
- **Goal:** Make multi-task ownership visible to permanent agents without a parallel claim system; lock checkout_conflict early-exit with a regression test.
- **Requirements:** R6, R12
- **Dependencies:** **U1+U2 before or same engine PR** (soft product dependency: list without pivot/critical-rules induces implement-from-heartbeat thrash). Code can land same PR as U1/U2.
- **Files:**
- `packages/engine/src/agent-heartbeat.ts` — Wake Delta assembly (task-scoped + no-task branches); FNXC comment; checkout_conflict still ~2628
- `packages/core/src/assigned-task-ranking.ts` (new, recommended pure helper) + export if public
- `packages/core/src/store.ts` — facade only; prefer not to bloat; optional thin re-export
- `packages/core/src/task-store/remaining-ops-6.ts` / `branch-group-ops.ts` — only if sharing sort/dep helpers without behavior change
- `packages/engine/src/__tests__/heartbeat-executor.test.ts` — multi-list + checkout_conflict; mock `getTasksByAssignedAgent` (already used for pause-cascade paths)
- `packages/engine/src/__tests__/heartbeat-test-helpers.ts` — ensure `createMockTaskStore` defaults include `getTasksByAssignedAgent`
- `packages/core/src/__tests__/assigned-task-ranking.test.ts` (new, if helper extracted)
- Optional docs line in `docs/agents.md` (or leave to U6)
- **Approach:** See **U5 multi-assign / pick-work design (deepened)** above. Summary:
1. After bound task resolution and **after** early exits that never prompt, call `getTasksByAssignedAgent(agentId, { excludeArchived: true })`.
2. Rank with pure helper; cap **8**; inject after `- assigned task:` under **coordination inventory** framing.
3. Count-only line for fully unactionable blocked/paused piles.
4. Keep auto-claim section separate and unlabeled as ownership.
5. Unit-test `checkout_conflict` early exit (`createFnAgent` not called).
6. **Do not** add `fn_task_list` assignee filter in this unit.
7. **Do not** auto-claim/checkout from list rendering.
- **Patterns:** Auto-claim candidate injection style; inbox early-exit tests that assert `resultJson` + no session.
- **Test scenarios:**
- **Happy path:** agent with two assigned open todos; bound `FN-A`; prompt contains both ids; `(bound)` only on `FN-A`; singular `- assigned task: FN-A` unchanged.
- **Empty omit:** `getTasksByAssignedAgent` → `[]` → no `your assigned tasks` / `assigned open tasks` block.
- **Cap + overflow:** 12 open tasks → 8 numbered lines + `+4 more` (or equivalent) + no-retry wording.
- **No-task + assigned backlog:** `taskId` unset, inbox/auto-claim null, assigned rows present → `- assigned task: none` **and** multi-list.
- **Foreign lease annotation:** assigned row with `checkedOutBy` other → `lease: held-by-other` (or equivalent) without claim attempt.
- **Fully blocked:** titled ranked lines exclude fully unactionable blocked; count-only line includes them.
- **checkout_conflict:** `agent.taskId` set, `getTask` returns foreign `checkedOutBy` → `status: completed`, `resultJson.reason === "checkout_conflict"`, `createFnAgent` not called; multi-list not required that tick.
- **Regression:** existing no-task auto-claim header still distinct from multi-assign header when both appear.
- **Verification:** File-scoped engine (+ optional core ranking) tests green; no marathon suite.
---
### U6. Operator docs: playbooks, CONCEPTS, agents.md accuracy
- **Goal:** Document permanent-agent tick behavior for operators and for implementers of U1–U5.
- **Requirements:** R11
- **Dependencies:** U1/U2 text should be near-final so docs match shipped procedure language
- **Files:**
- `docs/agents-playbooks.md` (new)
- `docs/agents.md` (link playbooks; fix upgrade create-if-missing wording; clarify heartbeat skill policy: no `fusion` role fallback)
- `docs/README.md` (index row)
- `CONCEPTS.md` (Permanent/durable agent, Heartbeat run, Checkout lease, Heartbeat procedure, Auto-claim, assignmentPolicy)
- **Approach:**
- Playbooks: manager health tick; IC/no-task claim-or-delegate; blocked bound task; message wake; empty wake no-op; stale in-review chase; anti-patterns (implement-from-heartbeat, blocked spam, self-review theater).
- No external competitor names.
- Optional short pointer in `packages/cli/skill/fusion/references/` for operators only — not full playbook paste.
- **Patterns:** Existing `docs/agents.md` composition section; plan doc style from first-run onboarding.
- **Test scenarios:**
- Test expectation: none for pure docs — unless existing doc inventory tests assert README rows; if so, update inventory/assertions.
- **Verification:** Links resolve; wording matches engine (especially upgrade and skill policy).
---
### U7. Optional: explicit force re-seed of default HEARTBEAT.md
- **Goal:** Give operators a deliberate path to pull new built-in procedure text when product wants it.
- **Requirements:** R11 (only if product chooses overwrite semantics)
- **Dependencies:** U2 (new default content exists)
- **Files:**
- `packages/engine/src/agent-instructions.ts` or upgrade route handler
- `packages/dashboard/src/routes/register-agent-core-routes.ts`
- `packages/dashboard/app/components/AgentDetailView.tsx`
- i18n strings for confirm dialog
- Route + UI tests
- **Approach:**
- Keep `ensureDefaultHeartbeatProcedureFile` create-if-missing for create.
- Upgrade endpoint gains explicit `force: true` (or separate action) that overwrites with `HEARTBEAT_PROCEDURE`, with UI confirm “replaces your edits.”
- Default upgrade without force remains path-fix + create-if-missing.
- **Execution note:** Only implement if product confirms force re-seed; otherwise document-only fix in U6 is enough.
- **Test scenarios:**
- Force true: existing file content becomes current `HEARTBEAT_PROCEDURE`.
- Force false / default: existing custom content preserved.
- **Verification:** Route unit tests + UI confirm path.
---
## Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Existing `HEARTBEAT.md` stays on old text | U1 system-prompt critical rules still apply; U6 documents; U7 optional force |
| Procedure bloat burns tokens | Keep critical rules short; playbooks stay in docs |
| Breaking FN-5053 no-task tool alignment | Dual edit of no-task procedure + tests |
| Preset rewrite churn for tests | Prefer template helpers first; restructure presets as explicit sub-decision in U4 |
| Multi-assign list noise | Cap 8 + rank; omit when zero; count-only for fully blocked |
| Multi-list induces implement-from-heartbeat | Coordination-inventory header + U1/U2 first |
| Dual lists (multi-assign + auto-claim) confuse agents | Distinct headers; disjoint membership |
| Agents still implement in heartbeat | Keep strict pivot language; system-prompt forbid |
| assignTask vs assignedAgentId drift | Multi-list makes visible; follow-up fix if production shows gap |
| Coarse blocked tier vs inbox | Acceptable for v1; optional dep hydrate later |
---
## Alternatives Considered
1. **Inject large permanent-agent skill every heartbeat** — Rejected for token cost and skill policy (no fusion fallback on heartbeat). Opt-in compact skill deferred.
2. **Overwrite all HEARTBEAT.md on deploy** — Rejected; destroys operator edits. Force upgrade only with confirm.
3. **Put standing template into core createAgent** — Deferred; dashboard seeding covers create UI paths; API-only creates can adopt later.
4. **Collapse heartbeat into implement-in-wake sessions** — Rejected; fights executor/worktree/merge model.
---
## Assumptions
- Permanent agents remain primarily **coordinators** under default strict discipline.
- Snapshot-based procedure tests are the intended lock for procedure text.
- `getTasksByAssignedAgent` (TaskStore facade; PG-backed after main cutover) is sufficient for Wake Delta injection without new schema.
- System-prompt / FN-5053 regressions are owned by `heartbeat-executor.test.ts` after main removed `heartbeat-session-prompt.test.ts`.
- No changeset required (docs + internal engine/dashboard behavior; no published package API marketing change unless `@runfusion/fusion` skill pointer is shipped — skill pointer is optional and docs-only preferred).
- Plan authored against branch tip `8e4514e58` (`origin/main` as of 2026-07-13 refresh).
---
## Success Metrics
- Reduced blocked-task re-comment loops in agent logs (qualitative / sample tasks).
- New agents from custom create show structured instructions by default.
- Heartbeat procedure snapshots and FN-5053 / no-pause prompt regressions in `heartbeat-executor.test.ts` green.
- Operators can follow playbooks without reading engine source.
- Multi-assign agents see more than a single bound id in Wake Delta when multiple tasks are assigned.
---
## Sources and Research
Internal exploration (this plan’s research pass + 2026-07-13 main refresh):
- Heartbeat constants, assembly, seed/upgrade: `packages/engine/src/agent-heartbeat.ts`, `agent-instructions.ts`, `heartbeat-procedure-resolver.ts`
- Prompt/procedure tests: `packages/engine/src/__tests__/heartbeat-executor.test.ts`, `agent-heartbeat-procedures.test.ts` (+ snapshots), `heartbeat-test-helpers.ts`
- Standing instructions surfaces: `packages/dashboard/app/components/agent-presets/`, `NewAgentDialog.tsx`, `AgentDetailView.tsx`, `agent-onboarding.ts`
- Ownership/claim/inbox: `packages/core/src/agent-store.ts`; TaskStore facades in `store.ts`; `getTasksByAssignedAgentImpl` / `selectNextTaskForAgentImpl` under `packages/core/src/task-store/`; docs checkout leasing in `docs/agents.md`
- Skill injection: `packages/engine/src/session-skill-context.ts` (heartbeat has no role fallback)
- Main tip at refresh: `8e4514e58` (PG cutover and follow-ups)
Prior art framing (private to planning): control-plane style agents emphasize disposition checklists, blocked dedup, checkout no-retry, and progressive skill packaging — adapted here to Fusion’s coordinator/executor split without competitor branding in product docs.
---
## Documentation Plan
| Doc | When |
|---|---|
| `docs/agents-playbooks.md` | U6 |
| `docs/agents.md` updates | U6 (and U7 if force re-seed) |
| `CONCEPTS.md` entries | U6 |
| FNXC comments on procedure/system-prompt edits | U1/U2 implementation |
---
## Open Questions
### Resolved by deepening (U5)
| Question | Decision |
|---|---|
| Multi-assign cap | **8** fixed + `+N more` |
| Wake Delta vs new tool | **Wake Delta only** in U5 |
| Membership filter | **`assignedAgentId`**, lease annotated |
| Fully blocked in ranked lines | **No** — count-only |
| U1/U2 before U5 | **Yes** (same PR OK) |
| `fn_task_list` assignee filter | **Defer** |
### Still open (non-blocking)
1. Should all 20 presets fully restructure to six headings in U4, or only blank/custom + interview?
*Default if unanswered:* blank/custom + interview first; expand presets if timeboxed room remains.
2. Should U7 force re-seed ship in the same release as U2?
*Default:* **defer U7**; U6 documents create-if-missing truth.
3. Include `in-review` / `triage` as low-rank titled multi-assign rows in v1, or count-only only?
*Default:* **count-only / omit titled** for non-actionable columns in v1; only in_progress / ready_todo / partial_blocked titled.

View File

@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import {
formatAssignedTasksWakeDeltaSection,
rankAssignedTasksForWakeDelta,
WAKE_DELTA_ASSIGNED_TASKS_CAP,
type AssignedTaskLike,
} from "../assigned-task-ranking.js";
function task(partial: Partial<AssignedTaskLike> & Pick<AssignedTaskLike, "id" | "column">): AssignedTaskLike {
return {
createdAt: "2026-07-01T00:00:00.000Z",
...partial,
};
}
describe("rankAssignedTasksForWakeDelta", () => {
it("orders in_progress before ready_todo before partial_blocked", () => {
const result = rankAssignedTasksForWakeDelta(
[
task({ id: "FN-T", column: "todo", title: "Ready", createdAt: "2026-07-03T00:00:00.000Z" }),
task({ id: "FN-B", column: "todo", title: "Blocked", dependencies: ["FN-X"], createdAt: "2026-07-02T00:00:00.000Z" }),
task({ id: "FN-P", column: "in-progress", title: "Active", createdAt: "2026-07-01T00:00:00.000Z" }),
],
{ agentId: "agent-1", boundTaskId: "FN-P" },
);
expect(result.ranked.map((r) => r.task.id)).toEqual(["FN-P", "FN-T", "FN-B"]);
expect(result.ranked[0]?.labels).toContain("bound");
});
it("excludes done/archived, counts paused as not actionable, and keeps open custom columns titled", () => {
const result = rankAssignedTasksForWakeDelta(
[
task({ id: "FN-1", column: "todo", title: "Open" }),
task({ id: "FN-2", column: "done", title: "Done" }),
task({ id: "FN-3", column: "todo", title: "Paused", paused: true }),
task({ id: "FN-4", column: "in-review", title: "Review" }),
task({ id: "FN-5", column: "ready-for-dev", title: "Custom workflow ready" }),
],
{ agentId: "agent-1" },
);
// todo first, then other-tier open columns (in-review + custom) by createdAt
expect(result.ranked.map((r) => r.task.id)).toEqual(["FN-1", "FN-4", "FN-5"]);
expect(result.ranked.find((r) => r.task.id === "FN-5")?.tier).toBe("other");
expect(result.notActionableCount).toBe(1); // paused only
expect(result.totalOpen).toBe(4); // excludes done
});
it("caps titled lines and marks truncated", () => {
const tasks = Array.from({ length: WAKE_DELTA_ASSIGNED_TASKS_CAP + 4 }, (_, i) =>
task({
id: `FN-${i}`,
column: "todo",
title: `Task ${i}`,
createdAt: `2026-07-${String(i + 1).padStart(2, "0")}T00:00:00.000Z`,
}),
);
const result = rankAssignedTasksForWakeDelta(tasks, { agentId: "agent-1" });
expect(result.ranked).toHaveLength(WAKE_DELTA_ASSIGNED_TASKS_CAP);
expect(result.truncated).toBe(true);
});
it("annotates foreign lease", () => {
const result = rankAssignedTasksForWakeDelta(
[task({ id: "FN-1", column: "todo", title: "Held", checkedOutBy: "other-agent" })],
{ agentId: "agent-1" },
);
expect(result.ranked[0]?.labels.some((l) => l.includes("held-by-other"))).toBe(true);
});
});
describe("formatAssignedTasksWakeDeltaSection", () => {
it("omits empty inventory", () => {
const result = rankAssignedTasksForWakeDelta([], { agentId: "agent-1" });
expect(formatAssignedTasksWakeDeltaSection(result)).toBe("");
});
it("omits single bound-only titled inventory by default", () => {
const result = rankAssignedTasksForWakeDelta(
[task({ id: "FN-1", column: "in-progress", title: "Only" })],
{ agentId: "agent-1", boundTaskId: "FN-1" },
);
expect(formatAssignedTasksWakeDeltaSection(result, { boundTaskId: "FN-1" })).toBe("");
});
it("renders multi inventory with coordination framing", () => {
const result = rankAssignedTasksForWakeDelta(
[
task({ id: "FN-1", column: "in-progress", title: "A" }),
task({ id: "FN-2", column: "todo", title: "B" }),
],
{ agentId: "agent-1", boundTaskId: "FN-1" },
);
const text = formatAssignedTasksWakeDeltaSection(result, { boundTaskId: "FN-1" });
expect(text).toContain("coordination inventory");
expect(text).toContain("FN-1");
expect(text).toContain("FN-2");
expect(text).toContain("(bound)");
});
});

View File

@@ -0,0 +1,196 @@
/*
FNXC:WakeDeltaMultiAssign 2026-07-13-12:15:
Permanent agents can own many tasks via assignedAgentId while agent.taskId is singular.
Heartbeat Wake Delta must surface a compact ranked inventory so coordinators can unblock/reassign without full-board thrash.
Membership is assignment-based; lease is annotation only; fully unactionable blocked stay count-only to avoid re-chase spam.
*/
/**
* Cap for titled multi-assign Wake Delta lines (plan U5: fixed 8, no setting).
*/
export const WAKE_DELTA_ASSIGNED_TASKS_CAP = 8;
export type AssignedTaskRankTier =
| "in_progress"
| "ready_todo"
| "partial_blocked"
| "other";
export interface AssignedTaskLike {
id: string;
column: string;
title?: string | null;
description?: string | null;
paused?: boolean | null;
dependencies?: string[] | null;
checkedOutBy?: string | null;
columnMovedAt?: string | null;
createdAt?: string | null;
deletedAt?: string | null;
}
export interface RankedAssignedTaskLine {
task: AssignedTaskLike;
tier: AssignedTaskRankTier;
labels: string[];
titleSnippet: string;
}
export interface RankAssignedTasksForWakeDeltaResult {
ranked: RankedAssignedTaskLine[];
totalOpen: number;
notActionableCount: number;
truncated: boolean;
}
function sortKey(task: AssignedTaskLike): string {
return task.columnMovedAt ?? task.createdAt ?? "";
}
function titleSnippet(task: AssignedTaskLike, max = 72): string {
const raw = (task.title?.trim() || task.description?.trim() || task.id).replace(/\s+/g, " ");
if (raw.length <= max) return raw;
return `${raw.slice(0, max - 1)}…`;
}
function isTerminalColumn(column: string): boolean {
return column === "done" || column === "archived";
}
/*
FNXC:WakeDeltaMultiAssign 2026-07-14-00:10:
Custom workflows use non-default column ids for ready/active work. Only treating
default `todo`/`in-progress` as titled hid assigned work as a bare count.
Map known default columns for rank quality; treat all other non-terminal open
columns (including custom workflow columns) as titled `other` so inventory stays
visible. Paused stays count-only to avoid re-chase noise.
*/
function tierForTask(task: AssignedTaskLike): AssignedTaskRankTier | "not_actionable" {
if (task.paused) return "not_actionable";
if (task.column === "in-progress") return "in_progress";
if (task.column === "todo") {
const deps = task.dependencies ?? [];
if (deps.length === 0) return "ready_todo";
// Coarse v1: non-empty deps ⇒ partial_blocked visibility (full dep hydrate deferred).
return "partial_blocked";
}
// Default triage/in-review and any project-specific open columns: keep titled
// at lowest rank so custom workflows do not hide assigned work as count-only.
return "other";
}
const TIER_ORDER: Record<AssignedTaskRankTier, number> = {
in_progress: 0,
ready_todo: 1,
partial_blocked: 2,
other: 3,
};
/**
* Rank open assigned tasks for Wake Delta multi-assign inventory.
* Excludes done/archived; titled lines only for actionable tiers; cap applied.
*/
export function rankAssignedTasksForWakeDelta(
tasks: AssignedTaskLike[],
options: {
agentId: string;
boundTaskId?: string | null;
cap?: number;
},
): RankAssignedTasksForWakeDeltaResult {
const cap = options.cap ?? WAKE_DELTA_ASSIGNED_TASKS_CAP;
const open = tasks.filter((t) => !t.deletedAt && !isTerminalColumn(t.column));
const titled: RankedAssignedTaskLine[] = [];
let notActionableCount = 0;
for (const task of open) {
const tierOrNa = tierForTask(task);
if (tierOrNa === "not_actionable") {
notActionableCount += 1;
continue;
}
const labels: string[] = [];
if (options.boundTaskId && task.id === options.boundTaskId) {
labels.push("bound");
}
if (task.checkedOutBy && task.checkedOutBy !== options.agentId) {
labels.push(`lease: held-by-other`);
}
titled.push({
task,
tier: tierOrNa,
labels,
titleSnippet: titleSnippet(task),
});
}
titled.sort((a, b) => {
const tierDiff = TIER_ORDER[a.tier] - TIER_ORDER[b.tier];
if (tierDiff !== 0) return tierDiff;
return sortKey(a.task).localeCompare(sortKey(b.task));
});
const truncated = titled.length > cap;
return {
ranked: titled.slice(0, cap),
totalOpen: open.length,
notActionableCount,
truncated,
};
}
/**
* Format ranked assigned tasks for Wake Delta markdown injection.
* Returns empty string when there is nothing useful to show.
*/
export function formatAssignedTasksWakeDeltaSection(
result: RankAssignedTasksForWakeDeltaResult,
options?: { showWhenSingleBoundOnly?: boolean; boundTaskId?: string | null },
): string {
const { ranked, totalOpen, notActionableCount, truncated } = result;
if (totalOpen === 0) return "";
// Prefer omit when only the bound task is titled and nothing else is open.
if (
ranked.length === 1 &&
options?.boundTaskId &&
ranked[0]?.task.id === options.boundTaskId &&
notActionableCount === 0 &&
!options.showWhenSingleBoundOnly
) {
return "";
}
if (ranked.length === 0 && notActionableCount === 0) return "";
const lines: string[] = [];
const actionableTotal = totalOpen - notActionableCount;
if (ranked.length > 0) {
const headerTotal = truncated
? `${ranked.length} of ${actionableTotal}`
: `${ranked.length}`;
lines.push(
`- your assigned tasks (coordination inventory — not an implement-from-heartbeat queue; ranked, ${headerTotal}):`,
);
ranked.forEach((row, index) => {
const labelSuffix = row.labels.length > 0 ? ` (${row.labels.join(", ")})` : "";
lines.push(
` ${index + 1}. ${row.task.id} [${row.tier}]${labelSuffix} ${row.titleSnippet}`,
);
});
if (truncated && actionableTotal > ranked.length) {
lines.push(
` (+${actionableTotal - ranked.length} more assigned open tasks; ranked list truncated — do not auto-retry checkout/claim)`,
);
}
}
if (notActionableCount > 0) {
lines.push(
`- also assigned not actionable now: ${notActionableCount} (paused)`,
);
}
return lines.join("\n");
}

View File

@@ -91,6 +91,17 @@ export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from ".
export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js";
export * from "./frontend-ux-policy.js";
export * from "./file-scope-classification.js";
export {
WAKE_DELTA_ASSIGNED_TASKS_CAP,
rankAssignedTasksForWakeDelta,
formatAssignedTasksWakeDeltaSection,
} from "./assigned-task-ranking.js";
export type {
AssignedTaskLike,
AssignedTaskRankTier,
RankedAssignedTaskLine,
RankAssignedTasksForWakeDeltaResult,
} from "./assigned-task-ranking.js";
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";

View File

@@ -72,6 +72,17 @@ export type { OverseerEventInput } from "./planner-overseer-events.js";
export * from "./frontend-ux-policy.js";
export * from "./file-scope-classification.js";
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
export {
WAKE_DELTA_ASSIGNED_TASKS_CAP,
rankAssignedTasksForWakeDelta,
formatAssignedTasksWakeDeltaSection,
} from "./assigned-task-ranking.js";
export type {
AssignedTaskLike,
AssignedTaskRankTier,
RankedAssignedTaskLine,
RankAssignedTasksForWakeDeltaResult,
} from "./assigned-task-ranking.js";
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
export {

View File

@@ -37,6 +37,7 @@ import { ExperimentalAgentOnboardingModal } from "./ExperimentalAgentOnboardingM
import { AgentPermissionPolicyEditor } from "./AgentPermissionPolicyEditor";
import { useFavorites } from "../hooks/useFavorites";
import { copyTextToClipboard } from "../utils/copyToClipboard";
import { STANDING_INSTRUCTIONS_TEMPLATE } from "./agent-presets/standing-instructions-template";
/**
* Simple className utility - joins class names conditionally
@@ -3125,6 +3126,21 @@ function InstructionsTab({
<label htmlFor="instructions-text">{t("agents.inlineInstructions", "Inline Instructions")}</label>
<div className="agent-content-toolbar">
<div className="agent-content-mode-toggle">
{!instructionsText.trim() && !showPreview && (
<button
type="button"
className="btn btn-sm"
data-testid="instructions-insert-template"
onClick={() => {
// FNXC:StandingInstructionsTemplate 2026-07-13-12:40:
// Empty-state only: insert six-section skeleton without overwriting non-empty instructions.
setInstructionsText(STANDING_INSTRUCTIONS_TEMPLATE);
setJustSaved(false);
}}
>
{t("agents.insertInstructionsTemplate", "Insert template")}
</button>
)}
<button
className={`btn btn-sm ${!showPreview ? "btn-primary" : ""}`}
onClick={() => setShowPreview(false)}

View File

@@ -17,6 +17,7 @@ import {
VALID_AGENT_CAPABILITIES,
type ThinkingLevel,
} from "./agent-presets/agentCreatePayload";
import { withStandingInstructionsTemplate } from "./agent-presets/standing-instructions-template";
import { SkillMultiselect } from "./SkillMultiselect";
import { AgentAvatar } from "./AgentAvatar";
import { ExperimentalAgentOnboardingModal } from "./ExperimentalAgentOnboardingModal";
@@ -234,7 +235,10 @@ export function NewAgentDialog({
setIcon(values.icon ?? "");
setRole(values.role);
setReportsTo(values.reportsTo ?? "");
setInstructionsText(values.instructionsText ?? "");
// FNXC:StandingInstructionsTemplate 2026-07-14-00:12:
// Prefill/onboarding can set custom tab programmatically with empty instructionsText.
// Seed the six-section skeleton for blank drafts; preserve non-empty interview content.
setInstructionsText(withStandingInstructionsTemplate(values.instructionsText ?? ""));
setHeartbeatProcedurePath(values.heartbeatProcedurePath ?? "");
setSoul(values.soul ?? "");
setMemory(values.memory ?? "");
@@ -470,7 +474,12 @@ export function NewAgentDialog({
aria-selected={stepZeroTab === "custom"}
tabIndex={stepZeroTab === "custom" ? 0 : -1}
className={`agent-dialog-tab${stepZeroTab === "custom" ? " active" : ""}`}
onClick={() => setStepZeroTab("custom")}
onClick={() => {
setStepZeroTab("custom");
// FNXC:StandingInstructionsTemplate 2026-07-13-12:35:
// Blank custom creates seed the six-section standing instructions skeleton so new permanent agents get structure without rewriting presets or existing agents.
setInstructionsText((prev) => withStandingInstructionsTemplate(prev));
}}
data-testid="agent-dialog-tab-custom"
>
{t("agents.tabCustom", "Custom agent")}

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import {
STANDING_INSTRUCTIONS_TEMPLATE,
hasStandingInstructionsStructure,
isStandingInstructionsEmpty,
withStandingInstructionsTemplate,
} from "../agent-presets/standing-instructions-template";
describe("standing-instructions-template", () => {
it("exports a six-section skeleton", () => {
expect(hasStandingInstructionsStructure(STANDING_INSTRUCTIONS_TEMPLATE)).toBe(true);
expect(STANDING_INSTRUCTIONS_TEMPLATE).toContain("## Description");
expect(STANDING_INSTRUCTIONS_TEMPLATE).toContain("## Collaboration & Escalation");
});
it("treats blank/whitespace as empty", () => {
expect(isStandingInstructionsEmpty("")).toBe(true);
expect(isStandingInstructionsEmpty(" \n")).toBe(true);
expect(isStandingInstructionsEmpty("Keep going")).toBe(false);
});
it("seeds template only when empty", () => {
expect(withStandingInstructionsTemplate("")).toBe(STANDING_INSTRUCTIONS_TEMPLATE);
expect(withStandingInstructionsTemplate("Already filled")).toBe("Already filled");
});
it("preserves trailing newline on non-empty body and strips other trailing whitespace", () => {
expect(withStandingInstructionsTemplate("Already filled\n")).toBe("Already filled\n");
expect(withStandingInstructionsTemplate("Already filled\n\n")).toBe("Already filled\n");
expect(withStandingInstructionsTemplate("Already filled ")).toBe("Already filled");
});
});

View File

@@ -0,0 +1,60 @@
/*
FNXC:StandingInstructionsTemplate 2026-07-13-12:30:
Permanent agents need a consistent standing-instructions skeleton (Description / Expertise / Priorities / Boundaries / Communication / Collaboration).
Seed instructionsText only for blank custom creates and empty-state insert — never silently rewrite existing agents.
*/
/** Six-section standing instructions skeleton for permanent agents. */
export const STANDING_INSTRUCTIONS_TEMPLATE = `## Description
## Expertise
## Priorities
## Boundaries
## Communication
## Collaboration & Escalation
`;
const SECTION_HEADINGS = [
"## Description",
"## Expertise",
"## Priorities",
"## Boundaries",
"## Communication",
"## Collaboration & Escalation",
] as const;
/** True when instructions are empty or whitespace-only. */
export function isStandingInstructionsEmpty(value: string | null | undefined): boolean {
return !value || value.trim().length === 0;
}
/**
* Return the template when body is empty; otherwise return the existing body unchanged.
*/
export function withStandingInstructionsTemplate(body?: string | null): string {
if (isStandingInstructionsEmpty(body)) {
return STANDING_INSTRUCTIONS_TEMPLATE;
}
/*
FNXC:StandingInstructionsTemplate 2026-07-14-12:00:
Preserve a trailing newline when the original non-empty body ended with one.
trimEnd strips trailing whitespace first; re-append "\n" only if body had a final newline.
*/
return body!.trimEnd() + (body!.endsWith("\n") ? "\n" : "");
}
/** Whether text already contains the six section headings. */
export function hasStandingInstructionsStructure(value: string | null | undefined): boolean {
if (!value) return false;
return SECTION_HEADINGS.every((heading) => value.includes(heading));
}

View File

@@ -82,6 +82,8 @@ Rules:
- thinkingLevel must be off|minimal|low|medium|high
- maxTurns must be a positive integer
- Use instructionsText for starter operating guidance/playbook content; do not create a separate playbook field
- Prefer structuring instructionsText with these markdown sections when drafting: ## Description, ## Expertise, ## Priorities, ## Boundaries, ## Communication, ## Collaboration & Escalation
- Freeform instructionsText is still acceptable for compatibility; sectioned structure is preferred for new agents
- modelHint and runtimeHint are optional draft suggestions only (not final runtime selection)
- heartbeatProcedurePath, heartbeatIntervalMs, and heartbeatEnabled are optional draft hints only.`;

View File

@@ -14,6 +14,7 @@ exports[`agent-heartbeat procedure templates > keeps lite no-task procedure stab
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.
Scoped-wake: one clear message action → act and exit.
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:
@@ -23,11 +24,11 @@ exports[`agent-heartbeat procedure templates > keeps lite no-task procedure stab
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.
durable memory. Never retry checkout/claim conflicts.
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.
8. **Disposition + exit** — acted / delegated / no-op with reason, then
fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (a created task, delegation,
message reply, memory append, or explicit "no-op with reason") is a bug. Do
@@ -48,10 +49,11 @@ exports[`agent-heartbeat procedure templates > keeps lite task procedure stable
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.
Scoped-wake: message/comment/task_assigned with one clear action → act and exit.
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.
plan is still correct. Blocked dedup: same blocker + no new context → no-op.
5. **Classify scope before acting** — label the next action as either:
- **In-scope execution:** directly advances the assigned task's current
acceptance criteria.
@@ -60,10 +62,12 @@ exports[`agent-heartbeat procedure templates > keeps lite task procedure stable
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.
Never retry checkout/claim conflicts.
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.
Note style: status line + done / remaining / next owner + FN-####.
8. **Final disposition** — acted with evidence / delegated / blocked with owner /
explicit no-op with reason — then call fn_heartbeat_done with a one-line summary.
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
@@ -76,9 +80,9 @@ exports[`agent-heartbeat procedure templates > keeps off no-task procedure stabl
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.
4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts.
5. **Persist progress** — use available ambient tools only.
6. **Exit** — call fn_heartbeat_done with a one-line summary.
6. **Disposition + exit** — acted / no-op with reason, then fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug."
`;
@@ -89,9 +93,9 @@ exports[`agent-heartbeat procedure templates > keeps off task procedure stable 1
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.
4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. Same-blocker no news → no-op with reason.
5. **Persist progress** — record the action via available task/memory tools.
6. **Exit** — call fn_heartbeat_done with a one-line summary.
6. **Disposition + exit** — acted / delegated / blocked / no-op with reason, then fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug."
`;
@@ -117,9 +121,13 @@ exports[`agent-heartbeat procedure templates > keeps strict no-task procedure st
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.
**Scoped-wake fast path:** message/comment with one clear ambient action →
act, disposition, exit without broad board thrash.
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.
If Wake Delta lists assigned open tasks while bind failed, treat them as
coordination inventory (unblock/reassign/delegate), not code work.
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).
@@ -127,10 +135,13 @@ exports[`agent-heartbeat procedure templates > keeps strict no-task procedure st
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.
durable memory. Never retry checkout/claim conflicts.
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
Note style when messaging or memory-appending: status + next owner.
8. **Final disposition checklist** — acted with evidence / follow-up created or
delegated / explicit no-op with reason.
9. **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,
@@ -159,6 +170,9 @@ exports[`agent-heartbeat procedure templates > keeps strict task procedure stabl
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.
**Scoped-wake fast path:** if the wake is a message, comment, or task_assigned
signal with one clear coordination action, take that action, complete the
disposition checklist, and exit — skip ambient board thrash.
4. **Classify the bound task** — if you have an assigned task, classify it as
exactly one of:
- **executor-class** — implementation work: writing code, tests,
@@ -172,19 +186,29 @@ exports[`agent-heartbeat procedure templates > keeps strict task procedure stabl
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.
already handled in step 2. **Blocked dedup:** if you already logged the
same blocker and Wake Delta shows no new context, do not re-chase — no-op.
- If the bound task is **coordination-class**, engage directly with the
bound task.
Treat any multi-assign list in Wake Delta as coordination inventory only —
not an implement-from-heartbeat queue.
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.
Never retry checkout/claim when another agent holds the lease.
6. **Persist progress** — fn_task_log for observations, fn_task_document_write
for durable findings, status updates only when the work warrants it.
Progress note style: short status line + done / remaining / next owner + FN-####.
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
8. **Final disposition checklist** — choose exactly one before exit:
- acted with evidence (log, document, message, delegation, or status change)
- follow-up created or delegated with clear owner
- blocked with named owner/action (or structured blockedBy)
- explicit no-op with reason (including blocked dedup / empty wake)
9. **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

View File

@@ -105,6 +105,9 @@ describe("executeHeartbeat", () => {
} as unknown as TaskDetail),
selectNextTaskForAgent: vi.fn().mockResolvedValue(null),
listTasks: vi.fn().mockResolvedValue([]),
// FNXC:WakeDeltaMultiAssign 2026-07-13-12:45:
// executeHeartbeat loads assigned inventory for Wake Delta; default empty so existing tests stay no-op.
getTasksByAssignedAgent: vi.fn().mockResolvedValue([]),
createTask: vi.fn().mockResolvedValue({
id: "FN-002",
description: "Created task",
@@ -1784,6 +1787,75 @@ describe("executeHeartbeat", () => {
expect(executionPrompt).toContain("autonomous heartbeat run");
});
it("Wake Delta includes multi-assign coordination inventory for sibling assignments", async () => {
const now = new Date().toISOString();
const getTasksByAssignedAgent = vi.fn().mockResolvedValue([
{
id: "FN-001",
column: "in-progress",
title: "Bound task",
dependencies: [],
createdAt: now,
updatedAt: now,
},
{
id: "FN-220",
column: "todo",
title: "Sibling todo",
dependencies: [],
createdAt: now,
updatedAt: now,
},
]);
mockTaskStore = createMockTaskStore({ getTasksByAssignedAgent });
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
expect(executionPrompt).toContain("- assigned task: FN-001");
expect(executionPrompt).toContain("coordination inventory");
expect(executionPrompt).toContain("FN-001");
expect(executionPrompt).toContain("FN-220");
expect(executionPrompt).toContain("(bound)");
expect(getTasksByAssignedAgent).toHaveBeenCalledWith("agent-001", { excludeArchived: true });
});
it("exits checkout_conflict without starting a session when lease is held by another agent", async () => {
const now = new Date().toISOString();
mockTaskStore = createMockTaskStore({
getTask: vi.fn().mockResolvedValue({
id: "FN-001",
title: "Leased elsewhere",
description: "desc",
prompt: "",
steps: [],
column: "todo",
checkedOutBy: "agent-other",
dependencies: [],
log: [],
attachments: [],
createdAt: now,
updatedAt: now,
} as unknown as TaskDetail),
});
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({
reason: "checkout_conflict",
taskId: "FN-001",
checkedOutBy: "agent-other",
});
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("identity agent without task gets soul in system prompt", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a CEO who prioritizes high-impact work" });
const mockSession = createMockAgentSession();
@@ -2985,6 +3057,24 @@ describe("executeHeartbeat", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("## Memory Boundaries");
});
it("both system prompts include durable critical rules that survive custom HEARTBEAT.md", () => {
for (const prompt of [HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT]) {
expect(prompt).toContain("## Critical Rules");
expect(prompt).toContain("Do NOT implement task body work");
expect(prompt).toContain("Checkout/claim conflict");
expect(prompt).toContain("Blocked-task dedup");
expect(prompt).toContain("coordination inventory");
}
});
it("strict procedures include disposition checklist and scoped-wake language", () => {
expect(HEARTBEAT_PROCEDURE).toContain("Final disposition checklist");
expect(HEARTBEAT_PROCEDURE).toContain("Scoped-wake fast path");
expect(HEARTBEAT_PROCEDURE).toContain("Blocked dedup");
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("Final disposition checklist");
expect(HEARTBEAT_NO_TASK_PROCEDURE).not.toContain("fn_task_log");
});
it("both prompts instruct replies to include reply_to_message_id", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("reply_to_message_id");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("reply_to_message_id");

View File

@@ -19,7 +19,21 @@
import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, RunMutationContext, Settings, AgentConfigRevision, ReflectionStore, ChatStore, ChatRoom, ChatRoomMessage, AgentMemoryInclusionMode } from "@fusion/core";
import { AutoClaimSnapshotManager, resolveFreshAutoClaimCandidates, type AutoClaimCandidate } from "./auto-claim-snapshot.js";
import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask, evaluateImplementationTaskBind, resolvePersistAgentThinkingLog, resolveAgentMemoryInclusionMode, FUSION_RUNTIME_SELF_AWARENESS, AWAITING_APPROVAL_PAUSE_REASON } from "@fusion/core";
import {
ApprovalRequestStore,
buildExecutionMemoryInstructions,
isEphemeralAgent,
hasAgentIdentity,
resolveEffectiveAgentPermissionPolicy,
canAgentTakeImplementationTask,
evaluateImplementationTaskBind,
resolvePersistAgentThinkingLog,
resolveAgentMemoryInclusionMode,
FUSION_RUNTIME_SELF_AWARENESS,
AWAITING_APPROVAL_PAUSE_REASON,
rankAssignedTasksForWakeDelta,
formatAssignedTasksWakeDeltaSection,
} from "@fusion/core";
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
import { Type, type Static } from "@earendil-works/pi-ai";
import { createHash } from "node:crypto";
@@ -417,7 +431,28 @@ export function taskRelevanceScore(agent: Agent, task: RelevanceScorableTask): n
FNXC:AgentPauseGuidance 2026-06-28-00:05:
Coordination agents must not pause tasks to handle failures or blockers because a pause suppresses scheduler and self-healing recovery.
Only use task pause when the user explicitly requests manual control; otherwise log blockers, route follow-up work, or let the task surface as failed.
FNXC:HeartbeatCriticalRules 2026-07-13-12:00:
Permanent-agent heartbeats need durable operating law that survives custom HEARTBEAT.md overrides.
Critical rules live in the system prompt (not only procedure text) so checkout no-retry, blocked dedup, one-action, and implement-from-executor stay in force for every wake.
*/
/**
* Always-on critical rules for permanent-agent heartbeats.
* Injected into both task-scoped and no-task system prompts so custom HEARTBEAT.md cannot erase them.
*/
export const HEARTBEAT_CRITICAL_RULES = `## Critical Rules
- ONE concrete coordination action per tick, then call fn_heartbeat_done (or an explicit no-op with reason).
- Do NOT implement task body work (code, tests, commits, multi-step coding) in a heartbeat — that is the executor path.
- Do NOT call fn_task_pause for failures or blockers; pause is only for explicit user manual control.
- Checkout/claim conflict: do NOT retry. Treat as terminal for this tick; pick other work or exit.
- Blocked-task dedup: if the same blocker is already logged and Wake Delta shows no new context, do not re-chase or re-comment — no-op with reason.
- Before fn_task_create, scan open tasks; do not create duplicates of work already covered.
- Prefer create/delegate to another agent over asking a human when an agent can do the work.
- Escalate via reports-to / chain of command when stuck after a concrete chase attempt.
- Progress notes: short status line + done / remaining / next owner + task ids (FN-####).
- Your assigned tasks list (when present) is coordination inventory, not an implement-from-heartbeat queue.`;
export const HEARTBEAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS}
You are a heartbeat agent running in a short execution window.
@@ -430,6 +465,8 @@ execution path handled by the executor. Do NOT do task body work or implementati
Your purpose is to keep momentum through coordination: surface blockers, respond to messages, manage memory,
delegate, and route work to the right place. Think in single-pass interventions, not coding sessions.
${HEARTBEAT_CRITICAL_RULES}
Your job:
1. Check your assigned task context — review its state, blockedBy field, and any new comments.
2. Do ONE useful coordination action.
@@ -480,8 +517,9 @@ Prefer fn_delegate_task when immediate ownership by a specific agent materially
## Common Patterns
- **Blocked task:** log the concrete blocker, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed; do not pause it unless the user explicitly requested manual control.
- **Blocked task:** log the concrete blocker once, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed; do not pause it unless the user explicitly requested manual control. If you already logged the same blocker and nothing new arrived, no-op with reason.
- **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause; do not use fn_task_pause as failure handling.
- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick.
- **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log.
- **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation.
- **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies.
@@ -535,6 +573,8 @@ You are a heartbeat agent running in a short execution window with no task assig
You are an ambient coordinator. You scan signals (messages, memory, board state), make one high-leverage move, and hand execution to the right workflow.
You are not expected to implement large code changes in no-task mode.
${HEARTBEAT_CRITICAL_RULES}
Your job:
1. Review your context — check messages, memory, and project state.
2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.
@@ -580,7 +620,8 @@ If unsure who should do the work, prefer fn_task_create and let scheduler routin
## Common Patterns
- **Failed or blocked task:** do NOT call fn_task_pause to handle the failure or blocker. Pausing is reserved for explicit user requests for manual control; instead surface the blocker through available task or message context, create/delegate follow-up work, or let the task surface as failed.
- **Failed or blocked task:** do NOT call fn_task_pause to handle the failure or blocker. Pausing is reserved for explicit user requests for manual control; instead surface the blocker through available task or message context, create/delegate follow-up work, or let the task surface as failed. If the same blocker was already chased and Wake Delta has no new context, no-op with reason.
- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick.
- **Unowned risk discovered:** create one focused task with concrete acceptance language.
- **Known specialist needed:** list agents, then delegate to matching role/capability.
- **Repeated confusion across runs:** append a concise memory entry so future agents avoid the same mistake.
@@ -645,6 +686,9 @@ export const HEARTBEAT_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tic
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.
**Scoped-wake fast path:** if the wake is a message, comment, or task_assigned
signal with one clear coordination action, take that action, complete the
disposition checklist, and exit — skip ambient board thrash.
4. **Classify the bound task** — if you have an assigned task, classify it as
exactly one of:
- **executor-class** — implementation work: writing code, tests,
@@ -658,19 +702,29 @@ export const HEARTBEAT_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tic
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.
already handled in step 2. **Blocked dedup:** if you already logged the
same blocker and Wake Delta shows no new context, do not re-chase — no-op.
- If the bound task is **coordination-class**, engage directly with the
bound task.
Treat any multi-assign list in Wake Delta as coordination inventory only —
not an implement-from-heartbeat queue.
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.
Never retry checkout/claim when another agent holds the lease.
6. **Persist progress** — fn_task_log for observations, fn_task_document_write
for durable findings, status updates only when the work warrants it.
Progress note style: short status line + done / remaining / next owner + FN-####.
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
8. **Final disposition checklist** — choose exactly one before exit:
- acted with evidence (log, document, message, delegation, or status change)
- follow-up created or delegated with clear owner
- blocked with named owner/action (or structured blockedBy)
- explicit no-op with reason (including blocked dedup / empty wake)
9. **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
@@ -690,10 +744,11 @@ export const HEARTBEAT_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick,
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.
Scoped-wake: message/comment/task_assigned with one clear action → act and exit.
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.
plan is still correct. Blocked dedup: same blocker + no new context → no-op.
5. **Classify scope before acting** — label the next action as either:
- **In-scope execution:** directly advances the assigned task's current
acceptance criteria.
@@ -702,10 +757,12 @@ export const HEARTBEAT_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick,
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.
Never retry checkout/claim conflicts.
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.
Note style: status line + done / remaining / next owner + FN-####.
8. **Final disposition** — acted with evidence / delegated / blocked with owner /
explicit no-op with reason — then call fn_heartbeat_done with a one-line summary.
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
@@ -716,9 +773,9 @@ export const HEARTBEAT_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick,
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.
4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. Same-blocker no news → no-op with reason.
5. **Persist progress** — record the action via available task/memory tools.
6. **Exit** — call fn_heartbeat_done with a one-line summary.
6. **Disposition + exit** — acted / delegated / blocked / no-op with reason, then fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`;
@@ -749,9 +806,13 @@ export const HEARTBEAT_NO_TASK_PROCEDURE_STRICT = `## Heartbeat Procedure (run e
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.
**Scoped-wake fast path:** message/comment with one clear ambient action →
act, disposition, exit without broad board thrash.
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.
If Wake Delta lists assigned open tasks while bind failed, treat them as
coordination inventory (unblock/reassign/delegate), not code work.
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).
@@ -759,10 +820,13 @@ export const HEARTBEAT_NO_TASK_PROCEDURE_STRICT = `## Heartbeat Procedure (run e
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.
durable memory. Never retry checkout/claim conflicts.
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
Note style when messaging or memory-appending: status + next owner.
8. **Final disposition checklist** — acted with evidence / follow-up created or
delegated / explicit no-op with reason.
9. **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,
@@ -782,6 +846,7 @@ export const HEARTBEAT_NO_TASK_PROCEDURE_LITE = `## Heartbeat Procedure (run eve
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.
Scoped-wake: one clear message action → act and exit.
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:
@@ -791,11 +856,11 @@ export const HEARTBEAT_NO_TASK_PROCEDURE_LITE = `## Heartbeat Procedure (run eve
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.
durable memory. Never retry checkout/claim conflicts.
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.
8. **Disposition + exit** — acted / delegated / no-op with reason, then
fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (a created task, delegation,
message reply, memory append, or explicit "no-op with reason") is a bug. Do
@@ -806,9 +871,9 @@ export const HEARTBEAT_NO_TASK_PROCEDURE_OFF = `## Heartbeat Procedure (run ever
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.
4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts.
5. **Persist progress** — use available ambient tools only.
6. **Exit** — call fn_heartbeat_done with a one-line summary.
6. **Disposition + exit** — acted / no-op with reason, then fn_heartbeat_done with a one-line summary.
Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`;
@@ -3169,6 +3234,35 @@ export class HeartbeatMonitor {
: "no triggering-message metadata"}`)
: null;
/*
FNXC:WakeDeltaMultiAssign 2026-07-13-12:20:
Inject compact ranked multi-assignment inventory into Wake Delta so permanent agents see siblings beyond singular agent.taskId.
Coordination inventory only — not an implement-from-heartbeat queue. Cap 8; fully unactionable blocked stay count-only.
FNXC:WakeDeltaMultiAssign 2026-07-14-12:00:
Skip getTasksByAssignedAgent for ephemeral agents — multi-assign inventory is permanent-agent coordination only.
*/
let multiAssignWakeDeltaLines: string[] = [];
if (!isAgentEphemeral && this.taskStore && typeof this.taskStore.getTasksByAssignedAgent === "function") {
try {
const assignedOpen = await this.taskStore.getTasksByAssignedAgent(agentId, { excludeArchived: true });
const ranked = rankAssignedTasksForWakeDelta(assignedOpen, {
agentId,
boundTaskId: isNoTaskRun ? null : taskId,
});
const section = formatAssignedTasksWakeDeltaSection(ranked, {
boundTaskId: isNoTaskRun ? null : taskId,
});
if (section) {
multiAssignWakeDeltaLines = section.split("\n");
}
} catch (err: unknown) {
heartbeatLog.warn(
`Failed to build multi-assign Wake Delta for ${agentId}: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
// Per-agent override of the default HEARTBEAT_PROCEDURE: if the agent
// configured a heartbeatProcedurePath pointing to a markdown file in
// the project, use that instead. Reloaded fresh each tick (matches the
@@ -3271,6 +3365,7 @@ export class HeartbeatMonitor {
`- source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
`- wake reason: ${wakeReason}`,
`- assigned task: none`,
...multiAssignWakeDeltaLines,
wakeInboxSnapshotLine,
...(wakeTriggerSourceLine ? [wakeTriggerSourceLine] : []),
`- pending messages: ${pendingMessages.length}`,
@@ -3377,6 +3472,7 @@ export class HeartbeatMonitor {
`- source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
`- wake reason: ${wakeReason}`,
`- assigned task: ${taskId}`,
...multiAssignWakeDeltaLines,
wakeInboxSnapshotLine,
...(wakeTriggerSourceLine ? [wakeTriggerSourceLine] : []),
`- pending messages: ${pendingMessages.length}`,