@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,391 @@
1
+ // The only place in the system that creates files. It adds files to a target home, never touches a
2
+ // file that already exists (FR-49), and atomically publishes at most one file (FR-53).
3
+ //
4
+ // Strategy: stage the file under a temporary name in its destination directory, then place it with
5
+ // `link`, which fails rather than overwrites. Staging first is what makes an interrupted
6
+ // commit leave no destination file at all; `link` instead of `rename` is what makes the check-then-
7
+ // place race fail the commit instead of destroying a file that appeared (C-3).
8
+ //
9
+ // This file has no runtime import of its own siblings on purpose: the interrupt test loads it in a
10
+ // bare Node child process.
11
+ import { randomBytes } from "node:crypto";
12
+ import { constants } from "node:fs";
13
+ import { access, link, lstat, mkdir, open, realpath, stat, unlink } from "node:fs/promises";
14
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
15
+ /** Leading dot and no session-like suffix: a leftover is a name no agent will read (T-STO-17). */
16
+ const TEMPORARY_PREFIX = ".resume-from-";
17
+ const TEMPORARY_SUFFIX = ".tmp";
18
+ class CommitFailure extends Error {
19
+ refusal;
20
+ path;
21
+ remainingPaths;
22
+ constructor(refusal, path, message, remainingPaths) {
23
+ super(message);
24
+ this.name = "CommitError";
25
+ this.refusal = refusal;
26
+ this.path = path;
27
+ if (remainingPaths !== undefined)
28
+ this.remainingPaths = remainingPaths;
29
+ }
30
+ }
31
+ /** Creates the guarded store. It holds no state between commits. */
32
+ export function createFileCommitter() {
33
+ return { commit };
34
+ }
35
+ async function commit(root, files) {
36
+ checkCardinality(files);
37
+ checkPaths(root, files);
38
+ const createdDirs = [];
39
+ const createdFiles = [];
40
+ const staged = [];
41
+ if (files.length === 0) {
42
+ return { createdPaths: [] };
43
+ }
44
+ let resolvedRoot;
45
+ try {
46
+ await createTargetRoot(root, createdDirs);
47
+ await verifyOwnedDirectories(createdDirs);
48
+ resolvedRoot = await checkDestinations(root, files);
49
+ await verifyOwnedDirectories(createdDirs);
50
+ }
51
+ catch (error) {
52
+ const remainingPaths = [
53
+ ...remainingFrom(error),
54
+ ...createdPathNames(createdFiles, createdDirs),
55
+ ];
56
+ throw asCommitFailure(error, remainingPaths);
57
+ }
58
+ try {
59
+ for (const file of files) {
60
+ await createParents(dirname(file.absolutePath), resolvedRoot, createdDirs);
61
+ await checkResolvedInside(resolvedRoot, dirname(file.absolutePath));
62
+ staged.push(await stage(file, resolvedRoot));
63
+ }
64
+ for (const item of staged) {
65
+ await place(item);
66
+ createdFiles.push({ path: item.destination, ...item.identity });
67
+ }
68
+ }
69
+ catch (error) {
70
+ const temporaryPaths = await discard(staged);
71
+ const remainingPaths = [
72
+ ...remainingFrom(error),
73
+ ...temporaryPaths,
74
+ ...createdPathNames(createdFiles, createdDirs),
75
+ ];
76
+ throw asCommitFailure(error, remainingPaths);
77
+ }
78
+ const remainingTemporaryPaths = await discard(staged);
79
+ if (remainingTemporaryPaths.length > 0) {
80
+ // Placement succeeded: the published destination is real. Only the temporary staging file(s)
81
+ // could not be removed. Pass them separately so the message is honest about what happened.
82
+ const publishedPaths = createdPathNames(createdFiles, createdDirs);
83
+ throw cleanupFailure(remainingTemporaryPaths, publishedPaths);
84
+ }
85
+ return { createdPaths: createdFiles.map(({ path }) => path) };
86
+ }
87
+ function checkCardinality(files) {
88
+ if (files.length <= 1)
89
+ return;
90
+ throw new CommitFailure("write-failed", null, `Refused to create ${files.length} files in one commit. A session must serialize to one file so placement is atomic across process interruption.`);
91
+ }
92
+ /** Creates a missing target root as part of this commit and records it for failure reporting. */
93
+ async function createTargetRoot(root, createdDirs) {
94
+ const missing = [];
95
+ let current = root;
96
+ while (!(await exists(current))) {
97
+ missing.push(current);
98
+ const parent = dirname(current);
99
+ if (parent === current)
100
+ break;
101
+ current = parent;
102
+ }
103
+ for (const missingDir of missing.reverse()) {
104
+ try {
105
+ await mkdir(missingDir, { mode: 0o700 });
106
+ const info = await lstat(missingDir);
107
+ createdDirs.push({ path: missingDir, dev: info.dev, ino: info.ino });
108
+ }
109
+ catch (error) {
110
+ if (codeOf(error) === "EEXIST")
111
+ continue;
112
+ throw new CommitFailure("write-failed", missingDir, writeFailed(missingDir, error));
113
+ }
114
+ }
115
+ }
116
+ async function verifyOwnedDirectories(directories) {
117
+ for (const owned of directories) {
118
+ const info = await lstat(owned.path).catch(() => null);
119
+ if (info === null || !info.isDirectory() || !sameIdentity(owned, info)) {
120
+ throw new CommitFailure("write-failed", owned.path, `Refused to use target root "${owned.path}": it changed while it was being created.`);
121
+ }
122
+ }
123
+ }
124
+ /** Refuses what no filesystem call could tell us: a relative path, or the same path listed twice. */
125
+ function checkPaths(root, files) {
126
+ if (!isAbsolute(root)) {
127
+ throw new CommitFailure("write-failed", root, `Refused to use target root "${root}": the root is not absolute.`);
128
+ }
129
+ const normalizedRoot = resolve(root);
130
+ const seen = new Set();
131
+ for (const file of files) {
132
+ const path = file.absolutePath;
133
+ if (!isAbsolute(path)) {
134
+ throw new CommitFailure("write-failed", path, `Refused to write "${path}": the path is not absolute. Pass an absolute path — a relative path is never resolved against the current directory.`);
135
+ }
136
+ const key = resolve(path);
137
+ if (!isInside(normalizedRoot, key)) {
138
+ throw new CommitFailure("write-failed", path, `Refused to write "${path}": it is outside the target root "${root}".`);
139
+ }
140
+ if (seen.has(key)) {
141
+ throw new CommitFailure("write-failed", path, `Refused to write "${path}": the same path is listed twice in one commit. Remove the duplicate — this store never overwrites a file, not even one it just created.`);
142
+ }
143
+ seen.add(key);
144
+ }
145
+ }
146
+ /** Runs before the first byte is written: nothing may exist, and every destination must be reachable. */
147
+ async function checkDestinations(root, files) {
148
+ let resolvedRoot;
149
+ try {
150
+ const rootInfo = await stat(root);
151
+ if (!rootInfo.isDirectory()) {
152
+ throw new CommitFailure("not-writable", root, `Refused to use target root "${root}": it is not a directory.`);
153
+ }
154
+ resolvedRoot = await realpath(root);
155
+ }
156
+ catch (error) {
157
+ if (error instanceof CommitFailure)
158
+ throw error;
159
+ throw new CommitFailure("not-writable", root, notWritable(root));
160
+ }
161
+ for (const file of files) {
162
+ if (await exists(file.absolutePath)) {
163
+ throw new CommitFailure("path-exists", file.absolutePath, alreadyExists(file.absolutePath));
164
+ }
165
+ }
166
+ const checked = new Set();
167
+ for (const file of files) {
168
+ const parent = dirname(file.absolutePath);
169
+ if (checked.has(parent))
170
+ continue;
171
+ checked.add(parent);
172
+ const anchor = await nearestExisting(parent);
173
+ // No ancestor at all, or an ancestor that is not a directory: only the write can say what is
174
+ // wrong there, and it reports it as a failed write.
175
+ if (anchor === null || !anchor.isDirectory)
176
+ continue;
177
+ await checkResolvedInside(resolvedRoot, anchor.path);
178
+ try {
179
+ await access(anchor.path, constants.W_OK | constants.X_OK);
180
+ }
181
+ catch {
182
+ throw new CommitFailure("not-writable", anchor.path, notWritable(anchor.path));
183
+ }
184
+ }
185
+ return resolvedRoot;
186
+ }
187
+ /** Opens an empty temporary file, proves where its inode lives, then writes through the stable FD. */
188
+ async function stage(file, resolvedRoot) {
189
+ const destination = file.absolutePath;
190
+ const temporary = join(dirname(destination), `${TEMPORARY_PREFIX}${randomBytes(8).toString("hex")}${TEMPORARY_SUFFIX}`);
191
+ let identity = null;
192
+ try {
193
+ const handle = await open(temporary, "wx", 0o600);
194
+ try {
195
+ const info = await handle.stat();
196
+ identity = { dev: info.dev, ino: info.ino };
197
+ await verifyFileInside(resolvedRoot, temporary, identity);
198
+ await handle.writeFile(file.bytes);
199
+ return {
200
+ temporary,
201
+ destination,
202
+ identity,
203
+ };
204
+ }
205
+ finally {
206
+ await handle.close();
207
+ }
208
+ }
209
+ catch (error) {
210
+ if (identity !== null && !(await removeOwnedFile({ path: temporary, ...identity }))) {
211
+ throw new CommitFailure("write-failed", destination, `${writeFailed(destination, error)} Cleanup was incomplete. This path may still exist; inspect it before retrying: ${temporary}.`, [temporary]);
212
+ }
213
+ throw new CommitFailure("write-failed", destination, writeFailed(destination, error));
214
+ }
215
+ }
216
+ async function verifyFileInside(resolvedRoot, path, identity) {
217
+ const resolvedPath = await realpath(path).catch((error) => {
218
+ throw new CommitFailure("write-failed", path, writeFailed(path, error));
219
+ });
220
+ const current = await lstat(path).catch(() => null);
221
+ if (current === null || !current.isFile() || !sameIdentity(identity, current)) {
222
+ throw new CommitFailure("write-failed", path, `Refused to stage through "${path}": it changed after the file was opened.`);
223
+ }
224
+ if (!isInside(resolvedRoot, resolvedPath)) {
225
+ throw new CommitFailure("write-failed", path, `Refused to stage through "${path}": it resolves outside the target root.`);
226
+ }
227
+ }
228
+ /** Publishes a staged file at its destination. Never overwrites: `link` fails if the path exists. */
229
+ async function place({ temporary, destination }) {
230
+ try {
231
+ await link(temporary, destination);
232
+ }
233
+ catch (error) {
234
+ if (codeOf(error) === "EEXIST") {
235
+ throw new CommitFailure("path-exists", destination, alreadyExists(destination));
236
+ }
237
+ throw new CommitFailure("write-failed", destination, writeFailed(destination, error));
238
+ }
239
+ }
240
+ /** Creates the missing directories of a chain, recording only the ones this commit created. */
241
+ async function createParents(directory, resolvedRoot, createdDirs) {
242
+ const missing = [];
243
+ let current = directory;
244
+ while (!(await exists(current))) {
245
+ missing.push(current);
246
+ const parent = dirname(current);
247
+ if (parent === current)
248
+ break;
249
+ current = parent;
250
+ }
251
+ for (const missingDir of missing.reverse()) {
252
+ try {
253
+ await mkdir(missingDir, { mode: 0o700 });
254
+ await checkResolvedInside(resolvedRoot, missingDir);
255
+ const info = await lstat(missingDir);
256
+ createdDirs.push({ path: missingDir, dev: info.dev, ino: info.ino });
257
+ }
258
+ catch (error) {
259
+ if (codeOf(error) === "EEXIST")
260
+ continue; // someone else created it; not ours to remove
261
+ throw new CommitFailure("write-failed", missingDir, writeFailed(missingDir, error));
262
+ }
263
+ }
264
+ }
265
+ /** Removes leftover staged files and returns the exact paths that could not be removed. */
266
+ async function discard(staged) {
267
+ const remaining = [];
268
+ for (const item of staged) {
269
+ const owned = { path: item.temporary, ...item.identity };
270
+ if (!(await removeOwnedFile(owned)))
271
+ remaining.push(item.temporary);
272
+ }
273
+ return remaining;
274
+ }
275
+ /** Lists paths created by the commit, with directories ordered deepest first. */
276
+ function createdPathNames(files, directories) {
277
+ return [...files.map(({ path }) => path), ...[...directories].reverse().map(({ path }) => path)];
278
+ }
279
+ async function removeOwnedFile(owned) {
280
+ const current = await identityAt(owned.path);
281
+ if (current === null)
282
+ return true;
283
+ if (!sameIdentity(owned, current))
284
+ return false;
285
+ try {
286
+ await unlink(owned.path);
287
+ return true;
288
+ }
289
+ catch (error) {
290
+ return isAbsent(error);
291
+ }
292
+ }
293
+ async function identityAt(path) {
294
+ try {
295
+ const info = await lstat(path);
296
+ return { dev: info.dev, ino: info.ino };
297
+ }
298
+ catch (error) {
299
+ if (isAbsent(error))
300
+ return null;
301
+ return { dev: Number.NaN, ino: Number.NaN };
302
+ }
303
+ }
304
+ function sameIdentity(left, right) {
305
+ return left.dev === right.dev && left.ino === right.ino;
306
+ }
307
+ async function checkResolvedInside(resolvedRoot, path) {
308
+ const resolvedPath = await realpath(path).catch((error) => {
309
+ throw new CommitFailure("not-writable", path, writeFailed(path, error));
310
+ });
311
+ if (!isInside(resolvedRoot, resolvedPath)) {
312
+ throw new CommitFailure("write-failed", path, `Refused to write through "${path}": it resolves outside the target root.`);
313
+ }
314
+ }
315
+ function isInside(root, path) {
316
+ const fromRoot = relative(root, path);
317
+ return (fromRoot === "" ||
318
+ (!isAbsolute(fromRoot) && !fromRoot.startsWith(`..${sep}`) && fromRoot !== ".."));
319
+ }
320
+ /** True when the path is taken, symlinks included — a dangling symlink is still a taken path. */
321
+ async function exists(path) {
322
+ try {
323
+ await lstat(path);
324
+ return true;
325
+ }
326
+ catch (error) {
327
+ const code = codeOf(error);
328
+ if (code === "ENOENT" || code === "ENOTDIR")
329
+ return false;
330
+ throw new CommitFailure("not-writable", path, notWritable(dirname(path)));
331
+ }
332
+ }
333
+ /** The closest ancestor of `directory` that exists, or null at the root. */
334
+ async function nearestExisting(directory) {
335
+ let current = directory;
336
+ for (;;) {
337
+ const info = await stat(current).catch(() => null);
338
+ if (info !== null)
339
+ return { path: current, isDirectory: info.isDirectory() };
340
+ const parent = dirname(current);
341
+ if (parent === current)
342
+ return null;
343
+ current = parent;
344
+ }
345
+ }
346
+ function asCommitFailure(error, remainingPaths) {
347
+ if (remainingPaths.length > 0) {
348
+ const original = error instanceof Error ? `${error.message} ` : "";
349
+ return new CommitFailure("write-failed", null, `${original}Cleanup was incomplete. These paths were retained or may still exist; inspect them before retrying: ${remainingPaths.join(", ")}.`, remainingPaths);
350
+ }
351
+ if (error instanceof CommitFailure)
352
+ return error;
353
+ return new CommitFailure("write-failed", null, `Failed to write the requested file: ${reasonOf(error)}. No destination was published. Check the target and the free space, then run the command again.`);
354
+ }
355
+ function remainingFrom(error) {
356
+ if (typeof error !== "object" ||
357
+ error === null ||
358
+ !("remainingPaths" in error) ||
359
+ !Array.isArray(error.remainingPaths)) {
360
+ return [];
361
+ }
362
+ return error.remainingPaths.filter((path) => typeof path === "string");
363
+ }
364
+ // FR-53: after successful placement the temporary staging file(s) failed to remove. The published
365
+ // destination is already on disk — only the temps need manual removal. `remainingPaths` carries
366
+ // only the temps so callers do not list a successfully committed file as a leftover.
367
+ function cleanupFailure(temporaryPaths, publishedPaths) {
368
+ return new CommitFailure("write-failed", null, `The file was placed successfully, but the temporary staging file could not be removed. Only these paths need inspection or manual removal: ${temporaryPaths.join(", ")}. The destination was published successfully: ${publishedPaths.join(", ")}. A retry will report path-exists because the import already landed.`, temporaryPaths);
369
+ }
370
+ function alreadyExists(path) {
371
+ return `Refused to write "${path}": that path already exists. This tool only adds files. Remove or rename the existing file, or choose a different target, then run the command again.`;
372
+ }
373
+ function notWritable(directory) {
374
+ return `Refused to write into "${directory}": the directory is not writable. Check its permissions, or choose a different target, then run the command again.`;
375
+ }
376
+ function writeFailed(path, cause) {
377
+ return `Failed to write "${path}": ${reasonOf(cause)}. Check the path and the free space, then run the command again.`;
378
+ }
379
+ function codeOf(error) {
380
+ if (typeof error === "object" && error !== null && "code" in error) {
381
+ return String(error.code);
382
+ }
383
+ return "";
384
+ }
385
+ function isAbsent(error) {
386
+ const code = codeOf(error);
387
+ return code === "ENOENT" || code === "ENOTDIR";
388
+ }
389
+ function reasonOf(error) {
390
+ return error instanceof Error ? error.message : String(error);
391
+ }
@@ -0,0 +1,2 @@
1
+ export type { Bytes, CommitError, CommitHandle, CommitRefusal, FileCommitter, PendingFile, } from "./contract.js";
2
+ export { createFileCommitter } from "./file-committer.js";
@@ -0,0 +1,2 @@
1
+ // Guarded file store — the only module that creates files at runtime.
2
+ export { createFileCommitter } from "./file-committer.js";
@@ -0,0 +1,12 @@
1
+ /** Which counting rule to use. One value per model family the targets use. */
2
+ export type EstimatorFamily = "claude" | "gpt" | "generic";
3
+ /** Estimates how many tokens a piece of text costs. */
4
+ export interface TokenEstimator {
5
+ /** Deterministic: the same text always returns the same count. Never negative. */
6
+ estimate(text: string): number;
7
+ }
8
+ /** Chooses an estimator. */
9
+ export interface EstimatorFactory {
10
+ /** Returns the estimator for a family. Falls back to "generic" for an unknown family. */
11
+ forFamily(family: EstimatorFamily): TokenEstimator;
12
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/platform/tokens/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,12 @@
1
+ import type { EstimatorFactory } from "./contract.js";
2
+ /**
3
+ * Counts the tokens of one piece of text. The whole of what this module knows about a
4
+ * tokenizer library: replacing the library is replacing this function.
5
+ */
6
+ export type EncodeCount = (text: string) => number;
7
+ /**
8
+ * Builds a factory over a given encoder. The parameter is the seam the tests use to stand a
9
+ * failing encoder up; every caller outside this module wants `estimatorFactory`.
10
+ */
11
+ export declare const createEstimatorFactory: (count?: EncodeCount) => EstimatorFactory;
12
+ export declare const estimatorFactory: EstimatorFactory;
@@ -0,0 +1,111 @@
1
+ import { encode } from "gpt-tokenizer/encoding/o200k_base";
2
+ /** How much text a segment holds before the next word start closes it. */
3
+ const SEGMENT_CHARS = 64;
4
+ /** Where a segment is cut when no word start follows — a run of text with no space in it. */
5
+ const SEGMENT_LIMIT_CHARS = 128;
6
+ /**
7
+ * The exact counter calls the encoder once per character, so it is bounded by an offset:
8
+ * text past this point is charged with the byte upper bound (`utf8UpperBound`) instead.
9
+ * 256 kB is already far past every target's context window, and the switch is by position
10
+ * rather than by content, which is what keeps the estimate monotone.
11
+ *
12
+ * Measured cost of the exact path: ≈ 760 ms at this ceiling (≈ 0.003 ms/char, ≈ 42× a
13
+ * single encode() call on the same text). Text beyond this offset is free by comparison:
14
+ * utf8UpperBound adds < 1 ms/MB. Revisit if a target's context window grows past 256 kB
15
+ * and exact counting at full length becomes necessary.
16
+ */
17
+ const EXACT_BUDGET_CHARS = 262_144;
18
+ /** UTF-8 bytes per token. The rule of thumb the heuristic families are built on. */
19
+ const BYTES_PER_TOKEN = 4;
20
+ /** A byte-level tokenizer cannot emit more tokens than the UTF-8 bytes it consumes. */
21
+ const utf8UpperBound = (text) => Buffer.byteLength(text, "utf8");
22
+ const isWhitespace = (code) => code === 32 || (code >= 9 && code <= 13);
23
+ /** Characters outside the BMP — emoji, mostly — cost far more tokens per byte than text does. */
24
+ const astralCount = (text) => {
25
+ let count = 0;
26
+ for (let index = 0; index < text.length; index++) {
27
+ const unit = text.charCodeAt(index);
28
+ if (unit >= 0xd800 && unit <= 0xdbff)
29
+ count++;
30
+ }
31
+ return count;
32
+ };
33
+ /**
34
+ * The counting rule for every family without an exact encoder, and the fallback for the one
35
+ * that has one. Monotone by construction: both terms only grow as text is appended.
36
+ */
37
+ const heuristicCount = (text) => Math.ceil(Buffer.byteLength(text, "utf8") / BYTES_PER_TOKEN) + astralCount(text);
38
+ /**
39
+ * Where the segment starting at `start` ends: the first word start past `SEGMENT_CHARS`, or
40
+ * the hard limit if the text runs on without one.
41
+ *
42
+ * The scan only ever moves forward, so a boundary is decided by the text before it and
43
+ * nothing appended later can move it. Earlier segments are therefore frozen, which is half
44
+ * of why the estimate cannot fall.
45
+ */
46
+ const segmentEnd = (text, start) => {
47
+ const end = Math.min(start + SEGMENT_LIMIT_CHARS, text.length);
48
+ for (let index = start + SEGMENT_CHARS; index < end; index++) {
49
+ const startsWord = !isWhitespace(text.charCodeAt(index)) && isWhitespace(text.charCodeAt(index - 1));
50
+ if (startsWord)
51
+ return index;
52
+ }
53
+ return end;
54
+ };
55
+ /**
56
+ * The largest count of any prefix of `segment`.
57
+ *
58
+ * Encoding is not monotone: appending a character can merge two tokens into one, so the raw
59
+ * count of a longer text is sometimes lower. Taking the envelope over the prefixes removes
60
+ * that, at the cost of over-counting by the size of the dip it absorbs.
61
+ */
62
+ const prefixEnvelope = (segment, count) => {
63
+ let largest = 0;
64
+ for (let end = 1; end <= segment.length; end++) {
65
+ const tokens = count(segment.slice(0, end));
66
+ if (tokens > largest)
67
+ largest = tokens;
68
+ }
69
+ return largest;
70
+ };
71
+ const exactCount = (text, count) => {
72
+ let total = 0;
73
+ let start = 0;
74
+ while (start < text.length) {
75
+ const end = segmentEnd(text, start);
76
+ const segment = text.slice(start, end);
77
+ total += start < EXACT_BUDGET_CHARS ? prefixEnvelope(segment, count) : utf8UpperBound(segment);
78
+ start = end;
79
+ }
80
+ return total;
81
+ };
82
+ const heuristicEstimator = { estimate: heuristicCount };
83
+ const exactEstimator = (count) => ({
84
+ estimate(text) {
85
+ try {
86
+ return exactCount(text, count);
87
+ }
88
+ catch {
89
+ return utf8UpperBound(text);
90
+ }
91
+ },
92
+ });
93
+ const encodeCount = (text) => encode(text).length;
94
+ /**
95
+ * Builds a factory over a given encoder. The parameter is the seam the tests use to stand a
96
+ * failing encoder up; every caller outside this module wants `estimatorFactory`.
97
+ */
98
+ export const createEstimatorFactory = (count = encodeCount) => {
99
+ const byFamily = new Map([
100
+ ["gpt", exactEstimator(count)],
101
+ // No public JavaScript tokenizer counts the Claude family, so it shares the heuristic.
102
+ ["claude", heuristicEstimator],
103
+ ["generic", heuristicEstimator],
104
+ ]);
105
+ return {
106
+ forFamily(family) {
107
+ return byFamily.get(family) ?? heuristicEstimator;
108
+ },
109
+ };
110
+ };
111
+ export const estimatorFactory = createEstimatorFactory();
@@ -0,0 +1,2 @@
1
+ export type { EstimatorFactory, EstimatorFamily, TokenEstimator } from "./contract.js";
2
+ export { createEstimatorFactory, estimatorFactory } from "./estimator.js";
@@ -0,0 +1 @@
1
+ export { createEstimatorFactory, estimatorFactory } from "./estimator.js";
@@ -0,0 +1,109 @@
1
+ /** Which agent produced or receives a session. Adding an agent adds one value (FR-57). */
2
+ export type AgentId = "pi" | "codex" | "claude-code";
3
+ /** Absolute path of an agent profile directory, for example "/Users/me/.claude-team" (FR-2). */
4
+ export type HomePath = string;
5
+ /** The agent's own identifier for a session. Unique inside one home. */
6
+ export type SessionId = string;
7
+ /** A session is identified by three values (FR-1). */
8
+ export interface SessionRef {
9
+ agent: AgentId;
10
+ home: HomePath;
11
+ id: SessionId;
12
+ }
13
+ /** One row of the selection list (FR-11). */
14
+ export interface SessionDescriptor {
15
+ ref: SessionRef;
16
+ /** Short human title. Derived from the first user message when the format has none. */
17
+ title: string;
18
+ /** ISO-8601 UTC. */
19
+ startedAt: string;
20
+ /** ISO-8601 UTC. Sort key of the listing, newest first (FR-14). */
21
+ updatedAt: string;
22
+ /** Turns the source holds, before any rule of section D or E runs. */
23
+ turnCount: number;
24
+ /** Absolute path of the repository the session ran in, or null when unknown (FR-13). */
25
+ repoPath: string | null;
26
+ /** Absolute path of the source file. Lets the user select by path (FR-12). */
27
+ filePath: string;
28
+ }
29
+ /** Who produced a turn. */
30
+ export type TurnRole = "user" | "agent";
31
+ /** Why the turn exists. Decides pinning and drop order (FR-22, FR-31, FR-32). */
32
+ export type TurnKind = "message" | "summary" | "tool-call";
33
+ /** Did the call change the repository? (FR-26) */
34
+ export type ToolEffect = "read-only" | "mutating" | "unknown";
35
+ /**
36
+ * A tool call that crossed over (FR-23).
37
+ * There is deliberately no field for the result body: FR-24 and FR-60 are
38
+ * enforced by this shape, not by adapter discipline.
39
+ */
40
+ export interface ToolCallRecord {
41
+ /** The original tool name. Never translated (FR-27). */
42
+ toolName: string;
43
+ /** The source arguments after deterministic credential redaction. */
44
+ argumentsText: string;
45
+ /** Exactly one line about the outcome (FR-23). */
46
+ outcomeLine: string;
47
+ effect: ToolEffect;
48
+ /** True when the source had a result body and it was dropped (FR-25). */
49
+ bodyDropped: boolean;
50
+ /**
51
+ * True when the source recorded any answer to this call (even an empty or error result).
52
+ * False when no result entry exists at all — the broken-tail signal (FR-54).
53
+ * This is a presence flag, not a content field; it cannot hold a result body.
54
+ */
55
+ resultRecorded?: boolean;
56
+ }
57
+ /** One turn of the canonical session. */
58
+ export interface CanonicalTurn {
59
+ /** Zero-based position in the source session. Stable across re-reads. */
60
+ index: number;
61
+ role: TurnRole;
62
+ kind: TurnKind;
63
+ /** Visible text. Empty when kind is "tool-call". */
64
+ text: string;
65
+ /** Set when kind is "tool-call", null otherwise. */
66
+ toolCall: ToolCallRecord | null;
67
+ /** ISO-8601 UTC when the source recorded one, null otherwise. */
68
+ timestamp: string | null;
69
+ }
70
+ /** Repository state of the source session (FR-36). */
71
+ export interface RepoSnapshot {
72
+ /** Commit the source ran at, or null when the source format does not record it. */
73
+ commit: string | null;
74
+ branch: string | null;
75
+ /** Files the source changed, derived from its mutating tool calls. */
76
+ changedPaths: string[];
77
+ }
78
+ /** Where a session came from (FR-22 metadata, FR-47 provenance). */
79
+ export interface SourceProvenance {
80
+ ref: SessionRef;
81
+ title: string;
82
+ startedAt: string;
83
+ updatedAt: string;
84
+ repo: RepoSnapshot;
85
+ }
86
+ /** A source session in the neutral vocabulary. Every rule of sections D to I runs on this. */
87
+ export interface CanonicalSession {
88
+ provenance: SourceProvenance;
89
+ turns: CanonicalTurn[];
90
+ }
91
+ /** Where the import is going, and how much room it has (FR-18, FR-29). */
92
+ export interface TargetProfile {
93
+ agent: AgentId;
94
+ home: HomePath;
95
+ /** Context window of the target, in tokens. */
96
+ windowTokens: number;
97
+ }
98
+ /** The import marker the user sees. It never enters the model context (FR-47, FR-48). */
99
+ export interface ProvenanceMarker {
100
+ sourceAgent: AgentId;
101
+ sourceHome: HomePath;
102
+ sourceSessionId: SessionId;
103
+ /** ISO-8601 UTC. */
104
+ importedAt: string;
105
+ /** One line naming what the rules dropped. */
106
+ droppedSummary: string;
107
+ /** The rendered marker, in the order it must be shown. */
108
+ lines: string[];
109
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/session/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 {};