Skip to content

feat: trail, with learnings in .trail/, a local check, and workspaces - #558

Closed
anirudhkumar-nanonets wants to merge 35 commits into
mainfrom
feat/trail-cloud
Closed

anirudhkumar-nanonets wants to merge 35 commits into
mainfrom
feat/trail-cloud

Conversation

@anirudhkumar-nanonets

@anirudhkumar-nanonets anirudhkumar-nanonets commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Moves: connect. In the last 7 days to 2026-10-07, 4 people opened a brain signup from graft and 3 settled it (PostHog DB 11), and 37 external coding brains were built (Agents DB 69). Both use the SQL in daily/project.md. Today's values weren't pulled because the Metabase connector didn't connect in this session.

This PR also adds measurement:

  • learning_reused, which tells whether learnings save work.
  • repo_seen, which counts repos with .trail/ committed.
  • note_saved now carries scope (repo or personal).

All three are 0 today because they're new.

Everything trail needs, in one PR. The team app is trailhq/trail-cloud#1; it replaces NanoNets/assign#3159.

trail is the command

  • @trailhq/trail ships trail, laid out by scripts/pack-trail.mjs into build/trail. @nanonets/graft ships graft. It's the same code, and graft's output doesn't change.
  • The freshness check is trail build --check, because trail check is the team check.
  • trail mcp serves trail_* tools and still answers graft_*. cli_name is on every event.

Learnings, committed with the code

  • trail note --title "…" saves a learning in the repo's .trail/learnings/, under ## Decided, ## Tried and ruled out and ## Watch out. It records the minutes and tokens the session took, from the Claude Code transcript. You commit it with the change.
    • --personal keeps one in ~/.trail instead, for something about your own setup.
    • trail init creates .trail/ and says to commit it.
  • A learning touches the files the session changed and the files it names. .trail/ itself never counts as one of those files.
  • ask puts matching learnings above the code. It marks a teammate's as theirs, and prints [trail] learnings saved ≈ N tokens: what the learning took to work out, less the cost of reading it. Claude closes its reply with one line of 7–8 words, like 📝 Started from Anirudh's learning · saved ~27k tokens, or 📝 Saved a learning for your team's next session when it saved one.
  • Teammates' pushed branches:
    • At session start, and every few minutes, trail fetches and reads which files each recent branch changes and which learnings it adds.
    • Learnings on an unmerged branch reach the next session before the branch merges. Only their first lines are shown, marked unreviewed.
    • When a branch changes the code your prompt is about, Claude starts its reply with ● overlap · Kumar's branch kumar/retry also changes client.go (pushed today 15:32).
    • This needs no server.
  • After a turn of real digging with no learning yet, the Stop hook asks once a session for one. Claude is told never to edit an existing learning: when it overrides one, it says why in the commit and saves a new one.
  • The install nudge: the committed trail skill's one-line description says where the learnings are and how to install trail. A teammate's Claude reads .trail/learnings/ even before they install anything, and tells them once.

trail check, on this machine

trail check holds the branch's diff against the repo's learnings (branches included), your own and the repo's skills. It's the deterministic half of the server check, ported to TypeScript, with its tests. Exit 1 on a conflict. It needs no sign-in.

While porting, I found that the Go rule read "Keep defaultRetryMax = 4: ten retries can…" as a decision for 10. The port takes the number from before the reason. The Go version in assign#3159 still has this bug.

Workspaces, through trail-cloud

  • Nobody types trail login. Once there's a learning, ask, note and check hand Claude a link to end its reply with, at most once a session, worded differently each time.
  • A background child registers the link with trail-cloud first, along with every repo Trail knows on this machine (~/.trail/repos/known.json), so the picker lists them. It then waits for the person to:
    1. sign in;
    2. create or pick a workspace;
    3. tick repos.
  • Once connected, it saves the connection in ~/.trail/cloud.json (owner-only, one per machine) and uploads every learning in the picked repos' .trail/. The next command says so once.
  • Uses are recorded in ~/.trail/outbox.jsonl and sent with the next sync, so reuse from before someone connected still counts on the team page.
  • A repo with learnings that isn't in the workspace gets one question, ever: add it? trail workspace add does it.
  • New: trail workspace shows the repos, team page and invite link, and trail logout disconnects. trail team shows the workspace's week; when you're not connected, it shows what this clone can see.
  • push and pull (the brain's rules) still run, unlisted.

Tested

  • npm test: 1,463 tests, 0 failing, 1 skipped.
  • Headless Claude Code sessions, two people in two clones of hashicorp/go-retryablehttp sharing a stand-in remote:
    1. Anirudh: saved the learning before committing, pushed it with the trail setup, and ended with the 📝 line and the link. 45 s, $0.29.
    2. Kumar: after git pull, Kumar's Claude answered from Anirudh's learning. 11 s, $0.10, with the reuse recorded.
    3. Kumar: changed the default anyway. trail check gave ✗ client.go:51 · changes defaultRetryMax from 4 to 10, quoting Anirudh. Claude explained the override in the commit.
    4. Anirudh: asked about the same code. Claude's reply opened with the overlap line for Kumar's pushed branch.
    5. Both: connected through trail-cloud. The team page showed 2 people, 3 sessions and reuse ×3.

Not published: @trailhq/trail needs the trailhq npm org, and trail-cloud needs a deploy.

@trailhq-graft

trailhq-graft Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

5 areas changed → 13 areas can be affected. 83 dependent symbols, depth 2.
Tests: 4 areas updated their tests.
Tag: @shhdwi — 11 of 18 areas · @qoole — Graph Building, Context Building · @Frankie-Xu — Upkeep Maintenance, Context Building

flowchart TB
  A0(("Brain Sync<br/>25 symbols"))
  A1(("Graph Building<br/>17 symbols"))
  A2(("Blast Analysis<br/>8 symbols"))
  A3(("Build And Review<br/>8 symbols"))
  A4(("Telemetry<br/>6 symbols"))
  AX(("8 smaller areas<br/>19 symbols"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
Brain Sync 25 src/brain/autopush.ts:L1-L270 autopush.ts — imports, depth 1 CLI Core, Claude Integration
Graph Building 17 src/graph/build.ts:L1-L411 build.ts — imports, depth 1 CLI Core, Notes Storage
Blast Analysis 8 src/blast/blast-cli.ts:L1-L193 blast-cli.ts — imports, depth 1 CLI Core
Build And Review 8 src/app/brain-build.ts:L1-L428 brain-build.ts — imports, depth 1 CLI Core
Telemetry 6 src/telemetry/identity.ts:L68-L81 installId — calls, depth 1 CLI Core
MCP Tools 4 src/mcp/tools.ts:L247-L329 callSingleTool — calls, depth 1 CLI Core, Notes Storage
7 smaller areas 15 Visualization Export, Claude Integration, Grep Search, … see below
Who knows this code — 6 people across 18 areas
Area Who knows it
CLI Core · changed @shhdwi — 41 commits, last 2mo ago
Claude Integration · changed @shhdwi — 24 commits, last 2mo ago
Host Configuration · changed @shhdwi — 11 commits, last 2mo ago
Build Scripts · changed @shhdwi — 1 commit, last 3mo ago
Graph Building · affected @shhdwi — 20 commits, last 2mo ago · @qoole — 5 commits, last 2mo ago
Telemetry · affected @afeddersen — 1 commit, last 1mo ago · Marcelo Caporale — 1 commit, last 1mo ago
MCP Tools · affected @shhdwi — 14 commits, last 2mo ago
Visualization Export · affected @shhdwi — 1 commit, last 3mo ago
…10 further areas

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 83 dependent symbols, grouped by area

Brain Sync — 25 symbols in 12 files

  • src/brain/autopush.ts:L1-L270 — autopush.ts (imports, depth 1)
    3: * push`, at most once a day, started by the session-start hook.
  • src/brain/autopush.ts:L80-L82 — patchTrailPushState (calls, depth 1)
    80: function patchTrailPushState(repo: string, patch: TrailPushState): void {
  • src/brain/link.ts:L1-L232 — link.ts (imports, depth 1)
    2: * The link between a repo's graft graph and a Trail brain.
  • src/brain/link.ts:L115-L117 — writeRulesCache (calls, depth 1)
    116: writeJsonAtomic(rulesCachePath(dir), cache, true);
  • src/brain/pull.ts:L1-L332 — pull.ts (imports, depth 1)
    2: * `graft trail pull`: one command for both things a trail hands this repo.
  • src/brain/push.ts:L287-L293 — pickedAgents (calls, depth 1)
    289: return [...new Set([...(readStamp(root)?.hosts ?? []), ...wiredHostIds(root)])];
  • src/brain/watch-trail.ts:L1-L395 — watch-trail.ts (imports, depth 1)
    2: * `graft trail watch`: wait until Trail has something for this repo, then say so
  • src/brain/attach.ts:L1-L92 — attach.ts (imports, depth 2)
    2: * Attach a brain's rules to an `ask` answer.
  • src/brain/autopush.ts:L238-L263 — maybeAutopush (calls, depth 2)
    238: export function maybeAutopush(repo: string, deps: AutopushDeps = {}): AutopushDecision {
  • src/brain/autopush.ts:L105-L112 — recordSeenSuggestions (calls, depth 2)
    105: export function recordSeenSuggestions(repo: string, count: number): void {
  • src/brain/autopush.ts:L90-L97 — recordTrailPush (calls, depth 2)
    90: export function recordTrailPush(repo: string, head: string, now = Date.now()): void {
  • src/brain/claude-md.ts:L1-L215 — claude-md.ts (imports, depth 2)
    3: * old name): write the CLAUDE.md changes accepted in Trail into this checkout's
  • src/brain/connect.ts:L1-L111 — connect.ts (imports, depth 2)
    25: * a secret must not appear in a command line
  • src/brain/connect.ts:L67-L86 — connectBrain (calls, depth 2)
    68: repo: string,
  • src/brain/context-files.ts:L1-L282 — context-files.ts (imports, depth 2)
    5: * AGENTS.md, a folder's own CLAUDE.md, Cursor rules, Claude skills — and a
  • src/brain/link.ts:L133-L137 — markRulesChecked (calls, depth 2)
  • src/brain/pull.ts:L225-L331 — runTrailPull (calls, depth 2)
    225: export async function runTrailPull(repo: string, link: BrainLink, opts: PullOptions): Promise<number> {
  • src/brain/push.ts:L1-L395 — push.ts (imports, depth 2)
    4: * The server path needs graft's GitHub App installed on the repo, which is an
  • src/brain/push.ts:L248-L279 — buildEarlyDigest (calls, depth 2)
    260: threads = await readThreads(ctx.owner, ctx.name, ctx.token, ctx.fetchImpl as never, undefined, EARLY_THREADS, undefined, ctx.threads);
  • src/brain/push.ts:L313-L378 — buildLocalDigest (calls, depth 2)
    320: return { error: "this directory has no GitHub `origin` remote — graft can only push a GitHub repository today" };
  • src/brain/upload.ts:L1-L255 — upload.ts (imports, depth 2)
    2: * Getting a digest to Trail quickly, and getting it there at all.
  • src/brain/watch-trail.ts:L101-L124 — readTrailSnapshot (calls, depth 2)
    102: repo: string,
  • src/brain/watch-trail.ts:L209-L218 — result (calls, depth 2)
    209: const result = (reason: WatchExitReason, extra: Partial<WatchTrailResult> = {}): WatchTrailResult => ({
  • src/brain/watch.ts:L1-L529 — watch.ts (imports, depth 2)
    4: * `graft trail push` used to end at "it is being mined into rules now — a few
  • src/brain/wire.ts:L1-L148 — wire.ts (imports, depth 2)
    2: * Writing a brain's rules into the instruction file each coding agent already

Graph Building — 17 symbols in 10 files

  • src/graph/build.ts:L1-L411 — build.ts (imports, depth 1)
    2: * `graph` — build `.context/graph.json` from a code repository.
  • src/graph/extract-cache.ts:L1-L224 — extract-cache.ts (imports, depth 1)
    2: * Per-file memo for Tier-1 extraction — `<outDir>/.cache/extract.json`.
  • src/graph/extract-cache.ts:L213-L223 — writeExtractCache (calls, depth 1)
    217: writeJsonAtomic(path, cache, true);
  • src/graph/fingerprint.ts:L1-L197 — fingerprint.ts (imports, depth 1)
    2: * The cheap "has the working tree moved?" probe that gates the pre-query rebuild.
  • src/graph/fingerprint.ts:L87-L103 — writeFingerprint (calls, depth 1)
    97: writeJsonAtomic(fingerprintPath(outDir), record, true);
  • src/graph/root.ts:L1-L46 — root.ts (imports, depth 1)
    3: * shell — started in a subdirectory of an indexed repo should still find the
  • src/graph/seed.ts:L1-L229 — seed.ts (imports, depth 1)
    2: * `seedGraph` — give a linked git worktree the graph its parent checkout already has.
  • src/graph/traverse-cli.ts:L1-L243 — traverse-cli.ts (imports, depth 1)
    4: * One command, one implementation: resolve a symbol via `resolveSymbol`, walk
  • src/graph/workspace-cli.ts:L88-L97 — runWorkspaceAsk (calls, depth 1)
    92: opts: { limit?: number; source?: boolean; full?: boolean; in?: string; json?: boolean },
  • src/graph/workspace-cli.ts:L1-L145 — workspace-cli.ts (imports, depth 1)
    12: import { formatAsk } from "../ask/ask.js";
  • src/graph/workspace.ts:L1-L725 — workspace.ts (imports, depth 1)
    3: * more immediate git-repo children and has no source graph of its own. Instead
  • src/graph/build.ts:L151-L410 — buildGraph (calls, depth 2)
    159: // including the repo's persisted directory and submodule choices.
  • src/graph/load.ts:L1-L116 — load.ts (imports, depth 2)
    2: * mtime-keyed in-process cache over the two readers `ask()` calls on every
  • src/graph/map.ts:L325-L350 — formatRepoMap (calls, depth 2)
    327: const header = `repo map — ${totals.files} files · ${totals.symbols} symbols · ${totals.edges} edges · ${totals.languages.join(", ")}`;
  • src/graph/traverse-cli.ts:L166-L242 — runCallersCommand (calls, depth 2)
    171: console.error(`✗ no graph found at ${contextDir} — run \`graft build\` first`);
  • src/graph/workspace.ts:L240-L558 — federateAsk (calls, depth 2)
    245: ): AskResult {
  • src/graph/workspace.ts:L659-L695 — federateCallers (calls, depth 2)
    670: const blocks: string[] = [];

Blast Analysis — 8 symbols in 7 files

  • src/blast/blast-cli.ts:L1-L193 — blast-cli.ts (imports, depth 1)
    4: * Kept out of cli.ts (argument wiring only) so the diff → seeds → walk → render
  • src/blast/modules.ts:L1-L99 — modules.ts (imports, depth 1)
    2: * Module labels for the blast diagram — the "what part of the system is this?"
  • src/blast/name.ts:L1-L303 — name.ts (imports, depth 1)
    5: * The point is scope. Getting labels from `build --deep` means summarising every
  • src/blast/blast-cli.ts:L168-L192 — exportRadius (calls, depth 2)
    184: repoName: repoLabel(root),
  • src/blast/blast.ts:L1-L542 — blast.ts (imports, depth 2)
    2: * Blast radius of a diff: what else in the repo depends on the lines this change
  • src/blast/evidence.ts:L1-L193 — evidence.ts (imports, depth 2)
    4: * `blast` knows which lines changed (git's hunks) and which symbols reach them
  • src/blast/render.ts:L1-L452 — render.ts (imports, depth 2)
    8: * GitHub renders Mermaid natively in comments, so there is nothing to host.
  • src/blast/viz.ts:L1-L223 — viz.ts (imports, depth 2)
    6: * can never answer the reviewer's next question: which symbols, at which lines,

Build And Review — 8 symbols in 7 files

  • src/app/brain-build.ts:L1-L428 — brain-build.ts (imports, depth 1)
    5: * repository URL, and a minute later they are looking at rules mined from their
  • src/app/review.ts:L1-L174 — review.ts (imports, depth 1)
    5: * the caller uses to publish it. Everything runs in-process against the same
  • src/app/brain-build-process.ts:L1-L152 — brain-build-process.ts (imports, depth 2)
    2: * Running a repository read somewhere it cannot stop the App answering.
  • src/app/brain-build-worker.ts:L1-L83 — brain-build-worker.ts (imports, depth 2)
    5: * does. It receives the credential it needs rather than the App's private key,
  • src/app/review-process.ts:L1-L269 — review-process.ts (imports, depth 2)
    2: * Running a review somewhere it cannot stop the App answering.
  • src/app/review-worker.ts:L1-L110 — review-worker.ts (imports, depth 2)
    5: * `reviewPullRequest` the server used to call directly, and reports back. Nothing
  • src/app/review.ts:L102-L133 — exportPage (calls, depth 2)
    120: repoName: job.repo,
  • src/app/server.ts:L1-L292 — server.ts (imports, depth 2)
    4: * Deliberately `node:http` and nothing else. This process handles a private key,

Telemetry — 6 symbols in 4 files

  • src/telemetry/identity.ts:L68-L81 — installId (calls, depth 1)
    68: export function installId(home?: string): Install {
  • src/telemetry/identity.ts:L55-L59 — patchState (calls, depth 1)
    55: export function patchState(patch: Partial<TelemetryState>, home?: string): TelemetryState {

…22 further symbols not listed.

Test signal per changed area — 4 ✓ · 1 –

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ✓ CLI Core — 52 of 210 reached · 10 test files changed here: test/brain-signup.test.ts, test/front-matter.test.ts, test/telemetry-contract.test.ts, test/trail-check-diff.test.ts, test/trail-check-local.test.ts, test/trail-cloud.test.ts, test/trail-name.test.ts, test/trail-notes.test.ts, test/trail-nudge.test.ts, test/trail-skills.test.ts
    • not reached: inBrand, repoWiredForTrail, setBrand, trailOnPath, readPackageName, runUpgrade, grouped, queryRoot, …150 more
  • ✓ Claude Integration — 15 of 37 reached · 9 test files changed here: test/claude-init.test.ts, test/claude-settings-merge.test.ts, test/claude-shim-resolve.test.ts, test/claude-shim-template.test.ts, test/claude-statusline.test.ts, test/trail-name.test.ts, test/trail-note-ask.test.ts, test/trail-notes.test.ts, test/trail-prompt-notes.test.ts
    • not reached: retrievalBody, b, teamNotesDirective, hookTimeoutIn, askForNote, trackRepoSeen, trailAtStart, main, …14 more
  • ✓ Notes Storage — 26 of 87 reached · 7 test files changed here: test/front-matter.test.ts, test/trail-cloud.test.ts, test/trail-home.test.ts, test/trail-note-ask.test.ts, test/trail-note-mentions.test.ts, test/trail-notes.test.ts, test/trail-nudge.test.ts
    • not reached: attachNotes, pointerFile, branchesStale, cachePath, commonGitDir, defaultRef, firstName, git, …53 more
  • ✓ Host Configuration — 7 of 13 reached · 1 test file changed here: test/claude-shim-template.test.ts
    • not reached: mergeJsonKey, removeJsonKey, isGraftCmd, stripClaudeSettings, stripCodexHooks, targets
  • – Build Scripts — no function, method or class changed here
84 test suites also reference this code

90 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/app-review-process.test.ts
  • test/ask-fusion.test.ts
  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/blast-name.test.ts
  • test/blast-viz.test.ts
  • test/brain-context-files-pull.test.ts
  • test/brain-early-upload.test.ts
  • test/brain-push-agents.test.ts
  • test/brain-trail-watch.test.ts
  • test/brain-upload.test.ts
  • test/brain-watch.test.ts
  • test/claude-format.test.ts
  • test/claude-hooks.test.ts
  • test/claude-md-pull.test.ts
  • test/claude-skill-template.test.ts
  • test/claude-state.test.ts
  • test/claude-tally.test.ts
  • test/cli-meta.test.ts
  • test/cli-picker.test.ts
  • …64 more

⚠️ 6 changed files not in the graph (.claude/helpers/graft-hooks.cjs, .claude/helpers/graft-statusline.cjs, .gitignore, README.md, TELEMETRY.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 82 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

@anirudhkumar-nanonets anirudhkumar-nanonets changed the title feat(cloud): trail login shares notes, adds team and check feat: trail, with session notes, skills from corrections, and team sharing Oct 8, 2026
@anirudhkumar-nanonets
anirudhkumar-nanonets changed the base branch from feat/trail-skills to main October 8, 2026 12:04
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
github-actions Bot added a commit that referenced this pull request Oct 8, 2026
@anirudhkumar-nanonets anirudhkumar-nanonets changed the title feat: trail, with session notes, skills from corrections, and team sharing feat: trail, with learnings in .trail/, a local check, and workspaces Oct 9, 2026
github-actions Bot added a commit that referenced this pull request Oct 9, 2026
github-actions Bot added a commit that referenced this pull request Oct 9, 2026
github-actions Bot added a commit that referenced this pull request Oct 9, 2026
github-actions Bot added a commit that referenced this pull request Oct 13, 2026
shhdwi added a commit that referenced this pull request Oct 13, 2026
Swaps the graft hero for the Trail-code banner from the README rewrite,
so the renamed repo shows the new name before PR #558 merges. The rest
of the README is unchanged.

Co-authored-by: Shrish Dwivedi <shrish@nanonets.com>
@anirudhkumar-nanonets

Copy link
Copy Markdown
Collaborator Author

Merged into main as part of #585, which was stacked on this branch.

github-actions Bot added a commit that referenced this pull request Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant