feat(FN-1646): add stale-spec guard to prevent execution of outdated tasks

- Add spec-staleness evaluator to check task specification age before execution
- Guard executor startup and resume to prevent running tasks with stale specs
- Guard scheduler dispatch to skip stale tasks and move them back to triage
- Add comprehensive tests for spec staleness detection (7d/14d thresholds)
- Add getFusionDir mock for executor staleness check tests
This commit is contained in:
gsxdsm
2026-04-12 17:49:22 -07:00
parent bf5ce66a07
commit db6e51d58c
6 changed files with 481 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import { generateReservedWorktreeName, slugify } from "./worktree-names.js";
import { schedulerLog } from "./logger.js";
import { type PrMonitor, type PrComment } from "./pr-monitor.js";
import { reconcileMissionFeatureState } from "./mission-feature-sync.js";
import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js";
/**
* Check whether two sets of file scope paths overlap.
@@ -640,6 +641,22 @@ export class Scheduler {
continue;
}
// Stale spec enforcement: check if PROMPT.md has aged beyond the configured threshold.
// When enabled, stale tasks are moved back to triage with status "needs-respecify"
// so they receive fresh specification before execution. This guard runs after
// filesystem validation so missing/unreadable files skip staleness checks entirely.
const promptPath = getPromptPath(this.store.getTasksDir(), task.id);
const staleness = await evaluateSpecStaleness({ settings, promptPath });
if (staleness.isStale) {
schedulerLog.warn(`Task ${task.id} specification is stale — ${staleness.reason}`);
await this.store.moveTask(task.id, "triage");
await this.store.updateTask(task.id, { status: "needs-respecify" });
await this.store.logEntry(task.id, staleness.reason);
continue;
}
// If staleness evaluation was skipped (missing/unreadable file), continue to
// existing scheduler logic which handles filesystem validation separately.
// Check file scope overlap when enabled
if (settings.groupOverlappingFiles) {
const taskScope = await this.store.parseFileScopeFromPrompt(task.id);