From f7e226c47d267b149e6fe075002eb91d51a3a3e2 Mon Sep 17 00:00:00 2001 From: Shrish Dwivedi Date: Thu, 15 Oct 2026 19:59:07 +0530 Subject: [PATCH 1/2] fix(init): make .trail/learnings before the first telemetry event The past-session read sends cold_start before init wired anything, so the repo didn't count as on trail yet and the event's repo id went to graft/.cache/. Since #619 nothing ignores graft/, so every fresh init with telemetry on left an untracked graft/ folder in git status. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 24 ++++++++++++++---------- test/trail-init-layout.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index eeb47230..8b7051e6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1512,6 +1512,20 @@ async function runInitCommand(dir: string, opts: InitOptions): Promise<{ ids: st const wantClaude = ids.includes("claude"); const cliPath = fileURLToPath(import.meta.url); + // Under trail, each repo gets its `.trail/learnings/`, committed with the + // code, and its folder in ~/.trail for personal learnings and corrections. + // First, before the reads below send their first event: that makes the + // repo one on trail, so the event's repo id lands in `.trail/cache/` and + // not in a `graft/` that nothing ignores. + const notesHome = new Map(); + if (TRAIL) + for (const target of targets) { + const { place } = ensureRepoHome(target); + const created = place.git ? ensureRepoTrail(place.checkout) : false; + notesHome.set(target, { created, dir: place.git ? place.checkout : place.dir }); + if (place.git) maybeRefreshBranches(place.checkout); + } + // Under trail, with Claude Code picked: what the person kept telling Claude // in this repo's past sessions, and what reviewers kept flagging on its // merged PRs, read while the rest of init runs and saved as learnings at @@ -1527,16 +1541,6 @@ async function runInitCommand(dir: string, opts: InitOptions): Promise<{ ids: st // Today's output, file by file, behind --verbose. Everything else gets one // line per step: the graph, then one per agent. const verbose = opts.verbose === true; - // Under trail, each repo gets its `.trail/learnings/`, committed with the - // code, and its folder in ~/.trail for personal learnings and corrections. - const notesHome = new Map(); - if (TRAIL) - for (const target of targets) { - const { place } = ensureRepoHome(target); - const created = place.git ? ensureRepoTrail(place.checkout) : false; - notesHome.set(target, { created, dir: place.git ? place.checkout : place.dir }); - if (place.git) maybeRefreshBranches(place.checkout); - } const reports: WireReport[] = []; for (const target of targets) { if (verbose && target !== repo) console.error(`\n— ${relative(repo, target)}/`); diff --git a/test/trail-init-layout.test.ts b/test/trail-init-layout.test.ts index 8af1d439..f4dc06b6 100644 --- a/test/trail-init-layout.test.ts +++ b/test/trail-init-layout.test.ts @@ -17,6 +17,7 @@ import { setupUncommitted } from "../src/claude/hooks.js"; import { setupCommitLine } from "../src/notes/notes.js"; import { ensureRepoTrail, REPO_TRAIL_README } from "../src/notes/repo-trail.js"; import { hooksShim } from "../src/claude/shim-template.js"; +import { CI_ENV_VARS } from "../src/telemetry/gate.js"; const git = (cwd: string, ...args: string[]) => spawnSync("git", ["-c", "commit.gpgsign=false", "-c", "user.name=T", "-c", "user.email=t@t.test", ...args], { cwd, encoding: "utf8" }); @@ -84,6 +85,27 @@ test("trail init leaves git status with only the shared files, and writes the sk assert.equal(readFileSync(join(d, ".trail", "hook.cjs"), "utf8"), hooksShim()); }); +test("trail init with telemetry on leaves no graft/ folder behind", () => { + // init's first event comes from the past-session read, before anything else + // is written: the repo id it mints must land in .trail/cache/, which + // .trail/.gitignore keeps out of git, not in a graft/ that nothing ignores. + const d = gitRepo("init-telemetry"); + const home = tmpRepo("init-telemetry-home"); + const env: NodeJS.ProcessEnv = { + ...process.env, + HOME: home, + USERPROFILE: home, + TRAIL_HOME: join(home, ".trail"), + GRAFT_POSTHOG_KEY: "phc_test_key", + GRAFT_POSTHOG_HOST: "https://127.0.0.1:9", + }; + for (const k of [...CI_ENV_VARS, "DO_NOT_TRACK", "TRAIL_NO_FETCH", "CLAUDECODE", "TRAIL_INVOKED_AS"]) delete env[k]; + const r = spawnSync(process.execPath, ["--import", "tsx", join(process.cwd(), "src", "bin", "trail.ts"), "init", d, "--agents", "claude", "--no-build", "--no-global"], { encoding: "utf8", env }); + assert.equal(r.status, 0, r.stderr); + assert.equal(existsSync(join(d, "graft")), false, "no graft/ in a repo on trail"); + assert.ok(existsSync(join(d, ".trail", "cache", ".cache", "telemetry-repo-id.json")), "the event was queued, with its repo id under .trail/cache/"); +}); + test("init over a repo set up the old way moves it: old shims, repo skill and trail's server go", () => { const d = gitRepo("old-layout"); const home = tmpRepo("old-layout-home"); From ede67d4a1754e7c87c474970a8f40a680e3667c6 Mon Sep 17 00:00:00 2001 From: Shrish Dwivedi Date: Thu, 15 Oct 2026 19:59:07 +0530 Subject: [PATCH 2/2] chore(release): 0.23.0 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 23 +++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0992cd29..e1136f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.23.0 + +### Changed + +- **One shared folder** (#619): `trail init` now shares `.trail/hook.cjs` and + one SessionStart line in `.claude/settings.json`. Everything else is + personal: `.claude/settings.local.json`, and the skill in `~/.claude`. Repos + set up the old way move over by themselves at the next session; commit + `.trail/` and `.claude/settings.json` once after that. +- **The code map lives in `.trail/cache/`** in a repo on trail, kept out of git + by `.trail/.gitignore`. An existing `graft/` map is moved there, not rebuilt. +- **No MCP server for Claude Code.** init stops adding it and removes the one + an older init added. Cursor keeps its server. +- **Overlap shows up after an edit, per function** (#617): a teammate's open + branch is mentioned only when you both change the same function, and merged + or closed branches no longer count. +- **A learning's cost counts from where its work began** (#620), not from the + start of a long session. + +### Fixed + +- A fresh `trail init` no longer leaves an untracked `graft/` folder behind. + ## 0.22.1 ### Added diff --git a/package-lock.json b/package-lock.json index 769a4fde..53f20592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nanonets/graft", - "version": "0.22.1", + "version": "0.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nanonets/graft", - "version": "0.22.1", + "version": "0.23.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1c23cefd..bbd74d8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nanonets/graft", - "version": "0.22.1", + "version": "0.23.0", "description": "Build a repo's context graph as a folder of linked markdown files: a local, regenerable cache that every query keeps in sync with your code.", "keywords": [ "ai",