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:
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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) ──────────────────────────────────────
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 28", () => {
|
||||
expect(db.getSchemaVersion()).toBe(29);
|
||||
expect(db.getSchemaVersion()).toBe(31);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user