feat(FN-2943): merge fusion/fn-2943

- **Active Agents panel overhaul**: Hoisted above the main agent list with a new `ActiveAgentsPanel.css` stylesheet; shows next heartbeat ETA and prevents cards from getting stuck in "Connecting..." state
- **Node routing policy**: Scheduler now enforces unavailable-node scheduling rules and routes tasks to eligible nodes more reliably (`packages/engine/src/scheduler.ts`, `node-routing-policy.ts`)
- **Task card merge timer**: Keeps the card timer live while a task is actively merging rather than resetting on merge start
- **Settings modal scroll**: Scrolls to top when auth actions succeed, with regression tests added
- **Task steps reset**: Resets steps when reopening a task that returns to todo or triage (FN-2966)
- **Core store additions**: New `packages/core/src/store.ts` test coverage for task state mutations
- **Dashboard SSE bus**: New `sse-bus.ts` for event bus communication in the dashboard
- **Version bumps**: `v0.9.0` release across all packages; `cli-alias`, `dashboard`, `engine`, `plugin-sdk`, and plugin changelogs updated
- **Changeset cleanup**: Removed 20 stale changeset files consumed by the v0.9.0 release; added two new ones for Active Agents panel fixes

Commits merged:
- test(FN-2943): complete Step 3 — add auth scroll-to-top regressions
- feat(FN-2943): complete Step 2 — scroll on successful auth actions
- feat(FN-2943): complete Step 1 — add settings content scroll ref
- feat(FN-2951): enforce unavailable-node scheduling and improve active agents panel
- chore: add changeset for Active Agents panel hoist + heartbeat ETA
- feat(dashboard): hoist Active Agents panel above main list, show next heartbeat
- fix(dashboard): add top margin between agent list and Active Agents panel
- fix(dashboard): unstick Active Agents panel cards from "Connecting..."
- chore(release): v0.9.0
- chore: add changeset for live merge timer on task card
- fix(dashboard): keep card timer live while task is actively merging
- feat(FN-2966): reset task steps when reopening to todo or triage

Files changed:
.changeset/active-agents-live-status-and-logs.md   |  13 -
 .changeset/active-agents-no-stuck-connecting.md    |  13 +
 .changeset/active-agents-panel-hoist-heartbeat.md  |  13 +
 .changeset/dashboard-merge-base-diff.md            |  13 -
 .changeset/dashboard-task-changed-files-only.md    |  13 -
 .changeset/engine-concurrency-races.md             |  13 -
 .changeset/fix-agent-heartbeat-scheduling.md       |   5 -
 .changeset/memory-dream-session-state.md           |  13 -
 .changeset/merger-ai-summarized-commits.md         |  13 -
 .changeset/multi-node-routing.md                   |  21 -
 .changeset/pluggable-notification-providers.md     |   4 -
 .changeset/pr-mode-push-branch-and-cleanup.md      |  16 -
 .../recover-mergeable-respects-merge-settings.md   |  13 -
 .changeset/remote-tailscale-qr-tui.md              |   7 -
 .changeset/require-pr-approval-setting.md          |  17 -
 .changeset/respec-from-in-review.md                |  13 -
 .changeset/restore-task-card-fallbacks.md          |  13 -
 .changeset/tui-log-copy-feedback.md                |  13 -
 .changeset/tui-wide-stats-stack.md                 |  13 -
 CHANGELOG.md                                       | 449 +++++++++++++++++++++
 package.json                                       |   2 +-
 packages/cli-alias/CHANGELOG.md                    |  60 +++
 packages/cli-alias/package.json                    |   2 +-
 packages/cli/CHANGELOG.md                          |  44 ++
 packages/cli/package.json                          |   2 +-
 packages/core/CHANGELOG.md                         |  41 ++
 packages/core/package.json                         |   2 +-
 packages/core/src/__tests__/store.test.ts          | 127 ++++++
 packages/core/src/store.ts                         |  35 +-
 packages/dashboard/CHANGELOG.md                    |  63 +++
 .../dashboard/app/components/ActiveAgentsPanel.css |  29 ++
 .../dashboard/app/components/ActiveAgentsPanel.tsx |  35 +-
 packages/dashboard/app/components/AgentsView.tsx   |   5 +-
 .../dashboard/app/components/SettingsModal.tsx     |  13 +-
 packages/dashboard/app/components/TaskCard.tsx     |  30 +-
 .../__tests__/ActiveAgentsPanel.test.tsx           |  42 ++
 .../app/components/__tests__/AgentsView.test.tsx   |  11 +-
 .../components/__tests__/SettingsModal.test.tsx    |  68 ++++
 .../app/components/__tests__/TaskCard.test.tsx     |  36 ++
 packages/dashboard/app/sse-bus.ts                  |  13 +
 packages/dashboard/package.json                    |   2 +-
 packages/desktop/CHANGELOG.md                      |  41 ++
 packages/desktop/package.json                      |   2 +-
 packages/engine/CHANGELOG.md                       |  59 +++
 packages/engine/package.json                       |   2 +-
 .../src/__tests__/node-routing-policy.test.ts      | 102 +++--
 .../src/__tests__/scheduler-node-routing.test.ts   |  43 +-
 packages/engine/src/index.ts                       |   1 +
 packages/engine/src/node-routing-policy.ts         |  41 +-
 packages/engine/src/scheduler.ts                   |  48 +--
 packages/mobile/CHANGELOG.md                       |  41 ++
 packages/mobile/package.json                       |   2 +-
 packages/pi-claude-cli/CHANGELOG.md                |  41 ++
 packages/pi-claude-cli/package.json                |   2 +-
 packages/plugin-sdk/CHANGELOG.md                   |  57 +++
 packages/plugin-sdk/package.json                   |   2 +-
 .../examples/fusion-plugin-auto-label/CHANGELOG.md |  21 +
 .../examples/fusion-plugin-auto-label/package.json |   2 +-
 .../examples/fusion-plugin-ci-status/CHANGELOG.md  |  21 +
 .../examples/fusion-plugin-ci-status/package.json  |   2 +-
 .../fusion-plugin-notification/CHANGELOG.md        |  21 +
 .../fusion-plugin-notification/package.json        |   2 +-
 .../fusion-plugin-settings-demo/CHANGELOG.md       |  21 +
 .../fusion-plugin-settings-demo/package.json       |   2 +-
 plugins/fusion-plugin-hermes-runtime/CHANGELOG.md  |  21 +
 plugins/fusion-plugin-hermes-runtime/package.json  |   2 +-
 .../fusion-plugin-openclaw-runtime/CHANGELOG.md    |  21 +
 .../fusion-plugin-openclaw-runtime/package.json    |   2 +-
 .../fusion-plugin-paperclip-runtime/CHANGELOG.md   |  21 +
 .../fusion-plugin-paperclip-runtime/package.json   |   2 +-
 70 files changed, 1617 insertions(+), 378 deletions(-)

Fusion-Task-Id: FN-2943
This commit is contained in:
Fusion
2026-04-29 14:42:30 -07:00
committed by gsxdsm
parent fb31f37b99
commit 7e832bbf92
7 changed files with 425 additions and 60 deletions

View File

@@ -251,6 +251,62 @@ describe("resolveAgentPrompt", () => {
const result = resolveAgentPrompt("triage", config);
expect(result).toContain("task_document_write");
});
it("default role prompts include explicit heartbeat run guidance", () => {
expect(resolveAgentPrompt("executor")).toContain("## Heartbeat Run Behavior");
expect(resolveAgentPrompt("triage")).toContain("## Heartbeat Run Behavior");
expect(resolveAgentPrompt("reviewer")).toContain("## Heartbeat Run Behavior");
expect(resolveAgentPrompt("merger")).toContain("## Heartbeat Run Behavior");
});
it("executor heartbeat guidance covers no-task engineering work", () => {
const result = resolveAgentPrompt("executor");
expect(result).toContain("execute your standing instructions");
expect(result).toContain("blocked or failing engineering work");
});
it("reviewer heartbeat guidance is findings-focused and customized per variant", () => {
const defaultReviewer = resolveAgentPrompt("reviewer");
expect(defaultReviewer).toContain("Look for work waiting on review");
const strictConfig: AgentPromptsConfig = {
roleAssignments: {
reviewer: "strict-reviewer",
},
};
const strictReviewer = resolveAgentPrompt("reviewer", strictConfig);
expect(strictReviewer).toContain("worst-case failure modes first");
expect(strictReviewer).toContain("under-reviewed");
});
it("triage heartbeat guidance is customized for standard and concise templates", () => {
const defaultTriage = resolveAgentPrompt("triage");
expect(defaultTriage).toContain("Patrol for vague requests");
const conciseConfig: AgentPromptsConfig = {
roleAssignments: {
triage: "concise-triage",
},
};
const conciseTriage = resolveAgentPrompt("triage", conciseConfig);
expect(conciseTriage).toContain("Keep heartbeat output lean and useful");
expect(conciseTriage).toContain("minimum complete PROMPT.md");
});
it("merger and senior-engineer heartbeat guidance is role-specific", () => {
const merger = resolveAgentPrompt("merger");
expect(merger).toContain("keep merge-ready work from stalling");
expect(merger).toContain("in-review and merge-ready queue");
const seniorConfig: AgentPromptsConfig = {
roleAssignments: {
executor: "senior-engineer",
},
};
const senior = resolveAgentPrompt("executor", seniorConfig);
expect(senior).toContain("autonomous senior-engineering pass");
expect(senior).toContain("architectural drift");
});
});
// ---------------------------------------------------------------------------

View File

@@ -24,6 +24,7 @@ import { runCommandAsync } from "../run-command.js";
const mockedRunCommandAsync = vi.mocked(runCommandAsync);
import { TaskStore, TaskHasDependentsError } from "../store.js";
import { AgentStore } from "../agent-store.js";
import { appendFile, readFile, writeFile, mkdir, rm, readdir, unlink } from "node:fs/promises";
import { join } from "node:path";
import { mkdtempSync, existsSync } from "node:fs";
@@ -4626,6 +4627,28 @@ Task with acceptance criteria
expect(after.count).toBe(0);
});
it("deleteTask clears linked agent task assignments", async () => {
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const agentStore = new AgentStore({ rootDir: store.getFusionDir() });
await agentStore.init();
try {
const task = await store.createTask({ description: "Delete me" });
const agent = await agentStore.createAgent({ name: "Delete watcher", role: "executor" });
await agentStore.assignTask(agent.id, task.id);
await store.deleteTask(task.id);
const updatedAgent = await agentStore.getAgent(agent.id);
expect(updatedAgent?.taskId).toBeUndefined();
} finally {
agentStore.close();
}
});
it("importLegacyAgentLogs imports JSONL entries from existing agent.log files", async () => {
const task = await createTestTask();
const dir = join(rootDir, ".fusion", "tasks", task.id);
@@ -7727,6 +7750,33 @@ Task with acceptance criteria
const dir = join(rootDir, ".fusion", "tasks", task.id);
expect(existsSync(dir)).toBe(false);
});
it("archiveTask clears stale linked agent assignments", async () => {
store.close();
store = new TaskStore(rootDir, globalDir);
await store.init();
const agentStore = new AgentStore({ rootDir: store.getFusionDir() });
await agentStore.init();
try {
const task = await store.createTask({ description: "Archive clears links" });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
const agent = await agentStore.createAgent({ name: "Archive watcher", role: "executor" });
await agentStore.assignTask(agent.id, task.id);
await store.archiveTask(task.id, false);
const updatedAgent = await agentStore.getAgent(agent.id);
expect(updatedAgent?.taskId).toBeUndefined();
} finally {
agentStore.close();
}
});
});
describe("archive log persistence", () => {

View File

@@ -855,6 +855,62 @@ Write a PROMPT.md specification to the given path. Be brief and precise — avoi
6. **Read first:** Examine codebase before writing spec.
7. **Be concise:** Short descriptions, minimal prose. Focus on what matters.`;
const EXECUTOR_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Treat each heartbeat as a short autonomous execution cycle.
- If a task is assigned: inspect the latest task state, continue the next concrete implementation step, run the smallest useful verification, and either advance the task or log the blocker precisely.
- If no task is assigned: execute your standing instructions. Review unread messages, scan for blocked or failing engineering work, create narrowly scoped follow-up tasks, and capture durable implementation notes other agents will need later.
- Do not idle simply because no task is linked. Use heartbeat time to reduce engineering risk, unblock work, and keep execution moving in small, concrete increments.`;
const TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Use heartbeat runs to keep the planning pipeline healthy.
- If a task is assigned: turn the rough request into a complete, execution-ready PROMPT.md with clear scope, steps, dependencies, and verification criteria.
- If no task is assigned: execute your planning instructions. Patrol for vague requests, blocked tasks that need better specification, review follow-ups that should become new tasks, and dependency gaps that are slowing executors down.
- Favor ambiguity reduction over busywork. Every heartbeat should leave the queue more actionable than you found it.`;
const REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Use heartbeat runs to keep review quality high and queues moving.
- If a task is assigned: perform the review with findings first, focusing on correctness, regressions, missing tests, and operational risk.
- If no task is assigned: execute your review instructions. Look for work waiting on review, failed validations, suspicious recent changes, and places where a second pass would prevent a bad merge.
- Prefer surfacing concrete findings, follow-up tasks, or merge blockers over rewriting implementation yourself.`;
const MERGER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Use heartbeat runs to keep merge-ready work from stalling.
- If a task is assigned: verify merge preconditions, resolve the next safe merge step, and surface conflicts or missing gates immediately.
- If no task is assigned: execute your merge instructions. Inspect the in-review and merge-ready queue, look for unresolved conflicts, missing approvals, broken post-review state, and tasks that are ready for the final merge push.
- Optimize for safe flow, not raw throughput. Clear blockers, communicate risks, and only move merge work forward when the repository stays trustworthy.`;
const SENIOR_ENGINEER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Treat each heartbeat as an autonomous senior-engineering pass.
- If a task is assigned: push the implementation forward decisively, making sound architectural choices, validating risky changes early, and documenting trade-offs that downstream agents should inherit.
- If no task is assigned: execute your standing instructions. Hunt for architectural drift, flaky quality gates, latent integration risk, and follow-up work that needs a strong technical owner.
- Spend heartbeat time where leverage is highest: unblock teams, reduce complexity, and turn vague engineering risk into concrete next actions.`;
const STRICT_REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Use heartbeat runs to enforce a high review bar.
- If a task is assigned: review for worst-case failure modes first, especially security, backward compatibility, edge cases, and missing regression coverage.
- If no task is assigned: execute your review instructions. Look for merges that feel under-reviewed, risky diffs that deserve another pass, and follow-up work needed before code should land.
- Bias toward precise findings and explicit risk articulation. A quiet heartbeat should mean the code is genuinely clean, not that you stopped looking.`;
const CONCISE_TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
Keep heartbeat output lean and useful.
- If a task is assigned: produce the minimum complete PROMPT.md needed for an executor to act safely.
- If no task is assigned: execute your planning instructions, scan for underspecified or blocked work, and turn it into short, actionable task specs or follow-up tickets.
- Prefer crisp decisions, clear file scope, and concrete verification steps over narrative detail.`;
// ---------------------------------------------------------------------------
// Built-in templates array
// ---------------------------------------------------------------------------
@@ -866,7 +922,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Default Executor",
description: "Standard task execution agent with full tooling and review support.",
role: "executor",
prompt: EXECUTOR_PROMPT_TEXT,
prompt: `${EXECUTOR_PROMPT_TEXT}\n\n${EXECUTOR_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -874,7 +930,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Default Triage",
description: "Standard task specification agent producing detailed PROMPT.md files.",
role: "triage",
prompt: TRIAGE_PROMPT_TEXT,
prompt: `${TRIAGE_PROMPT_TEXT}\n\n${TRIAGE_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -882,7 +938,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Default Reviewer",
description: "Standard independent code and plan reviewer with balanced criteria.",
role: "reviewer",
prompt: REVIEWER_PROMPT_TEXT,
prompt: `${REVIEWER_PROMPT_TEXT}\n\n${REVIEWER_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -890,7 +946,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Default Merger",
description: "Standard merge agent for squash merges with conflict resolution.",
role: "merger",
prompt: MERGER_BASE_PROMPT_TEXT,
prompt: `${MERGER_BASE_PROMPT_TEXT}\n\n${MERGER_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -898,7 +954,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Senior Engineer",
description: "Autonomous executor with architectural awareness, performance focus, and minimal hand-holding. Makes independent decisions on routine matters.",
role: "executor",
prompt: SENIOR_ENGINEER_PROMPT_TEXT,
prompt: `${SENIOR_ENGINEER_PROMPT_TEXT}\n\n${SENIOR_ENGINEER_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -906,7 +962,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Strict Reviewer",
description: "Rigorous reviewer with stricter criteria for security, edge cases, backward compatibility, and type safety. Issues REVISE more readily.",
role: "reviewer",
prompt: STRICT_REVIEWER_PROMPT_TEXT,
prompt: `${STRICT_REVIEWER_PROMPT_TEXT}\n\n${STRICT_REVIEWER_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
{
@@ -914,7 +970,7 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
name: "Concise Triage",
description: "Shorter, more focused specification format with minimal prose. Produces compact PROMPT.md files with essential information only.",
role: "triage",
prompt: CONCISE_TRIAGE_PROMPT_TEXT,
prompt: `${CONCISE_TRIAGE_PROMPT_TEXT}\n\n${CONCISE_TRIAGE_HEARTBEAT_GUIDANCE}`,
builtIn: true,
},
];

View File

@@ -2703,6 +2703,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
await this.atomicWriteTaskJson(dir, task);
if (toColumn === "done") {
this.clearLinkedAgentTaskIds(id, task.updatedAt);
}
// Update cache if watcher is active
if (this.isWatching) this.taskCache.set(id, { ...task });
@@ -3547,6 +3550,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
rewrittenDependents.push(updatedDependent);
}
this.clearLinkedAgentTaskIds(taskId);
this.db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
this.db.bumpLastModified();
});
@@ -3554,6 +3558,33 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return rewrittenDependents;
}
/**
* Clear `agent.taskId` links that point at a task which has transitioned out
* of active work. This keeps heartbeat scheduling aligned with live task
* storage and prevents stale task-scoped heartbeat runs.
*/
private clearLinkedAgentTaskIds(taskId: string, updatedAt: string = new Date().toISOString()): void {
const linkedAgents = this.db
.prepare("SELECT id FROM agents WHERE taskId = ?")
.all(taskId) as Array<{ id: string }>;
if (linkedAgents.length === 0) {
return;
}
this.db.prepare(`
UPDATE agents
SET
taskId = NULL,
updatedAt = ?,
data = CASE
WHEN json_valid(data) THEN json_set(json_remove(data, '$.taskId'), '$.updatedAt', ?)
ELSE data
END
WHERE taskId = ?
`).run(updatedAt, updatedAt, taskId);
}
/**
* Clean up the git branch associated with a task.
*
@@ -3887,6 +3918,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (!cleanup) {
await this.atomicWriteTaskJson(dir, task);
this.clearLinkedAgentTaskIds(id, task.updatedAt);
if (this.isWatching) this.taskCache.set(id, { ...task });
this.emit("task:moved", { task, from: "done" as Column, to: "archived" as Column });
return task;
@@ -3903,6 +3935,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const entry = await this.taskToArchiveEntry(task, task.columnMovedAt);
this.archiveDb.upsert(entry);
this.clearLinkedAgentTaskIds(id, task.updatedAt);
this.db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
this.db.bumpLastModified();