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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.22.0

graft is now **Trail-code**. The same build ships as two packages:
`@trailhq/trail-code` (the `trail` command) and `@nanonets/graft` (the `graft`
command, unchanged). Both keep getting every release.

### Added

- **`npm install -g @trailhq/trail-code`** gives you the `trail` command: the
code map plus learnings your team shares in `.trail/`.
- **`graft upgrade` adds `trail` alongside graft**, and the next Claude Code
session in a graft-wired repo moves it to trail by itself.
- **The agent passes the news on**: on a machine with graft and no trail,
Claude Code is told once a week to mention that `graft upgrade` adds trail.

### Fixed

- **Each Claude Code hook answers once**, not from both the repo's and
`~/.claude`'s copies, so learnings no longer arrive twice per prompt.
- **Hook shims find `@trailhq/trail-code`**, and `trail` warns in a repo whose
older graft hooks can't reach it.
- **No crash on npm 12**, which skips the grammars' install scripts: grammars
load per language and fall back to WASM.
- The package points at `github.com/trailhq/Trail-Code`.

## 0.19.0

### Added
Expand Down
2 changes: 1 addition & 1 deletion CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Language support

graft's multi-language support was shaped by community contributors who opened
The graft map's multi-language support was shaped by community contributors who opened
language pull requests. That wave of one-language-at-a-time PRs is exactly what
motivated the generic breadth tier (one tree-sitter grammar + `tags.scm` per
language) that now covers most of them. With gratitude to:
Expand Down
866 changes: 535 additions & 331 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

## Supported Versions

Graft is pre-1.0 and ships continuously from `main`. Security fixes are made against
the latest published npm release (`@nanonets/graft`); older versions are not
separately patched.
Trail-code (formerly graft) is pre-1.0 and ships continuously from `main`. Security
fixes are made against the latest published npm release (`@trailhq/trail`, also
published as `@nanonets/graft`); older versions are not separately patched.

## Reporting a Vulnerability

Please do not open a public GitHub issue for security vulnerabilities.

Report vulnerabilities privately via
[GitHub Security Advisories](https://github.com/NanoNets/Graft/security/advisories/new),
[GitHub Security Advisories](https://github.com/trailhq/Graft/security/advisories/new),
or email shrish@nanonets.com.

We'll acknowledge your report within 3 business days and aim to ship a fix or
Expand Down
52 changes: 28 additions & 24 deletions TELEMETRY.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Telemetry

graft collects a small set of **anonymous** usage events so we can tell whether
Trail-code collects a small set of **anonymous** usage events so we can tell whether
the thing works: how many repos get past a build, whether an agent reaches for
graft or falls back to grep, which commands earn their place, and what breaks in
Trail-code or falls back to grep, which commands earn their place, and what breaks in
the wild.

This document is the complete, authoritative contract: **if an event or property
is not listed here, graft does not send it.** The implementation lives in
is not listed here, Trail-code does not send it.** The implementation lives in
[`src/telemetry/contract.ts`](src/telemetry/contract.ts) and enforces this list
as a hard allowlist — unknown events and unknown properties are dropped before
anything is written, let alone sent. The code and this file are kept in
lockstep, and because the repo is open source you can verify that yourself.

This page covers anonymous usage stats only. A Trail workspace you choose to
connect is separate, and sends more: see
[What stays on your machine](README.md#what-stays-on-your-machine).

Nothing is ever sent from a command you run. Events are appended to a local file
and a detached background process posts them at most once a day, so no `graft
and a detached background process posts them at most once a day, so no `trail
ask` ever waits on the network.

The one exception is the `install` event below, which the npm postinstall hook
Expand All @@ -28,12 +32,12 @@ Every event carries only these common properties:

| Property | Example | Notes |
| --- | --- | --- |
| `app_version` | `0.12.0` | The graft version that produced the event |
| `app_version` | `0.12.0` | The Trail-code version that produced the event |
| `os` | `darwin` / `win32` / `linux` | Platform, nothing more |
| `arch` | `arm64` / `x64` | CPU architecture |
| `node_major` | `20` | Major version only |
| `ci` | `false` | Always false — CI never sends (see below) |
| `agent_host` | `claude-code` / `cursor` / `mcp` / `cli` | Which surface graft ran under |
| `agent_host` | `claude-code` / `cursor` / `mcp` / `cli` | Which surface Trail-code ran under |
| `cli_name` | `trail` / `graft` | Which of the two names started the process (graft is trail's old name) |
| `repo_id` | a random UUID | See "How it stays anonymous" |

Expand All @@ -42,20 +46,20 @@ The events:
| Event | Extra properties | When |
| --- | --- | --- |
| `install` | `global` (`true` for `npm i -g`, `false` for a project dependency) | The npm postinstall hook runs — once per machine per version |
| `first_run` | — | Once, the first time a graft command runs on a machine |
| `init_completed` | `agents` (the ids you selected, sorted), `consent` | `graft init` finishes |
| `first_run` | — | Once, the first time a `trail` or `graft` command runs on a machine |
| `init_completed` | `agents` (the ids you selected, sorted), `consent` | `trail init` finishes |
| `build_completed` | `files_bucket`, `langs`, `mode` (`fast`/`deep`), `duration_bucket`, `incremental` | A build succeeds |
| `build_failed` | `stage`, `code` — both fixed enums | A build throws |
| `query` | `command`, `surface` (`cli`/`mcp`/`hook`), `hit` (`ask` only), `notes` (`0`/`1`/`2`, how many session notes `ask` showed; only in a repo that keeps notes) | Any query command |
| `query` | `command`, `surface` (`cli`/`mcp`/`hook`), `hit` (`ask` only), `notes` (`0`/`1`/`2`, how many learnings `ask` showed; only in a repo that keeps learnings) | Any query command |
| `brain_signup_opened` | `mode` (`terminal`/`agent`/`nudge`), and for a nudge `command` (`ask`/`note`/`check`) and `wording` (`share`/`learned`) | A trail command hands the agent a sign-in link to pass on (at most once a session) |
| `brain_signup_settled` | `outcome` (`linked`/`timed_out`/`bad_callback`/`stopped`, and for an agent-run push `agent_link_opened`/`still_waiting`/`expired`/`unsupported`), `mode` (`terminal`/`agent`/`nudge`), `duration_bucket` | That handoff ends, one way or the other — for an agent-run push, each of its runs; for a nudge, each background wait for the sign-in (`linked`/`timed_out`/`expired`/`unsupported`) |
| `note_saved` | `has_cost` (`true`/`false`), `touches_bucket` (how many files the learning is about, bucketed), `scope` (`repo`/`personal`) | `trail note` saves a learning, in the repo's `.trail/` or on this machine. The title, the text and the paths are never sent |
| `learning_reused` | `teammate` (`true`/`false`), `saved_tokens_bucket` | `trail ask` shows learnings from earlier sessions. Which ones, and their text, are never sent |
| `repo_seen` | `trail_committed` (`true`/`false`), `learnings_bucket`, `workspace` (`true`/`false`) | Once a day per repo, when a Claude Code session starts in a repo trail runs in. The repo's name and paths are never sent; it's identified only by the random `repo_id` every event carries |
| `learnings_committed` | `added_bucket`, `total_bucket` (learnings committed on `HEAD`, bucketed), `first_look` (`true`/`false`) | A Claude Code session starts in a repo trail runs in and more learnings are committed there than at the last session. Their names and text are never sent |
| `cold_start` | `outcome` (`found`/`none_found`/`no_sessions`/`no_claude`/`failed`), `sessions_bucket`, `offered_bucket`, `picked_bucket` (how many it saved), `duration_bucket`, `model` | `trail init` reads your past Claude Code sessions in the repo, once, for things you told Claude more than once, and saves them as learnings. The reading is done by your own Claude Code; your messages and what it found are never sent to trail |
| `takeaway_saved` | `new_skill` (`true`/`false`) | `trail learn` saves a correction as a takeaway on a skill, on this machine (`~/.trail/`). Its text is never sent |
| `skills_folded` | `takeaways_bucket` (how many takeaways were folded, bucketed) | `trail skills fold` writes takeaways into a skill's SKILL.md |
| `takeaway_saved` | `new_skill` (`true`/`false`) | `trail learn` adds a correction to a skill, on this machine (`~/.trail/`). Its text is never sent |
| `skills_folded` | `takeaways_bucket` (how many corrections were folded, bucketed) | `trail skills fold` writes waiting corrections into a skill's SKILL.md |
| `session_summary` | `graft_reads_bucket`, `source_reads_bucket`, `saved_tokens_bucket`, `graft_turns_bucket`, `reported_turns_bucket` | Once, after an agent session ends |

Two rules govern every value above, and both are enforced in code rather than by
Expand Down Expand Up @@ -86,35 +90,35 @@ An example event, in full:
}
```

Run `graft telemetry debug` to print the exact batch your machine would send. It
Run `trail telemetry debug` to print the exact batch your machine would send. It
sends nothing.

### One thing graft reads locally
### One thing Trail-code reads locally

Two of those properties — `graft_turns_bucket` and `reported_turns_bucket` — need
to know something the others don't: whether the reply you actually read said what
graft saved. graft computes a saving on every retrieval call, but a turn that
Trail-code saved. Trail-code computes a saving on every retrieval call, but a turn that
saves 20,000 tokens in silence and a turn that saves nothing look identical in
`saved_tokens_bucket`, and that difference is the whole question.

The agent's own prose lives in one place a hook can reach: the transcript file
your editor writes, named on the Stop hook's stdin. So at the end of a turn that
used graft, graft reads the tail of that file, checks the reply for a "graft saved
used Trail-code, it reads the tail of that file, checks the reply for a "saved
~N tokens" line, and increments one of two counters. Nothing out of the file is
stored, and nothing out of it is sent — not the reply, not a fragment of it, not
its length. Two counts of turns cross the wire, as buckets, once per session.

A turn graft cannot check — an editor whose Stop hook names no transcript, an
A turn Trail-code cannot check — an editor whose Stop hook names no transcript, an
unreadable file — is counted in neither total, so the ratio always means "of the
turns we could read".

## What is never sent

No source code. No file paths, repo names, organisation names, git remotes, or
branch names. No symbol names, node summaries, or anything out of the graph. No
query strings, prompts, or agent output — graft reads the last reply of a
graft-using turn locally to decide whether it mentioned the saving (see "One
thing graft reads locally"); only the resulting count is sent, never the text. No error messages or stack traces — a
query strings, prompts, or agent output — Trail-code reads the last reply of a
turn that used it locally to decide whether it mentioned the saving (see "One
thing Trail-code reads locally"); only the resulting count is sent, never the text. No error messages or stack traces — a
failure contributes a code from a fixed list and nothing else. No environment
variables, API keys, model names, hostnames, usernames, or email addresses.

Expand All @@ -136,12 +140,12 @@ variables, API keys, model names, hostnames, usernames, or email addresses.

Any one of these fully disables telemetry:

1. **Uncheck "anonymous usage stats" in the `graft init` picker.** Takes effect
1. **Uncheck "anonymous usage stats" in the `trail init` picker.** Takes effect
immediately and is remembered for this machine.
2. **`graft telemetry disable`** at any time. `graft telemetry status` shows the
current state, and `graft telemetry enable` turns it back on.
2. **`trail telemetry disable`** at any time. `trail telemetry status` shows the
current state, and `trail telemetry enable` turns it back on.
3. **Set [`DO_NOT_TRACK`](https://web.archive.org/web/20251005054121/https://consoledonottrack.com/)** to any value other
than `0`. Respected unconditionally — it outranks graft's own setting.
than `0`. Respected unconditionally — it outranks Trail-code's own setting.
4. **Run in CI.** `CI`, `GITHUB_ACTIONS`, `GITLAB_CI` and friends switch it off
without being asked. A build server is not a user.
5. **Build from source.** The PostHog key is stamped in only at publish time
Expand All @@ -152,7 +156,7 @@ Any one of these fully disables telemetry:
## Where the events go

Official builds send to `https://events.nanonets.com`, Nanonets' own PostHog
front door, so graft's numbers sit in the same project as the rest of the
front door, so Trail-code's numbers sit in the same project as the rest of the
product rather than in a separate one. The client posts a single batch to
`/batch/`.

Expand Down
Binary file removed assets/graft-cold-vs-graft-dark.png
Binary file not shown.
Binary file removed assets/graft-cold-vs-graft.png
Binary file not shown.
Binary file removed assets/graft-comparison-demo.gif
Binary file not shown.
Binary file removed assets/graft-hero.png
Binary file not shown.
Binary file removed assets/graft-hooks-demo.gif
Binary file not shown.
Binary file removed assets/graft-site-act-demo.gif
Binary file not shown.
Binary file removed assets/graft-terminal.png
Binary file not shown.
Binary file added assets/trail-code-how-it-works-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/trail-code-how-it-works.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/github-app.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# The graft GitHub App
# The Trail-code GitHub App

Posts a blast-radius comment on every pull request, and hosts the interactive
graph behind a signed link.
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.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nanonets/graft",
"version": "0.21.1",
"version": "0.22.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 All @@ -14,11 +14,11 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/trailhq/Graft.git"
"url": "git+https://github.com/trailhq/Trail-Code.git"
},
"homepage": "https://github.com/trailhq/Graft#readme",
"homepage": "https://github.com/trailhq/Trail-Code#readme",
"bugs": {
"url": "https://github.com/trailhq/Graft/issues"
"url": "https://github.com/trailhq/Trail-Code/issues"
},
"publishConfig": {
"access": "public"
Expand Down
6 changes: 3 additions & 3 deletions scripts/pack-trail.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* Lays out the `@trailhq/trail` package from this repo's build, in build/trail/.
* Lays out the `@trailhq/trail-code` package from this repo's build, in build/trail/.
*
* trail and graft are one program under two names, published as two packages
* from the same `dist/`:
*
* @trailhq/trail bin: trail → dist/bin/trail.js
* @trailhq/trail-code bin: trail → dist/bin/trail.js
* @nanonets/graft bin: graft → dist/bin/graft.js (this repo's package.json)
*
* Each package ships exactly one command, so installing both never clashes on
Expand Down Expand Up @@ -32,7 +32,7 @@ if (!existsSync(join(root, 'dist', 'bin', 'trail.js'))) {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
const trail = {
...pkg,
name: '@trailhq/trail',
name: '@trailhq/trail-code',
description:
"Your team's shared memory for coding agents: a code map on your machine, and the learnings, decisions and skills your whole team shares. Formerly graft.",
keywords: [...new Set(['trail', ...(pkg.keywords ?? [])])],
Expand Down
4 changes: 2 additions & 2 deletions scripts/postinstall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { fileURLToPath, pathToFileURL } from 'node:url';

const root = dirname(dirname(fileURLToPath(import.meta.url)));

// The same code ships as @trailhq/trail (the `trail` command) and as
// The same code ships as @trailhq/trail-code (the `trail` command) and as
// @nanonets/graft (`graft`, its old name). The install event and the nudge say
// which one this is.
let pkgName = '@nanonets/graft';
try {
pkgName = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name ?? pkgName;
} catch { /* the graft defaults */ }
const isTrail = pkgName === '@trailhq/trail';
const isTrail = pkgName === '@trailhq/trail-code';
if (isTrail) process.env.TRAIL_INVOKED_AS = 'trail';

/**
Expand Down
4 changes: 2 additions & 2 deletions src/app/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,10 @@ export function skillFiles(root: string, max = MAX_SKILL_FILES): string[] {
return out;
}

/** Remove graft's own fenced regions, so the brain never learns graft from graft. */
/** Remove graft's own fenced regions (trail's too), so the brain never learns graft from graft. */
function stripManagedBlocks(text: string): string {
return text
.replace(/<!--\s*graft:(brain:)?start\s*-->[\s\S]*?<!--\s*graft:(brain:)?end\s*-->/g, "")
.replace(/<!--\s*(graft|trail):(brain:)?start\s*-->[\s\S]*?<!--\s*\1:(brain:)?end\s*-->/g, "")
.trim();
}

Expand Down
3 changes: 2 additions & 1 deletion src/blast/blast-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { GraphV1 } from "../graph/types.js";
import { blastRadiusIn, type BlastReport } from "./blast.js";
import { changedFiles, refExists } from "./diff.js";
import { markdownReport, mermaidDiagram, textReport } from "./render.js";
import { cmd } from "../brand.js";

export type BlastFormat = "text" | "markdown" | "mermaid" | "json";

Expand Down Expand Up @@ -68,7 +69,7 @@ export async function runBlastCommand(dir: string, opts: BlastCliOptions): Promi

const graph = loadGraphCached(contextDir);
if (!graph) {
console.error(`✗ no graph found at ${contextDir} — run \`graft build\` first`);
console.error(`✗ no graph found at ${contextDir} — run \`${cmd("graft build")}\` first`);
process.exit(1);
}

Expand Down
5 changes: 3 additions & 2 deletions src/blast/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { BlastReport, ChangedArea, ImpactedModule, Impacted, TestSignal } f
import type { Evidence } from "../viz/assemble.js";
import { fileReader, impactedEvidence, reachTerms } from "./evidence.js";
import { MAX_REVIEWERS, mention, sinceLabel, type Owner } from "./owners.js";
import { brand, cmd } from "../brand.js";

/** Diagram cap. Everything past it folds into one aggregate circle carrying the
* dropped counts, so the picture shrinks but never lies. */
Expand Down Expand Up @@ -116,7 +117,7 @@ export function markdownReport(r: BlastReport, opts: { root?: string } = {}): st
const out: string[] = [];
const symbols = r.modules.reduce((n, m) => n + m.symbols.length, 0);

out.push("### 🌱 graft blast radius");
out.push(`### 🌱 ${brand()} blast radius`);
out.push("");
out.push(headline(r, symbols));
const testLine = testHeadline(r);
Expand Down Expand Up @@ -155,7 +156,7 @@ export function markdownReport(r: BlastReport, opts: { root?: string } = {}): st
for (const line of caveats) out.push(line);
}
out.push("");
out.push(`<sub>\`graft blast\` · ${r.basis} · ${depthLabel(r.depth)} · ${plural(r.changed.length, "changed file")}</sub>`);
out.push(`<sub>\`${cmd("graft blast")}\` · ${r.basis} · ${depthLabel(r.depth)} · ${plural(r.changed.length, "changed file")}</sub>`);
return out.join("\n") + "\n";
}

Expand Down
Loading
Loading