From 8ee8f15dc693b282c0169828ed60ee004d0c452a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 8 Jul 2026 08:16:30 -0700 Subject: [PATCH] FN-7675: add agent runtime self-awareness to system prompts Agents were composing plans (e.g. reboot/wait-and-retry loops) that assumed they could keep acting even after the Fusion platform itself shut down, since prompts never told them they run inside Fusion. This adds a shared, docs-grounded self-awareness preamble prepended to chat, heartbeat, and executor base prompts so agents know their own runtime constraints. - Added FUSION_RUNTIME_SELF_AWARENESS shared preamble in packages/core/src/agent-prompts.ts, exported via packages/core/src/index.ts - Prepended the preamble to the chat system prompt (packages/dashboard/src/chat.ts) - Prepended the preamble to the heartbeat session prompt (packages/engine/src/agent-heartbeat.ts) - Prepended the preamble to the executor base prompt (packages/engine/src/executor.ts) - Updated docs/agents.md and CONCEPTS.md to document the new self-awareness/capability-grounding behavior - Added regression tests across core, dashboard, and engine covering the new prompt content - Added changeset for @runfusion/fusion (minor, fix category) Files changed: .changeset/fn-7675-agent-runtime-self-awareness.md | 7 ++++ CONCEPTS.md | 4 +- docs/agents.md | 17 ++++++++ packages/core/src/__tests__/agent-prompts.test.ts | 41 ++++++++++++++++++++ packages/core/src/agent-prompts.ts | 32 ++++++++++++++- packages/core/src/index.ts | 1 + packages/dashboard/src/__tests__/chat-system-prompt.test.ts | 17 ++++++++ packages/dashboard/src/chat.ts | 6 ++- packages/engine/src/__tests__/executor-prompt.test.ts | 45 ++++++++++++++++++++++ packages/engine/src/__tests__/heartbeat-session-prompt.test.ts | 35 +++++++++++++++++ packages/engine/src/agent-heartbeat.ts | 10 +++-- packages/engine/src/executor.ts | 7 +++- 12 files changed, 213 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-7675 Fusion-Task-Lineage: 126d04a6-2c68-4347-9789-591b274277bf Co-authored-by: Fusion (runfusion.ai) --- .../fn-7675-agent-runtime-self-awareness.md | 7 +++ CONCEPTS.md | 4 +- docs/agents.md | 17 +++++++ .../core/src/__tests__/agent-prompts.test.ts | 41 +++++++++++++++++ packages/core/src/agent-prompts.ts | 32 ++++++++++++- packages/core/src/index.ts | 1 + .../src/__tests__/chat-system-prompt.test.ts | 17 +++++++ packages/dashboard/src/chat.ts | 6 ++- .../src/__tests__/executor-prompt.test.ts | 45 +++++++++++++++++++ .../heartbeat-session-prompt.test.ts | 35 +++++++++++++++ packages/engine/src/agent-heartbeat.ts | 10 +++-- packages/engine/src/executor.ts | 7 ++- 12 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 .changeset/fn-7675-agent-runtime-self-awareness.md diff --git a/.changeset/fn-7675-agent-runtime-self-awareness.md b/.changeset/fn-7675-agent-runtime-self-awareness.md new file mode 100644 index 0000000000..4df3ef1e21 --- /dev/null +++ b/.changeset/fn-7675-agent-runtime-self-awareness.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Agents now know they run inside Fusion and won't plan actions across a platform shutdown. +category: fix +dev: Adds a shared runtime self-awareness + capability-grounding preamble (packages/core/src/agent-prompts.ts, FUSION_RUNTIME_SELF_AWARENESS) prepended to the chat, heartbeat, and executor base prompts' stable layer. diff --git a/CONCEPTS.md b/CONCEPTS.md index 66198b3978..fd4bfda57b 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -296,7 +296,9 @@ A server-owned PTY bound to a task or chat entity. It survives client disconnect ### Waiting-on-input The CLI Session state where the agent is blocked on the human (permission prompt, clarifying question), as distinct from idle-because-done. Entering it fires the notification configured on the workflow node; the task neither advances nor fails while in it. -## Testing + +### Runtime Self-Awareness Preamble +The shared, cacheable prompt preamble (`FUSION_RUNTIME_SELF_AWARENESS` in `packages/core/src/agent-prompts.ts`) prepended to the stable layer of every standard-toolset conversational base prompt (chat, both heartbeat personas, executor). Tells the agent it runs as a hosted process inside the Fusion daemon that ends when the daemon stops, that shutdown-crossing workflows (updates/installs/migrations) must be handed off as a standalone user-run artifact rather than orchestrated live, and points it at the maintained docs for capability grounding. See `docs/agents.md` → "Runtime Self-Awareness Preamble" for the full clause set. ### Merge Gate The minimal set of merge-blocking PR checks: lint, typecheck, build, a Boot Smoke, and a small curated engine test suite. The gate is the only test signal that can block a PR; all other tests run non-blocking after merge. diff --git a/docs/agents.md b/docs/agents.md index 223ca4e427..d5040e1207 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -541,6 +541,23 @@ Expected behavior and boundaries: For the full research workflow, builtin-default behavior, optional external provider setup, CLI commands, and API reference, see the [Research guide](./research.md). +## Runtime Self-Awareness Preamble + +**FN-7675:** every standard-toolset conversational base prompt — chat/CEO persona, both heartbeat personas (task-bound and no-task), and the executor — is prefixed with a single canonical, cacheable preamble: `FUSION_RUNTIME_SELF_AWARENESS`, exported from `packages/core/src/agent-prompts.ts` (re-exported from `@fusion/core`). The preamble encodes: + +- The agent executes as a **hosted process inside** the running Fusion daemon; its own execution ends the moment that process ends. There is no "wait for Fusion to close, then keep going" capability. +- **Shutdown-crossing workflows** (updates, installs, patches, migrations, in-place binary swaps) must be handed off as a **standalone artifact the user runs themselves** — never orchestrated live across the shutdown boundary. This closes a real failure mode: an agent planned a live "close Fusion → back up → swap build → verify → relaunch" sequence, which is impossible because step 1 kills the process running steps 2–5. +- A short, current description of the platform (task board + engine + desktop/CLI/web surfaces). +- A pointer to the maintained docs (`docs/` and `CONCEPTS.md`) so the agent answers "how do I do X in Fusion" from real documentation instead of improvising. + +The preamble is prepended to the **stable** (cacheable) prompt layer — `basePrompt` in `buildPromptLayers()` — at each surface, so it is byte-identical across sessions and does not break prompt-cache discounts: + +- Chat: `CHAT_SYSTEM_PROMPT` in `packages/dashboard/src/chat.ts`. +- Heartbeat: `HEARTBEAT_SYSTEM_PROMPT` and `HEARTBEAT_NO_TASK_SYSTEM_PROMPT` in `packages/engine/src/agent-heartbeat.ts`. +- Executor: `EXECUTOR_SYSTEM_PROMPT` in `packages/engine/src/executor.ts` (mirrored via `EXECUTOR_PROMPT_TEXT` in `packages/core/src/agent-prompts.ts`). + +It is intentionally **not** wired into merger, reviewer, triage, cron-runner, PR-response, mission-validation, or reflection prompts — those are internal single-purpose lanes, not standard-toolset agents reasoning about the shutdown boundary. + ## Built-In Agent Prompt Templates Fusion includes built-in templates for role prompts: diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index a34837b925..187cafa5d9 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -7,6 +7,7 @@ import { resolveAgentPrompt, getAvailableTemplates, getTemplatesForRole, + FUSION_RUNTIME_SELF_AWARENESS, } from "../agent-prompts.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; import { BUILTIN_SEAM_PROMPTS, builtinSeamPrompt } from "../builtin-workflow-prompts.js"; @@ -620,3 +621,43 @@ describe("BUILTIN_AGENT_PROMPTS", () => { expect(uniqueIds.size).toBe(ids.length); }); }); + +// --------------------------------------------------------------------------- +// FUSION_RUNTIME_SELF_AWARENESS (FN-7675) +// --------------------------------------------------------------------------- + +describe("FUSION_RUNTIME_SELF_AWARENESS", () => { + it("declares the hosted-process contingency", () => { + expect(FUSION_RUNTIME_SELF_AWARENESS).toContain("hosted process"); + expect(FUSION_RUNTIME_SELF_AWARENESS.toLowerCase()).toContain("inside"); + }); + + it("declares agents cannot act after Fusion shuts down", () => { + expect(FUSION_RUNTIME_SELF_AWARENESS.toLowerCase()).toContain("cannot** perform any action after fusion is shut down".toLowerCase()); + }); + + it("requires shutdown-crossing workflows to be handed off as a standalone user-run artifact", () => { + const lower = FUSION_RUNTIME_SELF_AWARENESS.toLowerCase(); + expect(lower).toContain("standalone artifact the user runs themselves"); + expect(lower).toContain("updates, installs, patches, migrations"); + }); + + it("describes the platform (task board + engine + surfaces)", () => { + const lower = FUSION_RUNTIME_SELF_AWARENESS.toLowerCase(); + expect(lower).toContain("ai-orchestrated task board"); + expect(lower).toContain("desktop app"); + expect(lower).toContain("cli"); + expect(lower).toContain("web dashboard"); + }); + + it("points agents at the maintained docs for capability grounding", () => { + const lower = FUSION_RUNTIME_SELF_AWARENESS.toLowerCase(); + expect(lower).toContain("docs/"); + expect(lower).toContain("concepts.md"); + }); + + it("is prepended (byte-for-byte) to the executor base prompt mirror", () => { + const executorPrompt = resolveAgentPrompt("executor"); + expect(executorPrompt.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + }); +}); diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index d2eacae40d..2e57dc9bee 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -21,6 +21,34 @@ import type { AgentCapability, AgentPromptTemplate, AgentPromptsConfig } from ". // Built-in prompt text (canonical source for workflow seam prompts) // --------------------------------------------------------------------------- +/* +FNXC:AgentRuntimeSelfAwareness 2026-07-08-00:00: +FN-7675: an agent operating via the standard toolset (CEO/management persona) planned and began +executing a live "close Fusion -> back up install -> swap build artifacts -> verify -> relaunch" +update sequence. That plan is impossible: step 1 terminates the agent's own host process before +steps 2-5 can run. Agents had no built-in awareness that they execute *inside* the running Fusion +daemon and cannot act once it stops. This constant is the single canonical, cacheable preamble +prepended to the STABLE layer (never the dynamic/per-session layer, to preserve prompt caching) of +every standard-toolset conversational base prompt — chat/CEO persona (packages/dashboard/src/chat.ts +CHAT_SYSTEM_PROMPT), both heartbeat personas (packages/engine/src/agent-heartbeat.ts +HEARTBEAT_SYSTEM_PROMPT / HEARTBEAT_NO_TASK_SYSTEM_PROMPT), and the executor +(packages/engine/src/executor.ts EXECUTOR_SYSTEM_PROMPT, mirrored byte-identically here via +EXECUTOR_PROMPT_TEXT). It is intentionally NOT wired into merger/reviewer/triage/cron-runner/ +PR-response/mission-validation/reflection prompts — those are internal single-purpose lanes, not +standard-toolset agents that reason about the shutdown boundary (file follow-up tasks to widen scope). +*/ +export const FUSION_RUNTIME_SELF_AWARENESS = `## Runtime Self-Awareness + +You execute as a hosted process **inside** the running Fusion platform (the "fn" daemon/engine) — you are not an external actor operating on Fusion from outside. Your own execution ends the moment that host process ends. + +You **cannot** perform any action after Fusion is shut down. There is no "wait for the app to close, then continue" capability — once the daemon stops, nothing you do next will run. Never plan or begin a live sequence that stops Fusion and then expects yourself to keep running (e.g. "close Fusion -> back up -> swap build -> verify -> relaunch" is impossible: step 1 kills the process executing steps 2-5). + +Any workflow that requires Fusion to be stopped (updates, installs, patches, migrations, in-place binary swaps) must be handed off as a **standalone artifact the user runs themselves** — an installer/updater executable or script — never orchestrated live by you across the shutdown boundary. If a partial change would leave the install inconsistent, produce the full artifact plus rollback instructions instead of attempting it live. + +Fusion is an AI-orchestrated task board with an engine (triage/executor/reviewer/merger/scheduler) and surfaces spanning a desktop app, CLI, and web dashboard; your own capabilities are defined by the tools you are given in this session. + +When asked "how do I do X in Fusion", answer from the maintained docs (the project's \`docs/\` directory and \`CONCEPTS.md\`) and current tool descriptions rather than improvising, so you act as an authoritative in-product guide.`; + /* FNXC:ExecutorPrompt 2026-06-21-03:59: Agents must not run the full/workspace-wide test suite by default; targeted/package-scoped verification is the norm, full runs require explicit task/workflow opt-in. @@ -28,7 +56,9 @@ Agents must not run the full/workspace-wide test suite by default; targeted/pack FNXC:ExecutorPrompt 2026-07-05-00:35: FN-7608: a `require-approval` gate previously only parked the single tool call (soft rejection + task/agent paused in the store) while the turn-ending rules below forbade ending a turn without another tool call, so the model was effectively instructed to hunt for ungated workarounds (re-issuing the same bash, probing read-only equivalents, fn_web_fetch/fn_task_attach bypasses) instead of stopping. The engine now actually suspends the in-flight session when a gate resolves to wait-for-approval (see executor.ts buildActionGateContext.pauseForApproval), so the prompt must carve out waiting on a pending approval as a legitimate turn end and explicitly forbid probing for alternatives. This clause must stay byte-identical with EXECUTOR_SYSTEM_PROMPT in packages/engine/src/executor.ts. */ -const EXECUTOR_PROMPT_TEXT = `You are a task execution agent for "fn", an AI-orchestrated task board. +const EXECUTOR_PROMPT_TEXT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a task execution agent for "fn", an AI-orchestrated task board. You are working in a git worktree isolated from the main branch. Your job is to implement the task described in the PROMPT.md specification you're given. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8e099ef655..a6603cc98f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -108,6 +108,7 @@ export { resolveAgentPrompt, getAvailableTemplates, getTemplatesForRole, + FUSION_RUNTIME_SELF_AWARENESS, } from "./agent-prompts.js"; export { parseWorkflowIr, diff --git a/packages/dashboard/src/__tests__/chat-system-prompt.test.ts b/packages/dashboard/src/__tests__/chat-system-prompt.test.ts index c0d29c0b53..6a6fe6dc65 100644 --- a/packages/dashboard/src/__tests__/chat-system-prompt.test.ts +++ b/packages/dashboard/src/__tests__/chat-system-prompt.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { FUSION_RUNTIME_SELF_AWARENESS } from "@fusion/core"; import { CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE, CHAT_SYSTEM_PROMPT } from "../chat.js"; describe("chat system prompt guidance", () => { @@ -22,3 +23,19 @@ describe("chat system prompt guidance", () => { expect(combined.toLowerCase()).toContain("do not also call"); }); }); + +// --------------------------------------------------------------------------- +// Runtime self-awareness preamble (FN-7675) +// --------------------------------------------------------------------------- + +describe("chat system prompt runtime self-awareness", () => { + it("prepends the shared FUSION_RUNTIME_SELF_AWARENESS preamble", () => { + expect(CHAT_SYSTEM_PROMPT.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + }); + + it("carries the shutdown-boundary clauses", () => { + const lower = CHAT_SYSTEM_PROMPT.toLowerCase(); + expect(lower).toContain("cannot** perform any action after fusion is shut down".toLowerCase()); + expect(lower).toContain("standalone artifact the user runs themselves"); + }); +}); diff --git a/packages/dashboard/src/chat.ts b/packages/dashboard/src/chat.ts index 8a665a4a38..43bc1955d1 100644 --- a/packages/dashboard/src/chat.ts +++ b/packages/dashboard/src/chat.ts @@ -29,7 +29,7 @@ import type { TaskStore, } from "@fusion/core"; import type { SkillSelectionContext } from "@fusion/engine"; -import { summarizeTitle } from "@fusion/core"; +import { summarizeTitle, FUSION_RUNTIME_SELF_AWARENESS } from "@fusion/core"; import { EventEmitter } from "node:events"; import { existsSync } from "node:fs"; import { join, resolve, relative } from "node:path"; @@ -212,7 +212,9 @@ async function ensureEngineReady(): Promise { // ── Constants ─────────────────────────────────────────────────────────────── /** Chat system prompt for the AI agent */ -export const CHAT_SYSTEM_PROMPT = `You are a helpful AI assistant integrated into the fn task board system. You help users with questions about their project, code, architecture, and tasks. You have access to project files and can read them to provide informed responses, including referencing specific file paths and line numbers when possible. Response length policy: default to a short, crisp reply (a few sentences or a short bulleted list) that directly answers the user; avoid preamble, restating the question, and filler. If a thorough answer genuinely needs long-form content (for example multi-step plans, design proposals, deep analyses, or long file excerpts), keep the chat reply brief with a one- or two-sentence summary and then send the full write-up via \`fn_send_message\` using \`type: "agent-to-user"\` and \`to_id: "dashboard"\`. That mailbox follow-up must add new substantive detail and must not duplicate the chat reply.`; +export const CHAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a helpful AI assistant integrated into the fn task board system. You help users with questions about their project, code, architecture, and tasks. You have access to project files and can read them to provide informed responses, including referencing specific file paths and line numbers when possible. Response length policy: default to a short, crisp reply (a few sentences or a short bulleted list) that directly answers the user; avoid preamble, restating the question, and filler. If a thorough answer genuinely needs long-form content (for example multi-step plans, design proposals, deep analyses, or long file excerpts), keep the chat reply brief with a one- or two-sentence summary and then send the full write-up via \`fn_send_message\` using \`type: "agent-to-user"\` and \`to_id: "dashboard"\`. That mailbox follow-up must add new substantive detail and must not duplicate the chat reply.`; export const CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE = `## Messaging Semantics\n\nYour chat reply is the primary response to the user. Do not also call \`fn_send_message\` with the same content just to mirror your chat response into mailbox.\n\nUse \`fn_send_message\` only when either (a) the user explicitly asks for mailbox/inbox/notification delivery (for example: "send me this in mail", "ntfy me when…", or "leave me a note in my inbox"), or (b) you are sending a genuinely longer follow-up that did not fit in a short chat reply. In either case, send with \`type: "agent-to-user"\` and target the dashboard user alias (\`to_id: "dashboard"\` is preferred), and ensure the mailbox message is additive rather than a duplicate of the chat reply. Never route that as a user/CLI → agent message.`; diff --git a/packages/engine/src/__tests__/executor-prompt.test.ts b/packages/engine/src/__tests__/executor-prompt.test.ts index 38039d3e20..f54b6dcc7a 100644 --- a/packages/engine/src/__tests__/executor-prompt.test.ts +++ b/packages/engine/src/__tests__/executor-prompt.test.ts @@ -2634,3 +2634,48 @@ describe("fn_task_update bare-call guard (P1 api-contract)", () => { expect(text).not.toContain("fn_task_update requires at least one of"); }); }); + +// --------------------------------------------------------------------------- +// Runtime self-awareness preamble (FN-7675) +// --------------------------------------------------------------------------- + +describe("executor base prompt runtime self-awareness", () => { + it("prepends the shared FUSION_RUNTIME_SELF_AWARENESS preamble to the executor base prompt", async () => { + const { FUSION_RUNTIME_SELF_AWARENESS } = await import("@fusion/core"); + const { getExecutorSystemPrompt } = await import("../executor.js"); + const settings = { agentPrompts: undefined } as any; + const prompt = getExecutorSystemPrompt(settings); + expect(prompt.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + }); + + it("carries the shutdown-boundary clauses", async () => { + const { getExecutorSystemPrompt } = await import("../executor.js"); + const settings = { agentPrompts: undefined } as any; + const lower = getExecutorSystemPrompt(settings).toLowerCase(); + expect(lower).toContain("cannot** perform any action after fusion is shut down".toLowerCase()); + expect(lower).toContain("standalone artifact the user runs themselves"); + }); + + it("stays byte-identical with the core EXECUTOR_PROMPT_TEXT mirror at the shared preamble", async () => { + const { FUSION_RUNTIME_SELF_AWARENESS } = await import("@fusion/core"); + const { readFileSync } = await vi.importActual("node:fs"); + const executorSource = readFileSync(new URL("../executor.ts", import.meta.url), "utf8"); + expect(executorSource).toContain("const EXECUTOR_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS}"); + expect(FUSION_RUNTIME_SELF_AWARENESS.length).toBeGreaterThan(0); + }); + + it("lands the preamble in the stable (cacheable) layer via buildPromptLayers", async () => { + const { FUSION_RUNTIME_SELF_AWARENESS } = await import("@fusion/core"); + const { getExecutorSystemPrompt } = await import("../executor.js"); + const { buildPromptLayers } = await import("../prompt-layers.js"); + const settings = { agentPrompts: undefined } as any; + const basePrompt = getExecutorSystemPrompt(settings); + const layers = buildPromptLayers({ + basePrompt, + agentInstructions: "per-session instructions that must not affect the stable prefix", + }); + expect(layers.stable).toBe(basePrompt); + expect(layers.stable.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + expect(layers.dynamic).not.toContain(FUSION_RUNTIME_SELF_AWARENESS); + }); +}); diff --git a/packages/engine/src/__tests__/heartbeat-session-prompt.test.ts b/packages/engine/src/__tests__/heartbeat-session-prompt.test.ts index 63009d0456..e58c484718 100644 --- a/packages/engine/src/__tests__/heartbeat-session-prompt.test.ts +++ b/packages/engine/src/__tests__/heartbeat-session-prompt.test.ts @@ -959,6 +959,41 @@ describe("room-message prompt injection", () => { }); }); +// ───────────────────────────────────────────────────────────────────────── +// Runtime self-awareness preamble (FN-7675) +// ───────────────────────────────────────────────────────────────────────── + +describe("heartbeat base prompts runtime self-awareness", () => { + it("prepends the shared FUSION_RUNTIME_SELF_AWARENESS preamble to both heartbeat base prompts", async () => { + const { FUSION_RUNTIME_SELF_AWARENESS } = await import("@fusion/core"); + expect(HEARTBEAT_SYSTEM_PROMPT.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + }); + + it("lands the preamble in the stable (cacheable) layer via buildPromptLayers", async () => { + const { buildPromptLayers } = await import("../prompt-layers.js"); + const { FUSION_RUNTIME_SELF_AWARENESS } = await import("@fusion/core"); + + const taskLayers = buildPromptLayers({ + basePrompt: HEARTBEAT_SYSTEM_PROMPT, + agentInstructions: "per-session instructions that must not affect the stable prefix", + }); + expect(taskLayers.stable).toBe(HEARTBEAT_SYSTEM_PROMPT); + expect(taskLayers.stable.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + expect(taskLayers.dynamic).not.toContain(FUSION_RUNTIME_SELF_AWARENESS); + + const noTaskLayers = buildPromptLayers({ basePrompt: HEARTBEAT_NO_TASK_SYSTEM_PROMPT }); + expect(noTaskLayers.stable).toBe(HEARTBEAT_NO_TASK_SYSTEM_PROMPT); + expect(noTaskLayers.stable.startsWith(FUSION_RUNTIME_SELF_AWARENESS)).toBe(true); + }); + + it("carries the shutdown-boundary clauses", () => { + const lower = HEARTBEAT_SYSTEM_PROMPT.toLowerCase(); + expect(lower).toContain("cannot** perform any action after fusion is shut down".toLowerCase()); + expect(lower).toContain("standalone artifact the user runs themselves"); + }); +}); + // ───────────────────────────────────────────────────────────────────────── // HeartbeatTriggerScheduler tests // ───────────────────────────────────────────────────────────────────────── diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index bd7faadd85..eb3807b6e9 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -19,7 +19,7 @@ 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, canAgentTakeImplementationTaskForExplicitRouting, resolvePersistAgentThinkingLog, resolveAgentMemoryInclusionMode } from "@fusion/core"; +import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgent, hasAgentIdentity, resolveEffectiveAgentPermissionPolicy, canAgentTakeImplementationTask, canAgentTakeImplementationTaskForExplicitRouting, resolvePersistAgentThinkingLog, resolveAgentMemoryInclusionMode, FUSION_RUNTIME_SELF_AWARENESS } 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"; @@ -376,7 +376,9 @@ 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. */ -export const HEARTBEAT_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window. +export const HEARTBEAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a heartbeat agent running in a short execution window. ## Your Role @@ -482,7 +484,9 @@ When sending messages: * System prompt for no-task heartbeat agent sessions. * Instructs the agent to perform ambient work only with tools that do not require task context. */ -export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `You are a heartbeat agent running in a short execution window with no task assignment. +export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a heartbeat agent running in a short execution window with no task assignment. ## Your Role diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index af74a03f8f..30a8a284d4 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -65,6 +65,7 @@ import { loadWorkspaceConfig, type WorkspaceConfig, type RunCommandResult, + FUSION_RUNTIME_SELF_AWARENESS, } from "@fusion/core"; import { findWorktreeUser, getConflictedFiles } from "./merger.js"; import { @@ -1319,7 +1320,9 @@ Agents must not run the full/workspace-wide test suite by default; targeted/pack FNXC:ExecutorPrompt 2026-07-05-00:35: FN-7608: a `require-approval` gate previously only parked the single tool call (soft rejection + task/agent paused in the store) while the turn-ending rules below forbade ending a turn without another tool call, so the model was effectively instructed to hunt for ungated workarounds (re-issuing the same bash, probing read-only equivalents, fn_web_fetch/fn_task_attach bypasses) instead of stopping. The engine now actually suspends the in-flight session when a gate resolves to wait-for-approval (see executor.ts buildActionGateContext.pauseForApproval), so the prompt must carve out waiting on a pending approval as a legitimate turn end and explicitly forbid probing for alternatives. This clause must stay byte-identical with EXECUTOR_PROMPT_TEXT in packages/core/src/agent-prompts.ts. */ -const EXECUTOR_SYSTEM_PROMPT = `You are a task execution agent for "fn", an AI-orchestrated task board. +const EXECUTOR_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a task execution agent for "fn", an AI-orchestrated task board. You are working in a git worktree isolated from the main branch. Your job is to implement the task described in the PROMPT.md specification you're given. @@ -1558,7 +1561,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri - Marking a step done before required review/tooling gates are satisfied`; /** Resolve the executor system prompt from settings, falling back to the hardcoded constant. */ -function getExecutorSystemPrompt(settings: Settings): string { +export function getExecutorSystemPrompt(settings: Settings): string { const customPrompt = resolveAgentPrompt("executor", settings.agentPrompts); const basePrompt = customPrompt || EXECUTOR_SYSTEM_PROMPT; const sections = [