taskplane 0.5.12 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  * @module orch/types
4
4
  */
5
5
  import { join } from "path";
6
+ import type { ExitClassification, TaskExitDiagnostic } from "./diagnostics.js";
6
7
 
7
8
  // ── Types ────────────────────────────────────────────────────────────
8
9
 
@@ -51,6 +52,12 @@ export interface OrchestratorConfig {
51
52
  monitoring: {
52
53
  poll_interval: number;
53
54
  };
55
+ /** Verification baseline fingerprinting settings (TP-032). */
56
+ verification: {
57
+ enabled: boolean;
58
+ mode: "strict" | "permissive";
59
+ flaky_reruns: number;
60
+ };
54
61
  }
55
62
 
56
63
  /** A parsed task from PROMPT.md, enriched for orchestrator use */
@@ -125,6 +132,8 @@ export interface TaskArea {
125
132
  export interface TaskRunnerConfig {
126
133
  task_areas: Record<string, TaskArea>;
127
134
  reference_docs: Record<string, string>;
135
+ /** Named testing/verification commands (e.g., { test: "npx vitest run" }). Used for baseline fingerprinting (TP-032). */
136
+ testing_commands?: Record<string, string>;
128
137
  }
129
138
 
130
139
  /** Result of a preflight check */
@@ -185,6 +194,11 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
185
194
  monitoring: {
186
195
  poll_interval: 5,
187
196
  },
197
+ verification: {
198
+ enabled: false,
199
+ mode: "permissive",
200
+ flaky_reruns: 1,
201
+ },
188
202
  };
189
203
 
190
204
  export const DEFAULT_TASK_RUNNER_CONFIG: TaskRunnerConfig = {
@@ -549,6 +563,31 @@ export interface LaneTaskOutcome {
549
563
  sessionName: string;
550
564
  /** Whether .DONE file was found */
551
565
  doneFileFound: boolean;
566
+ /**
567
+ * Number of commits preserved as partial progress for a failed task.
568
+ * 0 when no partial progress was saved (succeeded tasks, no commits, etc.).
569
+ * Optional for backward compatibility — defaults to 0 when absent.
570
+ */
571
+ partialProgressCommits?: number;
572
+ /**
573
+ * Saved branch name holding partial progress for a failed task.
574
+ * Undefined when no partial progress was saved.
575
+ * Optional for backward compatibility.
576
+ */
577
+ partialProgressBranch?: string;
578
+ /**
579
+ * Structured exit diagnostic for this task (v3, TP-030).
580
+ *
581
+ * Canonical structured exit data — preferred over the legacy `exitReason`
582
+ * string when present. Produced by `classifyExit()` after session ends,
583
+ * then enriched with progress/context metadata.
584
+ *
585
+ * Optional: absent for tasks that haven't exited yet, and for
586
+ * backward compatibility with pre-v3 code paths.
587
+ * Consumers should check `exitDiagnostic` first, falling back to
588
+ * `exitReason` for display.
589
+ */
590
+ exitDiagnostic?: TaskExitDiagnostic;
552
591
  }
553
592
 
554
593
  /**
@@ -868,6 +907,21 @@ export interface OrchBatchRuntimeState {
868
907
  dependencyGraph: DependencyGraph | null;
869
908
  /** Accumulated merge results across all waves */
870
909
  mergeResults: MergeWaveResult[];
910
+ /**
911
+ * v3 resilience state carried forward across resume cycles.
912
+ * Populated from persisted state on resume; defaults used for new batches.
913
+ */
914
+ resilience?: ResilienceState;
915
+ /**
916
+ * v3 diagnostics state carried forward across resume cycles.
917
+ * Populated from persisted state on resume; defaults used for new batches.
918
+ */
919
+ diagnostics?: BatchDiagnostics;
920
+ /**
921
+ * Unknown top-level fields from loaded persisted state.
922
+ * Carried forward so they survive serialization roundtrips.
923
+ */
924
+ _extraFields?: Record<string, unknown>;
871
925
  }
872
926
 
873
927
  /**
@@ -979,6 +1033,27 @@ export interface MergeResult {
979
1033
  verification: MergeVerification;
980
1034
  }
981
1035
 
1036
+ /**
1037
+ * Orchestrator-side verification baseline comparison result for a single lane.
1038
+ * Populated when verification baseline fingerprinting is enabled (testing.commands configured).
1039
+ */
1040
+ export interface VerificationBaselineResult {
1041
+ /** Whether baseline comparison was performed */
1042
+ performed: boolean;
1043
+ /** Number of new failures (not in baseline) */
1044
+ newFailureCount: number;
1045
+ /** Number of pre-existing failures (also in baseline) */
1046
+ preExistingCount: number;
1047
+ /** Number of failures that disappeared (fixed by the merge) */
1048
+ fixedCount: number;
1049
+ /** Classification: "pass" (no new failures), "verification_new_failure", "flaky_suspected" */
1050
+ classification: "pass" | "verification_new_failure" | "flaky_suspected";
1051
+ /** Human-readable summary of new failures (truncated) */
1052
+ newFailureSummary: string;
1053
+ /** Whether a flaky re-run was performed */
1054
+ flakyRerunPerformed: boolean;
1055
+ }
1056
+
982
1057
  /** Per-lane merge outcome, enriched by the orchestrator. */
983
1058
  export interface MergeLaneResult {
984
1059
  laneNumber: number;
@@ -990,6 +1065,12 @@ export interface MergeLaneResult {
990
1065
  durationMs: number;
991
1066
  /** Repo ID this lane targeted (workspace mode only). Undefined in repo mode. */
992
1067
  repoId?: string;
1068
+ /**
1069
+ * Orchestrator-side verification baseline result (TP-032).
1070
+ * Populated when baseline fingerprinting is enabled and a successful merge occurred.
1071
+ * Undefined when fingerprinting is not enabled or merge failed before verification.
1072
+ */
1073
+ verificationBaseline?: VerificationBaselineResult;
993
1074
  }
994
1075
 
995
1076
  /** Overall wave merge outcome. */
@@ -1002,6 +1083,24 @@ export interface MergeWaveResult {
1002
1083
  totalDurationMs: number;
1003
1084
  /** Per-repo merge outcomes (populated in workspace mode; empty in repo mode). */
1004
1085
  repoResults?: RepoMergeOutcome[];
1086
+ /**
1087
+ * TP-033: True when a verification rollback failed and safe-stop was triggered.
1088
+ * Engine MUST force `paused` phase regardless of `on_merge_failure` config,
1089
+ * and preserve all merge worktrees/branches for manual recovery.
1090
+ */
1091
+ rollbackFailed?: boolean;
1092
+ /**
1093
+ * TP-033: Transaction records for each lane merge attempt in this wave.
1094
+ * Populated when transactional envelope is active.
1095
+ */
1096
+ transactionRecords?: TransactionRecord[];
1097
+ /**
1098
+ * TP-033 R004-2: Errors encountered while persisting transaction records.
1099
+ * When non-empty, recovery commands in transaction records may reference
1100
+ * files that don't exist on disk. Operator should check `.pi/verification/`
1101
+ * manually.
1102
+ */
1103
+ persistenceErrors?: string[];
1005
1104
  }
1006
1105
 
1007
1106
  /** Per-repo merge outcome within a wave merge. */
@@ -1018,6 +1117,62 @@ export interface RepoMergeOutcome {
1018
1117
  failureReason: string | null;
1019
1118
  }
1020
1119
 
1120
+ // ── Merge Transaction Types (TP-033) ─────────────────────────────────
1121
+
1122
+ /**
1123
+ * Status of a transactional merge attempt for a single lane.
1124
+ *
1125
+ * - `committed`: Merge succeeded, verification passed, refs advanced.
1126
+ * - `rolled_back`: Verification failed, merge commit rolled back to baseHEAD.
1127
+ * - `rollback_failed`: Rollback attempted but failed — safe-stop triggered.
1128
+ * - `merge_failed`: Merge itself failed (conflict, crash, etc.) before verification.
1129
+ *
1130
+ * @since TP-033
1131
+ */
1132
+ export type TransactionStatus = "committed" | "rolled_back" | "rollback_failed" | "merge_failed";
1133
+
1134
+ /**
1135
+ * Transactional record for a single lane merge attempt.
1136
+ *
1137
+ * Persisted as JSON at:
1138
+ * `.pi/verification/{opId}/txn-b{batchId}-repo-{repoId}-wave-{n}-lane-{k}.json`
1139
+ *
1140
+ * Captures the complete ref state before and after merge, rollback outcome,
1141
+ * and recovery commands for safe-stop scenarios.
1142
+ *
1143
+ * @since TP-033
1144
+ */
1145
+ export interface TransactionRecord {
1146
+ /** Operator ID for this batch run */
1147
+ opId: string;
1148
+ /** Batch identifier */
1149
+ batchId: string;
1150
+ /** Wave index (0-based) */
1151
+ waveIndex: number;
1152
+ /** Lane number within the wave */
1153
+ laneNumber: number;
1154
+ /** Repo ID (undefined/null in repo mode, string in workspace mode) */
1155
+ repoId: string | null;
1156
+ /** HEAD of temp branch before this lane's merge commit (rollback target) */
1157
+ baseHEAD: string;
1158
+ /** HEAD of the lane's source branch (commit being merged in) */
1159
+ laneHEAD: string;
1160
+ /** HEAD of temp branch after merge commit (null if merge failed before commit) */
1161
+ mergedHEAD: string | null;
1162
+ /** Transaction outcome */
1163
+ status: TransactionStatus;
1164
+ /** Whether a rollback was attempted */
1165
+ rollbackAttempted: boolean;
1166
+ /** Rollback outcome detail (null if rollback not attempted) */
1167
+ rollbackResult: string | null;
1168
+ /** Recovery commands emitted on rollback failure (empty array otherwise) */
1169
+ recoveryCommands: string[];
1170
+ /** ISO timestamp when transaction started */
1171
+ startedAt: string;
1172
+ /** ISO timestamp when transaction completed */
1173
+ completedAt: string;
1174
+ }
1175
+
1021
1176
  // ── Merge Error Types ────────────────────────────────────────────────
1022
1177
 
1023
1178
  /**
@@ -1090,6 +1245,197 @@ export const MERGE_RESULT_READ_RETRY_DELAY_MS = 1_000;
1090
1245
  export const MERGE_SPAWN_RETRY_MAX = 2;
1091
1246
 
1092
1247
 
1248
+ // ── Merge Retry Policy Matrix (TP-033 Step 2) ───────────────────────
1249
+
1250
+ /**
1251
+ * Merge-related failure classifications for the retry policy matrix.
1252
+ *
1253
+ * These are the merge-phase failure classes from the resilience roadmap §4c.
1254
+ * Task-execution classes (api_error, context_overflow, etc.) are out of scope
1255
+ * for TP-033 and handled separately in Phase 1/3.
1256
+ *
1257
+ * @since TP-033
1258
+ */
1259
+ export type MergeFailureClassification =
1260
+ | "verification_new_failure"
1261
+ | "merge_conflict_unresolved"
1262
+ | "cleanup_post_merge_failed"
1263
+ | "git_worktree_dirty"
1264
+ | "git_lock_file";
1265
+
1266
+ /**
1267
+ * Retry policy for a single merge failure classification.
1268
+ *
1269
+ * Defines whether a failure class is retriable, the maximum retry attempts,
1270
+ * cooldown between retries (in milliseconds), and what happens on exhaustion.
1271
+ *
1272
+ * @since TP-033
1273
+ */
1274
+ export interface MergeRetryPolicy {
1275
+ /** Whether this failure class can be retried automatically */
1276
+ retriable: boolean;
1277
+ /** Maximum number of retry attempts (0 for non-retriable) */
1278
+ maxAttempts: number;
1279
+ /** Cooldown delay between retries in milliseconds (0 for immediate) */
1280
+ cooldownMs: number;
1281
+ /** Action when retries are exhausted or class is non-retriable */
1282
+ exhaustionAction: "pause" | "pause_wave_gate" | "pause_escalation";
1283
+ }
1284
+
1285
+ /**
1286
+ * Centralized retry policy matrix for merge-related failure classes.
1287
+ *
1288
+ * This is the **single source of truth** for retry behavior. Both engine.ts
1289
+ * and resume.ts consume this table through `computeMergeRetryDecision()` to
1290
+ * guarantee parity.
1291
+ *
1292
+ * Values from resilience roadmap §4c:
1293
+ *
1294
+ * | Classification | Retry? | Max | Cooldown | Exhaustion |
1295
+ * |-----------------------------|--------|-----|----------|---------------------|
1296
+ * | verification_new_failure | ✅ | 1 | 0ms | pause + diagnostic |
1297
+ * | merge_conflict_unresolved | ❌ | 0 | — | pause + escalation |
1298
+ * | cleanup_post_merge_failed | ✅ | 1 | 2000ms | pause (wave gate) |
1299
+ * | git_worktree_dirty | ✅ | 1 | 2000ms | pause |
1300
+ * | git_lock_file | ✅ | 2 | 3000ms | pause |
1301
+ *
1302
+ * @since TP-033
1303
+ */
1304
+ export const MERGE_RETRY_POLICY_MATRIX: Readonly<Record<MergeFailureClassification, MergeRetryPolicy>> = {
1305
+ verification_new_failure: {
1306
+ retriable: true,
1307
+ maxAttempts: 1,
1308
+ cooldownMs: 0,
1309
+ exhaustionAction: "pause",
1310
+ },
1311
+ merge_conflict_unresolved: {
1312
+ retriable: false,
1313
+ maxAttempts: 0,
1314
+ cooldownMs: 0,
1315
+ exhaustionAction: "pause_escalation",
1316
+ },
1317
+ cleanup_post_merge_failed: {
1318
+ retriable: true,
1319
+ maxAttempts: 1,
1320
+ cooldownMs: 2_000,
1321
+ exhaustionAction: "pause_wave_gate",
1322
+ },
1323
+ git_worktree_dirty: {
1324
+ retriable: true,
1325
+ maxAttempts: 1,
1326
+ cooldownMs: 2_000,
1327
+ exhaustionAction: "pause",
1328
+ },
1329
+ git_lock_file: {
1330
+ retriable: true,
1331
+ maxAttempts: 2,
1332
+ cooldownMs: 3_000,
1333
+ exhaustionAction: "pause",
1334
+ },
1335
+ };
1336
+
1337
+ /**
1338
+ * All merge failure classifications as a readonly array, for iteration/validation.
1339
+ * @since TP-033
1340
+ */
1341
+ export const MERGE_FAILURE_CLASSIFICATIONS: readonly MergeFailureClassification[] = [
1342
+ "verification_new_failure",
1343
+ "merge_conflict_unresolved",
1344
+ "cleanup_post_merge_failed",
1345
+ "git_worktree_dirty",
1346
+ "git_lock_file",
1347
+ ] as const;
1348
+
1349
+ /**
1350
+ * Decision output from the merge retry policy evaluator.
1351
+ *
1352
+ * Pure data structure — callers use this to decide whether to retry,
1353
+ * wait, or escalate to paused.
1354
+ *
1355
+ * @since TP-033
1356
+ */
1357
+ export interface MergeRetryDecision {
1358
+ /** Whether the merge should be retried */
1359
+ shouldRetry: boolean;
1360
+ /** Cooldown to wait before retry (0 if no retry or immediate) */
1361
+ cooldownMs: number;
1362
+ /** Human-readable reason for the decision */
1363
+ reason: string;
1364
+ /** Current retry count for this scope (after increment if retrying) */
1365
+ currentAttempt: number;
1366
+ /** Maximum attempts allowed for this classification */
1367
+ maxAttempts: number;
1368
+ /** Classification that was evaluated */
1369
+ classification: MergeFailureClassification;
1370
+ /** Exhaustion action if not retrying */
1371
+ exhaustionAction: MergeRetryPolicy["exhaustionAction"];
1372
+ }
1373
+
1374
+ /**
1375
+ * Outcome of the merge retry loop.
1376
+ *
1377
+ * Returned by `applyMergeRetryLoop()` to tell the caller what happened
1378
+ * during the retry cycle so it can take the appropriate action (continue,
1379
+ * break, force-pause, etc.).
1380
+ *
1381
+ * @since TP-033 R006
1382
+ */
1383
+ export type MergeRetryLoopOutcome =
1384
+ | {
1385
+ /** Retry succeeded — caller should continue normal post-merge flow */
1386
+ kind: "retry_succeeded";
1387
+ mergeResult: MergeWaveResult;
1388
+ }
1389
+ | {
1390
+ /** Safe-stop triggered during retry — caller should break the wave loop */
1391
+ kind: "safe_stop";
1392
+ mergeResult: MergeWaveResult;
1393
+ errorMessage: string;
1394
+ notifyMessage: string;
1395
+ }
1396
+ | {
1397
+ /**
1398
+ * Retry exhausted or failure is non-retriable — caller should
1399
+ * force `paused` regardless of on_merge_failure config.
1400
+ */
1401
+ kind: "exhausted";
1402
+ mergeResult: MergeWaveResult;
1403
+ classification: MergeFailureClassification | null;
1404
+ scopeKey: string;
1405
+ lastDecision: MergeRetryDecision;
1406
+ errorMessage: string;
1407
+ notifyMessage: string;
1408
+ }
1409
+ | {
1410
+ /** No retry attempted (unclassifiable or non-retriable with 0 attempts).
1411
+ * Caller should fall through to standard on_merge_failure policy. */
1412
+ kind: "no_retry";
1413
+ mergeResult: MergeWaveResult;
1414
+ classification: MergeFailureClassification | null;
1415
+ scopeKey: string;
1416
+ };
1417
+
1418
+ /**
1419
+ * Callbacks provided to `applyMergeRetryLoop()` for side effects
1420
+ * that differ between engine.ts and resume.ts.
1421
+ *
1422
+ * @since TP-033 R006
1423
+ */
1424
+ export interface MergeRetryCallbacks {
1425
+ /** Re-invoke mergeWaveByRepo and return the new result */
1426
+ performMerge: () => MergeWaveResult;
1427
+ /** Persist batch state with a trigger label */
1428
+ persist: (trigger: string) => void;
1429
+ /** Log a message */
1430
+ log: (message: string, details?: Record<string, unknown>) => void;
1431
+ /** Emit a notification */
1432
+ notify: (message: string, level: "info" | "warning" | "error") => void;
1433
+ /** Update the merge result in tracking arrays */
1434
+ updateMergeResult: (result: MergeWaveResult) => void;
1435
+ /** Sleep for cooldown (allows test injection) */
1436
+ sleep: (ms: number) => void;
1437
+ }
1438
+
1093
1439
  // ── View-Model Types ─────────────────────────────────────────────────
1094
1440
 
1095
1441
  /**
@@ -1146,6 +1492,131 @@ export interface OrchDashboardViewModel {
1146
1492
 
1147
1493
  // ── State Persistence Types (TS-009) ─────────────────────────────────
1148
1494
 
1495
+ // ── v3 Resilience & Diagnostics Sections (TP-030) ────────────────────
1496
+
1497
+ /**
1498
+ * Record of a single automated repair action taken by the orchestrator.
1499
+ *
1500
+ * Repair actions are deterministic strategies applied when known failure
1501
+ * classes are detected (e.g., stale worktree cleanup, lock file removal).
1502
+ * Each entry is immutable once written — history is append-only.
1503
+ *
1504
+ * @since v3 (TP-030)
1505
+ */
1506
+ export interface PersistedRepairRecord {
1507
+ /** Unique repair ID (e.g., "r-20260319-001") */
1508
+ id: string;
1509
+ /** Strategy name that was applied (e.g., "stale-worktree-cleanup", "lock-file-removal") */
1510
+ strategy: string;
1511
+ /** Outcome of the repair */
1512
+ status: "succeeded" | "failed" | "skipped";
1513
+ /** Repo ID targeted by the repair (undefined in repo mode) */
1514
+ repoId?: string;
1515
+ /** Epoch ms when the repair started */
1516
+ startedAt: number;
1517
+ /** Epoch ms when the repair ended */
1518
+ endedAt: number;
1519
+ }
1520
+
1521
+ /**
1522
+ * Resilience state section for batch-state.json.
1523
+ *
1524
+ * Tracks retry/repair metadata so the orchestrator can make informed
1525
+ * decisions about retries, force-resume, and failure escalation.
1526
+ *
1527
+ * All fields are required in a canonical v3 state. Migration from v1/v2
1528
+ * fills conservative defaults (no retries, no repairs, no forced resume).
1529
+ *
1530
+ * @since v3 (TP-030)
1531
+ */
1532
+ export interface ResilienceState {
1533
+ /** Whether the last resume was a --force resume */
1534
+ resumeForced: boolean;
1535
+ /**
1536
+ * Retry counts keyed by scope string.
1537
+ * Scope format: `{taskId}:w{waveIndex}:l{laneNumber}` (e.g., "TP-001:w0:l1").
1538
+ * Value is the number of retries attempted for that scope.
1539
+ */
1540
+ retryCountByScope: Record<string, number>;
1541
+ /**
1542
+ * Exit classification of the most recent failure (null if no failures).
1543
+ * Uses the same `ExitClassification` union from diagnostics.ts.
1544
+ */
1545
+ lastFailureClass: ExitClassification | null;
1546
+ /** Chronological history of automated repair actions. Append-only. */
1547
+ repairHistory: PersistedRepairRecord[];
1548
+ }
1549
+
1550
+ /**
1551
+ * Persisted summary of a single task's exit diagnostic.
1552
+ *
1553
+ * This is a compact representation stored in `diagnostics.taskExits`.
1554
+ * For the full diagnostic (tokens, progress, etc.), see the
1555
+ * `exitDiagnostic` field on `PersistedTaskRecord`.
1556
+ *
1557
+ * Uses `ExitClassification` from diagnostics.ts as the canonical
1558
+ * classification type — no duplication.
1559
+ *
1560
+ * @since v3 (TP-030)
1561
+ */
1562
+ export interface PersistedTaskExitSummary {
1563
+ /** Deterministic exit classification */
1564
+ classification: ExitClassification;
1565
+ /** Estimated cost in USD for this task's execution */
1566
+ cost: number;
1567
+ /** Wall-clock duration of the task in seconds */
1568
+ durationSec: number;
1569
+ /** Number of retry attempts (0 if never retried) */
1570
+ retries?: number;
1571
+ }
1572
+
1573
+ /**
1574
+ * Batch-level diagnostics section for batch-state.json.
1575
+ *
1576
+ * Aggregates per-task exit summaries and batch-wide cost for
1577
+ * dashboard display and post-mortem analysis.
1578
+ *
1579
+ * All fields are required in a canonical v3 state. Migration from v1/v2
1580
+ * fills conservative defaults (empty taskExits, zero batchCost).
1581
+ *
1582
+ * @since v3 (TP-030)
1583
+ */
1584
+ export interface BatchDiagnostics {
1585
+ /**
1586
+ * Per-task exit summaries keyed by task ID.
1587
+ * Populated as tasks complete during execution.
1588
+ */
1589
+ taskExits: Record<string, PersistedTaskExitSummary>;
1590
+ /** Accumulated batch cost in USD across all tasks */
1591
+ batchCost: number;
1592
+ }
1593
+
1594
+ /**
1595
+ * Create a default ResilienceState with conservative initial values.
1596
+ * Used when migrating v1/v2 states to v3, and for new batch creation.
1597
+ */
1598
+ export function defaultResilienceState(): ResilienceState {
1599
+ return {
1600
+ resumeForced: false,
1601
+ retryCountByScope: {},
1602
+ lastFailureClass: null,
1603
+ repairHistory: [],
1604
+ };
1605
+ }
1606
+
1607
+ /**
1608
+ * Create a default BatchDiagnostics with empty/zero initial values.
1609
+ * Used when migrating v1/v2 states to v3, and for new batch creation.
1610
+ */
1611
+ export function defaultBatchDiagnostics(): BatchDiagnostics {
1612
+ return {
1613
+ taskExits: {},
1614
+ batchCost: 0,
1615
+ };
1616
+ }
1617
+
1618
+ // ── Schema Version & Constants ───────────────────────────────────────
1619
+
1149
1620
  /**
1150
1621
  * Current schema version for batch-state.json.
1151
1622
  * Increment when the persisted schema changes in incompatible ways.
@@ -1156,14 +1627,21 @@ export interface OrchDashboardViewModel {
1156
1627
  * v2 — Repo-aware records (TP-006). Adds `repoId` and `resolvedRepoId`
1157
1628
  * to task records. Formalizes `repoId` on lane records. Adds
1158
1629
  * `mode` field to top-level state.
1630
+ * v3 — Resilience & diagnostics (TP-030). Adds optional `resilience`
1631
+ * section (retry counters, force-resume, failure classification,
1632
+ * repair history) and optional `diagnostics` section (per-task
1633
+ * exit summaries, batch cost). Task records gain optional
1634
+ * `exitDiagnostic` alongside legacy `exitReason`.
1635
+ * Both new sections are optional for v1/v2 migration paths.
1159
1636
  *
1160
1637
  * Compatibility policy:
1161
- * - loadBatchState() accepts v1 files and auto-upconverts to v2 in memory
1162
- * (via upconvertV1toV2()). The on-disk file is NOT rewritten.
1163
- * - saveBatchState() always writes v2.
1164
- * - Schema versions > 2 are rejected with STATE_SCHEMA_INVALID.
1638
+ * - loadBatchState() accepts v1, v2, and v3 files. v1 and v2 are
1639
+ * auto-upconverted to v3 in memory (chained: v1→v2→v3).
1640
+ * The on-disk file is NOT rewritten during load.
1641
+ * - saveBatchState() always writes v3.
1642
+ * - Schema versions > 3 are rejected with STATE_SCHEMA_INVALID.
1165
1643
  */
1166
- export const BATCH_STATE_SCHEMA_VERSION = 2;
1644
+ export const BATCH_STATE_SCHEMA_VERSION = 3;
1167
1645
 
1168
1646
  /**
1169
1647
  * Canonical file path for persisted batch state.
@@ -1258,6 +1736,30 @@ export interface PersistedTaskRecord {
1258
1736
  * repo target after prompt → area → workspace-default fallback.
1259
1737
  */
1260
1738
  resolvedRepoId?: string;
1739
+ /**
1740
+ * Number of commits preserved as partial progress for a failed task (TP-028).
1741
+ * Undefined when no partial progress was saved (succeeded tasks, no commits, etc.).
1742
+ * Optional for backward compatibility with pre-TP-028 state files.
1743
+ */
1744
+ partialProgressCommits?: number;
1745
+ /**
1746
+ * Saved branch name holding partial progress for a failed task (TP-028).
1747
+ * Undefined when no partial progress was saved.
1748
+ * Optional for backward compatibility with pre-TP-028 state files.
1749
+ */
1750
+ partialProgressBranch?: string;
1751
+ /**
1752
+ * Structured exit diagnostic for this task (v3, TP-030).
1753
+ *
1754
+ * Canonical structured exit data — preferred over the legacy `exitReason`
1755
+ * string when present. Contains deterministic classification, cost, timing,
1756
+ * and progress metadata.
1757
+ *
1758
+ * Optional for backward compatibility with v1/v2 state files and tasks
1759
+ * that haven't exited yet. Consumers should check `exitDiagnostic` first,
1760
+ * falling back to `exitReason` for display.
1761
+ */
1762
+ exitDiagnostic?: TaskExitDiagnostic;
1261
1763
  }
1262
1764
 
1263
1765
  /**
@@ -1363,9 +1865,18 @@ export interface PersistedRepoMergeOutcome {
1363
1865
  * - Lane records formalize `repoId` contract per mode
1364
1866
  * - v1 files are auto-upconverted: `mode` defaults to "repo", task/lane
1365
1867
  * `repoId` fields default to `undefined` (omitted from JSON)
1868
+ *
1869
+ * v3 additions (TP-030):
1870
+ * - `resilience` section (required): retry counters, force-resume intent,
1871
+ * failure classification, and repair history for automated recovery.
1872
+ * - `diagnostics` section (required): per-task exit summaries and batch cost.
1873
+ * - Task records gain optional `exitDiagnostic` (canonical structured exit
1874
+ * data alongside legacy `exitReason` string).
1875
+ * - Both sections are required in v3. Migration from v1/v2 fills
1876
+ * conservative defaults (see `defaultResilienceState()` / `defaultBatchDiagnostics()`).
1366
1877
  */
1367
1878
  export interface PersistedBatchState {
1368
- /** Schema version — must equal BATCH_STATE_SCHEMA_VERSION (currently 2) */
1879
+ /** Schema version — must equal BATCH_STATE_SCHEMA_VERSION (currently 3) */
1369
1880
  schemaVersion: number;
1370
1881
  /** Current batch execution phase */
1371
1882
  phase: OrchBatchPhase;
@@ -1412,6 +1923,23 @@ export interface PersistedBatchState {
1412
1923
  lastError: { code: string; message: string } | null;
1413
1924
  /** Accumulated error messages */
1414
1925
  errors: string[];
1926
+ /**
1927
+ * Resilience state for retry/recovery tracking (v3, TP-030).
1928
+ * Required in v3. Migration from v1/v2 fills conservative defaults.
1929
+ */
1930
+ resilience: ResilienceState;
1931
+ /**
1932
+ * Batch-level diagnostics for cost tracking and exit summaries (v3, TP-030).
1933
+ * Required in v3. Migration from v1/v2 fills conservative defaults.
1934
+ */
1935
+ diagnostics: BatchDiagnostics;
1936
+ /**
1937
+ * Unknown top-level fields captured during deserialization.
1938
+ * Preserved on roundtrip to avoid data loss from future schema extensions
1939
+ * or external tools writing additional fields.
1940
+ * Not serialized directly — merged back by `serializeBatchState()`.
1941
+ */
1942
+ _extraFields?: Record<string, unknown>;
1415
1943
  }
1416
1944
 
1417
1945