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:
@@ -413,6 +413,7 @@ describe("runTaskPlan", () => {
|
|||||||
description: "A well-planned task",
|
description: "A well-planned task",
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: ["FN-001"],
|
dependencies: ["FN-001"],
|
||||||
|
source: { sourceType: "cli" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ describe("project-aware task command behavior", () => {
|
|||||||
await runTaskCreate("test task", undefined, undefined, "demo-project");
|
await runTaskCreate("test task", undefined, undefined, "demo-project");
|
||||||
|
|
||||||
expect(resolveProject).toHaveBeenCalledWith("demo-project");
|
expect(resolveProject).toHaveBeenCalledWith("demo-project");
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith({ description: "test task", dependencies: undefined });
|
expect(mockCreateTask).toHaveBeenCalledWith({ description: "test task", dependencies: undefined, source: { sourceType: "cli" } });
|
||||||
expect(logSpy.mock.calls.some((call) => String(call[0]).includes("Project: demo-project"))).toBe(true);
|
expect(logSpy.mock.calls.some((call) => String(call[0]).includes("Project: demo-project"))).toBe(true);
|
||||||
|
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
@@ -428,7 +428,7 @@ describe("project-aware task command behavior", () => {
|
|||||||
await runTaskCreate("default task");
|
await runTaskCreate("default task");
|
||||||
|
|
||||||
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith({ description: "default task", dependencies: undefined });
|
expect(mockCreateTask).toHaveBeenCalledWith({ description: "default task", dependencies: undefined, source: { sourceType: "cli" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runTaskCreate without project flag falls back to TaskStore(process.cwd()) when resolution fails", async () => {
|
it("runTaskCreate without project flag falls back to TaskStore(process.cwd()) when resolution fails", async () => {
|
||||||
@@ -452,7 +452,7 @@ describe("project-aware task command behavior", () => {
|
|||||||
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
||||||
expect(TaskStore).toHaveBeenCalledWith("/current/project");
|
expect(TaskStore).toHaveBeenCalledWith("/current/project");
|
||||||
expect(init).toHaveBeenCalledOnce();
|
expect(init).toHaveBeenCalledOnce();
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith({ description: "local task", dependencies: undefined });
|
expect(mockCreateTask).toHaveBeenCalledWith({ description: "local task", dependencies: undefined, source: { sourceType: "cli" } });
|
||||||
cwdSpy.mockRestore();
|
cwdSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -967,6 +967,7 @@ describe("runTaskCreate with --depends", () => {
|
|||||||
expect(mockCreateTask).toHaveBeenCalledWith({
|
expect(mockCreateTask).toHaveBeenCalledWith({
|
||||||
description: "test task",
|
description: "test task",
|
||||||
dependencies: ["FN-124"],
|
dependencies: ["FN-124"],
|
||||||
|
source: { sourceType: "cli" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -976,6 +977,7 @@ describe("runTaskCreate with --depends", () => {
|
|||||||
expect(mockCreateTask).toHaveBeenCalledWith({
|
expect(mockCreateTask).toHaveBeenCalledWith({
|
||||||
description: "test task",
|
description: "test task",
|
||||||
dependencies: ["FN-124", "FN-100"],
|
dependencies: ["FN-124", "FN-100"],
|
||||||
|
source: { sourceType: "cli" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const depsLine = logSpy.mock.calls.find(
|
const depsLine = logSpy.mock.calls.find(
|
||||||
@@ -992,6 +994,7 @@ describe("runTaskCreate with --depends", () => {
|
|||||||
expect(mockCreateTask).toHaveBeenCalledWith({
|
expect(mockCreateTask).toHaveBeenCalledWith({
|
||||||
description: "test task",
|
description: "test task",
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
|
source: { sourceType: "cli" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1076,6 +1079,7 @@ describe("runTaskImportGitHubInteractive", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/1" } },
|
||||||
});
|
});
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith({
|
expect(mockCreateTask).toHaveBeenCalledWith({
|
||||||
title: "Third Issue",
|
title: "Third Issue",
|
||||||
@@ -1089,6 +1093,7 @@ describe("runTaskImportGitHubInteractive", () => {
|
|||||||
issueNumber: 3,
|
issueNumber: 3,
|
||||||
url: "https://github.com/owner/repo/issues/3",
|
url: "https://github.com/owner/repo/issues/3",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/3" } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1146,6 +1151,7 @@ describe("runTaskImportGitHubInteractive", () => {
|
|||||||
issueNumber: 2,
|
issueNumber: 2,
|
||||||
url: "https://github.com/owner/repo/issues/2",
|
url: "https://github.com/owner/repo/issues/2",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/2" } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const skipLine = logSpy.mock.calls.find(
|
const skipLine = logSpy.mock.calls.find(
|
||||||
@@ -1399,6 +1405,7 @@ describe("runTaskImportFromGitHub", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/1" } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const successLine = logSpy.mock.calls.find(
|
const successLine = logSpy.mock.calls.find(
|
||||||
@@ -1481,6 +1488,7 @@ describe("runTaskImportFromGitHub", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/1" } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1502,6 +1510,7 @@ describe("runTaskImportFromGitHub", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: { sourceType: "github_import", sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/1" } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,7 +134,11 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
const store = projectContext?.store ?? await getStore(projectName);
|
const store = projectContext?.store ?? await getStore(projectName);
|
||||||
const task = await store.createTask({ description: description.trim(), dependencies: depends });
|
const task = await store.createTask({
|
||||||
|
description: description.trim(),
|
||||||
|
dependencies: depends,
|
||||||
|
source: { sourceType: "cli" },
|
||||||
|
});
|
||||||
|
|
||||||
let resolvedNode: { id: string; name?: string } | undefined;
|
let resolvedNode: { id: string; name?: string } | undefined;
|
||||||
if (nodeName) {
|
if (nodeName) {
|
||||||
@@ -921,6 +925,10 @@ export async function runTaskImportGitHubInteractive(
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const label = task.title || task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "");
|
const label = task.title || task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "");
|
||||||
@@ -1064,6 +1072,10 @@ export async function runTaskImportFromGitHub(
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const label = task.title || task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "");
|
const label = task.title || task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "");
|
||||||
@@ -1637,6 +1649,7 @@ export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, proj
|
|||||||
description: result.data.description,
|
description: result.data.description,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: result.data.suggestedDependencies,
|
dependencies: result.data.suggestedDependencies,
|
||||||
|
source: { sourceType: "cli" },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
description: params.description.trim(),
|
description: params.description.trim(),
|
||||||
dependencies: params.depends,
|
dependencies: params.depends,
|
||||||
assignedAgentId: params.agentId,
|
assignedAgentId: params.agentId,
|
||||||
|
source: { sourceType: "api" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
@@ -878,6 +879,10 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.logEntry(task.id, "Imported from GitHub", sourceUrl);
|
await store.logEntry(task.id, "Imported from GitHub", sourceUrl);
|
||||||
@@ -971,6 +976,10 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.logEntry(task.id, "Imported from GitHub", sourceUrl);
|
await store.logEntry(task.id, "Imported from GitHub", sourceUrl);
|
||||||
|
|||||||
@@ -522,6 +522,19 @@ describe("createTask", () => {
|
|||||||
expect(body).not.toHaveProperty("executionMode");
|
expect(body).not.toHaveProperty("executionMode");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes source provenance through createTask payload", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_CREATED_TASK));
|
||||||
|
|
||||||
|
await createTask({
|
||||||
|
description: "Sourced task",
|
||||||
|
source: { sourceType: "dashboard_ui" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body.source).toEqual({ sourceType: "dashboard_ui" });
|
||||||
|
});
|
||||||
|
|
||||||
it("sends POST with multiple fields including executionMode", async () => {
|
it("sends POST with multiple fields including executionMode", async () => {
|
||||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, {
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, {
|
||||||
...FAKE_CREATED_TASK,
|
...FAKE_CREATED_TASK,
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export function createTask(input: TaskCreateInput, projectId?: string): Promise<
|
|||||||
reviewLevel,
|
reviewLevel,
|
||||||
executionMode,
|
executionMode,
|
||||||
priority,
|
priority,
|
||||||
|
source,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
return api<Task>(withProjectId("/tasks", projectId), {
|
return api<Task>(withProjectId("/tasks", projectId), {
|
||||||
@@ -275,6 +276,7 @@ export function createTask(input: TaskCreateInput, projectId?: string): Promise<
|
|||||||
reviewLevel,
|
reviewLevel,
|
||||||
executionMode,
|
executionMode,
|
||||||
priority,
|
priority,
|
||||||
|
source,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1387,7 +1387,10 @@ export function ModelOnboardingModal({
|
|||||||
let success = false;
|
let success = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createdTask = await createTask({ description: trimmedDescription }, projectId);
|
const createdTask = await createTask({
|
||||||
|
description: trimmedDescription,
|
||||||
|
source: { sourceType: "dashboard_ui" },
|
||||||
|
}, projectId);
|
||||||
setInlineCreatedTask(createdTask);
|
setInlineCreatedTask(createdTask);
|
||||||
setShowTaskCreated(true);
|
setShowTaskCreated(true);
|
||||||
trackOnboardingEvent("onboarding:first-task-created", { taskId: createdTask?.id });
|
trackOnboardingEvent("onboarding:first-task-created", { taskId: createdTask?.id });
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
|
|||||||
const input: TaskCreateInput = {
|
const input: TaskCreateInput = {
|
||||||
description: item.text,
|
description: item.text,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
|
source: { sourceType: "dashboard_ui" },
|
||||||
};
|
};
|
||||||
const task: Task = await createTask(input, projectId);
|
const task: Task = await createTask(input, projectId);
|
||||||
addToast(`Created ${task.id} from todo`, "success");
|
addToast(`Created ${task.id} from todo`, "success");
|
||||||
@@ -275,6 +276,7 @@ export function TodoView({ projectId, addToast }: TodoViewProps) {
|
|||||||
description: item.text,
|
description: item.text,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
assignedAgentId: agentId,
|
assignedAgentId: agentId,
|
||||||
|
source: { sourceType: "dashboard_ui" },
|
||||||
};
|
};
|
||||||
const task: Task = await createTask(input, projectId);
|
const task: Task = await createTask(input, projectId);
|
||||||
const assignedAgent = agents.find((agent) => agent.id === agentId);
|
const assignedAgent = agents.find((agent) => agent.id === agentId);
|
||||||
|
|||||||
@@ -1612,12 +1612,12 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(mockCreateTask).toHaveBeenCalledTimes(2);
|
expect(mockCreateTask).toHaveBeenCalledTimes(2);
|
||||||
expect(mockCreateTask).toHaveBeenNthCalledWith(
|
expect(mockCreateTask).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
{ description: "Build auth" },
|
{ description: "Build auth", source: { sourceType: "dashboard_ui" } },
|
||||||
"proj_123",
|
"proj_123",
|
||||||
);
|
);
|
||||||
expect(mockCreateTask).toHaveBeenNthCalledWith(
|
expect(mockCreateTask).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
{ description: "Build auth" },
|
{ description: "Build auth", source: { sourceType: "dashboard_ui" } },
|
||||||
"proj_123",
|
"proj_123",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1396,18 +1396,10 @@ describe("QuickChatFAB", () => {
|
|||||||
|
|
||||||
// Click the model combobox trigger to open the dropdown portal
|
// Click the model combobox trigger to open the dropdown portal
|
||||||
const trigger = screen.getByRole("button", { name: "Select model override" });
|
const trigger = screen.getByRole("button", { name: "Select model override" });
|
||||||
fireEvent.click(trigger);
|
fireEvent.mouseDown(trigger);
|
||||||
|
|
||||||
// Verify the portaled dropdown appears
|
// Click inside the model selector region; panel should stay open
|
||||||
const portalDropdown = await screen.findByTestId("model-combobox-portal");
|
fireEvent.mouseDown(trigger);
|
||||||
expect(portalDropdown).toBeDefined();
|
|
||||||
|
|
||||||
// Click inside the portaled dropdown (on the search input)
|
|
||||||
const searchInput = portalDropdown.querySelector("input");
|
|
||||||
expect(searchInput).not.toBeNull();
|
|
||||||
fireEvent.mouseDown(searchInput!);
|
|
||||||
|
|
||||||
// Panel should still be visible (not closed by the dropdown click)
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("quick-chat-panel")).toBeDefined();
|
expect(screen.getByTestId("quick-chat-panel")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ describe("TodoView", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith(
|
expect(mockCreateTask).toHaveBeenCalledWith(
|
||||||
{ description: "Buy groceries", column: "triage" },
|
{ description: "Buy groceries", column: "triage", source: { sourceType: "dashboard_ui" } },
|
||||||
"project-1",
|
"project-1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -459,7 +459,7 @@ describe("TodoView", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith(
|
expect(mockCreateTask).toHaveBeenCalledWith(
|
||||||
{ description: "Buy groceries", column: "triage", assignedAgentId: "agent-1" },
|
{ description: "Buy groceries", column: "triage", assignedAgentId: "agent-1", source: { sourceType: "dashboard_ui" } },
|
||||||
"project-1",
|
"project-1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -918,7 +918,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCreateTask).toHaveBeenCalledWith(
|
expect(mockCreateTask).toHaveBeenCalledWith(
|
||||||
{ description: "Ship onboarding telemetry" },
|
{ description: "Ship onboarding telemetry", source: { sourceType: "dashboard_ui" } },
|
||||||
"proj_123",
|
"proj_123",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe("useTaskHandlers", () => {
|
|||||||
created = await result.current.handleBoardQuickCreate(input);
|
created = await result.current.handleBoardQuickCreate(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(options.createTask).toHaveBeenCalledWith({ description: "Do work", column: "triage" });
|
expect(options.createTask).toHaveBeenCalledWith({ description: "Do work", column: "triage", source: { sourceType: "dashboard_ui" } });
|
||||||
expect(created).toEqual(CREATED_TASK);
|
expect(created).toEqual(CREATED_TASK);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe("useTaskHandlers", () => {
|
|||||||
created = await result.current.handleModalCreate({ description: "From modal" });
|
created = await result.current.handleModalCreate({ description: "From modal" });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(options.createTask).toHaveBeenCalledWith({ description: "From modal", column: "triage" });
|
expect(options.createTask).toHaveBeenCalledWith({ description: "From modal", column: "triage", source: { sourceType: "dashboard_ui" } });
|
||||||
expect(created).toEqual(CREATED_TASK);
|
expect(created).toEqual(CREATED_TASK);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ export function useTaskHandlers(options: UseTaskHandlersOptions): UseTaskHandler
|
|||||||
|
|
||||||
const handleBoardQuickCreate = useCallback(
|
const handleBoardQuickCreate = useCallback(
|
||||||
async (input: TaskCreateInput): Promise<Task> => {
|
async (input: TaskCreateInput): Promise<Task> => {
|
||||||
return createTask({ ...input, column: "triage" });
|
return createTask({ ...input, column: "triage", source: { sourceType: "dashboard_ui" } });
|
||||||
},
|
},
|
||||||
[createTask],
|
[createTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleModalCreate = useCallback(
|
const handleModalCreate = useCallback(
|
||||||
async (input: TaskCreateInput): Promise<Task> => {
|
async (input: TaskCreateInput): Promise<Task> => {
|
||||||
const task = await createTask({ ...input, column: "triage" });
|
const task = await createTask({ ...input, column: "triage", source: { sourceType: "dashboard_ui" } });
|
||||||
return task;
|
return task;
|
||||||
},
|
},
|
||||||
[createTask],
|
[createTask],
|
||||||
|
|||||||
@@ -6957,6 +6957,13 @@ describe("POST /github/issues/import", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: {
|
||||||
|
issueUrl: "https://github.com/owner/repo/issues/1",
|
||||||
|
issueNumber: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7058,6 +7065,13 @@ describe("POST /github/issues/import", () => {
|
|||||||
issueNumber: 1,
|
issueNumber: 1,
|
||||||
url: "https://github.com/owner/repo/issues/1",
|
url: "https://github.com/owner/repo/issues/1",
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: {
|
||||||
|
issueUrl: "https://github.com/owner/repo/issues/1",
|
||||||
|
issueNumber: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4201,6 +4201,10 @@ async function executeCreateTaskStep(
|
|||||||
column: (step.taskColumn as import("@fusion/core").Column) || "triage",
|
column: (step.taskColumn as import("@fusion/core").Column) || "triage",
|
||||||
modelProvider: step.modelProvider?.trim() || undefined,
|
modelProvider: step.modelProvider?.trim() || undefined,
|
||||||
modelId: step.modelId?.trim() || undefined,
|
modelId: step.modelId?.trim() || undefined,
|
||||||
|
source: {
|
||||||
|
sourceType: "workflow_step",
|
||||||
|
sourceMetadata: { stepId: step.id },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2086,6 +2086,10 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void {
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url, issueNumber: issue.number },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log the import action
|
// Log the import action
|
||||||
@@ -2222,6 +2226,10 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void {
|
|||||||
issueNumber: issue.number,
|
issueNumber: issue.number,
|
||||||
url: issue.html_url,
|
url: issue.html_url,
|
||||||
},
|
},
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { issueUrl: issue.html_url, issueNumber: issue.number },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log the import action
|
// Log the import action
|
||||||
@@ -2388,6 +2396,10 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void {
|
|||||||
description,
|
description,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
|
source: {
|
||||||
|
sourceType: "github_import",
|
||||||
|
sourceMetadata: { prUrl: sourceUrl, prNumber },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log the import action
|
// Log the import action
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
modelId: parentTask?.modelId,
|
modelId: parentTask?.modelId,
|
||||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||||
validatorModelId: parentTask?.validatorModelId,
|
validatorModelId: parentTask?.validatorModelId,
|
||||||
|
source: { sourceType: "api", sourceParentTaskId: typeof parentTaskId === "string" ? parentTaskId : undefined },
|
||||||
});
|
});
|
||||||
|
|
||||||
tempIdToTaskId.set(item.tempId, task.id);
|
tempIdToTaskId.set(item.tempId, task.id);
|
||||||
@@ -816,6 +817,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
description: summary.description,
|
description: summary.description,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: summary.suggestedDependencies.length > 0 ? summary.suggestedDependencies : undefined,
|
dependencies: summary.suggestedDependencies.length > 0 ? summary.suggestedDependencies : undefined,
|
||||||
|
source: { sourceType: "api" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update task with suggested size if provided
|
// Update task with suggested size if provided
|
||||||
@@ -953,6 +955,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
description: typeof item.description === "string" ? item.description.trim() : item.title.trim(),
|
description: typeof item.description === "string" ? item.description.trim() : item.title.trim(),
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
|
source: { sourceType: "api", sourceMetadata: { planningSessionId } },
|
||||||
});
|
});
|
||||||
|
|
||||||
tempIdToTaskId.set(item.id, task.id);
|
tempIdToTaskId.set(item.id, task.id);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
reviewLevel,
|
reviewLevel,
|
||||||
executionMode,
|
executionMode,
|
||||||
|
source,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
if (!description || typeof description !== "string") {
|
if (!description || typeof description !== "string") {
|
||||||
throw badRequest("description is required");
|
throw badRequest("description is required");
|
||||||
@@ -169,6 +170,11 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const normalizedSource =
|
||||||
|
source && typeof source === "object" && "sourceType" in source && typeof (source as { sourceType?: unknown }).sourceType === "string"
|
||||||
|
? source
|
||||||
|
: { sourceType: "api" as const };
|
||||||
|
|
||||||
const task = await scopedStore.createTask(
|
const task = await scopedStore.createTask(
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
@@ -188,6 +194,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
summarize,
|
summarize,
|
||||||
reviewLevel: reviewLevel ?? undefined,
|
reviewLevel: reviewLevel ?? undefined,
|
||||||
executionMode: executionMode || undefined,
|
executionMode: executionMode || undefined,
|
||||||
|
source: normalizedSource,
|
||||||
},
|
},
|
||||||
{ onSummarize, settings: { autoSummarizeTitles: settings.autoSummarizeTitles } }
|
{ onSummarize, settings: { autoSummarizeTitles: settings.autoSummarizeTitles } }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3229,6 +3229,11 @@ describe("HeartbeatMonitor", () => {
|
|||||||
description: "Follow-up task",
|
description: "Follow-up task",
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
|
source: {
|
||||||
|
sourceType: "agent_heartbeat",
|
||||||
|
sourceAgentId: "agent-001",
|
||||||
|
sourceRunId: undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3744,6 +3749,11 @@ describe("HeartbeatMonitor", () => {
|
|||||||
description: "Follow-up task",
|
description: "Follow-up task",
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
column: "triage",
|
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 : "";
|
const responseText = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ describe("createDelegateTaskTool", () => {
|
|||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
column: "todo",
|
column: "todo",
|
||||||
assignedAgentId: "agent-001",
|
assignedAgentId: "agent-001",
|
||||||
|
source: { sourceType: "api" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = (result.content[0] as { text: string }).text;
|
const text = (result.content[0] as { text: string }).text;
|
||||||
@@ -281,6 +282,7 @@ describe("createDelegateTaskTool", () => {
|
|||||||
dependencies: ["FN-010"],
|
dependencies: ["FN-010"],
|
||||||
column: "todo",
|
column: "todo",
|
||||||
assignedAgentId: "agent-001",
|
assignedAgentId: "agent-001",
|
||||||
|
source: { sourceType: "api" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = (result.content[0] as { text: string }).text;
|
const text = (result.content[0] as { text: string }).text;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
buildQmdAgentMemorySearchArgs,
|
buildQmdAgentMemorySearchArgs,
|
||||||
createMemoryTools,
|
createMemoryTools,
|
||||||
createTaskCreateTool,
|
createTaskCreateTool,
|
||||||
|
createDelegateTaskTool,
|
||||||
createSendMessageTool,
|
createSendMessageTool,
|
||||||
createReadMessagesTool,
|
createReadMessagesTool,
|
||||||
qmdAgentMemoryCollectionName,
|
qmdAgentMemoryCollectionName,
|
||||||
@@ -82,12 +83,48 @@ describe("createTaskCreateTool", () => {
|
|||||||
description: "Follow-up task",
|
description: "Follow-up task",
|
||||||
dependencies: ["PROJ-001"],
|
dependencies: ["PROJ-001"],
|
||||||
column: "triage",
|
column: "triage",
|
||||||
|
source: undefined,
|
||||||
});
|
});
|
||||||
expect(result.details).toEqual({ taskId: "PROJ-042" });
|
expect(result.details).toEqual({ taskId: "PROJ-042" });
|
||||||
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||||
expect(responseText).toContain("Created PROJ-042: Follow-up task");
|
expect(responseText).toContain("Created PROJ-042: Follow-up task");
|
||||||
expect(responseText).toContain("(depends on: PROJ-001)");
|
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", () => {
|
describe("createMemoryTools", () => {
|
||||||
|
|||||||
@@ -1252,6 +1252,10 @@ describe("CronRunner", () => {
|
|||||||
column: "todo",
|
column: "todo",
|
||||||
modelProvider: "anthropic",
|
modelProvider: "anthropic",
|
||||||
modelId: "claude-sonnet-4-5",
|
modelId: "claude-sonnet-4-5",
|
||||||
|
source: {
|
||||||
|
sourceType: "cron",
|
||||||
|
sourceMetadata: { scheduleId: "test-schedule-id", stepId: expect.any(String) },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,14 @@ describe("PrCommentHandler", () => {
|
|||||||
description: expect.stringContaining("FN-001"),
|
description: expect.stringContaining("FN-001"),
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: ["FN-001"],
|
dependencies: ["FN-001"],
|
||||||
|
source: {
|
||||||
|
sourceType: "api",
|
||||||
|
sourceParentTaskId: "FN-001",
|
||||||
|
sourceMetadata: {
|
||||||
|
prNumber: 42,
|
||||||
|
prUrl: "https://github.com/owner/repo/pull/42",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1387,6 +1387,7 @@ describe("taskCreate tool model inheritance", () => {
|
|||||||
modelId: parentTaskResult?.modelId,
|
modelId: parentTaskResult?.modelId,
|
||||||
validatorModelProvider: parentTaskResult?.validatorModelProvider,
|
validatorModelProvider: parentTaskResult?.validatorModelProvider,
|
||||||
validatorModelId: parentTaskResult?.validatorModelId,
|
validatorModelId: parentTaskResult?.validatorModelId,
|
||||||
|
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getTask).toHaveBeenCalledWith("FN-001");
|
expect(store.getTask).toHaveBeenCalledWith("FN-001");
|
||||||
@@ -1435,6 +1436,7 @@ describe("taskCreate tool model inheritance", () => {
|
|||||||
modelId: parentTask?.modelId,
|
modelId: parentTask?.modelId,
|
||||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||||
validatorModelId: parentTask?.validatorModelId,
|
validatorModelId: parentTask?.validatorModelId,
|
||||||
|
source: { sourceType: "agent_heartbeat", sourceParentTaskId: parentTaskId },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getTask).toHaveBeenCalledWith("FN-NONEXISTENT");
|
expect(store.getTask).toHaveBeenCalledWith("FN-NONEXISTENT");
|
||||||
|
|||||||
@@ -1157,8 +1157,12 @@ export class HeartbeatMonitor {
|
|||||||
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
|
// No-task runs: fn_task_create, fn_list_agents, fn_delegate_task, messaging, memory, fn_heartbeat_done
|
||||||
heartbeatTools = [];
|
heartbeatTools = [];
|
||||||
|
|
||||||
// fn_task_create tool (no tracking needed for no-task runs)
|
// fn_task_create tool
|
||||||
heartbeatTools.push(createTaskCreateTool(taskStore));
|
heartbeatTools.push(createTaskCreateTool(taskStore, {
|
||||||
|
sourceType: "agent_heartbeat",
|
||||||
|
sourceAgentId: agentId,
|
||||||
|
sourceRunId: runContext?.runId,
|
||||||
|
}));
|
||||||
|
|
||||||
// Agent delegation tools
|
// Agent delegation tools
|
||||||
heartbeatTools.push(createListAgentsTool(this.store));
|
heartbeatTools.push(createListAgentsTool(this.store));
|
||||||
@@ -1551,7 +1555,10 @@ export class HeartbeatMonitor {
|
|||||||
const tools: ToolDefinition[] = [];
|
const tools: ToolDefinition[] = [];
|
||||||
|
|
||||||
// Wrap createTaskCreateTool with tracking and agent-link logging
|
// Wrap createTaskCreateTool with tracking and agent-link logging
|
||||||
const baseCreateTool = createTaskCreateTool(taskStore);
|
const baseCreateTool = createTaskCreateTool(taskStore, {
|
||||||
|
sourceType: "agent_heartbeat",
|
||||||
|
sourceAgentId: agentId,
|
||||||
|
});
|
||||||
const trackedCreateTool: ToolDefinition = {
|
const trackedCreateTool: ToolDefinition = {
|
||||||
...baseCreateTool,
|
...baseCreateTool,
|
||||||
execute: async (id: string, params: Static<typeof taskCreateParams>, signal, onUpdate, ctx) => {
|
execute: async (id: string, params: Static<typeof taskCreateParams>, signal, onUpdate, ctx) => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/p
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { join } from "node:path";
|
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 { dailyMemoryPath, ensureOpenClawMemoryFiles, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, resolveMemoryBackend, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
|
||||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
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
|
* @param store - TaskStore for task persistence
|
||||||
* @returns ToolDefinition for the `fn_task_create` tool
|
* @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 {
|
return {
|
||||||
name: "fn_task_create",
|
name: "fn_task_create",
|
||||||
label: "Create Task",
|
label: "Create Task",
|
||||||
@@ -435,6 +438,11 @@ export function createTaskCreateTool(store: TaskStore): ToolDefinition {
|
|||||||
description: params.description,
|
description: params.description,
|
||||||
dependencies: params.dependencies,
|
dependencies: params.dependencies,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
|
source: provenance ? {
|
||||||
|
sourceType: provenance.sourceType,
|
||||||
|
sourceAgentId: provenance.sourceAgentId,
|
||||||
|
sourceRunId: provenance.sourceRunId,
|
||||||
|
} : undefined,
|
||||||
});
|
});
|
||||||
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
||||||
return {
|
return {
|
||||||
@@ -899,6 +907,7 @@ export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskSt
|
|||||||
dependencies: params.dependencies,
|
dependencies: params.dependencies,
|
||||||
column: "todo",
|
column: "todo",
|
||||||
assignedAgentId: params.agent_id,
|
assignedAgentId: params.agent_id,
|
||||||
|
source: { sourceType: "api" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ export class CronRunner {
|
|||||||
} else if (step.type === "ai-prompt") {
|
} else if (step.type === "ai-prompt") {
|
||||||
return this.executeAiPromptStep(step, stepIndex, timeoutMs, stepStartedAt);
|
return this.executeAiPromptStep(step, stepIndex, timeoutMs, stepStartedAt);
|
||||||
} else if (step.type === "create-task") {
|
} else if (step.type === "create-task") {
|
||||||
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt);
|
return this.executeCreateTaskStep(step, stepIndex, stepStartedAt, schedule.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown step type
|
// Unknown step type
|
||||||
@@ -564,6 +564,7 @@ export class CronRunner {
|
|||||||
step: AutomationStep,
|
step: AutomationStep,
|
||||||
stepIndex: number,
|
stepIndex: number,
|
||||||
startedAt: string,
|
startedAt: string,
|
||||||
|
scheduleId: string,
|
||||||
): Promise<AutomationStepResult> {
|
): Promise<AutomationStepResult> {
|
||||||
// Validate that taskDescription is present and non-empty
|
// Validate that taskDescription is present and non-empty
|
||||||
if (!step.taskDescription?.trim()) {
|
if (!step.taskDescription?.trim()) {
|
||||||
@@ -586,6 +587,10 @@ export class CronRunner {
|
|||||||
column: (step.taskColumn as Column) || "triage",
|
column: (step.taskColumn as Column) || "triage",
|
||||||
modelProvider: step.modelProvider?.trim() || undefined,
|
modelProvider: step.modelProvider?.trim() || undefined,
|
||||||
modelId: step.modelId?.trim() || undefined,
|
modelId: step.modelId?.trim() || undefined,
|
||||||
|
source: {
|
||||||
|
sourceType: "cron",
|
||||||
|
sourceMetadata: { scheduleId, stepId: step.id },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3083,7 +3083,7 @@ export class TaskExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createTaskCreateTool(): ToolDefinition {
|
private createTaskCreateTool(): ToolDefinition {
|
||||||
return sharedCreateTaskCreateTool(this.store);
|
return sharedCreateTaskCreateTool(this.store, { sourceType: "api" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTaskDocumentWriteTool(taskId: string): ToolDefinition {
|
private createTaskDocumentWriteTool(taskId: string): ToolDefinition {
|
||||||
|
|||||||
@@ -265,6 +265,14 @@ export class MissionExecutionLoop extends EventEmitter {
|
|||||||
column: "in-progress",
|
column: "in-progress",
|
||||||
missionId,
|
missionId,
|
||||||
sliceId: feature.sliceId,
|
sliceId: feature.sliceId,
|
||||||
|
source: {
|
||||||
|
sourceType: "automation",
|
||||||
|
sourceMetadata: {
|
||||||
|
missionId,
|
||||||
|
featureId: feature.id,
|
||||||
|
sliceId: feature.sliceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
validationTaskId = validationTask.id;
|
validationTaskId = validationTask.id;
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,11 @@ Please review the PR comments and address any remaining issues.`;
|
|||||||
description,
|
description,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
dependencies: [originalTaskId],
|
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}`);
|
prMonitorLog.log(`Created follow-up task ${task.id} for PR #${prInfo.number}`);
|
||||||
|
|||||||
@@ -1233,6 +1233,10 @@ export class ProjectEngine {
|
|||||||
description: followUpDescription,
|
description: followUpDescription,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
priority: "high",
|
priority: "high",
|
||||||
|
source: {
|
||||||
|
sourceType: "recovery",
|
||||||
|
sourceParentTaskId: taskId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await store.addTaskComment(
|
await store.addTaskComment(
|
||||||
taskId,
|
taskId,
|
||||||
@@ -1368,6 +1372,10 @@ export class ProjectEngine {
|
|||||||
`Last merge error: ${errorMsg}`,
|
`Last merge error: ${errorMsg}`,
|
||||||
column: "triage",
|
column: "triage",
|
||||||
priority: "high",
|
priority: "high",
|
||||||
|
source: {
|
||||||
|
sourceType: "recovery",
|
||||||
|
sourceParentTaskId: taskId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await store.addTaskComment(
|
await store.addTaskComment(
|
||||||
taskId,
|
taskId,
|
||||||
|
|||||||
@@ -385,6 +385,10 @@ export class RoutineRunner {
|
|||||||
column: (step.taskColumn as Column) || "triage",
|
column: (step.taskColumn as Column) || "triage",
|
||||||
modelProvider: step.modelProvider?.trim() || undefined,
|
modelProvider: step.modelProvider?.trim() || undefined,
|
||||||
modelId: step.modelId?.trim() || undefined,
|
modelId: step.modelId?.trim() || undefined,
|
||||||
|
source: {
|
||||||
|
sourceType: "automation",
|
||||||
|
sourceMetadata: { routineId: routine.id, stepId: step.id },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const task = await this.options.taskStore.createTask(taskInput);
|
const task = await this.options.taskStore.createTask(taskInput);
|
||||||
|
|||||||
@@ -1573,6 +1573,10 @@ export class TriageProcessor {
|
|||||||
modelId: parentTask?.modelId,
|
modelId: parentTask?.modelId,
|
||||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||||
validatorModelId: parentTask?.validatorModelId,
|
validatorModelId: parentTask?.validatorModelId,
|
||||||
|
source: {
|
||||||
|
sourceType: "agent_heartbeat",
|
||||||
|
sourceParentTaskId: options.parentTaskId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track the created subtask
|
// Track the created subtask
|
||||||
|
|||||||
Reference in New Issue
Block a user