From dff864e098f7eb710d89cc2189f641f825954d26 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 14 Jul 2026 08:23:11 -0700 Subject: [PATCH] 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). ## 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. --- CONCEPTS.md | 18 + docs/README.md | 2 +- docs/agent-paperclip-gap-analysis.md | 283 --------- docs/agents-playbooks.md | 71 +++ docs/agents.md | 10 + ...anent-agent-heartbeat-instructions-plan.md | 551 ++++++++++++++++++ .../__tests__/assigned-task-ranking.test.ts | 99 ++++ packages/core/src/assigned-task-ranking.ts | 196 +++++++ packages/core/src/index.gate.ts | 11 + packages/core/src/index.ts | 11 + .../app/components/AgentDetailView.tsx | 16 + .../app/components/NewAgentDialog.tsx | 13 +- .../standing-instructions-template.test.ts | 32 + .../standing-instructions-template.ts | 60 ++ packages/dashboard/src/agent-onboarding.ts | 2 + .../agent-heartbeat-procedures.test.ts.snap | 52 +- .../src/__tests__/heartbeat-executor.test.ts | 90 +++ packages/engine/src/agent-heartbeat.ts | 130 ++++- 18 files changed, 1330 insertions(+), 317 deletions(-) delete mode 100644 docs/agent-paperclip-gap-analysis.md create mode 100644 docs/agents-playbooks.md create mode 100644 docs/plans/2026-07-12-001-feat-permanent-agent-heartbeat-instructions-plan.md create mode 100644 packages/core/src/__tests__/assigned-task-ranking.test.ts create mode 100644 packages/core/src/assigned-task-ranking.ts create mode 100644 packages/dashboard/app/components/__tests__/standing-instructions-template.test.ts create mode 100644 packages/dashboard/app/components/agent-presets/standing-instructions-template.ts diff --git a/CONCEPTS.md b/CONCEPTS.md index fd4bfda57b..ac6c5a59f9 100644 --- a/CONCEPTS.md +++ b/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. diff --git a/docs/README.md b/docs/README.md index 28d5bfba19..bc66ff24df 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/docs/agent-paperclip-gap-analysis.md b/docs/agent-paperclip-gap-analysis.md deleted file mode 100644 index f9a66b1a75..0000000000 --- a/docs/agent-paperclip-gap-analysis.md +++ /dev/null @@ -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. diff --git a/docs/agents-playbooks.md b/docs/agents-playbooks.md new file mode 100644 index 0000000000..b7bfb41dc5 --- /dev/null +++ b/docs/agents-playbooks.md @@ -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) diff --git a/docs/agents.md b/docs/agents.md index b651a767fb..c48bbf314e 100644 --- a/docs/agents.md +++ b/docs/agents.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: diff --git a/docs/plans/2026-07-12-001-feat-permanent-agent-heartbeat-instructions-plan.md b/docs/plans/2026-07-12-001-feat-permanent-agent-heartbeat-instructions-plan.md new file mode 100644 index 0000000000..e71920b30b --- /dev/null +++ b/docs/plans/2026-07-12-001-feat-permanent-agent-heartbeat-instructions-plan.md @@ -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. diff --git a/packages/core/src/__tests__/assigned-task-ranking.test.ts b/packages/core/src/__tests__/assigned-task-ranking.test.ts new file mode 100644 index 0000000000..de998da353 --- /dev/null +++ b/packages/core/src/__tests__/assigned-task-ranking.test.ts @@ -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 & Pick): 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)"); + }); +}); diff --git a/packages/core/src/assigned-task-ranking.ts b/packages/core/src/assigned-task-ranking.ts new file mode 100644 index 0000000000..cea072314a --- /dev/null +++ b/packages/core/src/assigned-task-ranking.ts @@ -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 = { + 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"); +} diff --git a/packages/core/src/index.gate.ts b/packages/core/src/index.gate.ts index f11be2bf47..eb4d019ce3 100644 --- a/packages/core/src/index.gate.ts +++ b/packages/core/src/index.gate.ts @@ -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"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a7a719996..f518794bb9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 { diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx index edd2e495a5..0039f0720c 100644 --- a/packages/dashboard/app/components/AgentDetailView.tsx +++ b/packages/dashboard/app/components/AgentDetailView.tsx @@ -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({
+ {!instructionsText.trim() && !showPreview && ( + + )}