Files
fusion/packages/engine/src/agent-user-comments.ts
gsxdsm 01e04333ce FN-7294: pass user context into workflow reviewers
Pass operator-authored task context into reviewer and workflow gate prompts.

- Include de-duped user comments and legacy steering in Plan Review, code review, and workflow-step prompt agents.
- Preserve reviewer comment context on reduced context-limit retry prompts and refresh comments before in-session review calls.
- Document workflow prompt-agent comment behavior, add regression coverage, and add a patch changeset.

Files changed:
 .changeset/FN-7294-reviewer-comment-context.md     |   7 +
 docs/workflow-steps.md                             |   7 +
 .../src/__tests__/agent-user-comments.test.ts      |  81 ++++++++--
 .../executor-review-step-indexing.test.ts          | 165 ++++++++++++++++++++-
 .../src/__tests__/reviewer-workspace.test.ts       |  39 +++++
 packages/engine/src/__tests__/reviewer.test.ts     |  26 +++-
 packages/engine/src/__tests__/triage.test.ts       |  49 ++++++
 packages/engine/src/agent-user-comments.ts         |  42 ++++--
 packages/engine/src/executor.ts                    |  31 +++-
 packages/engine/src/reviewer.ts                    |  10 +-
 packages/engine/src/triage.ts                      |  20 ++-
 11 files changed, 443 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-7294

Fusion-Task-Lineage: ea5ff33e-99ba-4fdd-9257-9778849b91bb

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-30 12:56:03 -07:00

83 lines
3.2 KiB
TypeScript

import type { SteeringComment, TaskComment } from "@fusion/core";
const DEFAULT_USER_COMMENT_LIMIT = 20;
function commentTimestamp(comment: TaskComment): string {
return comment.updatedAt || comment.createdAt;
}
function timestampMs(comment: TaskComment): number {
const parsed = Date.parse(commentTimestamp(comment));
return Number.isFinite(parsed) ? parsed : 0;
}
function quoteCommentText(text: string): string[] {
const normalized = text.trim();
if (!normalized) return ["> (empty comment)"];
return normalized.split(/\r?\n/).map((line) => `> ${line}`);
}
function normalizeSteeringComment(comment: SteeringComment): TaskComment {
return {
id: comment.id,
text: comment.text,
author: comment.author,
createdAt: comment.createdAt,
};
}
/**
* FNXC:AgentSteering 2026-06-22-00:05:
* Task-detail chat and user comments must reach every agent lane that builds prompts: executor, merger, reviewer, and planner. This helper is the canonical formatter for next-prompt delivery outside the executor's live steering injection path, so merger and reviewer prompts do not drift or duplicate comment logic.
*
* FNXC:AgentSteering 2026-06-30-12:28:
* Reviewer gates are quality gates for explicit operator requirements. Select unified comments plus legacy steering comments here so mandatory Plan Review and optional workflow review nodes receive the same de-duped user-authored context without caller-specific formatting.
*
* FNXC:AgentSteering 2026-06-30-13:18:
* Reviewer callers must request an uncapped selection because older user requirements remain binding quality-gate context; the default limit stays for non-review prompts that need bounded context.
*/
export function selectUserCommentsForAgentContext(
task: { comments?: TaskComment[]; steeringComments?: SteeringComment[] },
opts: { limit?: number | null } = {},
): TaskComment[] {
const limit = opts.limit === null ? null : opts.limit ?? DEFAULT_USER_COMMENT_LIMIT;
if (limit !== null && limit <= 0) return [];
const byId = new Map<string, TaskComment>();
const candidates: TaskComment[] = [
...(task.comments ?? []),
...(task.steeringComments ?? []).map(normalizeSteeringComment),
];
for (const comment of candidates) {
if (comment.author !== "user") continue;
const existing = byId.get(comment.id);
if (!existing || timestampMs(existing) <= timestampMs(comment)) {
byId.set(comment.id, comment);
}
}
const sorted = [...byId.values()].sort((a, b) => timestampMs(a) - timestampMs(b));
return limit === null ? sorted : sorted.slice(-limit);
}
export function buildUserCommentsPromptSection(
comments: TaskComment[],
opts: { heading?: string; intro?: string } = {},
): string {
if (comments.length === 0) return "";
const heading = opts.heading ?? "## User Comments";
const intro = opts.intro ?? "The following user comments were posted on this task. Consider and address this user feedback when completing your agent pass.";
const lines = [heading, "", intro, ""];
for (const comment of comments) {
const timestamp = commentTimestamp(comment);
lines.push(`**${comment.author}** — ${timestamp}`);
lines.push(...quoteCommentText(comment.text));
lines.push("");
}
return lines.join("\n").trimEnd();
}