pointlock-runner 0.1.3

The Pointlock execution engine: state machine, verdict fold, crash-safe resume alignment, and localized repair.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
//! The public entry points: [`Runner::run`] and [`Runner::resume`] /
//! [`Runner::resume_with_subflows`] (spine ยง6; 07 ยง4โ€“ยง5).
//!
//! Since M2 the entry points accept the resolved subflow registry
//! (`Map<irHash, FlowIR>`, provided by the assembly layer; every entry
//! self-verifies at load). Resume comes in two regimes:
//!
//! - **Same-IR resume** (the common suspend/crash continuation): every
//!   completed step *instance* is adopted by its exact run path โ€” the
//!   walk falls back into open call frames and foreach iterations without
//!   restarting any frame (07 ยง4.6), with the archived control snapshots
//!   (cond / items / inputs) never re-evaluated (I3).
//! - **Cross-IR resume** (repair): the 07 ยง5.2 *flat* subset โ€” alignment,
//!   offline re-judge and the `requiresConfirmation` gates over top-level
//!   action steps. The nested rules (call down-drill, per-iteration
//!   alignment, order-consistency) land with the repair wave; the
//!   combination is refused with a typed error, never silently guessed.

use std::collections::{BTreeMap, BTreeSet};

use pointlock_ir::{
    ActionStepIR, AlignmentClass, AlignmentEntry, AlignmentReport, BindingState, CheckpointView,
    FlowIR, Hash, PathFrame, ReconcileResult, RequiresConfirmation, RunLogPayload, RunPath,
    SupervisePolicy, ir_hash,
};
use pointlock_provider_kit::{CancellationToken, ProviderSession, SessionOutcome};
use pointlock_store::{NewRun, Store};
use serde_json::{Map, Value};

use crate::align::{Alignment, Harvest, align, harvest, live_frame_pins};
use crate::engine::{
    Adopted, Execution, FrameState, FrontierWork, HumanRequestFact, RunOutcome, gated_mutating,
    instance_key, is_history, now_ms, params_with_defaults, replay_permitted, root_path,
};
use crate::error::{BlockedReason, RunnerError};
use crate::load::{LoadedFlow, check_attestation, load};
use crate::scope::ScopeSeed;

/// Options of [`Runner::run`].
pub struct RunOptions {
    /// Cooperative stop token, honored at step boundaries
    /// (`runSuspended` โ†’ [`RunOutcome::Suspended`]).
    pub stop: CancellationToken,
    /// Explicit run id; a UUIDv4 is generated when absent.
    pub run_id: Option<String>,
    /// The bound device (checkpoint hard binding; also `env.deviceId`).
    pub device_id: String,
    /// The device platform for `env.platform`, when known (comes from the
    /// lockfile at the assembly layer; the SPI attestation does not carry
    /// it).
    pub platform: Option<String>,
    /// The vision verifier consulted by `vision` verify-chain tails.
    /// `None` is equivalent to
    /// [`pointlock_vision::StubVisionVerifier`]: the vision channel cannot
    /// complete and reports `"vision verifier not configured"` โ€” the chain
    /// degrades honestly toward `unknown` (principle 4).
    pub vision: Option<std::sync::Arc<dyn pointlock_vision::VisionVerifier>>,
    /// The resolved subflow registry keyed by `irHash` (07 ยง1.3): every
    /// callee the flow's `subflows` table pins must be present; entries
    /// self-verify at load. Empty for flows without subflows.
    pub subflows: BTreeMap<Hash, FlowIR>,
    /// This segment's supervision policy (R13, spine ยง6.9): recorded in
    /// `runStarted.supervisePolicy` (explicitly `null` when absent) and
    /// gates action-step dispatch (`mutating` gates mutating steps,
    /// `all` every action step). Per segment, never inherited.
    pub supervise: Option<SupervisePolicy>,
    /// Injectable wall clock for human-deadline computation and lazy
    /// timeout settlement (tests); `None` uses the system clock.
    pub clock: Option<std::sync::Arc<dyn Fn() -> u64 + Send + Sync>>,
}

impl RunOptions {
    /// Options with a fresh stop token, no explicit run id, no vision
    /// verifier (stub-equivalent), an empty subflow registry, no
    /// supervision and the system clock.
    pub fn new(device_id: impl Into<String>) -> Self {
        RunOptions {
            stop: CancellationToken::new(),
            run_id: None,
            device_id: device_id.into(),
            platform: None,
            vision: None,
            subflows: BTreeMap::new(),
            supervise: None,
            clock: None,
        }
    }
}

/// Options of [`Runner::resume`].
#[derive(Default)]
pub struct ResumeOptions {
    /// Cooperative stop token (see [`RunOptions::stop`]).
    pub stop: CancellationToken,
    /// `env.platform`, when known.
    pub platform: Option<String>,
    /// The FlowIR the run originally executed โ€” optional, and worth
    /// supplying. Alignment reads the archived execution-time per-step
    /// hashes from the checkpoint's `StepRecord`s (harvested from
    /// `stepEntered`, spine ยง6.1 M1 note), so cross-IR resume works
    /// without it; supplying it additionally unlocks the preflight-only
    /// sub-domain comparison (07 ยง5.3 / 02 ยง12.3 ruling 6) โ€” a
    /// `judgeDirty` step whose only change is `preflight` adopts its
    /// archived verdict outright instead of re-judging or re-executing.
    /// Verified against the checkpoint's `irHash`
    /// ([`RunnerError::OldIrMismatch`]); a mismatch is a caller error,
    /// surfaced not ignored.
    pub old_flow_ir: Option<FlowIR>,
    /// This segment's supervision policy (R13, spine ยง6.9): recorded in
    /// `runResumed.supervisePolicy` (explicitly `null` when absent).
    /// Per segment, never inherited โ€” an unset value means this segment
    /// runs unsupervised regardless of previous segments; a supervision
    /// request already pending still settles by its arbitrated response.
    pub supervise: Option<SupervisePolicy>,
    /// The vision verifier of this segment (see [`RunOptions::vision`]);
    /// `None` is stub-equivalent โ€” vision tails degrade to `unknown`.
    pub vision: Option<std::sync::Arc<dyn pointlock_vision::VisionVerifier>>,
    /// Step ids the author FORCES back to execution this segment
    /// (07 ยง5.3, the CLI's repeatable `--force-reexecute <stepId>`):
    /// each named step classifies `effectDirty` regardless of its hashes,
    /// so the resume point rolls back to the earliest of them and they
    /// re-run against the live world.
    ///
    /// The escape hatch for a re-judge the author rejects โ€” an offline
    /// re-judge that can only reach `unknown` because the archive lacks
    /// the observation channel the new assertion needs (ใ€Œ็ผบๆ–™ โ†’
    /// unknownใ€), or an adopted result the author no longer trusts. It
    /// upgrades the CLASSIFICATION only: a forced step that is mutating
    /// and already effective still walks the 07 ยง5.4 gate and needs
    /// `--allow-mutating-reexec` besides โ€” forcing says "run it again",
    /// authorizing says "yes, even though the world holds its effect".
    /// Like the authorization list it covers this resume only, and it is
    /// cross-IR vocabulary: a same-IR resume has no classification to
    /// upgrade.
    pub force_reexecute: Vec<String>,
    /// Step ids the author explicitly authorized for mutating
    /// re-execution this segment (07 ยง5.4 step 2, the CLI's repeatable
    /// `--allow-mutating-reexec <stepId>`).
    ///
    /// Each id releases exactly one `requiresConfirmation` entry; there is
    /// no wildcard, and the authorization covers **this resume only** โ€”
    /// nothing about it is persisted, so the next resume re-gates from
    /// scratch. An id naming no gated step is refused rather than ignored:
    /// silently accepting it would let an author believe they had cleared
    /// something they had not.
    ///
    /// Releasing the gate does not skip the world check: the step still
    /// enters `probing` and evaluates its `preflight` (ยง5.4 step 3), which
    /// is what meets the residue of the earlier effect.
    pub allow_mutating_reexec: Vec<String>,
    /// Injectable wall clock (see [`RunOptions::clock`]).
    pub clock: Option<std::sync::Arc<dyn Fn() -> u64 + Send + Sync>>,
}

/// The runner: executes a sealed [`FlowIR`] against an open provider
/// session, journaling every transition into the single-writer store.
/// Entry signatures accept only `FlowIR`, never strings (principles 1/2).
pub struct Runner;

impl Runner {
    /// Runs a flow from the beginning (spine ยง6.2 pipeline; ยง6.1 event
    /// vocabulary). This segment's `supervisePolicy` is recorded verbatim
    /// in `runStarted` โ€” explicitly `null` when unsupervised (R13,
    /// per-segment self-describing ledger).
    pub async fn run(
        flow: &FlowIR,
        params: Value,
        session: Box<dyn ProviderSession>,
        store: &mut Store,
        opts: RunOptions,
    ) -> Result<RunOutcome, RunnerError> {
        let RunOptions {
            stop,
            run_id,
            device_id,
            platform,
            vision,
            subflows,
            supervise,
            clock,
        } = opts;
        let loaded = load(flow, &subflows)?;
        check_attestation(&loaded, session.attestation())?;
        let params = params_with_defaults(flow, params)?;

        let cursor = session.current_cursor().await?;
        let initial_lineage = vec![cursor.session_id.clone()];
        let run_id = store.begin_run(NewRun {
            run_id,
            flow_id: flow.flow_id.clone(),
            ir_hash: flow.ir_hash.clone(),
            lockfile_digest: flow.lockfile_digest.clone(),
            params_snapshot: Value::Object(params.clone()),
            binding: BindingState {
                device_id: device_id.clone(),
                session_lineage: vec![cursor.session_id.clone()],
                event_cursor: cursor,
            },
            created_at_ms: now_ms(),
        })?;
        store.append_event(
            &run_id,
            now_ms(),
            &root_path(flow),
            &RunLogPayload::RunStarted {
                ir_hash: flow.ir_hash.clone(),
                lockfile_digest: flow.lockfile_digest.clone(),
                params_snapshot: Value::Object(params.clone()),
                // R13: this segment's real policy โ€” explicitly null when
                // unsupervised (per-segment, self-describing).
                supervise_policy: supervise,
            },
        )?;

        let env = env_bindings(&device_id, platform.as_deref(), &run_id);
        let exec = Execution {
            flows: &loaded,
            session,
            store,
            run_id,
            stop,
            env,
            session_lineage: initial_lineage,
            pending_summaries: Default::default(),
            attempt_base: Default::default(),
            open_spans: Default::default(),
            live_frames: Default::default(),
            // A fresh run never re-touches a world it stopped watching.
            resumed: false,
            authorized: BTreeSet::new(),
            reentry_seen: false,
            adoptable: Default::default(),
            frontier: None,
            vision,
            supervise,
            human: Default::default(),
            settled: Default::default(),
            recorded_verdicts: Default::default(),
            hook_triggers: Default::default(),
            clock,
        };
        let root = FrameState::new(flow, root_path(flow), params, 1);
        exec.run(root, 0).await
    }

    /// Resumes a run (07 ยง4) without subflows. Legality โŸบ (A) every
    /// completed record is still recognized under the (possibly repaired)
    /// new IR โ€” recorded as `alignmentReport` in `runResumed`; (B) a
    /// pending intent on the frontier has been reconciled
    /// (`ProviderSession::reconcile`); (C) the world passes the resume
    /// probes โ€” the first to-execute step's declared `preflight` runs
    /// before its act; a step without probes resumes honestly `unprobed`
    /// (I3).
    pub async fn resume(
        new_flow: &FlowIR,
        run_id: &str,
        session: Box<dyn ProviderSession>,
        store: &mut Store,
        opts: ResumeOptions,
    ) -> Result<RunOutcome, RunnerError> {
        let subflows = BTreeMap::new();
        Self::resume_with_subflows(new_flow, &subflows, run_id, session, store, opts).await
    }

    /// [`Runner::resume`] with a resolved subflow registry โ€” required when
    /// the (new) flow pins callees; see [`RunOptions::subflows`].
    pub async fn resume_with_subflows(
        new_flow: &FlowIR,
        subflows: &BTreeMap<Hash, FlowIR>,
        run_id: &str,
        session: Box<dyn ProviderSession>,
        store: &mut Store,
        opts: ResumeOptions,
    ) -> Result<RunOutcome, RunnerError> {
        let loaded = load(new_flow, subflows)?;
        check_attestation(&loaded, session.attestation())?;
        let view = store.rebuild_checkpoint(run_id)?;
        let events = store.events(run_id)?;
        let facts = harvest(&events);

        // The optional old-IR integrity check: when the caller supplies
        // one, verify it is the IR the run executed (a mismatched old IR
        // is a caller error, surfaced not ignored).
        if let Some(old) = opts.old_flow_ir.as_ref() {
            let computed = ir_hash(old);
            if computed != view.ir_hash {
                return Err(RunnerError::OldIrMismatch {
                    expected: view.ir_hash.clone(),
                    computed,
                });
            }
        }

        if view.ir_hash == new_flow.ir_hash {
            resume_same_ir(loaded, view, facts, run_id, session, store, opts).await
        } else {
            resume_cross_ir(loaded, view, facts, run_id, session, store, opts).await
        }
    }

    /// The READ-ONLY alignment preview (08 ยง2.7): the resume path's
    /// classification verbatim โ€” same-IR trivial adoption or the flat
    /// cross-IR `align` โ€” but no session, no attestation, no writes, no
    /// commitment. The preview is not a promise: the world can drift
    /// between preview and resume; the resume-time preflight probes stay
    /// the final judge. A confirmation-gated alignment is a preview
    /// RESULT here (the report shows what the real resume would refuse),
    /// not an error.
    #[allow(clippy::too_many_arguments)]
    pub async fn align_preview(
        new_flow: &FlowIR,
        subflows: &BTreeMap<Hash, FlowIR>,
        run_id: &str,
        store: &Store,
        platform: Option<&str>,
        vision: Option<&dyn pointlock_vision::VisionVerifier>,
        forced: &[String],
        old_flow_ir: Option<&FlowIR>,
    ) -> Result<AlignmentReport, RunnerError> {
        let loaded = load(new_flow, subflows)?;
        let view = store.rebuild_checkpoint(run_id)?;
        let events = store.events(run_id)?;
        let facts = harvest(&events);

        // The same old-IR integrity check the real resume applies: a
        // mismatched old IR is a caller error, and rehearsing with it
        // would classify against the wrong sub-domains.
        if let Some(old) = old_flow_ir {
            let computed = ir_hash(old);
            if computed != view.ir_hash {
                return Err(RunnerError::OldIrMismatch {
                    expected: view.ir_hash.clone(),
                    computed,
                });
            }
        }

        // The preview mirrors resume's typed refusals โ€” a clean rehearsal
        // of a resume the runner would categorically refuse is a lie.
        if let Some(live_hook) = facts
            .live_frames
            .iter()
            .find(|path| path.iter().any(|f| matches!(f, PathFrame::Hook { .. })))
        {
            return Err(RunnerError::M0Unsupported {
                detail: format!(
                    "resume across a live handler-repair frame ({}) is not in the M2 subset โ€” \
                     the repair flow suspended mid-flight; hook-aware frame re-entry is \
                     registered for the repair wave",
                    pointlock_ir::render_run_path(live_hook)
                ),
            });
        }

        if view.ir_hash == new_flow.ir_hash {
            return Ok(same_ir_report(new_flow, &view));
        }

        if !is_alignable_path(&view.frontier.run_path) {
            return Err(RunnerError::M0Unsupported {
                detail: "the run's frontier sits inside a handler frame".to_owned(),
            });
        }
        // Mirrors resume_cross_ir's third hook state (an escalate human
        // still awaiting an answer): rehearsing a resume the runner would
        // categorically refuse is a lie.
        if view
            .human_pending
            .as_ref()
            .is_some_and(|pending| !is_alignable_path(&pending.run_path))
        {
            return Err(RunnerError::M0Unsupported {
                detail: "a handler escalation is still awaiting an answer".to_owned(),
            });
        }

        // `env.platform` comes from the caller (the serve endpoint reads
        // it from the SAME lockfile the resume assembly uses); when
        // absent, an expr predicate referencing it re-judges to unknown
        // in the preview (fail-closed) while the real resume would judge
        // it โ€” pass the platform to keep the rehearsal faithful.
        let seed = ScopeSeed::new(
            params_object(&view),
            &view.binding.device_id,
            platform,
            run_id,
        );
        match align(
            &loaded,
            &new_flow.body,
            &view,
            &facts,
            &seed,
            store,
            vision,
            // A preview shows what WOULD gate: it authorizes nothing. The
            // FORCED list and the old IR it does take โ€” the rehearsal must
            // classify exactly as the real resume will.
            &[],
            forced,
            old_flow_ir,
        )
        .await
        {
            Ok(alignment) => Ok(alignment.report),
            Err(RunnerError::RequiresConfirmation { report }) => Ok(*report),
            Err(other) => Err(other),
        }
    }
}

/// `env.*` bindings: `deviceId`, `runId`, and `platform` when known (the
/// platform comes from the assembly layer โ€” the SPI attestation does not
/// carry it). Run-constant, read-only pass-through across frames (07 ยง1.2).
fn env_bindings(device_id: &str, platform: Option<&str>, run_id: &str) -> Vec<(String, Value)> {
    let mut env = vec![
        ("deviceId".to_owned(), Value::String(device_id.to_owned())),
        ("runId".to_owned(), Value::String(run_id.to_owned())),
    ];
    if let Some(platform) = platform {
        env.push(("platform".to_owned(), Value::String(platform.to_owned())));
    }
    env
}

/// The same-IR alignment report: top-level instances with execution
/// history are trivially reusable (identical hashes by construction);
/// the rest re-execute as `new`. Shared by [`resume_same_ir`] and the
/// read-only [`Runner::align_preview`].
fn same_ir_report(new_flow: &FlowIR, view: &CheckpointView) -> AlignmentReport {
    let completed: BTreeMap<String, &pointlock_ir::StepRecord> = view
        .completed
        .iter()
        .map(|record| (instance_key(&record.run_path), record))
        .collect();
    let mut entries = Vec::new();
    for step in &new_flow.body {
        let mut path = root_path(new_flow);
        path.push(match step {
            pointlock_ir::StepIR::Call(call) => PathFrame::Call {
                step_id: Some(call.base.step_id.clone()),
                callee_flow_id: call.flow_ref.flow_id.clone(),
                callee_ir_hash: call.flow_ref.ir_hash.clone(),
            },
            other => PathFrame::Step {
                step_id: other.step_id().clone(),
            },
        });
        let key = instance_key(&path);
        let adopted = completed.get(&key).is_some_and(|record| is_history(record));
        entries.push(AlignmentEntry {
            run_path: path.clone(),
            step_id: step.step_id().clone(),
            class: if adopted {
                AlignmentClass::Reusable
            } else {
                AlignmentClass::New
            },
            reason: (!adopted).then(|| "no adoptable prior record".to_owned()),
        });
    }
    AlignmentReport {
        entries,
        resume_point: Some(view.frontier.run_path.clone()),
        requires_confirmation: Vec::new(),
    }
}

// โ”€โ”€โ”€ same-IR resume: frame-precise adoption (07 ยง4.6) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Resumes under the identical IR: every completed step instance is
/// adopted by its exact run path; open spans and live call frames are
/// re-entered without re-appending their events; the walk lands on the
/// frontier position inside any depth of nesting โ€” no frame restarts, no
/// snapshot re-evaluation.
async fn resume_same_ir(
    loaded: LoadedFlow<'_>,
    view: CheckpointView,
    facts: Harvest,
    run_id: &str,
    mut session: Box<dyn ProviderSession>,
    store: &mut Store,
    opts: ResumeOptions,
) -> Result<RunOutcome, RunnerError> {
    let new_flow = loaded.root;
    // The bind-time binding cursor (run-row meta, written once at
    // begin_run, never rewritten): the issuing credential of intents
    // dispatched before any resume (07 ยง4.5).
    let bind_cursor = store.run_meta(run_id)?.binding.event_cursor;
    // Adoption set: completed instances keyed by their instance path.
    let mut adoptable: BTreeMap<String, Adopted> = BTreeMap::new();
    for record in &view.completed {
        let key = instance_key(&record.run_path);
        adoptable.insert(
            key.clone(),
            Adopted {
                record: record.clone(),
                before_id: facts.before_observation.get(&key).cloned(),
                after_id: facts.after_observation.get(&key).cloned(),
            },
        );
    }
    let open_spans: BTreeMap<String, Value> = facts
        .open_spans
        .iter()
        .map(|path| {
            let key = instance_key(path);
            let inputs = facts
                .entered_inputs
                .get(&key)
                .cloned()
                .unwrap_or(Value::Null);
            (key, inputs)
        })
        .collect();
    // A live hook-launched repair frame (a suspension *inside* a repair
    // subflow) needs hook-aware frame re-entry โ€” a typed M2 refusal, never
    // a guess (the repair's own records stay archived and honest).
    if let Some(live_hook) = facts
        .live_frames
        .iter()
        .find(|path| path.iter().any(|f| matches!(f, PathFrame::Hook { .. })))
    {
        return Err(RunnerError::M0Unsupported {
            detail: format!(
                "resume across a live handler-repair frame ({}) is not in the M2 subset โ€” \
                 the repair flow suspended mid-flight; hook-aware frame re-entry is \
                 registered for the repair wave",
                pointlock_ir::render_run_path(live_hook)
            ),
        });
    }
    let live_frames = live_frame_pins(&facts);

    // The alignment report of a same-IR resume (shared with the
    // read-only preview โ€” one classification truth source).
    let mut report = same_ir_report(new_flow, &view);

    // (B) unconditional reconcile of a pending intent (07 ยง4.1/ยง4.4). The
    // frontier step is where the walk will land (everything before it is
    // adopted), so `at_resume` holds by construction.
    let mut frontier_work = None;
    let mut deferred_settle = None;
    let mut pending_adjudication: Option<Box<Adjudication>> = None;
    let mut blocked = None;
    if let Some(intent) = &view.frontier.pending_intent {
        let frontier_key = instance_key(&view.frontier.run_path);
        let new_step = loaded.resolve_action(&view.frontier.run_path);
        // Same-IR: the archived entered hash matches the resolved step's
        // by construction; a missing carrier fails closed (dirty).
        let effect_dirty = match (new_step, facts.entered_effect_hash.get(&frontier_key)) {
            (Some(step), Some(archived)) => *archived != step.base.effect_hash,
            _ => true,
        };
        let decision = match reconcile_frontier(
            &mut session,
            new_step,
            true,
            effect_dirty,
            &view,
            &facts,
            &bind_cursor,
            &mut report,
            intent,
        )
        .await
        {
            Ok(decision) => decision,
            Err(error) => {
                let _ = session.end(SessionOutcome::Shutdown, None).await;
                return Err(error);
            }
        };
        match decision {
            FrontierDecision::Work(work) => frontier_work = Some((frontier_key, work)),
            FrontierDecision::DeferredSettle(settle) => deferred_settle = Some(settle),
            FrontierDecision::Adjudicate(adjudication) => pending_adjudication = Some(adjudication),
            FrontierDecision::Blocked(reason) => blocked = Some(reason),
            FrontierDecision::Nothing => {}
        }
    }

    // The segment header: runResumed carries the alignment report, this
    // segment's supervisePolicy (explicitly null when unsupervised โ€”
    // R13), and the new generation's reseeded cursor (07 ยง4.5: taken
    // after the reconcile decisions, before this append; absent when the
    // RPC fails โ€” honest, never stale).
    let resumed_cursor = session.current_cursor().await.ok();
    store.append_event(
        run_id,
        now_ms(),
        &root_path(new_flow),
        &RunLogPayload::RunResumed {
            alignment_report: report.clone(),
            supervise_policy: opts.supervise,
            event_cursor: resumed_cursor,
        },
    )?;

    // A reconciled completed terminal that cannot be adopted at the
    // resume point is still recorded โ€” the ledger closes the intent and
    // keeps the world fact as evidence (07 ยง4.1).
    if let Some((path, call_id, outcome)) = deferred_settle {
        store.append_event(
            run_id,
            now_ms(),
            &path,
            &RunLogPayload::ActionSettled {
                call_id,
                outcome: crate::engine::quarantine_unpersistable(*outcome),
            },
        )?;
    }

    if let Some(adjudication) = pending_adjudication {
        // Phase 1 of the 07 ยง4.4 default escalation: the request (fresh or
        // re-awaited) is the segment's outcome โ€” the run suspends
        // `awaitingHuman` and the answer arrives through the ordinary
        // arbitration channel, durable for the next resume to consume.
        let Adjudication {
            run_path,
            request,
            pending,
        } = *adjudication;
        if let Some((request_id, prompt, presents)) = request {
            store.append_event(
                run_id,
                now_ms(),
                &run_path,
                &RunLogPayload::HumanRequested {
                    request_id,
                    purpose: pointlock_ir::HumanPurpose::Step,
                    mode: Some(pointlock_ir::vocab::HumanMode::RepairWorld),
                    prompt,
                    presents,
                    decisions: Some(vec![
                        "adopt".to_owned(),
                        "redo".to_owned(),
                        "abort".to_owned(),
                    ]),
                    output_schema: None,
                    deadline_at_ms: None,
                },
            )?;
        }
        let summary = crate::engine::capture_provider_state_summary(
            session.as_ref(),
            &view.binding.session_lineage,
            &view.binding.device_id,
            opts.platform.as_deref(),
        )
        .await;
        store.append_event(
            run_id,
            now_ms(),
            &root_path(new_flow),
            &RunLogPayload::RunSuspended {
                provider_state_summary: Some(summary),
                reason: Some(format!(
                    "awaiting human adjudication (requestId {})",
                    pending.request_id
                )),
            },
        )?;
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Ok(RunOutcome::AwaitingHuman { pending });
    }

    if let Some(reason) = blocked {
        // Suspension-instant profile (07 ยง2.2): the session is still
        // live at this pre-Execution blocked refusal.
        let summary = crate::engine::capture_provider_state_summary(
            session.as_ref(),
            &view.binding.session_lineage,
            &view.binding.device_id,
            opts.platform.as_deref(),
        )
        .await;
        store.append_event(
            run_id,
            now_ms(),
            &root_path(new_flow),
            &RunLogPayload::RunSuspended {
                provider_state_summary: Some(summary),
                reason: Some(reason.to_string()),
            },
        )?;
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Ok(RunOutcome::Blocked { reason });
    }

    let params = params_object(&view);
    let env = env_bindings(&view.binding.device_id, opts.platform.as_deref(), run_id);
    let exec = Execution {
        flows: &loaded,
        session,
        store,
        run_id: run_id.to_owned(),
        stop: opts.stop,
        env,
        attempt_base: facts.max_attempt.clone(),
        open_spans,
        live_frames,
        resumed: true,
        authorized: opts.allow_mutating_reexec.iter().cloned().collect(),
        reentry_seen: false,
        adoptable,
        frontier: frontier_work,
        session_lineage: view.binding.session_lineage.clone(),
        pending_summaries: BTreeMap::new(),
        vision: opts.vision.clone(),
        supervise: opts.supervise,
        human: facts.human_requests.clone(),
        settled: facts.settled.clone(),
        recorded_verdicts: facts.recorded_verdicts.clone(),
        hook_triggers: facts.hook_triggers.clone(),
        clock: opts.clock,
    };
    let root = FrameState::new(new_flow, root_path(new_flow), params, 1);
    exec.run(root, 0).await
}

// โ”€โ”€โ”€ cross-IR resume: the flat alignment subset (07 ยง5.2) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Resumes under a repaired IR. M2 subset: the old records and the new
/// body must both be flat top-level action steps; anything nested is a
/// typed refusal (the 07 ยง5.2 nested alignment rules land with the repair
/// wave).
async fn resume_cross_ir(
    loaded: LoadedFlow<'_>,
    view: CheckpointView,
    facts: Harvest,
    run_id: &str,
    mut session: Box<dyn ProviderSession>,
    store: &mut Store,
    opts: ResumeOptions,
) -> Result<RunOutcome, RunnerError> {
    let new_flow = loaded.root;
    // Bind-time credential, as in resume_same_ir (07 ยง4.5).
    let bind_cursor = store.run_meta(run_id)?.binding.event_cursor;
    // The FRONTIER may not sit inside a handler frame: resolving it means
    // walking a path `resolve_step` refuses by construction, and the
    // reconcile below would then have no step to reconcile against.
    // Completed hook-framed records are a different matter โ€” see
    // [`is_alignable_path`].
    if !is_alignable_path(&view.frontier.run_path) {
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Err(RunnerError::M0Unsupported {
            detail: format!(
                "the run's frontier sits inside a handler frame ({}); hook-aware frame \
                 re-entry is registered for the repair wave",
                pointlock_ir::render_run_path(&view.frontier.run_path)
            ),
        });
    }

    // Unfinished handler work in ANY of its three shapes is a categorical
    // refusal, and it is settled BEFORE alignment runs โ€” `align` can return
    // `RequiresConfirmation`, and letting that mask a resume the runner
    // would refuse outright would tell the operator to authorize step ids
    // for something that can never proceed. It is also the order
    // `align_preview` uses, and the preview promises to mirror resume's
    // typed refusals.
    //
    // (i) a repair subflow suspended mid-flight โ€” its call frame is still
    // open.
    if let Some(live_hook) = facts
        .live_frames
        .iter()
        .find(|path| path.iter().any(|f| matches!(f, PathFrame::Hook { .. })))
    {
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Err(RunnerError::M0Unsupported {
            detail: format!(
                "resume across a live handler-repair frame ({}) is not in the M2 subset โ€” \
                 the repair flow suspended mid-flight; hook-aware frame re-entry is \
                 registered for the repair wave",
                pointlock_ir::render_run_path(live_hook)
            ),
        });
    }
    // (ii) an escalate hook human still awaiting an answer. It leaves NO
    // other trace: it opens no span and pushes no frame (ใ€Œhook humans are
    // not body stepsใ€), so `live_frames`, `frontier` and `completed` are
    // all blind to it โ€” `humanPending` is the only carrier. Cross-IR it is
    // genuinely unsafe: the continuation is looked up by an instance key
    // rebuilt from the NEW host path, so renaming the host mints a SECOND
    // request and strands the first unanswerable, and deleting the host
    // strands it forever. Same-IR rebuilds the same key and settles
    // correctly, which is why this refusal lives here and not there.
    if let Some(pending) = view
        .human_pending
        .as_ref()
        .filter(|pending| !is_alignable_path(&pending.run_path))
    {
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Err(RunnerError::M0Unsupported {
            detail: format!(
                "a handler escalation is still awaiting an answer ({}); resuming it under a \
                 repaired IR needs hook-aware re-entry, which is registered for the repair \
                 wave โ€” answer or let it time out first",
                pointlock_ir::render_run_path(&pending.run_path)
            ),
        });
    }

    let seed = ScopeSeed::new(
        params_object(&view),
        &view.binding.device_id,
        opts.platform.as_deref(),
        run_id,
    );

    // (A) alignment first (07 ยง4.1). Classification runs on the archived
    // per-step hashes the fold harvested from `stepEntered` (spine ยง6.1
    // M1 note) โ€” the old FlowIR is not required.
    let mut alignment = match align(
        &loaded,
        &new_flow.body,
        &view,
        &facts,
        &seed,
        store,
        opts.vision.as_deref(),
        &opts.allow_mutating_reexec,
        &opts.force_reexecute,
        opts.old_flow_ir.as_ref(),
    )
    .await
    {
        Ok(alignment) => alignment,
        Err(error) => {
            // A pre-header refusal must not leak the opened session
            // (best-effort teardown, 04 ยง2.1).
            let _ = session.end(SessionOutcome::Shutdown, None).await;
            return Err(error);
        }
    };

    // (B) unconditional reconcile of a pending intent (07 ยง4.1/ยง4.4).
    let mut frontier_work = None;
    let mut deferred_settle = None;
    let mut pending_adjudication: Option<Box<Adjudication>> = None;
    let mut blocked = None;
    if let Some(intent) = &view.frontier.pending_intent {
        let frontier_key = instance_key(&view.frontier.run_path);
        // Resolved by PATH, not by a flat id scan: the frontier can sit
        // inside a branch, and same-IR resume already resolves it this way.
        let new_step = loaded.resolve_action(&view.frontier.run_path);
        // The frontier IS the resume point when its instance is the one
        // alignment named. Instance keys, not body indices: the comparison
        // has to keep working once the resume point can sit inside a
        // callee or an iteration.
        let at_resume = alignment.resume_key.as_deref() == Some(frontier_key.as_str());
        // ยง4.1 cross semantics: an effect-dirty frontier step's old result
        // is never adopted โ€” it is the product of the old binding. A
        // missing hash or a frontier step absent from the new IR fails
        // closed (dirty).
        let effect_dirty = match (new_step, facts.entered_effect_hash.get(&frontier_key)) {
            (Some(step), Some(archived)) => *archived != step.base.effect_hash,
            _ => true,
        };
        let decision = match reconcile_frontier(
            &mut session,
            new_step,
            at_resume,
            effect_dirty,
            &view,
            &facts,
            &bind_cursor,
            &mut alignment.report,
            intent,
        )
        .await
        {
            Ok(decision) => decision,
            Err(error) => {
                let _ = session.end(SessionOutcome::Shutdown, None).await;
                return Err(error);
            }
        };
        match decision {
            FrontierDecision::Work(work) => frontier_work = Some((frontier_key, work)),
            FrontierDecision::DeferredSettle(settle) => deferred_settle = Some(settle),
            FrontierDecision::Adjudicate(adjudication) => pending_adjudication = Some(adjudication),
            FrontierDecision::Blocked(reason) => blocked = Some(reason),
            FrontierDecision::Nothing => {}
        }
    }

    // The segment header (see the same-IR site for the cursor semantics).
    let resumed_cursor = session.current_cursor().await.ok();
    store.append_event(
        run_id,
        now_ms(),
        &root_path(new_flow),
        &RunLogPayload::RunResumed {
            alignment_report: alignment.report.clone(),
            supervise_policy: opts.supervise,
            event_cursor: resumed_cursor,
        },
    )?;

    // Offline re-judgements: new verdicts with `supersedes` lineage,
    // anchored at the old records' run paths (the fold re-projects the
    // completed records); written back via the *current* session
    // (07 ยง5.3 โ€” cross-session write-back is sound, the daemon only
    // persists).
    let rejudged = std::mem::take(&mut alignment.rejudged);
    for rejudge in rejudged {
        // Remote archival first so its outcome rides the event; a
        // failure is annotation material, never a resume error (04 ยง5 โ€”
        // the RunLog is the sole truth). Wire caps applied here like on
        // every other write-back: compaction is the runner's job (04 ยง5).
        let remote_archival_error = session
            .record_verdict(pointlock_provider_kit::VerdictWrite {
                status: rejudge.verdict.status,
                summary: crate::engine::cap_wire_summary(&rejudge.verdict),
                evidence: rejudge
                    .verdict
                    .evidence
                    .iter()
                    .take(pointlock_provider_kit::VERDICT_EVIDENCE_MAX_ENTRIES)
                    .cloned()
                    .collect(),
            })
            .await
            .err()
            .map(|error| format!("remote archival failed: {error}"));
        store.append_event(
            run_id,
            now_ms(),
            &rejudge.run_path,
            &RunLogPayload::VerdictRecorded {
                verdict: rejudge.verdict.clone(),
                localized: Vec::new(),
                localization_gaps: Vec::new(),
                remote_archival_error,
            },
        )?;
    }

    // A reconciled completed terminal that cannot be adopted at the
    // resume point is still recorded โ€” the ledger closes the intent
    // and keeps the world fact as evidence (07 ยง4.1).
    if let Some((path, call_id, outcome)) = deferred_settle {
        store.append_event(
            run_id,
            now_ms(),
            &path,
            &RunLogPayload::ActionSettled {
                call_id,
                outcome: crate::engine::quarantine_unpersistable(*outcome),
            },
        )?;
    }

    if let Some(adjudication) = pending_adjudication {
        // Phase 1 of the 07 ยง4.4 default escalation: the request (fresh or
        // re-awaited) is the segment's outcome โ€” the run suspends
        // `awaitingHuman` and the answer arrives through the ordinary
        // arbitration channel, durable for the next resume to consume.
        let Adjudication {
            run_path,
            request,
            pending,
        } = *adjudication;
        if let Some((request_id, prompt, presents)) = request {
            store.append_event(
                run_id,
                now_ms(),
                &run_path,
                &RunLogPayload::HumanRequested {
                    request_id,
                    purpose: pointlock_ir::HumanPurpose::Step,
                    mode: Some(pointlock_ir::vocab::HumanMode::RepairWorld),
                    prompt,
                    presents,
                    decisions: Some(vec![
                        "adopt".to_owned(),
                        "redo".to_owned(),
                        "abort".to_owned(),
                    ]),
                    output_schema: None,
                    deadline_at_ms: None,
                },
            )?;
        }
        let summary = crate::engine::capture_provider_state_summary(
            session.as_ref(),
            &view.binding.session_lineage,
            &view.binding.device_id,
            opts.platform.as_deref(),
        )
        .await;
        store.append_event(
            run_id,
            now_ms(),
            &root_path(new_flow),
            &RunLogPayload::RunSuspended {
                provider_state_summary: Some(summary),
                reason: Some(format!(
                    "awaiting human adjudication (requestId {})",
                    pending.request_id
                )),
            },
        )?;
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Ok(RunOutcome::AwaitingHuman { pending });
    }

    if let Some(reason) = blocked {
        // Suspension-instant profile (07 ยง2.2): the session is still
        // live at this pre-Execution blocked refusal.
        let summary = crate::engine::capture_provider_state_summary(
            session.as_ref(),
            &view.binding.session_lineage,
            &view.binding.device_id,
            opts.platform.as_deref(),
        )
        .await;
        store.append_event(
            run_id,
            now_ms(),
            &root_path(new_flow),
            &RunLogPayload::RunSuspended {
                provider_state_summary: Some(summary),
                reason: Some(reason.to_string()),
            },
        )?;
        let _ = session.end(SessionOutcome::Shutdown, None).await;
        return Ok(RunOutcome::Blocked { reason });
    }

    let Alignment {
        adoptable,
        teardown,
        ..
    } = alignment;
    // 07 ยง5.2 case (b): dismantle the stale frame ON THE LEDGER before
    // execution starts, mirroring `exec_call`'s abort unwind exactly โ€”
    // close the open spans innermost-first (the fold's exit pairing is
    // LIFO), pop each live frame right before its own call span closes,
    // and let the call step exit `aborted` (an aborted execution makes no
    // semantic claim; nothing here is adoptable history). Emitted AFTER
    // the deferred settle above, so a terminal the reconcile closed lands
    // on the still-open frontier span and is archived with it.
    //
    // The suspension chain is one nested sequence, so "the torn-down
    // subtree" is precisely the open spans at or under the call's own key.
    let torn = |key: &str| -> bool {
        teardown
            .as_deref()
            .is_some_and(|call| key == call || crate::align::is_instance_descendant(call, key))
    };
    if teardown.is_some() {
        let live_keys: BTreeSet<String> = facts
            .live_frames
            .iter()
            .map(|path| instance_key(path))
            .collect();
        for span in facts.open_spans.iter().rev() {
            let key = instance_key(span);
            if !torn(&key) {
                continue;
            }
            if live_keys.contains(&key) {
                // The span belongs to a call step whose frame is open: the
                // frame pops first, the span closes second โ€” the exact
                // unwind order of a live abort.
                store.append_event(
                    run_id,
                    now_ms(),
                    span,
                    &RunLogPayload::CallFramePopped { outputs: None },
                )?;
            }
            store.append_event(
                run_id,
                now_ms(),
                span,
                &RunLogPayload::StepExited {
                    provider_state_summary: None,
                    state: pointlock_ir::StepState::Aborted,
                    output: None,
                    localized: Vec::new(),
                    localization_gaps: Vec::new(),
                },
            )?;
        }
    }
    let open_spans: BTreeMap<String, Value> = facts
        .open_spans
        .iter()
        .filter(|path| !torn(&instance_key(path)))
        .map(|path| {
            let key = instance_key(path);
            let inputs = facts
                .entered_inputs
                .get(&key)
                .cloned()
                .unwrap_or(Value::Null);
            (key, inputs)
        })
        .collect();
    // The torn-down frame is gone from the ledger; handing its pin to the
    // engine would make `exec_call` skip the push for a frame that no
    // longer exists.
    let live_frames: BTreeMap<String, pointlock_ir::Hash> = live_frame_pins(&facts)
        .into_iter()
        .filter(|(key, _)| !torn(key))
        .collect();
    let params = params_object(&view);
    let env = env_bindings(&view.binding.device_id, opts.platform.as_deref(), run_id);
    let exec = Execution {
        flows: &loaded,
        session,
        store,
        run_id: run_id.to_owned(),
        stop: opts.stop,
        env,
        attempt_base: facts.max_attempt.clone(),
        open_spans,
        // Live call frames must not be pushed again on resume (07 ยง4.6);
        // the pin lets `exec_call` tell a plain re-entry from one that has
        // to rebase the frame onto a repaired callee (07 ยง5.2 case (a)).
        // The torn-down frame (case (b)) is filtered out above.
        live_frames,
        resumed: true,
        authorized: opts.allow_mutating_reexec.iter().cloned().collect(),
        reentry_seen: false,
        adoptable,
        frontier: frontier_work,
        session_lineage: view.binding.session_lineage.clone(),
        pending_summaries: BTreeMap::new(),
        vision: opts.vision.clone(),
        supervise: opts.supervise,
        human: facts.human_requests.clone(),
        settled: facts.settled.clone(),
        recorded_verdicts: facts.recorded_verdicts.clone(),
        hook_triggers: facts.hook_triggers.clone(),
        clock: opts.clock,
    };
    // Execution restarts at the top of the body; the adoption set does the
    // skipping, seeding each adopted step's output/verdict into its OWN
    // frame as it is reached. That is the same mechanism same-IR resume
    // uses, and the only one that can express a resume point at depth.
    let root = FrameState::new(new_flow, root_path(new_flow), params, 1);
    exec.run(root, 0).await
}

/// Whether cross-IR alignment can ADDRESS this path.
///
/// Exactly a hook guard, and says so rather than re-listing the seven
/// frames it accepts: the walker descends `if` branch bodies, `foreach`
/// rounds, and โ€” under the case (a) down-drill โ€” callee bodies, addressing
/// every one of them by instance key, so `flow`/`step`/`call`/`iteration`
/// (and the attempt/phase/assertion suffixes) are all classifiable. `hook`
/// is the one frame shape nothing addresses.
///
/// Applied to the FRONTIER only. Completed hook-framed records are not
/// refused โ€” 07 ยง5.2's last bullet rules ใ€Œhook ๅธงไธ‹็š„่ฎฐๅฝ•๏ผˆhandler ๅฎก่ฎก
/// ็—•๏ผ‰ไธๅ‚ไธŽๅฏน้ฝๅค็”จโ€ฆโ€ฆๆ—ง hook ่ฎฐๅฝ•ไธ€ๅพ‹ๅฝ’ๆกฃใ€: archive them, do not refuse
/// the resume. Refusing cost a real case โ€” a run whose `onFail` repair
/// subflow completed could never be repaired cross-IR afterwards โ€” and
/// archival is already structural rather than a promise:
/// - they are never ADOPTED: adoption is keyed by instance, and node keys
///   come from `child_frame`, which emits only `step`/`call`/`iteration`
///   segments. `instance_key` renders a hook frame as `/hook:<Hook>:<n>`,
///   which no `StepId` can spell, so no node key can ever collide;
/// - they are never ORPHAN-reported: the only hook-framed `StepRecord`s
///   come from a repair subflow's body, whose path always carries the
///   handler-launched `call` frame, and the orphan pass skips records
///   under a call frame the walk did not descend into. An escalate human
///   writes `humanRequested` and no step span at all, so it contributes
///   no record to misreport.
///
/// A LIVE hook frame is still refused, separately and before this: a
/// repair subflow suspended mid-flight needs hook-aware frame re-entry,
/// which is the repair wave's.
fn is_alignable_path(path: &RunPath) -> bool {
    !path
        .iter()
        .any(|frame| matches!(frame, PathFrame::Hook { .. }))
}

/// A pending human adjudication of an uncertain reconcile (07 ยง4.4): the
/// run suspends `awaitingHuman` on a synthesized `repairWorld` request
/// whose vocabulary is `adopt | redo | abort` (00 ยง6.7-B). Paired to its
/// intent BY CALL ID (carried in `presents`), so an answer ruled for one
/// dispatch can never be replayed onto a later one.
struct Adjudication {
    /// The hook-framed anchor (`<frontier>/hook:OnResumeDrift:1/adjudicate`).
    run_path: RunPath,
    /// A fresh request to append โ€” `(requestId, prompt, presents)`; `None`
    /// when an unanswered request for this callId is already on the ledger
    /// and the segment simply re-awaits it.
    request: Option<(String, String, Value)>,
    /// What the segment reports as the pending interaction.
    pending: pointlock_ir::HumanPending,
}

/// What the frontier reconcile decided.
enum FrontierDecision {
    /// Mid-flight work for the resume step.
    Work(FrontierWork),
    /// Close the intent in the ledger with the archived terminal; the step
    /// re-executes fresh.
    DeferredSettle(
        (
            pointlock_ir::RunPath,
            String,
            Box<pointlock_ir::ActionOutcome>,
        ),
    ),
    /// Human adjudication required: suspend `awaitingHuman` on the
    /// adjudication request (fresh or re-awaited).
    Adjudicate(Box<Adjudication>),
    /// Human adjudication impossible to even request (defense line).
    Blocked(BlockedReason),
    /// Nothing to carry over (e.g. neverDispatched off the resume point).
    Nothing,
}

/// Applies the 07 ยง4.4 decision table to a pending intent. `new_step` is
/// the frontier step as resolved in the new IR (nested paths supported);
/// `at_resume` states whether execution will land exactly on it;
/// `effect_dirty` is the ยง4.1 cross-semantics discriminator.
#[allow(clippy::too_many_arguments)]
async fn reconcile_frontier(
    session: &mut Box<dyn ProviderSession>,
    new_step: Option<&ActionStepIR>,
    at_resume: bool,
    effect_dirty: bool,
    view: &CheckpointView,
    facts: &Harvest,
    bind_cursor: &pointlock_ir::EventCursor,
    report: &mut AlignmentReport,
    intent: &pointlock_ir::PendingIntent,
) -> Result<FrontierDecision, RunnerError> {
    // The issuing credential (07 ยง4.5): per-intent exact state from the
    // ledger scan. `FromBinding` (no resume preceded the intent) resolves
    // to the BIND-TIME cursor โ€” the run-row binding written once at
    // begin_run โ€” NOT the folded view's cursor, which every
    // cursor-bearing resume reseeds to the newest generation (a
    // generation that never issued this intent). A missing harvest entry
    // means the ledger cannot attest the issuing generation at all:
    // fail-closed to Unknown, never a fabricated credential. Unknown is
    // answered with the uncertain branch WITHOUT an RPC.
    let issuing = facts
        .intent_issuing
        .get(&intent.call_id)
        .cloned()
        .unwrap_or(crate::align::IssuingCursor::Unknown);
    let fate = match &issuing {
        crate::align::IssuingCursor::FromBinding => {
            session.reconcile(&intent.call_id, bind_cursor).await?
        }
        crate::align::IssuingCursor::Known(cursor) => {
            session.reconcile(&intent.call_id, cursor).await?
        }
        crate::align::IssuingCursor::Unknown => ReconcileResult::LogUnavailable {
            reason: "the issuing session is unknowable (a resume predating the \
                     eventCursor carrier intervened); refusing to reconcile with \
                     a fabricated credential"
                .to_owned(),
        },
    };
    let intent_path = facts
        .intent_path
        .get(&intent.call_id)
        .cloned()
        .unwrap_or_else(|| view.frontier.run_path.clone());
    let mutating_gated = new_step.map(gated_mutating).unwrap_or(true);

    match fate {
        ReconcileResult::Completed { outcome } => {
            if !effect_dirty && at_resume {
                // The archived terminal โ€” whatever its four-way
                // discriminant โ€” is adopted and disposed through the same
                // settled-outcome path as a live execute (ยง6.7-B).
                return Ok(FrontierDecision::Work(FrontierWork::Adopt {
                    call_id: intent.call_id.clone(),
                    intent_path,
                    outcome,
                    args: intent.args_snapshot.clone(),
                    chain_index: facts.intent_chain_index.get(&intent.call_id).copied(),
                }));
            }
            // Not adoptable at the resume point. Whether re-execution
            // risks a second effect follows the 07 ยง5.4 criterion: only a
            // succeeded or timedOut terminal can have mutated the world;
            // an archived failed/cancelled left no effect to double.
            let effect_possible = matches!(
                outcome.as_ref(),
                pointlock_ir::ActionOutcome::Succeeded { .. }
                    | pointlock_ir::ActionOutcome::TimedOut { .. }
            );
            if effect_possible && mutating_gated {
                // The old action (possibly) took effect but its terminal
                // cannot be adopted (effect-dirty or positionally
                // invalidated): re-execution is a second effect โ€”
                // 07 ยง5.4 `frontierUnknown`, fail-closed.
                report.requires_confirmation.push(RequiresConfirmation {
                    run_path: view.frontier.run_path.clone(),
                    step_id: new_step.map(|step| step.base.step_id.clone()),
                    cause: "frontierUnknown".to_owned(),
                    reason: format!(
                        "callId {} reached a recorded {} terminal on the device but \
                         it is not adoptable; re-execution of the mutating step \
                         needs explicit authorization",
                        intent.call_id,
                        outcome.kind()
                    ),
                });
                return Err(RunnerError::RequiresConfirmation {
                    report: Box::new(report.clone()),
                });
            }
            Ok(FrontierDecision::DeferredSettle((
                intent_path,
                intent.call_id.clone(),
                outcome,
            )))
        }
        ReconcileResult::NeverDispatched => {
            if !effect_dirty && at_resume {
                // Safe replay: archived args, new callId, new WAL intent.
                Ok(FrontierDecision::Work(FrontierWork::Replay {
                    chain_index: facts.intent_chain_index.get(&intent.call_id).copied(),
                    args: intent.args_snapshot.clone(),
                }))
            } else {
                // The step re-executes fresh from ready (nothing happened
                // in the world).
                Ok(FrontierDecision::Nothing)
            }
        }
        ReconcileResult::StartedNoTerminal => uncertain_branch(
            new_step,
            intent,
            &view.frontier.run_path,
            facts,
            report,
            at_resume,
            effect_dirty,
            "startedNoTerminal",
            facts.intent_chain_index.get(&intent.call_id).copied(),
        ),
        ReconcileResult::LogUnavailable { reason } => uncertain_branch(
            new_step,
            intent,
            &view.frontier.run_path,
            facts,
            report,
            at_resume,
            effect_dirty,
            &format!("logUnavailable: {reason}"),
            facts.intent_chain_index.get(&intent.call_id).copied(),
        ),
    }
}

/// The uncertain reconcile branch (07 ยง4.4): replay only with the explicit
/// author permission (`idempotent` / `readonly`); otherwise the DEFAULT
/// `onResumeDrift` escalation โ€” a synthesized `repairWorld` human rules
/// `adopt | redo | abort` over the presented callId (00 ยง6.7-B). The
/// request and its answer live on the ordinary human ledger
/// (`humanRequested`/`humanResponded`), so the operator answers through
/// the same channels as any other wait and the ruling is durable: a crash
/// after the answer re-derives the same disposition.
///
/// A DECLARED `onResumeDrift` binding keeps serving the probe-drift ladder
/// it was written for; routing the reconcile adjudication through custom
/// bindings is registered for the repair wave.
#[allow(clippy::too_many_arguments)]
fn uncertain_branch(
    new_step: Option<&ActionStepIR>,
    intent: &pointlock_ir::PendingIntent,
    frontier_path: &RunPath,
    facts: &Harvest,
    report: &mut AlignmentReport,
    at_resume: bool,
    effect_dirty: bool,
    fate: &str,
    chain_index: Option<u32>,
) -> Result<FrontierDecision, RunnerError> {
    let permitted = new_step.map(replay_permitted).unwrap_or(false);
    if permitted {
        if at_resume && !effect_dirty {
            return Ok(FrontierDecision::Work(FrontierWork::Replay {
                chain_index,
                args: intent.args_snapshot.clone(),
            }));
        }
        // Fresh re-execution is equally safe for readonly/idempotent.
        return Ok(FrontierDecision::Nothing);
    }

    // The adjudication anchor: one hook-framed instance under the frontier
    // step. The leaf id is fixed โ€” identity per INTENT comes from the
    // callId carried in `presents`, checked below, so an answer ruled for
    // an earlier dispatch is never replayed onto this one.
    let mut hook_path = frontier_path.clone();
    hook_path.push(PathFrame::Hook {
        hook: pointlock_ir::HandlerHook::OnResumeDrift,
        trigger: 1,
    });
    hook_path.push(PathFrame::Step {
        step_id: "adjudicate".try_into().expect("a fixed valid step id"),
    });
    let key = instance_key(&hook_path);

    if let Some(fact) = facts.human_requests.get(&key)
        && fact.presents.get("callId").and_then(Value::as_str) == Some(intent.call_id.as_str())
    {
        match &fact.final_response {
            None => {
                // Asked and unanswered: re-await the same request, no
                // duplicate append.
                return Ok(FrontierDecision::Adjudicate(Box::new(Adjudication {
                    run_path: hook_path.clone(),
                    request: None,
                    pending: pending_of(fact, &hook_path),
                })));
            }
            Some(response) => {
                let ruling = response.get("decision").and_then(Value::as_str);
                match ruling {
                    Some("adopt") => {
                        if at_resume && !effect_dirty {
                            // The ruled effect stands; the step's own
                            // assertions verify it over a fresh
                            // observation ([`FrontierWork::ConfirmEffect`]).
                            return Ok(FrontierDecision::Work(FrontierWork::ConfirmEffect {
                                message: format!(
                                    "uncertain fate ({fate}) of callId {} adjudicated \
                                     `adopt`",
                                    intent.call_id
                                ),
                                args: intent.args_snapshot.clone(),
                            }));
                        }
                        // Adopted effect on a step that must nonetheless
                        // re-execute (effect-dirty / positionally
                        // invalidated): a second effect โ€” the 07 ยง5.4
                        // frontierUnknown gate, same as an unadoptable
                        // recorded terminal.
                        report.requires_confirmation.push(RequiresConfirmation {
                            run_path: frontier_path.clone(),
                            step_id: new_step.map(|step| step.base.step_id.clone()),
                            cause: "frontierUnknown".to_owned(),
                            reason: format!(
                                "callId {} was adjudicated `adopt` (the effect stands) but \
                                 the step is not adoptable here; re-execution of the \
                                 mutating step needs explicit authorization",
                                intent.call_id
                            ),
                        });
                        return Err(RunnerError::RequiresConfirmation {
                            report: Box::new(report.clone()),
                        });
                    }
                    Some("redo") => {
                        // I2 source (iv): the human's redo IS the license.
                        if at_resume && !effect_dirty {
                            return Ok(FrontierDecision::Work(FrontierWork::Replay {
                                chain_index,
                                args: intent.args_snapshot.clone(),
                            }));
                        }
                        return Ok(FrontierDecision::Nothing);
                    }
                    Some("abort") => {
                        return Ok(FrontierDecision::Work(FrontierWork::AbortRuled {
                            args: intent.args_snapshot.clone(),
                        }));
                    }
                    other => {
                        // The store arbitrates against the declared
                        // vocabulary, so this is a ledger anomaly โ€” the
                        // defense line blocks rather than guesses.
                        return Ok(FrontierDecision::Blocked(BlockedReason::RequiresHuman {
                            call_id: intent.call_id.clone(),
                            detail: format!(
                                "adjudication response carries an unusable decision \
                                 {other:?}; refusing to guess"
                            ),
                        }));
                    }
                }
            }
        }
    }

    // No adjudication asked yet (or the one on the ledger belongs to an
    // earlier dispatch): mint the request.
    let request_id = uuid::Uuid::new_v4().to_string();
    let prompt = format!(
        "the fate of callId {} is uncertain ({fate}) and the step is mutating and \
         not idempotent โ€” automatic replay is forbidden (I2). Inspect the device, \
         then rule: `adopt` (the effect happened; verify and continue), `redo` \
         (the effect did not happen or you undid it; dispatch again), or `abort` \
         (stop the run)",
        intent.call_id
    );
    let presents = serde_json::json!({
        "callId": intent.call_id,
        "fate": fate,
        "argsSnapshot": intent.args_snapshot,
    });
    let pending = pointlock_ir::HumanPending {
        run_path: hook_path.clone(),
        request_id: request_id.clone(),
        purpose: pointlock_ir::HumanPurpose::Step,
        mode: Some(pointlock_ir::vocab::HumanMode::RepairWorld),
        prompt: prompt.clone(),
        deadline_at_ms: None,
    };
    Ok(FrontierDecision::Adjudicate(Box::new(Adjudication {
        run_path: hook_path,
        request: Some((request_id, prompt, presents)),
        pending,
    })))
}

/// The pending descriptor of an already-asked adjudication.
fn pending_of(fact: &HumanRequestFact, hook_path: &RunPath) -> pointlock_ir::HumanPending {
    pointlock_ir::HumanPending {
        run_path: hook_path.clone(),
        request_id: fact.request_id.clone(),
        purpose: fact.purpose,
        mode: fact.mode,
        prompt: fact.prompt.clone(),
        deadline_at_ms: fact.deadline_at_ms,
    }
}

/// The params snapshot of a checkpoint as an object map (it was written by
/// `Runner::run` as an object; anything else folds to empty).
fn params_object(view: &CheckpointView) -> Map<String, Value> {
    match &view.params_snapshot {
        Value::Object(map) => map.clone(),
        _ => Map::new(),
    }
}