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", 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");