@nanonets/graft 0.22.0 → 0.23.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 (140) hide show
  1. package/README.md +11 -10
  2. package/dist/app/checkout.d.ts +0 -32
  3. package/dist/app/checkout.d.ts.map +1 -1
  4. package/dist/app/checkout.js +0 -45
  5. package/dist/app/checkout.js.map +1 -1
  6. package/dist/app/identity.d.ts +0 -56
  7. package/dist/app/identity.d.ts.map +1 -1
  8. package/dist/app/identity.js +0 -119
  9. package/dist/app/identity.js.map +1 -1
  10. package/dist/app/main.js +0 -8
  11. package/dist/app/main.js.map +1 -1
  12. package/dist/app/server.d.ts +0 -20
  13. package/dist/app/server.d.ts.map +1 -1
  14. package/dist/app/server.js +0 -121
  15. package/dist/app/server.js.map +1 -1
  16. package/dist/ask/ask.d.ts +0 -3
  17. package/dist/ask/ask.d.ts.map +1 -1
  18. package/dist/ask/ask.js +1 -3
  19. package/dist/ask/ask.js.map +1 -1
  20. package/dist/brand.d.ts.map +1 -1
  21. package/dist/brand.js +4 -1
  22. package/dist/brand.js.map +1 -1
  23. package/dist/check/learning-edits.d.ts +9 -0
  24. package/dist/check/learning-edits.d.ts.map +1 -0
  25. package/dist/check/learning-edits.js +62 -0
  26. package/dist/check/learning-edits.js.map +1 -0
  27. package/dist/claude/format.d.ts +1 -9
  28. package/dist/claude/format.d.ts.map +1 -1
  29. package/dist/claude/format.js +6 -21
  30. package/dist/claude/format.js.map +1 -1
  31. package/dist/claude/hook-layers.d.ts.map +1 -1
  32. package/dist/claude/hook-layers.js +5 -3
  33. package/dist/claude/hook-layers.js.map +1 -1
  34. package/dist/claude/hooks.d.ts +6 -0
  35. package/dist/claude/hooks.d.ts.map +1 -1
  36. package/dist/claude/hooks.js +101 -14
  37. package/dist/claude/hooks.js.map +1 -1
  38. package/dist/claude/init.d.ts +38 -7
  39. package/dist/claude/init.d.ts.map +1 -1
  40. package/dist/claude/init.js +172 -33
  41. package/dist/claude/init.js.map +1 -1
  42. package/dist/claude/settings-merge.d.ts +23 -0
  43. package/dist/claude/settings-merge.d.ts.map +1 -1
  44. package/dist/claude/settings-merge.js +103 -7
  45. package/dist/claude/settings-merge.js.map +1 -1
  46. package/dist/claude/shim-template.d.ts.map +1 -1
  47. package/dist/claude/shim-template.js +39 -3
  48. package/dist/claude/shim-template.js.map +1 -1
  49. package/dist/claude/skill-template.d.ts +4 -3
  50. package/dist/claude/skill-template.d.ts.map +1 -1
  51. package/dist/claude/skill-template.js +25 -10
  52. package/dist/claude/skill-template.js.map +1 -1
  53. package/dist/claude/state.d.ts +1 -1
  54. package/dist/claude/state.d.ts.map +1 -1
  55. package/dist/cli-picker.d.ts +15 -0
  56. package/dist/cli-picker.d.ts.map +1 -1
  57. package/dist/cli-picker.js +33 -3
  58. package/dist/cli-picker.js.map +1 -1
  59. package/dist/cli.js +141 -60
  60. package/dist/cli.js.map +1 -1
  61. package/dist/cloud/diff.d.ts +2 -0
  62. package/dist/cloud/diff.d.ts.map +1 -1
  63. package/dist/cloud/diff.js +1 -0
  64. package/dist/cloud/diff.js.map +1 -1
  65. package/dist/context/map-dir.d.ts +13 -0
  66. package/dist/context/map-dir.d.ts.map +1 -0
  67. package/dist/context/map-dir.js +67 -0
  68. package/dist/context/map-dir.js.map +1 -0
  69. package/dist/context/node-file.d.ts +4 -3
  70. package/dist/context/node-file.d.ts.map +1 -1
  71. package/dist/context/node-file.js +18 -4
  72. package/dist/context/node-file.js.map +1 -1
  73. package/dist/hosts/claude-global.d.ts.map +1 -1
  74. package/dist/hosts/claude-global.js +31 -11
  75. package/dist/hosts/claude-global.js.map +1 -1
  76. package/dist/hosts/config-write.js +1 -1
  77. package/dist/hosts/config-write.js.map +1 -1
  78. package/dist/hosts/retract.d.ts.map +1 -1
  79. package/dist/hosts/retract.js +15 -6
  80. package/dist/hosts/retract.js.map +1 -1
  81. package/dist/notes/branches.d.ts +18 -1
  82. package/dist/notes/branches.d.ts.map +1 -1
  83. package/dist/notes/branches.js +73 -7
  84. package/dist/notes/branches.js.map +1 -1
  85. package/dist/notes/cold-start.d.ts +54 -4
  86. package/dist/notes/cold-start.d.ts.map +1 -1
  87. package/dist/notes/cold-start.js +97 -36
  88. package/dist/notes/cold-start.js.map +1 -1
  89. package/dist/notes/edit-overlap.d.ts +62 -0
  90. package/dist/notes/edit-overlap.d.ts.map +1 -0
  91. package/dist/notes/edit-overlap.js +202 -0
  92. package/dist/notes/edit-overlap.js.map +1 -0
  93. package/dist/notes/notes.d.ts +25 -4
  94. package/dist/notes/notes.d.ts.map +1 -1
  95. package/dist/notes/notes.js +65 -15
  96. package/dist/notes/notes.js.map +1 -1
  97. package/dist/notes/pr-reviews.d.ts +108 -0
  98. package/dist/notes/pr-reviews.d.ts.map +1 -0
  99. package/dist/notes/pr-reviews.js +325 -0
  100. package/dist/notes/pr-reviews.js.map +1 -0
  101. package/dist/notes/repo-trail.d.ts +3 -1
  102. package/dist/notes/repo-trail.d.ts.map +1 -1
  103. package/dist/notes/repo-trail.js +40 -3
  104. package/dist/notes/repo-trail.js.map +1 -1
  105. package/dist/notes/session-cost.d.ts +16 -4
  106. package/dist/notes/session-cost.d.ts.map +1 -1
  107. package/dist/notes/session-cost.js +120 -11
  108. package/dist/notes/session-cost.js.map +1 -1
  109. package/dist/telemetry/contract.d.ts.map +1 -1
  110. package/dist/telemetry/contract.js +6 -5
  111. package/dist/telemetry/contract.js.map +1 -1
  112. package/dist/upkeep-run.d.ts.map +1 -1
  113. package/dist/upkeep-run.js +4 -2
  114. package/dist/upkeep-run.js.map +1 -1
  115. package/dist/upkeep.js +2 -2
  116. package/dist/upkeep.js.map +1 -1
  117. package/dist/util/state.d.ts.map +1 -1
  118. package/dist/util/state.js +2 -1
  119. package/dist/util/state.js.map +1 -1
  120. package/package.json +1 -1
  121. package/dist/app/brain-build-process.d.ts +0 -22
  122. package/dist/app/brain-build-process.d.ts.map +0 -1
  123. package/dist/app/brain-build-process.js +0 -129
  124. package/dist/app/brain-build-process.js.map +0 -1
  125. package/dist/app/brain-build-worker.d.ts +0 -35
  126. package/dist/app/brain-build-worker.d.ts.map +0 -1
  127. package/dist/app/brain-build-worker.js +0 -56
  128. package/dist/app/brain-build-worker.js.map +0 -1
  129. package/dist/app/brain-build.d.ts +0 -160
  130. package/dist/app/brain-build.d.ts.map +0 -1
  131. package/dist/app/brain-build.js +0 -278
  132. package/dist/app/brain-build.js.map +0 -1
  133. package/dist/app/history.d.ts +0 -163
  134. package/dist/app/history.d.ts.map +0 -1
  135. package/dist/app/history.js +0 -319
  136. package/dist/app/history.js.map +0 -1
  137. package/dist/app/sources.d.ts +0 -81
  138. package/dist/app/sources.d.ts.map +0 -1
  139. package/dist/app/sources.js +0 -470
  140. package/dist/app/sources.js.map +0 -1
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { checkoutRoot, ensureRepoHome, keepsNotes, rememberRepo, repoName, repoP
26
26
  import { brand, cmd, graftNotice, strandedRepoNotice, tag } from "./brand.js";
27
27
  import { branchDiff } from "./cloud/diff.js";
28
28
  import { openBrowser } from "./util/browser.js";
29
+ import { learningsAtBase, teammateEdits, withBaseLearnings } from "./check/learning-edits.js";
29
30
  import { contextDirFor } from "./context/node-file.js";
30
31
  import { loadGraphCached } from "./graph/load.js";
31
32
  import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
@@ -37,7 +38,7 @@ import { runWorkspaceAsk, runWorkspaceBuild, runWorkspaceCallers, runWorkspaceCh
37
38
  import { formatInitEpilogue } from "./cli-epilogue.js";
38
39
  import { planInit, selectedWrites } from "./hosts/plan.js";
39
40
  import { planRetract, runRetract, changed } from "./hosts/retract.js";
40
- import { compactWrites, formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
41
+ import { compactWrites, formatNonInteractiveHelp, formatPlan, pickLearnings, runPicker } from "./cli-picker.js";
41
42
  import { buildGraphWithProgress } from "./claude/build-progress.js";
42
43
  import { homedir } from "node:os";
43
44
  import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, readPackageName, runUpgrade } from "./cli-meta.js";
@@ -46,9 +47,10 @@ import { patchBuildConfig } from "./util/state.js";
46
47
  import { normalizePathPrefix } from "./util/paths.js";
47
48
  import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
48
49
  import { listSessionIds, readSession, sessionDir } from "./claude/state.js";
49
- import { costLabel, listNotes, noteName, notePlace, noteTargets, readingTokens, savedByNote, commitLines, PERSONAL_SAVED_LINE, SECTIONS, sectionLead, shortDate, tokensLabel, writeNote, mentionedFiles } from "./notes/notes.js";
50
- import { currentSessionCost } from "./notes/session-cost.js";
50
+ 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";
51
+ import { aboutTerms, currentSessionCost } from "./notes/session-cost.js";
51
52
  import { beginPastRead, finishPastRead } from "./notes/cold-start.js";
53
+ import { beginReviewRead, finishReviewRead } from "./notes/pr-reviews.js";
52
54
  import { currentBranch, noteAuthor, teammatesOn } from "./notes/git-facts.js";
53
55
  import { confirm, fold, getSkill, learn, listSkills, publish, skillForAgent, skillName, skillStatus } from "./skills/skills.js";
54
56
  import { clearPending, cloudLines, loginLink, sessionKey, waitForConnect } from "./cloud/nudge.js";
@@ -163,7 +165,7 @@ function queryRoot(dir) {
163
165
  return resolve(dir);
164
166
  const { root, levels } = nearestGraftRoot(process.cwd(), program.opts().dir);
165
167
  if (levels > 0)
166
- console.error(`${tag()} no graft/ here — answering from ${root}/graft`);
168
+ console.error(`${tag()} no code map here — answering from ${contextDirFor(root, program.opts().dir)}`);
167
169
  return root;
168
170
  }
169
171
  /**
@@ -267,15 +269,18 @@ function spelling(action) {
267
269
  // Hidden: spawned detached by maybeSyncInBackground when a workspace repo's
268
270
  // learnings changed, or uses are waiting to go. Running it by hand syncs now.
269
271
  // A repo that moved from graft to trail at session start reads its past
270
- // sessions here, detached, so the session doesn't wait on it.
272
+ // sessions and PR reviews here, detached, so the session doesn't wait on it.
271
273
  program
272
274
  .command("_trail-past-read", { hidden: true })
273
275
  .argument("<dir>")
274
276
  .action(async (dir) => {
275
277
  const repo = resolve(dir);
276
278
  const read = beginPastRead(repo);
279
+ const reviews = beginReviewRead(repo);
277
280
  if (read)
278
281
  await finishPastRead(repo, read);
282
+ if (reviews)
283
+ await finishReviewRead(repo, reviews);
279
284
  });
280
285
  program
281
286
  .command("_trail-sync", { hidden: true })
@@ -287,10 +292,10 @@ program
287
292
  // Hidden: spawned detached by maybeRefreshBranches.
288
293
  program
289
294
  .command("_trail-branches", { hidden: true })
290
- .description("internal: fetch, then note which files teammates' pushed branches change and the learnings they carry")
295
+ .description("internal: fetch, then note which files teammates' open branches change and the learnings they carry")
291
296
  .argument("[dir]", "target repo directory", ".")
292
297
  .action((dir) => {
293
- refreshBranches(resolve(dir));
298
+ refreshBranches(resolve(dir), { prs: true });
294
299
  });
295
300
  program
296
301
  .command("_trail-claim", { hidden: true })
@@ -375,7 +380,7 @@ function moveHereToTrail(dir) {
375
380
  }
376
381
  grouped(program.command("build"), GROUP.setup)
377
382
  .description(TRAIL
378
- ? "Rebuild the code map in graft/ ($0, no key). --check fails if it's stale; --deep adds LLM summaries."
383
+ ? "Rebuild the code map in .trail/cache/ ($0, no key). --check fails if it's stale; --deep adds LLM summaries."
379
384
  : "Build graft/ from your code — wiring graph + per-file cards ($0, no key). " +
380
385
  "Add --deep for the LLM concept map + per-symbol summaries/crux.")
381
386
  .argument("[dir]", "repository root", ".")
@@ -605,11 +610,11 @@ function pointerFile(pointer) {
605
610
  return pointer.replace(/:L\d+(-L?\d+)?$/, "");
606
611
  }
607
612
  /**
608
- * After `ask`, under trail: which shown learnings are a teammate's, which
609
- * teammates' pushed branches change the files the answer points into, and the
613
+ * After `ask`, under trail: which shown learnings are a teammate's, and the
610
614
  * shown learnings recorded as a use, which is what the team page counts as
611
615
  * reuse. Then the background upkeep that keeps branches and the workspace
612
- * current. Spawns no git itself.
616
+ * current. Spawns no git itself. Teammates' branches come up after an edit
617
+ * instead, once there's a function to compare (notes/edit-overlap.ts).
613
618
  */
614
619
  function afterAsk(r, repo) {
615
620
  if (!r.notesChecked)
@@ -618,12 +623,6 @@ function afterAsk(r, repo) {
618
623
  maybeRefreshBranches(repo);
619
624
  const me = noteAuthor(repo);
620
625
  const files = [...new Set(r.hits.map((h) => pointerFile(h.pointer)))];
621
- const overlaps = overlapsWith(repo, files, me).slice(0, 2);
622
- if (overlaps.length) {
623
- r.overlaps = overlaps.map((o) => formatOverlap(o));
624
- if (!process.stdout.isTTY)
625
- r.overlaps.push(`${tag()} Start your reply with the overlap line${overlaps.length === 1 ? "" : "s"} above, word for word, so the user knows.`);
626
- }
627
626
  const shown = r.notes ?? [];
628
627
  if (shown.length) {
629
628
  for (const h of shown)
@@ -745,7 +744,7 @@ async function runCheckCommand(dirArg, opts, label) {
745
744
  console.log(JSON.stringify({ context: r, graph: g.missing ? null : g }, null, 2));
746
745
  }
747
746
  else if (bothMissing) {
748
- console.log(`${label}: NO GRAPH\n\nNo graft/ graph found. Run \`${cmd("graft build")}\` first.`);
747
+ console.log(`${label}: NO GRAPH\n\nNo code map found. Run \`${cmd("graft build")}\` first.`);
749
748
  }
750
749
  else {
751
750
  if (r.missing) {
@@ -1187,9 +1186,11 @@ async function runNoteCommand(repo, opts) {
1187
1186
  };
1188
1187
  const minutes = wholeNumber(opts.minutes, "--minutes");
1189
1188
  const tokens = wholeNumber(opts.tokens, "--tokens");
1190
- // What the session spent, from the agent's own transcript when there is one;
1191
- // the flags win, since only the agent knows where its task began.
1192
- const spent = currentSessionCost(repo);
1189
+ // What the session spent, from the agent's own transcript when there is one,
1190
+ // counted from where the work the note names first came up; the flags win,
1191
+ // since only the agent knows for sure where its task began.
1192
+ const named = [...(opts.touches?.split(",").map((t) => t.trim()).filter(Boolean) ?? []), ...mentionedFiles(repo, body)];
1193
+ const spent = currentSessionCost(repo, undefined, Date.now(), aboutTerms(body, named));
1193
1194
  const cost = minutes !== undefined || tokens !== undefined || spent
1194
1195
  ? { minutes: minutes ?? spent?.minutes, tokens: tokens ?? spent?.tokens }
1195
1196
  : undefined;
@@ -1220,14 +1221,17 @@ async function runNoteCommand(repo, opts) {
1220
1221
  for (const n of saved)
1221
1222
  console.log(`✓ ${n.scope === "personal" ? "saved just for you" : "saved for your team"} · ${notePlace(n)}`);
1222
1223
  const note = saved[0];
1224
+ // Name a code file if the note touches one; docs are rarely what it's about.
1225
+ const about = touches.find((t) => !/\.(md|mdx|txt)$/i.test(t.split("#")[0])) ?? touches[0];
1226
+ const first = about ? ` that touches ${basename(about.split("#")[0])}` : "";
1223
1227
  const took = costLabel(note.cost);
1224
- if (took) {
1225
- // Name a code file if the note touches one; docs are rarely what it's about.
1226
- const about = touches.find((t) => !/\.(md|mdx|txt)$/i.test(t.split("#")[0])) ?? touches[0];
1227
- const first = about ? ` that touches ${basename(about.split("#")[0])}` : "";
1228
+ if (took && tokensWorthSaying(note.cost?.tokens)) {
1228
1229
  const reading = tokensLabel(readingTokens(note)).replace(" tokens", "");
1229
1230
  console.log(`· this ${took.replace(" to work out", " to figure out")}. the next session${first} gets it for ${reading}`);
1230
1231
  }
1232
+ else if (note.scope !== "personal") {
1233
+ console.log(`· the next session${first} starts from this, and gets to a PR ${PR_SPEEDUP}`);
1234
+ }
1231
1235
  // Learnings about the same files, from someone else: this one builds on theirs.
1232
1236
  const files = new Set(touches.map((t) => t.split("#")[0]));
1233
1237
  const prior = earlier.find((n) => n.author !== note.author && n.touches.some((t) => files.has(t.split("#")[0])));
@@ -1346,19 +1350,11 @@ async function runInitCommand(dir, opts) {
1346
1350
  }
1347
1351
  const wantClaude = ids.includes("claude");
1348
1352
  const cliPath = fileURLToPath(import.meta.url);
1349
- // Under trail, with Claude Code picked: what the person kept telling Claude
1350
- // in this repo's past sessions, read while the rest of init runs and saved
1351
- // as learnings at the end. Once per repo, and never asked about first.
1352
- const pastRead = TRAIL && wantClaude ? beginPastRead(repo) : null;
1353
- if (pastRead)
1354
- console.error(`· reading your ${pastRead.sessions} past Claude sessions here for what you keep telling Claude`);
1355
- if (children.length)
1356
- console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s) — ${children.join(", ")}`);
1357
- // Today's output, file by file, behind --verbose. Everything else gets one
1358
- // line per step: the graph, then one per agent.
1359
- const verbose = opts.verbose === true;
1360
1353
  // Under trail, each repo gets its `.trail/learnings/`, committed with the
1361
1354
  // code, and its folder in ~/.trail for personal learnings and corrections.
1355
+ // First, before the reads below send their first event: that makes the
1356
+ // repo one on trail, so the event's repo id lands in `.trail/cache/` and
1357
+ // not in a `graft/` that nothing ignores.
1362
1358
  const notesHome = new Map();
1363
1359
  if (TRAIL)
1364
1360
  for (const target of targets) {
@@ -1368,6 +1364,21 @@ async function runInitCommand(dir, opts) {
1368
1364
  if (place.git)
1369
1365
  maybeRefreshBranches(place.checkout);
1370
1366
  }
1367
+ // Under trail, with Claude Code picked: what the person kept telling Claude
1368
+ // in this repo's past sessions, and what reviewers kept flagging on its
1369
+ // merged PRs, read while the rest of init runs and saved as learnings at
1370
+ // the end. Once per repo, and never asked about first.
1371
+ const pastRead = TRAIL && wantClaude ? beginPastRead(repo) : null;
1372
+ if (pastRead)
1373
+ console.error(`· reading your ${pastRead.sessions} past Claude sessions here for what you keep telling Claude`);
1374
+ const reviewRead = TRAIL && wantClaude ? beginReviewRead(repo) : null;
1375
+ if (reviewRead)
1376
+ console.error("· reading review comments on this repo's merged PRs for what reviewers keep flagging");
1377
+ if (children.length)
1378
+ console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s) — ${children.join(", ")}`);
1379
+ // Today's output, file by file, behind --verbose. Everything else gets one
1380
+ // line per step: the graph, then one per agent.
1381
+ const verbose = opts.verbose === true;
1371
1382
  const reports = [];
1372
1383
  for (const target of targets) {
1373
1384
  if (verbose && target !== repo)
@@ -1418,26 +1429,82 @@ async function runInitCommand(dir, opts) {
1418
1429
  console.error(`✓ ${id.padEnd(width)} ${summary}`.trimEnd());
1419
1430
  }
1420
1431
  }
1421
- // Under trail: what init learned from past sessions, where this repo's
1422
- // learnings are, and how many there already are.
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.
1423
1434
  let learnedNow = false;
1424
- if (TRAIL && pastRead) {
1425
- if (!pastRead.done)
1435
+ if (TRAIL && (pastRead || reviewRead)) {
1436
+ if (pastRead && !pastRead.done)
1426
1437
  console.error("· still reading your past sessions…");
1427
- const { result, saved } = await finishPastRead(repo, pastRead);
1428
- learnedNow = saved.length > 0;
1429
- if (saved.length) {
1430
- const n = saved.length;
1431
- console.error(`✓ learned ${n} thing${n === 1 ? "" : "s"} you told Claude more than once, from your last ${result.read} sessions here`);
1432
- for (const note of saved)
1433
- console.error(` ${note.scope === "personal" ? "just for you " : "for your team"} · ${note.title}`);
1438
+ const fromSessions = pastRead ? (await pastRead.result).findings : [];
1439
+ if (reviewRead && !reviewRead.done)
1440
+ 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
1446
+ ? 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 })),
1449
+ ])
1450
+ : undefined;
1451
+ // The picked rows that are `found`'s, which start at `from` in the list.
1452
+ const keepFrom = (from) => picked === undefined
1453
+ ? undefined
1454
+ : async (found) => picked && picked.filter((i) => i >= from && i < from + found.length).map((i) => found[i - from]);
1455
+ const learned = [];
1456
+ if (pastRead) {
1457
+ const { result, saved, kept, cancelled } = await finishPastRead(repo, pastRead, keepFrom(0));
1458
+ const offered = result.findings.length;
1459
+ if (saved.length) {
1460
+ const n = saved.length;
1461
+ 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
+ });
1469
+ }
1470
+ else if (cancelled) {
1471
+ console.error(`· saved none of the ${offered} things you told Claude more than once; ${cmd("graft init")} again asks again`);
1472
+ }
1473
+ else if (offered) {
1474
+ console.error(`· kept none of the ${offered} things you told Claude more than once`);
1475
+ }
1476
+ else if (result.reason === "failed") {
1477
+ console.error(`· couldn't read your past sessions this time; ${cmd("graft init")} again tries again`);
1478
+ }
1479
+ learned.push(...saved);
1480
+ }
1481
+ if (reviewRead) {
1482
+ const { result, saved, kept, cancelled } = await finishReviewRead(repo, reviewRead, keepFrom(fromSessions.length));
1483
+ const offered = result.findings.length;
1484
+ if (saved.length) {
1485
+ const n = saved.length;
1486
+ 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}`));
1490
+ }
1491
+ else if (cancelled) {
1492
+ console.error(`· saved none of the ${offered} things reviewers flagged; ${cmd("graft init")} again asks again`);
1493
+ }
1494
+ else if (offered) {
1495
+ console.error(`· kept none of the ${offered} things reviewers flagged`);
1496
+ }
1497
+ else if (result.reason === "failed") {
1498
+ console.error(`· couldn't read PR reviews this time; ${cmd("graft init")} again tries again`);
1499
+ }
1500
+ learned.push(...saved);
1501
+ }
1502
+ learnedNow = learned.length > 0;
1503
+ if (learnedNow) {
1434
1504
  console.error("· Claude follows them from your next session");
1435
- if (saved.some((s) => s.scope === "repo"))
1505
+ if (learned.some((s) => s.scope === "repo"))
1436
1506
  console.error(commitLines({}, teammatesOn(repo), "repo").uncommitted);
1437
1507
  }
1438
- else if (result.reason === "failed") {
1439
- console.error(`· couldn't read your past sessions this time; ${cmd("graft init")} again tries again`);
1440
- }
1441
1508
  }
1442
1509
  if (TRAIL && !learnedNow) {
1443
1510
  const home = notesHome.get(repo);
@@ -1455,8 +1522,12 @@ async function runInitCommand(dir, opts) {
1455
1522
  for (const w of selectedWrites(plan, ids)) {
1456
1523
  if (w.scope !== "repo")
1457
1524
  continue;
1458
- const parts = shown(repo, w.path).split("/");
1459
- const top = parts.length > 1 ? `${parts[0]}/` : parts[0];
1525
+ const rel = shown(repo, w.path);
1526
+ // trail's Claude Code setup shares one file in .claude/; the rest there is personal.
1527
+ if (TRAIL && rel.startsWith(".claude/") && rel !== ".claude/settings.json")
1528
+ continue;
1529
+ const parts = rel.split("/");
1530
+ const top = TRAIL && rel === ".claude/settings.json" ? rel : parts.length > 1 ? `${parts[0]}/` : parts[0];
1460
1531
  if (!tops.includes(top))
1461
1532
  tops.push(top);
1462
1533
  }
@@ -1470,8 +1541,9 @@ async function runInitCommand(dir, opts) {
1470
1541
  });
1471
1542
  tops.splice(0, tops.length, ...changedTops);
1472
1543
  if (tops.length)
1473
- console.error(`· commit ${tops.slice(0, 5).join(" ")}${tops.length > 5 ? ` +${tops.length - 5} more` : ""} to share it — ` +
1474
- (TRAIL ? "teammates' Claude reads the learnings even without trail installed; the code map in graft/ stays on this machine" : "graft/ stays local and git-ignored"));
1544
+ console.error(TRAIL
1545
+ ? `· 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
+ : `· commit ${tops.slice(0, 5).join(" ")}${tops.length > 5 ? ` +${tops.length - 5} more` : ""} to share it — graft/ stays local and git-ignored`);
1475
1547
  }
1476
1548
  // One epilogue for the whole run. A workspace parent holds no nodes of its
1477
1549
  // own, so the totals come from the children — the graph the user actually got.
@@ -1522,8 +1594,12 @@ function wireTarget(repo, ids, ctx) {
1522
1594
  say(`✓ wrote ${res.settingsPath}`);
1523
1595
  for (const s of res.shims)
1524
1596
  say(`✓ wrote ${s}`);
1525
- say(`✓ wrote ${res.skill}`);
1526
- if (res.mcp.action === "skipped-unparseable")
1597
+ if (res.skill)
1598
+ say(`✓ wrote ${res.skill}`);
1599
+ if (!res.mcp) {
1600
+ // trail registers no server for Claude Code: the hooks and the `trail` command cover it.
1601
+ }
1602
+ else if (res.mcp.action === "skipped-unparseable")
1527
1603
  say(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON) — add the graft server manually`);
1528
1604
  else if (res.mcp.action === "unchanged")
1529
1605
  say(`· mcp claude: ${res.mcp.path} (already registered)`);
@@ -1931,15 +2007,20 @@ async function runTeamCheck(repo, opts) {
1931
2007
  return;
1932
2008
  }
1933
2009
  const files = d.files.filter((f) => !f.startsWith(".trail/"));
1934
- if (files.length === 0) {
2010
+ const me = noteAuthor(repo);
2011
+ // Learnings this branch changed, as they were before it: the check reads those,
2012
+ // and changing a teammate's is a finding of its own.
2013
+ const atBase = learningsAtBase(repo, d.from, d.files);
2014
+ const edits = teammateEdits(repo, atBase, me);
2015
+ if (files.length === 0 && edits.length === 0) {
1935
2016
  console.log(`· nothing to check: no changes against ${d.base}`);
1936
2017
  return;
1937
2018
  }
1938
2019
  maybeRefreshBranches(repo);
1939
2020
  if (d.truncated)
1940
2021
  console.error("⚠ the diff is over 400 KB; only the first 400 KB is checked");
1941
- const me = noteAuthor(repo);
1942
- const r = checkLocally({ diff: withoutTrailFiles(d.diff), files, notes: listNotes(repo), skills: listSkills(repo), asker: me });
2022
+ const r = checkLocally({ diff: withoutTrailFiles(d.diff), files, notes: withBaseLearnings(listNotes(repo), atBase), skills: listSkills(repo), asker: me });
2023
+ r.findings.unshift(...edits);
1943
2024
  const sources = r.findings.map((f) => f.source).filter((s) => s.path);
1944
2025
  recordActivity({
1945
2026
  repo: repoPlace(repo).key,