@herbertgao/pi-extensions 2026.8.8 → 2026.8.10

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 (266) hide show
  1. package/README.md +6 -3
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +15 -13
  31. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  32. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  33. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  34. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  35. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  36. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  37. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  38. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  39. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  40. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  42. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  43. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  44. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  45. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  46. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  47. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  48. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  49. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  50. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  51. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  52. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  53. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  54. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  55. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  56. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  57. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  58. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  59. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  60. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  61. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  62. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  63. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  64. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  65. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  66. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  67. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  68. package/node_modules/pi-web-access/CHANGELOG.md +13 -0
  69. package/node_modules/pi-web-access/README.md +2 -2
  70. package/node_modules/pi-web-access/chrome-cookies.ts +12 -7
  71. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  72. package/node_modules/pi-web-access/index.ts +24 -10
  73. package/node_modules/pi-web-access/package.json +1 -1
  74. package/node_modules/resume-from/LICENSE +21 -0
  75. package/node_modules/resume-from/README.md +161 -0
  76. package/node_modules/resume-from/dist/adapters/claude-code/adapter.d.ts +25 -0
  77. package/node_modules/resume-from/dist/adapters/claude-code/adapter.js +60 -0
  78. package/node_modules/resume-from/dist/adapters/claude-code/contract.d.ts +8 -0
  79. package/node_modules/resume-from/dist/adapters/claude-code/contract.js +4 -0
  80. package/node_modules/resume-from/dist/adapters/claude-code/entries.d.ts +30 -0
  81. package/node_modules/resume-from/dist/adapters/claude-code/entries.js +107 -0
  82. package/node_modules/resume-from/dist/adapters/claude-code/index.d.ts +3 -0
  83. package/node_modules/resume-from/dist/adapters/claude-code/index.js +2 -0
  84. package/node_modules/resume-from/dist/adapters/claude-code/layout.d.ts +29 -0
  85. package/node_modules/resume-from/dist/adapters/claude-code/layout.js +80 -0
  86. package/node_modules/resume-from/dist/adapters/claude-code/readback.d.ts +9 -0
  87. package/node_modules/resume-from/dist/adapters/claude-code/readback.js +39 -0
  88. package/node_modules/resume-from/dist/adapters/claude-code/reader.d.ts +37 -0
  89. package/node_modules/resume-from/dist/adapters/claude-code/reader.js +415 -0
  90. package/node_modules/resume-from/dist/adapters/claude-code/redaction.d.ts +26 -0
  91. package/node_modules/resume-from/dist/adapters/claude-code/redaction.js +132 -0
  92. package/node_modules/resume-from/dist/adapters/claude-code/validation.d.ts +4 -0
  93. package/node_modules/resume-from/dist/adapters/claude-code/validation.js +39 -0
  94. package/node_modules/resume-from/dist/adapters/claude-code/writer.d.ts +23 -0
  95. package/node_modules/resume-from/dist/adapters/claude-code/writer.js +306 -0
  96. package/node_modules/resume-from/dist/adapters/codex/contract.d.ts +15 -0
  97. package/node_modules/resume-from/dist/adapters/codex/contract.js +4 -0
  98. package/node_modules/resume-from/dist/adapters/codex/index.d.ts +6 -0
  99. package/node_modules/resume-from/dist/adapters/codex/index.js +44 -0
  100. package/node_modules/resume-from/dist/adapters/codex/read.d.ts +28 -0
  101. package/node_modules/resume-from/dist/adapters/codex/read.js +270 -0
  102. package/node_modules/resume-from/dist/adapters/codex/readback.d.ts +9 -0
  103. package/node_modules/resume-from/dist/adapters/codex/readback.js +47 -0
  104. package/node_modules/resume-from/dist/adapters/codex/redaction.d.ts +26 -0
  105. package/node_modules/resume-from/dist/adapters/codex/redaction.js +132 -0
  106. package/node_modules/resume-from/dist/adapters/codex/rollout.d.ts +75 -0
  107. package/node_modules/resume-from/dist/adapters/codex/rollout.js +185 -0
  108. package/node_modules/resume-from/dist/adapters/codex/validation.d.ts +7 -0
  109. package/node_modules/resume-from/dist/adapters/codex/validation.js +78 -0
  110. package/node_modules/resume-from/dist/adapters/codex/write.d.ts +17 -0
  111. package/node_modules/resume-from/dist/adapters/codex/write.js +141 -0
  112. package/node_modules/resume-from/dist/adapters/contract.d.ts +74 -0
  113. package/node_modules/resume-from/dist/adapters/contract.js +4 -0
  114. package/node_modules/resume-from/dist/adapters/pi/adapter.d.ts +15 -0
  115. package/node_modules/resume-from/dist/adapters/pi/adapter.js +209 -0
  116. package/node_modules/resume-from/dist/adapters/pi/contract.d.ts +24 -0
  117. package/node_modules/resume-from/dist/adapters/pi/contract.js +4 -0
  118. package/node_modules/resume-from/dist/adapters/pi/format.d.ts +162 -0
  119. package/node_modules/resume-from/dist/adapters/pi/format.js +223 -0
  120. package/node_modules/resume-from/dist/adapters/pi/index.d.ts +7 -0
  121. package/node_modules/resume-from/dist/adapters/pi/index.js +10 -0
  122. package/node_modules/resume-from/dist/adapters/pi/parse.d.ts +41 -0
  123. package/node_modules/resume-from/dist/adapters/pi/parse.js +371 -0
  124. package/node_modules/resume-from/dist/adapters/pi/redaction.d.ts +26 -0
  125. package/node_modules/resume-from/dist/adapters/pi/redaction.js +132 -0
  126. package/node_modules/resume-from/dist/adapters/pi/serialize.d.ts +22 -0
  127. package/node_modules/resume-from/dist/adapters/pi/serialize.js +87 -0
  128. package/node_modules/resume-from/dist/adapters/pi/validate.d.ts +14 -0
  129. package/node_modules/resume-from/dist/adapters/pi/validate.js +134 -0
  130. package/node_modules/resume-from/dist/bin.d.ts +35 -0
  131. package/node_modules/resume-from/dist/bin.js +135 -0
  132. package/node_modules/resume-from/dist/contract.d.ts +10 -0
  133. package/node_modules/resume-from/dist/contract.js +4 -0
  134. package/node_modules/resume-from/dist/host/agents.d.ts +29 -0
  135. package/node_modules/resume-from/dist/host/agents.js +20 -0
  136. package/node_modules/resume-from/dist/host/cli/args.d.ts +21 -0
  137. package/node_modules/resume-from/dist/host/cli/args.js +187 -0
  138. package/node_modules/resume-from/dist/host/cli/contract.d.ts +27 -0
  139. package/node_modules/resume-from/dist/host/cli/contract.js +4 -0
  140. package/node_modules/resume-from/dist/host/cli/index.d.ts +2 -0
  141. package/node_modules/resume-from/dist/host/cli/index.js +1 -0
  142. package/node_modules/resume-from/dist/host/cli/presentation.d.ts +2 -0
  143. package/node_modules/resume-from/dist/host/cli/presentation.js +11 -0
  144. package/node_modules/resume-from/dist/host/cli/render.d.ts +3 -0
  145. package/node_modules/resume-from/dist/host/cli/render.js +70 -0
  146. package/node_modules/resume-from/dist/host/cli/runner.d.ts +8 -0
  147. package/node_modules/resume-from/dist/host/cli/runner.js +125 -0
  148. package/node_modules/resume-from/dist/host/contract.d.ts +42 -0
  149. package/node_modules/resume-from/dist/host/contract.js +4 -0
  150. package/node_modules/resume-from/dist/host/entry.d.ts +52 -0
  151. package/node_modules/resume-from/dist/host/entry.js +91 -0
  152. package/node_modules/resume-from/dist/host/index.d.ts +11 -0
  153. package/node_modules/resume-from/dist/host/index.js +10 -0
  154. package/node_modules/resume-from/dist/host/pi-extension/command.d.ts +18 -0
  155. package/node_modules/resume-from/dist/host/pi-extension/command.js +103 -0
  156. package/node_modules/resume-from/dist/host/pi-extension/contract.d.ts +34 -0
  157. package/node_modules/resume-from/dist/host/pi-extension/contract.js +4 -0
  158. package/node_modules/resume-from/dist/host/pi-extension/index.d.ts +6 -0
  159. package/node_modules/resume-from/dist/host/pi-extension/index.js +4 -0
  160. package/node_modules/resume-from/dist/host/pi-extension/picker.d.ts +16 -0
  161. package/node_modules/resume-from/dist/host/pi-extension/picker.js +35 -0
  162. package/node_modules/resume-from/dist/host/pi-extension/presentation.d.ts +2 -0
  163. package/node_modules/resume-from/dist/host/pi-extension/presentation.js +11 -0
  164. package/node_modules/resume-from/dist/host/pi-extension/register.d.ts +25 -0
  165. package/node_modules/resume-from/dist/host/pi-extension/register.js +11 -0
  166. package/node_modules/resume-from/dist/host/pi-extension/ui.d.ts +20 -0
  167. package/node_modules/resume-from/dist/host/pi-extension/ui.js +1 -0
  168. package/node_modules/resume-from/dist/host/profile.d.ts +8 -0
  169. package/node_modules/resume-from/dist/host/profile.js +31 -0
  170. package/node_modules/resume-from/dist/host/registry.d.ts +8 -0
  171. package/node_modules/resume-from/dist/host/registry.js +33 -0
  172. package/node_modules/resume-from/dist/host/wiring.d.ts +41 -0
  173. package/node_modules/resume-from/dist/host/wiring.js +67 -0
  174. package/node_modules/resume-from/dist/import/confirmation.d.ts +6 -0
  175. package/node_modules/resume-from/dist/import/confirmation.js +27 -0
  176. package/node_modules/resume-from/dist/import/contract.d.ts +41 -0
  177. package/node_modules/resume-from/dist/import/contract.js +4 -0
  178. package/node_modules/resume-from/dist/import/discovery/contract.d.ts +58 -0
  179. package/node_modules/resume-from/dist/import/discovery/contract.js +4 -0
  180. package/node_modules/resume-from/dist/import/discovery/errors.d.ts +9 -0
  181. package/node_modules/resume-from/dist/import/discovery/errors.js +12 -0
  182. package/node_modules/resume-from/dist/import/discovery/finder.d.ts +10 -0
  183. package/node_modules/resume-from/dist/import/discovery/finder.js +129 -0
  184. package/node_modules/resume-from/dist/import/discovery/homes.d.ts +23 -0
  185. package/node_modules/resume-from/dist/import/discovery/homes.js +83 -0
  186. package/node_modules/resume-from/dist/import/discovery/index.d.ts +2 -0
  187. package/node_modules/resume-from/dist/import/discovery/index.js +2 -0
  188. package/node_modules/resume-from/dist/import/discovery/ordering.d.ts +3 -0
  189. package/node_modules/resume-from/dist/import/discovery/ordering.js +21 -0
  190. package/node_modules/resume-from/dist/import/errors.d.ts +22 -0
  191. package/node_modules/resume-from/dist/import/errors.js +20 -0
  192. package/node_modules/resume-from/dist/import/index.d.ts +3 -0
  193. package/node_modules/resume-from/dist/import/index.js +5 -0
  194. package/node_modules/resume-from/dist/import/landing/contract.d.ts +44 -0
  195. package/node_modules/resume-from/dist/import/landing/contract.js +4 -0
  196. package/node_modules/resume-from/dist/import/landing/errors.d.ts +15 -0
  197. package/node_modules/resume-from/dist/import/landing/errors.js +15 -0
  198. package/node_modules/resume-from/dist/import/landing/handover.d.ts +5 -0
  199. package/node_modules/resume-from/dist/import/landing/handover.js +12 -0
  200. package/node_modules/resume-from/dist/import/landing/index.d.ts +4 -0
  201. package/node_modules/resume-from/dist/import/landing/index.js +4 -0
  202. package/node_modules/resume-from/dist/import/landing/lander.d.ts +7 -0
  203. package/node_modules/resume-from/dist/import/landing/lander.js +133 -0
  204. package/node_modules/resume-from/dist/import/landing/marker.d.ts +9 -0
  205. package/node_modules/resume-from/dist/import/landing/marker.js +46 -0
  206. package/node_modules/resume-from/dist/import/pipeline.d.ts +17 -0
  207. package/node_modules/resume-from/dist/import/pipeline.js +134 -0
  208. package/node_modules/resume-from/dist/import/preview/builder.d.ts +6 -0
  209. package/node_modules/resume-from/dist/import/preview/builder.js +76 -0
  210. package/node_modules/resume-from/dist/import/preview/contract.d.ts +39 -0
  211. package/node_modules/resume-from/dist/import/preview/contract.js +4 -0
  212. package/node_modules/resume-from/dist/import/preview/format.d.ts +19 -0
  213. package/node_modules/resume-from/dist/import/preview/format.js +45 -0
  214. package/node_modules/resume-from/dist/import/preview/index.d.ts +2 -0
  215. package/node_modules/resume-from/dist/import/preview/index.js +1 -0
  216. package/node_modules/resume-from/dist/import/preview/warnings.d.ts +11 -0
  217. package/node_modules/resume-from/dist/import/preview/warnings.js +80 -0
  218. package/node_modules/resume-from/dist/import/transfer/contract.d.ts +48 -0
  219. package/node_modules/resume-from/dist/import/transfer/contract.js +4 -0
  220. package/node_modules/resume-from/dist/import/transfer/index.d.ts +2 -0
  221. package/node_modules/resume-from/dist/import/transfer/index.js +1 -0
  222. package/node_modules/resume-from/dist/import/transfer/rules.d.ts +3 -0
  223. package/node_modules/resume-from/dist/import/transfer/rules.js +228 -0
  224. package/node_modules/resume-from/dist/import/wiring.d.ts +24 -0
  225. package/node_modules/resume-from/dist/import/wiring.js +27 -0
  226. package/node_modules/resume-from/dist/index.d.ts +36 -0
  227. package/node_modules/resume-from/dist/index.js +36 -0
  228. package/node_modules/resume-from/dist/platform/config/contract.d.ts +35 -0
  229. package/node_modules/resume-from/dist/platform/config/contract.js +4 -0
  230. package/node_modules/resume-from/dist/platform/config/defaults.d.ts +16 -0
  231. package/node_modules/resume-from/dist/platform/config/defaults.js +22 -0
  232. package/node_modules/resume-from/dist/platform/config/errors.d.ts +9 -0
  233. package/node_modules/resume-from/dist/platform/config/errors.js +12 -0
  234. package/node_modules/resume-from/dist/platform/config/index.d.ts +5 -0
  235. package/node_modules/resume-from/dist/platform/config/index.js +6 -0
  236. package/node_modules/resume-from/dist/platform/config/loader.d.ts +10 -0
  237. package/node_modules/resume-from/dist/platform/config/loader.js +47 -0
  238. package/node_modules/resume-from/dist/platform/config/paths.d.ts +13 -0
  239. package/node_modules/resume-from/dist/platform/config/paths.js +37 -0
  240. package/node_modules/resume-from/dist/platform/config/validate.d.ts +9 -0
  241. package/node_modules/resume-from/dist/platform/config/validate.js +117 -0
  242. package/node_modules/resume-from/dist/platform/repo/contract.d.ts +30 -0
  243. package/node_modules/resume-from/dist/platform/repo/contract.js +4 -0
  244. package/node_modules/resume-from/dist/platform/repo/git.d.ts +21 -0
  245. package/node_modules/resume-from/dist/platform/repo/git.js +74 -0
  246. package/node_modules/resume-from/dist/platform/repo/index.d.ts +2 -0
  247. package/node_modules/resume-from/dist/platform/repo/index.js +1 -0
  248. package/node_modules/resume-from/dist/platform/repo/reader.d.ts +8 -0
  249. package/node_modules/resume-from/dist/platform/repo/reader.js +130 -0
  250. package/node_modules/resume-from/dist/platform/store/contract.d.ts +31 -0
  251. package/node_modules/resume-from/dist/platform/store/contract.js +4 -0
  252. package/node_modules/resume-from/dist/platform/store/file-committer.d.ts +3 -0
  253. package/node_modules/resume-from/dist/platform/store/file-committer.js +391 -0
  254. package/node_modules/resume-from/dist/platform/store/index.d.ts +2 -0
  255. package/node_modules/resume-from/dist/platform/store/index.js +2 -0
  256. package/node_modules/resume-from/dist/platform/tokens/contract.d.ts +12 -0
  257. package/node_modules/resume-from/dist/platform/tokens/contract.js +4 -0
  258. package/node_modules/resume-from/dist/platform/tokens/estimator.d.ts +12 -0
  259. package/node_modules/resume-from/dist/platform/tokens/estimator.js +111 -0
  260. package/node_modules/resume-from/dist/platform/tokens/index.d.ts +2 -0
  261. package/node_modules/resume-from/dist/platform/tokens/index.js +1 -0
  262. package/node_modules/resume-from/dist/session/contract.d.ts +109 -0
  263. package/node_modules/resume-from/dist/session/contract.js +4 -0
  264. package/node_modules/resume-from/package.json +80 -0
  265. package/node_modules/resume-from/shims/pi/extensions/resume-from.js +118 -0
  266. package/package.json +19 -14
@@ -0,0 +1,133 @@
1
+ import { LandingFailure } from "./errors.js";
2
+ import { buildHandover, describeSession } from "./handover.js";
3
+ import { buildMarker } from "./marker.js";
4
+ /** The one landing level that moves the user in (FR-43, FR-44). */
5
+ const SWITCH_LEVEL = "create-and-switch";
6
+ const RETRY = "Fix the cause, then run the import again.";
7
+ const COMMIT_NEXT_STEP = {
8
+ "path-exists": "Nothing was created and nothing existing was touched. Remove or rename that path, then run the import again.",
9
+ "not-writable": "Nothing was created. Give the target home write permission, then run the import again.",
10
+ "write-failed": "Nothing was created. Check the free space and the permissions of the target home, then run the import again.",
11
+ };
12
+ function reasonOf(cause) {
13
+ return cause instanceof Error ? cause.message : String(cause);
14
+ }
15
+ /**
16
+ * A rejection is only treated as a CommitError when its refusal is one this
17
+ * module knows a next step for. Anything else gets the generic next step
18
+ * instead of an interpolated "undefined" (FR-56).
19
+ */
20
+ function asCommitError(cause) {
21
+ if (typeof cause !== "object" || cause === null || !("refusal" in cause))
22
+ return null;
23
+ const { refusal } = cause;
24
+ return typeof refusal === "string" && Object.hasOwn(COMMIT_NEXT_STEP, refusal)
25
+ ? cause
26
+ : null;
27
+ }
28
+ function commitNextStep(refused) {
29
+ if (refused === null)
30
+ return `Nothing was created. ${RETRY}`;
31
+ if (refused.remainingPaths !== undefined && refused.remainingPaths.length > 0) {
32
+ return `Cleanup was incomplete. Inspect these paths, then run the import again: ${refused.remainingPaths.join(", ")}.`;
33
+ }
34
+ return COMMIT_NEXT_STEP[refused.refusal];
35
+ }
36
+ function failAfterCommit(createdPaths, stage, what, home) {
37
+ const preserved = createdPaths.length === 0
38
+ ? `No paths were created in ${home}.`
39
+ : `The committed paths were preserved because automatic removal could delete replacements. Inspect them before retrying: ${createdPaths.join(", ")}.`;
40
+ return new LandingFailure(stage, `${what} ${preserved} ${RETRY}`);
41
+ }
42
+ async function runLanding(plan, adapter, committer, runtime, importedAt, buildCommand) {
43
+ const home = plan.target.home;
44
+ const target = plan.target.agent;
45
+ // Second gate: the preview should already have stopped this plan (FR-33).
46
+ if (plan.blockedReason !== null) {
47
+ throw new LandingFailure("serialize",
48
+ // blockedReason ends in a sentence and carries the cause-specific advice (FR-33, FR-56).
49
+ `The plan cannot be imported: ${plan.blockedReason} Nothing was written to ${home}. ${RETRY}`);
50
+ }
51
+ const capabilities = adapter.capabilities();
52
+ const marker = buildMarker(plan, importedAt);
53
+ const session = {
54
+ provenance: plan.provenance,
55
+ turns: plan.turns,
56
+ };
57
+ let serialized;
58
+ try {
59
+ serialized = adapter.serialize(session, plan.target, marker);
60
+ }
61
+ catch (cause) {
62
+ throw new LandingFailure("serialize", `The ${target} adapter could not turn the plan into its own session format: ${reasonOf(cause)}. Nothing was written to ${home}. ${RETRY}`);
63
+ }
64
+ if (serialized.files.length !== 1) {
65
+ throw new LandingFailure("serialize", `The ${target} adapter produced ${serialized.files.length} files for one session. Exactly one file is required for atomic placement, so nothing was written to ${home}. Report this as an adapter bug.`);
66
+ }
67
+ let defects;
68
+ try {
69
+ defects = adapter.validate(serialized);
70
+ }
71
+ catch (cause) {
72
+ throw new LandingFailure("validate", `The ${target} adapter could not check the new session: ${reasonOf(cause)}. Nothing was written to ${home}. ${RETRY}`);
73
+ }
74
+ if (defects.length > 0) {
75
+ const summary = defects.map((defect) => `${defect.path}: ${defect.message}`).join("; ");
76
+ throw new LandingFailure("validate", `The new session is not valid for ${target}, so it was not placed: ${summary}. Nothing was written to ${home}. Report this as a bug, then run the import again once the adapter is fixed.`, { defects });
77
+ }
78
+ let createdPaths;
79
+ try {
80
+ ({ createdPaths } = await committer.commit(home, serialized.files));
81
+ }
82
+ catch (cause) {
83
+ const refused = asCommitError(cause);
84
+ const where = refused?.path ? ` (path: ${refused.path})` : "";
85
+ const next = commitNextStep(refused);
86
+ throw new LandingFailure("commit", `Creating the new session in ${home} failed: ${reasonOf(cause)}${where} ${next}`);
87
+ }
88
+ let stored;
89
+ try {
90
+ stored = await adapter.readBack(home, serialized.sessionId);
91
+ }
92
+ catch (cause) {
93
+ throw failAfterCommit(createdPaths, "read-back", `The new session in ${home} could not be read back: ${reasonOf(cause)}.`, home);
94
+ }
95
+ if (stored.sessionId !== serialized.sessionId) {
96
+ throw failAfterCommit(createdPaths, "read-back", `${target} read back session ${stored.sessionId} instead of the new session ${serialized.sessionId}.`, home);
97
+ }
98
+ if (stored.itemCount !== serialized.itemCount) {
99
+ throw failAfterCommit(createdPaths, "read-back", `${target} stored ${stored.itemCount} of the ${serialized.itemCount} items that were sent, so the session is incomplete.`, home);
100
+ }
101
+ if (!stored.openable) {
102
+ throw failAfterCommit(createdPaths, "read-back", `${target} could not open the new session ${serialized.sessionId}.`, home);
103
+ }
104
+ const ref = { agent: target, home, id: serialized.sessionId };
105
+ const landed = {
106
+ ref,
107
+ itemsSent: serialized.itemCount,
108
+ itemsStored: stored.itemCount,
109
+ marker,
110
+ };
111
+ const handover = () => buildHandover(buildCommand, target, serialized.sessionId);
112
+ if (capabilities.landing !== SWITCH_LEVEL) {
113
+ return { ...landed, switched: false, handover: handover() };
114
+ }
115
+ let outcome;
116
+ try {
117
+ outcome = await adapter.switchTo(home, serialized.sessionId, runtime);
118
+ }
119
+ catch (cause) {
120
+ // The session is valid, so it is kept: destroying it would destroy work.
121
+ throw new LandingFailure("switch", `The new session was created and is valid, but moving you into it failed: ${reasonOf(cause)}. The session was kept. Open it yourself: ${handover().command}`);
122
+ }
123
+ // A declined move is not a failure: the session stays and the user opens it later.
124
+ return outcome.switched
125
+ ? { ...landed, switched: true, handover: null }
126
+ : { ...landed, switched: false, handover: handover() };
127
+ }
128
+ export function createSessionLander(options = {}) {
129
+ const buildCommand = options.handoverCommand ?? describeSession;
130
+ return {
131
+ land: (plan, adapter, committer, runtime, importedAt) => runLanding(plan, adapter, committer, runtime, importedAt, buildCommand),
132
+ };
133
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProvenanceMarker, TransferPlan } from "./contract.js";
2
+ /** One line naming what the rules removed (FR-47). Never more than one line. */
3
+ export declare function summariseDrops(plan: TransferPlan): string;
4
+ /**
5
+ * The provenance marker the user sees (FR-47). `lines` renders the same facts
6
+ * in the order they must be shown; where they are shown is the adapter's
7
+ * declared capability (FR-48), not this module's decision.
8
+ */
9
+ export declare function buildMarker(plan: TransferPlan, importedAt: string): ProvenanceMarker;
@@ -0,0 +1,46 @@
1
+ const NOTHING_DROPPED = "Nothing was dropped: the whole session crossed over.";
2
+ const DROPPED_PREFIX = "Dropped on import: ";
3
+ const PART_SEPARATOR = "; ";
4
+ function counted(amount, one, many) {
5
+ return `${amount} ${amount === 1 ? one : many}`;
6
+ }
7
+ /** One line naming what the rules removed (FR-47). Never more than one line. */
8
+ export function summariseDrops(plan) {
9
+ const parts = [];
10
+ if (plan.droppedTurnCount > 0)
11
+ parts.push(counted(plan.droppedTurnCount, "turn", "turns"));
12
+ if (plan.bodiesDropped > 0) {
13
+ parts.push(counted(plan.bodiesDropped, "tool result body", "tool result bodies"));
14
+ }
15
+ const brokenTails = plan.drops.filter((drop) => drop.reason === "broken-tail").length;
16
+ if (brokenTails === 1)
17
+ parts.push("an incomplete trailing tool call");
18
+ if (brokenTails > 1)
19
+ parts.push(`${brokenTails} incomplete trailing tool calls`);
20
+ if (parts.length === 0)
21
+ return NOTHING_DROPPED;
22
+ return `${DROPPED_PREFIX}${parts.join(PART_SEPARATOR)}.`;
23
+ }
24
+ /**
25
+ * The provenance marker the user sees (FR-47). `lines` renders the same facts
26
+ * in the order they must be shown; where they are shown is the adapter's
27
+ * declared capability (FR-48), not this module's decision.
28
+ */
29
+ export function buildMarker(plan, importedAt) {
30
+ const { ref } = plan.provenance;
31
+ const droppedSummary = summariseDrops(plan);
32
+ return {
33
+ sourceAgent: ref.agent,
34
+ sourceHome: ref.home,
35
+ sourceSessionId: ref.id,
36
+ importedAt,
37
+ droppedSummary,
38
+ lines: [
39
+ `Imported from ${ref.agent}`,
40
+ `Source home: ${ref.home}`,
41
+ `Source session: ${ref.id}`,
42
+ `Imported at: ${importedAt}`,
43
+ droppedSummary,
44
+ ],
45
+ };
46
+ }
@@ -0,0 +1,17 @@
1
+ import type { AgentAdapter, FileCommitter, ImportConfig, ImportPipeline, PreviewBuilder, SessionFinder, SessionLander, TokenEstimator, TransferRules } from "./contract.js";
2
+ /** The four stages, and the services they are driven with. `wiring.ts` builds them. */
3
+ export interface PipelineStages {
4
+ finder: SessionFinder;
5
+ rules: TransferRules;
6
+ /** A preview builder that reads the repository the request names. */
7
+ previewFor(repoRoot: string): PreviewBuilder;
8
+ lander: SessionLander;
9
+ /** Every adapter the composition root constructed. This module only looks one up. */
10
+ adapters: readonly AgentAdapter[];
11
+ config: ImportConfig;
12
+ estimator: TokenEstimator;
13
+ committer: FileCommitter;
14
+ /** ISO-8601 UTC. Only the landing reads it, so a preview and a commit agree (FR-47). */
15
+ now(): string;
16
+ }
17
+ export declare function createPipelineFromStages(stages: PipelineStages): ImportPipeline;
@@ -0,0 +1,134 @@
1
+ // The three operations every host drives, and the order of the four stages behind them.
2
+ // Nothing here knows an agent: the source adapter comes from the chosen session and the
3
+ // target adapter from the target profile (FR-6, FR-60).
4
+ import { confirmationMatches, confirmationToken } from "./confirmation.js";
5
+ import { ImportFailure, reasonOf } from "./errors.js";
6
+ /** The role a target adapter must declare to receive an import (FR-59). */
7
+ const TARGET_ROLE = "target";
8
+ const PREVIEW_AGAIN = "Preview it again, then confirm.";
9
+ const IMPORT_AGAIN = "Fix the cause, then run the import again.";
10
+ function scopeOf(request) {
11
+ return {
12
+ repoRoot: request.repoRoot,
13
+ onlyAgent: request.onlyAgent,
14
+ onlyHome: request.onlyHome,
15
+ };
16
+ }
17
+ /**
18
+ * The adapter that receives the import. This lookup is the only place `AgentId` decides
19
+ * anything in this module, and a lookup is not a branch on the pair (FR-59, FR-60).
20
+ */
21
+ function targetAdapter(adapters, target) {
22
+ const found = adapters.filter((adapter) => adapter.capabilities().agent === target.agent);
23
+ if (found.length === 0) {
24
+ throw new ImportFailure("target", `There is no adapter for "${target.agent}", so an import cannot be sent to it. Run the import from an agent the tool supports, or add an adapter for "${target.agent}".`);
25
+ }
26
+ const usable = found.find((adapter) => adapter.capabilities().roles.includes(TARGET_ROLE));
27
+ if (!usable) {
28
+ throw new ImportFailure("target", `The "${target.agent}" adapter cannot receive an import: it declares the role(s) ${found[0]?.capabilities().roles.join(", ")} and not "${TARGET_ROLE}". Choose a target agent whose adapter has the "${TARGET_ROLE}" role.`);
29
+ }
30
+ return usable;
31
+ }
32
+ /** A rejection carrying what the landing already explained (FR-50, FR-56). */
33
+ function isLandingError(cause) {
34
+ return cause instanceof Error && "stage" in cause && "defects" in cause;
35
+ }
36
+ export function createPipelineFromStages(stages) {
37
+ async function resolve(request) {
38
+ try {
39
+ return await stages.finder.resolve(scopeOf(request), request.selection);
40
+ }
41
+ catch (cause) {
42
+ throw new ImportFailure("discovery", `The session could not be chosen: ${reasonOf(cause)}`, {
43
+ cause,
44
+ });
45
+ }
46
+ }
47
+ async function load(descriptor) {
48
+ try {
49
+ return await stages.finder.load(descriptor);
50
+ }
51
+ catch (cause) {
52
+ throw new ImportFailure("discovery", `The source session ${descriptor.ref.id} could not be read: ${reasonOf(cause)}. Nothing was written. Check that ${descriptor.filePath} still exists and can be read, then run the import again.`, { cause });
53
+ }
54
+ }
55
+ /**
56
+ * Resolve, load, apply the rules, build the report. `commit` runs exactly this again
57
+ * rather than taking a plan from the host: the numbered-list hosts of FR-10 are separate
58
+ * processes, and a plan cannot travel between them.
59
+ */
60
+ async function compute(request) {
61
+ const descriptor = await resolve(request);
62
+ const session = await load(descriptor);
63
+ let plan;
64
+ try {
65
+ plan = stages.rules.apply(session, request.target, stages.config, stages.estimator);
66
+ }
67
+ catch (cause) {
68
+ throw new ImportFailure("plan", `What to carry over could not be decided for session ${descriptor.ref.id}: ${reasonOf(cause)}. Nothing was written. ${IMPORT_AGAIN}`, { cause });
69
+ }
70
+ let report;
71
+ try {
72
+ report = await stages.previewFor(request.repoRoot).build(plan);
73
+ }
74
+ catch (cause) {
75
+ throw new ImportFailure("preview", `The preview of session ${descriptor.ref.id} could not be built: ${reasonOf(cause)}. Nothing was written. ${IMPORT_AGAIN}`, { cause });
76
+ }
77
+ const token = confirmationToken(descriptor, plan, report);
78
+ return {
79
+ descriptor,
80
+ plan,
81
+ report: { ...report, confirmationToken: token },
82
+ };
83
+ }
84
+ return {
85
+ async list(request) {
86
+ try {
87
+ return await stages.finder.list(scopeOf(request));
88
+ }
89
+ catch (cause) {
90
+ throw new ImportFailure("discovery", `The sessions of this repository could not be listed: ${reasonOf(cause)}. Check that ${request.repoRoot} is readable, then run the list again.`, { cause });
91
+ }
92
+ },
93
+ async preview(request) {
94
+ // Refused here as well as in commit, so a host is told about an impossible
95
+ // target before it shows the user a preview it could never confirm.
96
+ targetAdapter(stages.adapters, request.target);
97
+ // A blocked plan still produces a report: the user is told why it cannot run (FR-33).
98
+ return (await compute(request)).report;
99
+ },
100
+ async commit(request, runtime, suppliedToken) {
101
+ const adapter = targetAdapter(stages.adapters, request.target);
102
+ const { descriptor, plan, report } = await compute(request);
103
+ if (!confirmationMatches(report.confirmationToken, suppliedToken)) {
104
+ throw new ImportFailure("confirmation", `The source session or preview changed after confirmation. Nothing was written to ${request.target.home}. ${PREVIEW_AGAIN}`);
105
+ }
106
+ if (plan.blockedReason !== null) {
107
+ // blockedReason already ends in a sentence and carries the cause-specific
108
+ // advice (FR-33, FR-56); this adds only what the pipeline alone knows.
109
+ throw new ImportFailure("blocked", `The import cannot run: ${plan.blockedReason} Nothing was written to ${request.target.home}. ${PREVIEW_AGAIN}`);
110
+ }
111
+ // The freshness check. The listing reports when the source file last moved; the plan
112
+ // carries when the content it was built from was last recorded. A file that moved past
113
+ // its own last turn is a session the user has not seen (FR-20, FR-56).
114
+ if (descriptor.updatedAt !== plan.provenance.updatedAt) {
115
+ throw new ImportFailure("source-changed", `The source session ${descriptor.ref.id} changed after the preview: ${descriptor.filePath} was last written at ${descriptor.updatedAt}, and the conversation the preview showed ends at ${plan.provenance.updatedAt}. Nothing was written. ${PREVIEW_AGAIN}`);
116
+ }
117
+ try {
118
+ return await stages.lander.land(plan, adapter, stages.committer, runtime, stages.now());
119
+ }
120
+ catch (cause) {
121
+ // The landing states what failed and what to do next for every one of its stages;
122
+ // anything else reaching here is a defect and gets the generic next step (FR-56).
123
+ const known = isLandingError(cause);
124
+ const message = known
125
+ ? cause.message
126
+ : `The import could not be placed in ${request.target.home}: ${reasonOf(cause)}. ${IMPORT_AGAIN}`;
127
+ throw new ImportFailure("landing", message, {
128
+ cause,
129
+ defects: known ? cause.defects : [],
130
+ });
131
+ }
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,6 @@
1
+ import type { PreviewBuilder, RepoReader } from "./contract.js";
2
+ /**
3
+ * Builds the preview from a plan and the repository state read at the moment of the call.
4
+ * It writes nothing (FR-16) and renders one shape for every direction (FR-21).
5
+ */
6
+ export declare function createPreviewBuilder(repo: RepoReader, cwd: string): PreviewBuilder;
@@ -0,0 +1,76 @@
1
+ import { formatTokens, oneLine, plural } from "./format.js";
2
+ import { planWarnings, repoWarning, sortWarnings } from "./warnings.js";
3
+ /** Paths are longer than titles and are not shortened to the same length. */
4
+ const MAX_PATH_LENGTH = 160;
5
+ const MAX_REASON_LENGTH = 200;
6
+ const UNTITLED = "(untitled)";
7
+ // Each blockedReason now carries its own cause-specific advice (FR-33, FR-56), so no
8
+ // single fallback string is accurate for all block causes.
9
+ function headerLines(plan) {
10
+ const source = plan.provenance.ref;
11
+ const title = oneLine(plan.provenance.title) || UNTITLED;
12
+ return [
13
+ `Source: ${source.agent} — ${title} (${oneLine(source.home, MAX_PATH_LENGTH)})`,
14
+ `Target: ${plan.target.agent} (${oneLine(plan.target.home, MAX_PATH_LENGTH)})`,
15
+ countLine(plan),
16
+ ];
17
+ }
18
+ /** FR-17: both counts, and both come from the plan. */
19
+ function countLine(plan) {
20
+ const kept = plan.keptTurnCount;
21
+ const crossing = kept === 0
22
+ ? "Nothing would cross over"
23
+ : `${kept} ${plural(kept, "turn crosses", "turns cross")} over`;
24
+ return plan.droppedTurnCount > 0 ? `${crossing}, ${plan.droppedTurnCount} dropped` : crossing;
25
+ }
26
+ /** FR-35. A plan that dropped nothing produces no line at all — never "0 turns dropped". */
27
+ function dropLines(plan) {
28
+ const lines = [];
29
+ const budgetDrops = plan.drops.filter((drop) => drop.reason === "budget").length;
30
+ if (budgetDrops > 0) {
31
+ lines.push(`${budgetDrops} older ${plural(budgetDrops, "turn", "turns")} dropped`);
32
+ }
33
+ if (plan.bodiesDropped > 0) {
34
+ lines.push(`${plan.bodiesDropped} tool result ${plural(plan.bodiesDropped, "body", "bodies")} dropped`);
35
+ }
36
+ return lines;
37
+ }
38
+ function blockedLines(blockedReason) {
39
+ if (blockedReason === null)
40
+ return [];
41
+ return [`✖ The import cannot run: ${oneLine(blockedReason, MAX_REASON_LENGTH)}`];
42
+ }
43
+ /**
44
+ * Builds the preview from a plan and the repository state read at the moment of the call.
45
+ * It writes nothing (FR-16) and renders one shape for every direction (FR-21).
46
+ */
47
+ export function createPreviewBuilder(repo, cwd) {
48
+ return {
49
+ async build(plan) {
50
+ const collected = planWarnings(plan);
51
+ const fromRepo = await repoWarning(plan, repo, cwd);
52
+ if (fromRepo !== null)
53
+ collected.push(fromRepo);
54
+ const warnings = sortWarnings(collected);
55
+ const header = headerLines(plan);
56
+ const budgetLine = `Budget: ${formatTokens(plan.estimatedTokens)} tokens of a ${formatTokens(plan.target.windowTokens)} window`;
57
+ const drops = dropLines(plan);
58
+ const blocked = plan.blockedReason !== null;
59
+ return {
60
+ headerLines: header,
61
+ budgetLine,
62
+ warnings,
63
+ dropLines: drops,
64
+ blocked,
65
+ blockedReason: plan.blockedReason,
66
+ lines: [
67
+ ...header,
68
+ budgetLine,
69
+ ...warnings.map((warning) => warning.line),
70
+ ...drops,
71
+ ...blockedLines(plan.blockedReason),
72
+ ],
73
+ };
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,39 @@
1
+ import type { DropReason, PinReason, TransferPlan, TurnDrop, TurnPin } from "../transfer/contract.js";
2
+ export type { DropReason, PinReason, TransferPlan, TurnDrop, TurnPin };
3
+ import type { CommitDistance, RepoIdentity, RepoReader } from "../../platform/repo/contract.js";
4
+ export type { CommitDistance, RepoIdentity, RepoReader };
5
+ import type { AgentId, CanonicalSession, CanonicalTurn, HomePath, RepoSnapshot, SessionId, SessionRef, SourceProvenance, TargetProfile, ToolCallRecord, ToolEffect, TurnKind, TurnRole } from "../../session/contract.js";
6
+ export type { AgentId, CanonicalSession, CanonicalTurn, HomePath, RepoSnapshot, SessionId, SessionRef, SourceProvenance, TargetProfile, ToolCallRecord, ToolEffect, TurnKind, TurnRole, };
7
+ /** What a warning is about. Repository warnings sort first (FR-19). */
8
+ export type WarningKind = "repo-state" | "budget" | "broken-tail" | "capability";
9
+ /** One warning line of the preview (FR-19). */
10
+ export interface PreviewWarning {
11
+ kind: WarningKind;
12
+ /** The rendered line, ready to show. */
13
+ line: string;
14
+ }
15
+ /** Everything the user sees before confirming (FR-16 to FR-21). */
16
+ export interface PreviewReport {
17
+ /** Opaque binding that must be returned unchanged to commit this exact preview (FR-20). */
18
+ confirmationToken: string;
19
+ /** Source, target, and the turn counts that cross and are dropped (FR-17). */
20
+ headerLines: string[];
21
+ /** For example "Budget: 34k tokens of a 200k window" (FR-18). */
22
+ budgetLine: string;
23
+ /** Repository warnings first (FR-19). */
24
+ warnings: PreviewWarning[];
25
+ /** For example "12 older turns dropped" (FR-35). */
26
+ dropLines: string[];
27
+ /** True when the import cannot run. Nothing may be written (FR-33). */
28
+ blocked: boolean;
29
+ /** Set when blocked is true. States what to change. */
30
+ blockedReason: string | null;
31
+ /** The whole preview, rendered. The same shape for all nine directions (FR-21). */
32
+ lines: string[];
33
+ }
34
+ /** Preview content before the pipeline binds it to a confirmation token. */
35
+ export type PreviewContent = Omit<PreviewReport, "confirmationToken">;
36
+ /** Builds the preview from a plan and the current repository state. */
37
+ export interface PreviewBuilder {
38
+ build(plan: TransferPlan): Promise<PreviewContent>;
39
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/import/preview/module.md — the Public Contract section is the normative home.
2
+ // Declarations only: no behaviour, no defaults. If this file and module.md disagree,
3
+ // the document wins and this file is corrected.
4
+ export {};
@@ -0,0 +1,19 @@
1
+ /** How the preview writes numbers, commits and untrusted text. Nothing here reads a plan. */
2
+ /** The thousands abbreviation of FR-18: 34000 becomes "34k", 840 stays "840". */
3
+ export declare function formatTokens(tokens: number): string;
4
+ /** The short commit form the preview shows. */
5
+ export declare function shortCommit(commit: string): string;
6
+ /**
7
+ * True when two commits name the same one, whether abbreviated or full. A prefix match
8
+ * needs at least a short commit on both sides: source text is untrusted, and "9" must
9
+ * not silence the warning that the tree moved.
10
+ */
11
+ export declare function sameCommit(left: string, right: string): boolean;
12
+ /**
13
+ * One line in, one line out. Source titles and paths are attacker-controlled text
14
+ * (FR-60): folding newlines and control characters is what stops a session title
15
+ * from forging a preview line of its own.
16
+ */
17
+ export declare function oneLine(text: string, maxLength?: number): string;
18
+ /** Singular or plural, chosen by the count. */
19
+ export declare function plural(count: number, one: string, many: string): string;
@@ -0,0 +1,45 @@
1
+ /** How the preview writes numbers, commits and untrusted text. Nothing here reads a plan. */
2
+ const THOUSAND = 1000;
3
+ const SHORT_COMMIT_LENGTH = 7;
4
+ const MAX_TEXT_LENGTH = 80;
5
+ /** The thousands abbreviation of FR-18: 34000 becomes "34k", 840 stays "840". */
6
+ export function formatTokens(tokens) {
7
+ const value = Math.max(0, Math.round(tokens));
8
+ return value >= THOUSAND ? `${Math.round(value / THOUSAND)}k` : String(value);
9
+ }
10
+ /** The short commit form the preview shows. */
11
+ export function shortCommit(commit) {
12
+ return commit.replace(/\s+/gu, "").slice(0, SHORT_COMMIT_LENGTH);
13
+ }
14
+ /**
15
+ * True when two commits name the same one, whether abbreviated or full. A prefix match
16
+ * needs at least a short commit on both sides: source text is untrusted, and "9" must
17
+ * not silence the warning that the tree moved.
18
+ */
19
+ export function sameCommit(left, right) {
20
+ const a = left.replace(/\s+/gu, "").toLowerCase();
21
+ const b = right.replace(/\s+/gu, "").toLowerCase();
22
+ if (a === "" || b === "")
23
+ return false;
24
+ if (a === b)
25
+ return true;
26
+ if (a.length < SHORT_COMMIT_LENGTH || b.length < SHORT_COMMIT_LENGTH)
27
+ return false;
28
+ return a.startsWith(b) || b.startsWith(a);
29
+ }
30
+ /**
31
+ * One line in, one line out. Source titles and paths are attacker-controlled text
32
+ * (FR-60): folding newlines and control characters is what stops a session title
33
+ * from forging a preview line of its own.
34
+ */
35
+ export function oneLine(text, maxLength = MAX_TEXT_LENGTH) {
36
+ const flat = text
37
+ .replace(/\p{Cc}/gu, " ")
38
+ .replace(/\s+/gu, " ")
39
+ .trim();
40
+ return flat.length > maxLength ? `${flat.slice(0, maxLength)}…` : flat;
41
+ }
42
+ /** Singular or plural, chosen by the count. */
43
+ export function plural(count, one, many) {
44
+ return count === 1 ? one : many;
45
+ }
@@ -0,0 +1,2 @@
1
+ export { createPreviewBuilder } from "./builder.js";
2
+ export type { PreviewBuilder, PreviewContent, PreviewReport, PreviewWarning, WarningKind, } from "./contract.js";
@@ -0,0 +1 @@
1
+ export { createPreviewBuilder } from "./builder.js";
@@ -0,0 +1,11 @@
1
+ import type { PreviewWarning, RepoReader, TransferPlan } from "./contract.js";
2
+ /** Orders warnings by kind, keeping the order they were added within one kind. */
3
+ export declare function sortWarnings(warnings: PreviewWarning[]): PreviewWarning[];
4
+ /**
5
+ * Compares the source commit with the current HEAD (FR-37, FR-38). A repository that
6
+ * cannot be read, or a commit this tree does not have, is said to be unknown — never
7
+ * guessed and never left out (FR-36). None of these block (FR-39).
8
+ */
9
+ export declare function repoWarning(plan: TransferPlan, repo: RepoReader, cwd: string): Promise<PreviewWarning | null>;
10
+ /** The warnings that come from the plan alone. */
11
+ export declare function planWarnings(plan: TransferPlan): PreviewWarning[];
@@ -0,0 +1,80 @@
1
+ import { formatTokens, plural, sameCommit, shortCommit } from "./format.js";
2
+ /** Repository warnings sort first (FR-19); the rest follow this order. */
3
+ const KIND_ORDER = ["repo-state", "budget", "broken-tail", "capability"];
4
+ const NOT_KNOWN = "not known here";
5
+ const GLYPH = "⚠";
6
+ /** Orders warnings by kind, keeping the order they were added within one kind. */
7
+ export function sortWarnings(warnings) {
8
+ return [...warnings].sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind));
9
+ }
10
+ function repoState(line) {
11
+ return { kind: "repo-state", line };
12
+ }
13
+ /** "(14 commits ahead)", "(3 commits behind)", or "" when the tree did not move. */
14
+ function describeGap({ ahead, behind }) {
15
+ const commits = (count) => `${count} ${plural(count, "commit", "commits")}`;
16
+ if (ahead > 0 && behind > 0)
17
+ return ` (${commits(ahead)} ahead, ${behind} behind)`;
18
+ if (ahead > 0)
19
+ return ` (${commits(ahead)} ahead)`;
20
+ if (behind > 0)
21
+ return ` (${commits(behind)} behind)`;
22
+ return "";
23
+ }
24
+ /**
25
+ * Compares the source commit with the current HEAD (FR-37, FR-38). A repository that
26
+ * cannot be read, or a commit this tree does not have, is said to be unknown — never
27
+ * guessed and never left out (FR-36). None of these block (FR-39).
28
+ */
29
+ export async function repoWarning(plan, repo, cwd) {
30
+ const unreadable = repoState(`${GLYPH} The repository state could not be read, so the source commit is ${NOT_KNOWN}.`);
31
+ let identity;
32
+ try {
33
+ identity = await repo.identify(cwd);
34
+ }
35
+ catch {
36
+ return unreadable;
37
+ }
38
+ const sourceCommit = plan.provenance.repo.commit;
39
+ if (sourceCommit === null || sourceCommit.trim() === "") {
40
+ return repoState(`${GLYPH} The source session did not record a commit, so the repository state is ${NOT_KNOWN}.`);
41
+ }
42
+ const source = shortCommit(sourceCommit);
43
+ if (identity.root === null) {
44
+ return repoState(`${GLYPH} Source ran at ${source}. This directory is not a repository, so the repository state is ${NOT_KNOWN}.`);
45
+ }
46
+ if (identity.head === null) {
47
+ return repoState(`${GLYPH} Source ran at ${source}. This repository has no commit yet, so the repository state is ${NOT_KNOWN}.`);
48
+ }
49
+ if (sameCommit(sourceCommit, identity.head))
50
+ return null;
51
+ const head = shortCommit(identity.head);
52
+ let distance;
53
+ try {
54
+ distance = await repo.distanceFrom(sourceCommit);
55
+ }
56
+ catch {
57
+ return unreadable;
58
+ }
59
+ if (!distance.known) {
60
+ return repoState(`${GLYPH} Source ran at ${source}, which is ${NOT_KNOWN}. The tree is now at ${head}.`);
61
+ }
62
+ return repoState(`${GLYPH} Source ran at ${source}. The tree is now at ${head}${describeGap(distance)}.`);
63
+ }
64
+ /** The warnings that come from the plan alone. */
65
+ export function planWarnings(plan) {
66
+ const warnings = [];
67
+ if (plan.estimatedTokens > plan.budgetTokens) {
68
+ warnings.push({
69
+ kind: "budget",
70
+ line: `${GLYPH} The import is ${formatTokens(plan.estimatedTokens)} tokens, over its ${formatTokens(plan.budgetTokens)} budget.`,
71
+ });
72
+ }
73
+ if (plan.brokenTailDropped) {
74
+ warnings.push({
75
+ kind: "broken-tail",
76
+ line: `${GLYPH} The last tool call was incomplete and was dropped.`,
77
+ });
78
+ }
79
+ return warnings;
80
+ }