@narumitw/pi-sync 0.54.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +40 -15
  2. package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
  3. package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
  4. package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
  5. package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
  6. package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
  7. package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
  8. package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
  9. package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
  10. package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
  11. package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
  12. package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
  13. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
  14. package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
  15. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
  16. package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
  17. package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
  18. package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
  19. package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
  20. package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
  21. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
  22. package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
  23. package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
  24. package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
  25. package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
  26. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
  27. package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
  28. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
  29. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
  30. package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
  31. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
  32. package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
  33. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
  34. package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
  35. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
  36. package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
  37. package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
  38. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
  39. package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
  40. package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
  41. package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
  42. package/dist/chunks/chunk-QGK445K7.ts +21 -0
  43. package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
  44. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
  45. package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
  46. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
  47. package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
  48. package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
  49. package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
  50. package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
  51. package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
  52. package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
  53. package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
  54. package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
  55. package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
  56. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
  57. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  58. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  59. package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
  60. package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
  61. package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
  62. package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
  63. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
  64. package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
  65. package/dist/chunks/config-KGIYTTXQ.ts +37 -0
  66. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
  67. package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
  68. package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
  69. package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
  70. package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
  71. package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
  72. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
  73. package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
  74. package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
  75. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
  76. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
  77. package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
  78. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
  79. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
  80. package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
  81. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
  82. package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
  83. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
  84. package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
  85. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
  86. package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
  87. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
  88. package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
  89. package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
  90. package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
  91. package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
  92. package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
  93. package/dist/index.ts +246 -131
  94. package/dist/index.ts.map +3 -3
  95. package/docs/agent-mode-verification.md +99 -0
  96. package/docs/continuous-sync-verification.md +87 -0
  97. package/docs/settings.md +131 -11
  98. package/docs/tui-contract.md +61 -0
  99. package/package.json +3 -2
  100. package/src/backends/git/git-backend.ts +1 -4
  101. package/src/backends/git/git-runner.ts +1 -5
  102. package/src/backends/s3/s3-backend.ts +3 -25
  103. package/src/backends/s3/s3-client.ts +14 -2
  104. package/src/backends/snapshot-pointer.ts +27 -0
  105. package/src/backends/webdav/webdav-backend.ts +3 -29
  106. package/src/backends/webdav/webdav-client.ts +11 -7
  107. package/src/commands/command-execution.ts +7 -3
  108. package/src/commands/command-handler.ts +15 -3
  109. package/src/commands/command-types.ts +10 -0
  110. package/src/commands/command.ts +4 -1
  111. package/src/settings/config.ts +20 -10
  112. package/src/settings/settings-store.ts +1 -34
  113. package/src/settings/settings-types.ts +10 -0
  114. package/src/settings/settings-validation.ts +4 -1
  115. package/src/snapshot/snapshot-codec.ts +1 -5
  116. package/src/state/lock.ts +13 -10
  117. package/src/state/merge-baseline-store.ts +19 -1
  118. package/src/state/state-directory.ts +8 -1
  119. package/src/sync/agent-candidate.ts +251 -0
  120. package/src/sync/agent-plan.ts +87 -0
  121. package/src/sync/agent-policy.ts +13 -0
  122. package/src/sync/agent-request.ts +17 -0
  123. package/src/sync/agent-runner.ts +188 -0
  124. package/src/sync/agent-store.ts +151 -0
  125. package/src/sync/agent-sync.ts +91 -0
  126. package/src/sync/apply-review.ts +63 -0
  127. package/src/sync/automatic-sync.ts +46 -33
  128. package/src/sync/continuous-errors.ts +85 -0
  129. package/src/sync/continuous-lease.ts +221 -0
  130. package/src/sync/continuous-record.ts +179 -0
  131. package/src/sync/continuous-storage.ts +55 -0
  132. package/src/sync/continuous-sync.ts +516 -0
  133. package/src/sync/continuous-watch.ts +59 -0
  134. package/src/sync/merged-sync.ts +268 -112
  135. package/src/sync/remote-snapshot.ts +2 -3
  136. package/src/sync/settings-merge.ts +19 -3
  137. package/src/sync/setup-switch.ts +1 -5
  138. package/src/sync/startup-check.ts +51 -29
  139. package/src/sync/startup-timings.ts +90 -0
  140. package/src/sync/sync-inspection.ts +16 -5
  141. package/src/sync/sync-mutations.ts +55 -28
  142. package/src/sync/sync-queries.ts +19 -3
  143. package/src/sync/text-merge.ts +3 -3
  144. package/src/sync-extension.ts +75 -19
  145. package/src/ui/agent-mode-setting.ts +91 -0
  146. package/src/ui/manager-state.ts +12 -6
  147. package/src/ui/manager-ui.ts +57 -7
  148. package/src/ui/merge-review.ts +3 -3
  149. package/src/ui/merged-sync-summary.ts +2 -18
  150. package/src/ui/remote-selection-ui.ts +5 -5
  151. package/src/ui/settings-frame.ts +55 -0
  152. package/src/ui/settings-ui.ts +223 -43
  153. package/src/ui/setup/s3-credentials-ui.ts +1 -5
  154. package/src/ui/setup/setup-name-ui.ts +2 -5
  155. package/src/ui/setup/setup-prompts.ts +4 -3
  156. package/src/ui/setup/setup-switcher.ts +2 -0
  157. package/src/ui/sync-behavior.ts +15 -0
  158. package/src/ui/sync-format.ts +69 -38
  159. package/src/ui/sync-setups-ui.ts +3 -5
  160. package/src/ui/transfer-changes.ts +33 -0
  161. package/src/ui/transfer-review.ts +27 -0
  162. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  163. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  164. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  165. package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
  166. package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
  167. package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
  168. package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
  169. package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
  170. package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
  171. package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
  172. package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
  173. package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
  174. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  175. package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
  176. package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
  177. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  178. package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
  179. package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
  180. package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
  181. package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
  182. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
  183. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
  184. /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
  185. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
  186. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
  187. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
  188. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
  189. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
  190. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
  191. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
  192. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
  193. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
  194. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
@@ -1,16 +1,29 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
- clearMergeJournal,
4
+ confirmMergeReview
5
+ } from "./chunk-5XZOC7FD.ts";
6
+ import {
7
+ formatRemoteSelectionStatus,
8
+ readSnapshotForHead,
9
+ requireCompatibleRemoteSelection
10
+ } from "./chunk-YK52VDXG.ts";
11
+ import {
12
+ createSyncBackend
13
+ } from "./chunk-OEWQVWLC.ts";
14
+ import {
5
15
  conflictArtifactFingerprint,
6
16
  conflictDirectory,
7
17
  conflictGroups,
8
- mergeJournalIdentity,
9
18
  readConflictArtifact,
19
+ saveConflictArtifact
20
+ } from "./chunk-ED3TYIVW.ts";
21
+ import {
22
+ clearMergeJournal,
23
+ mergeJournalIdentity,
10
24
  readMergeJournal,
11
- saveConflictArtifact,
12
25
  writeMergeJournal
13
- } from "./chunk-ORQRHEEO.ts";
26
+ } from "./chunk-GNH73BJ5.ts";
14
27
  import {
15
28
  fileHashMap,
16
29
  hasLocalChanges,
@@ -18,65 +31,44 @@ import {
18
31
  sameHashes,
19
32
  syncPolicyChanged
20
33
  } from "./chunk-BDB4IDM3.ts";
21
- import {
22
- formatRemoteSelectionStatus,
23
- readSnapshotForHead,
24
- requireCompatibleRemoteSelection
25
- } from "./chunk-V6VZ5TLG.ts";
26
- import {
27
- createSyncBackend
28
- } from "./chunk-SDZCTCYQ.ts";
29
34
  import {
30
35
  applySnapshotTransaction,
31
36
  recoverPendingSnapshotTransactions,
32
37
  syncMutationParents
33
- } from "./chunk-CL7BZRRQ.ts";
38
+ } from "./chunk-CRKTAPSL.ts";
34
39
  import {
35
40
  backupLocal,
36
41
  captureMutationOwner,
37
42
  protectedSessionPaths
38
- } from "./chunk-IJ6VV6FN.ts";
43
+ } from "./chunk-UOHR7LRO.ts";
39
44
  import {
40
45
  safeTerminalText,
41
46
  snapshotPathLabel
42
47
  } from "./chunk-AMJRUXFT.ts";
43
48
  import {
44
- TEXT_MERGE_WORK_LIMIT,
45
- isMergeTextPath,
46
- mergeSession,
47
- mergeText,
48
- pruneMergeBaselines,
49
- readMergeAncestor,
50
- readMergeAncestors,
51
- stageMergeBaseline
52
- } from "./chunk-QE7NZZWT.ts";
49
+ createStartupTimings
50
+ } from "./chunk-UZPMDLTL.ts";
53
51
  import {
54
- mergePathIdentity,
55
- planFileMerge
56
- } from "./chunk-76NB4R7U.ts";
57
- import {
58
- readStateForConfig,
59
- statePathForConfig,
60
- syncStateFingerprint,
61
- writeStateForConfig
62
- } from "./chunk-4QVPTIZB.ts";
52
+ SyncDecisionRequiredError
53
+ } from "./chunk-7D5SF7P3.ts";
63
54
  import {
64
55
  loadConfig,
65
56
  syncCheckConfigFingerprint,
66
57
  syncConfigReviewIdentity
67
- } from "./chunk-3B4Z2SKV.ts";
68
- import {
69
- SyncBackendConflictError,
70
- expectedRemoteHead
71
- } from "./chunk-YDVQYUHE.ts";
72
- import {
73
- SyncDecisionRequiredError
74
- } from "./chunk-7D5SF7P3.ts";
58
+ } from "./chunk-QO333NM4.ts";
75
59
  import {
76
60
  overlayLocalFields,
77
61
  portableSnapshot,
78
62
  sameLocalFields
79
63
  } from "./chunk-A6HU4ERM.ts";
64
+ import {
65
+ SyncBackendConflictError,
66
+ expectedRemoteHead
67
+ } from "./chunk-YDVQYUHE.ts";
68
+ import {
69
+ mergeSettingsJson,
70
+ resolveContentConflicts
71
+ } from "./chunk-5Q5SSKNG.ts";
80
72
  import {
81
73
  agentDir,
82
74
  createSnapshot,
@@ -96,10 +88,26 @@ import {
96
88
  snapshotTarget
97
89
  } from "./chunk-QOFMXPM5.ts";
98
90
  import {
99
- jsonEqual,
100
- parseSettingsDocument,
101
- renderSettingsMembers
102
- } from "./chunk-ZW5ZLGBI.ts";
91
+ TEXT_MERGE_WORK_LIMIT,
92
+ hasCurrentMergeBaseline,
93
+ pruneMergeBaselines,
94
+ readMergeAncestor,
95
+ readMergeAncestors,
96
+ stageMergeBaseline
97
+ } from "./chunk-IWZH5KYS.ts";
98
+ import {
99
+ mergePathIdentity,
100
+ planFileMerge
101
+ } from "./chunk-76NB4R7U.ts";
102
+ import {
103
+ startupTransferEnabled
104
+ } from "./chunk-PRUEIIK7.ts";
105
+ import {
106
+ readStateForConfig,
107
+ statePathForConfig,
108
+ syncStateFingerprint,
109
+ writeStateForConfig
110
+ } from "./chunk-UPNVIUBN.ts";
103
111
  import {
104
112
  assertWithinRoot,
105
113
  isDeniedPath,
@@ -110,216 +118,32 @@ import {
110
118
  toPosix
111
119
  } from "./chunk-HUZ5CR4W.ts";
112
120
 
113
- // src/ui/merge-review.ts
114
- import { truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
115
- import { defineMenu, runCustomInteraction, runDocumentReview, runMenu } from "@narumitw/pi-tui-kit";
121
+ // src/ui/merged-sync-summary.ts
122
+ import { truncateToWidth } from "@earendil-works/pi-tui";
116
123
 
117
- // src/ui/sync-confirmation.ts
118
- import { Key, matchesKey, SelectList, truncateToWidth } from "@earendil-works/pi-tui";
119
- import { HorizontalRule, hardWrapTerminalDocument, sanitizeTerminalText } from "@narumitw/pi-tui-kit";
120
- function createSyncConfirmation(options) {
121
- const { tui, theme, keybindings } = options;
122
- const choices = ["cancel", "approve", "details"];
123
- const list = new SelectList(
124
- [
125
- { value: "cancel", label: "No, cancel" },
126
- { value: "approve", label: `Yes, ${sanitizeTerminalText(options.confirmationLabel)}` },
127
- { value: "details", label: "View details" }
128
- ],
129
- 3,
130
- {
131
- selectedPrefix: (text) => theme.fg("accent", text),
132
- selectedText: (text) => theme.fg("accent", text),
133
- description: (text) => theme.fg("muted", text),
134
- scrollInfo: (text) => theme.fg("dim", text),
135
- noMatch: (text) => theme.fg("muted", text)
136
- }
137
- );
138
- const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
139
- let selected = 0;
140
- let offset = 0;
141
- let maximumOffset = 0;
142
- let lastWidth = 0;
143
- let lastRows = 0;
144
- let seen = /* @__PURE__ */ new Set();
145
- let summary = [];
146
- let reviewed = false;
147
- let disposed = false;
148
- let finished = false;
149
- let summaryStart = 0;
150
- let summaryEnd = 0;
151
- let actionsStart = 0;
152
- const finish = (choice) => {
153
- if (disposed || finished) return;
154
- finished = true;
155
- options.complete(choice);
156
- };
157
- const select = (index) => {
158
- selected = (index + choices.length) % choices.length;
159
- list.setSelectedIndex(selected);
160
- };
161
- list.onSelectionChange = (item) => {
162
- selected = Math.max(0, choices.indexOf(item.value));
163
- };
164
- list.onSelect = (item) => {
165
- const choice = choices[selected] ?? "cancel";
166
- if (item.value === choice && (choice !== "approve" || reviewed)) finish(choice);
167
- };
168
- const component = {
169
- render(width, expose = true) {
170
- if (!Number.isFinite(width) || width <= 0) {
171
- reviewed = false;
172
- return [];
173
- }
174
- const cells = Math.floor(width);
175
- const rows = Math.max(1, Math.floor(tui.terminal.rows) - 3);
176
- if (cells !== lastWidth || rows !== lastRows) {
177
- summary = options.lines.flatMap((line) => hardWrapTerminalDocument(line, cells));
178
- seen = /* @__PURE__ */ new Set();
179
- offset = 0;
180
- select(0);
181
- lastWidth = cells;
182
- lastRows = rows;
183
- }
184
- const framed = rows >= 9;
185
- const actions = list.render(cells);
186
- const viewport = Math.max(0, rows - actions.length - 3 - (framed ? 2 : 0));
187
- maximumOffset = Math.max(0, summary.length - viewport);
188
- offset = Math.min(offset, maximumOffset);
189
- const visible = summary.slice(offset, offset + viewport);
190
- if (expose) for (let index = offset; index < offset + visible.length; index++) seen.add(index);
191
- reviewed = viewport > 0 && seen.size === summary.length;
192
- const hint = reviewed ? "Select an action." : "Navigation keys scroll the summary before selecting Yes.";
193
- const body = [
194
- theme.fg("accent", theme.bold(hardWrapTerminalDocument(options.title, cells).join(" "))),
195
- ...visible.map((line) => theme.fg("text", line)),
196
- theme.fg(
197
- "muted",
198
- `Summary ${visible.length ? offset + 1 : 0}-${offset + visible.length}/${summary.length}${reviewed ? "" : " \u2014 review before Yes"}`
199
- ),
200
- ...actions,
201
- theme.fg("dim", hint)
202
- ];
203
- summaryStart = Number(framed) + 1;
204
- summaryEnd = summaryStart + visible.length;
205
- actionsStart = summaryEnd + 1;
206
- const frame = framed ? [...rule.render(cells), ...body, ...rule.render(cells)] : body;
207
- return frame.slice(0, rows).map((line) => truncateToWidth(line, cells, ""));
208
- },
209
- invalidate() {
210
- list.invalidate();
211
- rule.invalidate();
212
- },
213
- handleInput(data) {
214
- if (disposed || finished) return;
215
- if (matchesKey(data, Key.ctrl("c")) || keybindings.matches(data, "tui.select.cancel")) {
216
- finish("cancel");
217
- return;
218
- }
219
- component.render(tui.terminal.columns ?? lastWidth, false);
220
- if (keybindings.matches(data, "tui.select.up")) {
221
- if (!reviewed || selected === 0 && offset > 0) offset = Math.max(0, offset - 1);
222
- else select(selected - 1);
223
- } else if (keybindings.matches(data, "tui.select.down")) {
224
- if (!reviewed || selected === 0 && offset < maximumOffset) offset = Math.min(maximumOffset, offset + 1);
225
- else select(selected + 1);
226
- } else if (keybindings.matches(data, "tui.select.confirm")) {
227
- const choice = choices[selected] ?? "cancel";
228
- if (choice !== "approve" || reviewed) finish(choice);
229
- }
230
- tui.requestRender();
231
- },
232
- handleMouse(event) {
233
- if (disposed || finished || event.width !== lastWidth) return void 0;
234
- component.render(event.width, false);
235
- if (event.y >= summaryStart && event.y < summaryEnd && event.type === "wheel" && event.wheelDelta) {
236
- offset = Math.max(0, Math.min(maximumOffset, offset + (event.wheelDelta < 0 ? -1 : 1)));
237
- select(0);
238
- tui.requestRender();
239
- return { handled: true, render: true };
240
- }
241
- if (event.y < actionsStart || event.y >= actionsStart + choices.length || event.y >= lastRows) return void 0;
242
- return list.handleMouse({ ...event, y: event.y - actionsStart, height: choices.length });
243
- },
244
- dispose() {
245
- disposed = true;
246
- seen.clear();
247
- }
248
- };
249
- return component;
124
+ // src/ui/transfer-changes.ts
125
+ function transferChanges(before, after, protectedPaths = /* @__PURE__ */ new Set()) {
126
+ const beforeMap = before ? fileHashMap(before) : {};
127
+ const afterMap = fileHashMap(after);
128
+ return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
129
+ if (protectedPaths.has(filePath)) return [];
130
+ if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
131
+ if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
132
+ return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
133
+ });
250
134
  }
251
-
252
- // src/ui/merge-review.ts
253
- async function confirmMergeReview(ctx, title, content, signal, isCurrent, confirmationLabel = "apply changes", summaryLines) {
254
- const lines = summaryLines ?? [
255
- ...content.split("\n").slice(0, 6).map((line) => truncateToWidth2(safeTerminalText(line), 100)),
256
- "View details for the complete review."
257
- ];
258
- while (isCurrent() && !signal?.aborted) {
259
- const choice = { value: "cancel" };
260
- const menu = defineMenu({
261
- start: "confirm",
262
- screens: {
263
- confirm: () => ({
264
- kind: "choice",
265
- title,
266
- lines,
267
- initialItemId: "cancel",
268
- items: [
269
- { id: "cancel", label: "No, cancel" },
270
- { id: "approve", label: `Yes, ${confirmationLabel}` },
271
- { id: "details", label: "View details" }
272
- ],
273
- action: "choose",
274
- hint: "close"
275
- })
276
- },
277
- actions: {
278
- choose: ({ itemId }) => {
279
- if (itemId === "approve" || itemId === "details") choice.value = itemId;
280
- return { kind: "close" };
281
- }
282
- }
283
- });
284
- const result = ctx.mode === "tui" && ctx.hasUI ? await runCustomInteraction(ctx, {
285
- create: ({ tui, theme, keybindings, complete }) => createSyncConfirmation({ title, lines, confirmationLabel, tui, theme, keybindings, complete }),
286
- signal,
287
- isCurrent,
288
- onError: () => {
289
- }
290
- }) : await runMenu(ctx, menu, { getState: () => void 0, signal, isCurrent, onError: () => {
291
- } });
292
- if (!isCurrent() || signal?.aborted) return false;
293
- if (result.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
294
- if (result.kind === "unsupported")
295
- throw new Error("Merge review requires observable TUI or RPC; review the plan before using --yes.");
296
- if (result.kind === "completed") choice.value = result.value;
297
- else if (result.kind !== "closed") return false;
298
- if (choice.value === "approve") return true;
299
- if (choice.value !== "details") return false;
300
- const detail = await runDocumentReview(ctx, {
301
- title: "Sync change details",
302
- content,
303
- format: { kind: "text" },
304
- viewportSize: "adaptive",
305
- hint: "back",
306
- signal,
307
- isCurrent,
308
- onError: () => {
309
- }
310
- });
311
- if (!isCurrent() || signal?.aborted) return false;
312
- if (detail.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
313
- if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
314
- }
315
- return false;
135
+ function transferCounts(changes) {
136
+ return {
137
+ add: changes.filter((change) => change.action === "Add").length,
138
+ update: changes.filter((change) => change.action === "Update").length,
139
+ remove: changes.filter((change) => change.action === "Delete").length
140
+ };
316
141
  }
317
142
 
318
143
  // src/ui/merged-sync-summary.ts
319
- import { truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
320
144
  function mergedSyncSummary(input) {
321
- const local = changes(input.localBefore, input.localAfter);
322
- const remote = changes(input.remoteBefore, input.remoteAfter);
145
+ const local = transferChanges(input.localBefore, input.localAfter);
146
+ const remote = transferChanges(input.remoteBefore, input.remoteAfter);
323
147
  const all = [...local, ...remote];
324
148
  const updatesOnly = all.length > 0 && all.every((change) => change.action === "Update");
325
149
  const verb = updatesOnly ? "Update" : "Apply changes to";
@@ -343,7 +167,7 @@ function mergedSyncSummary(input) {
343
167
  return [
344
168
  `${heading}: ${counts.join(", ")}`,
345
169
  ...(compact ? items.slice(0, 3) : items).map(
346
- (item) => ` ${compact ? truncateToWidth3(label(item, side), 80) : label(item, side)}`
170
+ (item) => ` ${compact ? truncateToWidth(label(item, side), 80) : label(item, side)}`
347
171
  ),
348
172
  ...compact && items.length > 3 ? [` ${items.length - 3} more files \u2014 View details`] : []
349
173
  ];
@@ -351,6 +175,7 @@ function mergedSyncSummary(input) {
351
175
  const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
352
176
  const safety = [
353
177
  `Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
178
+ "Remote removals affect the active snapshot; retained history is not erased.",
354
179
  `Sessions: ${input.sessions ? "included \u2014 may contain private conversations" : "not included"}`,
355
180
  input.groups.length ? `Unresolved groups: ${input.groups.length} \u2014 these paths stay unchanged on each side` : "Conflicts: none",
356
181
  ...input.publish && input.capability === "read-check-write-verify" ? ["Warning: simultaneous remote writes can still race; visible changes are rejected."] : [],
@@ -362,7 +187,7 @@ function mergedSyncSummary(input) {
362
187
  title,
363
188
  confirmationLabel,
364
189
  lines: [
365
- ...context.map((line) => truncateToWidth3(line, 100)),
190
+ ...context.map((line) => truncateToWidth(line, 100)),
366
191
  ...section(local, "local", true),
367
192
  ...section(remote, "remote", true),
368
193
  ...safety
@@ -381,15 +206,6 @@ function mergedSyncSummary(input) {
381
206
  ].join("\n")
382
207
  };
383
208
  }
384
- function changes(before, after) {
385
- const beforeMap = fileHashMap(before);
386
- const afterMap = fileHashMap(after);
387
- return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
388
- if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
389
- if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
390
- return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
391
- });
392
- }
393
209
 
394
210
  // src/sync/conflict-artifact-ownership.ts
395
211
  import fs from "node:fs/promises";
@@ -534,76 +350,18 @@ async function pruneCompletedConflicts(config, backend, state, local, remote, va
534
350
  }
535
351
  }
536
352
 
537
- // src/sync/content-conflicts.ts
538
- import { createHash } from "node:crypto";
539
- function snapshotFile(filePath, bytes) {
540
- return {
541
- path: filePath,
542
- contentBase64: bytes.toString("base64"),
543
- sha256: createHash("sha256").update(bytes).digest("hex")
544
- };
545
- }
546
- async function resolveContentConflicts(config, state, local, remote, plan, protectedPaths, options = {}) {
547
- if (plan.kind !== "planned" || !config.mergeContent) return plan;
548
- options.signal?.throwIfAborted();
549
- const budget = options.textBudget ?? { remaining: TEXT_MERGE_WORK_LIMIT };
550
- const decisions = [...plan.decisions];
551
- let ancestors = [];
552
- try {
553
- ancestors = await readMergeAncestors(config, state) ?? [];
554
- } catch {
555
- options.signal?.throwIfAborted();
556
- return plan;
557
- }
558
- options.signal?.throwIfAborted();
559
- const oursByPath = new Map(local.files.map((file) => [file.path, file]));
560
- const theirsByPath = new Map(remote.files.map((file) => [file.path, file]));
561
- const ancestorsByPath = new Map(ancestors.map((file) => [file.path, file]));
562
- const decisionIndex = new Map(decisions.map((decision, index) => [decision.path, index]));
563
- const protectedKeys = new Set([...protectedPaths].map(mergePathIdentity));
564
- for (const conflict of plan.conflicts) {
565
- options.signal?.throwIfAborted();
566
- if (conflict.reason !== "both-changed" || protectedKeys.has(mergePathIdentity(conflict.path))) continue;
567
- const session = conflict.path.startsWith("sessions/") && conflict.path.endsWith(".jsonl");
568
- if (!session && (!isMergeTextPath(conflict.path) || budget.remaining === 0)) continue;
569
- const ours = oursByPath.get(conflict.path);
570
- const theirs = theirsByPath.get(conflict.path);
571
- if (!ours || !theirs) continue;
572
- const ancestor = ancestorsByPath.get(conflict.path);
573
- const base = ancestor ? Buffer.from(ancestor.contentBase64, "base64") : void 0;
574
- if (!base) continue;
575
- const localBytes = Buffer.from(ours.contentBase64, "base64");
576
- const remoteBytes = Buffer.from(theirs.contentBase64, "base64");
577
- const bytes = session ? mergeSession(base, localBytes, remoteBytes) : mergeText(base, localBytes, remoteBytes, { budget, signal: options.signal });
578
- if (!bytes) continue;
579
- const index = decisionIndex.get(conflict.path);
580
- if (index === void 0) continue;
581
- decisions[index] = {
582
- kind: "accepted",
583
- path: conflict.path,
584
- source: "merged",
585
- file: snapshotFile(conflict.path, bytes)
586
- };
587
- }
588
- return {
589
- kind: "planned",
590
- decisions,
591
- conflicts: decisions.filter((item) => item.kind === "conflict")
592
- };
593
- }
594
-
595
353
  // src/sync/merge-apply.ts
596
- import { createHash as createHash3, randomUUID } from "node:crypto";
354
+ import { createHash as createHash2, randomUUID } from "node:crypto";
597
355
  import fs4 from "node:fs/promises";
598
356
  import path4 from "node:path";
599
357
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
600
358
 
601
359
  // src/snapshot/snapshot-apply.ts
602
- import { createHash as createHash2 } from "node:crypto";
360
+ import { createHash } from "node:crypto";
603
361
  import fs3 from "node:fs/promises";
604
362
  import path3 from "node:path";
605
363
  function sha256(value) {
606
- return createHash2("sha256").update(value).digest("hex");
364
+ return createHash("sha256").update(value).digest("hex");
607
365
  }
608
366
  function fileHashMap2(snapshot) {
609
367
  return Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));
@@ -912,7 +670,7 @@ async function assertReviewedTree(directory, relative, beforeHashes) {
912
670
  const stat = await fs4.lstat(child);
913
671
  if (stat.isDirectory()) {
914
672
  await assertReviewedTree(child, childRelative, beforeHashes);
915
- } else if (stat.isFile() && stat.nlink === 1 && beforeHashes[childRelative] === createHash3("sha256").update(await fs4.readFile(child)).digest("hex")) {
673
+ } else if (stat.isFile() && stat.nlink === 1 && beforeHashes[childRelative] === createHash2("sha256").update(await fs4.readFile(child)).digest("hex")) {
916
674
  continue;
917
675
  } else {
918
676
  throw new Error("Reviewed directory contains unknown or changed files; publication refused.");
@@ -1082,7 +840,7 @@ async function applyMergedSnapshot(before, after, protectedPaths, options, valid
1082
840
  } else {
1083
841
  if (!stat.isFile() || stat.nlink > 1)
1084
842
  throw new Error("Merged transfer target is no longer an independent regular file.");
1085
- hash = createHash3("sha256").update(await fs4.readFile(target2)).digest("hex");
843
+ hash = createHash2("sha256").update(await fs4.readFile(target2)).digest("hex");
1086
844
  }
1087
845
  } catch (error) {
1088
846
  if (error.code !== "ENOENT") throw error;
@@ -1260,53 +1018,7 @@ function progressState(config, head, snapshot, progress) {
1260
1018
  }
1261
1019
 
1262
1020
  // src/sync/settings-conflicts.ts
1263
- import { createHash as createHash4 } from "node:crypto";
1264
-
1265
- // src/sync/settings-merge.ts
1266
- var COUPLED_FIELDS = [
1267
- ["defaultProvider", "defaultModel"],
1268
- ["queueMode", "steeringMode"],
1269
- ["websockets", "transport"],
1270
- ["skills", "enableSkillCommands"],
1271
- ["enableAnalytics", "trackingId"]
1272
- ];
1273
- function mergeSettingsJson(ancestor, local, remote) {
1274
- let documents;
1275
- try {
1276
- documents = [ancestor, local, remote].map(parseSettingsDocument);
1277
- } catch {
1278
- return { kind: "review", reason: "unsupported-format", fields: [] };
1279
- }
1280
- const [base, left, right] = documents;
1281
- const selected = /* @__PURE__ */ new Map();
1282
- const fields = new Set(documents.flatMap((document) => [...document.root.members.keys()]));
1283
- const units = COUPLED_FIELDS.map((group) => [...group]);
1284
- const coupled = new Set(units.flat());
1285
- units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));
1286
- const conflicts = [];
1287
- for (const unit of units) {
1288
- const equal = (first, second) => unit.every(
1289
- (field) => jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value)
1290
- );
1291
- const source = equal(left, right) ? left : equal(left, base) ? right : equal(right, base) ? left : void 0;
1292
- if (!source) {
1293
- conflicts.push(...unit.filter((field) => fields.has(field)));
1294
- continue;
1295
- }
1296
- for (const field of unit) {
1297
- const member = source.root.members.get(field);
1298
- if (member) selected.set(field, { document: source, node: member.node });
1299
- }
1300
- }
1301
- if (conflicts.length) return { kind: "review", reason: "field-conflict", fields: conflicts };
1302
- try {
1303
- return { kind: "merged", content: renderSettingsMembers(left, selected) };
1304
- } catch {
1305
- return { kind: "review", reason: "unsupported-format", fields: [] };
1306
- }
1307
- }
1308
-
1309
- // src/sync/settings-conflicts.ts
1021
+ import { createHash as createHash3 } from "node:crypto";
1310
1022
  async function resolveSettingsConflicts(config, state, local, remote, plan) {
1311
1023
  if (plan.kind !== "planned") return { plan, fields: [] };
1312
1024
  const fields = [];
@@ -1336,7 +1048,7 @@ async function resolveSettingsConflicts(config, state, local, remote, plan) {
1336
1048
  file: {
1337
1049
  path: decision.path,
1338
1050
  contentBase64: merged.content.toString("base64"),
1339
- sha256: createHash4("sha256").update(merged.content).digest("hex")
1051
+ sha256: createHash3("sha256").update(merged.content).digest("hex")
1340
1052
  }
1341
1053
  };
1342
1054
  }
@@ -1351,7 +1063,11 @@ function formatDiff(local, remote) {
1351
1063
  const localMap = fileHashMap(local);
1352
1064
  const remoteMap = fileHashMap(remote);
1353
1065
  const allPaths = [.../* @__PURE__ */ new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
1354
- const lines = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
1066
+ const lines = [
1067
+ `local: ${local.files.length} files`,
1068
+ `remote: ${safeTerminalText(remote.id)} (${remote.files.length} files)`,
1069
+ ""
1070
+ ];
1355
1071
  let changed = 0;
1356
1072
  for (const filePath of allPaths) {
1357
1073
  if (!Object.hasOwn(localMap, filePath)) {
@@ -1369,76 +1085,87 @@ function formatDiff(local, remote) {
1369
1085
  return lines.join("\n");
1370
1086
  }
1371
1087
  function formatSnapshotOnlyDiff(title, snapshot) {
1372
- return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)].join(
1373
- "\n"
1374
- );
1088
+ return [
1089
+ `${safeTerminalText(title)}: ${safeTerminalText(snapshot.id)}`,
1090
+ ...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)
1091
+ ].join("\n");
1375
1092
  }
1376
1093
  function formatPushSummary(config, destination, upload, head, preservedRemoteFileCount = 0, remote) {
1377
1094
  return [
1378
1095
  `Sync setup: ${safeTerminalText(config.setupName)}`,
1379
1096
  `Storage location: ${safeTerminalText(destination)}`,
1380
- `Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
1097
+ `Direction: Local \u2192 Remote. Source: ${safeTerminalText(agentDir())}.`,
1098
+ `Publish a snapshot containing ${upload.files.length} files; unchanged and preserved files are not counted as changes.`,
1381
1099
  `Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
1382
- head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
1383
- "Publication effect: the backend's active head will reference the new immutable snapshot.",
1100
+ head ? `Remote latest: ${safeTerminalText(head.snapshotId)}` : "Remote latest: empty",
1101
+ "Publication effect: the backend's active head will reference the new immutable snapshot. Removed paths leave the active snapshot; retained history is not erased.",
1384
1102
  ...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
1385
1103
  formatPublicationPreview(remote, upload),
1386
- preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
1104
+ config.skipSecretScan ? "Warning: possible-secret scanning is disabled for this push." : preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
1387
1105
  ].join("\n");
1388
1106
  }
1389
- function formatApplyPreview(local, remote) {
1390
- return formatDirectionalChanges(local, remote, {
1391
- add: "Add locally",
1392
- update: "Update locally",
1393
- remove: "Remove locally"
1394
- });
1107
+ function formatApplyPreview(local, remote, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
1108
+ return formatDirectionalChanges(
1109
+ local,
1110
+ remote,
1111
+ {
1112
+ add: "Add locally",
1113
+ update: "Update locally",
1114
+ remove: "Remove locally"
1115
+ },
1116
+ protectedPaths,
1117
+ additionalRemovals
1118
+ );
1395
1119
  }
1396
- function formatPullSummary(config, destination, local, remote, protectedSessionCount) {
1120
+ function formatPullSummary(config, destination, local, remote, protectedPaths, additionalRemovals = []) {
1397
1121
  return [
1398
1122
  `Sync setup: ${safeTerminalText(config.setupName)}`,
1399
1123
  `Storage location: ${safeTerminalText(destination)}`,
1124
+ "Direction: Remote \u2192 Local.",
1400
1125
  `Snapshot: ${safeTerminalText(remote.id)}`,
1401
1126
  `Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
1402
- `Protected live sessions: ${protectedSessionCount || "none"}`,
1403
- formatApplyPreview(local, remote),
1127
+ `Protected live sessions: ${protectedPaths.size || "none"}`,
1128
+ formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
1129
+ ...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
1404
1130
  "A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
1405
1131
  ].join("\n");
1406
1132
  }
1407
- function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedSessionCount) {
1133
+ function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedPaths, additionalRemovals = []) {
1408
1134
  return [
1409
1135
  `Sync setup: ${safeTerminalText(config.setupName)}`,
1410
1136
  `Storage location: ${safeTerminalText(destination)}`,
1137
+ "Direction: Historical snapshot \u2192 Local and remote active head.",
1411
1138
  `Snapshot: ${safeTerminalText(requestedSnapshot)}`,
1412
1139
  `Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
1413
- `Protected live sessions: ${protectedSessionCount || "none"}`,
1414
- formatApplyPreview(local, remote),
1140
+ `Protected live sessions: ${protectedPaths.size || "none"}`,
1141
+ formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
1142
+ ...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
1415
1143
  "A local backup is created before applying; the backend's active head will change."
1416
1144
  ].join("\n");
1417
1145
  }
1418
1146
  function formatPublicationPreview(remote, upload) {
1419
- if (!remote) {
1420
- return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
1421
- "\n"
1422
- );
1423
- }
1424
1147
  return formatDirectionalChanges(remote, upload, {
1425
1148
  add: "Add remotely",
1426
1149
  update: "Update remotely",
1427
1150
  remove: "Remove remotely"
1428
1151
  });
1429
1152
  }
1430
- function formatDirectionalChanges(before, after, labels) {
1431
- const beforeMap = fileHashMap(before);
1432
- const afterMap = fileHashMap(after);
1433
- const paths = [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
1434
- const lines = [];
1435
- for (const filePath of paths) {
1436
- if (!Object.hasOwn(beforeMap, filePath)) lines.push(`${labels.add}: ${snapshotPathLabel(filePath)}`);
1437
- else if (!Object.hasOwn(afterMap, filePath)) lines.push(`${labels.remove}: ${snapshotPathLabel(filePath)}`);
1438
- else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
1153
+ function formatDirectionalChanges(before, after, labels, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
1154
+ const changes = transferChanges(before, after, protectedPaths);
1155
+ const known = new Set(changes.map((change) => change.path));
1156
+ for (const filePath of additionalRemovals) {
1157
+ if (!known.has(filePath) && !protectedPaths.has(filePath)) {
1158
+ changes.push({ path: filePath, action: "Delete" });
1159
+ known.add(filePath);
1160
+ }
1439
1161
  }
1440
- if (lines.length === 0) lines.push("No file changes.");
1441
- return lines.join("\n");
1162
+ const counts = transferCounts(changes);
1163
+ const label = { Add: labels.add, Update: labels.update, Delete: labels.remove };
1164
+ return [
1165
+ `Changes: ${counts.add} add \xB7 ${counts.update} update \xB7 ${counts.remove} remove`,
1166
+ ...changes.map((change) => `${label[change.action]}: ${snapshotPathLabel(change.path)}`),
1167
+ ...changes.length ? [] : ["No file changes."]
1168
+ ].join("\n");
1442
1169
  }
1443
1170
  function countPreservedRemoteFiles(local, upload) {
1444
1171
  const localPaths = new Set(local.files.map((file) => file.path));
@@ -1503,7 +1230,12 @@ function safeList(values) {
1503
1230
  // src/sync/merged-sync.ts
1504
1231
  var MAX_ATTEMPTS = 3;
1505
1232
  async function mergeSync(ctx, options, factory = createSyncBackend, resolution) {
1506
- const validateOwner = captureMutationOwner(ctx, options.signal);
1233
+ const timings = options.startupTimings ?? createStartupTimings();
1234
+ const sessionOwner = captureMutationOwner(ctx, options.signal);
1235
+ const validateOwner = (config2) => {
1236
+ sessionOwner();
1237
+ options.background?.validate(config2);
1238
+ };
1507
1239
  const isCurrent = () => {
1508
1240
  try {
1509
1241
  validateOwner();
@@ -1513,7 +1245,9 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1513
1245
  }
1514
1246
  };
1515
1247
  const config = await loadConfig(options.setup);
1248
+ validateOwner(config);
1516
1249
  const configToken = syncCheckConfigFingerprint(config);
1250
+ const modelToken = JSON.stringify(ctx.model);
1517
1251
  const sessionRoot = config.include.includes("sessions") ? await effectiveSessionRoot(ctx) : void 0;
1518
1252
  validateOwner();
1519
1253
  const snapshotOptions = {
@@ -1521,10 +1255,10 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1521
1255
  sessionDir: sessionRoot,
1522
1256
  signal: options.signal
1523
1257
  };
1524
- const validate = async () => {
1258
+ const validate = () => timings.measure("transfer authorization", async () => {
1525
1259
  validateOwner();
1526
1260
  const latest = await loadConfig(options.setup);
1527
- validateOwner();
1261
+ validateOwner(latest);
1528
1262
  if (configToken !== syncCheckConfigFingerprint(latest))
1529
1263
  throw new Error("Sync setup or settings changed during transfer; reopen sync.");
1530
1264
  if (sessionRoot !== void 0) {
@@ -1533,21 +1267,31 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1533
1267
  if (currentRoot !== sessionRoot)
1534
1268
  throw new Error("Session storage root changed during transfer; evidence retained for review.");
1535
1269
  }
1536
- if (options.auto && (!latest.automaticTransfer || !ctx.isIdle()))
1270
+ if (config.agentMode && JSON.stringify(ctx.model) !== modelToken)
1271
+ throw new Error("Agent mode paused: selected model changed during transfer.");
1272
+ if (options.auto && (!startupTransferEnabled(latest) || !ctx.isIdle()))
1537
1273
  throw new Error("Automatic transfer is no longer authorized at this idle boundary.");
1538
- };
1274
+ });
1539
1275
  await validate();
1540
- const backend = await factory(config);
1276
+ const backend = await timings.measure("transfer backend initialization", () => factory(config));
1541
1277
  await validate();
1542
- if (options.auto && backend.capability === "read-check-write-verify") {
1543
- throw new Error(
1544
- "Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Use manual sync."
1545
- );
1546
- }
1547
- const pending = await readMergeJournal(config);
1278
+ const pending = await timings.measure("transfer journal read", () => readMergeJournal(config));
1548
1279
  await validate();
1549
1280
  if (pending) {
1550
- const recovered = await completeJournal(ctx, config, backend, pending, validate, options.signal, options.auto);
1281
+ const recovered = await timings.measure(
1282
+ "transfer journal recovery",
1283
+ () => completeJournal(
1284
+ ctx,
1285
+ config,
1286
+ backend,
1287
+ pending,
1288
+ validate,
1289
+ timings,
1290
+ options.signal,
1291
+ options.auto,
1292
+ options.background?.validate
1293
+ )
1294
+ );
1551
1295
  if (!recovered) {
1552
1296
  if ((options.auto || !options.silent) && !options.signal?.aborted)
1553
1297
  ctx.ui.notify(
@@ -1560,24 +1304,51 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1560
1304
  ctx.ui.notify("Recovered the committed merged transfer. Run sync again to inspect newer changes.", "info");
1561
1305
  return "applied";
1562
1306
  }
1307
+ if ((options.auto || config.agentMode) && backend.capability === "read-check-write-verify") {
1308
+ throw new Error(
1309
+ "Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Disable Agent mode and use manual sync."
1310
+ );
1311
+ }
1563
1312
  const textBudget = { remaining: TEXT_MERGE_WORK_LIMIT };
1564
1313
  const createdArtifacts = [];
1565
1314
  const ownedStatePath = statePathForConfig(config);
1566
1315
  try {
1567
1316
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1568
1317
  await validate();
1569
- const state = await readStateForConfig(config);
1318
+ const state = await timings.measure("transfer state read", () => readStateForConfig(config));
1570
1319
  if (state.unresolved?.length && !config.partialSync)
1571
1320
  throw new Error(
1572
1321
  "Partial progress remains unresolved; re-enable partial sync or review an explicit force direction."
1573
1322
  );
1574
- const localRaw = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1323
+ const localRaw = await timings.measure(
1324
+ "transfer local snapshot",
1325
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1326
+ );
1575
1327
  const local = portableSnapshot(localRaw, config.localFields);
1576
1328
  if (!sameLocalFields(state.localFields, config.localFields))
1577
1329
  throw new Error("Local-field rules changed; review and confirm a directional migration first.");
1578
- const head = await backend.readHead(options.signal);
1330
+ const head = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
1579
1331
  await validate();
1580
- const rawRemote = head ? await readSnapshotForHead(backend, head, options.signal) : void 0;
1332
+ if (options.auto && !resolution && !config.partialSync && head && state.lastAppliedSnapshot && state.lastRemoteRevision && !state.unresolved?.length && head.snapshotId === state.lastAppliedSnapshot && backend.sameRevision(head.revision, state.lastRemoteRevision) && inspectRemoteSelection(config.include, { selection: head.selection, files: [] }).kind === "same" && matchesAcceptedState(state, config, head, local)) {
1333
+ const unchanged = await timings.measure("transfer unchanged verification", async () => {
1334
+ if (!await hasCurrentMergeBaseline(config, local, state)) return false;
1335
+ await validate();
1336
+ const currentLocal = await timings.measure(
1337
+ "transfer local snapshot",
1338
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1339
+ );
1340
+ await validate();
1341
+ const currentState = await timings.measure("transfer state read", () => readStateForConfig(config));
1342
+ await validate();
1343
+ return sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) && syncStateFingerprint(currentState) === syncStateFingerprint(state);
1344
+ });
1345
+ await validate();
1346
+ if (unchanged) {
1347
+ timings.markUnchanged();
1348
+ return "applied";
1349
+ }
1350
+ }
1351
+ const rawRemote = head ? await timings.measure("transfer snapshot download", () => readSnapshotForHead(backend, head, options.signal)) : void 0;
1581
1352
  await validate();
1582
1353
  if (rawRemote) {
1583
1354
  const files = [...local.files, ...rawRemote.files];
@@ -1590,12 +1361,16 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1590
1361
  const remote = rawRemote ? portableSnapshot(filterSnapshotForConfigPolicy(rawRemote, config), rawRemote.localFields) : void 0;
1591
1362
  if (rawRemote) {
1592
1363
  requireCompatibleRemoteSelection(config, rawRemote);
1593
- const validation = planFileMerge({
1594
- baseline: state.lastFileHashes,
1595
- local: local.files,
1596
- remote: rawRemote.files,
1597
- selectionCompatible: true
1598
- });
1364
+ const validation = await timings.measure(
1365
+ "transfer merge planning",
1366
+ () => planFileMerge({
1367
+ baseline: state.lastFileHashes,
1368
+ local: local.files,
1369
+ remote: rawRemote.files,
1370
+ selectionCompatible: true
1371
+ })
1372
+ );
1373
+ await validate();
1599
1374
  const managedPaths = /* @__PURE__ */ new Set([
1600
1375
  ...Object.keys(state.lastFileHashes),
1601
1376
  ...[...local.files, ...remote?.files ?? []].map((file) => file.path)
@@ -1622,30 +1397,35 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1622
1397
  throw review("The established remote is missing; choose an explicit recovery direction.", "remote-empty");
1623
1398
  const selectionState = inspectRemoteSelection(config.include, rawRemote);
1624
1399
  const selectionCompatible = selectionState.kind === "same";
1625
- let plan = planFileMerge({
1626
- baseline: state.lastFileHashes,
1627
- local: local.files,
1628
- remote: remote.files,
1629
- selectionCompatible,
1630
- protectedPaths: protectedSessionPaths(ctx, sessionRoot)
1631
- });
1400
+ let plan = await timings.measure(
1401
+ "transfer merge planning",
1402
+ () => planFileMerge({
1403
+ baseline: state.lastFileHashes,
1404
+ local: local.files,
1405
+ remote: remote.files,
1406
+ selectionCompatible,
1407
+ protectedPaths: protectedSessionPaths(ctx, sessionRoot)
1408
+ })
1409
+ );
1410
+ await validate();
1632
1411
  let resolved = { plan, fields: [] };
1633
1412
  if (config.mergeSettings) {
1634
1413
  try {
1635
- resolved = await resolveSettingsConflicts(config, state, local, remote, plan);
1414
+ resolved = await timings.measure(
1415
+ "transfer settings merge",
1416
+ () => resolveSettingsConflicts(config, state, local, remote, plan)
1417
+ );
1636
1418
  } catch (error) {
1637
1419
  if (!config.partialSync) throw error;
1638
1420
  }
1639
1421
  }
1640
1422
  await validate();
1641
- plan = await resolveContentConflicts(
1642
- config,
1643
- state,
1644
- local,
1645
- remote,
1646
- resolved.plan,
1647
- protectedSessionPaths(ctx, sessionRoot),
1648
- { textBudget, signal: options.signal }
1423
+ plan = await timings.measure(
1424
+ "transfer content merge",
1425
+ () => resolveContentConflicts(config, state, local, remote, resolved.plan, protectedSessionPaths(ctx, sessionRoot), {
1426
+ textBudget,
1427
+ signal: options.signal
1428
+ })
1649
1429
  );
1650
1430
  await validate();
1651
1431
  if (resolution) {
@@ -1662,7 +1442,30 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1662
1442
  );
1663
1443
  await validate();
1664
1444
  }
1665
- if (plan.kind !== "planned" || plan.conflicts.length && (!config.partialSync || options.auto)) {
1445
+ if (config.agentMode && !resolution) {
1446
+ const { resolveAgentPlan } = await import("./agent-plan-QCUJWBP4.ts");
1447
+ await validate();
1448
+ plan = await resolveAgentPlan(
1449
+ ctx,
1450
+ options,
1451
+ config,
1452
+ backend,
1453
+ state,
1454
+ local,
1455
+ localRaw,
1456
+ remote,
1457
+ head,
1458
+ plan,
1459
+ snapshotOptions,
1460
+ validate
1461
+ );
1462
+ await validate();
1463
+ if (plan.kind !== "planned" || plan.conflicts.length && !config.partialSync)
1464
+ throw new Error(
1465
+ "Agent mode paused: unsupported or refused conflict groups remain; no transfer was performed."
1466
+ );
1467
+ }
1468
+ if (plan.kind !== "planned" || plan.conflicts.length && (!config.partialSync || options.auto && !config.agentMode)) {
1666
1469
  const decision = review(
1667
1470
  selectionCompatible ? `Conflicting or protected paths require review; no merged transfer was performed. Use /sync diff and an explicit direction.${resolved.fields.length ? ` Settings fields: ${resolved.fields.map(safeTerminalText).join(", ")}` : ""}` : "Remote selection metadata is unavailable; review the included content and choose an explicit direction before merging.",
1668
1471
  selectionCompatible ? "both-changed" : "remote-or-policy-changed"
@@ -1752,37 +1555,49 @@ An explicit direction adopts this setup's included-content policy; no automatic
1752
1555
  const publish = !sameHashes(fileHashMap(publication), fileHashMap(remote)) || Boolean(config.partialSync && rawRemote.version !== 3);
1753
1556
  const apply = !sameHashes(fileHashMap(localRaw), fileHashMap(after));
1754
1557
  if (!publish && !apply) {
1755
- const beforeEvidence = await backend.readHead(options.signal);
1558
+ const beforeEvidence = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
1756
1559
  await validate();
1757
1560
  if (!beforeEvidence || !backend.sameRevision(beforeEvidence.revision, head.revision))
1758
1561
  throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
1759
1562
  progress = await persistProgress();
1760
1563
  await validate();
1761
- const currentLocal = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1564
+ const currentLocal = await timings.measure(
1565
+ "transfer local snapshot",
1566
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1567
+ );
1762
1568
  await validate();
1763
- if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
1569
+ if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
1764
1570
  throw new Error(
1765
1571
  "Local content or baseline changed before baseline acceptance; retry from a fresh observation."
1766
1572
  );
1767
1573
  await validate();
1768
- const currentHead = await backend.readHead(options.signal);
1574
+ const currentHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
1769
1575
  await validate();
1770
1576
  if (!currentHead || !backend.sameRevision(currentHead.revision, head.revision))
1771
1577
  throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
1772
- await writeAcceptedState(config, head, accepted, validate, progress);
1773
- await pruneMergeBaselines(
1774
- config,
1775
- acceptedState(config, head, accepted, progress),
1776
- captureMutationOwner(ctx, options.signal)
1578
+ await timings.measure(
1579
+ "transfer baseline write",
1580
+ () => writeAcceptedState(config, head, accepted, validate, progress)
1581
+ );
1582
+ await timings.measure(
1583
+ "transfer cleanup",
1584
+ () => pruneMergeBaselines(
1585
+ config,
1586
+ acceptedState(config, head, accepted, progress),
1587
+ captureMutationOwner(ctx, options.signal)
1588
+ )
1777
1589
  );
1778
1590
  await validate();
1779
- await pruneCompletedConflicts(
1780
- config,
1781
- backend.identity,
1782
- acceptedState(config, head, accepted, progress),
1783
- accepted,
1784
- remote,
1785
- captureMutationOwner(ctx, options.signal)
1591
+ await timings.measure(
1592
+ "transfer cleanup",
1593
+ () => pruneCompletedConflicts(
1594
+ config,
1595
+ backend.identity,
1596
+ acceptedState(config, head, accepted, progress),
1597
+ accepted,
1598
+ remote,
1599
+ captureMutationOwner(ctx, options.signal)
1600
+ )
1786
1601
  );
1787
1602
  await validate();
1788
1603
  if (!options.silent)
@@ -1792,11 +1607,14 @@ An explicit direction adopts this setup's included-content policy; no automatic
1792
1607
  );
1793
1608
  return "applied";
1794
1609
  }
1795
- await preflightMergedTargets(localRaw, after, snapshotOptions, ctx.sessionManager.getSessionFile?.());
1610
+ await timings.measure(
1611
+ "transfer preflight",
1612
+ () => preflightMergedTargets(localRaw, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
1613
+ );
1796
1614
  await validate();
1797
1615
  if (!config.skipSecretScan && scanSnapshot(upload).length)
1798
1616
  throw new Error("Refusing to merge possible secrets. Review managed content before syncing.");
1799
- if (!options.yes) {
1617
+ if (!options.yes && !config.agentMode) {
1800
1618
  const summary = mergedSyncSummary({
1801
1619
  setupName: config.setupName,
1802
1620
  destination: backend.destination,
@@ -1822,19 +1640,25 @@ An explicit direction adopts this setup's included-content policy; no automatic
1822
1640
  return "cancelled";
1823
1641
  }
1824
1642
  await validate();
1825
- const refreshed = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1643
+ const refreshed = await timings.measure(
1644
+ "transfer local snapshot",
1645
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1646
+ );
1826
1647
  if (!sameHashes(fileHashMap(refreshed), fileHashMap(localRaw)))
1827
1648
  throw new Error("Local content changed during review; no merged transfer was performed.");
1828
- if (syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
1649
+ if (syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
1829
1650
  throw new Error("Sync baseline changed during review; retry from a fresh observation.");
1830
1651
  await validate();
1831
- const refreshedHead = await backend.readHead(options.signal);
1652
+ const refreshedHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
1832
1653
  await validate();
1833
1654
  if (!refreshedHead || !backend.sameRevision(refreshedHead.revision, head.revision))
1834
1655
  throw new Error("Remote changed during review; no merged transfer was performed.");
1835
1656
  progress = await persistProgress();
1836
1657
  await validate();
1837
- const backup = await backupLocal(config.snapshotIdentity, snapshotOptions, options.signal);
1658
+ const backup = await timings.measure(
1659
+ "transfer backup",
1660
+ () => backupLocal(config.snapshotIdentity, snapshotOptions, options.signal)
1661
+ );
1838
1662
  await validate();
1839
1663
  const journal = {
1840
1664
  version: 1,
@@ -1850,10 +1674,15 @@ An explicit direction adopts this setup's included-content policy; no automatic
1850
1674
  ...sessionRoot !== void 0 ? { sessionRoot } : {},
1851
1675
  ...!publish ? { committedHead: head, applyOnly: true } : {}
1852
1676
  };
1853
- await writeMergeJournal(config, journal);
1854
- const atCommitState = syncStateFingerprint(await readStateForConfig(config));
1677
+ await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
1678
+ const atCommitState = syncStateFingerprint(
1679
+ await timings.measure("transfer state read", () => readStateForConfig(config))
1680
+ );
1855
1681
  await validate();
1856
- const atCommit = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1682
+ const atCommit = await timings.measure(
1683
+ "transfer local snapshot",
1684
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1685
+ );
1857
1686
  await validate();
1858
1687
  if (!sameHashes(fileHashMap(atCommit), fileHashMap(localRaw)) || atCommitState !== journal.stateIdentity) {
1859
1688
  await validate();
@@ -1863,7 +1692,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
1863
1692
  );
1864
1693
  }
1865
1694
  try {
1866
- await preflightMergedTargets(atCommit, after, snapshotOptions, ctx.sessionManager.getSessionFile?.());
1695
+ await timings.measure(
1696
+ "transfer preflight",
1697
+ () => preflightMergedTargets(atCommit, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
1698
+ );
1867
1699
  await validate();
1868
1700
  } catch (error) {
1869
1701
  await validate();
@@ -1872,13 +1704,16 @@ An explicit direction adopts this setup's included-content policy; no automatic
1872
1704
  }
1873
1705
  try {
1874
1706
  if (publish) {
1875
- const result = await backend.publishSnapshot(upload, expectedRemoteHead(head), {
1876
- signal: options.signal,
1877
- onCommit: options.onCommit
1878
- });
1707
+ const result = await timings.measure(
1708
+ "transfer remote publication",
1709
+ () => backend.publishSnapshot(upload, expectedRemoteHead(head), {
1710
+ signal: options.signal,
1711
+ onCommit: options.onCommit
1712
+ })
1713
+ );
1879
1714
  journal.committedHead = result.head;
1880
1715
  journal.warnings = result.warnings;
1881
- await writeMergeJournal(config, journal);
1716
+ await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
1882
1717
  } else options.onCommit?.();
1883
1718
  } catch (error) {
1884
1719
  if (error instanceof SyncBackendConflictError && error.phase === "before-commit" && !error.candidateMayHaveBeenActive) {
@@ -1888,7 +1723,20 @@ An explicit direction adopts this setup's included-content policy; no automatic
1888
1723
  }
1889
1724
  throw mergeTransferError(error, config.backend);
1890
1725
  }
1891
- const completed = await completeJournal(ctx, config, backend, journal, validate, options.signal, options.auto);
1726
+ const completed = await timings.measure(
1727
+ "transfer journal recovery",
1728
+ () => completeJournal(
1729
+ ctx,
1730
+ config,
1731
+ backend,
1732
+ journal,
1733
+ validate,
1734
+ timings,
1735
+ options.signal,
1736
+ options.auto,
1737
+ options.background?.validate
1738
+ )
1739
+ );
1892
1740
  if (!completed) {
1893
1741
  if (!options.signal?.aborted && (options.auto || !options.silent))
1894
1742
  ctx.ui.notify(
@@ -1911,10 +1759,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
1911
1759
  return "applied";
1912
1760
  }
1913
1761
  } finally {
1914
- await releaseConflictArtifacts(config, ownedStatePath, createdArtifacts);
1762
+ await timings.measure("transfer cleanup", () => releaseConflictArtifacts(config, ownedStatePath, createdArtifacts));
1915
1763
  }
1916
1764
  }
1917
- async function completeJournal(ctx, config, backend, journal, validate, signal, auto = false) {
1765
+ async function completeJournal(ctx, config, backend, journal, validate, timings, signal, auto = false, validateBackground) {
1918
1766
  if (journal.identity !== mergeJournalIdentity(config, backend.identity) || !sameLocalFields(journal.upload.localFields, config.localFields))
1919
1767
  throw new Error(
1920
1768
  "Merge journal belongs to a different setup, selection or local-field policy; preserve it for reviewed recovery."
@@ -1928,11 +1776,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1928
1776
  );
1929
1777
  }
1930
1778
  const snapshotOptions = { ...snapshotOptionsForContext(ctx, config), sessionDir: sessionRoot, signal };
1931
- const head = await backend.readHead(signal);
1779
+ const head = await timings.measure("transfer remote head", () => backend.readHead(signal));
1932
1780
  if (!journal.committedHead && head && (backend.sameRevision(head.revision, journal.expectedHead.revision) || backend.matchesUncommittedRecoveryHead?.(head, journal.expectedHead))) {
1933
1781
  await validate();
1934
- const local = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1935
- if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await readStateForConfig(config)) !== journal.stateIdentity) {
1782
+ const local = await timings.measure(
1783
+ "transfer local snapshot",
1784
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1785
+ );
1786
+ if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== journal.stateIdentity) {
1936
1787
  throw new Error(
1937
1788
  "Uncertain publication is not active, but local content or baseline changed; keep the journal for reviewed recovery."
1938
1789
  );
@@ -1942,7 +1793,7 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1942
1793
  }
1943
1794
  await validate();
1944
1795
  if (journal.committedHead && (!head || !backend.sameRevision(head.revision, journal.committedHead.revision))) {
1945
- const state2 = await readStateForConfig(config);
1796
+ const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
1946
1797
  await validate();
1947
1798
  if (matchesAcceptedState(state2, config, journal.committedHead, journal.accepted ?? journal.after, journal.progress)) {
1948
1799
  await clearMergeJournal(config);
@@ -1953,8 +1804,11 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1953
1804
  }
1954
1805
  }
1955
1806
  if (journal.applyOnly && (!head || !backend.sameRevision(head.revision, journal.expectedHead.revision))) {
1956
- const local = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1957
- const state2 = await readStateForConfig(config);
1807
+ const local = await timings.measure(
1808
+ "transfer local snapshot",
1809
+ () => createSnapshot(config.snapshotIdentity, snapshotOptions)
1810
+ );
1811
+ const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
1958
1812
  await validate();
1959
1813
  if (sameHashes(fileHashMap(local), fileHashMap(journal.before)) && syncStateFingerprint(state2) === journal.stateIdentity) {
1960
1814
  await clearMergeJournal(config);
@@ -1967,11 +1821,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1967
1821
  "Publication outcome cannot be reconciled with the current remote head. Journal and backup retained; do not retry publication or restore old bytes blindly."
1968
1822
  );
1969
1823
  }
1970
- const observed = await readSnapshotForHead(backend, head, signal);
1824
+ const observed = await timings.measure(
1825
+ "transfer snapshot download",
1826
+ () => readSnapshotForHead(backend, head, signal)
1827
+ );
1971
1828
  requireCompatibleRemoteSelection(config, observed);
1972
1829
  if (!sameHashes(fileHashMap(observed), fileHashMap(journal.upload)) || JSON.stringify(observed.selection) !== JSON.stringify(journal.upload.selection))
1973
1830
  throw new Error("Remote snapshot does not match the recorded merge publication.");
1974
- const state = await readStateForConfig(config);
1831
+ const state = await timings.measure("transfer state read", () => readStateForConfig(config));
1975
1832
  if (matchesAcceptedState(state, config, head, journal.accepted ?? journal.after, journal.progress)) {
1976
1833
  await validate();
1977
1834
  await clearMergeJournal(config);
@@ -1992,38 +1849,51 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1992
1849
  const validateOwner = captureMutationOwner(ctx, signal);
1993
1850
  const validateMutation = () => {
1994
1851
  validateOwner();
1852
+ validateBackground?.();
1995
1853
  if (auto && !ctx.isIdle()) throw new Error("Automatic file apply is no longer idle; evidence retained.");
1996
1854
  };
1997
- await applyMergedSnapshot(
1998
- journal.before,
1999
- journal.after,
2000
- protectedSessionPaths(ctx, sessionRoot),
2001
- { ...snapshotOptions, validateMutation },
2002
- validate,
2003
- ctx.sessionManager.getSessionFile?.()
1855
+ await timings.measure(
1856
+ "transfer local apply",
1857
+ () => applyMergedSnapshot(
1858
+ journal.before,
1859
+ journal.after,
1860
+ protectedSessionPaths(ctx, sessionRoot),
1861
+ { ...snapshotOptions, validateMutation },
1862
+ validate,
1863
+ ctx.sessionManager.getSessionFile?.()
1864
+ )
2004
1865
  );
2005
1866
  await validate();
2006
- const headAtAcceptance = await backend.readHead(signal);
1867
+ const headAtAcceptance = await timings.measure("transfer remote head", () => backend.readHead(signal));
2007
1868
  await validate();
2008
1869
  if (!headAtAcceptance || !backend.sameRevision(headAtAcceptance.revision, head.revision))
2009
1870
  throw new Error("Remote changed during merged apply; journal and backup retained for review.");
2010
- await writeAcceptedState(config, head, journal.accepted ?? journal.after, validate, journal.progress);
1871
+ await timings.measure(
1872
+ "transfer baseline write",
1873
+ () => writeAcceptedState(config, head, journal.accepted ?? journal.after, validate, journal.progress)
1874
+ );
2011
1875
  await validate();
2012
1876
  await clearMergeJournal(config);
2013
1877
  await validate();
2014
- await pruneMergeBaselines(
2015
- config,
2016
- acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
2017
- captureMutationOwner(ctx, signal)
1878
+ await timings.measure(
1879
+ "transfer cleanup",
1880
+ () => pruneMergeBaselines(
1881
+ config,
1882
+ acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
1883
+ captureMutationOwner(ctx, signal)
1884
+ )
2018
1885
  );
2019
1886
  await validate();
2020
- await pruneCompletedConflicts(
2021
- config,
2022
- backend.identity,
2023
- acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
2024
- journal.accepted ?? journal.after,
2025
- journal.upload,
2026
- captureMutationOwner(ctx, signal)
1887
+ await timings.measure(
1888
+ "transfer cleanup",
1889
+ () => pruneCompletedConflicts(
1890
+ config,
1891
+ backend.identity,
1892
+ acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
1893
+ journal.accepted ?? journal.after,
1894
+ journal.upload,
1895
+ captureMutationOwner(ctx, signal)
1896
+ )
2027
1897
  );
2028
1898
  await validate();
2029
1899
  return true;
@@ -2055,8 +1925,8 @@ async function writeAcceptedState(config, head, snapshot, validate, progress) {
2055
1925
  }
2056
1926
 
2057
1927
  export {
2058
- confirmMergeReview,
2059
1928
  applySnapshot,
1929
+ addTopLevelCaseVariantDeletes,
2060
1930
  formatDiff,
2061
1931
  formatSnapshotOnlyDiff,
2062
1932
  formatPushSummary,
@@ -2067,4 +1937,4 @@ export {
2067
1937
  createSyncDecision,
2068
1938
  mergeSync
2069
1939
  };
2070
- //# sourceMappingURL=chunk-MYPSMJHJ.ts.map
1940
+ //# sourceMappingURL=chunk-OTZRCKYP.ts.map