feat(FEAT-001): add loop state columns and validator run tables for mission execution loop

This commit adds the schema migration and types for the mission execution loop validation system:
- Adds loop state tracking columns to mission_features table (loopState, implementationAttemptCount, validatorAttemptCount, lastValidatorRunId, lastValidatorStatus, generatedFromFeatureId, generatedFromRunId)
- Creates mission_validator_runs table for tracking validation runs
- Creates mission_validator_failures table for assertion failure records
- Creates mission_fix_feature_lineage table for tracking fix feature relationships
- Adds workflowStepRetries column to tasks table for retry tracking
- Adds FEATURE_LOOP_STATES and VALIDATOR_RUN_STATUSES enums
- Updates TaskStore to support workflowStepRetries field
- Updates TaskExecutor to handle workflow step failures with retry logic

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
gsxdsm
2026-04-11 17:06:41 -07:00
parent aa1866cc82
commit 429d5855ee
12 changed files with 658 additions and 23 deletions

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
const index = db
.prepare(

View File

@@ -112,7 +112,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
});
it("seeds lastModified", () => {
@@ -135,7 +135,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
});
it("does not overwrite existing config on re-init", () => {
@@ -742,7 +742,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -767,11 +767,11 @@ describe("schema migrations", () => {
const db = new Database(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
db.close();
});
@@ -787,7 +787,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -811,7 +811,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -915,7 +915,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1281,7 +1281,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 29;
const SCHEMA_VERSION = 31;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -144,6 +144,7 @@ CREATE TABLE IF NOT EXISTS tasks (
planningModelProvider TEXT,
planningModelId TEXT,
mergeRetries INTEGER,
workflowStepRetries INTEGER,
recoveryRetryCount INTEGER,
nextRecoveryAt TEXT,
error TEXT,
@@ -1080,6 +1081,95 @@ export class Database {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxFeatureAssertionsAssertionId ON mission_feature_assertions(assertionId)`);
});
}
// Workflow step failure retry support (FN-1586)
// Adds workflowStepRetries column to track retry attempts for workflow step hard failures
if (version < 30) {
this.applyMigration(30, () => {
this.addColumnIfMissing("tasks", "workflowStepRetries", "INTEGER");
});
}
// Loop state and validator run tables (FEAT-001)
// Adds loop state tracking columns to mission_features for the execution loop:
// implementationAttemptCount, validatorAttemptCount, lastValidatorRunId, lastValidatorStatus,
// generatedFromFeatureId, generatedFromRunId, loopState
if (version < 31) {
this.applyMigration(31, () => {
// Add loop state columns to mission_features
this.addColumnIfMissing("mission_features", "loopState", "TEXT NOT NULL DEFAULT 'idle'");
this.addColumnIfMissing("mission_features", "implementationAttemptCount", "INTEGER NOT NULL DEFAULT 0");
this.addColumnIfMissing("mission_features", "validatorAttemptCount", "INTEGER NOT NULL DEFAULT 0");
this.addColumnIfMissing("mission_features", "lastValidatorRunId", "TEXT");
this.addColumnIfMissing("mission_features", "lastValidatorStatus", "TEXT");
this.addColumnIfMissing("mission_features", "generatedFromFeatureId", "TEXT");
this.addColumnIfMissing("mission_features", "generatedFromRunId", "TEXT");
// Create mission_validator_runs table for tracking validation runs
this.db.exec(`
CREATE TABLE IF NOT EXISTS mission_validator_runs (
id TEXT PRIMARY KEY,
featureId TEXT NOT NULL,
milestoneId TEXT NOT NULL,
sliceId TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running',
triggerType TEXT,
implementationAttempt INTEGER NOT NULL DEFAULT 0,
validatorAttempt INTEGER NOT NULL DEFAULT 0,
summary TEXT,
blockedReason TEXT,
startedAt TEXT NOT NULL,
completedAt TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (featureId) REFERENCES mission_features(id) ON DELETE CASCADE,
FOREIGN KEY (milestoneId) REFERENCES milestones(id) ON DELETE CASCADE,
FOREIGN KEY (sliceId) REFERENCES slices(id) ON DELETE CASCADE
)
`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorRunsFeatureId ON mission_validator_runs(featureId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorRunsMilestoneId ON mission_validator_runs(milestoneId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorRunsSliceId ON mission_validator_runs(sliceId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorRunsStatus ON mission_validator_runs(status)`);
// Create mission_validator_failures table for assertion failure records
this.db.exec(`
CREATE TABLE IF NOT EXISTS mission_validator_failures (
id TEXT PRIMARY KEY,
runId TEXT NOT NULL,
featureId TEXT NOT NULL,
assertionId TEXT NOT NULL,
message TEXT,
expected TEXT,
actual TEXT,
createdAt TEXT NOT NULL,
FOREIGN KEY (runId) REFERENCES mission_validator_runs(id) ON DELETE CASCADE,
FOREIGN KEY (featureId) REFERENCES mission_features(id) ON DELETE CASCADE
)
`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorFailuresRunId ON mission_validator_failures(runId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorFailuresFeatureId ON mission_validator_failures(featureId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxValidatorFailuresAssertionId ON mission_validator_failures(assertionId)`);
// Create mission_fix_feature_lineage table for tracking fix feature relationships
this.db.exec(`
CREATE TABLE IF NOT EXISTS mission_fix_feature_lineage (
id TEXT PRIMARY KEY,
sourceFeatureId TEXT NOT NULL,
fixFeatureId TEXT NOT NULL,
runId TEXT NOT NULL,
failedAssertionIds TEXT NOT NULL DEFAULT '[]',
createdAt TEXT NOT NULL,
FOREIGN KEY (sourceFeatureId) REFERENCES mission_features(id) ON DELETE CASCADE,
FOREIGN KEY (fixFeatureId) REFERENCES mission_features(id) ON DELETE CASCADE,
FOREIGN KEY (runId) REFERENCES mission_validator_runs(id) ON DELETE CASCADE
)
`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxFixLineageSourceFeatureId ON mission_fix_feature_lineage(sourceFeatureId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxFixLineageFixFeatureId ON mission_fix_feature_lineage(fixFeatureId)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxFixLineageRunId ON mission_fix_feature_lineage(runId)`);
});
}
}
/**

View File

@@ -179,6 +179,8 @@ export {
AUTOPILOT_STATES,
MISSION_EVENT_TYPES,
SLICE_PLAN_STATES,
FEATURE_LOOP_STATES,
VALIDATOR_RUN_STATUSES,
} from "./mission-types.js";
export type {
MissionStatus,
@@ -188,6 +190,8 @@ export type {
InterviewState,
AutopilotState,
SlicePlanState,
FeatureLoopState,
ValidatorRunStatus,
MissionEventType,
AutopilotStatus,
Mission,
@@ -213,6 +217,11 @@ export type {
FeatureEventPayload,
FeatureDeletedPayload,
FeatureLinkedPayload,
// Validator run types
MissionValidatorRun,
MissionAssertionFailureRecord,
MissionFixFeatureLineage,
MissionFeatureLoopSnapshot,
// Contract assertion types
MISSION_ASSERTION_STATUSES,
MILESTONE_VALIDATION_STATES,

View File

@@ -2453,6 +2453,191 @@ describe("MissionStore", () => {
expect(description).not.toContain("## Contract Assertions");
});
});
// ── Loop State & Validator Run Schema Tests ───────────────────────────
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 31 after migration", () => {
expect(db.getSchemaVersion()).toBe(31);
});
it("mission_features table has loop state columns", () => {
const cols = db.prepare("PRAGMA table_info(mission_features)").all() as Array<{ name: string }>;
const colNames = new Set(cols.map((c) => c.name));
expect(colNames).toContain("loopState");
expect(colNames).toContain("implementationAttemptCount");
expect(colNames).toContain("validatorAttemptCount");
expect(colNames).toContain("lastValidatorRunId");
expect(colNames).toContain("lastValidatorStatus");
expect(colNames).toContain("generatedFromFeatureId");
expect(colNames).toContain("generatedFromRunId");
});
it("mission_validator_runs table exists with correct schema", () => {
const cols = db.prepare("PRAGMA table_info(mission_validator_runs)").all() as Array<{ name: string }>;
const colNames = new Set(cols.map((c) => c.name));
expect(colNames).toContain("id");
expect(colNames).toContain("featureId");
expect(colNames).toContain("milestoneId");
expect(colNames).toContain("sliceId");
expect(colNames).toContain("status");
expect(colNames).toContain("triggerType");
expect(colNames).toContain("implementationAttempt");
expect(colNames).toContain("validatorAttempt");
expect(colNames).toContain("summary");
expect(colNames).toContain("blockedReason");
expect(colNames).toContain("startedAt");
expect(colNames).toContain("completedAt");
expect(colNames).toContain("createdAt");
expect(colNames).toContain("updatedAt");
});
it("mission_validator_failures table exists with correct schema", () => {
const cols = db.prepare("PRAGMA table_info(mission_validator_failures)").all() as Array<{ name: string }>;
const colNames = new Set(cols.map((c) => c.name));
expect(colNames).toContain("id");
expect(colNames).toContain("runId");
expect(colNames).toContain("featureId");
expect(colNames).toContain("assertionId");
expect(colNames).toContain("message");
expect(colNames).toContain("expected");
expect(colNames).toContain("actual");
expect(colNames).toContain("createdAt");
});
it("mission_fix_feature_lineage table exists with correct schema", () => {
const cols = db.prepare("PRAGMA table_info(mission_fix_feature_lineage)").all() as Array<{ name: string }>;
const colNames = new Set(cols.map((c) => c.name));
expect(colNames).toContain("id");
expect(colNames).toContain("sourceFeatureId");
expect(colNames).toContain("fixFeatureId");
expect(colNames).toContain("runId");
expect(colNames).toContain("failedAssertionIds");
expect(colNames).toContain("createdAt");
});
it("addFeature creates feature with correct loop state defaults", () => {
const mission = store.createMission({ title: "Loop State Test" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const feature = store.addFeature(slice.id, { title: "Test Feature" });
expect(feature.loopState).toBe("idle");
expect(feature.implementationAttemptCount).toBe(0);
expect(feature.validatorAttemptCount).toBe(0);
expect(feature.lastValidatorRunId).toBeUndefined();
expect(feature.lastValidatorStatus).toBeUndefined();
expect(feature.generatedFromFeatureId).toBeUndefined();
expect(feature.generatedFromRunId).toBeUndefined();
});
it("getFeature returns feature with correct loop state defaults via rowToFeature", () => {
const mission = store.createMission({ title: "Loop State Test" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const created = store.addFeature(slice.id, { title: "Test Feature" });
const retrieved = store.getFeature(created.id);
expect(retrieved).toBeDefined();
expect(retrieved!.loopState).toBe("idle");
expect(retrieved!.implementationAttemptCount).toBe(0);
expect(retrieved!.validatorAttemptCount).toBe(0);
expect(retrieved!.lastValidatorRunId).toBeUndefined();
expect(retrieved!.lastValidatorStatus).toBeUndefined();
expect(retrieved!.generatedFromFeatureId).toBeUndefined();
expect(retrieved!.generatedFromRunId).toBeUndefined();
});
it("updateFeature persists loop state fields", () => {
const mission = store.createMission({ title: "Loop State Test" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const feature = store.addFeature(slice.id, { title: "Test Feature" });
const updated = store.updateFeature(feature.id, {
loopState: "implementing",
implementationAttemptCount: 1,
validatorAttemptCount: 0,
lastValidatorRunId: "VR-TEST-001",
lastValidatorStatus: "running",
});
expect(updated.loopState).toBe("implementing");
expect(updated.implementationAttemptCount).toBe(1);
expect(updated.validatorAttemptCount).toBe(0);
expect(updated.lastValidatorRunId).toBe("VR-TEST-001");
expect(updated.lastValidatorStatus).toBe("running");
// Verify persisted
const retrieved = store.getFeature(feature.id);
expect(retrieved!.loopState).toBe("implementing");
expect(retrieved!.implementationAttemptCount).toBe(1);
expect(retrieved!.lastValidatorRunId).toBe("VR-TEST-001");
expect(retrieved!.lastValidatorStatus).toBe("running");
});
it("existing feature read has correct defaults for new columns", () => {
// Create a feature using the store (which sets loop state defaults)
const mission = store.createMission({ title: "Existing Feature Test" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const feature = store.addFeature(slice.id, { title: "Existing Feature" });
// Simulate reading from DB directly (as rowToFeature would)
const row = db.prepare("SELECT * FROM mission_features WHERE id = ?").get(feature.id);
expect((row as any).loopState).toBe("idle");
expect((row as any).implementationAttemptCount).toBe(0);
expect((row as any).validatorAttemptCount).toBe(0);
expect((row as any).lastValidatorRunId).toBeNull();
expect((row as any).lastValidatorStatus).toBeNull();
});
it("migration is idempotent - running twice does not fail", () => {
const versionBefore = db.getSchemaVersion();
// init() calls migrate(), calling again should be a no-op
db.init();
const versionAfter = db.getSchemaVersion();
expect(versionAfter).toBe(versionBefore);
});
it("foreign key constraints exist on validator runs table", () => {
// Create full hierarchy
const mission = store.createMission({ title: "FK Test" });
const milestone = store.addMilestone(mission.id, { title: "MS" });
const slice = store.addSlice(milestone.id, { title: "SL" });
const feature = store.addFeature(slice.id, { title: "FK Feature" });
// Insert a validator run
const now = new Date().toISOString();
db.prepare(`
INSERT INTO mission_validator_runs (id, featureId, milestoneId, sliceId, status, implementationAttempt, validatorAttempt, startedAt, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run("VR-TEST-001", feature.id, milestone.id, slice.id, "running", 1, 1, now, now, now);
// Verify the run exists
const run = db.prepare("SELECT * FROM mission_validator_runs WHERE id = ?").get("VR-TEST-001");
expect(run).toBeDefined();
expect((run as any).featureId).toBe(feature.id);
});
it("validator runs index exists", () => {
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='mission_validator_runs'").all() as Array<{ name: string }>;
const indexNames = new Set(indexes.map((i) => i.name));
expect(indexNames).toContain("idxValidatorRunsFeatureId");
});
it("validator failures index exists", () => {
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='mission_validator_failures'").all() as Array<{ name: string }>;
const indexNames = new Set(indexes.map((i) => i.name));
expect(indexNames).toContain("idxValidatorFailuresRunId");
});
it("fix lineage index exists", () => {
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='mission_fix_feature_lineage'").all() as Array<{ name: string }>;
const indexNames = new Set(indexes.map((i) => i.name));
expect(indexNames).toContain("idxFixLineageSourceFeatureId");
});
});
});
// vi import for vitest mocking

View File

@@ -234,6 +234,13 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
status: row.status as FeatureStatus,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
loopState: (row.loopState as import("./mission-types.js").FeatureLoopState) || "idle",
implementationAttemptCount: row.implementationAttemptCount ?? 0,
validatorAttemptCount: row.validatorAttemptCount ?? 0,
lastValidatorRunId: row.lastValidatorRunId || undefined,
lastValidatorStatus: row.lastValidatorStatus as import("./mission-types.js").ValidatorRunStatus || undefined,
generatedFromFeatureId: row.generatedFromFeatureId || undefined,
generatedFromRunId: row.generatedFromRunId || undefined,
};
}
@@ -1401,11 +1408,14 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
status: "defined",
createdAt: now,
updatedAt: now,
loopState: "idle",
implementationAttemptCount: 0,
validatorAttemptCount: 0,
};
this.db.prepare(`
INSERT INTO mission_features (id, sliceId, title, description, acceptanceCriteria, status, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO mission_features (id, sliceId, title, description, acceptanceCriteria, status, loopState, implementationAttemptCount, validatorAttemptCount, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
feature.id,
feature.sliceId,
@@ -1413,6 +1423,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
feature.description ?? null,
feature.acceptanceCriteria ?? null,
feature.status,
feature.loopState ?? "idle",
feature.implementationAttemptCount ?? 0,
feature.validatorAttemptCount ?? 0,
feature.createdAt,
feature.updatedAt,
);
@@ -1477,6 +1490,13 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
acceptanceCriteria = ?,
status = ?,
taskId = ?,
loopState = ?,
implementationAttemptCount = ?,
validatorAttemptCount = ?,
lastValidatorRunId = ?,
lastValidatorStatus = ?,
generatedFromFeatureId = ?,
generatedFromRunId = ?,
updatedAt = ?
WHERE id = ?
`).run(
@@ -1485,6 +1505,13 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
updated.acceptanceCriteria ?? null,
updated.status,
updated.taskId ?? null,
updated.loopState ?? "idle",
updated.implementationAttemptCount ?? 0,
updated.validatorAttemptCount ?? 0,
updated.lastValidatorRunId ?? null,
updated.lastValidatorStatus ?? null,
updated.generatedFromFeatureId ?? null,
updated.generatedFromRunId ?? null,
updated.updatedAt,
updated.id,
);

View File

@@ -31,6 +31,14 @@ export type SlicePlanState = (typeof SLICE_PLAN_STATES)[number];
export const FEATURE_STATUSES = ["defined", "triaged", "in-progress", "done", "blocked"] as const;
export type FeatureStatus = (typeof FEATURE_STATUSES)[number];
/** Loop state values for a feature's execution loop lifecycle */
export const FEATURE_LOOP_STATES = ["idle", "implementing", "validating", "needs_fix", "passed", "blocked"] as const;
export type FeatureLoopState = (typeof FEATURE_LOOP_STATES)[number];
/** Status values for a validator run */
export const VALIDATOR_RUN_STATUSES = ["running", "passed", "failed", "blocked", "error"] as const;
export type ValidatorRunStatus = (typeof VALIDATOR_RUN_STATUSES)[number];
/** Interview state for AI-assisted specification */
export const INTERVIEW_STATES = ["not_started", "in_progress", "completed", "needs_update"] as const;
export type InterviewState = (typeof INTERVIEW_STATES)[number];
@@ -221,6 +229,130 @@ export interface MissionFeature {
createdAt: string;
/** ISO-8601 timestamp of last update */
updatedAt: string;
/** Current loop state for the execution loop (idle, implementing, validating, needs_fix, passed, blocked) */
loopState?: FeatureLoopState;
/** Number of implementation attempts made for this feature */
implementationAttemptCount?: number;
/** Number of validation attempts made for this feature */
validatorAttemptCount?: number;
/** ID of the last validator run for this feature */
lastValidatorRunId?: string;
/** Status of the last validator run (passed, failed, blocked, error) */
lastValidatorStatus?: ValidatorRunStatus;
/** Feature ID that generated this feature as a fix (for lineage tracking) */
generatedFromFeatureId?: string;
/** Validator run ID that generated this feature as a fix (for lineage tracking) */
generatedFromRunId?: string;
}
// ── Validator Run & Loop Types ──────────────────────────────────────
/**
* A validator run represents a single execution of the validation phase
* for a feature within the mission execution loop.
*/
export interface MissionValidatorRun {
/** Unique identifier (e.g., "VR-XXXXXXXX-XXXX") */
id: string;
/** Parent feature ID */
featureId: string;
/** Parent milestone ID */
milestoneId: string;
/** Parent slice ID */
sliceId: string;
/** Current status of the run */
status: ValidatorRunStatus;
/** What triggered this run (e.g., "task_completion", "manual", "scheduled") */
triggerType?: string;
/** Which implementation attempt this run corresponds to */
implementationAttempt: number;
/** Which validation attempt this run corresponds to */
validatorAttempt: number;
/** Summary of the validation run results */
summary?: string;
/** Reason for blocked status if applicable */
blockedReason?: string;
/** ISO-8601 timestamp when the run started */
startedAt: string;
/** ISO-8601 timestamp when the run completed (if completed) */
completedAt?: string;
/** ISO-8601 timestamp of creation */
createdAt: string;
/** ISO-8601 timestamp of last update */
updatedAt: string;
}
/**
* An assertion failure record represents a single assertion failure
* within a validator run.
*/
export interface MissionAssertionFailureRecord {
/** Unique identifier (e.g., "VAF-XXXXXXXX-XXXX") */
id: string;
/** Parent validator run ID */
runId: string;
/** Feature ID this failure belongs to */
featureId: string;
/** Assertion ID that failed */
assertionId: string;
/** Human-readable failure message */
message?: string;
/** Expected value or behavior */
expected?: string;
/** Actual value or behavior */
actual?: string;
/** ISO-8601 timestamp of creation */
createdAt: string;
}
/**
* A fix feature lineage record tracks the relationship between a source
* feature and a generated fix feature within the execution loop.
*/
export interface MissionFixFeatureLineage {
/** Unique identifier (e.g., "FFL-XXXXXXXX-XXXX") */
id: string;
/** Source feature ID that failed validation */
sourceFeatureId: string;
/** Generated fix feature ID */
fixFeatureId: string;
/** Validator run ID that triggered the fix generation */
runId: string;
/** JSON array of assertion IDs that failed and triggered the fix */
failedAssertionIds: string[];
/** ISO-8601 timestamp of creation */
createdAt: string;
}
/**
* A complete loop state snapshot for a feature, including all validator
* runs, failures, and lineage information.
*/
export interface MissionFeatureLoopSnapshot {
/** Feature ID */
featureId: string;
/** The feature object */
feature: MissionFeature;
/** Current loop state */
loopState: FeatureLoopState;
/** Number of implementation attempts */
implementationAttemptCount: number;
/** Number of validation attempts */
validatorAttemptCount: number;
/** ID of the last validator run */
lastValidatorRunId?: string;
/** Status of the last validator run */
lastValidatorStatus?: ValidatorRunStatus;
/** Feature ID that generated this feature (if applicable) */
generatedFromFeatureId?: string;
/** Validator run ID that generated this feature (if applicable) */
generatedFromRunId?: string;
/** All validator runs for this feature, newest first */
validatorRuns: MissionValidatorRun[];
/** All assertion failures across all runs */
failures: MissionAssertionFailureRecord[];
/** All lineage entries for this feature (as source or fix) */
lineage: MissionFixFeatureLineage[];
}
// ── Input Types (for creation) ──────────────────────────────────────

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 28", () => {
expect(db.getSchemaVersion()).toBe(29);
expect(db.getSchemaVersion()).toBe(31);
});
});
});

View File

@@ -197,6 +197,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
planningModelProvider: row.planningModelProvider || undefined,
planningModelId: row.planningModelId || undefined,
mergeRetries: row.mergeRetries ?? undefined,
workflowStepRetries: row.workflowStepRetries ?? undefined,
stuckKillCount: row.stuckKillCount ?? undefined,
recoveryRetryCount: row.recoveryRetryCount ?? undefined,
nextRecoveryAt: row.nextRecoveryAt || undefined,
@@ -285,14 +286,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
id, title, description, "column", status, size, reviewLevel, currentStep,
worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
stuckKillCount, recoveryRetryCount, nextRecoveryAt, error,
workflowStepRetries, stuckKillCount, recoveryRetryCount, nextRecoveryAt, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(
task.id,
@@ -317,6 +318,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.planningModelProvider ?? null,
task.planningModelId ?? null,
task.mergeRetries ?? null,
task.workflowStepRetries ?? null,
task.stuckKillCount ?? 0,
task.recoveryRetryCount ?? null,
task.nextRecoveryAt ?? null,
@@ -1604,7 +1606,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask(
id: string,
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
runContext?: RunMutationContext,
): Promise<Task> {
return this.withTaskLock(id, async () => {
@@ -1698,6 +1700,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (updates.size !== undefined) task.size = updates.size;
if (updates.reviewLevel !== undefined) task.reviewLevel = updates.reviewLevel;
if (updates.mergeRetries !== undefined) task.mergeRetries = updates.mergeRetries;
if (updates.workflowStepRetries !== undefined) task.workflowStepRetries = updates.workflowStepRetries;
if (updates.stuckKillCount === null) {
task.stuckKillCount = undefined;
} else if (updates.stuckKillCount !== undefined) {

View File

@@ -673,6 +673,10 @@ export interface Task {
workflowStepResults?: WorkflowStepResult[];
/** Number of merge retry attempts made for this task (auto-merge conflict recovery) */
mergeRetries?: number;
/** Number of workflow step failure retry attempts made for this task.
* When pre-merge workflow steps fail, the executor retries up to MAX_WORKFLOW_STEP_RETRIES
* times before marking the task as failed. Cleared on successful workflow step completion. */
workflowStepRetries?: number;
/** Number of times the stuck-task detector has killed this task's agent session.
* Incremented by the self-healing manager on each stuck kill. When this reaches
* `maxStuckKills`, the task is marked as permanently failed instead of re-queued. */