Files
fusion/packages/engine/src/required-workflow-artifacts.ts
gsxdsm de2cad7535 fix(workflows): reject missing plan review artifacts (#2390)
## 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 -->
2026-07-21 17:06:26 -07:00

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;
}