- 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
235 lines
6.7 KiB
TypeScript
235 lines
6.7 KiB
TypeScript
import type { TaskStore } from "@fusion/core";
|
|
import type { PrInfo } from "@fusion/core";
|
|
import { prMonitorLog } from "./logger.js";
|
|
|
|
interface PrComment {
|
|
id: number;
|
|
body: string;
|
|
user: { login: string };
|
|
created_at: string;
|
|
updated_at: string;
|
|
html_url: string;
|
|
}
|
|
|
|
/**
|
|
* Analyzes PR comments for actionable feedback and creates
|
|
* steering comments or follow-up tasks.
|
|
*/
|
|
export class PrCommentHandler {
|
|
// Keywords that suggest actionable feedback
|
|
private readonly ACTION_KEYWORDS = [
|
|
"fix",
|
|
"change",
|
|
"update",
|
|
"remove",
|
|
"add",
|
|
"should",
|
|
"need to",
|
|
"needs to",
|
|
"please",
|
|
"consider",
|
|
"suggest",
|
|
"recommend",
|
|
];
|
|
|
|
// Non-actionable patterns to filter out
|
|
private readonly NON_ACTIONABLE_PATTERNS = [
|
|
/^\s*lgtm\s*$/i,
|
|
/^\s*looks? good\s*$/i,
|
|
/^\s*thanks?\s*$/i,
|
|
/^\s*thank you\s*$/i,
|
|
/^\s*nice\s*$/i,
|
|
/^\s*great\s*$/i,
|
|
/^\s*awesome\s*$/i,
|
|
/^\s*👍\s*$/,
|
|
/^\s*✅\s*$/,
|
|
];
|
|
|
|
constructor(private store: TaskStore) {}
|
|
|
|
/**
|
|
* Process new PR comments for a task.
|
|
* Called by PrMonitor when new comments are detected.
|
|
*/
|
|
async handleNewComments(
|
|
taskId: string,
|
|
prInfo: PrInfo,
|
|
comments: PrComment[]
|
|
): Promise<void> {
|
|
for (const comment of comments) {
|
|
await this.processComment(taskId, prInfo, comment);
|
|
}
|
|
}
|
|
|
|
private async processComment(
|
|
taskId: string,
|
|
prInfo: PrInfo,
|
|
comment: PrComment
|
|
): Promise<void> {
|
|
// Skip non-actionable comments
|
|
if (this.isNonActionable(comment.body)) {
|
|
prMonitorLog.log(`Skipping non-actionable comment #${comment.id}`);
|
|
return;
|
|
}
|
|
|
|
// Check if comment contains actionable feedback
|
|
const isActionable = this.isActionable(comment.body);
|
|
const hasCodeSuggestions = this.hasCodeBlock(comment.body);
|
|
|
|
if (!isActionable && !hasCodeSuggestions) {
|
|
prMonitorLog.log(`Comment #${comment.id} does not contain actionable feedback`);
|
|
return;
|
|
}
|
|
|
|
// Build comment text
|
|
const text = this.buildCommentText(prInfo, comment, hasCodeSuggestions);
|
|
|
|
try {
|
|
await this.store.addTaskComment(taskId, text, "agent");
|
|
prMonitorLog.log(`Added comment for PR review #${comment.id}`);
|
|
} catch (err) {
|
|
prMonitorLog.error(`Failed to add comment for ${taskId}:`, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a comment is non-actionable (LGTM, thanks, etc.)
|
|
*/
|
|
private isNonActionable(body: string): boolean {
|
|
const trimmed = body.trim();
|
|
return this.NON_ACTIONABLE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
}
|
|
|
|
/**
|
|
* Check if a comment contains actionable feedback keywords.
|
|
*/
|
|
private isActionable(body: string): boolean {
|
|
const lowerBody = body.toLowerCase();
|
|
return this.ACTION_KEYWORDS.some((keyword) => lowerBody.includes(keyword));
|
|
}
|
|
|
|
/**
|
|
* Check if a comment contains code blocks suggesting changes.
|
|
*/
|
|
private hasCodeBlock(body: string): boolean {
|
|
// Look for code blocks (``` or `code`)
|
|
return /```[\s\S]*?```/.test(body) || /`[^`]+`/.test(body);
|
|
}
|
|
|
|
/**
|
|
* Build comment text from PR review comment.
|
|
*/
|
|
private buildCommentText(
|
|
prInfo: PrInfo,
|
|
comment: PrComment,
|
|
hasCodeSuggestions: boolean
|
|
): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push(`**PR Review Feedback** from @${comment.user.login}`);
|
|
lines.push(`**PR:** #${prInfo.number} (${prInfo.status})`);
|
|
if (prInfo.status !== "open") {
|
|
lines.push(`**Note:** This PR is already ${prInfo.status}. Treat the feedback as follow-up work.`);
|
|
}
|
|
lines.push("");
|
|
|
|
// Truncate comment body if too long
|
|
const maxBodyLength = 500;
|
|
let body = comment.body.trim();
|
|
if (body.length > maxBodyLength) {
|
|
body = body.slice(0, maxBodyLength) + "...";
|
|
}
|
|
lines.push(body);
|
|
lines.push("");
|
|
|
|
if (hasCodeSuggestions) {
|
|
lines.push("💡 This comment contains code suggestions. Please review and apply if appropriate.");
|
|
}
|
|
|
|
lines.push(`[View on GitHub](${comment.html_url})`);
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Handle "changes requested" PR review state.
|
|
* Moves the task back to in-progress with reviewer feedback as a steering comment,
|
|
* closing the feedback loop so the agent can address the requested changes.
|
|
*/
|
|
async handleChangesRequested(
|
|
taskId: string,
|
|
prInfo: PrInfo,
|
|
reviewerLogin: string,
|
|
reviewBody: string,
|
|
): Promise<void> {
|
|
try {
|
|
const task = await this.store.getTask(taskId);
|
|
if (task.column !== "in-review") {
|
|
prMonitorLog.log(`Task ${taskId} not in-review (${task.column}), skipping changes-requested handling`);
|
|
return;
|
|
}
|
|
|
|
// Add reviewer feedback as a steering comment
|
|
const feedbackText = [
|
|
`**Changes Requested** by @${reviewerLogin} on PR #${prInfo.number}`,
|
|
"",
|
|
reviewBody ? reviewBody.slice(0, 800) : "(no review body)",
|
|
"",
|
|
"Please address the requested changes and update the PR.",
|
|
].join("\n");
|
|
|
|
await this.store.addTaskComment(taskId, feedbackText, "agent");
|
|
await this.store.moveTask(taskId, "in-progress");
|
|
await this.store.logEntry(
|
|
taskId,
|
|
`PR #${prInfo.number}: changes requested by @${reviewerLogin} — moved back to in-progress`,
|
|
);
|
|
prMonitorLog.log(`Task ${taskId} moved to in-progress after changes requested on PR #${prInfo.number}`);
|
|
} catch (err) {
|
|
prMonitorLog.error(`Failed to handle changes-requested for ${taskId}:`, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a follow-up task when a PR is closed with unaddressed feedback.
|
|
* This is called when a PR is merged or closed.
|
|
*/
|
|
async createFollowUpTask(
|
|
originalTaskId: string,
|
|
prInfo: PrInfo,
|
|
unaddressedComments: PrComment[]
|
|
): Promise<void> {
|
|
if (unaddressedComments.length === 0) return;
|
|
|
|
const summary = unaddressedComments
|
|
.map((c) => `- @${c.user.login}: ${c.body.slice(0, 100).trim()}${c.body.length > 100 ? "..." : ""}`)
|
|
.join("\n");
|
|
|
|
const description = `Follow-up for ${originalTaskId}
|
|
|
|
PR #${prInfo.number} was ${prInfo.status} with unaddressed feedback:
|
|
|
|
${summary}
|
|
|
|
Please review the PR comments and address any remaining issues.`;
|
|
|
|
try {
|
|
const task = await this.store.createTask({
|
|
title: `Follow-up: Address PR #${prInfo.number} feedback`,
|
|
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}`);
|
|
} catch (err) {
|
|
prMonitorLog.error(`Failed to create follow-up task:`, err);
|
|
}
|
|
}
|
|
}
|