@nanonets/graft 0.23.0 → 0.24.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 (287) hide show
  1. package/README.md +4 -4
  2. package/dist/ai/crux.d.ts +31 -3
  3. package/dist/ai/crux.d.ts.map +1 -1
  4. package/dist/ai/crux.js +88 -10
  5. package/dist/ai/crux.js.map +1 -1
  6. package/dist/ai/llm/openai.d.ts +101 -0
  7. package/dist/ai/llm/openai.d.ts.map +1 -1
  8. package/dist/ai/llm/openai.js +396 -6
  9. package/dist/ai/llm/openai.js.map +1 -1
  10. package/dist/ai/llm/recover-tool.d.ts +2 -0
  11. package/dist/ai/llm/recover-tool.d.ts.map +1 -1
  12. package/dist/ai/llm/recover-tool.js +1 -1
  13. package/dist/ai/llm/recover-tool.js.map +1 -1
  14. package/dist/ai/synthesize.d.ts.map +1 -1
  15. package/dist/ai/synthesize.js +100 -1
  16. package/dist/ai/synthesize.js.map +1 -1
  17. package/dist/claude/format.d.ts +21 -0
  18. package/dist/claude/format.d.ts.map +1 -1
  19. package/dist/claude/format.js +46 -1
  20. package/dist/claude/format.js.map +1 -1
  21. package/dist/claude/hooks.d.ts +5 -9
  22. package/dist/claude/hooks.d.ts.map +1 -1
  23. package/dist/claude/hooks.js +71 -58
  24. package/dist/claude/hooks.js.map +1 -1
  25. package/dist/claude/init.js +3 -3
  26. package/dist/claude/init.js.map +1 -1
  27. package/dist/claude/note-signals.d.ts +28 -0
  28. package/dist/claude/note-signals.d.ts.map +1 -0
  29. package/dist/claude/note-signals.js +325 -0
  30. package/dist/claude/note-signals.js.map +1 -0
  31. package/dist/claude/shim-template.d.ts.map +1 -1
  32. package/dist/claude/shim-template.js +17 -6
  33. package/dist/claude/shim-template.js.map +1 -1
  34. package/dist/claude/skill-template.d.ts.map +1 -1
  35. package/dist/claude/skill-template.js +49 -18
  36. package/dist/claude/skill-template.js.map +1 -1
  37. package/dist/claude/state.d.ts +4 -3
  38. package/dist/claude/state.d.ts.map +1 -1
  39. package/dist/claude/state.js.map +1 -1
  40. package/dist/claude/sync-run.js +1 -1
  41. package/dist/claude/sync-run.js.map +1 -1
  42. package/dist/cli-picker.d.ts +12 -5
  43. package/dist/cli-picker.d.ts.map +1 -1
  44. package/dist/cli-picker.js +24 -8
  45. package/dist/cli-picker.js.map +1 -1
  46. package/dist/cli.d.ts +2 -0
  47. package/dist/cli.d.ts.map +1 -1
  48. package/dist/cli.js +149 -43
  49. package/dist/cli.js.map +1 -1
  50. package/dist/context/batches.d.ts +29 -0
  51. package/dist/context/batches.d.ts.map +1 -0
  52. package/dist/context/batches.js +130 -0
  53. package/dist/context/batches.js.map +1 -0
  54. package/dist/context/build.d.ts +7 -2
  55. package/dist/context/build.d.ts.map +1 -1
  56. package/dist/context/build.js +84 -50
  57. package/dist/context/build.js.map +1 -1
  58. package/dist/context/node-file.d.ts.map +1 -1
  59. package/dist/context/node-file.js +41 -4
  60. package/dist/context/node-file.js.map +1 -1
  61. package/dist/engine.d.ts +2 -0
  62. package/dist/engine.d.ts.map +1 -1
  63. package/dist/engine.js +1 -0
  64. package/dist/engine.js.map +1 -1
  65. package/dist/graph/cards.d.ts.map +1 -1
  66. package/dist/graph/cards.js +9 -4
  67. package/dist/graph/cards.js.map +1 -1
  68. package/dist/graph/enrich.d.ts.map +1 -1
  69. package/dist/graph/enrich.js +8 -3
  70. package/dist/graph/enrich.js.map +1 -1
  71. package/dist/graph/generic.d.ts.map +1 -1
  72. package/dist/graph/generic.js +207 -6
  73. package/dist/graph/generic.js.map +1 -1
  74. package/dist/graph/lsp/client.d.ts +18 -0
  75. package/dist/graph/lsp/client.d.ts.map +1 -1
  76. package/dist/graph/lsp/client.js +40 -2
  77. package/dist/graph/lsp/client.js.map +1 -1
  78. package/dist/graph/lsp/enrich.d.ts.map +1 -1
  79. package/dist/graph/lsp/enrich.js +9 -1
  80. package/dist/graph/lsp/enrich.js.map +1 -1
  81. package/dist/graph/lsp/registry.d.ts +7 -0
  82. package/dist/graph/lsp/registry.d.ts.map +1 -1
  83. package/dist/graph/lsp/registry.js +27 -2
  84. package/dist/graph/lsp/registry.js.map +1 -1
  85. package/dist/graph/queries/gleam.scm +51 -0
  86. package/dist/graph/resolve.d.ts.map +1 -1
  87. package/dist/graph/resolve.js +65 -3
  88. package/dist/graph/resolve.js.map +1 -1
  89. package/dist/graph/workspace-cli.d.ts +2 -0
  90. package/dist/graph/workspace-cli.d.ts.map +1 -1
  91. package/dist/graph/workspace-cli.js +1 -1
  92. package/dist/graph/workspace-cli.js.map +1 -1
  93. package/dist/init-guard.d.ts +11 -0
  94. package/dist/init-guard.d.ts.map +1 -0
  95. package/dist/init-guard.js +21 -0
  96. package/dist/init-guard.js.map +1 -0
  97. package/dist/mcp/tools.d.ts.map +1 -1
  98. package/dist/mcp/tools.js +2 -1
  99. package/dist/mcp/tools.js.map +1 -1
  100. package/dist/notes/branches.d.ts +7 -1
  101. package/dist/notes/branches.d.ts.map +1 -1
  102. package/dist/notes/branches.js +31 -3
  103. package/dist/notes/branches.js.map +1 -1
  104. package/dist/notes/cold-start.d.ts +12 -2
  105. package/dist/notes/cold-start.d.ts.map +1 -1
  106. package/dist/notes/cold-start.js +46 -5
  107. package/dist/notes/cold-start.js.map +1 -1
  108. package/dist/notes/credit.d.ts +37 -0
  109. package/dist/notes/credit.d.ts.map +1 -0
  110. package/dist/notes/credit.js +78 -0
  111. package/dist/notes/credit.js.map +1 -0
  112. package/dist/notes/find-notes.d.ts +17 -0
  113. package/dist/notes/find-notes.d.ts.map +1 -0
  114. package/dist/notes/find-notes.js +48 -0
  115. package/dist/notes/find-notes.js.map +1 -0
  116. package/dist/notes/flatten.d.ts +31 -0
  117. package/dist/notes/flatten.d.ts.map +1 -0
  118. package/dist/notes/flatten.js +73 -0
  119. package/dist/notes/flatten.js.map +1 -0
  120. package/dist/notes/git-facts.d.ts +7 -1
  121. package/dist/notes/git-facts.d.ts.map +1 -1
  122. package/dist/notes/git-facts.js +12 -2
  123. package/dist/notes/git-facts.js.map +1 -1
  124. package/dist/notes/home.d.ts +24 -6
  125. package/dist/notes/home.d.ts.map +1 -1
  126. package/dist/notes/home.js +82 -15
  127. package/dist/notes/home.js.map +1 -1
  128. package/dist/notes/learning-fields.d.ts +102 -0
  129. package/dist/notes/learning-fields.d.ts.map +1 -0
  130. package/dist/notes/learning-fields.js +214 -0
  131. package/dist/notes/learning-fields.js.map +1 -0
  132. package/dist/notes/memory-notes.d.ts +71 -0
  133. package/dist/notes/memory-notes.d.ts.map +1 -0
  134. package/dist/notes/memory-notes.js +239 -0
  135. package/dist/notes/memory-notes.js.map +1 -0
  136. package/dist/notes/note-gate.d.ts +42 -0
  137. package/dist/notes/note-gate.d.ts.map +1 -0
  138. package/dist/notes/note-gate.js +181 -0
  139. package/dist/notes/note-gate.js.map +1 -0
  140. package/dist/notes/notes.d.ts +29 -77
  141. package/dist/notes/notes.d.ts.map +1 -1
  142. package/dist/notes/notes.js +69 -228
  143. package/dist/notes/notes.js.map +1 -1
  144. package/dist/notes/pr-reviews.d.ts +35 -7
  145. package/dist/notes/pr-reviews.d.ts.map +1 -1
  146. package/dist/notes/pr-reviews.js +156 -46
  147. package/dist/notes/pr-reviews.js.map +1 -1
  148. package/dist/notes/secrets.d.ts +28 -0
  149. package/dist/notes/secrets.d.ts.map +1 -0
  150. package/dist/notes/secrets.js +362 -0
  151. package/dist/notes/secrets.js.map +1 -0
  152. package/dist/notes/session-cost.d.ts +2 -0
  153. package/dist/notes/session-cost.d.ts.map +1 -1
  154. package/dist/notes/session-cost.js +8 -3
  155. package/dist/notes/session-cost.js.map +1 -1
  156. package/dist/notes/targets.d.ts +37 -0
  157. package/dist/notes/targets.d.ts.map +1 -0
  158. package/dist/notes/targets.js +108 -0
  159. package/dist/notes/targets.js.map +1 -0
  160. package/dist/notes/team-check.d.ts +22 -0
  161. package/dist/notes/team-check.d.ts.map +1 -0
  162. package/dist/notes/team-check.js +130 -0
  163. package/dist/notes/team-check.js.map +1 -0
  164. package/dist/notes/trail-home.d.ts +3 -0
  165. package/dist/notes/trail-home.d.ts.map +1 -0
  166. package/dist/notes/trail-home.js +12 -0
  167. package/dist/notes/trail-home.js.map +1 -0
  168. package/dist/search/grep-cli.d.ts +5 -2
  169. package/dist/search/grep-cli.d.ts.map +1 -1
  170. package/dist/search/grep-cli.js +11 -4
  171. package/dist/search/grep-cli.js.map +1 -1
  172. package/dist/search/ignored-code.d.ts +9 -0
  173. package/dist/search/ignored-code.d.ts.map +1 -0
  174. package/dist/search/ignored-code.js +73 -0
  175. package/dist/search/ignored-code.js.map +1 -0
  176. package/dist/skills/answers-prompt.d.ts +13 -0
  177. package/dist/skills/answers-prompt.d.ts.map +1 -0
  178. package/dist/skills/answers-prompt.js +83 -0
  179. package/dist/skills/answers-prompt.js.map +1 -0
  180. package/dist/skills/never-save.d.ts +5 -0
  181. package/dist/skills/never-save.d.ts.map +1 -0
  182. package/dist/skills/never-save.js +158 -0
  183. package/dist/skills/never-save.js.map +1 -0
  184. package/dist/skills/params.d.ts +39 -0
  185. package/dist/skills/params.d.ts.map +1 -0
  186. package/dist/skills/params.js +275 -0
  187. package/dist/skills/params.js.map +1 -0
  188. package/dist/skills/ran-less.d.ts +5 -0
  189. package/dist/skills/ran-less.d.ts.map +1 -0
  190. package/dist/skills/ran-less.js +51 -0
  191. package/dist/skills/ran-less.js.map +1 -0
  192. package/dist/skills/risk-delta.d.ts +32 -0
  193. package/dist/skills/risk-delta.d.ts.map +1 -0
  194. package/dist/skills/risk-delta.js +56 -0
  195. package/dist/skills/risk-delta.js.map +1 -0
  196. package/dist/skills/risk.d.ts +73 -0
  197. package/dist/skills/risk.d.ts.map +1 -0
  198. package/dist/skills/risk.js +396 -0
  199. package/dist/skills/risk.js.map +1 -0
  200. package/dist/skills/types.d.ts +422 -0
  201. package/dist/skills/types.d.ts.map +1 -0
  202. package/dist/skills/types.js +2 -0
  203. package/dist/skills/types.js.map +1 -0
  204. package/dist/telemetry/contract.d.ts.map +1 -1
  205. package/dist/telemetry/contract.js +8 -6
  206. package/dist/telemetry/contract.js.map +1 -1
  207. package/dist/telemetry/notice.d.ts.map +1 -1
  208. package/dist/telemetry/notice.js +3 -1
  209. package/dist/telemetry/notice.js.map +1 -1
  210. package/dist/trace/check-reads.d.ts +36 -0
  211. package/dist/trace/check-reads.d.ts.map +1 -0
  212. package/dist/trace/check-reads.js +57 -0
  213. package/dist/trace/check-reads.js.map +1 -0
  214. package/dist/trace/host.d.ts +40 -0
  215. package/dist/trace/host.d.ts.map +1 -0
  216. package/dist/trace/host.js +84 -0
  217. package/dist/trace/host.js.map +1 -0
  218. package/dist/trace/redact.d.ts +8 -0
  219. package/dist/trace/redact.d.ts.map +1 -0
  220. package/dist/trace/redact.js +63 -0
  221. package/dist/trace/redact.js.map +1 -0
  222. package/dist/trace/seen-cache.d.ts +9 -0
  223. package/dist/trace/seen-cache.d.ts.map +1 -0
  224. package/dist/trace/seen-cache.js +46 -0
  225. package/dist/trace/seen-cache.js.map +1 -0
  226. package/dist/trace/shell-scan.d.ts +52 -0
  227. package/dist/trace/shell-scan.d.ts.map +1 -0
  228. package/dist/trace/shell-scan.js +318 -0
  229. package/dist/trace/shell-scan.js.map +1 -0
  230. package/dist/trace/shell-words.d.ts +91 -0
  231. package/dist/trace/shell-words.d.ts.map +1 -0
  232. package/dist/trace/shell-words.js +558 -0
  233. package/dist/trace/shell-words.js.map +1 -0
  234. package/dist/trace/shell.d.ts +91 -0
  235. package/dist/trace/shell.d.ts.map +1 -0
  236. package/dist/trace/shell.js +435 -0
  237. package/dist/trace/shell.js.map +1 -0
  238. package/dist/trace/state.d.ts +27 -0
  239. package/dist/trace/state.d.ts.map +1 -0
  240. package/dist/trace/state.js +98 -0
  241. package/dist/trace/state.js.map +1 -0
  242. package/dist/trace/tamper-code.d.ts +9 -0
  243. package/dist/trace/tamper-code.d.ts.map +1 -0
  244. package/dist/trace/tamper-code.js +174 -0
  245. package/dist/trace/tamper-code.js.map +1 -0
  246. package/dist/trace/tamper-git.d.ts +12 -0
  247. package/dist/trace/tamper-git.d.ts.map +1 -0
  248. package/dist/trace/tamper-git.js +77 -0
  249. package/dist/trace/tamper-git.js.map +1 -0
  250. package/dist/trace/types.d.ts +477 -0
  251. package/dist/trace/types.d.ts.map +1 -0
  252. package/dist/trace/types.js +12 -0
  253. package/dist/trace/types.js.map +1 -0
  254. package/dist/trace/walls-glob.d.ts +11 -0
  255. package/dist/trace/walls-glob.d.ts.map +1 -0
  256. package/dist/trace/walls-glob.js +264 -0
  257. package/dist/trace/walls-glob.js.map +1 -0
  258. package/dist/trace/walls-quoted.d.ts +25 -0
  259. package/dist/trace/walls-quoted.d.ts.map +1 -0
  260. package/dist/trace/walls-quoted.js +81 -0
  261. package/dist/trace/walls-quoted.js.map +1 -0
  262. package/dist/upkeep.d.ts +1 -1
  263. package/dist/upkeep.d.ts.map +1 -1
  264. package/dist/upkeep.js +4 -1
  265. package/dist/upkeep.js.map +1 -1
  266. package/dist/util/actions-folder.d.ts +2 -0
  267. package/dist/util/actions-folder.d.ts.map +1 -0
  268. package/dist/util/actions-folder.js +9 -0
  269. package/dist/util/actions-folder.js.map +1 -0
  270. package/dist/util/front-matter.d.ts +31 -4
  271. package/dist/util/front-matter.d.ts.map +1 -1
  272. package/dist/util/front-matter.js +95 -3
  273. package/dist/util/front-matter.js.map +1 -1
  274. package/dist/util/private-file.d.ts +14 -0
  275. package/dist/util/private-file.d.ts.map +1 -0
  276. package/dist/util/private-file.js +82 -0
  277. package/dist/util/private-file.js.map +1 -0
  278. package/dist/util/private-fs.d.ts +25 -0
  279. package/dist/util/private-fs.d.ts.map +1 -0
  280. package/dist/util/private-fs.js +253 -0
  281. package/dist/util/private-fs.js.map +1 -0
  282. package/dist/util/state.d.ts.map +1 -1
  283. package/dist/util/state.js +3 -0
  284. package/dist/util/state.js.map +1 -1
  285. package/dist/viewer/app.js +2 -2
  286. package/package.json +5 -5
  287. package/scripts/run-tests.mjs +14 -2
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ import { contextDirFor } from "./context/node-file.js";
31
31
  import { loadGraphCached } from "./graph/load.js";
32
32
  import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
33
33
  import { isWorkspaceBuildRoot, readWorkspace } from "./graph/workspace.js";
34
+ import { NOT_GIT_LINE, homeFolderRefusal, isGitWorkTree } from "./init-guard.js";
34
35
  import { hasGraftIndex, nearestGraftRoot } from "./graph/root.js";
35
36
  import { unsupportedExtensions, supportedExtensions } from "./graph/source-files.js";
36
37
  import { discoverWorkspaceChildren } from "./graph/scopes.js";
@@ -48,10 +49,13 @@ import { normalizePathPrefix } from "./util/paths.js";
48
49
  import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
49
50
  import { listSessionIds, readSession, sessionDir } from "./claude/state.js";
50
51
  import { costLabel, listNotes, noteName, notePlace, noteTargets, readingTokens, savedByNote, commitLines, PERSONAL_SAVED_LINE, PR_SPEEDUP, SECTIONS, tokensWorthSaying, sectionLead, shortDate, tokensLabel, writeNote, mentionedFiles } from "./notes/notes.js";
52
+ import { findLearning, liveLearnings } from "./notes/learning-fields.js";
51
53
  import { aboutTerms, currentSessionCost } from "./notes/session-cost.js";
52
54
  import { beginPastRead, finishPastRead } from "./notes/cold-start.js";
53
- import { beginReviewRead, finishReviewRead } from "./notes/pr-reviews.js";
55
+ import { anchoredReviews, beginReviewRead, finishReviewRead } from "./notes/pr-reviews.js";
56
+ import { finishMemoryRead, memoryCandidates, offeredBefore } from "./notes/memory-notes.js";
54
57
  import { currentBranch, noteAuthor, teammatesOn } from "./notes/git-facts.js";
58
+ import { gateNote, namesRepoSymbol, symbolCandidates, trackedFiles } from "./notes/note-gate.js";
55
59
  import { confirm, fold, getSkill, learn, listSkills, publish, skillForAgent, skillName, skillStatus } from "./skills/skills.js";
56
60
  import { clearPending, cloudLines, loginLink, sessionKey, waitForConnect } from "./cloud/nudge.js";
57
61
  import { addRepo, clearCloud, cloudEnabled, fetchMe, fetchTeam, isError, readCloud, writeCloud } from "./cloud/workspace.js";
@@ -389,6 +393,8 @@ grouped(program.command("build"), GROUP.setup)
389
393
  .option("--deep", "run the LLM pass: concept nodes (graft/*.md) + per-symbol summary/crux")
390
394
  .option("-e, --extensions <exts...>", 'code extensions to include (e.g. ".ts" ".py"); an extension with no parser is ignored with a warning that lists the supported set')
391
395
  .option("-j, --concurrency <n>", "files summarized in parallel during --deep (default 5)")
396
+ .option("--synth-concurrency <n>", "concept synthesis batches in flight at once during --deep (default 4). Separate from -j: " +
397
+ "each synthesis call carries a whole batch of summaries, not one file")
392
398
  .option("--no-reuse", "re-parse every file instead of replaying unchanged ones from the extraction cache")
393
399
  .option("--lsp", "add compiler-grade call edges via a language server if one is installed (opt-in, slower; e.g. rust-analyzer, clangd)")
394
400
  .option("--allow-partial", "with --deep: exit 0 even when some files' summaries failed (default: a degraded meaning tier exits 1)")
@@ -422,6 +428,11 @@ grouped(program.command("build"), GROUP.setup)
422
428
  console.error(`✗ --concurrency must be a number, got "${opts.concurrency}"`);
423
429
  process.exit(1);
424
430
  }
431
+ const synthConcurrency = opts.synthConcurrency ? Math.max(1, Number(opts.synthConcurrency)) : undefined;
432
+ if (opts.synthConcurrency && !Number.isFinite(synthConcurrency)) {
433
+ console.error(`✗ --synth-concurrency must be a number, got "${opts.synthConcurrency}"`);
434
+ process.exit(1);
435
+ }
425
436
  warnUnsupportedExtensions(opts.extensions);
426
437
  // Persisted BEFORE the build itself runs, so this invocation's walks (and
427
438
  // every later no-flag build / hooks refresh) see it identically — the
@@ -498,6 +509,7 @@ grouped(program.command("build"), GROUP.setup)
498
509
  deep: !!deep,
499
510
  extensions: opts.extensions,
500
511
  concurrency,
512
+ synthConcurrency,
501
513
  childConfig: cliConfig(),
502
514
  override: buildGlobalDir,
503
515
  includeDirs: opts.includeDir,
@@ -514,6 +526,7 @@ grouped(program.command("build"), GROUP.setup)
514
526
  const c = await engine.init(dir, {
515
527
  extensions: opts.extensions,
516
528
  onlyDirs,
529
+ synthConcurrency,
517
530
  onProgress: ({ phase, index, total, file }) => process.stderr.write(`\r${phase === "summarize" ? "reading" : "writing"} concepts ${index + 1}/${total}: ${file.slice(0, 40).padEnd(40)}`),
518
531
  }).catch((err) => {
519
532
  // Only the stage and a code enum; the message stays on this machine.
@@ -521,7 +534,11 @@ grouped(program.command("build"), GROUP.setup)
521
534
  throw err;
522
535
  });
523
536
  process.stderr.write("\n");
524
- console.log(`✓ concepts: ${c.nodes} nodes, ${c.links} links from ${c.files} files (${c.summarized} read, ${c.cached} cached)`);
537
+ const conceptLine = `concepts: ${c.nodes} nodes, ${c.links} links from ${c.files} files (${c.summarized} read, ${c.cached} cached)`;
538
+ if (c.nodes === 0)
539
+ console.error(`⚠ ${conceptLine}`);
540
+ else
541
+ console.log(`✓ ${conceptLine}`);
525
542
  for (const e of c.errors)
526
543
  console.error(`✗ ${e}`);
527
544
  conceptErrors = c.errors;
@@ -983,6 +1000,8 @@ grouped(program.command("note", { hidden: !TRAIL }), GROUP.memory)
983
1000
  .option("--author <name>", "who wrote it (default: the first word of git config user.name)")
984
1001
  .option("--minutes <n>", "what it took to work out, when the agent's transcript can't say")
985
1002
  .option("--tokens <n>", "the same, in tokens")
1003
+ .option("--replaces <file>", "an earlier learning this one takes over from (its file name in .trail/learnings/); the old file stays")
1004
+ .option("--anyway", "save it even if trail's checks (names code, says why, not status, not a repeat) turn it away")
986
1005
  .option("--json", "print the saved learning as JSON")
987
1006
  .argument("[dir]", "repository root", ".")
988
1007
  .action(async (dir, opts) => {
@@ -1198,19 +1217,60 @@ async function runNoteCommand(repo, opts) {
1198
1217
  // session edited. Explicit --touches means just this repo.
1199
1218
  const targets = opts.touches
1200
1219
  ? [{ dir: repo, touches: opts.touches.split(",").map((t) => t.trim()).filter(Boolean) }]
1201
- : noteTargets(repo, spent?.edited ?? [], spent?.startedAt).map((t) => ({
1202
- ...t,
1203
- // And the files the note names, for a session that read code without editing it.
1204
- touches: [...new Set([...t.touches, ...mentionedFiles(t.dir, body)])].slice(0, 10),
1220
+ : noteTargets(repo, spent?.edited ?? [], spent?.startedAt, spent ? { read: spent.read } : undefined).map((t) => ({
1221
+ dir: t.dir,
1222
+ // Then the files the note names, then the ones the session only read.
1223
+ touches: [...new Set([...t.touches, ...mentionedFiles(t.dir, body), ...(t.read ?? [])])].slice(0, 10),
1205
1224
  }));
1225
+ // A touch is a file git tracks or one this session edited: never a file
1226
+ // that was just lying around (a token, a scratch file).
1227
+ const real = (dir, files) => {
1228
+ const tracked = trackedFiles(dir, files.map((f) => f.split("#")[0]));
1229
+ const edited = new Set((spent?.edited ?? []).map((f) => relative(dir, f).split("\\").join("/")));
1230
+ return files.filter((f) => tracked.has(f.split("#")[0]) || edited.has(f.split("#")[0]));
1231
+ };
1232
+ for (const t of targets)
1233
+ t.touches = real(t.dir, t.touches);
1234
+ const scope = opts.personal ? "personal" : "repo";
1235
+ const primary = targets[0];
1236
+ const existing = listNotes(primary.dir, { branches: false });
1237
+ let old;
1238
+ if (opts.replaces) {
1239
+ const want = basename(opts.replaces.trim());
1240
+ old = findLearning(existing, want);
1241
+ if (!old) {
1242
+ console.error(`✗ --replaces: no learning named ${want} here. The names are the files in .trail/learnings/.`);
1243
+ process.exitCode = 1;
1244
+ return;
1245
+ }
1246
+ }
1247
+ // What `replaces:` holds is the old learning's id (learning-fields.ts), not its file name.
1248
+ const replaces = old?.id;
1249
+ const text = `${opts.title}\n${body}`;
1250
+ const gate = gateNote({
1251
+ title: opts.title,
1252
+ body,
1253
+ personal: scope === "personal",
1254
+ files: real(primary.dir, [...new Set([...mentionedFiles(primary.dir, text), ...(opts.touches ? primary.touches : [])])]),
1255
+ namesSymbol: namesRepoSymbol(primary.dir, symbolCandidates(text)),
1256
+ touches: primary.touches,
1257
+ replaces,
1258
+ }, liveLearnings(existing).filter((n) => (n.scope ?? "repo") === scope));
1259
+ if (gate.problems.length && !opts.anyway) {
1260
+ console.error("✗ not saved yet:");
1261
+ for (const p of gate.problems)
1262
+ console.error(` · ${p}`);
1263
+ console.error(`Fix it and run ${cmd("graft note")} again, or add --anyway if a check is wrong.`);
1264
+ process.exitCode = 1;
1265
+ return;
1266
+ }
1206
1267
  const author = opts.author?.trim() || noteAuthor(repo);
1207
1268
  const date = new Date().toISOString().slice(0, 10);
1208
- const scope = opts.personal ? "personal" : "repo";
1209
1269
  const earlier = [];
1210
1270
  const saved = [];
1211
1271
  for (const t of targets) {
1212
1272
  earlier.push(...listNotes(t.dir));
1213
- saved.push(writeNote(t.dir, { title: opts.title, body, author, date, branch: currentBranch(t.dir), cost, touches: t.touches }, scope));
1273
+ saved.push(writeNote(t.dir, { title: opts.title, body, author, date, branch: currentBranch(t.dir), cost, touches: t.touches, replaces }, scope));
1214
1274
  }
1215
1275
  const touches = targets.flatMap((t) => t.touches);
1216
1276
  track("note_saved", { has_cost: String(cost?.tokens !== undefined || cost?.minutes !== undefined), touches_bucket: countBucket(touches.length), scope: saved[0]?.scope ?? scope }, { repo });
@@ -1239,6 +1299,10 @@ async function runNoteCommand(repo, opts) {
1239
1299
  console.log(`· builds on ${prior.author ? `${prior.author}'s` : "a"} learning from ${shortDate(prior.date)}`);
1240
1300
  if (!SECTIONS.some((s) => sectionLead(body, s.heading) !== null))
1241
1301
  console.log("· tip: learnings read best under ## Decided, ## Tried and ruled out and ## Watch out");
1302
+ for (const w of gate.warnings)
1303
+ console.log(`· ${w}`);
1304
+ if (old)
1305
+ console.log(`· takes over from ${noteName(old)}, which stays as the record`);
1242
1306
  const repoSaved = saved.filter((n) => n.scope === "repo");
1243
1307
  if (repoSaved.length) {
1244
1308
  // Who else works on these files, for the case for committing it.
@@ -1246,7 +1310,8 @@ async function runNoteCommand(repo, opts) {
1246
1310
  console.log(`${lines.uncommitted} · git add ${repoSaved.map(notePlace).join(" ")}`);
1247
1311
  if (!process.stdout.isTTY && TRAIL)
1248
1312
  console.log(`[trail] Close your reply with one line, word for word. If this learning is committed by the end of your turn: ${lines.committed}\n` +
1249
- ` If it isn't: ${lines.uncommitted}`);
1313
+ ` If it isn't: ${commitAsk(note.title, about)}\n` +
1314
+ " On a yes, commit it with the change: in the change's own commit when that isn't pushed yet.");
1250
1315
  }
1251
1316
  else {
1252
1317
  console.log("· just for you: kept on this machine, out of the repo");
@@ -1259,6 +1324,11 @@ async function runNoteCommand(repo, opts) {
1259
1324
  }
1260
1325
  printLines(cloudLines(repo, { command: "note" }));
1261
1326
  }
1327
+ /** What the agent asks after saving a team learning it hasn't committed: who reads it, and when. */
1328
+ export function commitAsk(title, about) {
1329
+ const when = about ? `when they open ${basename(about.split("#")[0])}` : "on their next session here";
1330
+ return `📝 Learning: "${title.replace(/"/g, "'")}" · your teammates' Claude reads it ${when}. Commit it with this change? (y/n)`;
1331
+ }
1262
1332
  /** Lines that close a command's output, with a blank line before them. */
1263
1333
  function printLines(lines) {
1264
1334
  if (lines.length)
@@ -1277,6 +1347,14 @@ async function runInitCommand(dir, opts) {
1277
1347
  }
1278
1348
  const repo = resolve(dir);
1279
1349
  const explicit = Array.isArray(opts.agents) ? opts.agents : undefined;
1350
+ // Checked before anything is written: run from the home folder, init wired
1351
+ // it, mapped every code file under it, then said to commit.
1352
+ const refusal = homeFolderRefusal(repo);
1353
+ if (refusal) {
1354
+ console.error(refusal);
1355
+ process.exitCode = 1;
1356
+ return null;
1357
+ }
1280
1358
  if (explicit) {
1281
1359
  const validIds = [...hostIds(), "claude"];
1282
1360
  const unknown = explicit.filter((id) => !validIds.includes(id));
@@ -1429,23 +1507,29 @@ async function runInitCommand(dir, opts) {
1429
1507
  console.error(`✓ ${id.padEnd(width)} ${summary}`.trimEnd());
1430
1508
  }
1431
1509
  }
1432
- // Under trail: what init learned from past sessions and PR reviews, where
1433
- // this repo's learnings are, and how many there already are.
1510
+ // Under trail: what init learned, where this repo's learnings are, and how
1511
+ // many there already are. The team's come from what is tied to the code:
1512
+ // what reviewers kept flagging on merged PRs, and Claude Code's notes about
1513
+ // the repo on this machine. What the person kept telling Claude is theirs.
1434
1514
  let learnedNow = false;
1435
- if (TRAIL && (pastRead || reviewRead)) {
1515
+ let teamSaved = 0;
1516
+ const memory = TRAIL && wantClaude ? memoryCandidates(repo, { offered: offeredBefore(repo) }) : [];
1517
+ if (TRAIL && (pastRead || reviewRead || memory.length)) {
1436
1518
  if (pastRead && !pastRead.done)
1437
1519
  console.error("· still reading your past sessions…");
1438
1520
  const fromSessions = pastRead ? (await pastRead.result).findings : [];
1439
1521
  if (reviewRead && !reviewRead.done)
1440
1522
  console.error("· still reading PR reviews…");
1441
- const fromReviews = reviewRead ? (await reviewRead.result).findings : [];
1442
- // On a terminal, the person ticks which to keep in one list for both
1443
- // sources, all ticked to start; a scripted run, or one with --yes, keeps
1444
- // them all. Cancelling saves none, and the next init asks again.
1445
- const picked = process.stdin.isTTY && process.stderr.isTTY && !opts.yes && fromSessions.length + fromReviews.length
1523
+ const fromReviews = reviewRead ? anchoredReviews(repo, (await reviewRead.result).findings) : [];
1524
+ // On a terminal, the person ticks which to keep in one list, the team's
1525
+ // first and ticked, their own unticked. A scripted run, or one with
1526
+ // --yes, keeps the team's and what they told Claude. Cancelling saves
1527
+ // none, and the next init asks again.
1528
+ const picked = process.stdin.isTTY && process.stderr.isTTY && !opts.yes && fromReviews.length + memory.length + fromSessions.length
1446
1529
  ? await pickLearnings([
1447
- ...fromSessions.map((f) => ({ title: f.title, forTeam: f.for === "team", sessions: f.sessions.length })),
1448
- ...fromReviews.map((f) => ({ title: f.title, forTeam: true, prs: f.prs.length })),
1530
+ ...fromReviews.map((f) => ({ title: f.title, forTeam: true, prs: f.prs })),
1531
+ ...memory.map((c) => ({ title: c.title, forTeam: c.forTeam, memory: true })),
1532
+ ...fromSessions.map((f) => ({ title: f.title, forTeam: false, sessions: f.sessions.length })),
1449
1533
  ])
1450
1534
  : undefined;
1451
1535
  // The picked rows that are `found`'s, which start at `from` in the list.
@@ -1453,56 +1537,70 @@ async function runInitCommand(dir, opts) {
1453
1537
  ? undefined
1454
1538
  : async (found) => picked && picked.filter((i) => i >= from && i < from + found.length).map((i) => found[i - from]);
1455
1539
  const learned = [];
1456
- if (pastRead) {
1457
- const { result, saved, kept, cancelled } = await finishPastRead(repo, pastRead, keepFrom(0));
1458
- const offered = result.findings.length;
1540
+ // Reviews first: which of them are offered depends on the learnings already saved.
1541
+ if (reviewRead) {
1542
+ const { result, saved, kept, cancelled } = await finishReviewRead(repo, reviewRead, keepFrom(0));
1543
+ const offered = fromReviews.length;
1459
1544
  if (saved.length) {
1460
1545
  const n = saved.length;
1461
1546
  const what = n < offered ? `kept ${n} of the ${offered} things` : `learned ${n} thing${n === 1 ? "" : "s"}`;
1462
- console.error(`✓ ${what} you told Claude more than once, from your last ${result.read} sessions here`);
1463
- // Each with how many sessions it was said in: saveFindings keeps the findings' order.
1464
- saved.forEach((note, i) => {
1465
- const said = kept[i]?.sessions.length ?? 0;
1466
- const count = said > 1 ? `told Claude in ${said} sessions: ` : "";
1467
- console.error(` ${note.scope === "personal" ? "just for you " : "for your team"} · ${count}${note.title}`);
1468
- });
1547
+ console.error(`✓ ${what} reviewers made authors change, from the last ${result.prs} merged PRs here`);
1548
+ // saveReviewFindings keeps the findings' order.
1549
+ saved.forEach((note, i) => console.error(` for your team · from review on ${(kept[i]?.prs ?? []).map((n) => `#${n}`).join(", ")}: ${note.title}`));
1469
1550
  }
1470
1551
  else if (cancelled) {
1471
- console.error(`· saved none of the ${offered} things you told Claude more than once; ${cmd("graft init")} again asks again`);
1552
+ console.error(`· saved none of the ${offered} things reviewers made authors change; ${cmd("graft init")} again asks again`);
1472
1553
  }
1473
1554
  else if (offered) {
1474
- console.error(`· kept none of the ${offered} things you told Claude more than once`);
1555
+ console.error(`· kept none of the ${offered} things reviewers made authors change`);
1475
1556
  }
1476
1557
  else if (result.reason === "failed") {
1477
- console.error(`· couldn't read your past sessions this time; ${cmd("graft init")} again tries again`);
1558
+ console.error(`· couldn't read PR reviews this time; ${cmd("graft init")} again tries again`);
1478
1559
  }
1479
1560
  learned.push(...saved);
1480
1561
  }
1481
- if (reviewRead) {
1482
- const { result, saved, kept, cancelled } = await finishReviewRead(repo, reviewRead, keepFrom(fromSessions.length));
1562
+ if (memory.length) {
1563
+ const { saved, cancelled } = await finishMemoryRead(repo, memory, keepFrom(fromReviews.length));
1564
+ if (saved.length) {
1565
+ console.error(`✓ kept ${saved.length} of the ${memory.length} notes your Claude keeps about this repo`);
1566
+ for (const note of saved)
1567
+ console.error(` ${note.scope === "personal" ? "just for you " : "for your team"} · ${note.title}`);
1568
+ }
1569
+ else if (cancelled) {
1570
+ console.error(`· saved none of the ${memory.length} notes your Claude keeps about this repo; ${cmd("graft init")} again asks again`);
1571
+ }
1572
+ learned.push(...saved);
1573
+ }
1574
+ if (pastRead) {
1575
+ const { result, saved, kept, cancelled } = await finishPastRead(repo, pastRead, keepFrom(fromReviews.length + memory.length));
1483
1576
  const offered = result.findings.length;
1484
1577
  if (saved.length) {
1485
1578
  const n = saved.length;
1486
1579
  const what = n < offered ? `kept ${n} of the ${offered} things` : `learned ${n} thing${n === 1 ? "" : "s"}`;
1487
- console.error(`✓ ${what} reviewers flagged on more than one PR, from the last ${result.prs} merged PRs here`);
1488
- // saveReviewFindings keeps the findings' order.
1489
- saved.forEach((note, i) => console.error(` for your team · flagged in review on ${kept[i]?.prs.length ?? 2} PRs: ${note.title}`));
1580
+ console.error(`✓ ${what} you told Claude more than once, from your last ${result.read} sessions here, just for you`);
1581
+ // Each with how many sessions it was said in: saveFindings keeps the findings' order.
1582
+ saved.forEach((note, i) => {
1583
+ const said = kept[i]?.sessions.length ?? 0;
1584
+ const count = said > 1 ? `told Claude in ${said} sessions: ` : "";
1585
+ console.error(` just for you · ${count}${note.title}`);
1586
+ });
1490
1587
  }
1491
1588
  else if (cancelled) {
1492
- console.error(`· saved none of the ${offered} things reviewers flagged; ${cmd("graft init")} again asks again`);
1589
+ console.error(`· saved none of the ${offered} things you told Claude more than once; ${cmd("graft init")} again asks again`);
1493
1590
  }
1494
1591
  else if (offered) {
1495
- console.error(`· kept none of the ${offered} things reviewers flagged`);
1592
+ console.error(`· kept none of the ${offered} things you told Claude more than once`);
1496
1593
  }
1497
1594
  else if (result.reason === "failed") {
1498
- console.error(`· couldn't read PR reviews this time; ${cmd("graft init")} again tries again`);
1595
+ console.error(`· couldn't read your past sessions this time; ${cmd("graft init")} again tries again`);
1499
1596
  }
1500
1597
  learned.push(...saved);
1501
1598
  }
1502
1599
  learnedNow = learned.length > 0;
1600
+ teamSaved = learned.filter((s) => s.scope === "repo").length;
1503
1601
  if (learnedNow) {
1504
1602
  console.error("· Claude follows them from your next session");
1505
- if (learned.some((s) => s.scope === "repo"))
1603
+ if (teamSaved)
1506
1604
  console.error(commitLines({}, teammatesOn(repo), "repo").uncommitted);
1507
1605
  }
1508
1606
  }
@@ -1511,7 +1609,7 @@ async function runInitCommand(dir, opts) {
1511
1609
  if (home) {
1512
1610
  const learnings = listNotes(repo, { branches: false }).filter((n) => n.scope === "repo");
1513
1611
  if (learnings.length === 0)
1514
- console.error(`✓ learnings ${LEARNINGS_DIR}/ · sessions that take real digging leave one there, for your whole team`);
1612
+ console.error(`✓ learnings ${LEARNINGS_DIR}/ · a session that fixes a bug, takes a correction or opens a PR leaves one there, for your whole team`);
1515
1613
  else
1516
1614
  console.error(`✓ learnings ${learnings.length} in ${LEARNINGS_DIR}/, from ${new Set(learnings.map((n) => n.author || "someone")).size} ${new Set(learnings.map((n) => n.author)).size === 1 ? "person" : "people"}`);
1517
1615
  }
@@ -1540,7 +1638,11 @@ async function runInitCommand(dir, opts) {
1540
1638
  return r.status !== 0 || r.stdout.trim().length > 0;
1541
1639
  });
1542
1640
  tops.splice(0, tops.length, ...changedTops);
1543
- if (tops.length)
1641
+ // Outside git there is nothing to commit to, and `git status` failing above
1642
+ // made every path look changed.
1643
+ if (TRAIL && !children.length && !isGitWorkTree(repo))
1644
+ console.error(NOT_GIT_LINE);
1645
+ else if (tops.length)
1544
1646
  console.error(TRAIL
1545
1647
  ? `· commit ${tops.slice(0, 5).join(" and ")}${tops.length > 5 ? ` +${tops.length - 5} more` : ""} once — after that, teammates' Claude reads your learnings even before they install trail`
1546
1648
  : `· commit ${tops.slice(0, 5).join(" ")}${tops.length > 5 ? ` +${tops.length - 5} more` : ""} to share it — graft/ stays local and git-ignored`);
@@ -1558,6 +1660,10 @@ async function runInitCommand(dir, opts) {
1558
1660
  nodes: graphs.reduce((n, g) => n + g.meta.nodeCount, 0),
1559
1661
  edges: graphs.reduce((n, g) => n + g.meta.edgeCount, 0),
1560
1662
  }));
1663
+ // Run by an agent, not a person: the last line tells it to ask about the
1664
+ // commit, since the person never saw the picker and Claude Code folds the rest.
1665
+ if (teamSaved && !process.stderr.isTTY)
1666
+ console.error(`→ ${teamSaved} team learning${teamSaved === 1 ? "" : "s"} in ${LEARNINGS_DIR}/ ${teamSaved === 1 ? "isn't" : "aren't"} committed yet. Ask the user: "Commit ${teamSaved === 1 ? "it" : `the ${teamSaved}`} so your teammates' Claude starts from ${teamSaved === 1 ? "it" : "them"}? (yes / no)"`);
1561
1667
  // Sorted so `claude,cursor` and `cursor,claude` aggregate as one value.
1562
1668
  track("init_completed", { agents: [...ids].sort().join(","), consent: consent === undefined ? "unasked" : String(consent) }, { repo });
1563
1669
  return { ids };