FN-6237: apply frontend UX criteria as workflow policy
Move frontend UX criteria out of triage self-instructions and into deterministic workflow policy. - Add a shared frontend UX policy helper that matches frontend file scopes and injects the byte-equivalent checklist idempotently. - Apply the policy during triage finalization and persist the updated PROMPT.md when applicable. - Replace the legacy prompt-based injection instructions with a pointer to the deterministic policy. - Cover path matching, insertion behavior, and checklist parity with focused core tests. - Add a minor changeset for the published Fusion package. Files changed: .changeset/fn-6237-frontend-ux-policy.md | 5 + .../core/src/__tests__/frontend-ux-policy.test.ts | 119 +++++++++++++++++ packages/core/src/agent-prompts.ts | 29 +--- packages/core/src/frontend-ux-policy.ts | 148 +++++++++++++++++++++ packages/core/src/index.ts | 1 + packages/engine/src/triage.ts | 19 ++- 6 files changed, 291 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-6237 Fusion-Task-Lineage: 7ac95064-9ec6-4760-8609-20c35075aca1
This commit is contained in:
5
.changeset/fn-6237-frontend-ux-policy.md
Normal file
5
.changeset/fn-6237-frontend-ux-policy.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Move Frontend UX criteria injection from AI self-instructions into deterministic engine-applied workflow policy, preserving the byte-equivalent checklist and idempotent insertion behavior.
|
||||||
119
packages/core/src/__tests__/frontend-ux-policy.test.ts
Normal file
119
packages/core/src/__tests__/frontend-ux-policy.test.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
FRONTEND_UX_CRITERIA_SECTION,
|
||||||
|
applyFrontendUxCriteria,
|
||||||
|
matchesFrontendUxPath,
|
||||||
|
} from "../frontend-ux-policy.js";
|
||||||
|
import { WORKFLOW_STEP_TEMPLATES } from "../types.js";
|
||||||
|
|
||||||
|
const EXACT_FRONTEND_UX_CRITERIA = `## Frontend UX Criteria
|
||||||
|
|
||||||
|
- [ ] **Design tokens only** — no hardcoded \`px\` values except \`0\`, no hardcoded hex/rgb colors; use CSS custom properties (\`--color-*\`, \`--spacing-*\`, etc.)
|
||||||
|
- [ ] **Icon sizing** — match the surrounding component's icon size convention (default lucide size unless the local pattern already uses an explicit \`size={N}\`)
|
||||||
|
- [ ] **Semantic color tokens for status** — use \`--color-error\` for stderr/error states, \`--color-warning\` for starting/pending states; never hardcode status colors
|
||||||
|
- [ ] **Component reuse** — reach for existing classes (\`.btn\`, \`.btn-icon\`, \`.card\`, \`.input\`) before writing one-off styles
|
||||||
|
- [ ] **Responsive scaffolding** — add \`@media (max-width: 768px)\` overrides for any new layout; verify mobile usability
|
||||||
|
- [ ] **Single canonical nav destination** — each route must appear in exactly one of: Header primary nav, Header overflow menu, or MobileNavBar More; no duplicates across all three
|
||||||
|
- [ ] **Status-indicator dot convention** — use the existing \`.status-dot\` pattern (size, border, animation) rather than custom dot styling
|
||||||
|
- [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page
|
||||||
|
`;
|
||||||
|
|
||||||
|
function promptWithFileScope(paths: string[]): string {
|
||||||
|
return `# Task: FN-0000 - Example
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
Implement the requested change without disturbing surrounding behavior.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
${paths.map((path) => `- \`${path}\``).join("\n")}
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Works as expected
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractInsertedCriteria(prompt: string): string {
|
||||||
|
const start = prompt.indexOf("## Frontend UX Criteria");
|
||||||
|
expect(start).toBeGreaterThanOrEqual(0);
|
||||||
|
const rest = prompt.slice(start);
|
||||||
|
expect(rest.slice(FRONTEND_UX_CRITERIA_SECTION.length)).toMatch(/^\n## File Scope/);
|
||||||
|
return rest.slice(0, FRONTEND_UX_CRITERIA_SECTION.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("frontend UX policy", () => {
|
||||||
|
it("preserves the byte-exact criteria section fixture", () => {
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION).toBe(EXACT_FRONTEND_UX_CRITERIA);
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION.endsWith("\n")).toBe(true);
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION.endsWith("\n\n")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["dashboard package", "packages/dashboard/src/server.ts"],
|
||||||
|
["app components", "packages/plugin/app/components/Button.tsx"],
|
||||||
|
["app hooks", "packages/plugin/app/hooks/useThing.ts"],
|
||||||
|
["app css", "packages/plugin/app/layout.css"],
|
||||||
|
["app tsx", "packages/plugin/app/routes.tsx"],
|
||||||
|
])("injects exactly once after Mission for %s scope", (_label, path) => {
|
||||||
|
const original = promptWithFileScope([path]);
|
||||||
|
const injected = applyFrontendUxCriteria(original);
|
||||||
|
|
||||||
|
expect(injected).toContain(FRONTEND_UX_CRITERIA_SECTION);
|
||||||
|
expect(extractInsertedCriteria(injected)).toBe(FRONTEND_UX_CRITERIA_SECTION);
|
||||||
|
expect(injected.match(/## Frontend UX Criteria/g)).toHaveLength(1);
|
||||||
|
expect(injected).toMatch(/## Mission\n\nImplement the requested change without disturbing surrounding behavior\.\n\n## Frontend UX Criteria\n\n- \[ \] \*\*Design tokens only\*\*/);
|
||||||
|
expect(applyFrontendUxCriteria(injected)).toBe(injected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["backend", "packages/engine/src/triage.ts"],
|
||||||
|
["config json", "package.json"],
|
||||||
|
["eslint config", "eslint.config.mjs"],
|
||||||
|
["docs", "docs/dashboard-guide.md"],
|
||||||
|
["dashboard src css excluded from rule 4 but matched by dashboard package", "packages/dashboard/src/styles.css", true],
|
||||||
|
["component css covered by component rule, not rule 4", "packages/plugin/app/components/Button.css", true],
|
||||||
|
])("matches the expected frontend classification for %s", (_label, path, expected = false) => {
|
||||||
|
expect(matchesFrontendUxPath(path)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not inject for backend-only, config-only, or docs-only file scopes", () => {
|
||||||
|
for (const path of ["packages/engine/src/triage.ts", "package.json", "eslint.config.mjs", "docs/testing.md"]) {
|
||||||
|
const original = promptWithFileScope([path]);
|
||||||
|
expect(applyFrontendUxCriteria(original)).toBe(original);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses caller-provided file scope paths without reparsing prompt markdown", () => {
|
||||||
|
const promptWithoutFileScope = `# Task: FN-0000 - Example
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
Implement dashboard UI.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Works as expected
|
||||||
|
`;
|
||||||
|
|
||||||
|
const injected = applyFrontendUxCriteria(promptWithoutFileScope, ["packages/dashboard/app/routes.tsx"]);
|
||||||
|
|
||||||
|
expect(injected).toContain(FRONTEND_UX_CRITERIA_SECTION);
|
||||||
|
expect(injected.match(/## Frontend UX Criteria/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps checklist tokens aligned with the frontend UX design persona", () => {
|
||||||
|
const persona = WORKFLOW_STEP_TEMPLATES.find((template) => template.id === "frontend-ux-design");
|
||||||
|
expect(persona?.name).toBe("Frontend UX Design");
|
||||||
|
expect(persona?.prompt).toContain("design tokens");
|
||||||
|
expect(persona?.prompt).toContain("Component Reuse");
|
||||||
|
expect(persona?.prompt).toContain("Responsive Behavior");
|
||||||
|
expect(persona?.prompt).toContain("Visual Hierarchy");
|
||||||
|
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Design tokens only");
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Component reuse");
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Responsive scaffolding");
|
||||||
|
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Visual hierarchy preserved");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -508,34 +508,7 @@ If the task targets a different task ID (audit, forensic walk, historical reconc
|
|||||||
- \`.fusion/\` is gitignored, so a fresh worktree from \`main\` does **not** include \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`. The running worktree's own \`.fusion/\` (if present) is scratch/session state for the running task only, not source of truth.
|
- \`.fusion/\` is gitignored, so a fresh worktree from \`main\` does **not** include \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`. The running worktree's own \`.fusion/\` (if present) is scratch/session state for the running task only, not source of truth.
|
||||||
- Prefer \`fn_task_get\` / \`fn_task_list\` when the target task ID is known; fall back to project-root filesystem reads only when tools cannot provide needed evidence.
|
- Prefer \`fn_task_get\` / \`fn_task_list\` when the target task ID is known; fall back to project-root filesystem reads only when tools cannot provide needed evidence.
|
||||||
|
|
||||||
## Frontend UX Criteria Injection
|
<!-- Frontend UX criteria are applied deterministically by packages/core/src/frontend-ux-policy.ts and mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts. -->`;;
|
||||||
|
|
||||||
<!-- UX criteria mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts — keep them aligned. -->
|
|
||||||
|
|
||||||
If the derived **File Scope** touches any of the following paths:
|
|
||||||
- \`packages/dashboard/**\`
|
|
||||||
- \`packages/*/app/components/**\`
|
|
||||||
- \`packages/*/app/hooks/**\`
|
|
||||||
- Any \`*.css\` or \`*.tsx\` file inside a dashboard-like package
|
|
||||||
|
|
||||||
…then **PREPEND** a \`## Frontend UX Criteria\` section to the generated PROMPT.md, placed immediately after the \`## Mission\` section.
|
|
||||||
|
|
||||||
Use this exact checklist (keep it verbatim — do not expand or reorder):
|
|
||||||
|
|
||||||
\`\`\`markdown
|
|
||||||
## Frontend UX Criteria
|
|
||||||
|
|
||||||
- [ ] **Design tokens only** — no hardcoded \`px\` values except \`0\`, no hardcoded hex/rgb colors; use CSS custom properties (\`--color-*\`, \`--spacing-*\`, etc.)
|
|
||||||
- [ ] **Icon sizing** — match the surrounding component's icon size convention (default lucide size unless the local pattern already uses an explicit \`size={N}\`)
|
|
||||||
- [ ] **Semantic color tokens for status** — use \`--color-error\` for stderr/error states, \`--color-warning\` for starting/pending states; never hardcode status colors
|
|
||||||
- [ ] **Component reuse** — reach for existing classes (\`.btn\`, \`.btn-icon\`, \`.card\`, \`.input\`) before writing one-off styles
|
|
||||||
- [ ] **Responsive scaffolding** — add \`@media (max-width: 768px)\` overrides for any new layout; verify mobile usability
|
|
||||||
- [ ] **Single canonical nav destination** — each route must appear in exactly one of: Header primary nav, Header overflow menu, or MobileNavBar More; no duplicates across all three
|
|
||||||
- [ ] **Status-indicator dot convention** — use the existing \`.status-dot\` pattern (size, border, animation) rather than custom dot styling
|
|
||||||
- [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Only inject this section when the task genuinely touches frontend UI. Omit it for backend-only, config-only, or documentation-only tasks.`;;
|
|
||||||
|
|
||||||
// FN-6235: single source for the built-in reviewer policy; the engine REVIEWER_SYSTEM_PROMPT duplicate was removed.
|
// FN-6235: single source for the built-in reviewer policy; the engine REVIEWER_SYSTEM_PROMPT duplicate was removed.
|
||||||
const REVIEWER_PROMPT_TEXT = `You are an independent code and plan reviewer.
|
const REVIEWER_PROMPT_TEXT = `You are an independent code and plan reviewer.
|
||||||
|
|||||||
148
packages/core/src/frontend-ux-policy.ts
Normal file
148
packages/core/src/frontend-ux-policy.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Frontend UX criteria policy for generated task specifications.
|
||||||
|
*
|
||||||
|
* The checklist mirrors the `frontend-ux-design` reviewer persona in
|
||||||
|
* packages/core/src/types.ts. Keep this policy byte-equivalent with the legacy
|
||||||
|
* triage prompt checklist and idempotent when applied to generated PROMPT.md
|
||||||
|
* content.
|
||||||
|
*
|
||||||
|
* Rule 4 deterministically expands the legacy "Any CSS or TSX file inside a
|
||||||
|
* dashboard-like package" rule: match files under a package `app/` directory
|
||||||
|
* ending in `.css` or `.tsx`, excluding the `components/` and `hooks/` subtrees
|
||||||
|
* already covered by rules 2 and 3.
|
||||||
|
*/
|
||||||
|
export const FRONTEND_UX_PATH_GLOBS = [
|
||||||
|
"packages/dashboard/**",
|
||||||
|
"packages/*/app/components/**",
|
||||||
|
"packages/*/app/hooks/**",
|
||||||
|
"packages/*/app/**/*.css",
|
||||||
|
"packages/*/app/**/*.tsx",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-exact Frontend UX Criteria section copied from the legacy triage prompt.
|
||||||
|
* The section intentionally ends with exactly one trailing newline.
|
||||||
|
*/
|
||||||
|
export const FRONTEND_UX_CRITERIA_SECTION = `## Frontend UX Criteria
|
||||||
|
|
||||||
|
- [ ] **Design tokens only** — no hardcoded \`px\` values except \`0\`, no hardcoded hex/rgb colors; use CSS custom properties (\`--color-*\`, \`--spacing-*\`, etc.)
|
||||||
|
- [ ] **Icon sizing** — match the surrounding component's icon size convention (default lucide size unless the local pattern already uses an explicit \`size={N}\`)
|
||||||
|
- [ ] **Semantic color tokens for status** — use \`--color-error\` for stderr/error states, \`--color-warning\` for starting/pending states; never hardcode status colors
|
||||||
|
- [ ] **Component reuse** — reach for existing classes (\`.btn\`, \`.btn-icon\`, \`.card\`, \`.input\`) before writing one-off styles
|
||||||
|
- [ ] **Responsive scaffolding** — add \`@media (max-width: 768px)\` overrides for any new layout; verify mobile usability
|
||||||
|
- [ ] **Single canonical nav destination** — each route must appear in exactly one of: Header primary nav, Header overflow menu, or MobileNavBar More; no duplicates across all three
|
||||||
|
- [ ] **Status-indicator dot convention** — use the existing \`.status-dot\` pattern (size, border, animation) rather than custom dot styling
|
||||||
|
- [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FRONTEND_UX_HEADING = "## Frontend UX Criteria";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure deterministic injection helper. When `fileScopePaths` is omitted, the
|
||||||
|
* helper parses `## File Scope` from the supplied prompt markdown using the same
|
||||||
|
* section shape as the engine triage parser.
|
||||||
|
*/
|
||||||
|
export function applyFrontendUxCriteria(promptMarkdown: string, fileScopePaths?: string[]): string {
|
||||||
|
if (promptMarkdown.includes(FRONTEND_UX_HEADING)) {
|
||||||
|
return promptMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = fileScopePaths ?? parseFileScopeFromPromptMarkdown(promptMarkdown);
|
||||||
|
if (!paths.some((path) => matchesFrontendUxPath(path))) {
|
||||||
|
return promptMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertFrontendUxCriteriaAfterMission(promptMarkdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesFrontendUxPath(path: string): boolean {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (!normalized) return false;
|
||||||
|
|
||||||
|
if (matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[0])) return true;
|
||||||
|
if (matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[1])) return true;
|
||||||
|
if (matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[2])) return true;
|
||||||
|
|
||||||
|
const isAppCssOrTsx = matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[3])
|
||||||
|
|| matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[4]);
|
||||||
|
if (!isAppCssOrTsx) return false;
|
||||||
|
|
||||||
|
return !matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[1])
|
||||||
|
&& !matchGlob(normalized, FRONTEND_UX_PATH_GLOBS[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFileScopeFromPromptMarkdown(text: string): string[] {
|
||||||
|
const match = text.match(/^##\s+File Scope\s*\n([\s\S]*?)(?=^##\s+|$)/m);
|
||||||
|
if (!match) return [];
|
||||||
|
|
||||||
|
const entries: string[] = [];
|
||||||
|
for (const line of match[1].split("\n")) {
|
||||||
|
const cleaned = normalizeFileScopeLine(line);
|
||||||
|
if (cleaned) entries.push(cleaned);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFileScopeLine(line: string): string {
|
||||||
|
let cleaned = line.trim();
|
||||||
|
if (!cleaned || cleaned.startsWith("<!--")) return "";
|
||||||
|
cleaned = cleaned.replace(/^[-*]\s+/, "").trim();
|
||||||
|
cleaned = cleaned.replace(/^`([^`]+)`.*$/, "$1").trim();
|
||||||
|
cleaned = cleaned.replace(/`/g, "").trim();
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertFrontendUxCriteriaAfterMission(content: string): string {
|
||||||
|
const missionMatch = content.match(/^##\s+Mission\s*$/m);
|
||||||
|
if (!missionMatch || missionMatch.index === undefined) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerEnd = missionMatch.index + missionMatch[0].length;
|
||||||
|
const rest = content.slice(headerEnd);
|
||||||
|
const nextHeading = rest.search(/\n##\s/);
|
||||||
|
const sectionEndAbsolute = nextHeading === -1 ? content.length : headerEnd + nextHeading;
|
||||||
|
const before = content.slice(0, sectionEndAbsolute).trimEnd();
|
||||||
|
const after = content.slice(sectionEndAbsolute);
|
||||||
|
return `${before}\n\n${FRONTEND_UX_CRITERIA_SECTION}${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if a path matches a glob pattern (simple glob support: * and **). */
|
||||||
|
function matchGlob(path: string, pattern: string): boolean {
|
||||||
|
const regexPattern = globToRegexPattern(normalizePath(pattern));
|
||||||
|
return new RegExp(`^${regexPattern}$`).test(normalizePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function globToRegexPattern(pattern: string): string {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < pattern.length; i += 1) {
|
||||||
|
const char = pattern[i];
|
||||||
|
const next = pattern[i + 1];
|
||||||
|
const afterNext = pattern[i + 2];
|
||||||
|
|
||||||
|
if (char === "*" && next === "*" && afterNext === "/") {
|
||||||
|
out += "(?:.*/)?";
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "*" && next === "*") {
|
||||||
|
out += ".*";
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "*") {
|
||||||
|
out += "[^/]*";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += escapeRegex(char);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(char: string): string {
|
||||||
|
return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
return path.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export type {
|
|||||||
} from "./branch-assignment.js";
|
} from "./branch-assignment.js";
|
||||||
export { customProviderRegistryKey } from "./custom-provider-key.js";
|
export { customProviderRegistryKey } from "./custom-provider-key.js";
|
||||||
export { redactSecrets } from "./redact-secrets.js";
|
export { redactSecrets } from "./redact-secrets.js";
|
||||||
|
export * from "./frontend-ux-policy.js";
|
||||||
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
||||||
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
|
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
resolveAgentMemoryInclusionMode,
|
resolveAgentMemoryInclusionMode,
|
||||||
extractIntentSignature,
|
extractIntentSignature,
|
||||||
findNearDuplicates,
|
findNearDuplicates,
|
||||||
|
applyFrontendUxCriteria,
|
||||||
type NearDuplicateCandidate,
|
type NearDuplicateCandidate,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||||
@@ -64,7 +65,7 @@ import { withRateLimitRetry } from "./rate-limit-retry.js";
|
|||||||
import { computeRecoveryDecision, formatDelay, MAX_RECOVERY_RETRIES } from "./recovery-policy.js";
|
import { computeRecoveryDecision, formatDelay, MAX_RECOVERY_RETRIES } from "./recovery-policy.js";
|
||||||
import type { StuckTaskDetector } from "./stuck-task-detector.js";
|
import type { StuckTaskDetector } from "./stuck-task-detector.js";
|
||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import {
|
import {
|
||||||
@@ -2150,7 +2151,7 @@ export class TriageProcessor {
|
|||||||
|
|
||||||
private async finalizeApprovedTask(
|
private async finalizeApprovedTask(
|
||||||
task: Task,
|
task: Task,
|
||||||
written: string,
|
writtenInput: string,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
options: {
|
options: {
|
||||||
isReplan?: boolean;
|
isReplan?: boolean;
|
||||||
@@ -2158,6 +2159,7 @@ export class TriageProcessor {
|
|||||||
recoveryLogAction?: string;
|
recoveryLogAction?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
let written = writtenInput;
|
||||||
const dupMatch = written.match(/^DUPLICATE:\s*([A-Z]+-\d+)/i);
|
const dupMatch = written.match(/^DUPLICATE:\s*([A-Z]+-\d+)/i);
|
||||||
|
|
||||||
if (dupMatch) {
|
if (dupMatch) {
|
||||||
@@ -2257,6 +2259,19 @@ export class TriageProcessor {
|
|||||||
} catch {
|
} catch {
|
||||||
// Fail open on persisted PROMPT.md parsing and keep using the in-memory parse.
|
// Fail open on persisted PROMPT.md parsing and keep using the in-memory parse.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promptWithFrontendUxCriteria = applyFrontendUxCriteria(written, parsedFileScope);
|
||||||
|
if (promptWithFrontendUxCriteria !== written) {
|
||||||
|
const promptPath = join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
||||||
|
try {
|
||||||
|
await writeFile(promptPath, promptWithFrontendUxCriteria, "utf-8");
|
||||||
|
written = promptWithFrontendUxCriteria;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
planLog.warn(`${task.id}: failed to write Frontend UX Criteria to PROMPT.md (${message})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let taskIntentSignature: ReturnType<typeof extractIntentSignature> = {
|
let taskIntentSignature: ReturnType<typeof extractIntentSignature> = {
|
||||||
routePaths: [],
|
routePaths: [],
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user