## Summary Workflows could reach Plan Review without an authoritative PROMPT.md, producing misleading approvals or stranding the task. Planning now verifies durable prompt persistence before releasing the card, and every workflow entry/review surface fails closed when its required plan is absent. Confirmed absence triggers bounded automatic replanning; TaskStore read outages retry in place; exhausted recovery parks visibly without consuming review-fix budget or overriding pause, manual-review, terminal, or merge-confirmed state. Related: FN-8455 ## Validation - Focused workflow-artifact, graph-recovery, review, writer, and triage regression suites pass. - @fusion/engine typecheck passes. - Repository lint, changeset validation, and diff checks pass. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Plan Review now fails closed when `PROMPT.md` is missing or blank, returning a revision request with a typed `failureValue`. * Required workflow artifacts are treated as missing unless they exist with non-empty content; read failures are handled separately. * Recovery now deterministically chooses replan vs “park-failed” with bounded retries, and records a `task:required-artifact-missing` audit event. * **Workflow Improvements** * Triage and approval now persist `PROMPT.md` through the dedicated prompt-write flow and verify it was stored exactly. * Optional-group remediation preserves typed required-artifact missing failures for pre-merge fixes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
57 lines
2.2 KiB
TypeScript
57 lines
2.2 KiB
TypeScript
import type { WorkflowIr, WorkflowIrArtifact } from "@fusion/core";
|
|
|
|
export const REQUIRED_ARTIFACT_MISSING_PREFIX = "required-artifact-missing:";
|
|
export const REQUIRED_ARTIFACT_READ_FAILED_PREFIX = "required-artifact-read-failed:";
|
|
|
|
/*
|
|
FNXC:WorkflowArtifacts 2026-07-21-17:00:
|
|
Planning-owned and step-source artifacts are executable workflow contracts, so
|
|
they must contain non-whitespace content before the graph can consume them.
|
|
*/
|
|
export function requiresNonEmptyWorkflowArtifact(artifact: WorkflowIrArtifact): boolean {
|
|
return artifact.producedBy === "planning" || artifact.role === "step-source";
|
|
}
|
|
|
|
export function workflowEntryArtifacts(ir: WorkflowIr): WorkflowIrArtifact[] {
|
|
const artifacts = "artifacts" in ir && Array.isArray(ir.artifacts) ? ir.artifacts : [];
|
|
return artifacts.filter(requiresNonEmptyWorkflowArtifact);
|
|
}
|
|
|
|
export function requiredArtifactMissingValue(keys: readonly string[]): string {
|
|
const normalizedKeys = [...new Set(keys.map((key) => key.trim()).filter(Boolean))];
|
|
if (normalizedKeys.length === 0) {
|
|
throw new Error("At least one required artifact key is needed");
|
|
}
|
|
return `${REQUIRED_ARTIFACT_MISSING_PREFIX}${JSON.stringify(normalizedKeys)}`;
|
|
}
|
|
|
|
export function parseRequiredArtifactMissingValue(value: string | undefined): string[] | null {
|
|
if (!value?.startsWith(REQUIRED_ARTIFACT_MISSING_PREFIX)) return null;
|
|
const payload = value.slice(REQUIRED_ARTIFACT_MISSING_PREFIX.length);
|
|
try {
|
|
const parsed = JSON.parse(payload) as unknown;
|
|
if (Array.isArray(parsed)) {
|
|
const keys = parsed
|
|
.filter((key): key is string => typeof key === "string")
|
|
.map((key) => key.trim())
|
|
.filter(Boolean);
|
|
return keys.length > 0 ? [...new Set(keys)] : null;
|
|
}
|
|
} catch {
|
|
// Pre-JSON signals used a comma-delimited payload; keep them recoverable.
|
|
}
|
|
const keys = payload
|
|
.split(",")
|
|
.map((key) => key.trim())
|
|
.filter(Boolean);
|
|
return keys.length > 0 ? [...new Set(keys)] : null;
|
|
}
|
|
|
|
export function requiredArtifactReadFailedValue(key: string): string {
|
|
return `${REQUIRED_ARTIFACT_READ_FAILED_PREFIX}${key}`;
|
|
}
|
|
|
|
export function isRequiredArtifactReadFailedValue(value: string | undefined): boolean {
|
|
return value?.startsWith(REQUIRED_ARTIFACT_READ_FAILED_PREFIX) === true;
|
|
}
|