@nanonets/graft 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/README.md +549 -315
  2. package/dist/app/review-process.d.ts.map +1 -1
  3. package/dist/app/review-process.js +6 -5
  4. package/dist/app/review-process.js.map +1 -1
  5. package/dist/app/sources.js +2 -2
  6. package/dist/app/sources.js.map +1 -1
  7. package/dist/ask/ask.d.ts +12 -6
  8. package/dist/ask/ask.d.ts.map +1 -1
  9. package/dist/ask/ask.js +28 -38
  10. package/dist/ask/ask.js.map +1 -1
  11. package/dist/bin/graft.d.ts +3 -0
  12. package/dist/bin/graft.d.ts.map +1 -0
  13. package/dist/bin/graft.js +8 -0
  14. package/dist/bin/graft.js.map +1 -0
  15. package/dist/bin/trail.d.ts +3 -0
  16. package/dist/bin/trail.d.ts.map +1 -0
  17. package/dist/bin/trail.js +6 -0
  18. package/dist/bin/trail.js.map +1 -0
  19. package/dist/blast/blast-cli.d.ts.map +1 -1
  20. package/dist/blast/blast-cli.js +2 -1
  21. package/dist/blast/blast-cli.js.map +1 -1
  22. package/dist/blast/render.d.ts.map +1 -1
  23. package/dist/blast/render.js +3 -2
  24. package/dist/blast/render.js.map +1 -1
  25. package/dist/brand.d.ts +97 -0
  26. package/dist/brand.d.ts.map +1 -0
  27. package/dist/brand.js +254 -0
  28. package/dist/brand.js.map +1 -0
  29. package/dist/check/local-check.d.ts +104 -0
  30. package/dist/check/local-check.d.ts.map +1 -0
  31. package/dist/check/local-check.js +830 -0
  32. package/dist/check/local-check.js.map +1 -0
  33. package/dist/check/types.d.ts +31 -0
  34. package/dist/check/types.d.ts.map +1 -0
  35. package/dist/check/types.js +2 -0
  36. package/dist/check/types.js.map +1 -0
  37. package/dist/claude/format.d.ts +44 -1
  38. package/dist/claude/format.d.ts.map +1 -1
  39. package/dist/claude/format.js +113 -19
  40. package/dist/claude/format.js.map +1 -1
  41. package/dist/claude/hook-layers.d.ts +8 -0
  42. package/dist/claude/hook-layers.d.ts.map +1 -0
  43. package/dist/claude/hook-layers.js +72 -0
  44. package/dist/claude/hook-layers.js.map +1 -0
  45. package/dist/claude/hooks.d.ts +34 -0
  46. package/dist/claude/hooks.d.ts.map +1 -1
  47. package/dist/claude/hooks.js +220 -12
  48. package/dist/claude/hooks.js.map +1 -1
  49. package/dist/claude/init.d.ts +12 -1
  50. package/dist/claude/init.d.ts.map +1 -1
  51. package/dist/claude/init.js +48 -19
  52. package/dist/claude/init.js.map +1 -1
  53. package/dist/claude/session-metrics.d.ts +1 -1
  54. package/dist/claude/session-metrics.d.ts.map +1 -1
  55. package/dist/claude/session-metrics.js +11 -8
  56. package/dist/claude/session-metrics.js.map +1 -1
  57. package/dist/claude/settings-merge.d.ts +1 -0
  58. package/dist/claude/settings-merge.d.ts.map +1 -1
  59. package/dist/claude/settings-merge.js +21 -18
  60. package/dist/claude/settings-merge.js.map +1 -1
  61. package/dist/claude/shim-template.d.ts +17 -2
  62. package/dist/claude/shim-template.d.ts.map +1 -1
  63. package/dist/claude/shim-template.js +116 -22
  64. package/dist/claude/shim-template.js.map +1 -1
  65. package/dist/claude/skill-template.d.ts +10 -1
  66. package/dist/claude/skill-template.d.ts.map +1 -1
  67. package/dist/claude/skill-template.js +132 -1
  68. package/dist/claude/skill-template.js.map +1 -1
  69. package/dist/claude/state.d.ts +8 -0
  70. package/dist/claude/state.d.ts.map +1 -1
  71. package/dist/claude/state.js.map +1 -1
  72. package/dist/claude/statusline.d.ts.map +1 -1
  73. package/dist/claude/statusline.js +4 -1
  74. package/dist/claude/statusline.js.map +1 -1
  75. package/dist/claude/tally.js +1 -1
  76. package/dist/claude/tally.js.map +1 -1
  77. package/dist/cli-epilogue.d.ts +0 -6
  78. package/dist/cli-epilogue.d.ts.map +1 -1
  79. package/dist/cli-epilogue.js +5 -4
  80. package/dist/cli-epilogue.js.map +1 -1
  81. package/dist/cli-meta.d.ts +8 -0
  82. package/dist/cli-meta.d.ts.map +1 -1
  83. package/dist/cli-meta.js +28 -8
  84. package/dist/cli-meta.js.map +1 -1
  85. package/dist/cli-picker.d.ts.map +1 -1
  86. package/dist/cli-picker.js +11 -7
  87. package/dist/cli-picker.js.map +1 -1
  88. package/dist/cli.d.ts +0 -5
  89. package/dist/cli.d.ts.map +1 -1
  90. package/dist/cli.js +1037 -595
  91. package/dist/cli.js.map +1 -1
  92. package/dist/cloud/diff.d.ts +18 -0
  93. package/dist/cloud/diff.d.ts.map +1 -0
  94. package/dist/cloud/diff.js +58 -0
  95. package/dist/cloud/diff.js.map +1 -0
  96. package/dist/cloud/nudge.d.ts +66 -0
  97. package/dist/cloud/nudge.d.ts.map +1 -0
  98. package/dist/cloud/nudge.js +269 -0
  99. package/dist/cloud/nudge.js.map +1 -0
  100. package/dist/cloud/workspace-sync.d.ts +58 -0
  101. package/dist/cloud/workspace-sync.d.ts.map +1 -0
  102. package/dist/cloud/workspace-sync.js +268 -0
  103. package/dist/cloud/workspace-sync.js.map +1 -0
  104. package/dist/cloud/workspace.d.ts +159 -0
  105. package/dist/cloud/workspace.d.ts.map +1 -0
  106. package/dist/cloud/workspace.js +134 -0
  107. package/dist/cloud/workspace.js.map +1 -0
  108. package/dist/context/check.d.ts +1 -1
  109. package/dist/context/check.d.ts.map +1 -1
  110. package/dist/context/check.js +9 -8
  111. package/dist/context/check.js.map +1 -1
  112. package/dist/context/node-file.js +1 -1
  113. package/dist/context/node-file.js.map +1 -1
  114. package/dist/context/savings.d.ts.map +1 -1
  115. package/dist/context/savings.js +10 -7
  116. package/dist/context/savings.js.map +1 -1
  117. package/dist/engine.d.ts.map +1 -1
  118. package/dist/engine.js +3 -1
  119. package/dist/engine.js.map +1 -1
  120. package/dist/graph/build.d.ts +30 -0
  121. package/dist/graph/build.d.ts.map +1 -1
  122. package/dist/graph/build.js +65 -19
  123. package/dist/graph/build.js.map +1 -1
  124. package/dist/graph/cards.d.ts.map +1 -1
  125. package/dist/graph/cards.js +4 -2
  126. package/dist/graph/cards.js.map +1 -1
  127. package/dist/graph/check.d.ts.map +1 -1
  128. package/dist/graph/check.js +17 -18
  129. package/dist/graph/check.js.map +1 -1
  130. package/dist/graph/container.d.ts.map +1 -1
  131. package/dist/graph/container.js +7 -2
  132. package/dist/graph/container.js.map +1 -1
  133. package/dist/graph/extract-cache.d.ts +4 -0
  134. package/dist/graph/extract-cache.d.ts.map +1 -1
  135. package/dist/graph/extract-cache.js.map +1 -1
  136. package/dist/graph/extract.d.ts +2 -0
  137. package/dist/graph/extract.d.ts.map +1 -1
  138. package/dist/graph/extract.js +5 -32
  139. package/dist/graph/extract.js.map +1 -1
  140. package/dist/graph/generic.d.ts +7 -1
  141. package/dist/graph/generic.d.ts.map +1 -1
  142. package/dist/graph/generic.js +48 -8
  143. package/dist/graph/generic.js.map +1 -1
  144. package/dist/graph/native.d.ts +38 -0
  145. package/dist/graph/native.d.ts.map +1 -0
  146. package/dist/graph/native.js +169 -0
  147. package/dist/graph/native.js.map +1 -0
  148. package/dist/graph/refresh.d.ts.map +1 -1
  149. package/dist/graph/refresh.js +3 -2
  150. package/dist/graph/refresh.js.map +1 -1
  151. package/dist/graph/traverse-cli.d.ts.map +1 -1
  152. package/dist/graph/traverse-cli.js +4 -3
  153. package/dist/graph/traverse-cli.js.map +1 -1
  154. package/dist/graph/workspace-cli.d.ts +2 -0
  155. package/dist/graph/workspace-cli.d.ts.map +1 -1
  156. package/dist/graph/workspace-cli.js +13 -1
  157. package/dist/graph/workspace-cli.js.map +1 -1
  158. package/dist/graph/workspace.d.ts.map +1 -1
  159. package/dist/graph/workspace.js +6 -5
  160. package/dist/graph/workspace.js.map +1 -1
  161. package/dist/hosts/antigravity.d.ts +10 -3
  162. package/dist/hosts/antigravity.d.ts.map +1 -1
  163. package/dist/hosts/antigravity.js +27 -10
  164. package/dist/hosts/antigravity.js.map +1 -1
  165. package/dist/hosts/claude-global.d.ts +27 -4
  166. package/dist/hosts/claude-global.d.ts.map +1 -1
  167. package/dist/hosts/claude-global.js +90 -9
  168. package/dist/hosts/claude-global.js.map +1 -1
  169. package/dist/hosts/codex-hooks.d.ts.map +1 -1
  170. package/dist/hosts/codex-hooks.js +1 -2
  171. package/dist/hosts/codex-hooks.js.map +1 -1
  172. package/dist/hosts/config-write.d.ts.map +1 -1
  173. package/dist/hosts/config-write.js +2 -1
  174. package/dist/hosts/config-write.js.map +1 -1
  175. package/dist/hosts/cursor-hooks.d.ts +5 -2
  176. package/dist/hosts/cursor-hooks.d.ts.map +1 -1
  177. package/dist/hosts/cursor-hooks.js +14 -9
  178. package/dist/hosts/cursor-hooks.js.map +1 -1
  179. package/dist/hosts/init.d.ts +3 -0
  180. package/dist/hosts/init.d.ts.map +1 -1
  181. package/dist/hosts/init.js +32 -10
  182. package/dist/hosts/init.js.map +1 -1
  183. package/dist/hosts/instructions.d.ts +9 -4
  184. package/dist/hosts/instructions.d.ts.map +1 -1
  185. package/dist/hosts/instructions.js +19 -16
  186. package/dist/hosts/instructions.js.map +1 -1
  187. package/dist/hosts/mcp-config.d.ts +19 -4
  188. package/dist/hosts/mcp-config.d.ts.map +1 -1
  189. package/dist/hosts/mcp-config.js +81 -30
  190. package/dist/hosts/mcp-config.js.map +1 -1
  191. package/dist/hosts/plan.d.ts.map +1 -1
  192. package/dist/hosts/plan.js +11 -9
  193. package/dist/hosts/plan.js.map +1 -1
  194. package/dist/hosts/registry.d.ts +25 -2
  195. package/dist/hosts/registry.d.ts.map +1 -1
  196. package/dist/hosts/registry.js +53 -0
  197. package/dist/hosts/registry.js.map +1 -1
  198. package/dist/hosts/retract.d.ts.map +1 -1
  199. package/dist/hosts/retract.js +73 -33
  200. package/dist/hosts/retract.js.map +1 -1
  201. package/dist/hosts/sections.d.ts +17 -1
  202. package/dist/hosts/sections.d.ts.map +1 -1
  203. package/dist/hosts/sections.js +52 -2
  204. package/dist/hosts/sections.js.map +1 -1
  205. package/dist/mcp/instructions.d.ts +1 -25
  206. package/dist/mcp/instructions.d.ts.map +1 -1
  207. package/dist/mcp/instructions.js +6 -4
  208. package/dist/mcp/instructions.js.map +1 -1
  209. package/dist/mcp/server.d.ts.map +1 -1
  210. package/dist/mcp/server.js +7 -3
  211. package/dist/mcp/server.js.map +1 -1
  212. package/dist/mcp/tool-names.d.ts +10 -3
  213. package/dist/mcp/tool-names.d.ts.map +1 -1
  214. package/dist/mcp/tool-names.js +13 -5
  215. package/dist/mcp/tool-names.js.map +1 -1
  216. package/dist/mcp/tools.d.ts.map +1 -1
  217. package/dist/mcp/tools.js +6 -5
  218. package/dist/mcp/tools.js.map +1 -1
  219. package/dist/notes/ask-notes.d.ts +8 -0
  220. package/dist/notes/ask-notes.d.ts.map +1 -0
  221. package/dist/notes/ask-notes.js +18 -0
  222. package/dist/notes/ask-notes.js.map +1 -0
  223. package/dist/notes/branches.d.ts +85 -0
  224. package/dist/notes/branches.d.ts.map +1 -0
  225. package/dist/notes/branches.js +238 -0
  226. package/dist/notes/branches.js.map +1 -0
  227. package/dist/notes/cold-start.d.ts +127 -0
  228. package/dist/notes/cold-start.d.ts.map +1 -0
  229. package/dist/notes/cold-start.js +476 -0
  230. package/dist/notes/cold-start.js.map +1 -0
  231. package/dist/notes/git-facts.d.ts +17 -0
  232. package/dist/notes/git-facts.d.ts.map +1 -0
  233. package/dist/notes/git-facts.js +117 -0
  234. package/dist/notes/git-facts.js.map +1 -0
  235. package/dist/notes/home.d.ts +87 -0
  236. package/dist/notes/home.d.ts.map +1 -0
  237. package/dist/notes/home.js +423 -0
  238. package/dist/notes/home.js.map +1 -0
  239. package/dist/notes/notes.d.ts +178 -0
  240. package/dist/notes/notes.d.ts.map +1 -0
  241. package/dist/notes/notes.js +498 -0
  242. package/dist/notes/notes.js.map +1 -0
  243. package/dist/notes/repo-trail.d.ts +16 -0
  244. package/dist/notes/repo-trail.d.ts.map +1 -0
  245. package/dist/notes/repo-trail.js +74 -0
  246. package/dist/notes/repo-trail.js.map +1 -0
  247. package/dist/notes/session-cost.d.ts +21 -0
  248. package/dist/notes/session-cost.d.ts.map +1 -0
  249. package/dist/notes/session-cost.js +162 -0
  250. package/dist/notes/session-cost.js.map +1 -0
  251. package/dist/search/grep-cli.d.ts.map +1 -1
  252. package/dist/search/grep-cli.js +4 -3
  253. package/dist/search/grep-cli.js.map +1 -1
  254. package/dist/skills/skills.d.ts +118 -0
  255. package/dist/skills/skills.d.ts.map +1 -0
  256. package/dist/skills/skills.js +370 -0
  257. package/dist/skills/skills.js.map +1 -0
  258. package/dist/telemetry/contract.d.ts +2 -14
  259. package/dist/telemetry/contract.d.ts.map +1 -1
  260. package/dist/telemetry/contract.js +39 -24
  261. package/dist/telemetry/contract.js.map +1 -1
  262. package/dist/telemetry/gate.d.ts.map +1 -1
  263. package/dist/telemetry/gate.js +2 -1
  264. package/dist/telemetry/gate.js.map +1 -1
  265. package/dist/telemetry/notice.d.ts +1 -1
  266. package/dist/telemetry/notice.d.ts.map +1 -1
  267. package/dist/telemetry/notice.js +6 -5
  268. package/dist/telemetry/notice.js.map +1 -1
  269. package/dist/telemetry/track.d.ts.map +1 -1
  270. package/dist/telemetry/track.js +5 -0
  271. package/dist/telemetry/track.js.map +1 -1
  272. package/dist/upkeep-run.d.ts +7 -0
  273. package/dist/upkeep-run.d.ts.map +1 -1
  274. package/dist/upkeep-run.js +52 -12
  275. package/dist/upkeep-run.js.map +1 -1
  276. package/dist/upkeep.d.ts +9 -19
  277. package/dist/upkeep.d.ts.map +1 -1
  278. package/dist/upkeep.js +38 -59
  279. package/dist/upkeep.js.map +1 -1
  280. package/dist/util/browser.d.ts +3 -0
  281. package/dist/util/browser.d.ts.map +1 -0
  282. package/dist/util/browser.js +14 -0
  283. package/dist/util/browser.js.map +1 -0
  284. package/dist/util/front-matter.d.ts +19 -0
  285. package/dist/util/front-matter.d.ts.map +1 -0
  286. package/dist/util/front-matter.js +21 -0
  287. package/dist/util/front-matter.js.map +1 -0
  288. package/dist/util/state.d.ts +1 -18
  289. package/dist/util/state.d.ts.map +1 -1
  290. package/dist/util/state.js +4 -2
  291. package/dist/util/state.js.map +1 -1
  292. package/dist/viz/assemble.js +1 -1
  293. package/dist/viz/assemble.js.map +1 -1
  294. package/dist/viz/serve.d.ts.map +1 -1
  295. package/dist/viz/serve.js +4 -3
  296. package/dist/viz/serve.js.map +1 -1
  297. package/package.json +7 -7
  298. package/scripts/pack-trail.mjs +53 -0
  299. package/scripts/postinstall.mjs +17 -2
  300. package/scripts/run-tests.mjs +4 -1
  301. package/dist/brain/attach.d.ts +0 -44
  302. package/dist/brain/attach.d.ts.map +0 -1
  303. package/dist/brain/attach.js +0 -63
  304. package/dist/brain/attach.js.map +0 -1
  305. package/dist/brain/claude-md.d.ts +0 -61
  306. package/dist/brain/claude-md.d.ts.map +0 -1
  307. package/dist/brain/claude-md.js +0 -171
  308. package/dist/brain/claude-md.js.map +0 -1
  309. package/dist/brain/connect.d.ts +0 -59
  310. package/dist/brain/connect.d.ts.map +0 -1
  311. package/dist/brain/connect.js +0 -80
  312. package/dist/brain/connect.js.map +0 -1
  313. package/dist/brain/context-files.d.ts +0 -79
  314. package/dist/brain/context-files.d.ts.map +0 -1
  315. package/dist/brain/context-files.js +0 -245
  316. package/dist/brain/context-files.js.map +0 -1
  317. package/dist/brain/link.d.ts +0 -106
  318. package/dist/brain/link.d.ts.map +0 -1
  319. package/dist/brain/link.js +0 -167
  320. package/dist/brain/link.js.map +0 -1
  321. package/dist/brain/pull.d.ts +0 -17
  322. package/dist/brain/pull.d.ts.map +0 -1
  323. package/dist/brain/pull.js +0 -172
  324. package/dist/brain/pull.js.map +0 -1
  325. package/dist/brain/push.d.ts +0 -155
  326. package/dist/brain/push.d.ts.map +0 -1
  327. package/dist/brain/push.js +0 -304
  328. package/dist/brain/push.js.map +0 -1
  329. package/dist/brain/signup.d.ts +0 -117
  330. package/dist/brain/signup.d.ts.map +0 -1
  331. package/dist/brain/signup.js +0 -276
  332. package/dist/brain/signup.js.map +0 -1
  333. package/dist/brain/upload.d.ts +0 -61
  334. package/dist/brain/upload.d.ts.map +0 -1
  335. package/dist/brain/upload.js +0 -210
  336. package/dist/brain/upload.js.map +0 -1
  337. package/dist/brain/watch.d.ts +0 -209
  338. package/dist/brain/watch.d.ts.map +0 -1
  339. package/dist/brain/watch.js +0 -396
  340. package/dist/brain/watch.js.map +0 -1
  341. package/dist/brain/wire.d.ts +0 -38
  342. package/dist/brain/wire.d.ts.map +0 -1
  343. package/dist/brain/wire.js +0 -133
  344. package/dist/brain/wire.js.map +0 -1
  345. package/dist/legacy-args.d.ts +0 -12
  346. package/dist/legacy-args.d.ts.map +0 -1
  347. package/dist/legacy-args.js +0 -31
  348. package/dist/legacy-args.js.map +0 -1
@@ -0,0 +1,178 @@
1
+ export interface NoteCost {
2
+ minutes?: number;
3
+ tokens?: number;
4
+ }
5
+ export interface Note {
6
+ /** Where the note is: an absolute path on this machine. */
7
+ path: string;
8
+ title: string;
9
+ author: string;
10
+ /** YYYY-MM-DD. */
11
+ date: string;
12
+ branch?: string;
13
+ cost?: NoteCost;
14
+ /** `file` or `file#Symbol`, relative to the top of the checkout (or to the folder `listNotes` was asked about). */
15
+ touches: string[];
16
+ body: string;
17
+ /** Written by someone other than whoever is asking. */
18
+ teammate?: boolean;
19
+ /** `repo`: in the repo's `.trail/learnings/`, for everyone. `personal`: in ~/.trail, this machine only. */
20
+ scope?: "repo" | "personal";
21
+ /** A rule or preference for every session here, not knowledge about one
22
+ * task: what the user kept having to tell Claude. The session-start hook
23
+ * lists these up front, since no single task would bring them up. */
24
+ standing?: boolean;
25
+ /** Found on a teammate's pushed branch (`origin/kumar/jitter`) rather than in
26
+ * this checkout. `path` is then `<ref>:.trail/learnings/<name>`, readable
27
+ * with `git show`, and the whole body travels with the note. */
28
+ ref?: string;
29
+ }
30
+ /** Which kind of learning to write: for the repo (the default) or for this person only. */
31
+ export type LearningScope = "repo" | "personal";
32
+ /** The three sections a note is written under. Notes may have others; these are the ones `ask` quotes. */
33
+ export declare const SECTIONS: readonly [{
34
+ readonly key: "decided";
35
+ readonly heading: "Decided";
36
+ readonly label: "decided";
37
+ }, {
38
+ readonly key: "ruledOut";
39
+ readonly heading: "Tried and ruled out";
40
+ readonly label: "ruled out";
41
+ }, {
42
+ readonly key: "watchOut";
43
+ readonly heading: "Watch out";
44
+ readonly label: "watch out";
45
+ }];
46
+ export declare function renderNote(n: Omit<Note, "path">): string;
47
+ /** A note file, or null when it isn't one (no title in its frontmatter). */
48
+ export declare function parseNote(text: string, path: string): Note | null;
49
+ /** The file name a note is known by, wherever it sits: one learning on a branch and in the tree is one learning. */
50
+ export declare function noteName(n: Note): string;
51
+ /**
52
+ * Every learning that bears on the folder `dir`, newest first: the repo's
53
+ * committed ones in `.trail/learnings/`, this person's own in ~/.trail, and
54
+ * the ones on teammates' pushed branches that haven't merged yet (from the
55
+ * branch cache, so nothing here runs git). For a folder that holds several
56
+ * repos, its own and each repo's. Touches come back relative to `dir`, so
57
+ * they line up with a query run there. `children: false` leaves a folder's
58
+ * repos out. One file name is one learning: the working tree's copy wins,
59
+ * then this machine's, then a branch's. Unreadable files are skipped.
60
+ */
61
+ export declare function listNotes(dir: string, opts?: {
62
+ children?: boolean;
63
+ branches?: boolean;
64
+ }): Note[];
65
+ /** The first line of a section's text, or null when the note has no such section. */
66
+ export declare function sectionLead(body: string, heading: string): string | null;
67
+ /** `Bbox coordinates are off on rotated PDFs` → `bbox-coordinates-are-off-on-rotated`. */
68
+ export declare function slug(s: string, words?: number): string;
69
+ export interface WriteNoteInput {
70
+ title: string;
71
+ body: string;
72
+ author: string;
73
+ date: string;
74
+ branch?: string;
75
+ cost?: NoteCost;
76
+ touches?: string[];
77
+ standing?: boolean;
78
+ }
79
+ /**
80
+ * Write a new learning for the repo `dir` is in, and return it: into the
81
+ * repo's `.trail/learnings/` (made if needed), or with `scope: "personal"`,
82
+ * or outside git, into ~/.trail. Never overwrites: a second one with the same
83
+ * name on the same day gets `-2`, `-3`.
84
+ */
85
+ export declare function writeNote(dir: string, input: WriteNoteInput, scope?: LearningScope): Note;
86
+ /**
87
+ * The standing learnings for `dir`: the team's in the checkout and the
88
+ * person's own, never ones on a teammate's unmerged branch, which nobody has
89
+ * reviewed yet.
90
+ */
91
+ export declare function standingLearnings(dir: string): Note[];
92
+ /** Where a learning is, as the agent and the person read it: `.trail/learnings/x.md`, `~/.trail/…`, or `origin/b:.trail/…`. */
93
+ export declare function notePlace(n: Note): string;
94
+ export interface NoteTarget {
95
+ /** A folder in the repo the note goes to. */
96
+ dir: string;
97
+ /** What the session touched there, relative to the top of that checkout. */
98
+ touches: string[];
99
+ }
100
+ /**
101
+ * Which repos a session's note belongs in, and what it touched in each.
102
+ *
103
+ * The repo the note was saved from gets the files it has uncommitted or
104
+ * committed since `since`. Every other repo the session edited (`edited`:
105
+ * absolute paths, from the agent's transcript) gets the note too, with its
106
+ * own files, so a session that changed a backend and the library it calls
107
+ * leaves a note in both. Saved from a folder that holds several repos, the
108
+ * note goes to each repo the session changed, or stays with the folder when
109
+ * it changed none. Two worktrees of one repo are one repo.
110
+ */
111
+ export declare function noteTargets(dir: string, edited?: string[], since?: number): NoteTarget[];
112
+ /**
113
+ * Files of the checkout at `dir` that a note's text names by path. A session
114
+ * that only read code edits nothing, so without these its note would touch no
115
+ * file, and nothing that looks a note up by file (`trail check`, the ranked
116
+ * search) would ever find it.
117
+ */
118
+ export declare function mentionedFiles(dir: string, body: string): string[];
119
+ export interface NoteHit {
120
+ note: Note;
121
+ score: number;
122
+ /** Files the note touches that this query's results also point into. */
123
+ shared: string[];
124
+ }
125
+ /**
126
+ * The notes worth showing next to a query's results: ones whose title or body
127
+ * share the query's words, or that touch a file the results point into. At
128
+ * most `limit`, best first. A note has to clear a bar — two of the query's
129
+ * words, or one in its title, or a shared file — so an unrelated note never
130
+ * rides along just because it exists.
131
+ */
132
+ export declare function findNotes(notes: Note[], query: string, files?: string[], limit?: number): NoteHit[];
133
+ /** `2026-10-07` → `Oct 7`; anything else as it came. */
134
+ export declare function shortDate(date: string): string;
135
+ /** `~9.6k tokens`, `~600 tokens`. */
136
+ export declare function tokensLabel(n: number): string;
137
+ /** `took 12 min and ~9.6k tokens to work out`, or null when the note has no cost. */
138
+ export declare function costLabel(cost?: NoteCost): string | null;
139
+ /** What reading a note costs an agent, in tokens: its text at ~4 characters a token. */
140
+ export declare function readingTokens(n: Note): number;
141
+ /**
142
+ * What reusing a learning saves: what it took to work out, less what reading
143
+ * it costs. 0 when it carries no token cost, or reading it costs as much.
144
+ */
145
+ export declare function savedByNote(n: Note): number;
146
+ /** `Anirudh's learning`, `a learning`, `Kumar's learning on kumar/jitter`. */
147
+ export declare function whoseLearning(n: Note): string;
148
+ /**
149
+ * The block `ask` prints above its results: whose learning each is, when,
150
+ * what it was about and what it cost, then the first line of each section and
151
+ * where to read the rest. A learning on a teammate's unmerged branch says so,
152
+ * with the `git show` that prints it, since there's no file here to open. An agent
153
+ * relays the cost when it says what the turn saved, which is how the saving
154
+ * becomes countable.
155
+ */
156
+ export declare function formatNoteHits(hits: NoteHit[]): string[];
157
+ /** The line an agent closes its reply with after saving a learning just for the user. */
158
+ export declare const PERSONAL_SAVED_LINE = "\uD83D\uDCDD Saved just for you \u00B7 stays on this machine, out of the repo";
159
+ /** `Anay`, `Anay and Rushil`, `Anay, Rushil and 3 others`. */
160
+ export declare function namesLabel(names: string[]): string;
161
+ /**
162
+ * The lines that close a reply after a team learning is saved, one for each
163
+ * way the turn can end, since only the agent knows whether it committed the
164
+ * learning by then. Uncommitted, it makes the case for committing: who else
165
+ * works on the same files, and the head start their agents would get, which
166
+ * is what the learning took to work out.
167
+ */
168
+ export declare function commitLines(note: Pick<Note, "cost">, teammates: string[], where?: "files" | "repo"): {
169
+ committed: string;
170
+ uncommitted: string;
171
+ };
172
+ /**
173
+ * The line that credits reused learnings, for the savings accumulator and the
174
+ * agent's closing line: what they took to work out, less what reading them
175
+ * here costs. Null when no shown learning carries a token cost.
176
+ */
177
+ export declare function learningSavingsLine(hits: NoteHit[]): string | null;
178
+ //# sourceMappingURL=notes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../../src/notes/notes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Learnings: one short note per coding session that took real digging.
3
+ *
4
+ * A learning is what a session worked out that the code itself doesn't say:
5
+ * what was decided, what was tried and ruled out, and what to watch out for,
6
+ * plus what it cost to figure out. The next session on the same code reads it
7
+ * before exploring, so nobody pays for it twice.
8
+ *
9
+ * Most learnings are about the code, so they go in the repo's `.trail/learnings/`
10
+ * and are committed with the change (repo-trail.ts): every teammate's agent
11
+ * starts from them, and teammates' pushed branches bring theirs before they
12
+ * merge (branches.ts). A personal one, about one person's setup or habits,
13
+ * stays on their machine in `~/.trail/repos/<repo>/notes/` (home.ts).
14
+ *
15
+ * One file per session and never edited after it's written. A summary only,
16
+ * never a transcript: the agent writes the body, and nothing here reads
17
+ * source files or prompts into it.
18
+ */
19
+ import { readdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { dirname, join, relative, resolve, sep } from "node:path";
22
+ import { matter } from "../util/front-matter.js";
23
+ import { readBranchCache } from "./branches.js";
24
+ import { changedFiles, ownPath } from "./git-facts.js";
25
+ import { checkoutRoot, childCheckouts, ensureRepoHome, notePlaces, repoPlace, shownPath } from "./home.js";
26
+ import { ensureRepoTrail, LEARNINGS_DIR, learningsDir } from "./repo-trail.js";
27
+ /** The three sections a note is written under. Notes may have others; these are the ones `ask` quotes. */
28
+ export const SECTIONS = [
29
+ { key: "decided", heading: "Decided", label: "decided" },
30
+ { key: "ruledOut", heading: "Tried and ruled out", label: "ruled out" },
31
+ { key: "watchOut", heading: "Watch out", label: "watch out" },
32
+ ];
33
+ /* -------------------------------------------------------------------------- */
34
+ /* reading and writing one note */
35
+ /* -------------------------------------------------------------------------- */
36
+ /** A YAML scalar that round-trips: plain when it can be, double-quoted otherwise. */
37
+ function scalar(s) {
38
+ return /^[A-Za-z0-9][^:#\n"'{}[\],&*!|>%@`]*$/.test(s) && s.trim() === s ? s : JSON.stringify(s);
39
+ }
40
+ export function renderNote(n) {
41
+ const fm = ["---", `title: ${scalar(n.title)}`, `author: ${scalar(n.author)}`, `date: ${n.date}`];
42
+ if (n.branch)
43
+ fm.push(`branch: ${scalar(n.branch)}`);
44
+ const cost = [];
45
+ if (n.cost?.minutes !== undefined)
46
+ cost.push(`minutes: ${n.cost.minutes}`);
47
+ if (n.cost?.tokens !== undefined)
48
+ cost.push(`tokens: ${n.cost.tokens}`);
49
+ if (cost.length)
50
+ fm.push(`cost: { ${cost.join(", ")} }`);
51
+ if (n.standing)
52
+ fm.push("standing: true");
53
+ if (n.touches.length)
54
+ fm.push("touches:", ...n.touches.map((t) => ` - ${scalar(t)}`));
55
+ fm.push("---", "");
56
+ return `${fm.join("\n")}\n${n.body.trim()}\n`;
57
+ }
58
+ function num(v) {
59
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
60
+ }
61
+ /** A note file, or null when it isn't one (no title in its frontmatter). */
62
+ export function parseNote(text, path) {
63
+ let parsed;
64
+ try {
65
+ parsed = matter(text);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ const d = parsed.data;
71
+ if (typeof d.title !== "string" || !d.title.trim())
72
+ return null;
73
+ const date = d.date instanceof Date ? d.date.toISOString().slice(0, 10) : String(d.date ?? "");
74
+ const cost = (d.cost ?? {});
75
+ const minutes = num(cost.minutes);
76
+ const tokens = num(cost.tokens);
77
+ return {
78
+ path,
79
+ title: d.title.trim(),
80
+ author: typeof d.author === "string" ? d.author : "",
81
+ date,
82
+ branch: typeof d.branch === "string" ? d.branch : undefined,
83
+ cost: minutes !== undefined || tokens !== undefined ? { minutes, tokens } : undefined,
84
+ touches: Array.isArray(d.touches) ? d.touches.map(String) : [],
85
+ body: parsed.content.trim(),
86
+ ...(d.standing === true ? { standing: true } : {}),
87
+ };
88
+ }
89
+ /** A touch saved relative to `checkout`, as a path relative to `here`. */
90
+ function rebaseTouch(t, checkout, here) {
91
+ if (checkout === here)
92
+ return t;
93
+ const [file, ...sym] = t.split("#");
94
+ const rel = relative(here, join(checkout, file)).split(sep).join("/");
95
+ return [rel, ...sym].join("#");
96
+ }
97
+ /** Every `.md` in `notesDir` that parses as a note, with touches rebased onto `here`. */
98
+ function readNotesIn(notesDir, checkout, here, scope) {
99
+ let files;
100
+ try {
101
+ files = readdirSync(notesDir).filter((f) => f.endsWith(".md"));
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ const out = [];
107
+ for (const f of files) {
108
+ try {
109
+ const n = parseNote(readFileSync(join(notesDir, f), "utf8"), join(notesDir, f));
110
+ if (n)
111
+ out.push({ ...n, scope, touches: n.touches.map((t) => rebaseTouch(t, checkout, here)) });
112
+ }
113
+ catch {
114
+ /* unreadable: skip */
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+ /** The file name a note is known by, wherever it sits: one learning on a branch and in the tree is one learning. */
120
+ export function noteName(n) {
121
+ return n.path.slice(Math.max(n.path.lastIndexOf("/"), n.path.lastIndexOf(":")) + 1);
122
+ }
123
+ /**
124
+ * Every learning that bears on the folder `dir`, newest first: the repo's
125
+ * committed ones in `.trail/learnings/`, this person's own in ~/.trail, and
126
+ * the ones on teammates' pushed branches that haven't merged yet (from the
127
+ * branch cache, so nothing here runs git). For a folder that holds several
128
+ * repos, its own and each repo's. Touches come back relative to `dir`, so
129
+ * they line up with a query run there. `children: false` leaves a folder's
130
+ * repos out. One file name is one learning: the working tree's copy wins,
131
+ * then this machine's, then a branch's. Unreadable files are skipped.
132
+ */
133
+ export function listNotes(dir, opts = {}) {
134
+ const here = resolve(dir);
135
+ const out = [];
136
+ const seen = new Set();
137
+ const add = (notes) => {
138
+ for (const n of notes) {
139
+ const name = noteName(n);
140
+ if (seen.has(name))
141
+ continue;
142
+ seen.add(name);
143
+ out.push(n);
144
+ }
145
+ };
146
+ const places = notePlaces(here);
147
+ for (const place of opts.children === false ? places.slice(0, 1) : places) {
148
+ if (place.git)
149
+ add(readNotesIn(learningsDir(place.checkout), place.checkout, here, "repo"));
150
+ add(readNotesIn(join(place.dir, "notes"), place.checkout, here, "personal"));
151
+ if (place.git && opts.branches !== false)
152
+ add(branchNotes(place.checkout, here));
153
+ }
154
+ return out.sort((a, b) => (a.date === b.date ? b.path.localeCompare(a.path) : b.date.localeCompare(a.date)));
155
+ }
156
+ /** The learnings on teammates' pushed branches, from the branch cache. */
157
+ function branchNotes(checkout, here) {
158
+ const cache = readBranchCache(checkout);
159
+ if (!cache)
160
+ return [];
161
+ const out = [];
162
+ for (const b of cache.branches) {
163
+ for (const l of b.learnings) {
164
+ const n = parseNote(l.text, `${b.ref}:${LEARNINGS_DIR}/${l.name}`);
165
+ if (n)
166
+ out.push({ ...n, scope: "repo", ref: b.ref, touches: n.touches.map((t) => rebaseTouch(t, checkout, here)) });
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ /** The first line of a section's text, or null when the note has no such section. */
172
+ export function sectionLead(body, heading) {
173
+ const lines = body.split(/\r?\n/);
174
+ const at = lines.findIndex((l) => l.trim().toLowerCase() === `## ${heading.toLowerCase()}`);
175
+ if (at === -1)
176
+ return null;
177
+ for (let i = at + 1; i < lines.length; i++) {
178
+ const l = lines[i].trim();
179
+ if (l.startsWith("## "))
180
+ break;
181
+ if (l)
182
+ return l.replace(/^[-*]\s+/, "");
183
+ }
184
+ return null;
185
+ }
186
+ /** `Bbox coordinates are off on rotated PDFs` → `bbox-coordinates-are-off-on-rotated`. */
187
+ export function slug(s, words = 6) {
188
+ return s
189
+ .toLowerCase()
190
+ .normalize("NFKD")
191
+ .replace(/[^a-z0-9\s-]/g, " ")
192
+ .split(/[\s-]+/)
193
+ .filter(Boolean)
194
+ .slice(0, words)
195
+ .join("-");
196
+ }
197
+ /**
198
+ * Write a new learning for the repo `dir` is in, and return it: into the
199
+ * repo's `.trail/learnings/` (made if needed), or with `scope: "personal"`,
200
+ * or outside git, into ~/.trail. Never overwrites: a second one with the same
201
+ * name on the same day gets `-2`, `-3`.
202
+ */
203
+ export function writeNote(dir, input, scope = "repo") {
204
+ const { place } = ensureRepoHome(dir);
205
+ const inRepo = scope === "repo" && place.git;
206
+ let notes;
207
+ if (inRepo) {
208
+ ensureRepoTrail(place.checkout);
209
+ notes = learningsDir(place.checkout);
210
+ }
211
+ else {
212
+ notes = join(place.dir, "notes");
213
+ }
214
+ const base = [input.date, slug(input.title) || "note", slug(input.author, 1)].filter(Boolean).join("-");
215
+ let name = `${base}.md`;
216
+ for (let i = 2; existsSync(join(notes, name)); i++)
217
+ name = `${base}-${i}.md`;
218
+ const note = {
219
+ title: input.title.trim(),
220
+ author: input.author,
221
+ date: input.date,
222
+ branch: input.branch,
223
+ cost: input.cost,
224
+ touches: input.touches ?? [],
225
+ body: input.body,
226
+ scope: inRepo ? "repo" : "personal",
227
+ ...(input.standing ? { standing: true } : {}),
228
+ };
229
+ writeFileSync(join(notes, name), renderNote(note));
230
+ return { ...note, path: join(notes, name) };
231
+ }
232
+ /**
233
+ * The standing learnings for `dir`: the team's in the checkout and the
234
+ * person's own, never ones on a teammate's unmerged branch, which nobody has
235
+ * reviewed yet.
236
+ */
237
+ export function standingLearnings(dir) {
238
+ return listNotes(dir, { branches: false }).filter((n) => n.standing && !n.ref);
239
+ }
240
+ /** Where a learning is, as the agent and the person read it: `.trail/learnings/x.md`, `~/.trail/…`, or `origin/b:.trail/…`. */
241
+ export function notePlace(n) {
242
+ if (n.ref)
243
+ return n.path;
244
+ // Windows paths come with backslashes; the place is shown with `/` either way.
245
+ const path = n.path.replace(/\\/g, "/");
246
+ const at = path.lastIndexOf(`/${LEARNINGS_DIR}/`);
247
+ return at === -1 ? shownPath(n.path) : path.slice(at + 1);
248
+ }
249
+ /** Most files one note lists per repo. */
250
+ const TOUCH_LIMIT = 10;
251
+ /**
252
+ * Which repos a session's note belongs in, and what it touched in each.
253
+ *
254
+ * The repo the note was saved from gets the files it has uncommitted or
255
+ * committed since `since`. Every other repo the session edited (`edited`:
256
+ * absolute paths, from the agent's transcript) gets the note too, with its
257
+ * own files, so a session that changed a backend and the library it calls
258
+ * leaves a note in both. Saved from a folder that holds several repos, the
259
+ * note goes to each repo the session changed, or stays with the folder when
260
+ * it changed none. Two worktrees of one repo are one repo.
261
+ */
262
+ export function noteTargets(dir, edited = [], since) {
263
+ const here = repoPlace(dir);
264
+ const byKey = new Map();
265
+ const add = (checkout, files) => {
266
+ const key = repoPlace(checkout).key;
267
+ const entry = byKey.get(key) ?? { dir: checkout, files: [] };
268
+ for (const f of files)
269
+ if (f && !ownPath(f) && !entry.files.includes(f))
270
+ entry.files.push(f);
271
+ byKey.set(key, entry);
272
+ };
273
+ if (here.git)
274
+ add(here.checkout, changedFiles(here.checkout, since));
275
+ else if (edited.length === 0)
276
+ for (const c of childCheckouts(here.checkout))
277
+ add(c, changedFiles(c, since));
278
+ // The agent's own files (memory, plans, scratch under ~/.claude) aren't what a note is about.
279
+ const agentFiles = join(homedir(), ".claude") + sep;
280
+ for (const f of edited) {
281
+ if (f.startsWith(agentFiles))
282
+ continue;
283
+ const top = checkoutRoot(dirname(f));
284
+ if (top)
285
+ add(top, [relative(top, f).split(sep).join("/")]);
286
+ else if (!here.git && !relative(here.checkout, f).startsWith(".."))
287
+ add(here.checkout, [relative(here.checkout, f).split(sep).join("/")]);
288
+ }
289
+ const out = [...byKey.entries()]
290
+ .filter(([key, e]) => e.files.length > 0 || key === here.key)
291
+ .map(([, e]) => ({ dir: e.dir, touches: e.files.slice(0, TOUCH_LIMIT) }));
292
+ // The repo it was saved from first; a folder of repos only when nothing else was touched.
293
+ out.sort((a, b) => Number(repoPlace(b.dir).key === here.key) - Number(repoPlace(a.dir).key === here.key));
294
+ const touched = out.filter((t) => t.touches.length > 0);
295
+ if (!here.git && touched.length > 0)
296
+ return touched;
297
+ return out.length ? out : [{ dir: here.checkout, touches: [] }];
298
+ }
299
+ /** A path-shaped word in a note: `client.go`, `src/a/b.ts:12-30`, `x.go#Fn`. */
300
+ const MENTION = /(?:^|[\s(`'"\[])((?:[\w.-]+\/)*[\w-][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,5})(?=$|[\s:#)`'",.;\]])/gm;
301
+ /**
302
+ * Files of the checkout at `dir` that a note's text names by path. A session
303
+ * that only read code edits nothing, so without these its note would touch no
304
+ * file, and nothing that looks a note up by file (`trail check`, the ranked
305
+ * search) would ever find it.
306
+ */
307
+ export function mentionedFiles(dir, body) {
308
+ const out = [];
309
+ for (const m of body.matchAll(MENTION)) {
310
+ const f = m[1].replace(/^\.\//, "");
311
+ if (out.includes(f) || ownPath(f) || f.startsWith(".."))
312
+ continue;
313
+ if (existsSync(join(dir, f)))
314
+ out.push(f);
315
+ if (out.length >= TOUCH_LIMIT)
316
+ break;
317
+ }
318
+ return out;
319
+ }
320
+ /* -------------------------------------------------------------------------- */
321
+ /* finding the notes that matter for a query */
322
+ /* -------------------------------------------------------------------------- */
323
+ const STOP = new Set("the and for with from that this what when where which into does how why are was were not but you your our out its it's can should would could about after before over under use using used fix find make".split(" "));
324
+ function terms(s) {
325
+ return [...new Set(s.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length >= 3 && !STOP.has(t)))];
326
+ }
327
+ /** The file part of a `file#Symbol` touch. */
328
+ function touchFile(t) {
329
+ return t.split("#")[0];
330
+ }
331
+ /**
332
+ * The notes worth showing next to a query's results: ones whose title or body
333
+ * share the query's words, or that touch a file the results point into. At
334
+ * most `limit`, best first. A note has to clear a bar — two of the query's
335
+ * words, or one in its title, or a shared file — so an unrelated note never
336
+ * rides along just because it exists.
337
+ */
338
+ export function findNotes(notes, query, files = [], limit = 2) {
339
+ const q = terms(query);
340
+ const hitFiles = new Set(files);
341
+ const out = [];
342
+ for (const note of notes) {
343
+ const title = new Set(terms(note.title));
344
+ const text = new Set(terms(`${note.body} ${note.touches.join(" ")}`));
345
+ let score = 0;
346
+ let matched = 0;
347
+ for (const t of q) {
348
+ if (title.has(t)) {
349
+ score += 3;
350
+ matched++;
351
+ }
352
+ else if (text.has(t)) {
353
+ score += 1;
354
+ matched++;
355
+ }
356
+ }
357
+ const shared = [...new Set(note.touches.map(touchFile))].filter((f) => hitFiles.has(f));
358
+ score += shared.length * 4;
359
+ const clears = shared.length > 0 || matched >= 2 || [...title].some((t) => q.includes(t));
360
+ if (clears && score >= 3)
361
+ out.push({ note, score, shared });
362
+ }
363
+ return out.sort((a, b) => b.score - a.score || b.note.date.localeCompare(a.note.date)).slice(0, limit);
364
+ }
365
+ /* -------------------------------------------------------------------------- */
366
+ /* how a note reads inside a query's output */
367
+ /* -------------------------------------------------------------------------- */
368
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
369
+ /** `2026-10-07` → `Oct 7`; anything else as it came. */
370
+ export function shortDate(date) {
371
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
372
+ if (!m)
373
+ return date;
374
+ return `${MONTHS[Number(m[2]) - 1] ?? m[2]} ${Number(m[3])}`;
375
+ }
376
+ /** `~9.6k tokens`, `~600 tokens`. */
377
+ export function tokensLabel(n) {
378
+ if (n >= 1000)
379
+ return `~${(n / 1000).toFixed(n >= 10_000 ? 0 : 1).replace(/\.0$/, "")}k tokens`;
380
+ return `~${n} tokens`;
381
+ }
382
+ /** `took 12 min and ~9.6k tokens to work out`, or null when the note has no cost. */
383
+ export function costLabel(cost) {
384
+ if (!cost)
385
+ return null;
386
+ const parts = [];
387
+ if (cost.minutes !== undefined)
388
+ parts.push(`${cost.minutes} min`);
389
+ if (cost.tokens !== undefined)
390
+ parts.push(tokensLabel(cost.tokens));
391
+ return parts.length ? `took ${parts.join(" and ")} to work out` : null;
392
+ }
393
+ /** `'a b'`, with any single quote inside closed, escaped and reopened: safe to paste into a shell. */
394
+ function shellQuote(s) {
395
+ return `'${s.replace(/'/g, `'\\''`)}'`;
396
+ }
397
+ function clip(s, n = 88) {
398
+ return s.length > n ? `${s.slice(0, n - 1).trimEnd()}…` : s;
399
+ }
400
+ /** What reading a note costs an agent, in tokens: its text at ~4 characters a token. */
401
+ export function readingTokens(n) {
402
+ return Math.ceil(renderNote(n).length / 4);
403
+ }
404
+ /**
405
+ * What reusing a learning saves: what it took to work out, less what reading
406
+ * it costs. 0 when it carries no token cost, or reading it costs as much.
407
+ */
408
+ export function savedByNote(n) {
409
+ const took = n.cost?.tokens ?? 0;
410
+ return Math.max(0, took - readingTokens(n));
411
+ }
412
+ /** `Anirudh's learning`, `a learning`, `Kumar's learning on kumar/jitter`. */
413
+ export function whoseLearning(n) {
414
+ const who = n.author ? `${n.author}'s learning` : "a learning";
415
+ return n.ref ? `${who} on ${n.ref.replace(/^origin\//, "")}` : who;
416
+ }
417
+ /**
418
+ * The block `ask` prints above its results: whose learning each is, when,
419
+ * what it was about and what it cost, then the first line of each section and
420
+ * where to read the rest. A learning on a teammate's unmerged branch says so,
421
+ * with the `git show` that prints it, since there's no file here to open. An agent
422
+ * relays the cost when it says what the turn saved, which is how the saving
423
+ * becomes countable.
424
+ */
425
+ export function formatNoteHits(hits) {
426
+ const lines = [];
427
+ for (const { note } of hits) {
428
+ const who = `${note.author ? `${note.author}'s learning` : "a learning"}${note.teammate ? ", a teammate's" : ""}`;
429
+ lines.push(`from ${who} · ${shortDate(note.date)} · ${note.title}`);
430
+ const cost = costLabel(note.cost);
431
+ for (const s of SECTIONS) {
432
+ const lead = sectionLead(note.body, s.heading);
433
+ if (lead)
434
+ lines.push(` ${s.label.padEnd(11)} ${clip(lead)}`);
435
+ }
436
+ if (note.ref) {
437
+ // On a teammate's branch, not reviewed yet: where to read the rest, and that it's unmerged.
438
+ lines.push(` on ${note.ref.replace(/^origin\//, "")}, pushed and not merged yet · git show ${shellQuote(note.path)}${cost ? ` · ${cost}` : ""}`);
439
+ lines.push("");
440
+ continue;
441
+ }
442
+ lines.push(` ${notePlace(note)}${cost ? ` · ${cost}` : ""}`);
443
+ lines.push("");
444
+ }
445
+ return lines;
446
+ }
447
+ /** The line an agent closes its reply with after saving a learning just for the user. */
448
+ export const PERSONAL_SAVED_LINE = "📝 Saved just for you · stays on this machine, out of the repo";
449
+ /** `Anay`, `Anay and Rushil`, `Anay, Rushil and 3 others`. */
450
+ export function namesLabel(names) {
451
+ if (names.length <= 2)
452
+ return names.join(" and ");
453
+ if (names.length === 3)
454
+ return `${names[0]}, ${names[1]} and ${names[2]}`;
455
+ return `${names[0]}, ${names[1]} and ${names.length - 2} others`;
456
+ }
457
+ /** `48k`, `3.8k`, `600`. */
458
+ function tokensShort(n) {
459
+ return tokensLabel(n).replace(/^~/, "").replace(" tokens", "");
460
+ }
461
+ /**
462
+ * The lines that close a reply after a team learning is saved, one for each
463
+ * way the turn can end, since only the agent knows whether it committed the
464
+ * learning by then. Uncommitted, it makes the case for committing: who else
465
+ * works on the same files, and the head start their agents would get, which
466
+ * is what the learning took to work out.
467
+ */
468
+ export function commitLines(note, teammates, where = "files") {
469
+ const start = note.cost?.tokens ? `a ${tokensShort(note.cost.tokens)}-token head start` : "a head start";
470
+ const place = where === "files" ? "this part of the codebase" : "this repo";
471
+ if (!teammates.length) {
472
+ return {
473
+ committed: `📝 Committed · your teammates' agents now get ${start}`,
474
+ uncommitted: `🌱 Commit what you learned and give your teammates' agents ${start}`,
475
+ };
476
+ }
477
+ const who = namesLabel(teammates);
478
+ const work = teammates.length === 1 ? "works" : "work";
479
+ return {
480
+ committed: `📝 Committed · ${who} ${work} in ${place}, and their agents now get ${start}`,
481
+ uncommitted: `🌱 ${who} ${work} in ${place}. Commit what you learned and give their agents ${start}`,
482
+ };
483
+ }
484
+ /**
485
+ * The line that credits reused learnings, for the savings accumulator and the
486
+ * agent's closing line: what they took to work out, less what reading them
487
+ * here costs. Null when no shown learning carries a token cost.
488
+ */
489
+ export function learningSavingsLine(hits) {
490
+ const saved = hits.reduce((sum, h) => sum + savedByNote(h.note), 0);
491
+ if (saved <= 0)
492
+ return null;
493
+ const read = hits.reduce((sum, h) => sum + readingTokens(h.note), 0);
494
+ const minutes = hits.reduce((sum, h) => sum + (h.note.cost?.minutes ?? 0), 0);
495
+ const time = minutes > 0 ? ` and ~${minutes} min` : "";
496
+ return `[trail] learnings saved ≈ ${saved.toLocaleString("en-US")} tokens${time} (estimate): what they took to work out, less the ~${read.toLocaleString("en-US")} tokens to read them here.`;
497
+ }
498
+ //# sourceMappingURL=notes.js.map