@autopilot-harness/cli 0.2.5 → 0.2.7

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.
@@ -500,7 +500,7 @@ function evaluateVerifyReport(options) {
500
500
  return { outcome: "pass" };
501
501
  }
502
502
  function hasNoCodeCompletionEvidence(options) {
503
- const { reportPath, currentItemId, projectRoot } = options;
503
+ const { reportPath, currentItemId, projectRoot, notBefore } = options;
504
504
  if (!currentItemId || typeof currentItemId !== "string") return false;
505
505
  let root;
506
506
  if (projectRoot !== void 0 && projectRoot !== null) {
@@ -528,8 +528,44 @@ function hasNoCodeCompletionEvidence(options) {
528
528
  }
529
529
  const ok = report.ok;
530
530
  if (ok === false) return false;
531
+ const notBeforeRaw = typeof notBefore === "string" && notBefore.trim() && !notBefore.includes("\0") ? notBefore.trim() : null;
532
+ if (notBeforeRaw) {
533
+ const floor = Date.parse(notBeforeRaw);
534
+ if (Number.isNaN(floor)) return false;
535
+ const rawAt = report.at;
536
+ if (rawAt !== void 0 && rawAt !== null) {
537
+ const atMs = parseEvidenceAtMs(rawAt);
538
+ if (Number.isNaN(atMs) || atMs < floor) {
539
+ return false;
540
+ }
541
+ }
542
+ }
531
543
  return true;
532
544
  }
545
+ function parseEvidenceAtMs(rawAt) {
546
+ let atMs = NaN;
547
+ if (typeof rawAt === "number" && Number.isFinite(rawAt)) {
548
+ atMs = rawAt;
549
+ } else if (typeof rawAt === "string" && rawAt.trim() && !rawAt.includes("\0")) {
550
+ const trimmed = rawAt.trim();
551
+ if (/^[+-]?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed)) {
552
+ const n = Number(trimmed);
553
+ if (Number.isFinite(n)) atMs = n;
554
+ } else {
555
+ atMs = Date.parse(trimmed);
556
+ }
557
+ }
558
+ if (!Number.isNaN(atMs) && atMs >= 0) {
559
+ const whole = Math.trunc(atMs);
560
+ if (Math.abs(atMs - whole) < 1e-9) {
561
+ const digits = String(Math.abs(whole)).length;
562
+ if (digits >= 1 && digits <= 10) {
563
+ atMs = whole * 1e3;
564
+ }
565
+ }
566
+ }
567
+ return atMs;
568
+ }
533
569
  function defaultVerifyReportPath(projectRoot) {
534
570
  const root = normalizeProjectRoot(projectRoot) ?? "";
535
571
  return path2.join(root, ".autopilot", "verify-last.json");
@@ -1123,9 +1159,10 @@ var StateStore = class _StateStore {
1123
1159
  ).run(nowIso(), conversationId);
1124
1160
  }
1125
1161
  /**
1126
- * Column-only redeliver hold (recover claim sleep). Also clears chain_pending so
1127
- * armChain=false recover cannot leave a phantom E3 arm. Avoids updateReviewChain
1128
- * read-merge-write clobbering confirm_left / pending under concurrency.
1162
+ * Column-only redeliver hold (recover claim sleep). Does not touch
1163
+ * chain_pending callers that need disarm (executing / ambient non-resumeFix)
1164
+ * clearChainPending before this. Mid-fix ambient resumeFix keeps
1165
+ * chain_pending for abort wasArmed handoff.
1129
1166
  */
1130
1167
  setPendingRedeliverHold(conversationId, at) {
1131
1168
  if (this.isInvalidConversationId(conversationId)) {
@@ -1137,7 +1174,7 @@ var StateStore = class _StateStore {
1137
1174
  }
1138
1175
  this.db.prepare(
1139
1176
  `UPDATE review_chains SET
1140
- pending_redeliver_at = ?, chain_pending = 0, updated_at = ?
1177
+ pending_redeliver_at = ?, updated_at = ?
1141
1178
  WHERE conversation_id = ?`
1142
1179
  ).run(stamp, nowIso(), conversationId);
1143
1180
  }
@@ -1336,7 +1373,7 @@ var StateStore = class _StateStore {
1336
1373
  * Column-only: neutralize fix/confirm/pending re-entry without ensure/session.
1337
1374
  * Used when pause-threshold upsert failed but the session is still armed — a
1338
1375
  * later completed stop must not resume the review loop via code_edited/pending
1339
- * or loopCount>0→E3 (fix_round cleared so bare loopCount cannot re-arm).
1376
+ * or checklist executing fix_round>0→E3 (fix_round cleared here).
1340
1377
  */
1341
1378
  neutralizeReviewChain(conversationId) {
1342
1379
  if (this.isInvalidConversationId(conversationId)) {
@@ -1419,7 +1456,8 @@ var StateStore = class _StateStore {
1419
1456
  }
1420
1457
  /**
1421
1458
  * Column-only pause/disarm when the full upsertSession pause write failed.
1422
- * Without this, loopCount>0 completed stops can still hit E3 while armed.
1459
+ * Without this, checklist executing completed stops can still hit E3 via
1460
+ * fix_round>0 / chain_pending while the session looks armed.
1423
1461
  */
1424
1462
  pauseSessionForRepeatedErrors(conversationId, errorCount, lastError) {
1425
1463
  if (this.isInvalidConversationId(conversationId)) {
@@ -2436,11 +2474,16 @@ var ReviewEngine = class {
2436
2474
  const transcriptPath = input.transcriptPath?.trim() || void 0;
2437
2475
  const events = transcriptPath ? readTranscriptTail(transcriptPath) : [];
2438
2476
  if (chain.code_edited === 1) {
2439
- return this.e2Fix(session, chain);
2477
+ const tip = chain.pending_followup?.trim() ?? "";
2478
+ if (!isRecoverOrStuckFollowupMessage(tip)) {
2479
+ const fix = this.e2Fix(session, chain);
2480
+ if (fix) return fix;
2481
+ }
2440
2482
  }
2483
+ const chainLive = this.store.getReviewChain(session.conversation_id) ?? chain;
2441
2484
  const redelivered = this.tryRedeliverPending(
2442
2485
  session.conversation_id,
2443
- chain,
2486
+ chainLive,
2444
2487
  events,
2445
2488
  transcriptPath
2446
2489
  );
@@ -2450,23 +2493,112 @@ var ReviewEngine = class {
2450
2493
  }
2451
2494
  if (this.pendingBlocksAdvance(
2452
2495
  session.conversation_id,
2453
- chain,
2496
+ chainLive,
2454
2497
  events,
2455
2498
  transcriptPath
2456
2499
  )) {
2457
2500
  return null;
2458
2501
  }
2459
- if (chain.confirm_left !== null && chain.confirm_left > 0) {
2460
- return this.e4Confirm(session, chain);
2502
+ const chainAfterPending = this.store.getReviewChain(session.conversation_id) ?? chainLive;
2503
+ {
2504
+ const liveTip = chainAfterPending.pending_followup?.trim() ?? "";
2505
+ if (isRecoverOrStuckFollowupMessage(liveTip)) {
2506
+ return null;
2507
+ }
2508
+ }
2509
+ if (chainAfterPending.code_edited === 1) {
2510
+ const tip = chainAfterPending.pending_followup?.trim() ?? "";
2511
+ if (!isRecoverOrStuckFollowupMessage(tip)) {
2512
+ const fix = this.e2Fix(session, chainAfterPending);
2513
+ if (fix) return fix;
2514
+ const afterE2 = this.store.getReviewChain(session.conversation_id) ?? chainAfterPending;
2515
+ const afterTip = afterE2.pending_followup?.trim() ?? "";
2516
+ if (isRecoverOrStuckFollowupMessage(afterTip)) {
2517
+ const again = this.tryRedeliverPending(
2518
+ session.conversation_id,
2519
+ afterE2,
2520
+ events,
2521
+ transcriptPath
2522
+ );
2523
+ if (again) return again;
2524
+ return null;
2525
+ }
2526
+ }
2527
+ }
2528
+ if (chainAfterPending.confirm_left !== null && chainAfterPending.confirm_left > 0) {
2529
+ return this.e4Confirm(session, chainAfterPending);
2530
+ }
2531
+ if (chainAfterPending.confirm_left === 0 || chainAfterPending.item_confirm_complete === 1 && chainAfterPending.confirm_left === null) {
2532
+ return this.e5Gate(session, chainAfterPending);
2533
+ }
2534
+ if (chainAfterPending.chain_pending === 0 && chainAfterPending.fix_round > 0 && chainAfterPending.code_edited === 0 && chainAfterPending.item_confirm_complete === 0 && isChecklistExecuting(session)) {
2535
+ const cid2 = session.conversation_id;
2536
+ const rearmed = this.store.exclusiveWrite(() => {
2537
+ if (!this.sessionRunnable(cid2)) {
2538
+ return { commit: false, value: false };
2539
+ }
2540
+ const fresh = this.store.getReviewChain(cid2);
2541
+ if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1 || fresh.fix_round <= 0 || isRecoverOrStuckFollowupMessage(
2542
+ fresh.pending_followup?.trim() ?? ""
2543
+ )) {
2544
+ return { commit: false, value: false };
2545
+ }
2546
+ this.store.updateReviewChain(cid2, { code_edited: 1 });
2547
+ return { commit: true, value: true };
2548
+ });
2549
+ if (rearmed) {
2550
+ const live = this.store.getReviewChain(cid2);
2551
+ if (live?.code_edited === 1) {
2552
+ const fix = this.e2Fix(session, live);
2553
+ if (fix) return fix;
2554
+ const afterE2 = this.store.getReviewChain(cid2) ?? live;
2555
+ const afterTip = afterE2.pending_followup?.trim() ?? "";
2556
+ if (isRecoverOrStuckFollowupMessage(afterTip)) {
2557
+ const again = this.tryRedeliverPending(
2558
+ cid2,
2559
+ afterE2,
2560
+ events,
2561
+ transcriptPath
2562
+ );
2563
+ if (again) return again;
2564
+ return null;
2565
+ }
2566
+ }
2567
+ }
2461
2568
  }
2462
- if (chain.confirm_left === 0 || chain.item_confirm_complete === 1 && chain.confirm_left === null) {
2463
- return this.e5Gate(session, chain);
2569
+ const chainNow = this.store.getReviewChain(input.conversationId) ?? chain;
2570
+ if (chainNow.code_edited === 1) {
2571
+ const tip = chainNow.pending_followup?.trim() ?? "";
2572
+ if (isRecoverOrStuckFollowupMessage(tip)) {
2573
+ const again = this.tryRedeliverPending(
2574
+ session.conversation_id,
2575
+ chainNow,
2576
+ events,
2577
+ transcriptPath
2578
+ );
2579
+ if (again) return again;
2580
+ return null;
2581
+ }
2582
+ const fix = this.e2Fix(session, chainNow);
2583
+ if (fix) return fix;
2584
+ const afterE2 = this.store.getReviewChain(session.conversation_id) ?? chainNow;
2585
+ const afterTip = afterE2.pending_followup?.trim() ?? "";
2586
+ if (isRecoverOrStuckFollowupMessage(afterTip)) {
2587
+ const again = this.tryRedeliverPending(
2588
+ session.conversation_id,
2589
+ afterE2,
2590
+ events,
2591
+ transcriptPath
2592
+ );
2593
+ if (again) return again;
2594
+ return null;
2595
+ }
2464
2596
  }
2465
- const inChain = chain.chain_pending === 1 || input.loopCount > 0 && chain.fix_round > 0 && isChecklistExecuting(session);
2466
- if (chain.confirm_left === null && chain.item_confirm_complete === 0 && inChain) {
2467
- return this.e3ArmConfirm(session, chain);
2597
+ const inChain = chainNow.chain_pending === 1 || chainNow.fix_round > 0 && isChecklistExecuting(session);
2598
+ if (chainNow.confirm_left === null && chainNow.item_confirm_complete === 0 && inChain) {
2599
+ return this.e3ArmConfirm(session, chainNow);
2468
2600
  }
2469
- return this.e0NoCodeContinue(session, chain);
2601
+ return this.e0NoCodeContinue(session, chainNow);
2470
2602
  } catch (err) {
2471
2603
  const msg = err instanceof Error ? err.message : String(err);
2472
2604
  if (msg.includes("No session for conversation") || msg.includes("Invalid conversation id")) {
@@ -2479,18 +2611,16 @@ var ReviewEngine = class {
2479
2611
  const pending = chain.pending_followup?.trim();
2480
2612
  if (!pending) return false;
2481
2613
  if (events.length > 0 && automationFollowupPresent(events, pending)) {
2482
- try {
2483
- const cleared = this.store.clearPendingFollowupIf(
2484
- conversationId,
2485
- (m) => m.trim() === pending
2486
- );
2487
- if (!cleared) {
2614
+ const cleared = this.clearMatchingPendingAndArmAmbient(
2615
+ conversationId,
2616
+ pending
2617
+ );
2618
+ if (!cleared) {
2619
+ try {
2488
2620
  const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
2489
- if (live && !(events.length > 0 && automationFollowupPresent(events, live))) {
2490
- return true;
2491
- }
2621
+ if (live) return true;
2622
+ } catch {
2492
2623
  }
2493
- } catch {
2494
2624
  }
2495
2625
  return false;
2496
2626
  }
@@ -2498,17 +2628,23 @@ var ReviewEngine = class {
2498
2628
  return true;
2499
2629
  }
2500
2630
  tryRedeliverPending(conversationId, chain, events, transcriptPath) {
2501
- const pending = chain.pending_followup?.trim();
2631
+ let pending = chain.pending_followup?.trim() ?? "";
2632
+ if (!pending) {
2633
+ try {
2634
+ pending = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
2635
+ } catch {
2636
+ pending = "";
2637
+ }
2638
+ }
2502
2639
  if (!pending) return null;
2503
2640
  if (!transcriptPath) return null;
2504
2641
  if (events.length > 0 && automationFollowupPresent(events, pending)) {
2505
- try {
2506
- const cleared = this.store.clearPendingFollowupIf(
2507
- conversationId,
2508
- (m) => m.trim() === pending
2509
- );
2510
- if (cleared) return null;
2511
- } catch {
2642
+ const cleared = this.clearMatchingPendingAndArmAmbient(
2643
+ conversationId,
2644
+ pending
2645
+ );
2646
+ if (cleared) {
2647
+ return null;
2512
2648
  }
2513
2649
  }
2514
2650
  if (events.length > 0 && followupInFlight(events)) {
@@ -2528,10 +2664,16 @@ var ReviewEngine = class {
2528
2664
  return { commit: false, value: null };
2529
2665
  }
2530
2666
  if (events.length > 0 && automationFollowupPresent(events, livePending)) {
2531
- this.store.clearPendingFollowupIf(
2667
+ const cleared = this.store.clearPendingFollowupIf(
2532
2668
  conversationId,
2533
2669
  (m) => m.trim() === livePending
2534
2670
  );
2671
+ if (cleared) {
2672
+ this.armAmbientChainAfterDeliveredFixTip(
2673
+ conversationId,
2674
+ livePending
2675
+ );
2676
+ }
2535
2677
  return { commit: true, value: null };
2536
2678
  }
2537
2679
  try {
@@ -2546,10 +2688,13 @@ var ReviewEngine = class {
2546
2688
  return { commit: true, value: null };
2547
2689
  }
2548
2690
  if (events.length > 0 && automationFollowupPresent(events, after)) {
2549
- this.store.clearPendingFollowupIf(
2691
+ const cleared = this.store.clearPendingFollowupIf(
2550
2692
  conversationId,
2551
2693
  (m) => m.trim() === after
2552
2694
  );
2695
+ if (cleared) {
2696
+ this.armAmbientChainAfterDeliveredFixTip(conversationId, after);
2697
+ }
2553
2698
  return { commit: true, value: null };
2554
2699
  }
2555
2700
  return {
@@ -2604,15 +2749,45 @@ var ReviewEngine = class {
2604
2749
  * Clear sticky code_edited so Stop→revert→resend cannot open a phantom fix
2605
2750
  * chain on the next completed stop (disk may already be clean).
2606
2751
  * Leave fix/confirm pending alone (delivery retry still valid if inject raced).
2607
- * Both clears share one exclusiveWrite so a mid-abort failure cannot leave
2752
+ *
2753
+ * Project-scope ambient/planning: after recover soft-reset cleared
2754
+ * chain_pending, sticky code_edited may be the only handoff marker. Clearing
2755
+ * it without a freeze leaves fix_round residue that cannot E3 (bare fix_round
2756
+ * must not phantom-confirm). Freeze into confirm_left, or re-arm chain_pending
2757
+ * when an undelivered fix tip remains. Checklist executing keeps residue-E2.
2758
+ *
2759
+ * Clears + freeze share one exclusiveWrite so a mid-abort failure cannot leave
2608
2760
  * recover gone while code_edited sticky (or the reverse).
2609
2761
  */
2610
2762
  handleAbortedStop(session) {
2611
2763
  try {
2612
2764
  const cid2 = session.conversation_id;
2765
+ const ambientFreeze = this.config.reviewScope === "project" && !isChecklistExecuting(session);
2613
2766
  this.store.exclusiveWrite(() => {
2614
2767
  this.store.clearPendingFollowupIf(cid2, isRecoverOrStuckFollowupMessage);
2768
+ const chain = this.store.getReviewChain(cid2);
2769
+ const pending = chain?.pending_followup?.trim() ?? "";
2770
+ const fixPending = this.isFixFollowupMessage(pending);
2771
+ const mid = this.isMidConfirmOrE5(chain);
2772
+ const fixRound = chain?.fix_round ?? 0;
2773
+ const wasArmed = chain?.chain_pending === 1;
2615
2774
  this.store.clearCodeEdited(cid2);
2775
+ if (ambientFreeze && !mid && fixRound > 0) {
2776
+ if (fixPending) {
2777
+ this.store.updateReviewChain(cid2, { chain_pending: 1 });
2778
+ } else if (!pending && wasArmed) {
2779
+ const rounds = this.config.confirmRounds;
2780
+ if (rounds > 0) {
2781
+ this.store.updateReviewChain(cid2, {
2782
+ confirm_left: rounds,
2783
+ chain_pending: 0,
2784
+ code_edited: 0
2785
+ });
2786
+ } else {
2787
+ this.store.updateReviewChain(cid2, { chain_pending: 1 });
2788
+ }
2789
+ }
2790
+ }
2616
2791
  return { commit: true, value: void 0 };
2617
2792
  });
2618
2793
  } catch {
@@ -2798,7 +2973,10 @@ var ReviewEngine = class {
2798
2973
  if (!stamp2) {
2799
2974
  return { commit: false, value: { role: "failed" } };
2800
2975
  }
2801
- this.store.clearChainPending(conversationId);
2976
+ const liveRedeliver = this.store.getReviewChain(conversationId);
2977
+ if (liveRedeliver?.code_edited !== 1) {
2978
+ this.store.clearChainPending(conversationId);
2979
+ }
2802
2980
  this.armRecoverClaimRedeliverHold(conversationId, debounceMs);
2803
2981
  return {
2804
2982
  commit: true,
@@ -2809,13 +2987,15 @@ var ReviewEngine = class {
2809
2987
  this.applySoftResetAmbientChainForErrorRecover(conversationId);
2810
2988
  const live = this.store.getReviewChain(conversationId);
2811
2989
  if (fixTipAtClaim && live && !this.isMidConfirmOrE5(live)) {
2812
- this.store.updateReviewChain(conversationId, { code_edited: 1 });
2990
+ this.store.updateReviewChain(conversationId, {
2991
+ code_edited: 1,
2992
+ chain_pending: 1
2993
+ });
2813
2994
  }
2814
2995
  } else {
2815
- const resumeFix = !this.isMidConfirmOrE5(chain) && (this.isFixFollowupMessage(pending) || chain?.code_edited === 1 || fixTipAtClaim);
2816
- if (resumeFix) {
2817
- this.store.updateReviewChain(conversationId, { code_edited: 1 });
2818
- }
2996
+ this.applyExecutingErrorRecoverChainAdjustments(conversationId, {
2997
+ fixTipAtClaim
2998
+ });
2819
2999
  }
2820
3000
  this.store.savePendingFollowup(conversationId, message, {
2821
3001
  armChain: false
@@ -2824,7 +3004,14 @@ var ReviewEngine = class {
2824
3004
  if (!stamp) {
2825
3005
  return { commit: false, value: { role: "failed" } };
2826
3006
  }
2827
- this.store.clearChainPending(conversationId);
3007
+ if (!ambient) {
3008
+ this.store.clearChainPending(conversationId);
3009
+ } else {
3010
+ const liveAfter = this.store.getReviewChain(conversationId);
3011
+ if (liveAfter?.code_edited !== 1) {
3012
+ this.store.clearChainPending(conversationId);
3013
+ }
3014
+ }
2828
3015
  this.armRecoverClaimRedeliverHold(conversationId, debounceMs);
2829
3016
  return {
2830
3017
  commit: true,
@@ -2999,7 +3186,10 @@ var ReviewEngine = class {
2999
3186
  if (!this.store.casBumpPendingFollowupAt(conversationId, expectedStamp)) {
3000
3187
  return { commit: false, value: null };
3001
3188
  }
3002
- this.store.clearChainPending(conversationId);
3189
+ const afterBump = this.store.getReviewChain(conversationId);
3190
+ if (afterBump?.code_edited !== 1) {
3191
+ this.store.clearChainPending(conversationId);
3192
+ }
3003
3193
  return {
3004
3194
  commit: true,
3005
3195
  value: {
@@ -3118,29 +3308,39 @@ var ReviewEngine = class {
3118
3308
  */
3119
3309
  emitRecoverAfterFailedClaim(conversationId, action, opts) {
3120
3310
  const ambientSoftReset = opts?.ambientSoftReset === true;
3121
- try {
3122
- return this.store.exclusiveWrite(() => {
3123
- const session = this.store.getSession(conversationId);
3124
- if (!session || !this.sessionErrorRecoverable(session)) {
3125
- return { commit: false, value: null };
3126
- }
3127
- const pending = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3128
- if (isRecoverFollowupMessage(pending)) {
3129
- return { commit: false, value: null };
3130
- }
3131
- if (ambientSoftReset) {
3132
- this.applySoftResetAmbientChainForErrorRecover(conversationId);
3133
- }
3134
- this.store.savePendingFollowup(conversationId, action.message, {
3135
- armChain: false
3136
- });
3137
- const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3138
- if (!isRecoverFollowupMessage(live)) {
3139
- return { commit: false, value: null };
3140
- }
3141
- this.store.clearChainPending(conversationId);
3142
- return { commit: true, value: action };
3311
+ const writeRecover = () => this.store.exclusiveWrite(() => {
3312
+ const session = this.store.getSession(conversationId);
3313
+ if (!session || !this.sessionErrorRecoverable(session)) {
3314
+ return { commit: false, value: null };
3315
+ }
3316
+ const pending = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3317
+ if (isRecoverFollowupMessage(pending)) {
3318
+ return { commit: false, value: null };
3319
+ }
3320
+ if (ambientSoftReset) {
3321
+ this.applySoftResetAmbientChainForErrorRecover(conversationId);
3322
+ } else {
3323
+ this.applyExecutingErrorRecoverChainAdjustments(conversationId);
3324
+ }
3325
+ this.store.savePendingFollowup(conversationId, action.message, {
3326
+ armChain: false
3143
3327
  });
3328
+ const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3329
+ if (!isRecoverFollowupMessage(live)) {
3330
+ return { commit: false, value: null };
3331
+ }
3332
+ if (!ambientSoftReset) {
3333
+ this.store.clearChainPending(conversationId);
3334
+ } else {
3335
+ const liveChain = this.store.getReviewChain(conversationId);
3336
+ if (liveChain?.code_edited !== 1) {
3337
+ this.store.clearChainPending(conversationId);
3338
+ }
3339
+ }
3340
+ return { commit: true, value: action };
3341
+ });
3342
+ try {
3343
+ return writeRecover();
3144
3344
  } catch {
3145
3345
  if (!this.sessionStillErrorRecoverable(conversationId)) {
3146
3346
  return null;
@@ -3152,9 +3352,40 @@ var ReviewEngine = class {
3152
3352
  }
3153
3353
  } catch {
3154
3354
  }
3355
+ try {
3356
+ return writeRecover();
3357
+ } catch {
3358
+ }
3359
+ try {
3360
+ return this.store.exclusiveWrite(() => {
3361
+ if (!this.sessionStillErrorRecoverable(conversationId)) {
3362
+ return { commit: false, value: null };
3363
+ }
3364
+ const pending = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3365
+ if (isRecoverFollowupMessage(pending)) {
3366
+ return { commit: false, value: null };
3367
+ }
3368
+ this.store.savePendingFollowup(conversationId, action.message, {
3369
+ armChain: false
3370
+ });
3371
+ const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
3372
+ if (!isRecoverFollowupMessage(live)) {
3373
+ return { commit: false, value: null };
3374
+ }
3375
+ const liveChain = this.store.getReviewChain(conversationId);
3376
+ if (liveChain?.code_edited !== 1) {
3377
+ this.store.clearChainPending(conversationId);
3378
+ }
3379
+ return { commit: true, value: action };
3380
+ });
3381
+ } catch {
3382
+ }
3155
3383
  this.emit(conversationId, action);
3156
3384
  try {
3157
- this.store.clearChainPending(conversationId);
3385
+ const liveChain = this.store.getReviewChain(conversationId);
3386
+ if (liveChain?.code_edited !== 1) {
3387
+ this.store.clearChainPending(conversationId);
3388
+ }
3158
3389
  } catch {
3159
3390
  }
3160
3391
  try {
@@ -3197,6 +3428,37 @@ var ReviewEngine = class {
3197
3428
  return "failed";
3198
3429
  }
3199
3430
  }
3431
+ /**
3432
+ * Executing error-recover chain adjust (call under exclusiveWrite, before
3433
+ * savePendingFollowup + clearChainPending). Mirrors ambient readyForE3 /
3434
+ * resumeFix without soft-resetting mid-confirm/E5.
3435
+ *
3436
+ * Ready-for-E3 (delivered fix, chain_pending only, empty pending) arms
3437
+ * confirm_left=confirmRounds so post-recover continues E4 — not another E2
3438
+ * solely from fix_round>0. Mid-fix residue still forces code_edited.
3439
+ */
3440
+ applyExecutingErrorRecoverChainAdjustments(conversationId, opts) {
3441
+ this.store.ensureReviewChain(conversationId);
3442
+ const chain = this.store.getReviewChain(conversationId);
3443
+ if (!chain) return;
3444
+ if (isRecoverFollowupMessage(chain.pending_followup?.trim() ?? "")) {
3445
+ return;
3446
+ }
3447
+ const pending = chain.pending_followup?.trim() ?? "";
3448
+ const readyForE3 = !this.isMidConfirmOrE5(chain) && chain.chain_pending === 1 && chain.code_edited === 0 && pending.length === 0;
3449
+ const rounds = this.config.confirmRounds;
3450
+ if (readyForE3 && rounds > 0) {
3451
+ this.store.updateReviewChain(conversationId, {
3452
+ confirm_left: rounds,
3453
+ code_edited: 0
3454
+ });
3455
+ return;
3456
+ }
3457
+ const resumeFix = !this.isMidConfirmOrE5(chain) && (this.isFixFollowupMessage(pending) || chain.code_edited === 1 || opts?.fixTipAtClaim === true || chain.fix_round > 0);
3458
+ if (resumeFix) {
3459
+ this.store.updateReviewChain(conversationId, { code_edited: 1 });
3460
+ }
3461
+ }
3200
3462
  /**
3201
3463
  * Ambient/planning error recover: drop confirm/pending arming so recover
3202
3464
  * (armChain=false) cannot leave chain_pending=1 → phantom E3.
@@ -3211,7 +3473,8 @@ var ReviewEngine = class {
3211
3473
  * do not regress to another fix. Ready-for-E3 requires empty pending (not
3212
3474
  * merely !fixPending). Force code_edited only for an active fix
3213
3475
  * (code_edited / undelivered fix pending / chain_pending with pending still
3214
- * set). Bare leftover fix_round is ignored (ambient phantom residue).
3476
+ * set). Mid-fix resumeFix keeps chain_pending=1 so abort can freeze via
3477
+ * wasArmed; bare leftover fix_round stays disarmed (ambient phantom residue).
3215
3478
  */
3216
3479
  applySoftResetAmbientChainForErrorRecover(conversationId) {
3217
3480
  this.store.ensureReviewChain(conversationId);
@@ -3230,7 +3493,10 @@ var ReviewEngine = class {
3230
3493
  this.store.softResetAmbientChainUnlessRecover(conversationId, {
3231
3494
  confirm_left: atE5 || midConfirm ? chain.confirm_left : readyForE3 && rounds > 0 ? rounds : null,
3232
3495
  item_confirm_complete: atE5 ? chain.item_confirm_complete : 0,
3233
- chain_pending: 0,
3496
+ // Mid-fix resume keeps chain_pending so abort clearing code_edited still
3497
+ // has wasArmed handoff (freeze into confirm). Ready-for-E3 / idle residue
3498
+ // stay 0 — bare fix_round must not phantom-E3 after recover tip clears.
3499
+ chain_pending: resumeFix ? 1 : 0,
3234
3500
  // Preserve an in-flight edit marker (E2 wins over E4); else force only for mid-fix.
3235
3501
  code_edited: resumeFix || chain.code_edited === 1 ? 1 : 0
3236
3502
  });
@@ -3240,6 +3506,51 @@ var ReviewEngine = class {
3240
3506
  const line = firstSubstantiveLine(message) || message.trim();
3241
3507
  return line.startsWith("Review fix") || line.startsWith("\u81EA\u5BA1\u4FEE\u590D");
3242
3508
  }
3509
+ /**
3510
+ * Unlocked callers: clear snapshot needle + ambient E3 arm in one
3511
+ * exclusiveWrite. A crash mid-pair must not leave tip cleared with
3512
+ * chain_pending still 0 (silent ambient stall). Safe under an open writer
3513
+ * only via {@link armAmbientChainAfterDeliveredFixTip} after an inline clear
3514
+ * (clearPendingFollowupIf runs in-txn when writeDepth > 0).
3515
+ */
3516
+ clearMatchingPendingAndArmAmbient(conversationId, pending) {
3517
+ try {
3518
+ return this.store.exclusiveWrite(() => {
3519
+ const cleared = this.store.clearPendingFollowupIf(
3520
+ conversationId,
3521
+ (m) => m.trim() === pending
3522
+ );
3523
+ if (cleared) {
3524
+ this.armAmbientChainAfterDeliveredFixTip(conversationId, pending);
3525
+ }
3526
+ return { commit: true, value: cleared };
3527
+ });
3528
+ } catch {
3529
+ return false;
3530
+ }
3531
+ }
3532
+ /**
3533
+ * After a delivered ambient fix tip is cleared, recover/abort may have left
3534
+ * chain_pending=0. Re-arm so this completed stop can E3 → confirm instead of
3535
+ * silent stall. Never arms bare stale fix_round without a delivered fix tip
3536
+ * (keeps F-ERR-AMBIENT-STALE-FIX-ROUND / F-ABORT-NO-PHANTOM-FIX).
3537
+ * Call under the same writer txn as the clear when possible — throws must
3538
+ * propagate so the clear can roll back. Still arms when code_edited=1 so a
3539
+ * concurrent abort clearing sticky edit keeps wasArmed handoff.
3540
+ */
3541
+ armAmbientChainAfterDeliveredFixTip(conversationId, deliveredPending) {
3542
+ if (!this.isFixFollowupMessage(deliveredPending)) return;
3543
+ if (this.config.reviewScope !== "project") return;
3544
+ const session = this.store.getSession(conversationId);
3545
+ if (!session || isChecklistExecuting(session)) return;
3546
+ const chain = this.store.getReviewChain(conversationId);
3547
+ if (!chain || chain.fix_round <= 0) return;
3548
+ if (this.isMidConfirmOrE5(chain)) return;
3549
+ if (chain.chain_pending === 1) return;
3550
+ const live = chain.pending_followup?.trim() ?? "";
3551
+ if (live) return;
3552
+ this.store.updateReviewChain(conversationId, { chain_pending: 1 });
3553
+ }
3243
3554
  /** Mid-confirm or E5-ready — must not phantom-arm code_edited over E4/E5. */
3244
3555
  isMidConfirmOrE5(chain) {
3245
3556
  if (!chain) return false;
@@ -3287,7 +3598,9 @@ var ReviewEngine = class {
3287
3598
  */
3288
3599
  e0NoCodeContinue(session, chain) {
3289
3600
  if (!isChecklistExecuting(session)) return null;
3290
- if (chain.code_edited === 1 || chain.confirm_left !== null || chain.item_confirm_complete === 1 || chain.chain_pending === 1) {
3601
+ if (chain.code_edited === 1 || chain.confirm_left !== null || chain.item_confirm_complete === 1 || chain.chain_pending === 1 || // Mid-item review residue (e.g. recover cleared chain_pending): never
3602
+ // soft-advance/done — caller should have taken residue-E2 or E3.
3603
+ chain.fix_round > 0) {
3291
3604
  return null;
3292
3605
  }
3293
3606
  const parsed = this.parseSessionChecklist(session);
@@ -3322,7 +3635,8 @@ var ReviewEngine = class {
3322
3635
  if (!hasNoCodeCompletionEvidence({
3323
3636
  reportPath,
3324
3637
  currentItemId: liveId,
3325
- projectRoot: trustRoot ?? void 0
3638
+ projectRoot: trustRoot ?? void 0,
3639
+ notBefore: liveChain?.updated_at
3326
3640
  })) {
3327
3641
  return null;
3328
3642
  }
@@ -3378,7 +3692,7 @@ var ReviewEngine = class {
3378
3692
  return { commit: false, value: null };
3379
3693
  }
3380
3694
  const fresh = this.store.getReviewChain(cid2);
3381
- if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1) {
3695
+ if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1 || fresh.fix_round > 0 || isRecoverOrStuckFollowupMessage(fresh.pending_followup?.trim() ?? "")) {
3382
3696
  return { commit: false, value: null };
3383
3697
  }
3384
3698
  const refreshed = this.parseSessionChecklist(lockedSession);
@@ -3398,7 +3712,8 @@ var ReviewEngine = class {
3398
3712
  if (!hasNoCodeCompletionEvidence({
3399
3713
  reportPath,
3400
3714
  currentItemId: targets.current.id,
3401
- projectRoot: trustRoot ?? void 0
3715
+ projectRoot: trustRoot ?? void 0,
3716
+ notBefore: fresh.updated_at
3402
3717
  })) {
3403
3718
  return { commit: false, value: null };
3404
3719
  }
@@ -3489,7 +3804,7 @@ var ReviewEngine = class {
3489
3804
  return { commit: false, value: null };
3490
3805
  }
3491
3806
  const fresh = this.store.getReviewChain(cid2);
3492
- if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1) {
3807
+ if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1 || fresh.fix_round > 0 || isRecoverOrStuckFollowupMessage(fresh.pending_followup?.trim() ?? "")) {
3493
3808
  return { commit: false, value: null };
3494
3809
  }
3495
3810
  const refreshed = this.parseSessionChecklist(sess);
@@ -3508,7 +3823,8 @@ var ReviewEngine = class {
3508
3823
  if (hasNoCodeCompletionEvidence({
3509
3824
  reportPath,
3510
3825
  currentItemId: lockedItem.id,
3511
- projectRoot: trustRoot ?? void 0
3826
+ projectRoot: trustRoot ?? void 0,
3827
+ notBefore: fresh.updated_at
3512
3828
  })) {
3513
3829
  return { commit: false, value: null };
3514
3830
  }
@@ -3568,7 +3884,7 @@ var ReviewEngine = class {
3568
3884
  return { commit: false, value: false };
3569
3885
  }
3570
3886
  const fresh = this.store.getReviewChain(conversationId);
3571
- if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1) {
3887
+ if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || fresh.chain_pending === 1 || fresh.fix_round > 0) {
3572
3888
  return { commit: false, value: false };
3573
3889
  }
3574
3890
  this.store.updateReviewChain(conversationId, {
@@ -3615,6 +3931,10 @@ var ReviewEngine = class {
3615
3931
  if (!fresh || fresh.code_edited !== 1) {
3616
3932
  return { commit: false, value: null };
3617
3933
  }
3934
+ const pendingLive = fresh.pending_followup?.trim() ?? "";
3935
+ if (isRecoverOrStuckFollowupMessage(pendingLive)) {
3936
+ return { commit: false, value: null };
3937
+ }
3618
3938
  const fixRound = this.nextSessionRound(fresh);
3619
3939
  const message = this.render("review.fix", { round: fixRound, total: rounds });
3620
3940
  const out = {
@@ -3651,7 +3971,8 @@ var ReviewEngine = class {
3651
3971
  return { commit: false, value: null };
3652
3972
  }
3653
3973
  const fresh = this.store.getReviewChain(cid2);
3654
- if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1) {
3974
+ const pendingLive = fresh?.pending_followup?.trim() ?? "";
3975
+ if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== null || fresh.item_confirm_complete === 1 || this.isFixFollowupMessage(pendingLive) || isRecoverOrStuckFollowupMessage(pendingLive)) {
3655
3976
  return { commit: false, value: null };
3656
3977
  }
3657
3978
  const sessionRound = this.nextSessionRound(fresh);
@@ -3701,6 +4022,10 @@ var ReviewEngine = class {
3701
4022
  if (!fresh || fresh.code_edited === 1 || fresh.confirm_left !== expectedLeft) {
3702
4023
  return { commit: false, value: null };
3703
4024
  }
4025
+ const pendingLive = fresh.pending_followup?.trim() ?? "";
4026
+ if (isRecoverOrStuckFollowupMessage(pendingLive)) {
4027
+ return { commit: false, value: null };
4028
+ }
3704
4029
  const sessionRound = this.nextSessionRound(fresh);
3705
4030
  const message = this.render(kind, {
3706
4031
  n,
@@ -3780,6 +4105,10 @@ var ReviewEngine = class {
3780
4105
  if (!atE5) {
3781
4106
  return { commit: false, value: null };
3782
4107
  }
4108
+ const pendingLive = fresh.pending_followup?.trim() ?? "";
4109
+ if (isRecoverOrStuckFollowupMessage(pendingLive)) {
4110
+ return { commit: false, value: null };
4111
+ }
3783
4112
  const sess = this.store.getSession(cid2);
3784
4113
  if (!sess || !isChecklistExecuting(sess)) {
3785
4114
  return { commit: false, value: null };
@@ -3870,6 +4199,10 @@ var ReviewEngine = class {
3870
4199
  if (!atE5) {
3871
4200
  return { commit: false, value: null };
3872
4201
  }
4202
+ const pendingLive = fresh.pending_followup?.trim() ?? "";
4203
+ if (isRecoverOrStuckFollowupMessage(pendingLive)) {
4204
+ return { commit: false, value: null };
4205
+ }
3873
4206
  const lockedSession = this.store.getSession(cid2);
3874
4207
  if (!lockedSession || !sessionReviewRunnable(lockedSession, this.config.reviewScope)) {
3875
4208
  return { commit: false, value: null };
@@ -75,7 +75,7 @@ export interface HooksFile {
75
75
  [event: string]: HookCommand[] | undefined;
76
76
  };
77
77
  }
78
- export declare const PACKAGE_VERSION = "0.2.5";
78
+ export declare const PACKAGE_VERSION = "0.2.7";
79
79
  export declare const HOOK_MARKER = "autopilot-harness";
80
80
  export declare const AUTOPILOT_EVENTS: readonly ["beforeSubmitPrompt", "afterFileEdit", "stop"];
81
81
  //# sourceMappingURL=types.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /** Shared types for Autopilot init. */
2
- export const PACKAGE_VERSION = "0.2.5";
2
+ export const PACKAGE_VERSION = "0.2.7";
3
3
  export const HOOK_MARKER = "autopilot-harness";
4
4
  export const AUTOPILOT_EVENTS = [
5
5
  "beforeSubmitPrompt",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autopilot-harness/cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Autopilot harness CLI — structured planning, checklist execution, and multi-lens self-review for Cursor and Claude Code (bin: autopilot-harness)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "@clack/prompts": "^0.10.0",
32
32
  "commander": "^13.1.0",
33
33
  "yaml": "^2.7.0",
34
- "@autopilot-harness/core": "0.2.5",
35
- "@autopilot-harness/port-cursor": "0.2.5",
36
- "@autopilot-harness/i18n": "0.2.5",
37
- "@autopilot-harness/port-claude-code": "0.2.5"
34
+ "@autopilot-harness/core": "0.2.7",
35
+ "@autopilot-harness/port-claude-code": "0.2.7",
36
+ "@autopilot-harness/i18n": "0.2.7",
37
+ "@autopilot-harness/port-cursor": "0.2.7"
38
38
  },
39
39
  "devDependencies": {
40
40
  "esbuild": "^0.25.12",