Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
24 changes: 14 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { created: boolean; dir: string }>();
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
Expand All @@ -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<string, { created: boolean; dir: string }>();
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)}/`);
Expand Down
22 changes: 22 additions & 0 deletions test/trail-init-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down Expand Up @@ -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");
Expand Down
Loading