supercode-harness 0.4.17

The optional native Supercode agent and tool harness
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
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
//! §4 "Presets" (`docs/composable-harness/COMPOSABLE-HARNESS-DESIGN.md`) —
//! P2 of the composable-harness migration (design §5.2, phase **P2**).
//!
//! The six reserved built-in presets, compiled in as TOML consts, transcribed
//! faithfully from the design doc's §4.1-§4.5 TOML blocks (and §4's intro
//! paragraph for the sixth, `supercode-default`, which the doc defines by
//! prose rather than a TOML block — S10 fix: "`pi-core` MINUS `{trust,
//! session_tree, session_share, server, plugins}`", not "`pi-core` plus
//! extras").
//!
//! **Syntax fix (P2 judgment call).** The design doc's `[capabilities.X]
//! { enabled = true, ... }` lines combine a TOML table-HEADER and an
//! inline-table VALUE on one line, which is not valid TOML (verified
//! empirically against the `toml` crate: `invalid table header, expected
//! newline`). Naively rewriting them as dotted-key assignments
//! (`capabilities.X = { ... }`) is also unsafe wherever such a line appears
//! *after* an already-open `[capabilities.permissions]` table (cc-parity,
//! cx-parity, oc-parity all have this): a dotted key inside an open table is
//! relative to the CURRENT table, so `capabilities.permissions.sandbox = {..}`
//! written while inside `[capabilities.permissions]` nests as
//! `capabilities.permissions.capabilities.permissions.sandbox`, silently
//! corrupting the structure. The transcription below instead expands every
//! `[capabilities.X] { k = v, ... }` shorthand into the equivalent explicit
//! form — a real `[capabilities.X]` table header (always root-absolute,
//! never context-relative) followed by `k = v` lines — which is safe
//! regardless of surrounding context and preserves the exact same resolved
//! structure. Every block below is verified to parse into `HarnessConfig`
//! in this module's tests, and each preset's resolved shape is golden-tested
//! against §4.6's per-preset verdicts in
//! `crates/harness/tests/composable_presets.rs`.
//!
//! Comments from the design doc are preserved verbatim inside each TOML
//! block for traceability back to the source section.

/// `pi-core` — design §4.1.
pub const PI_CORE_TOML: &str = r#"# built-in preset: pi-core — the §1 core with pi's exact defaults, plus pi's four kept extras.
schema_version = 1

[core]
effort = "medium"                       # pi default thinking level (pi§3, src:core/defaults.ts:3)
max_tool_output_bytes = 51200           # pi's shared truncation policy: 50KB / 2000 lines (pi§1, truncate.ts)
project_context = true                  # AGENTS.md/CLAUDE.md global + ancestor walk (pi§2 "Context files")
env_context = true                      # pi appends Current date + cwd to the prompt (pi§2, system-prompt.ts:88-170)

[core.retry]                            # pi agent-level auto-retry (pi§3)
enabled = true
max_retries = 3
base_delay_ms = 2000

[core.tools]
enabled = ["read_file", "bash", "edit_file", "write_file"]  # pi's default-ACTIVE four (pi§1, src:core/sdk.ts:245)
schema_tier = "full"
[core.tools.read_file]
multimodal = true                       # pi read returns images as attachments (pi§1, read.ts)

[core.skills]
enabled = true                          # agentskills.io discovery + progressive disclosure (pi§2; D-7 met by read_file)
                                        # No `harness` here: `supercode-default` extends this preset and is DEFINED as
                                        # today's unfiltered default stack (§4 intro), which discovers no SKILL.md
                                        # package and offers no `skill` tool. Naming a root table here would change what
                                        # `supercode` with no config file at all does.

[core.compaction]
enabled = true
after_messages = 0                      # pi's trigger is token pressure, never message count (pi§2)
reserve_tokens = 16384                  # compaction.reserveTokens default (pi§2, pi§6)
keep_recent_tokens = 20000              # compaction.keepRecentTokens default (pi§2)
summarize = true                        # structured Goal/Constraints/Progress/… summary (pi§2, compaction.md)

[core.steering]
steering_mode = "one-at-a-time"         # pi delivery-mode defaults (pi§3 "Message queue", pi§6)
follow_up_mode = "one-at-a-time"

# ---- modules ON (each is on pi's kept-list, pi§10 closing) ----
[capabilities.trust]
enabled = true
default = "ask"
# pi's ONE built-in gate (pi§4, trust-manager.ts; defaultProjectTrust "ask")
[capabilities.session_tree]
enabled = true
branch_summaries = true
labels = true
# THE core pi feature (pi§10; D5)
[capabilities.session_share]
enabled = true
# /share gist public link (pi§8); human /export HTML is core now (§1.6, S6) and on regardless
[capabilities.server]
enabled = true
# --mode json / --mode rpc embedding ladder (pi§8, pi§10)
[capabilities.plugins]
enabled = true
# everything-is-an-extension (pi§7; D-10 dep satisfied by trust above)
[capabilities.tui]
enabled = true
# §1.9 recorded deviation, default-on in parity presets

# ---- notable OFFs (each a pi FIRST-PARTY omission, pi§10 / catalog §3) ----
[capabilities.tools_search]
enabled = false
# grep/find/ls exist but are OPT-IN even in pi (pi§1 "--tools"); one line re-enables
[capabilities.mcp]
enabled = false
# "intentionally does not include built-in MCP" (pi§7)
[capabilities.subagents]
enabled = false
# example extension only (pi§3 "NO subagents")
[capabilities.permissions]
enabled = false
approval = "never"
sandbox = "danger_full_access"
                                # pi has NO popups/rules/sandbox (pi§4; README "Permissions & containerization").
                                # C3 fires its MANDATORY warning here BY DESIGN — pi's own docs say containers, not trust in the harness.
[capabilities.plan_mode]
enabled = false
# example ext only (pi§10)
[capabilities.todos]
enabled = false
# example ext only (pi§10)
[capabilities.tools_background]
enabled = false
# "tmux instead" (pi§10)
[capabilities.tools_web]
enabled = false
# web search ships as a SKILL in pi (pi§10)
[capabilities.checkpoint]
enabled = false
# git-checkpoint example ext only (pi§5)
[capabilities.memory]
enabled = false
# no memory subsystem (pi§2)
[capabilities.hooks]
enabled = false
# pi's "hooks" are code extensions, not config-registered commands
[capabilities.deferred_tools]
enabled = false
[capabilities.cache]
enabled = false
[capabilities.reduction]
enabled = false
# supercode-only OPTIONAL policies off; A7 truncation + rehydrate stay always-on core regardless (§1.13, S1/S7 — no longer a D-8/D-8-error risk)
[capabilities.model_catalog]
enabled = false
[capabilities.model_oauth]
enabled = false
# pi HAS /login OAuth (pi§9) — deferred module 27, recorded gap
"#;

/// `cc-parity` — design §4.2.
pub const CC_PARITY_TOML: &str = r#"# built-in preset: cc-parity — Claude Code's default surface, composed.
schema_version = 1

[core]
model = "anthropic/claude-opus-4-8"     # CC account-default Opus 4.8 (cc§9 "Account-type defaults")
effort = "medium"
env_context = true                      # CC startup context: cwd/git status (cc§2 "Startup context"); BP-4 adds the approval/sandbox policy line the catalog:90 semantics name, re-emitted per turn on change
context_injections = true               # BP-4 (catalog:91): CC splices ambient reminder blocks into every session (hook `additionalContext`, `<system-reminder>` blocks, cc§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
project_context = true                  # CLAUDE.md tiers + directory walk (cc§2); global tier included (§1.4)
nested_instructions = true               # S6/S12 home: subdir CLAUDE.md loaded on demand, CC default (catalog:84) — closes a gap-ledger row
instruction_imports  = true              # S6/S12 home: `@path` imports, depth 4, CC default (catalog:85) — closes a gap-ledger row
project_root_markers = [".git"]          # BP-4: bounds the ANCESTOR walk (catalog:81) — CC walks UP from cwd and concatenates root→cwd, closest read last (cc§2 "Directory-walk loading")
project_doc_max_bytes = 0                # BP-4 (catalog:87): CC documents NO byte cap on CLAUDE.md — its hygiene levers are the two below — and §3.1 spells "uncapped" as 0. Stated explicitly rather than left absent so the preset says what CC does instead of leaving it to a default.
project_doc_excludes = []                # BP-4 (catalog:87): CC's `claudeMdExcludes` (cc§2) — glob/absolute-path list of CLAUDE.md files to skip; CC ships it EMPTY, same as the rule sets below
project_doc_strip_comments = true        # BP-4 (catalog:87): CC strips block-level `<!-- … -->` from CLAUDE.md before injection so maintainer notes cost no tokens (cc§2 "HTML comment stripping")
parallel_tool_calls = true               # BP-2: CC runs an assistant turn's independent sibling calls as a concurrent batch (cc§1, catalog:59) — the gated batch path in `Agent::run_tools_concurrently`, armed
tool_output_spill = true                 # BP-2: CC's Bash-overflow → session file recovery door (catalog:58): a capped tool result keeps its full bytes in a per-session spill file the notice names, readable with `read_file` — no `capabilities.reduction` required
file_mentions = true                    # BP-5 (catalog D2 "@-file mentions / attachments"): `@` in the prompt injects that file's context (cc§2 "`@`-file mentions"). Deny-rule aware, as CC documents it — a mention resolving to a protected path (see `permissions.protected_paths` below) inlines the refusal, never the bytes.
output_style = "default"                # BP-5 (catalog D2 "Output style / personality module"): CC's `outputStyle` setting at the value CC itself ships — the Default style, which appends nothing to the base prompt (cc§7 "Output styles"). The LAYER is what this arms: naming any other style (`explanatory`, `learning`, or a `~/.claude/output-styles/<name>.md` of the user's own) swaps the response-style instructions without touching `system_prompt`.
path_rules = true                       # BP-5 (catalog D2 "Path-scoped rules"): CC's `.claude/rules/*.md` (cc§2). A rule with no `paths:` joins the instruction blob at startup; a rule WITH `paths:` waits and is injected the first time a tool touches a matching file — CC's own "loads only when Claude touches matching files".

[core.model_switch]
allow_switch = true                     # BP-13 (catalog D9 "Mid-session model switching"): CC's `/model` + Alt+P change the model without losing the session (cc§9). Arms the governed switch — reasoning-artifact filtering (dep 8) plus a persisted `model_change` record in the session journal.
                                        # `notice` is deliberately NOT set: CC switches SILENTLY (no injected switch instructions); that is Codex's behaviour, and cx-parity sets it there.

[core.tools]
# BP-3: `current_time`/`sleep` join the optional default-tool names (the
# `view_image` precedent, §1.2's "fifth optional default-tool name, not a new
# module"). The catalog's clock/sleep row is `✓*` for CC — ScheduleWakeup
# paces the loop but neither reports the time nor pauses it — so cc-parity
# supplies the capability itself rather than the footnote.
enabled = ["read_file", "bash", "edit_file", "write_file", "current_time", "sleep"]
schema_tier = "full"
[core.tools.read_file]
multimodal = true                       # CC Read renders images/PDFs/notebooks (cc§1 Read)
line_numbers = true                     # BP-2: CC Read's `cat -n` gutter, numbered from `offset` (cc§1 Read, catalog:26)
[core.tools.edit_file]
require_read_before_edit = true         # S6/S12 home: CC Edit refuses unless the file was read this conversation (catalog:32) — closes the cc-parity gap-ledger row
notebook_aware = true                   # S6/S12 home: NotebookEdit cell-level replace/insert/delete (catalog:40) — closes the cc-parity gap-ledger row
[core.tools.bash]
timeout_secs = 120                      # CC default 2 min, model-raisable (cc§1 Bash)

[core.skills]
enabled = true                          # SKILL.md dirs + commands, descriptions-only until invoked (cc§7 Skills)
harness = "claude-code"                 # BP-6: the loop discovers SKILL.md from CC's own documented roots and precedence — enterprise/managed > `~/.claude/skills` > plugin bundles (`plugin:skill`) > project `.claude/skills`, plus nested subdirectory skills as `dir:skill` (cc§7 "Skill locations & precedence")
shell_injection = true                  # BP-5 (catalog D2 "Shell-output injection in templates/skills"): CC executes `` !`cmd` `` inline and ```` ```! ```` blocks inside a skill/command body AT LOAD TIME (cc§7 "Dynamic context injection"), disabled org-wide with `disableSkillShellExecution` — this key is that switch stated positively. Every extracted command is decided by the one permissions engine with THIS preset's rules and protected paths; under `approval = "untrusted"` a bare `bash` is `Ask`, so a body that wants its own command run declares it in `allowed-tools`, exactly as CC requires.

[core.compaction]
enabled = true
summarize = true                        # CC auto-compaction near limit + /compact [instructions] (cc§2). BP-4: this key now arms the CORE span-summary side-call too — design §1.5 makes "an LLM summary of the compacted span" part of obligation 5, and §3.1 annotates this very key "SpanSummary side-call … D-9 small-model fallback"
reserve_tokens = 16384                  # CC's threshold is pct-based (CLAUDE_CODE_AUTOCOMPACT_PCT_OVERRIDE, cc§2); reserve is our §1.5 equivalent
focus_instructions = ""                 # BP-4 (catalog:98): CC has NO standing compaction focus — steering is per invocation, `/compact [instructions]` (cc§2). Stated empty so the preset says that, rather than leaving the knob to a default, and so the mechanism is armed at the one place it belongs: `Agent::compact_now(focus)`.

[core.session]
append_only = true                      # BP-8 (catalog:150): CC flushes every event to its session JSONL as the turn runs (cc§5), so a crash mid-turn keeps the turn. Arms `crate::session_journal` — supercode's own store otherwise rewrites `<name>.jsonl` only at end-of-turn.
queue_persist = true                    # BP-8 (catalog:154): CC writes QUEUE-OPERATION records for prompts typed while it is busy (cc§5), so a pending input survives a restart. The journal armed above is where those records go.
auto_title = true                       # BP-7 (catalog:150): CC writes `ai-title` records — a cheap-model title for every session (cc§5). The titler, its small-model routing and its `SessionStore::set_title` write were all built and CLI-wired at P4b; this preset never set the gate, so under cc-parity no title was ever generated. `small_model` is pinned below (`capabilities.model_catalog`), so this runs on Haiku, not Opus.

[core.prompts]
# BP-7 (catalog §4a "Review mode"): CC's `/code-review` + `/security-review` are
# purpose-built review TURNS with a fixed report format, not a separate agent
# (cc§10). This template IS that format; `Agent::review` sends it as an ordinary
# turn of this same session, so the review inherits the session's tools,
# permissions, transcript and records.
code-review = """Review the current code changes as a dedicated review turn. {args}
Inspect the diff and the files it touches with the available tools before judging anything.
Report in exactly these sections, omitting a section only when it is genuinely empty:
1. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
2. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
3. Reuse and simplification — existing code the change should have used; code the change makes dead.
4. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""

# ---- modules ON ----
[capabilities.tools_search]
enabled = true
glob = true
content_search = true
list_dir = false
                               # Glob + Grep, promptless read-class (cc§1); list_dir OFF (S15 fix) — CC lists dirs via Bash/Glob, Read rejects directories (catalog:33 fn²), so the module's dir-listing sub-tool would be a capability CC users never see
[capabilities.tools_web]
enabled = true
fetch = true
search = true
# WebFetch + WebSearch (cc§1)
[capabilities.tools_question]
enabled = true
# AskUserQuestion (cc§1)
[capabilities.todos]
enabled = true
persist = true
goals = true
# Task*/TodoWrite checklist, persists across compaction (cc§1, cc§3)
# BP-7 (catalog:138): `goals` is this module's persistent-objective variant (design §2 module 7) — CC's `/goal`, a standing condition restated at the tail of every request and persisted as `<session>.goal.json`, distinct from the per-stretch `update_plan` checklist above.
[capabilities.plan_mode]
enabled = true
effort = "high"                         # BP-13 (catalog D9, the plan-mode half of "Reasoning effort / thinking budgets"): planning is the phase that most rewards deeper reasoning, and CC's plan mode is where a session does its thinking (cc§3). While the mode is live the request carries this level instead of `[core] effort`; leaving the mode restores it.
# Shift+Tab / EnterPlanMode read-only mode (cc§3); dep met by permissions.rules below
[capabilities.subagents]
enabled = true
max_depth = 2
background = true
background_prompts = "parent"
                               # Agent tool; background-by-default v2.1.198+, nested allowed (cc§1, cc§3)
                               # C6 resolved via the schema key (S2 fix, not prose): background_prompts = "parent" — background children surface prompts in the parent session (cc§3, claude-code.md:98)
[capabilities.tools_background]
enabled = true
# run_in_background + Ctrl+B (cc§1, cc§8); C6: same parent-surfaced queue as subagents.background_prompts above

[capabilities.permissions]
enabled = true
approval = "untrusted"                  # CC tiered default: read-only never prompts, Bash/edits prompt first-use (cc§4 "Tiered defaults")
                                        # No C3: approval != never.
auto_approved_tools = ["read_file", "glob", "search", "ask_user", "current_time", "sleep", "enter_plan_mode", "exit_plan_mode", "update_plan"]
                                        # CC read-only tier (cc§4); `list_dir` removed (S15 — module tool is off above)
                                        # BP-3: the new no-side-effect tools join that tier — CC never prompts before
                                        # AskUserQuestion/EnterPlanMode/ExitPlanMode, and under `approval = "untrusted"`
                                        # every tool NOT listed here asks first, which would put a permission prompt in
                                        # front of the question prompt (and would refuse both outright in a headless run).
                                        # `exit_plan_mode` carries its own explicit plan approval, so the generic gate in
                                        # front of it is pure double-prompting.
                                        # BP-8 (catalog:156): `update_plan` joins the same tier — CC's TodoWrite is a
                                        # checklist write with no side effect outside the session and never prompts
                                        # (cc§1/cc§3). Under `approval = "untrusted"` it otherwise asks on every plan
                                        # update, which in a headless run refuses the plan outright — i.e. the plan row's
                                        # own behaviour would be unreachable under this preset.
# module 12 in table form (S5): OS sandbox OFF (CC's `/sandbox` is opt-in, cc§4), fs tier unconfined.
# One key `permissions.sandbox` — the table form, not the bare-scalar shorthand, so no collision.
[capabilities.permissions.sandbox]
enabled = false
tier = "danger_full_access"
env_policy = "inherit"                  # BP-10 (catalog "Child-process env sanitization", cc `✓*`): CC's env control is `sandbox.credentials.envVars` — an OPT-IN of the opt-in `/sandbox` (cc§4). With the OS sandbox off above, a CC Bash call inherits the user's environment, so the preset SAYS "inherit" rather than leaving the knob to a default that happens to agree; cx-parity states the other posture on the same key.
escalation = "ask"                      # BP-10 (catalog "Sandbox-escalation path", cc `✓*` allowUnsandboxedCommands): with `enabled = false` nothing is confined, so this never fires under cc-parity today — it is the answer for the `/sandbox` session, where CC asks before running a command outside the sandbox rather than refusing it outright.
[capabilities.permissions.sandbox.network]
enabled = false                         # BP-10: CC's own default. `/sandbox` is opt-in and its network proxy with it (cc§4 `sandbox.network.*`); stated so the preset carries CC's posture on the key instead of defaulting to it silently.
[capabilities.permissions.rules]
enabled = true                          # deny→ask→allow first-match IS the CC algebra — native, no translation (C5 decision; cc§4 "Rule sets & evaluation")
deny  = []
ask   = []
allow = []                              # CC ships empty rule sets; "don't ask again" persists into allow at runtime (cc§4)
[capabilities.permissions.protected_paths]
enabled = true                          # never-auto-approved set (cc§4 "Protected paths")
# Rule-layer floor: file-tools + bash redirect targets + apply_patch + known
# argv-writers (tee/dd/cp/mv/install/sed -i/truncate/ln); an opaque or
# dynamic bash write is forced to Ask. Complete OS-level write confinement
# is `permissions.sandbox`'s job (module 10), not this table's — see
# `crate::permissions` module doc / `Config::permissions_protected_paths`.
paths = [".git/**", ".env*", ".claude/**", ".vscode/**", ".idea/**", "~/.claude/settings*"]
[capabilities.permissions.approvals]
persist = true                          # BP-10 (catalog "Session approval caching"): CC records "don't ask again" PER PROJECT + command, not per process (cc§4) — the grant is still there tomorrow. Stored beside the session's other per-project records ($SUPERCODE_HOME/approvals/<project tag>.json) and reversible: delete it (or `ApprovalCache::clear`) and the next matching call asks again.

[capabilities.trust]
enabled = true
default = "ask"
# workspace trust gates project allow-rules (cc§4)
[capabilities.mcp]
enabled = true
# stdio/HTTP/OAuth, resources, prompts-as-commands (cc§7)
[capabilities.deferred_tools]
enabled = true
core = ["read_file", "bash", "edit_file", "write_file", "glob", "search", "update_plan", "ask_user", "enter_plan_mode", "exit_plan_mode", "current_time", "sleep"]
                               # CC defers MCP tool definitions BY DEFAULT behind ToolSearch (cc§7 "Tool search"); builtins stay eager
                               # BP-3: the new built-ins are eager for the same reason the older ones are — CC advertises AskUserQuestion/EnterPlanMode/ExitPlanMode up front, and a question tool the model must first tool_search for is not the same capability
[capabilities.hooks]
enabled = true
# config-registered lifecycle hooks (cc§7: 30 events; module ships the CC-compatible subset first)
[capabilities.memory]
enabled = true
# auto memory MEMORY.md + topic files (cc§2); D-9 dep → model_catalog below
[capabilities.checkpoint]
enabled = true
# per-prompt file-history-snapshot → /rewind (cc§5)
[capabilities.session_tree]
enabled = true
branch_summaries = false
labels = false
                               # CC has the tree DATA MODEL (uuid/parentUuid, cc§5) + /rewind; summaries/labels are pi-isms
[capabilities.model_catalog]
enabled = true
small_model = "anthropic/claude-haiku-4-5"
fallback = []                           # CC ships NO fallback model; `--fallback-model` (≤3) is per-invocation (cc§9). The chain is EXECUTED by the loop when one is given — see `Agent::complete_with_fallback`.
provider = "anthropic"                  # BP-13: which provider's alias scope is in force — CC's friendly names resolve per provider/account (cc§9)
account = "max"                         # BP-13 (cc§9 "Account-type defaults"): on Max, `default`/`best` mean Opus; another plan's scope would mean something else
service_tier = "auto"                   # BP-13 (catalog D9 "Fast mode / service tiers"): CC's standard tier; `/fast` moves the session to `priority` and warns about the cache churn (cc§9)
allowed_models = []                     # BP-13 (catalog D9 "Org model allowlists"): CC ships availableModels EMPTY — unrestricted. Stated so the preset says CC's posture rather than leaving it defaulted.
denied_models = []
                               # aliases + ANTHROPIC_SMALL_FAST_MODEL + fallback chains (cc§9)
[capabilities.model_catalog.aliases]
"*[1m]" = "{}[1m]"                      # BP-13: CC's 1M-context SUFFIX form (cc§9 `sonnet[1m]`) — a pattern alias whose captured stem is itself alias-resolved, so `sonnet[1m]` lands on `anthropic/claude-sonnet-4-6[1m]`
[capabilities.model_catalog.providers.anthropic.aliases]
fast = "anthropic/claude-haiku-4-5"     # BP-13: provider-scoped — `fast` means Haiku only while this session talks to Anthropic
[capabilities.model_catalog.providers.anthropic.accounts.max.aliases]
default = "anthropic/claude-opus-4-8"   # BP-13 (cc§9): the Max plan's account defaults
best = "anthropic/claude-opus-4-8"
[capabilities.model_catalog.models."anthropic/claude-haiku-4-5"]
max_effort = "low"                      # BP-13: the per-model effort TIER — the small/fast model is not asked for deep reasoning, whatever `[core] effort` says
[capabilities.tui]
enabled = true

# ---- notable OFFs ----
[capabilities.tools_apply_patch]
enabled = false
# CC is edit-only (C1; catalog §5 conflict 1)
[capabilities.tools_persistent_shell]
enabled = false
[capabilities.lsp]
enabled = false
# CC's LSP is inactive until a plugin installs it (cc§1) — off matches default
[capabilities.formatters]
enabled = false
[capabilities.session_share]
enabled = false
# no PUBLIC share links in CC (D5 OC+PI-only row); `/export`+`/copy` are core now (§1.6 `export_format`, S6) and stay on regardless
[capabilities.server]
enabled = false
# CC has no local HTTP server surface; SDK is in-process
[capabilities.reduction]
enabled = false
[capabilities.cache]
enabled = true
plan = "imported_prefix"
warnings = true
# BP-4 deviation from §4.2's own `enabled = false` line, recorded here rather than silently:
# that line's reason ("CC caching is provider-automatic") does not survive the catalog's own
# grading of the same behavior. catalog:110 "Cache-aware context architecture" marks CC ✓ with
# the cache-action matrix, cache-preserving `/cd` and TTL switches (cc§2) — i.e. CC deliberately
# SHAPES the cached prefix and warns when an action would churn it, which is a harness behavior,
# not a provider one (Anthropic prompt caching is driven by explicit breakpoints, and something
# has to place them). supercode's equivalent is exactly `CachePlan::ImportedPrefix` + the
# imported-prefix compaction clamp + `AgentEvent::CacheWarning`, all already implemented and
# wired — with the module off they simply never fired under this preset, which is the gap the
# ledger row named. `warnings = true` is C2's referee, and now reaches `Config::cache_warnings`.
[capabilities.structured_output]
enabled = false
# --json-schema is headless-only surface; enable per-run
[capabilities.model_oauth]
enabled = false
# recorded gap: CC's DEFAULT auth is subscription OAuth (cc§9) — module 27 deferred
"#;

/// `cx-parity` — design §4.3.
pub const CX_PARITY_TOML: &str = r#"# built-in preset: cx-parity — Codex's default surface, composed.
schema_version = 1

[core]
effort = "medium"                       # model_reasoning_effort default tier (cx§6, cx§9)
env_context = true                      # <environment_context> block: cwd/sandbox/approval (cx§2) — BP-4 supplies the approval/sandbox line this comment already claims, and re-emits the block on change (cx§2 "re-emitted on change")
context_injections = true               # BP-4 (catalog:91): Codex's whole `context/` library (~25 block types) is spliced at assembly time (cx§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
project_context = true                  # AGENTS.md hierarchy, root-down concat, 32KiB cap (cx§2)
project_root_markers = [".git"]         # BP-4: cx's own `project_root_markers` default (cx§6 config census) — the git root the AGENTS.md walk climbs to before descending root→cwd (cx§2)
project_doc_max_bytes = 32768           # BP-4 (catalog:87): cx's documented `project_doc_max_bytes` default, 32 KiB (cx§2, cx§6 "project_doc_max_bytes (default 32768)") — the cap the `project_context` line above already claims but nothing enforced
project_doc_excludes = []               # BP-4: Codex has no exclude list (that is CC's `claudeMdExcludes`); stated empty so the preset's hygiene posture is complete rather than defaulted
project_doc_strip_comments = false      # BP-4: Codex strips nothing from AGENTS.md — the HTML-comment strip is CC-only (catalog:87)
# P2 placement fix: §4.3's own TOML block places `shell_env_snapshot` under
# `[core.tools]`, but §3.1's schema (the "annotated, exhaustive" canonical
# definition, line ~598) defines it as a direct `[core]` scalar, not a
# `core.tools.*` key — `CoreToolsConfig` has no such field, so a literal
# under-`[core.tools]` placement would silently parse-and-drop it. Moved
# here to match §3.1 (the schema doc doesn't have this key twice with two
# different homes; §4.3 is corrected to agree with it).
shell_env_snapshot = true               # S6/S12 home: shell-env snapshotting, cx stable-on feature (catalog:338) — closes a gap-ledger row
parallel_tool_calls = true              # BP-2: Codex runs sibling tool calls concurrently behind its RwLock gate (cx§1, catalog:59) — the same batch path, armed
tool_output_spill = true                # BP-2: Codex token-caps a tool result with no spill file of its own (catalog:58 `✓*`); supercode's capped result names a per-session spill file the model reads back with `cat` (cx's own read pathway), so the truncation is recoverable rather than lossy
file_mentions = true                    # BP-5 (catalog D2 "@-file mentions / attachments"): Codex's `@`-mention popup and `/mention` insert a path into the prompt (cx§2 "`@`-mentions (files)"); the path then has to become CONTEXT, which is what this key does. Same permissions-engine read check the cc side gets — cx's `.codex/**` protected paths refuse in place.
output_style = "none"                   # BP-5 (catalog D2 "Output style / personality module"): Codex's `personality` key at its neutral value (cx§6 `personality (none|friendly|pragmatic)`, cx§2 "Personality layer", `/personality`). `friendly`/`pragmatic` are the swaps; `none` is the selection Codex makes when nobody has chosen, and it appends nothing.
# C4 (catalog §5 conflict 4): Codex's base prompt VARIES BY APPROVAL MODE (cx§2: "proactively run
# tests only under never"). This preset pins prompt + approval together; when CONTINUING an
# imported rollout, the emulate path replays the rollout's own persisted base_instructions
# verbatim (session_meta carries them — cx§2:101; supercode SessionMeta.system_prompt), which is
# exact prompt parity by construction rather than imitation.

[core.tools]
# BP-3: five more optional default-tool names (the `view_image` precedent),
# each closing a gap-ledger row the catalog scores `✓` for Codex:
#   `request_user_input` — cx's own experimental spelling of the question
#      tool, registered as an ALIAS of the same tool object `ask_user` is, so
#      a continued Codex session's calls keep resolving (catalog:45).
#   `current_time` + `sleep` — cx's clock/sleep features (catalog:53).
#   `get_context_remaining` + `new_context` — cx's token_budget feature
#      (catalog:54).
#   `image_gen` — cx's image feature (catalog:52); the tool posts to the
#      SESSION's own provider `/v1/images/generations` and reports
#      unsupported_action when that provider has no image route.
enabled = ["bash", "view_image", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
                                        # Codex has NO read/write/edit/glob/grep function tools:
                                        # reads via shell (cat, rg), writes via apply_patch (cx§1 "File reads/writes"; D1 footnote ¹).
                                        # Disabling edit/write advertising is ALSO the C1 resolution.
                                        # `view_image` (S6/S12 home, catalog:28) closes the gap-ledger row: with `read_file` off, cx-parity
                                        # would otherwise have NO image-input pathway at all, unlike stock Codex's dedicated tool.
schema_tier = "full"

[core.skills]
enabled = true                          # SKILL.md discovery, $skill mentions (cx§7 Skills).
harness = "codex"                       # BP-6: the loop discovers SKILL.md from Codex's own documented roots — admin `/etc/codex/skills`, the bundled `$CODEX_HOME/skills/.system` cache, user `~/.agents/skills` + `$CODEX_HOME/skills`, repo `.agents/skills` from cwd to the repo root (cx§7) — invoked by `$slug` mention or the `skill` tool
implicit_match = false                  # cx§7 also matches a skill IMPLICITLY from its description; off here, so only an explicit `$slug`/`/skill:` invocation or a `skill` tool call ever spends a body's tokens
                                        # D-7 (S3-amended, no longer a judgment call): the read pathway is bash (`cat`) in the codex
                                        # shape — §2.1's D-7 now names read_file|bash explicitly; the resolver warns, doesn't error.

[core.compaction]
enabled = true
summarize = true                        # /compact + auto-compaction at model_auto_compact_token_limit (cx§2). BP-4: also arms the CORE span-summary side-call (design §1.5 obligation 5; §3.1 "SpanSummary side-call")
focus_instructions = ""                 # BP-4 (catalog:98): Codex has no standing compaction focus either — `/compact [instructions]` steers per invocation (cx§2). Empty states that explicitly.
reserve_tokens = 16384                  # BP-1: §4.3's own TOML block armed NEITHER trigger, so `Agent::maybe_compact`
                                        # returned false on its `threshold.is_none() && reserve_tokens.is_none()` guard
                                        # and cx-parity could never compact at all — contradicting the `summarize` line's
                                        # own comment ("auto-compaction at model_auto_compact_token_limit"). That limit is
                                        # an absolute TOKEN limit, i.e. §1.5's context-window-pressure trigger, not a
                                        # message count — so the pressure knob is the one to arm (pi-core's `after_messages
                                        # = 0` comment states the same reading: "the trigger is token pressure, never
                                        # message count"). Value transcribed from cc-parity/pi-core's own 16384, the §1.5
                                        # `reserve_tokens` equivalent this schema expresses a foreign auto-compact
                                        # threshold as.

[core.model_switch]
allow_switch = true                     # BP-13 (catalog D9 "Mid-session model switching"): Codex's `/model` changes the model without losing the thread (cx§9). Arms the governed switch — dep-8 reasoning-artifact filtering plus a persisted `model_change` record in the session journal.
notice = true                           # BP-13: Codex additionally INJECTS switch instructions into the conversation on a mid-session change (cx§9), so the incoming model reads the handoff instead of inferring it. CC does not, which is why cc-parity leaves this unset.

[core.session]
append_only = true                      # BP-8 (catalog:150): Codex appends every rollout line and flushes per line, with a retry (cx§5) — a crash mid-turn keeps the turn. Arms `crate::session_journal`.
                                        # `queue_persist` is deliberately NOT set: catalog:154 is `—` for Codex (no queue-operation records), and a cx-parity session must not gain an input-durability guarantee stock Codex does not have.
auto_title = true                       # BP-7 (catalog:150, cx `✓*`): Codex derives a title/preview for every rollout into its SQLite index and offers `/title` for a manual override (cx§5). supercode's equivalent is the same small-model titler cc-parity uses; the VARIANT the catalog footnotes is that cx derives its default from the first message rather than a model call, which is why this row is `✓*` for cx and not `✓`.

[core.prompts]
# BP-7 (catalog §4a "Review mode"): Codex ships `/review` AND a `codex review`
# subcommand with a `review_model` of its own (cx§3). The template below is the
# report format; the model choice stays this session's model, since cx-parity
# pins no `review_model` (upstream leaves it unset by default too).
code-review = """Review the current code changes as a dedicated review turn. {args}
Read the diff and the files it touches with the shell before judging anything.
Report in exactly these sections, omitting a section only when it is genuinely empty:
1. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
2. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
3. Reuse and simplification — existing code the change should have used; code the change makes dead.
4. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""

# ---- modules ON ----
[capabilities.tools_persistent_shell]
enabled = true
# exec_command/write_stdin PTY unified exec (cx§1); supercode has it (builtins.rs:981-984)
[capabilities.tools_apply_patch]
enabled = true
per_model = true
                               # freeform envelope, default write path (cx§1); per_model honors C1 via model_catalog bits (cx§9)
[capabilities.todos]
enabled = true
persist = true
goals = true
# update_plan is ALWAYS registered (cx§1); goals (S6/S12 home, catalog:138, cx `/goal`) is the persistent-objective variant of this same module — closes a gap-ledger row
# BP-7: the `goals` key above is that variant, armed. Codex keeps its goal in `goals_1.sqlite`; supercode keeps the same single record in `<session>.goal.json`, restated at the tail of every request while it stands.
[capabilities.tools_web]
enabled = true
fetch = false
search = true
                               # Codex has hosted web_search but NO web-fetch tool (cx§1); cached mode default
[capabilities.tools_background]
enabled = true
# background terminals, /ps //stop (cx§1); C6 (S8-corrected defense): under `model_requested`, tools run sandboxed WITHOUT prompting unless the model itself escalates — from a background task's perspective that's an auto-run default, satisfying C6's auto-policy requirement without needing a separate allow-list
[capabilities.subagents]
enabled = true
max_depth = 1
background = true
background_prompts = "auto_policy"
                               # multi_agent default-on, agents.max_depth default 1 (cx§1, cx§6)
                               # BP-7 (catalog:135 "Background subagents + resume", cx `✓ v2 mailbox
                               # (send_message/wait/interrupt)`): §4.3's own line read
                               # `background = false`, which contradicted the very column it was
                               # transcribing — Codex's multi-agent v2 detaches children and talks to
                               # them through a mailbox. On, with `background_prompts = "auto_policy"`
                               # (C6's required companion, and the honest one for cx: under
                               # `approval = "model_requested"` tools run sandboxed without prompting
                               # unless the MODEL escalates, so a detached child has no interactive
                               # prompt to surface to a parent — the same reasoning
                               # `capabilities.tools_background`'s own C6 comment above already gives).
[capabilities.tools_question]
enabled = true
# BP-3 (flipped from `false`): the catalog scores cx `✓*` for the structured
# user-question tool — `request_user_input` EXISTS at the pin, behind an
# experimental flag — and the parity ledger's denominator is that column, so
# cx-parity has to supply the capability rather than the footnote. The module
# registers the tool; `[core.tools] enabled` above additionally registers
# Codex's own spelling as an alias. §2.1's `tools_question → tui|server` dep is
# met by `[capabilities.tui]` below.
[capabilities.deferred_tools]
enabled = true
core = ["bash", "shell", "apply_patch", "update_plan", "ask_user", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
                               # ToolExposure::Deferred + native tool_search is Codex's own mechanism (cx§1)
                               # BP-3: the new built-ins stay eager — a feature tool the model must tool_search for first is not the same capability Codex ships
[capabilities.structured_output]
enabled = true
# --output-schema final-response contract (cx§8); module 33, Config.response_format

[capabilities.permissions]
enabled = true
approval = "model_requested"            # S8 fix: Codex `on-request` default is "the MODEL decides when to ask" (cx§4, protocol.rs:921-924) — NOT supercode's `OnRequest` (client-side allowlist check, config.rs:39-40, 299-302); using the wrong enum value would prompt on every non-allowlisted tool call where stock Codex prompts almost never. `model_requested` is the NEW distinct mode (§3.2) the module must re-implement escalation-initiated-by-the-model for.
[capabilities.permissions.sandbox]
tier = "workspace_write"                # writes in cwd + tmp, no network (cx§4); RECOMMENDED POSTURE (S16 fix), not upstream's labeled default — codex.md names no sandbox mode "(default)" (unlike approval); this is upstream's own steered guidance ("prefer --sandbox workspace-write", the deprecated --full-auto warning)
                                        # BP-10: the TABLE form of the same key (§3.1 defines `sandbox = "X"` as identical to `sandbox = { tier = "X" }`), so the three knobs below can be stated. `enabled` is deliberately left UNSET, exactly as the bare form left it — `crate::sandbox::os_sandbox_active` then keeps the tier-driven trigger this preset already had.
env_policy = "filtered"                 # BP-10 (catalog "Child-process env sanitization", cx ✓): Codex's `shell_environment_policy` filters secrets from a spawned shell BY DEFAULT (cx§4). Until this line, cx-parity resolved to `Inherit` and no sanitization happened under the preset at all — the mechanism existed and nothing armed it.
escalation = "ask"                      # BP-10 (catalog "Sandbox-escalation path", cx ✓): when a confining tier cannot be enforced on this host, Codex does not silently run unconfined — the user is asked. No handler installed still denies (fail-closed, `crate::sandbox::resolve_escalation`).
[capabilities.permissions.sandbox.network]
enabled = true                          # BP-10 (catalog "Network sandbox / domain rules", cx ✓): `workspace_write` cuts subprocess network (cx§4). Real on this host: macOS seatbelt `(deny network*)`, Linux a fresh network namespace. Per-DOMAIN filtering of arbitrary subprocess traffic is the remaining gap — see the ledger row's note.
[capabilities.permissions.rules]
enabled = true                          # execpolicy .rules allow/prompt/forbidden → translated into deny→ask→allow (C5)
deny  = []
ask   = []
allow = []
[capabilities.permissions.protected_paths]
enabled = true
# Rule-layer floor (file-tools + bash redirect targets + apply_patch + known
# argv-writers; opaque/dynamic bash writes forced to Ask) — NOT the same as
# cx's `workspace_write` OS sandbox read-only mount above; see
# `crate::permissions` module doc for exactly what is/isn't covered here.
paths = [".git/**", ".codex/**"]        # read-only even inside writable roots (cx§4 workspace-write)
[capabilities.permissions.approvals]
persist = true                          # BP-10: cx's `with_cached_approval` grants are saved as prefix rules that outlive the process (cx§4). Same per-project store and the same one-file reversibility as cc-parity.

# BP-10 (catalog "Named permission profiles", cx §4 `[permissions.<name>]`
# Beta: "extends, fs+net rules"). Reusable, INHERITABLE bundles, selectable
# per run without editing config — `supercode --permission-profile <name>`
# (sugar for `-c capabilities.permissions.profile=<name>`, so the selection
# goes through the same resolver every other key does). The three bundles
# below are Codex's own three postures expressed in this schema; no
# `profile` key is set, so cx-parity's own top-level permission keys stand
# until a run names one.
# Each bundle carries the SANDBOX TIER + RULES cx§4 names ("extends, fs+net
# rules"), deliberately not `approval`: the approval mode is coupled to the
# base prompt (C4) and to §2.2 C6's background-prompt dependency, so a
# bundle that silently changed it would make a permission switch a
# loop-shape switch. The mechanism accepts `approval` from a user's own
# bundle; these shipped three do not use it.
[capabilities.permissions.profiles.read-only]
sandbox = "read_only"
[capabilities.permissions.profiles.workspace-write]
sandbox = "workspace_write"
[capabilities.permissions.profiles.locked-down]
extends = "read-only"                   # the inheritance half of the row: this bundle IS read-only, plus a shell floor
rules = { deny = ["bash", "shell", "background_exec"] }

[capabilities.trust]
enabled = true
default = "ask"
# [projects] trust_level gate + hook hash-trust (cx§4:153, cx§7)
[capabilities.mcp]
enabled = true
serve = true
# full client stack (cx§7); serve = codex mcp-server analog (module 16)
[capabilities.hooks]
enabled = true
# CC-compatible 10-event shape, hash-trusted (cx§7 "Lifecycle hooks")
[capabilities.model_catalog]
enabled = true
provider = "openai"                     # BP-13: the alias scope in force for this session (cx§9)
service_tier = "auto"                   # BP-13 (catalog D9 "Fast mode / service tiers"): cx's `model_service_tier` default; `/fast` moves the session to `priority` (cx§9)
allowed_models = []                     # BP-13 (catalog D9): Codex pins features/profiles by requirements, not by a model allowlist — stated empty so the preset says so
denied_models = []
# capability bits (apply_patch_tool_type, supports_search_tool) drive
                               # per-model tool swaps — the C1 resolution machinery (cx§9 "Model catalog")
# BP-5 (catalog D2 "Per-model-family base-prompt selection", cx§2 "Per-model
# base instructions"): Codex selects its system prompt PER MODEL FAMILY from
# bundled markdown — gpt_5_codex_prompt.md, gpt-5.1-codex-max_prompt.md,
# gpt-5.2-codex_prompt.md, gpt_5_1_prompt.md, gpt_5_2_prompt.md, and
# prompt_with_apply_patch_instructions.md, the variant that appends the full
# apply_patch tutorial. The FUNCTIONAL split across that set is exactly that
# tutorial: a codex-tuned family is taught the patch envelope, a general
# family is not. These two entries are that split, in supercode own words —
# never a copy of upstream prompt text. A model matching neither keeps
# core.system_prompt, so this table narrows nothing.
# Keys are model-id globs and the MOST SPECIFIC (longest) match wins:
# openai/gpt-5.2-codex takes *codex*, openai/gpt-5.1 takes *gpt-5*.
[capabilities.model_catalog.base_prompts]
"*gpt-5*" = """
You are a coding agent running in a terminal. Work through the shell: read
with cat/rg, change files by writing them out, and verify with the project own
commands before reporting anything as done. Prefer the smallest change that
fixes the problem, and say plainly what you did and what you did not check.
"""
"*codex*" = """
You are a coding agent running in a terminal. Work through the shell: read
with cat/rg, change files by writing them out, and verify with the project own
commands before reporting anything as done. Prefer the smallest change that
fixes the problem, and say plainly what you did and what you did not check.

File edits go through the apply_patch envelope. One envelope may carry several
operations, and every path is relative to the working directory:

*** Begin Patch
*** Add File: path/to/new.rs
+the whole new file, one + per line
*** Update File: path/to/existing.rs
@@ context line locating the hunk
-the exact line being replaced
+its replacement
*** Delete File: path/to/gone.rs
*** End Patch

Context lines carry a leading space, removals a -, additions a +. An update
whose context does not appear verbatim in the file is rejected whole, so read
the file first and quote it exactly.
"""
[capabilities.model_catalog.models."openai/gpt-5*"]
apply_patch = true                      # BP-13: the freeform apply_patch envelope IS this family's write path (cx§1) — so `edit_file`/`write_file` are never co-advertised to it
search_tool = true
[capabilities.model_catalog.models."openai/gpt-4*"]
apply_patch = false                     # BP-13: a family the catalog marks as NOT taking the envelope gets `edit_file`/`write_file` instead — the tool SURFACE adapts to the model, inside `ToolRegistry::from_config`'s own selection
[capabilities.tui]
enabled = true

# ---- notable OFFs ----
[capabilities.tools_search]
enabled = false
# no glob/grep tools; "prefer rg" via shell is prompt guidance (cx§2)
[capabilities.plan_mode]
enabled = false
# /plan is effort-tier steering, not a CC/OC restriction mode (cx§6; catalog D1 CC+OC)
[capabilities.memory]
enabled = false
# [features].memories = false default (cx§6, cx§7)
[capabilities.checkpoint]
enabled = true
restore = false
# BP-7 (catalog:112 "Turn diff tracking", cx `✓ turn_diff_tracker`): §4.3's own
# line read `enabled = false` with the reason "no shadow-git; ghost_snapshot is a
# legacy no-op (cx§6)". That reason is about the RESTORE half, and it still
# stands — `restore = false` states it as a key rather than as prose, and
# `CheckpointObserver::restore` refuses under it. But Codex genuinely DOES track
# each turn's cumulative file diff (cx§3 `turn_diff_tracker`), and this module is
# where supercode captures per-turn pre-images, so leaving the whole table off
# meant cx-parity captured no turn-diff data at all — the gap the ledger row
# named. On: tracking without restoring, which is exactly Codex's shape.
# `file-checkpointing-code-restore` is `—` for cx and stays a cc-only row.
[capabilities.session_tree]
enabled = false
# rollout is STRICTLY LINEAR (C7); fork = truncate+copy (D5 footnote ¹³)
[capabilities.session_share]
enabled = false
[capabilities.lsp]
enabled = false
[capabilities.formatters]
enabled = false
[capabilities.server]
enabled = false
# app-server parity is out of preset scope — see gaps
[capabilities.reduction]
enabled = false
[capabilities.cache]
enabled = false
[capabilities.model_oauth]
enabled = false
# ChatGPT-subscription login (cx§9) — module 27 deferred
"#;

/// `oc-parity` — design §4.4.
pub const OC_PARITY_TOML: &str = r#"# built-in preset: oc-parity — opencode's default surface, composed.
schema_version = 1

[core]
env_context = true
project_context = true                  # AGENTS.md + instructions[] concat (oc§6)
nested_instructions = true              # S6/S12 home: nested AGENTS.md auto-attached only for touched-file dirs, oc default (catalog:84; opencode.md:34 "nested-AGENTS.md") — closes an oc-parity gap-ledger row
instruction_imports  = true             # S6/S12 home: `instructions[]` config imports, oc default (catalog:85; opencode.md:367) — closes an oc-parity gap-ledger row
max_tool_output_bytes = 51200           # tool_output.max_bytes default 51200 / 2000 lines (oc§1 Truncate service)

[core.session]
auto_title = true                       # S6/S12 home: hidden title+summary agents (deny-all utility agents) on small_model, oc default (catalog:150; opencode.md:169-170,235) — closes an oc-parity gap-ledger row; small_model is "" below so this falls back to the main model per D-9 until a cheap model is configured

[core.tools]
enabled = ["read_file", "bash", "edit_file", "write_file"]  # oc registry core (oc§1; read subsumes ls)
schema_tier = "full"
[core.tools.read_file]
multimodal = true                       # images/PDFs as attachments (oc§1 read)
[core.tools.bash]
timeout_secs = 120                      # flags.bashDefaultTimeoutMs default 120000 (oc§1 bash)

[core.skills]
enabled = true                          # skill tool + .opencode/skills + remote registries (oc§7)
harness = "opencode"                    # BP-6: the loop reads opencode's own roots — `{skill,skills}` under the global config dir and every `.opencode` dir (oc§7)

[core.compaction]
enabled = true
summarize = true                        # compaction{auto,prune,…} (oc§6)

# ---- modules ON ----
[capabilities.tools_search]
enabled = true
# glob + grep via ripgrep (oc§1)
[capabilities.todos]
enabled = true
persist = true
# todowrite → SQLite todo table (oc§1)
[capabilities.tools_web]
enabled = true
fetch = true
search = false
                               # webfetch is default; websearch only under the Zen provider / exa flags (oc§1 "webSearchEnabled")
[capabilities.subagents]
enabled = true
max_depth = 2
background = false
                               # task tool → child session via parentID, resumable task_id (oc§1); background is env-gated experimental → off
[capabilities.tools_apply_patch]
enabled = true
per_model = true
                               # THE C1 precedent: swapped in (edit/write out) for gpt-* models (oc§1 apply_patch; catalog §5 conflict 1)
[capabilities.plan_mode]
enabled = false
# S18 fix (flipped from `true`): opencode's plan_enter/plan_exit TOOLS — exactly what this module is defined by (§2 module 8) — are DENY-BY-DEFAULT at the pin (opencode.md:251), and this preset's own translated rule set below denies them. What oc actually runs by default is the LEGACY generation: the plan agent is a permission-ruleset agent (edit denied) — already expressible as an agent-scoped `permissions.rules` restriction, not the tool-based `plan_mode` module. Enabling `plan_mode` here would contradict oc's own deny-default; off is the honest reading.

[capabilities.permissions]
enabled = true
approval = "on_request"                 # ask-flow with once|always|reject replies (oc§4 "Ask/approve flow")
sandbox  = "danger_full_access"         # opencode has NO OS sandbox (catalog D4: sandbox is CC+CX only)
[capabilities.permissions.rules]
enabled = true
# opencode's default policy, TRANSLATED per the C5 decision (last-match-wins → deny→ask→allow
# first-match). Source policy (oc§4 "Default policy"): {"*": allow} with carve-outs
# doom_loop: ask, external_directory: ask, question: deny, plan_enter/plan_exit: deny,
# read {*.env: ask, *.env.*: ask, *.env.example: allow}.
#
# S4 fix — this is NOT "the same fixed point" as oc's last-match algebra, and is recorded honestly
# as THREE NAMED DEVIATIONS rather than claimed as exact parity:
#   1. `.env.example` → ASK here, not ALLOW. Under first-match deny→ask→allow, a read of
#      `.env.example` matches the ask-rule `read_file(*.env.*)` (glob matches) BEFORE the allow
#      list is ever consulted, so it asks where stock opencode allows. The engine's rule grammar
#      has no specificity/negation to express "ask unless a more-specific allow" — fixing this
#      would require adding that to the grammar (not done here); the deviation is in the SAFE
#      direction (stricter) and is named, not hidden.
#   2. `doom_loop` is NOT a rule-language pattern at all — it's a repetition TRIGGER (same tool
#      call repeated), not a tool/path match. Routed instead to its actual mechanism: the P4
#      doom-loop breaker (a call-repetition counter + PreToolHook default, §5.2 P4) — no rule
#      entry for it below.
#   3. `external_directory` is an oc PERMISSION CATEGORY (any tool touching paths outside the
#      worktree), not a tool name — routed instead to its actual permission category: `[core]
#      additional_dirs` (Config.additional_dirs, config.rs:169) governs which extra roots are
#      writable at all; paths outside cwd AND outside `additional_dirs` are simply not reachable,
#      which is a stricter (not equivalent) reading of oc's ask-by-default.
deny  = ["plan_enter", "plan_exit"]      # matches module 8's off-by-default above (S18) and oc's own "plan_enter/plan_exit: deny"
ask   = ["read_file(*.env)", "read_file(*.env.*)"]   # includes .env.example per deviation 1 above (glob matches before any allow)
allow = ["*"]
[capabilities.permissions.protected_paths]
enabled = false
# oc does .env protection through rules (above), not a path module

[capabilities.trust]
enabled = true
default = "ask"
# DELIBERATE SAFETY DEVIATION: opencode LACKS a project trust gate (catalog §3 closing) yet loads
# .opencode/ plugins/tools/commands from the repo. Our resolver treats plugins→trust as a HARD dep
# (D-10: "config-borne code execution without a trust gate is an injection hole") — so oc-parity
# ships the gate ON. This only NARROWS behavior (§3.3 monotonic-tightening spirit); recorded, not hidden.

[capabilities.mcp]
enabled = true
# local/remote/OAuth servers (oc§7)
[capabilities.plugins]
enabled = true
# .opencode/plugin + npm specs (oc§7); dep on trust satisfied above
[capabilities.lsp]
enabled = true
# 38 auto-spawned servers; diagnostics into edit/write results (oc§7, oc§10)
[capabilities.formatters]
enabled = true
diff_back = true
# ~27 format-on-write formatters; diff_back honors C10 (oc§7; oc§10; catalog §5 conflict 10)
[capabilities.checkpoint]
enabled = true
# shadow-git snapshots + revert/unrevert (oc§4 "Snapshots"/"Revert")
[capabilities.session_share]
enabled = true
# share manual|auto|disabled, default manual (oc§5, oc§6)
[capabilities.server]
enabled = true
# the client/server split: every frontend is an HTTP client (oc§8)
[capabilities.model_catalog]
enabled = true
small_model = ""
# models.dev catalog + small_model config key (oc§6, oc§9)
[capabilities.tui]
enabled = true

# ---- notable OFFs ----
[capabilities.tools_question]
enabled = false
# question tool is DENY-by-default outside build/plan agents (oc§1, oc§4)
[capabilities.tools_background]
enabled = false
# background subagents are env-gated experimental at the pin (oc§1)
[capabilities.session_tree]
enabled = false
# oc sessions are parent/child linear, no in-place tree (D5; C7)
[capabilities.memory]
enabled = false
[capabilities.hooks]
enabled = false
# no config-registered hooks; the plugin API is the interception layer (oc§7)
[capabilities.deferred_tools]
enabled = false
# opencode advertises eagerly (D1: deferred is CC+CX)
[capabilities.cache]
enabled = false
[capabilities.reduction]
enabled = false
# oc "prune" is the LOSSY analog (catalog §1 UNIQUE OC note); ours stays off to match, mechanism on per §1.13
[capabilities.structured_output]
enabled = false
[capabilities.model_oauth]
enabled = false
# provider /login flows (oc§9) — module 27 deferred
"#;

/// `token-saver` — design §4.5.
pub const TOKEN_SAVER_TOML: &str = r#"# built-in preset: token-saver — the reduction spine over the minimal core.
schema_version = 1
extends = "pi-core"                     # smallest surface = cheapest surface; every knob below overrides it

[core.tools]
schema_tier = "minimal"                 # TR-8/T5 schema tiering (config.rs:219-225)
# C9 (catalog §5 conflict 9): a GLOBAL minimal tier is a footgun for models trained on exact
# schemas. Per-tool override survives the global — pin any load-bearing tool back:
[core.tools.edit_file]
schema_tier = "full"                    # exact-string edit is the least forgiving schema; keep it verbatim

[core.compaction]
enabled = true
reserve_tokens = 24576                  # trigger earlier than pi's 16384 — spend the summary, save the window
keep_recent_tokens = 10000              # aggressive: half of pi's keep budget (recall traded — see caveats)
summarize = true                        # SpanSummary side-call (reduce.rs:274-289) → small_model below (D-9)

[capabilities.reduction]                # module 23 — ALL genuinely-optional passes on (≡ CLI reduce=true, userconfig.rs:33-38)
enabled = true
# NOTE (S7): no `truncation` key here — A7 ToolOutputTruncated (reduce.rs:95-103) is always-on core
# plumbing (§1.13), never a `[capabilities.reduction]` toggle, in token-saver same as every other preset.
stale_reads = true                      # A8 FileReadElided (reduce.rs:104-111)
diff_reads = true                       # TR-3 FileReadDiffed (reduce.rs:202-217)
duplicates = true                       # TR-2 DuplicateOutput (reduce.rs:228-234)
supersede = true                        # TR-6 Superseded (reduce/supersede.rs)
tool_input_elision = true               # TR-10 ToolInputElided (reduce.rs:148-169)
normalize_output = true                 # T30 OutputNormalized (reduce/normalize.rs)
image_redaction = true                  # A9 ImageRedacted (reduce.rs:112-116) — ON here, off everywhere else
span_summaries = true                   # TR-7 (reduce/summarize.rs; D-9)
handoff = true                          # reduce/handoff.rs — smallest-faithful-context model handoff

[capabilities.deferred_tools]           # module 24 — the FLAGSHIP lever (SPEC.md B6)
enabled = true
core = ["read_file", "bash", "edit_file", "write_file"]  # builtins stay eager; everything else behind tool_search

[capabilities.cache]                    # module 25 — the C2 referee
enabled = true
plan = "imported_prefix"                # CachePlan::ImportedPrefix (config.rs:88-96)
warnings = true                         # cache_warnings (config.rs:227-239): every prefix-churning feature must answer to this

[capabilities.model_catalog]            # module 26 — D-9 consumer
enabled = true
small_model = "anthropic/claude-haiku-4-5"  # compaction summaries + span summaries route here, not the main model
"#;

/// `supercode-default` — design §4 intro (S10 fix).
pub const SUPERCODE_DEFAULT_TOML: &str = r#"# built-in preset: supercode-default — pi-core MINUS {trust, session_tree,
# session_share, server, plugins}, PLUS the six extra with_builtins() builtins
# ON, notify available, reduction off (design §4 intro paragraph, S10 fix: NOT
# "pi-core plus extras" — pi-core itself turns those five modules ON to match
# pi's kept-list, so this preset is pi-core's core knobs UNCHANGED with a
# capability delta). This is what `supercode` resolves to with NO config file
# at all — "today's defaults, named and warned" rather than implicit
# (design:958-960).
schema_version = 1
extends = "pi-core"

# core knobs identical to pi-core (§4.1's [core]/[core.retry]/[core.tools]/
# [core.skills]/[core.compaction]/[core.steering] blocks) — unchanged, nothing
# to override here; inherited verbatim via `extends`.

# ---- the S10 delta over pi-core: OFF (today's CLI has none of these — "—"
# across the board in §2's Today column) ----
[capabilities.trust]
enabled = false
[capabilities.session_tree]
enabled = false
[capabilities.session_share]
enabled = false
[capabilities.server]
enabled = false
[capabilities.plugins]
enabled = false

# ---- the six extra with_builtins() builtins (tools/mod.rs:179-192), ON ----
# list_dir/glob/search -> tools_search; apply_patch -> tools_apply_patch;
# persistent_shell -> tools_persistent_shell; update_plan -> todos.
[capabilities.tools_search]
enabled = true
[capabilities.tools_apply_patch]
enabled = true
# NOT per_model: with_builtins() registers every tool struct unconditionally
# with no per-model filtering at all (§4.6 "faithful to today's actual
# unfiltered default stack") — this is what makes C1's warning fire here,
# honestly, rather than suppressing it with a `per_model` bit today's CLI
# doesn't actually have.
[capabilities.tools_persistent_shell]
enabled = true
[capabilities.todos]
enabled = true

# notify available (today's CLI already ships full notify support end to end
# — userconfig.rs:61-71 — unlike pi-core, which doesn't mention it at all).
[capabilities.notify]
enabled = true

# reduction off (policies only; A7 truncation + rehydrate stay always-on core
# regardless, §1.13) — already off by inheritance from pi-core; restated for
# clarity per the design intro's explicit "reduction off" callout.
[capabilities.reduction]
enabled = false

# permissions stays off too (approval = never, sandbox = danger_full_access)
# — identical to pi-core's own values (config.rs:36-38, tools/mod.rs:40-42);
# restated verbatim so the C3 mandatory warning fires here by the same
# mechanism as pi-core's, naming today's actual default stack rather than
# leaving it implicit (design:971-974).
[capabilities.permissions]
enabled = false
approval = "never"
sandbox = "danger_full_access"
"#;

/// The six reserved built-in preset names (design §4, opening paragraph).
pub const RESERVED_PRESET_NAMES: &[&str] = &[
    "pi-core",
    "cc-parity",
    "cx-parity",
    "oc-parity",
    "token-saver",
    "supercode-default",
];

/// Look up a built-in preset's compiled-in TOML text by name. Returns `None`
/// for anything not one of the six [`RESERVED_PRESET_NAMES`] — the resolver
/// (`configfile.rs` §3.5) falls back to treating the name as a file path in
/// that case (user/global layer only, §3.3).
pub fn lookup(name: &str) -> Option<&'static str> {
    match name {
        "pi-core" => Some(PI_CORE_TOML),
        "cc-parity" => Some(CC_PARITY_TOML),
        "cx-parity" => Some(CX_PARITY_TOML),
        "oc-parity" => Some(OC_PARITY_TOML),
        "token-saver" => Some(TOKEN_SAVER_TOML),
        "supercode-default" => Some(SUPERCODE_DEFAULT_TOML),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::configfile::{resolve, HarnessConfig, ResolveOptions};
    use crate::modules::ModuleId;
    use crate::tools::ToolRegistry;

    /// Resolve a built-in preset exactly the way `supercode harness parity`
    /// does — the real resolver, strict, no `[experimental]` table at all.
    fn resolved(name: &str) -> crate::configfile::Resolved {
        let toml = lookup(name).unwrap();
        resolve(toml, None, &ResolveOptions { strict: true })
            .unwrap_or_else(|e| panic!("preset `{name}` failed to resolve: {e}"))
    }

    fn registry_names(config: &crate::Config) -> Vec<String> {
        ToolRegistry::from_config(config)
            .iter()
            .map(|t| t.name().to_string())
            .collect()
    }

    /// Every reserved preset's compiled-in TOML must be valid TOML that
    /// parses into a `HarnessConfig` — the design's own claim ("they were
    /// made TOML-valid in the final design commit") verified mechanically
    /// rather than trusted, since the doc's literal `[capabilities.X] { .. }`
    /// shorthand is NOT valid TOML as written (see the module doc comment).
    #[test]
    fn every_reserved_preset_parses() {
        for name in RESERVED_PRESET_NAMES {
            let toml = lookup(name).unwrap_or_else(|| panic!("no TOML for preset `{name}`"));
            HarnessConfig::from_toml_str(toml)
                .unwrap_or_else(|e| panic!("preset `{name}` failed to parse: {e}"));
        }
    }

    /// `lookup` returns `None` for anything not a reserved name (the
    /// resolver's built-in-vs-path branch point, §3.5 step 1).
    #[test]
    fn lookup_returns_none_for_non_preset_names() {
        assert!(lookup("not-a-real-preset").is_none());
        assert!(lookup("./some/path.toml").is_none());
        assert!(lookup("").is_none());
    }

    /// BP-1 AC1 (cc side). The RESOLVED `cc-parity` preset — no
    /// `[experimental]` table anywhere — must actually register the two
    /// tools its `[capabilities.tools_web] { fetch = true, search = true }`
    /// block declares. Before BP-1 `ToolRegistry::from_config` bailed to
    /// `with_builtins()` unless `[experimental] module_registry = true`,
    /// which no preset sets, so this block resolved, was golden-tested,
    /// and then had no effect on the tool surface at all.
    ///
    /// "Advertised" is `Config::tool_enabled` (the filter
    /// `Agent::tool_schemas` applies to the registry), not the schema
    /// array itself: cc-parity also turns `deferred_tools` on, so a
    /// non-core tool legitimately reaches the model through `tool_search`
    /// rather than the eager array — that deferral is CC's own behavior
    /// (cc§7 "Tool search"), not a gap.
    #[test]
    fn cc_parity_registers_and_advertises_the_web_tools() {
        let r = resolved("cc-parity");
        assert!(
            r.config.module_registry,
            "a resolved preset must drive the module registry with no experimental flag"
        );
        let names = registry_names(&r.config);
        for tool in ["web_fetch", "web_search"] {
            assert!(
                names.contains(&tool.to_string()),
                "cc-parity must register `{tool}`; registry = {names:?}"
            );
            assert!(
                r.config.tool_enabled(tool),
                "cc-parity must advertise `{tool}`"
            );
        }
    }

    /// BP-1 AC1 (cc side, MCP). `mcp_module_on` in the CLI is exactly
    /// "is `ModuleId::McpClient` in the resolved activation set" now, and
    /// that predicate is what gates MCP resource-tool registration,
    /// prompts-as-commands, server instructions, and the http/sse
    /// transports in `attach_mcp`. Under cc-parity it must be true.
    #[test]
    fn cc_parity_activates_the_mcp_client_module() {
        let r = resolved("cc-parity");
        assert!(r.config.module_activation.is_active(ModuleId::McpClient));
        assert_eq!(r.modules.get("mcp"), Some(&true));
    }

    /// BP-1 AC1 (cx side). `cx-parity`'s `[core.tools] enabled = ["bash",
    /// "view_image"]` is the preset's statement that Codex has no
    /// read/write/edit function tools at all (reads go through `cat`,
    /// writes through `apply_patch`) — and it must now be the registry's
    /// statement too.
    #[test]
    fn cx_parity_registers_view_image_and_no_file_tools() {
        let r = resolved("cx-parity");
        let names = registry_names(&r.config);
        assert!(
            names.contains(&"view_image".to_string()),
            "cx-parity must register `view_image` (its only image pathway); registry = {names:?}"
        );
        for tool in ["read_file", "write_file", "edit_file"] {
            assert!(
                !names.contains(&tool.to_string()),
                "cx-parity must NOT register `{tool}`; registry = {names:?}"
            );
        }
        // The write path Codex actually uses is still there.
        assert!(names.contains(&"apply_patch".to_string()));
        assert!(names.contains(&"bash".to_string()));
    }

    /// BP-3: the tool surface `cc-parity` claims, checked against the
    /// registry the RESOLVED preset actually builds — the same predicate
    /// `supercode harness parity`'s `tool` evidence resolves through.
    /// `enter_plan_mode`/`exit_plan_mode` come from
    /// `[capabilities.plan_mode]`, `ask_user` from
    /// `[capabilities.tools_question]`, `current_time`/`sleep` from
    /// `[core.tools] enabled`.
    #[test]
    fn cc_parity_registers_the_new_core_tools() {
        let r = resolved("cc-parity");
        let names = registry_names(&r.config);
        for tool in [
            "ask_user",
            "enter_plan_mode",
            "exit_plan_mode",
            "current_time",
            "sleep",
        ] {
            assert!(
                names.contains(&tool.to_string()),
                "cc-parity must register `{tool}`; registry = {names:?}"
            );
            assert!(
                r.config.tool_enabled(tool),
                "cc-parity must advertise `{tool}`"
            );
        }
        // Codex-only rows stay out of the CC surface (the catalog scores
        // both `—` for cc), and so does cx's own question spelling.
        for tool in [
            "request_user_input",
            "image_gen",
            "new_context",
            "get_context_remaining",
        ] {
            assert!(
                !names.contains(&tool.to_string()),
                "cc-parity must NOT register `{tool}`; registry = {names:?}"
            );
        }
    }

    /// BP-3: the same check for `cx-parity`, including Codex's own
    /// `request_user_input` spelling registered ALONGSIDE `ask_user` (one
    /// tool object, two registered names) and the deliberate absence of the
    /// plan-mode tools — cx's `/plan` is user-driven steering, so the
    /// module stays off there and only the mode itself is available.
    #[test]
    fn cx_parity_registers_the_new_core_tools_including_the_codex_spelling() {
        let r = resolved("cx-parity");
        let names = registry_names(&r.config);
        for tool in [
            "ask_user",
            "request_user_input",
            "current_time",
            "sleep",
            "get_context_remaining",
            "new_context",
            "image_gen",
        ] {
            assert!(
                names.contains(&tool.to_string()),
                "cx-parity must register `{tool}`; registry = {names:?}"
            );
            assert!(
                r.config.tool_enabled(tool),
                "cx-parity must advertise `{tool}`"
            );
        }
        for tool in ["enter_plan_mode", "exit_plan_mode"] {
            assert!(
                !names.contains(&tool.to_string()),
                "cx-parity keeps `[capabilities.plan_mode]` off, so `{tool}` must not be \
                 registered; registry = {names:?}"
            );
        }
    }

    /// BP-3: the new built-ins are advertised EAGERLY under both presets'
    /// `deferred_tools` — a question tool the model must first `tool_search`
    /// for is not the capability CC/CX ship.
    #[test]
    fn the_new_core_tools_are_eager_under_both_parity_presets() {
        for (preset, tools) in [
            (
                "cc-parity",
                &[
                    "ask_user",
                    "enter_plan_mode",
                    "exit_plan_mode",
                    "current_time",
                    "sleep",
                ][..],
            ),
            (
                "cx-parity",
                &[
                    "ask_user",
                    "request_user_input",
                    "current_time",
                    "sleep",
                    "get_context_remaining",
                    "new_context",
                    "image_gen",
                ][..],
            ),
        ] {
            let r = resolved(preset);
            let crate::config::ToolAdvertising::Deferred { core } = &r.config.tool_advertising
            else {
                panic!("{preset} enables `deferred_tools`, so advertising must be Deferred");
            };
            for tool in tools {
                assert!(
                    core.iter().any(|c| c == tool),
                    "{preset} must advertise `{tool}` eagerly; core = {core:?}"
                );
            }
        }
    }

    /// BP-3 (§2 module 6): the question tool, driven end to end over the
    /// RESOLVED preset — resolve, build the registry, call the tool, and
    /// let a mock frontend answer it. This is the behaviour the ledger row
    /// claims; registration alone would not be.
    #[tokio::test]
    async fn cc_parity_ask_user_is_answered_by_a_mock_frontend() {
        struct Frontend;
        #[async_trait::async_trait]
        impl crate::mcp::McpElicitationHandler for Frontend {
            async fn handle(
                &self,
                request: &crate::mcp::ElicitationRequest,
            ) -> crate::mcp::ElicitationResponse {
                // The frontend sees the real question text and the answer
                // schema, exactly as it would off the broker.
                assert!(request.message.contains("Which database?"), "{request:?}");
                assert_eq!(
                    request.requested_schema["properties"]["q1"]["type"],
                    "string"
                );
                crate::mcp::ElicitationResponse {
                    action: crate::mcp::ElicitationAction::Accept,
                    content: Some(serde_json::json!({"q1": "sqlite"})),
                }
            }
        }

        let r = resolved("cc-parity");
        let registry = ToolRegistry::from_config(&r.config);
        let tool = registry.get("ask_user").expect("cc-parity registers it");
        let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());

        // Deny-default first: with no frontend attached the tool refuses
        // rather than hanging.
        let args = serde_json::json!({"questions": [{
            "question": "Which database?",
            "header": "Database",
            "options": [{"label": "postgres"}, {"label": "sqlite"}]
        }]});
        let refused = tool.execute(args.clone(), &ctx).await;
        assert!(
            refused.is_err(),
            "headless must be deny-default, got {refused:?}"
        );

        ctx.question_handler = Some(crate::tools::UserQuestionHandler(std::sync::Arc::new(
            Frontend,
        )));
        let answer = tool
            .execute(args, &ctx)
            .await
            .expect("the frontend answers");
        assert!(answer.contains("Database: sqlite"), "{answer}");
    }

    /// BP-3 (§2 module 8): `exit_plan_mode` over the RESOLVED `cc-parity`
    /// preset — the plan is presented on the session's approval door, a
    /// refusal keeps the mode on, and only an approval clears it.
    #[tokio::test]
    async fn cc_parity_plan_exit_needs_the_approval_door() {
        struct Door(std::sync::atomic::AtomicBool);
        impl crate::permissions::PermissionsApprovalHandler for Door {
            fn ask(
                &self,
                req: &crate::permissions::ApprovalRequest,
            ) -> crate::permissions::ApprovalOutcome {
                assert_eq!(req.tool, "exit_plan_mode");
                assert!(
                    req.subject
                        .is_some_and(|s| s.contains("rewrite the parser")),
                    "the approval must carry the plan: {:?}",
                    req.subject
                );
                // Refuse the first time, approve the second.
                if self.0.swap(true, std::sync::atomic::Ordering::SeqCst) {
                    crate::permissions::ApprovalOutcome::Allow
                } else {
                    crate::permissions::ApprovalOutcome::Deny
                }
            }
        }

        let r = resolved("cc-parity");
        let registry = ToolRegistry::from_config(&r.config);
        let enter = registry.get("enter_plan_mode").expect("registered");
        let exit = registry.get("exit_plan_mode").expect("registered");
        let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());
        ctx.approval_handler = Some(crate::tools::ToolApprovalHandler(std::sync::Arc::new(
            Door(std::sync::atomic::AtomicBool::new(false)),
        )));

        enter
            .execute(serde_json::json!({}), &ctx)
            .await
            .expect("entering plan mode");
        assert!(ctx.plan_mode.is_active());

        let refused = exit
            .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
            .await
            .expect("a refusal is a result, not an error");
        assert!(refused.contains("did NOT approve"), "{refused}");
        assert!(
            ctx.plan_mode.is_active(),
            "a refused exit must keep the mode on"
        );

        let approved = exit
            .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
            .await
            .expect("approved exit");
        assert!(approved.contains("APPROVED"), "{approved}");
        assert!(!ctx.plan_mode.is_active());
    }

    /// BP-1 AC2. `[core.compaction] summarize` was parsed into
    /// `CoreCompactionConfig` and dropped; it now reaches `Config`. Every
    /// preset sets it (directly or by inheriting `pi-core`).
    #[test]
    fn compaction_summarize_reaches_config_for_every_preset() {
        for name in RESERVED_PRESET_NAMES {
            let r = resolved(name);
            assert!(
                r.config.compaction_summarize,
                "preset `{name}` sets `core.compaction.summarize = true`; it must reach Config"
            );
        }
    }

    /// BP-1 AC2 (cx trigger). `cx-parity` armed NEITHER compaction trigger,
    /// so `Agent::maybe_compact` returned false on its
    /// `threshold.is_none() && compaction_reserve_tokens.is_none()` guard
    /// and the preset could never compact. The design's own comment on that
    /// block names an auto-compact TOKEN limit, i.e. §1.5's pressure
    /// trigger.
    #[test]
    fn cx_parity_arms_the_compaction_pressure_trigger() {
        let r = resolved("cx-parity");
        assert!(r.config.compaction_enabled);
        assert_eq!(r.config.compaction_reserve_tokens, Some(16384));
    }

    /// The explicit OPT-OUT survives: `[experimental] module_registry =
    /// false` pins a resolved config back to the unfiltered
    /// `with_builtins()` stack.
    #[test]
    fn module_registry_false_is_an_explicit_opt_out() {
        let toml = format!("{CC_PARITY_TOML}\n[experimental]\nmodule_registry = false\n");
        let r = resolve(&toml, None, &ResolveOptions::default()).expect("resolves");
        assert!(!r.config.module_registry);
        let names = registry_names(&r.config);
        let builtins: Vec<String> = ToolRegistry::with_builtins()
            .iter()
            .map(|t| t.name().to_string())
            .collect();
        assert_eq!(names, builtins);
    }

    /// `pi-core` has no `extends` (it is a root); the other five all resolve
    /// somewhere (four are roots too, `token-saver`/`supercode-default`
    /// extend `pi-core`) — sanity-checking the chain shape golden tests will
    /// exercise in full.
    #[test]
    fn token_saver_and_supercode_default_extend_pi_core() {
        let ts = HarnessConfig::from_toml_str(TOKEN_SAVER_TOML).unwrap();
        assert_eq!(ts.extends.as_deref(), Some("pi-core"));
        let sd = HarnessConfig::from_toml_str(SUPERCODE_DEFAULT_TOML).unwrap();
        assert_eq!(sd.extends.as_deref(), Some("pi-core"));
        let pc = HarnessConfig::from_toml_str(PI_CORE_TOML).unwrap();
        assert_eq!(pc.extends, None);
    }

    // ---- BP-2: tool fidelity under the resolved parity presets ----------

    /// BP-2 helper: the tool context an `Agent` built from `config` would
    /// hand every tool call, rooted at `cwd` — the resolved preset's own
    /// `[core.tools.*]` knobs, not a hand-assembled context.
    fn preset_ctx(config: &crate::Config) -> crate::tools::ToolContext {
        let (ctx, _, _) = crate::agent::build_tool_context(config);
        ctx
    }

    fn tmp_dir(tag: &str) -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!(
            "supercode-bp2-{tag}-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0)
        ));
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    async fn run_tool(
        config: &crate::Config,
        tool: &str,
        args: serde_json::Value,
    ) -> crate::error::Result<String> {
        let registry = ToolRegistry::from_config(config);
        let t = registry
            .get(tool)
            .unwrap_or_else(|| panic!("`{tool}` is not registered under this preset"));
        t.execute(args, &preset_ctx(config)).await
    }

    /// BP-2 row `file-read-tool-paged-line-numbered` (cc). Claude Code's
    /// Read returns a `cat -n` gutter and its model cites those numbers;
    /// supercode's `read_file` returned the raw slice. Under the RESOLVED
    /// cc-parity preset it must now number, and number from `offset`.
    #[tokio::test]
    async fn cc_parity_read_file_numbers_lines_cat_n_style() {
        let mut r = resolved("cc-parity");
        assert!(
            r.config.read_file_line_numbers,
            "cc-parity must set `[core.tools.read_file] line_numbers`"
        );
        let dir = tmp_dir("readnum");
        std::fs::write(dir.join("sample.txt"), "alpha\nbeta\ngamma\n").unwrap();
        r.config.cwd = dir.clone();

        let whole = run_tool(
            &r.config,
            "read_file",
            serde_json::json!({"path": "sample.txt"}),
        )
        .await
        .unwrap();
        assert_eq!(
            whole, "     1\talpha\n     2\tbeta\n     3\tgamma\n",
            "cc-parity read_file must emit a `cat -n` gutter"
        );

        let sliced = run_tool(
            &r.config,
            "read_file",
            serde_json::json!({"path": "sample.txt", "offset": 2, "limit": 2}),
        )
        .await
        .unwrap();
        assert_eq!(
            sliced, "     2\tbeta\n     3\tgamma",
            "an `offset` read must number from the offset, not from 1"
        );
    }

    /// BP-2 (same row, the other direction): pi-core keeps the unnumbered
    /// raw slice — the gutter is Claude Code's behavior, and a preset that
    /// does not claim it must not silently acquire it.
    #[tokio::test]
    async fn pi_core_read_file_keeps_the_raw_unnumbered_slice() {
        let mut r = resolved("pi-core");
        assert!(!r.config.read_file_line_numbers);
        let dir = tmp_dir("readraw");
        std::fs::write(dir.join("sample.txt"), "alpha\nbeta\n").unwrap();
        r.config.cwd = dir.clone();
        let out = run_tool(
            &r.config,
            "read_file",
            serde_json::json!({"path": "sample.txt"}),
        )
        .await
        .unwrap();
        assert_eq!(out, "alpha\nbeta\n");
    }

    /// BP-2 row `multimodal-read-images-pdf-notebook` (cc). The multimodal
    /// branch used to be images-only, so a PDF or a notebook came back as
    /// UTF-8-lossy soup. Under the resolved cc-parity preset a PDF returns
    /// its extracted text pages and an `.ipynb` returns its cells WITH
    /// their outputs.
    #[tokio::test]
    async fn cc_parity_read_file_renders_pdf_text_and_notebook_cells() {
        let mut r = resolved("cc-parity");
        assert!(r.config.read_file_multimodal);
        let dir = tmp_dir("multimodal");
        r.config.cwd = dir.clone();

        std::fs::write(
            dir.join("doc.pdf"),
            b"%PDF-1.4\n1 0 obj\n<< /Type /Page >>\nendobj\n2 0 obj\n<< >>\nstream\nBT (parity page text) Tj ET\nendstream\nendobj\n%%EOF\n"
                .as_slice(),
        )
        .unwrap();
        let pdf = run_tool(
            &r.config,
            "read_file",
            serde_json::json!({"path": "doc.pdf"}),
        )
        .await
        .unwrap();
        assert!(pdf.contains("--- page 1 ---"), "{pdf}");
        assert!(pdf.contains("parity page text"), "{pdf}");

        let nb = serde_json::json!({
            "metadata": {"kernelspec": {"display_name": "Python 3"}},
            "cells": [{
                "cell_type": "code", "execution_count": 2, "source": ["print('hi')\n"],
                "outputs": [{"output_type": "stream", "name": "stdout", "text": ["hi\n"]}]
            }]
        });
        std::fs::write(dir.join("nb.ipynb"), nb.to_string()).unwrap();
        let out = run_tool(
            &r.config,
            "read_file",
            serde_json::json!({"path": "nb.ipynb"}),
        )
        .await
        .unwrap();
        assert!(out.contains("--- cell 0 (code) [2] ---"), "{out}");
        assert!(out.contains("print('hi')"), "{out}");
        assert!(out.contains("[stdout]\nhi"), "{out}");
    }

    /// BP-2 row `parallel-tool-call-execution`. The concurrent batch path
    /// existed but neither parity preset armed it, so sibling calls always
    /// ran sequentially under cc-parity/cx-parity — both harnesses run
    /// them concurrently. pi-core keeps the sequential path.
    #[test]
    fn both_parity_presets_arm_parallel_tool_calls() {
        for name in ["cc-parity", "cx-parity"] {
            assert!(
                resolved(name).config.parallel_tool_calls,
                "`{name}` must set `core.parallel_tool_calls`"
            );
        }
        assert!(
            !resolved("pi-core").config.parallel_tool_calls,
            "pi-core keeps the sequential path"
        );
    }

    /// BP-2 row `read-before-edit-enforcement` (cc). The refusal used to be
    /// path-only — a file read once and then modified behind the model's
    /// back still edited cleanly. Under the resolved cc-parity preset the
    /// "and unchanged" half must hold, and a Bash view must satisfy the
    /// rule the way CC's does.
    ///
    /// Driven through ONE `ToolContext`, the way an `Agent` does: the read
    /// record lives on the context, so a per-call context would be testing
    /// nothing.
    #[tokio::test]
    async fn cc_parity_edit_refuses_unread_and_stale_files_and_accepts_a_bash_view() {
        let mut r = resolved("cc-parity");
        assert!(r.config.edit_file_require_read_before_edit);
        let dir = tmp_dir("staleedit");
        r.config.cwd = dir.clone();
        let path = dir.join("code.txt");
        std::fs::write(&path, "alpha\n").unwrap();

        let ctx = preset_ctx(&r.config);
        let registry = ToolRegistry::from_config(&r.config);
        let read = registry.get("read_file").unwrap();
        let edit = registry.get("edit_file").unwrap();
        let bash = registry.get("bash").unwrap();
        let edit_call = |old: &str, new: &str| serde_json::json!({"path": "code.txt", "old_string": old, "new_string": new});

        // 1. never read → refused, naming the read requirement.
        let never = edit
            .execute(edit_call("alpha", "beta"), &ctx)
            .await
            .expect_err("an unread file must be refused");
        assert!(
            never.to_string().contains("must be read with `read_file`"),
            "{never}"
        );

        // 2. read, then edited → accepted; and the model's own second edit
        //    of the file it just wrote is still accepted (its view is the
        //    bytes it wrote, not a stale one).
        read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
            .await
            .unwrap();
        edit.execute(edit_call("alpha", "beta"), &ctx)
            .await
            .expect("a read file edits");
        edit.execute(edit_call("beta", "gamma"), &ctx)
            .await
            .expect("the model's own consecutive edit is not stale");

        // 3. changed on disk behind the model's back → refused as stale.
        std::fs::write(&path, "one\ntwo\n").unwrap();
        read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
            .await
            .unwrap();
        std::fs::write(&path, "one\ntwo\nthree (someone else)\n").unwrap();
        let stale = edit
            .execute(edit_call("one", "1"), &ctx)
            .await
            .expect_err("an edit against a changed file must be refused");
        assert!(
            stale
                .to_string()
                .contains("has changed on disk since it was read"),
            "{stale}"
        );

        // 4. a single-file Bash view satisfies the rule (CC's exemption).
        bash.execute(serde_json::json!({"command": "cat code.txt"}), &ctx)
            .await
            .unwrap();
        edit.execute(edit_call("two", "2"), &ctx)
            .await
            .expect("a `cat` view satisfies read-before-edit");
    }

    /// BP-2: the Bash-view exemption is exactly the inventory's narrow rule
    /// — a composed or transformed view is NOT a view of the file.
    #[test]
    fn bash_view_exemption_is_narrow() {
        use crate::tools::bash_view_target;
        assert_eq!(
            bash_view_target("cat src/lib.rs").as_deref(),
            Some("src/lib.rs")
        );
        assert_eq!(bash_view_target("/bin/cat x.txt").as_deref(), Some("x.txt"));
        assert_eq!(
            bash_view_target("head -n 20 x.txt").as_deref(),
            Some("x.txt")
        );
        assert_eq!(
            bash_view_target("sed -n '1,5p' x.txt").as_deref(),
            Some("x.txt")
        );
        assert_eq!(bash_view_target("grep foo x.txt").as_deref(), Some("x.txt"));
        // Not exemptions: pipes, redirects, composition, multi-file,
        // a non-viewer, and `sed` without `-n` (which prints edited output).
        assert_eq!(bash_view_target("cat x.txt | head -5"), None);
        assert_eq!(bash_view_target("cat x.txt > y.txt"), None);
        assert_eq!(bash_view_target("cat x.txt; rm x.txt"), None);
        assert_eq!(bash_view_target("cat a.txt b.txt"), None);
        assert_eq!(bash_view_target("echo hi"), None);
        assert_eq!(bash_view_target("sed 's/a/b/' x.txt"), None);
        assert_eq!(bash_view_target("cat $(ls)"), None);
    }

    /// BP-2: a one-shot local HTTP server (the same offline pattern the
    /// P4c web tests use) — no test in this crate touches the real network.
    async fn one_shot_http(body: &str, content_type: &str) -> std::net::SocketAddr {
        use tokio::io::{AsyncReadExt, AsyncWriteExt};
        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
            body.len()
        );
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move {
            if let Ok((mut sock, _)) = listener.accept().await {
                let mut buf = [0u8; 4096];
                let _ = sock.read(&mut buf).await;
                let _ = sock.write_all(response.as_bytes()).await;
                let _ = sock.flush().await;
            }
        });
        addr
    }

    /// Serializes the two tests that set process-wide web env vars.
    static WEB_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// BP-2 row `web-fetch-tool` (cc). The tool returned the raw response
    /// body with no markdown conversion and no cache, where CC's WebFetch
    /// does both. Under the resolved cc-parity preset a fetched HTML page
    /// must come back as markdown, and a second fetch of the same URL must
    /// be served from the on-disk cache — proven by the server being
    /// one-shot: a live second fetch could not succeed.
    #[tokio::test]
    async fn cc_parity_web_fetch_converts_html_to_markdown_and_caches_it() {
        let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let cache = tmp_dir("webcache");
        std::env::set_var(crate::tools::WEB_CACHE_DIR_ENV, &cache);

        let html = "<html><head><title>t</title><style>b{}</style></head><body>\
            <h1>Parity</h1><p>Fetched <strong>page</strong>.</p>\
            <a href=\"https://example.com/next\">next</a></body></html>";
        let addr = one_shot_http(html, "text/html; charset=utf-8").await;
        let url = format!("http://127.0.0.1:{}/page", addr.port());

        let r = resolved("cc-parity");
        let first = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
            .await
            .unwrap();
        assert!(first.contains("markdown"), "{first}");
        assert!(first.contains("# Parity"), "{first}");
        assert!(first.contains("Fetched **page**."), "{first}");
        assert!(
            first.contains("[next](https://example.com/next)"),
            "{first}"
        );
        assert!(
            !first.contains("<h1>"),
            "raw markup reached the model: {first}"
        );

        // The one-shot server is finished; only the cache can answer.
        let second = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
            .await
            .expect("the second fetch must be served from the cache");
        assert!(second.starts_with("[web_fetch: cached "), "{second}");
        assert!(second.contains("# Parity"), "{second}");

        std::env::remove_var(crate::tools::WEB_CACHE_DIR_ENV);
    }

    /// BP-2 row `web-search-tool`. supercode bundled no backend at all: with
    /// no operator URL the tool returned a configuration error, where CC and
    /// Codex both search out of the box. The default endpoint is now a
    /// documented public one, the operator override still wins, and a
    /// results page is rendered as titles/urls/snippets rather than dumped.
    ///
    /// Offline: the override points at a local one-shot server serving a
    /// results page in the shape the default backend returns.
    #[tokio::test]
    async fn parity_presets_web_search_renders_results_from_its_backend() {
        let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        assert_eq!(
            crate::tools::DEFAULT_WEB_SEARCH_URL,
            "https://html.duckduckgo.com/html/",
            "the built-in backend must need no operator configuration"
        );
        let page = "<html><body><div class=\"result\">\
            <a class=\"result__a\" href=\"//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fdoc\">Example doc</a>\
            <a class=\"result__snippet\">What the page says.</a></div></body></html>";
        let addr = one_shot_http(page, "text/html").await;
        std::env::set_var(
            crate::tools::WEB_SEARCH_URL_ENV,
            format!("http://127.0.0.1:{}/search", addr.port()),
        );
        let r = resolved("cx-parity");
        let out = run_tool(
            &r.config,
            "web_search",
            serde_json::json!({"query": "example"}),
        )
        .await
        .unwrap();
        std::env::remove_var(crate::tools::WEB_SEARCH_URL_ENV);
        assert!(out.contains("1 results"), "{out}");
        assert!(
            out.contains("1. Example doc — https://example.com/doc"),
            "{out}"
        );
        assert!(out.contains("What the page says."), "{out}");
    }
}