@@ -3284,6 +3284,36 @@ class TaskGraphExecutionService {
32843284 };
32853285 });
32863286 }
3287+ /**
3288+ * A dispatched grouped repair whose completion evidence never settled leaves
3289+ * its record in 'dispatched' with planning edits already applied. When a later
3290+ * combined planning review approves that content, accept the repair as done:
3291+ * otherwise a later planning cycle misreads the record as a partial edit and
3292+ * dead-ends the Goal even though its planning was approved.
3293+ */
3294+ async closeOutSupersededPlanningRepair(changePath) {
3295+ return this.withTaskGraphMutationLease(changePath, async () => {
3296+ const resolvedChangePath = path.resolve(changePath);
3297+ const recordPath = path.join(resolvedChangePath, 'artifacts', 'agents', PLANNING_REPAIR_FILE);
3298+ if (!(await this.fileService.exists(recordPath)))
3299+ return false;
3300+ let record;
3301+ try {
3302+ record = await this.fileService.readJSON(recordPath);
3303+ }
3304+ catch {
3305+ return false;
3306+ }
3307+ if (record.status !== 'dispatched')
3308+ return false;
3309+ const context = await this.capturePlanningContext(resolvedChangePath);
3310+ record.status = 'completed';
3311+ record.completedAt = new Date().toISOString();
3312+ record.afterSnapshotHash = context.targetSnapshotHash;
3313+ await this.fileService.writeJSON(recordPath, record);
3314+ return true;
3315+ });
3316+ }
32873317 async buildPostRepairReviewSections(changePath, projectRoot) {
32883318 const recordPath = path.join(changePath, 'artifacts', 'agents', PLANNING_REPAIR_FILE);
32893319 if (!(await this.fileService.exists(recordPath)))
@@ -3295,7 +3325,11 @@ class TaskGraphExecutionService {
32953325 catch {
32963326 return [];
32973327 }
3298- if (record.status !== 'completed' || record.postRepairReviewMode === 'deterministic')
3328+ // Include repair context for a dispatched-but-unsettled repair too: its
3329+ // edits are already in the planning files, and the fresh reviewer must
3330+ // know which findings that repair was resolving.
3331+ if ((record.status !== 'completed' && record.status !== 'dispatched')
3332+ || record.postRepairReviewMode === 'deterministic')
32993333 return [];
33003334 return [
33013335 '',
@@ -8067,10 +8101,12 @@ class TaskGraphExecutionService {
80678101 }
80688102 }
80698103 if (normalizedPath.endsWith(`/${constants_1.FILE_NAMES.TASKS}`.toLowerCase()) || normalizedPath === constants_1.FILE_NAMES.TASKS.toLowerCase()) {
8070- // tasks.md is derived from task graph state; checklist ticks are execution
8071- // progress, not planning content.
8072- const checklistNeutral = content.replace(/^(\s*[-*+]\s+\[)[xX](\])/gm, '$1 $2');
8073- return this.hashMeaningfulDocumentation(checklistNeutral);
8104+ // tasks.md is fully derived from task graph state. Its churn — checklist
8105+ // ticks, repair-wave lines, closeout checklists — is execution progress,
8106+ // not planning content, so it contributes a constant to the snapshot and
8107+ // can never invalidate a planning approval. It remains a valid
8108+ // repair-scope target; semantic task changes surface via the task graph.
8109+ return (0, crypto_1.createHash)('sha256').update('ospec:planning:derived:tasks.md', 'utf8').digest('hex');
80748110 }
80758111 return this.hashMeaningfulDocumentation(content);
80768112 }
0 commit comments