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

- Add task `source` provenance field tracking how tasks enter the system (e.g. `"manual"`, `"heartbeat"`, `"executor"`, `"triage"`, `"cron"`, `"routine"`)
- Wire `source` into shared create-task tools, executor agent, triage agent, cron runner, and routine runner
- Add `source` to heartbeat task creation in `agent-heartbeat.ts` and mission execution loop in `mission-execution-loop.ts`
- Wire `source` field into dashboard routes (`register-planning-subtask-routes.ts`, `register-task-workflow-routes.ts`) and GitHub integration (`register-git-github.ts`)
- Add `source` to the API layer and task store handlers in the dashboard
- Expose `source` in the `fn` CLI task command and `task-plan` extension tool
- Add integration tests for provenance across engine (agent-tools, agent-heartbeat, cron-runner, triage, pr-comment-handler) and dashboard routes
- Update existing tests to assert the correct source values
- Bulk-revert stale changeset files that were superseded by the v0.9.1 release commit

Commits merged:
- test(FN-2923): complete Step 14 — align tests with task source provenance
- test(FN-2923): complete Steps 12-13 — update and add provenance tests
- feat(FN-2923): complete Step 10 — wire CLI task provenance
- feat(FN-2923): complete Step 9 — wire dashboard UI and API source fields
- feat(FN-2923): complete Step 8 — wire dashboard route provenance
- feat(FN-2923): complete Step 7 — wire remaining engine provenance
- feat(FN-2923): complete Step 6 — add routine create-task provenance
- feat(FN-2923): complete Step 5 — add cron create-task provenance
- feat(FN-2923): complete Step 4 — add triage subtask source
- feat(FN-2923): complete Step 3 — add executor create-task source
- feat(FN-2923): complete Step 2 — add heartbeat task provenance
- feat(FN-2923): complete Step 1 — add provenance to shared create-task tools
- feat(FN-2945): merge fusion/fn-2945
- chore(release): v0.9.1

Files changed:
.changeset/active-agents-no-stuck-connecting.md    | 13 ---
 .changeset/active-agents-panel-hoist-heartbeat.md  | 13 ---
 .changeset/fix-agent-heartbeat-terminal-links.md   |  5 --
 .changeset/fix-experimental-defaults.md            |  5 --
 .changeset/improve-git-manager-diff-pane.md        |  5 --
 .changeset/insights-two-pane-layout.md             |  5 --
 .changeset/show-planning-tasks-immediately.md      |  5 --
 CHANGELOG.md                                       | 92 ++++++++++++++++++++++
 package.json                                       |  2 +-
 packages/cli-alias/CHANGELOG.md                    | 15 ++++
 packages/cli-alias/package.json                    |  2 +-
 packages/cli/CHANGELOG.md                          | 12 +++
 packages/cli/package.json                          |  2 +-
 packages/cli/src/__tests__/task-plan.test.ts       |  1 +
 packages/cli/src/commands/__tests__/task.test.ts   | 15 +++-
 packages/cli/src/commands/task.ts                  | 15 +++-
 packages/cli/src/extension.ts                      |  9 +++
 packages/core/CHANGELOG.md                         |  7 ++
 packages/core/package.json                         |  2 +-
 packages/dashboard/CHANGELOG.md                    | 14 ++++
 .../app/__tests__/agent-css-classes.test.ts        |  3 +-
 packages/dashboard/app/__tests__/api.test.ts       | 13 +++
 packages/dashboard/app/api/legacy.ts               |  2 +
 packages/dashboard/app/components/AgentsView.css   | 22 +++---
 .../app/components/ModelOnboardingModal.tsx        |  5 +-
 packages/dashboard/app/components/TodoView.tsx     |  2 +
 .../__tests__/ModelOnboardingModal.test.tsx        |  4 +-
 .../app/components/__tests__/QuickChatFAB.test.tsx | 14 +---
 .../app/components/__tests__/TodoView.test.tsx     |  4 +-
 .../__tests__/agents-view-mobile.test.tsx          |  5 +-
 .../components/__tests__/onboarding-flow.test.tsx  |  2 +-
 .../app/hooks/__tests__/useTaskHandlers.test.ts    |  4 +-
 packages/dashboard/app/hooks/useTaskHandlers.ts    |  4 +-
 packages/dashboard/package.json                    |  2 +-
 packages/dashboard/src/__tests__/routes.test.ts    | 14 ++++
 packages/dashboard/src/routes.ts                   |  4 +
 .../dashboard/src/routes/register-git-github.ts    | 12 +++
 .../src/routes/register-planning-subtask-routes.ts |  3 +
 .../src/routes/register-task-workflow-routes.ts    |  7 ++
 packages/desktop/CHANGELOG.md                      |  7 ++
 packages/desktop/package.json                      |  2 +-
 packages/engine/CHANGELOG.md                       | 11 +++
 packages/engine/package.json                       |  2 +-
 .../engine/src/__tests__/agent-heartbeat.test.ts   | 10 +++
 .../src/__tests__/agent-tools-delegation.test.ts   |  2 +
 packages/engine/src/__tests__/agent-tools.test.ts  | 37 +++++++++
 packages/engine/src/__tests__/cron-runner.test.ts  |  4 +
 .../src/__tests__/pr-comment-handler.test.ts       |  8 ++
 packages/engine/src/__tests__/triage.test.ts       |  2 +
 packages/engine/src/agent-heartbeat.ts             | 13 ++-
 packages/engine/src/agent-tools.ts                 | 13 ++-
 packages/engine/src/cron-runner.ts                 |  7 +-
 packages/engine/src/executor.ts                    |  2 +-
 packages/engine/src/mission-execution-loop.ts      |  8 ++
 packages/engine/src/pr-comment-handler.ts          |  5 ++
 packages/engine/src/project-engine.ts              |  8 ++
 packages/engine/src/routine-runner.ts              |  4 +
 packages/engine/src/triage.ts                      |  4 +
 packages/mobile/CHANGELOG.md                       |  7 ++
 packages/mobile/package.json                       |  2 +-
 packages/pi-claude-cli/CHANGELOG.md                |  7 ++
 packages/pi-claude-cli/package.json                |  2 +-
 packages/plugin-sdk/CHANGELOG.md                   | 10 +++
 packages/plugin-sdk/package.json                   |  2 +-
 .../examples/fusion-plugin-auto-label/CHANGELOG.md |  8 ++
 .../examples/fusion-plugin-auto-label/package.json |  2 +-
 .../examples/fusion-plugin-ci-status/CHANGELOG.md  |  8 ++
 .../examples/fusion-plugin-ci-status/package.json  |  2 +-
 .../fusion-plugin-notification/CHANGELOG.md        |  8 ++
 .../fusion-plugin-notification/package.json        |  2 +-
 .../fusion-plugin-settings-demo/CHANGELOG.md       |  8 ++
 .../fusion-plugin-settings-demo/package.json       |  2 +-
 plugins/fusion-plugin-hermes-runtime/CHANGELOG.md  |  8 ++
 plugins/fusion-plugin-hermes-runtime/package.json  |  2 +-
 .../fusion-plugin-openclaw-runtime/CHANGELOG.md    |  8 ++
 .../fusion-plugin-openclaw-runtime/package.json    |  2 +-
 .../fusion-plugin-paperclip-runtime/CHANGELOG.md   |  8 ++
 .../fusion-plugin-paperclip-runtime/package.json   |  2 +-
 78 files changed, 499 insertions(+), 115 deletions(-)

Fusion-Task-Id: FN-2923
This commit is contained in:
Fusion
2026-04-29 17:20:39 -07:00
committed by gsxdsm
parent 8d559fb079
commit a285c2988b
34 changed files with 229 additions and 32 deletions

View File

@@ -3229,6 +3229,11 @@ describe("HeartbeatMonitor", () => {
description: "Follow-up task",
dependencies: undefined,
column: "triage",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",
sourceRunId: undefined,
},
});
});
});
@@ -3744,6 +3749,11 @@ describe("HeartbeatMonitor", () => {
description: "Follow-up task",
dependencies: undefined,
column: "triage",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",
sourceRunId: undefined,
},
});
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";

View File

@@ -187,6 +187,7 @@ describe("createDelegateTaskTool", () => {
dependencies: undefined,
column: "todo",
assignedAgentId: "agent-001",
source: { sourceType: "api" },
});
const text = (result.content[0] as { text: string }).text;
@@ -281,6 +282,7 @@ describe("createDelegateTaskTool", () => {
dependencies: ["FN-010"],
column: "todo",
assignedAgentId: "agent-001",
source: { sourceType: "api" },
});
const text = (result.content[0] as { text: string }).text;

View File

@@ -7,6 +7,7 @@ import {
buildQmdAgentMemorySearchArgs,
createMemoryTools,
createTaskCreateTool,
createDelegateTaskTool,
createSendMessageTool,
createReadMessagesTool,
qmdAgentMemoryCollectionName,
@@ -82,12 +83,48 @@ describe("createTaskCreateTool", () => {
description: "Follow-up task",
dependencies: ["PROJ-001"],
column: "triage",
source: undefined,
});
expect(result.details).toEqual({ taskId: "PROJ-042" });
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(responseText).toContain("Created PROJ-042: Follow-up task");
expect(responseText).toContain("(depends on: PROJ-001)");
});
it("passes explicit provenance to store.createTask", async () => {
const store = {
createTask: vi.fn().mockResolvedValue({ id: "PROJ-099", description: "Test", dependencies: [], column: "triage" }),
};
const tool = createTaskCreateTool(store as any, {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-123",
});
await tool.execute("call-1", { description: "Test" } as any, undefined, undefined, {} as any);
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-123", sourceRunId: undefined },
}));
});
});
describe("createDelegateTaskTool", () => {
it("creates delegated tasks with api source provenance", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const taskStore = {
createTask: vi.fn().mockResolvedValue({ id: "FN-100", dependencies: [], description: "Delegated" }),
};
const tool = createDelegateTaskTool(agentStore as any, taskStore as any);
await tool.execute("call-1", { agent_id: "agent-1", description: "Delegated" } as any, undefined, undefined, {} as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "api" },
}));
});
});
describe("createMemoryTools", () => {

View File

@@ -1252,6 +1252,10 @@ describe("CronRunner", () => {
column: "todo",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
source: {
sourceType: "cron",
sourceMetadata: { scheduleId: "test-schedule-id", stepId: expect.any(String) },
},
});
});

View File

@@ -213,6 +213,14 @@ describe("PrCommentHandler", () => {
description: expect.stringContaining("FN-001"),
column: "triage",
dependencies: ["FN-001"],
source: {
sourceType: "api",
sourceParentTaskId: "FN-001",
sourceMetadata: {
prNumber: 42,
prUrl: "https://github.com/owner/repo/pull/42",
},
},
});
});

View File

@@ -1387,6 +1387,7 @@ describe("taskCreate tool model inheritance", () => {
modelId: parentTaskResult?.modelId,
validatorModelProvider: parentTaskResult?.validatorModelProvider,
validatorModelId: parentTaskResult?.validatorModelId,
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
});
expect(store.getTask).toHaveBeenCalledWith("FN-001");
@@ -1435,6 +1436,7 @@ describe("taskCreate tool model inheritance", () => {
modelId: parentTask?.modelId,
validatorModelProvider: parentTask?.validatorModelProvider,
validatorModelId: parentTask?.validatorModelId,
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
});
expect(store.getTask).toHaveBeenCalledWith("FN-NONEXISTENT");

View File

@@ -1157,8 +1157,12 @@ export class HeartbeatMonitor {
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
heartbeatTools = [];
// fn_task_create tool (no tracking needed for no-task runs)
heartbeatTools.push(createTaskCreateTool(taskStore));
// fn_task_create tool
heartbeatTools.push(createTaskCreateTool(taskStore, {
sourceType: "agent_heartbeat",
sourceAgentId: agentId,
sourceRunId: runContext?.runId,
}));
// Agent delegation tools
heartbeatTools.push(createListAgentsTool(this.store));
@@ -1551,7 +1555,10 @@ export class HeartbeatMonitor {
const tools: ToolDefinition[] = [];
// Wrap createTaskCreateTool with tracking and agent-link logging
const baseCreateTool = createTaskCreateTool(taskStore);
const baseCreateTool = createTaskCreateTool(taskStore, {
sourceType: "agent_heartbeat",
sourceAgentId: agentId,
});
const trackedCreateTool: ToolDefinition = {
...baseCreateTool,
execute: async (id: string, params: Static<typeof taskCreateParams>, signal, onUpdate, ctx) => {

View File

@@ -11,7 +11,7 @@ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/p
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join } from "node:path";
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message } from "@fusion/core";
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType } from "@fusion/core";
import { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { Type, type Static } from "@mariozechner/pi-ai";
@@ -420,7 +420,10 @@ async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryCon
* @param store - TaskStore for task persistence
* @returns ToolDefinition for the `fn_task_create` tool
*/
export function createTaskCreateTool(store: TaskStore): ToolDefinition {
export function createTaskCreateTool(
store: TaskStore,
provenance?: { sourceType: SourceType; sourceAgentId?: string; sourceRunId?: string },
): ToolDefinition {
return {
name: "fn_task_create",
label: "Create Task",
@@ -435,6 +438,11 @@ export function createTaskCreateTool(store: TaskStore): ToolDefinition {
description: params.description,
dependencies: params.dependencies,
column: "triage",
source: provenance ? {
sourceType: provenance.sourceType,
sourceAgentId: provenance.sourceAgentId,
sourceRunId: provenance.sourceRunId,
} : undefined,
});
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
return {
@@ -899,6 +907,7 @@ export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskSt
dependencies: params.dependencies,
column: "todo",
assignedAgentId: params.agent_id,
source: { sourceType: "api" },
});
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";

View File

@@ -388,7 +388,7 @@ export class CronRunner {
} else if (step.type === "ai-prompt") {
return this.executeAiPromptStep(step, stepIndex, timeoutMs, stepStartedAt);
} else if (step.type === "create-task") {
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt);
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt, schedule.id);
}
// Unknown step type
@@ -564,6 +564,7 @@ export class CronRunner {
step: AutomationStep,
stepIndex: number,
startedAt: string,
scheduleId: string,
): Promise<AutomationStepResult> {
// Validate that taskDescription is present and non-empty
if (!step.taskDescription?.trim()) {
@@ -586,6 +587,10 @@ export class CronRunner {
column: (step.taskColumn as Column) || "triage",
modelProvider: step.modelProvider?.trim() || undefined,
modelId: step.modelId?.trim() || undefined,
source: {
sourceType: "cron",
sourceMetadata: { scheduleId, stepId: step.id },
},
};
try {

View File

@@ -3083,7 +3083,7 @@ export class TaskExecutor {
}
private createTaskCreateTool(): ToolDefinition {
return sharedCreateTaskCreateTool(this.store);
return sharedCreateTaskCreateTool(this.store, { sourceType: "api" });
}
private createTaskDocumentWriteTool(taskId: string): ToolDefinition {

View File

@@ -265,6 +265,14 @@ export class MissionExecutionLoop extends EventEmitter {
column: "in-progress",
missionId,
sliceId: feature.sliceId,
source: {
sourceType: "automation",
sourceMetadata: {
missionId,
featureId: feature.id,
sliceId: feature.sliceId,
},
},
});
validationTaskId = validationTask.id;

View File

@@ -219,6 +219,11 @@ Please review the PR comments and address any remaining issues.`;
description,
column: "triage",
dependencies: [originalTaskId],
source: {
sourceType: "api",
sourceParentTaskId: originalTaskId,
sourceMetadata: { prNumber: prInfo.number, prUrl: prInfo.url },
},
});
prMonitorLog.log(`Created follow-up task ${task.id} for PR #${prInfo.number}`);

View File

@@ -1233,6 +1233,10 @@ export class ProjectEngine {
description: followUpDescription,
column: "triage",
priority: "high",
source: {
sourceType: "recovery",
sourceParentTaskId: taskId,
},
});
await store.addTaskComment(
taskId,
@@ -1368,6 +1372,10 @@ export class ProjectEngine {
`Last merge error: ${errorMsg}`,
column: "triage",
priority: "high",
source: {
sourceType: "recovery",
sourceParentTaskId: taskId,
},
});
await store.addTaskComment(
taskId,

View File

@@ -385,6 +385,10 @@ export class RoutineRunner {
column: (step.taskColumn as Column) || "triage",
modelProvider: step.modelProvider?.trim() || undefined,
modelId: step.modelId?.trim() || undefined,
source: {
sourceType: "automation",
sourceMetadata: { routineId: routine.id, stepId: step.id },
},
};
try {
const task = await this.options.taskStore.createTask(taskInput);

View File

@@ -1573,6 +1573,10 @@ export class TriageProcessor {
modelId: parentTask?.modelId,
validatorModelProvider: parentTask?.validatorModelProvider,
validatorModelId: parentTask?.validatorModelId,
source: {
sourceType: "agent_heartbeat",
sourceParentTaskId: options.parentTaskId,
},
});
// Track the created subtask