Skip to content

Get overlap warnings ready to go live - #738

Open
anirudhkumar-nanonets wants to merge 7 commits into
mainfrom
claude/overlap-warnings-go-live-b60dde
Open

anirudhkumar-nanonets wants to merge 7 commits into
mainfrom
claude/overlap-warnings-go-live-b60dde

Conversation

@anirudhkumar-nanonets

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

Copy link
Copy Markdown
Collaborator

The issue: in Claude Desktop worktree sessions, overlap warnings never fired, and learnings attached to a file never reached Claude when it opened or edited that file. Where overlap warnings did fire, they warned about branches that were already merged: without gh, 2 in 3 warnings on this repo were false alarms. And a doc comment Claude added above a function a teammate is changing gave no warning.
Why: in a worktree session the hooks take the main checkout as the project (CLAUDE_PROJECT_DIR), so an edited file came out as .claude/worktrees/<name>/src/... and matched no branch and no learning's touches. Separately, Trail-Code squash-merges PRs and leaves the branch on GitHub, so git still calls a merged branch open. And Claude's Edit repeats a line or two it doesn't change to anchor the edit, like the } closing the code above; the check counted those as edited and took the smallest symbol they touched.
The fix: both hooks now name the file from the checkout it's in. Overlap warnings skip a function whose added lines were already added on main (git log -S), and they also cover the person's own other open branches, never the one they're on. Three events count what reaches Claude: overlap_warned, overlap_acted and learnings_on_touch.

Moves:

  • Share of overlap warnings that were real = overlap_warned with pr=open ÷ overlap_warned with pr≠unknown (split by whose). Acted = overlap_acted ÷ overlap_warned.
  • Learnings that reach Claude on file touch in worktree sessions = learnings_on_touch with worktree=true.
  • All three are in PostHog (Metabase db 11, posthog_event_full_view). Today: 0. None of these events existed (I queried the last 3 days for overlap% and got 0 rows).
  • Offline on this repo, the real share of overlap warnings went from 48% to 84% with gh and from 32% to 93% without it, and none of the 86 real warnings were dropped.

What changed

  • src/notes/home.ts: otherCheckout(project, file) returns the checkout file is in when it's another checkout of the same repo (a linked worktree), comparing the shared git dirs by real path. A file in the project's own checkout, outside git, or in another repo (a submodule included) gives null.
  • src/claude/hooks.ts:
    • editOverlap compares from that checkout, reading the branch list the session start wrote for the project. It uses payloadSessionId like the prompt hook.
    • learningsOnTouch names the file, and reads learnings, from that checkout, so a learning on the worktree's branch counts.
  • src/notes/edit-overlap.ts:
    • landedSince drops a function when each of the 3 longest lines (12+ characters) the branch added to it was also added on main since the fork. It runs only when both changes land in the same function, at about 7 ms per check.
    • An edit counts only the lines it changed, less the lines new_string keeps from old_string at either end (anchors). A comment block directly above a function is part of it (withComment), since Go doc comments and JSDoc sit outside the span the parser gives the function.
    • Own branches count, except the one the edited checkout is on. The result carries mine, and the directive says "Your other open branch … also changes …".
  • src/notes/branches.ts: the same gh pr list call also keeps which branches have an open PR (open), for pr: open | none | unknown.
  • src/claude/overlap-telemetry.ts (new):
    • overlap_warned fires per warning shown, with pr and whose (teammate or mine).
    • overlap_acted fires once per warning, when a later prompt in that session names the teammate or the branch.
  • learnings_on_touch fires in learningsOnTouch with via (read, edit or command) and worktree (true or false).
  • TELEMETRY.md and contract.ts: the three events, all with closed-set values. No names, branches or paths travel.

How I found and measured it

  • Worktree: on 30 Oct I had Claude edit withLock() in a Desktop worktree session with trail 0.26.0 installed. Shrish's open Wait instead of crashing when Windows refuses a lock file that's being deleted #682 changes that function. The hook ran and returned the blast radius but no overlap line. Its session and stats files were written under the main checkout. The new worktree tests for both hooks fail without the fix and pass with it.
  • Real share: I simulated an edit to every function each cached branch changes, through functionOverlaps, as a teammate with no branches of their own, and counted a warning as real when its branch has an open PR (gh pr list):
before after
with gh 86 real of 179 (48%) 86 real of 102 (84%)
without gh 84 real of 266 (32%) 84 real of 90 (93%)
  • Own branches: these add 100 possible branch-and-function pairs across 13 branches here. 84 are on open PRs and 16 are on 2 branches with no PR.
  • Cost: per edit to a shared function, 62 ms median and 170 ms p90, up from 38 and 155 ms.

End to end: two people, two isolated machines, real Claude Code

Each person has their own HOME, .gitconfig, npm prefix with @trailhq/trail-code packed from this branch, clone, and trail init -y. Their hooks are wired into their own ~/.claude/settings.json. Each also has their own telemetry listener on localhost (GRAFT_POSTHOG_HOST). They share only a bare git remote standing in for GitHub; the macOS keychain folder is linked in so claude can sign in.

Overlap warnings. Kumar pushed kumar/backoff, which changes backoff(). Priya pushed priya/retries, which changes retries(), then added a worktree.

  • Priya ran claude -p from her main clone, editing the worktree's copy, which is the desktop-app setup. Her reply ended with:

    🔀 trail · Kumar is also changing backoff() on kumar/backoff: it keeps the ×100 but caps the result at 5 seconds… That edits the same line as yours, so merging both will conflict.
    🔀 trail · Your branch priya/retries also changes retries(), making it return 6… you'll need to pick one when the two branches meet.

  • Kumar ran claude -p in his clone, editing retries():

    🔀 trail · Priya is also changing retries() on her branch priya/retries, raising it from 4 to 6… It's worth agreeing on the number with Priya first.

  • In both sessions Claude ran the git diff origin/main...origin/<branch> the warning names before answering.
  • Events received by each listener at /batch/: Priya sent overlap_warned with whose=teammate and with whose=mine, then overlap_acted with whose=teammate after a follow-up prompt named Kumar. Kumar sent overlap_warned with whose=teammate. pr was unknown (no gh), and no event carried names, branches or paths.

Learnings on file touch. In Priya's setup, Claude reads her worktree's copy of src/retry.ts, which a learning on the worktree's branch lists.

  • Before (trail built without the learningsOnTouch fix): the learning did not reach Claude.
  • After: Claude got the learning and ended its reply with "📝 The 5 s cap comes from Kumar's learning: the API gateway drops a request after 6 s…". learnings_on_touch was queued with via=read and worktree=true.

Live, in Claude Desktop

On 30 Oct I installed this branch's build globally and ran three Desktop worktree sessions on another repo of ours (Go), each asked to add a one-line comment to one function:

  • Inside a function a teammate's open branch changes: Claude got the warning, ran the git diff it names, and ended with a correct "🔀 trail ·" line about a likely one-line merge conflict. The session recorded it as pr=open, whose=teammate.
  • Inside a function no open branch changes: no warning.
  • Above a function a teammate's open branch changes: Claude wrote a Go doc comment above func and anchored the Edit on the } closing the struct above. No warning: the edit's lines ran from the struct's last line to the func line, and the struct was the smaller symbol. With the anchor and doc-comment fix, replaying that exact Edit warns about the right branch and function, and the other two come out the same.

Choices I made

  • I used git log -S on the branch's added lines rather than comparing the function's text with main's. Comparing text dropped only 119 of 182 merged-branch warnings, because main often changes a function again after the merge.
  • One same-repo check (otherCheckout) serves both hooks. Without it, learningsOnTouch would read another repo's learnings for a file Claude opened there.
  • "Acted" means the person named the teammate or branch. It's a proxy: it's cheap, needs no UI, and doesn't count Claude reading the diff, which the directive tells it to do every time.

Follow-ups (not in this PR)

  1. Treat a branch with no open PR that hasn't been pushed for 7 days as not open work, using the open list. That removes the 16 false alarms left with gh.
  2. Without gh, merged branches fill the 30-branch cap and push out real ones (7 here). Run the added-lines check when the list is built.
  3. Skip bot authors, add the PR number to the directive, and add an off switch.
  4. Flag learnings that name code that's gone #731 passes dir to notesOnTouch for its stale check. Once both are in, that call should pass the file's checkout. They merge without a conflict.

Checks

  • npm test with a scratch HOME: 4,132 pass and 3 fail. All three (graft-to-trail, hosts-trail, upkeep-hooks) also fail on unchanged main. In an earlier run, personAtTerminal reads the real process table… in skills-store-held.test.ts failed once under load; it reads the live process table, passes alone, and this PR doesn't touch it.
  • The two new tests for anchor lines and doc comments fail without the fix and pass with it.
  • trail check: 3 conflicts, none of them real. TELEMETRY.md:71 and hooks.ts:113 are word matches ("Claude … whose", "trail … never … loads") against the Oct 27 learning on Claude's real HOME. hooks.ts:195 matches the Oct 22 learning on filtering inside listNotes; this only changes which checkout listNotes reads, and adds no filter.

This takes in what was #741, which I closed.

🤖 Generated with Claude Code

… follow-ups

trail check flags TELEMETRY.md:71 against the Oct 27 learning on Claude's real HOME: a word match on "Claude ... trail ... never" in the new telemetry row, not the same change.
@trailhq-graft

trailhq-graft Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 9 areas can be affected. 44 dependent symbols, depth 2.
Tests: 2 areas updated their tests.
Tag: @shhdwi — 12 of 12 areas · @afeddersen — Telemetry Tracking

flowchart TB
  A0(("Cloud Onboarding<br/>13 symbols"))
  A1(("Claude Session Metrics<br/>10 symbols"))
  A2(("CLI Team Commands<br/>9 symbols"))
  A3(("Notes Storage<br/>4 symbols"))
  A4(("Setup Initialization<br/>3 symbols"))
  AX(("4 smaller areas<br/>5 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
Cloud Onboarding 13 src/cloud/nudge.ts:L1-L505 nudge.ts — imports, depth 1 Branch Overlap Detection, Telemetry Contract
Claude Session Metrics 10 src/claude/session-metrics.ts:L1-L215 session-metrics.ts — imports, depth 1 Telemetry Contract
CLI Team Commands 9 src/cli.ts:L2550-L2568 branchOverlapTexts — calls, depth 1 Branch Overlap Detection, Telemetry Contract
Notes Storage 4 src/notes/memory-notes.ts:L1-L280 memory-notes.ts — imports, depth 1 Branch Overlap Detection, Telemetry Contract
Setup Initialization 3 src/init/keep.ts:L1-L387 keep.ts — imports, depth 1 Telemetry Contract
Telemetry Tracking 2 src/telemetry/sessions.ts:L1-L113 sessions.ts — imports, depth 1 Telemetry Contract
3 smaller areas 3 CLI Skills, Learning Sessions, MCP Tools see below
Who knows this code — 2 people across 12 areas
Area Who knows it
Telemetry Contract · changed @shhdwi — 1 commit, last 7d ago
Branch Overlap Detection · changed @shhdwi — 2 commits, last 7d ago
Claude Overlap Telemetry · changed @shhdwi — 16 commits, last 3d ago
Cloud Onboarding · affected @shhdwi — 28 commits, last 3d ago
Claude Session Metrics · affected @shhdwi — 23 commits, last 3d ago
CLI Team Commands · affected @shhdwi — 51 commits, last 3d ago
Notes Storage · affected @shhdwi — 6 commits, last 3d ago
Setup Initialization · affected @shhdwi — 10 commits, last 3d ago
…4 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 44 dependent symbols, grouped by area

Cloud Onboarding — 13 symbols in 6 files

  • src/cloud/nudge.ts:L1-L505 — nudge.ts (imports, depth 1)
    39: /** The same, as the telemetry names them. */
  • src/cloud/onboarding.ts:L674-L715 — teammatesChecked (calls, depth 1)
    681: const me = (git(repo, ["config", "user.email"]) ?? "").trim().toLowerCase();
  • src/cloud/workspace-sync.ts:L235-L277 — workspaceLearnings (calls, depth 1)
    255: for (const b of readBranchCache(checkout)?.branches ?? []) for (const l of b.learnings) add(l.name, l.text, b.ref);
  • src/cloud/workspace-sync.ts:L355-L386 — syncRepo (calls, depth 1)
    362: if (opts.fetch !== false) refreshBranches(place.checkout);
  • src/cloud/lookup-activity.ts:L1-L60 — lookup-activity.ts (imports, depth 2)
    8: import { noteAuthor } from "../notes/git-facts.js";
  • src/cloud/onboarding.ts:L662-L667 — teammatesFromGit (calls, depth 2)
  • src/cloud/other-repos.ts:L102-L135 — setUpPicked (calls, depth 2)
  • src/cloud/pair.ts:L308-L367 — waitAndUpload (calls, depth 2)
    360: const res = place.git ? await syncRepo(place.checkout, readCloud() ?? cloud, { fetch: false, fetchImpl: opts.fetchImpl }) : null;
  • src/cloud/pair.ts:L1-L368 — pair.ts (imports, depth 2)
    18: import { noteAuthor } from "../notes/git-facts.js";
  • src/cloud/workspace-sync.ts:L299-L301 — learningsPayload (calls, depth 2)
  • src/cloud/workspace-sync.ts:L294-L296 — reviewPayload (calls, depth 2)
  • src/cloud/workspace-sync.ts:L661-L668 — runSyncChild (calls, depth 2)
  • src/cloud/workspace-sync.ts:L459-L473 — syncWorkspaceRepos (calls, depth 2)

Claude Session Metrics — 10 symbols in 10 files

  • src/claude/session-metrics.ts:L1-L215 — session-metrics.ts (imports, depth 1)
    8: * {@link SessionState}; before this module nothing ever incremented the first
  • src/claude/skills-wiring.ts:L1-L116 — skills-wiring.ts (imports, depth 1)
    33: function send(event: HookMsEvent, started: number, sent: Sent, env: NodeJS.ProcessEnv): void {
  • src/claude/format.ts:L1-L416 — format.ts (imports, depth 2)
    2: import type { Stats, SessionState } from './state.js';
  • src/claude/hook-io.ts:L1-L140 — hook-io.ts (imports, depth 2)
    63: * sent SIGTERM and let go: it releases its lock and exits on its own, and is
  • src/claude/prompt-skills.ts:L1-L327 — prompt-skills.ts (imports, depth 2)
    4: * skill a prompt brings back. `handlePrompt` calls it right after the
  • src/claude/skill-use.ts:L1-L80 — skill-use.ts (imports, depth 2)
    16: import { noteAuthor } from "../notes/git-facts.js";
  • src/claude/stats.ts:L1-L27 — stats.ts (imports, depth 2)
    4: import type { Stats } from './state.js';
  • src/claude/statusline.ts:L1-L39 — statusline.ts (imports, depth 2)
    3: import { readStats, readSession, emptyStats, type Stats } from './state.js';
  • src/claude/stop.ts:L1-L216 — stop.ts (imports, depth 2)
    21: import { acquireLock, patchStats, readSession, readStats, writeSession, type SessionState } from './state.js';
  • src/claude/sync-run.ts:L1-L48 — sync-run.ts (imports, depth 2)
    4: import { patchStats, releaseLock, resolveContextDir } from './state.js';

CLI Team Commands — 9 symbols in 1 file

  • src/cli.ts:L2550-L2568 — branchOverlapTexts (calls, depth 1)
    2551: const branches = readBranchCache(repo)?.branches ?? [];
  • src/cli.ts:L1-L2844 — cli.ts (calls, depth 1)
    5: * graft/ and a clone has the graph. A workspace parent (≥2 git children)
  • src/cli.ts:L2575-L2603 — localTeam (calls, depth 2)
  • src/cli.ts:L2497-L2547 — teamCommand (calls, depth 2)
    2508: const cloud = (here.git ? cloudForRepo(here.key) : null) ?? readCloud();
  • src/cli.ts:L2470-L2473 — leftOutLines (calls, depth 2)
  • src/cli.ts:L773-L787 — afterAsk (calls, depth 2)
  • src/cli.ts:L1476-L1909 — runInitCommand (calls, depth 2)
    1568: // unchecked the row must not have this run's init_completed sent.
  • src/cli.ts:L2629-L2678 — runTeamCheck (calls, depth 2)
    2630: // From the top of the checkout: git names changed files from there, and a learning's touches have to
  • src/cli.ts:L2358-L2463 — workspaceCommand (calls, depth 2)
    2387: const mine = here.git ? cloudIn(seen.map((x) => x.cloud), here.key) : null;

Notes Storage — 4 symbols in 3 files

  • src/notes/memory-notes.ts:L1-L280 — memory-notes.ts (imports, depth 1)
    10: * names code that exists now and the repo doesn't already say it; the rest,
  • src/notes/notes.ts:L217-L233 — branchNotes (calls, depth 1)
    218: const cache = readBranchCache(checkout);
  • src/notes/pr-reviews.ts:L1-L577 — pr-reviews.ts (imports, depth 1)
    19: * in what was sent. The learnings are about the repo, so they're the team's,
  • src/notes/notes.ts:L195-L214 — listNotes (calls, depth 2)
    209: if (place.git) add(readNotesIn(learningsDir(place.checkout), place.checkout, here, "repo"));

Setup Initialization — 3 symbols in 3 files

  • src/init/keep.ts:L1-L387 — keep.ts (imports, depth 1)
    189: const names: KeptName[] = [];
  • src/init/run.ts:L1-L445 — run.ts (imports, depth 1)
    10: * never a `claude` process, even a stand-in; the relay with plain names
  • src/init/review-screen.ts:L1-L316 — review-screen.ts (imports, depth 2)
    33: /** How many skills the naming call was sent (its screen line). */

Telemetry Tracking — 2 symbols in 2 files

  • src/telemetry/sessions.ts:L1-L113 — sessions.ts (imports, depth 1)
    26: import { readSession, writeSession, sessionDir, listSessionIds } from '../claude/state.js';
  • src/telemetry/track.ts:L1-L162 — track.ts (imports, depth 1)
    29: /** One queued event, exactly as it will be sent. */

CLI Skills — 1 symbol in 1 file

  • src/skills/cli-skills.ts:L1-L376 — cli-skills.ts (imports, depth 2)
    201: const root = runCheckout(sheet, x.env, place.git && place.key === sheet.repoKey ? place.checkout : '');

Learning Sessions — 1 symbol in 1 file

  • src/learn/sessions.ts:L1-L310 — sessions.ts (imports, depth 1)
    10: * (what they were answering), a reply that names a decision, and the

MCP Tools — 1 symbol in 1 file

  • src/mcp/tools.ts:L1-L337 — tools.ts (imports, depth 2)
    30: import { canonicalToolName } from './tool-names.js';
Test signal per changed area — 2 ✓ · 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.

  • – Telemetry Contract — 1 test file changed here: test/telemetry-contract.test.ts
  • ✓ Branch Overlap Detection — 8 of 15 reached · 3 test files changed here: test/trail-edit-overlap.test.ts, test/trail-learning-on-touch.test.ts, test/trail-overlap-telemetry.test.ts
    • not reached: pullRequests, withComment, symbolsIn, addedLines, addedTo, landedSince, anchors
  • ✓ Claude Overlap Telemetry — 3 of 6 reached · 1 test file changed here: test/trail-overlap-telemetry.test.ts
    • not reached: editOverlap, learningsOnTouch, names
61 test suites also reference this code

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

  • test/claude-copy-dashes.test.ts
  • test/claude-format.test.ts
  • test/claude-hook-timeouts.test.ts
  • test/claude-hooks-1b.test.ts
  • test/claude-hooks-perf.test.ts
  • test/claude-hooks-skills.test.ts
  • test/claude-hooks.test.ts
  • test/claude-host-standdown.test.ts
  • test/claude-post-bash-walls.test.ts
  • test/claude-prompt-learnings.test.ts
  • test/claude-prompt-private.test.ts
  • test/claude-session-start-learnings.test.ts
  • test/claude-skills-journey-fixtures.ts
  • test/claude-start-skills-1b.test.ts
  • test/claude-state.test.ts
  • test/claude-statusline.test.ts
  • test/claude-stop-trace.test.ts
  • test/claude-stop.test.ts
  • test/claude-tally.test.ts
  • test/hosts-codex-hooks.test.ts
  • …41 more

⚠️ 6 changed files not in the graph (.trail/learnings/2026-10-29-overlap-warnings-catch-a-squash-merged-anirudhkumar.md, .trail/learnings/2026-10-30-hooks-in-a-desktop-worktree-session-anirudhkumar-2.md, .trail/learnings/2026-10-30-hooks-in-a-desktop-worktree-session-anirudhkumar.md, .trail/learnings/2026-10-30-overlap-checks-count-only-the-lines-anirudhkumar.md, .trail/learnings/2026-10-30-two-person-end-to-end-tests-anirudhkumar.md) — no parser claims the extension, or the index predates the file.

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

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

github-actions Bot added a commit that referenced this pull request Oct 29, 2026
trail check flags TELEMETRY.md:71 and hooks.ts:113 against the Oct 27 learning on Claude's real HOME: word matches ("claude ... trail", "trail ... never ... loads"), not the same change.
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
…e one way

trail check flags TELEMETRY.md and hooks.ts lines against the Oct 27 learning on Claude's real HOME (word matches on "Claude ... copy ... project"), and the listNotes line against the Oct 22 learning on filtering inside listNotes: this only changes which checkout listNotes reads, no filter is added.
github-actions Bot added a commit that referenced this pull request Oct 30, 2026
github-actions Bot added a commit that referenced this pull request Oct 30, 2026

This branch has not been deployed

No deployments
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