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:
18
CONCEPTS.md
18
CONCEPTS.md
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
71
docs/agents-playbooks.md
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
99
packages/core/src/__tests__/assigned-task-ranking.test.ts
Normal file
99
packages/core/src/__tests__/assigned-task-ranking.test.ts
Normal 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)");
|
||||
});
|
||||
});
|
||||
196
packages/core/src/assigned-task-ranking.ts
Normal file
196
packages/core/src/assigned-task-ranking.ts
Normal 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");
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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.`;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user