Prevent mission-linked done tasks from bypassing assertion validation and recover stalled validation triggers after restart. - require assertion-linked done features to have a passed validator result before slice/milestone completion - keep assertion-linked features in progress when linked tasks move to done/archived until validation passes - restart mission loop recovery when needed and re-trigger done/archived implementing features that still need validation - add regression coverage for scheduler, mission loop recovery, mission store completion gating, and reliability trigger continuity - document the new FN-5715 reliability backstop and recovery behavior Files changed: .changeset/fn-5715-mission-validation-trigger.md | 7 + AGENTS.md | 1 + docs/architecture.md | 1 + docs/missions.md | 11 +- packages/core/src/__tests__/mission-integration.test.ts | 2 +- packages/core/src/__tests__/mission-store.test.ts | 30 +++- packages/core/src/mission-store.ts | 19 ++- packages/engine/src/__tests__/mission-execution-loop.test.ts | 95 +++++++++++++ packages/engine/src/__tests__/reliability-interactions/mission-validation-trigger-gap.test.ts | 151 +++++++++++++++++++++ packages/engine/src/__tests__/scheduler.test.ts | 54 ++++++++ packages/engine/src/mission-autopilot.ts | 7 +- packages/engine/src/mission-execution-loop.ts | 25 ++++ packages/engine/src/mission-feature-sync.ts | 32 ++++- packages/engine/src/scheduler.ts | 12 +- 14 files changed, 431 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-5715 Fusion-Task-Lineage: 98b1cbd6-6b68-40a0-b4b3-c512183adf8b
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import type { MissionFeature, Task, TaskStore } from "@fusion/core";
|
|
import { getTaskCompletionBlockerForStore } from "./task-completion.js";
|
|
|
|
export type MissionFeatureSyncTargetStatus = "done" | "in-progress" | "triaged";
|
|
|
|
export interface MissionFeatureSyncContext {
|
|
hasLinkedAssertions?: boolean;
|
|
}
|
|
|
|
export type MissionFeatureSyncDecision =
|
|
| { kind: "failure"; reason: string }
|
|
| { kind: "blocked"; reason: string }
|
|
| { kind: "update"; status: MissionFeatureSyncTargetStatus; reason: string }
|
|
| { kind: "noop" };
|
|
|
|
export async function reconcileMissionFeatureState(
|
|
taskStore: Pick<TaskStore, "getTask">,
|
|
task: Task,
|
|
feature: Pick<MissionFeature, "id" | "status" | "lastValidatorStatus">,
|
|
context: MissionFeatureSyncContext = {},
|
|
): Promise<MissionFeatureSyncDecision> {
|
|
if (task.status === "failed" && feature.status === "in-progress") {
|
|
return {
|
|
kind: "failure",
|
|
reason: `task ${task.id} failed while feature ${feature.id} is in-progress`,
|
|
};
|
|
}
|
|
|
|
const hasUnvalidatedAssertions = context.hasLinkedAssertions === true
|
|
&& feature.lastValidatorStatus !== "passed";
|
|
|
|
if (task.column === "done") {
|
|
const blocker = await getTaskCompletionBlockerForStore(taskStore, task);
|
|
if (blocker) {
|
|
return { kind: "blocked", reason: blocker };
|
|
}
|
|
|
|
if (hasUnvalidatedAssertions) {
|
|
if (feature.status !== "in-progress") {
|
|
return {
|
|
kind: "update",
|
|
status: "in-progress",
|
|
reason: `task ${task.id} completed; awaiting assertion validation`,
|
|
};
|
|
}
|
|
return { kind: "noop" };
|
|
}
|
|
|
|
if (feature.status !== "done") {
|
|
return {
|
|
kind: "update",
|
|
status: "done",
|
|
reason: `task ${task.id} completed`,
|
|
};
|
|
}
|
|
|
|
return { kind: "noop" };
|
|
}
|
|
|
|
if (task.column === "archived") {
|
|
if (hasUnvalidatedAssertions) {
|
|
if (feature.status !== "in-progress") {
|
|
return {
|
|
kind: "update",
|
|
status: "in-progress",
|
|
reason: `task ${task.id} archived; awaiting assertion validation`,
|
|
};
|
|
}
|
|
return { kind: "noop" };
|
|
}
|
|
|
|
if (feature.status !== "done") {
|
|
return {
|
|
kind: "update",
|
|
status: "done",
|
|
reason: `task ${task.id} was archived after completion`,
|
|
};
|
|
}
|
|
|
|
return { kind: "noop" };
|
|
}
|
|
|
|
if (
|
|
(task.column === "in-progress" || task.column === "in-review")
|
|
&& (feature.status === "triaged" || feature.status === "defined")
|
|
) {
|
|
return {
|
|
kind: "update",
|
|
status: "in-progress",
|
|
reason: task.column === "in-review"
|
|
? `task ${task.id} is in review`
|
|
: `task ${task.id} started`,
|
|
};
|
|
}
|
|
|
|
if (
|
|
(task.column === "triage" || task.column === "todo")
|
|
&& feature.status === "in-progress"
|
|
) {
|
|
return {
|
|
kind: "update",
|
|
status: "triaged",
|
|
reason: `task ${task.id} returned to triage`,
|
|
};
|
|
}
|
|
|
|
return { kind: "noop" };
|
|
}
|