Skip to content

Show file learnings in worktree sessions - #741

Closed
anirudhkumar-nanonets wants to merge 1 commit into
mainfrom
claude/touch-learnings-worktree
Closed

anirudhkumar-nanonets wants to merge 1 commit into
mainfrom
claude/touch-learnings-worktree

Conversation

@anirudhkumar-nanonets

Copy link
Copy Markdown
Collaborator

The issue: in a Claude Desktop worktree session, learnings never reached Claude when it opened or edited a file they list.
Why: the hooks take the main checkout as the project (CLAUDE_PROJECT_DIR), so learningsOnTouch named the worktree's file .claude/worktrees/<name>/src/..., which matches no learning's touches. It also read learnings from the main checkout instead of the worktree's branch.
The fix: when the file is in another checkout of the same repo, learningsOnTouch names the file and reads learnings from that checkout. A new learnings_on_touch event counts each learning shown this way, and whether it came through a worktree.

Moves: learnings that reach Claude on file touch in worktree sessions = learnings_on_touch with worktree=true in PostHog (Metabase db 11, posthog_event_full_view). Today: 0. No event existed. In a worktree session the true value was also 0, because of this bug.

What changed

  • src/notes/home.ts: otherCheckout(checkout, 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, so those cases behave as before.
  • src/claude/hooks.ts learningsOnTouch: names the file, and reads the learnings, from that checkout. Session state still lives with the project.
  • learnings_on_touch event (via: read, edit or command; worktree: true or false) in contract.ts and TELEMETRY.md.

Tests

  • New: in a worktree session, a Read of the worktree's copy brings the main checkout's committed learning, and an Edit brings a learning that exists only on the worktree's branch. A file in an unrelated repo or in the main checkout gets nothing new. The test fails on main and passes with the fix.
  • New: the event is queued with worktree=true for the worktree's copy and false for the main checkout's.
  • npm test with a scratch HOME: 4,134 pass and 3 fail (graft-to-trail, hosts-trail, upkeep-hooks). The same 3 fail on unchanged main.
  • trail check: 3 conflicts, none of them real. TELEMETRY.md:71 and hooks.ts:175 are word matches ("Claude … copy … project") against the Oct 27 learning on Claude's real HOME. hooks.ts:187 matches the Oct 22 learning on filtering inside listNotes; this only changes which checkout listNotes reads, and adds no filter.

End to end, on an isolated machine

This uses the scratch "Priya" machine from the #738 test: its own HOME, git identity, clone and trail init, with trail installed from a packed build. Claude runs from her main clone and reads her worktree's copy of src/retry.ts, the way the desktop app works. A learning on the worktree's branch lists that file.

  • Before (trail built from Get overlap warnings ready to go live #738, without this fix): the learning did not reach Claude, and its reply said nothing about it.
  • After (trail built from this branch): the learning reached Claude, which 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.

Notes for review

  • Flag learnings that name code that's gone #731 changes the next lines of learningsOnTouch (it passes dir to notesOnTouch for its stale check). The two merge cleanly. Once both are in, that call should pass repo, so the stale check looks at the worktree's code.
  • Get overlap warnings ready to go live #738 fixes the same worktree mistake for overlap warnings (editOverlap). It uses checkoutRoot without the same-repo check, which is fine there because another repo's git has none of the cached branches.
  • Bash commands that name a file already worked in worktree sessions: the command runs in the worktree, so its paths are repo-relative.

🤖 Generated with Claude Code

trail check flags TELEMETRY.md:71 and hooks.ts:175 against the Oct 27 learning on Claude's real HOME (word matches on "Claude ... copy ... project"), and hooks.ts:187 against the Oct 22 learning on filtering inside listNotes: this only changes which checkout listNotes reads, no filter is added.
@trailhq-graft

trailhq-graft Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 9 areas can be affected. 26 dependent symbols, depth 2.
Tests: no test reaches Hook Learning; 1 area updated its tests.
Tag: @shhdwi — 12 of 12 areas · @afeddersen — Event Telemetry

flowchart TB
  A0(("Claude Session State<br/>12 symbols"))
  A1(("Initialization Flow<br/>3 symbols"))
  A2(("Cloud Collaboration<br/>3 symbols"))
  A3(("Notes Storage<br/>2 symbols"))
  A4(("Event Telemetry<br/>2 symbols"))
  AX(("4 smaller areas<br/>4 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
Claude Session State 12 src/claude/session-metrics.ts:L1-L215 session-metrics.ts — imports, depth 1 Telemetry Contract
Initialization Flow 3 src/init/keep.ts:L1-L387 keep.ts — imports, depth 1 Telemetry Contract
Cloud Collaboration 3 src/cloud/nudge.ts:L1-L505 nudge.ts — imports, depth 1 Telemetry Contract
Notes Storage 2 src/notes/memory-notes.ts:L1-L280 memory-notes.ts — imports, depth 1 Telemetry Contract
Event Telemetry 2 src/telemetry/sessions.ts:L1-L113 sessions.ts — imports, depth 1 Telemetry Contract
CLI Skills 1 src/skills/cli-skills.ts:L1-L376 cli-skills.ts — imports, depth 2 Telemetry Contract
3 smaller areas 3 CLI Entry Point, 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
Hook Learning · changed @shhdwi — 14 commits, last 3d ago
Home Notes · changed @shhdwi — 1 commit, last 8d ago
Claude Session State · affected @shhdwi — 25 commits, last 3d ago
Initialization Flow · affected @shhdwi — 10 commits, last 3d ago
Cloud Collaboration · affected @shhdwi — 22 commits, last 3d ago
Notes Storage · affected @shhdwi — 4 commits, last 3d ago
Event Telemetry · affected @shhdwi — 1 commit, last 7d ago · @afeddersen — 1 commit, last 2mo 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 26 dependent symbols, grouped by area

Claude Session State — 12 symbols in 12 files

  • src/claude/session-metrics.ts:L1-L215 — session-metrics.ts (imports, depth 1)
    37: import type { AgentHost } from '../telemetry/contract.js';
  • src/claude/skills-wiring.ts:L1-L116 — skills-wiring.ts (imports, depth 1)
    16: import { hookMsBucket } from '../telemetry/contract.js';
  • src/claude/state.ts:L1-L165 — state.ts (imports, depth 1)
    10: import type { AgentHost } from '../telemetry/contract.js';
  • src/claude/format.ts:L1-L416 — format.ts (imports, depth 2)
    368: `Save a learning when the session finds what the code can't tell the next person: a bug's cause after failed attempts, an approach dropped and why, a choice and why, or the user correcting how something is done here. \`${cmd('graft note')} --title "<the decision in plain words a teammate understands, no file names>"\` with the text on stdin, under ## Decided, ## Tried and ruled out and ## Watch out, giving the why. Put files, commands and values in the body under "In the code:". Never for setting up trail, status, or what the diff shows. A summary, never a transcript, and nothing secret. A learning is added to this repo unless it's about how this user likes to work or their own machine: then add --personal and it stays just with them. A learning is followed by default and the user can say otherwise in the moment; only when the user says it must hold even if someone asks otherwise, add --always-follow. One added to this repo goes in .trail/learnings/; commit it with the change so your teammates' Claude starts from it. Close your reply with the line ${cmd('graft note')} prints for it: for one added to this repo, the one for whether it's committed by then (check with git status --porcelain .trail/learnings).\n` +
  • src/claude/hook-io.ts:L1-L140 — hook-io.ts (imports, depth 2)
    ```25: * Append --dir <contextDir> for the hooks' own `graft ask`/`graft check````
  • src/claude/prompt-hook.ts:L1-L307 — prompt-hook.ts (imports, depth 2)
    17: import { keepsNotes } from '../notes/home.js';
  • src/claude/prompt-skills.ts:L1-L327 — prompt-skills.ts (imports, depth 2)
    58: /** What tests swap: g5's two calls, telemetry, the gate (a temp folder reads as scratch), the probes' git, the deadline's clock. */
  • 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)
  • src/claude/statusline.ts:L1-L39 — statusline.ts (imports, depth 2)
    6: import { noteCount } from '../notes/home.js';
  • src/claude/stop.ts:L1-L216 — stop.ts (imports, depth 2)
    25: import { trailHome } from '../notes/trail-home.js';
  • src/claude/sync-run.ts:L1-L48 — sync-run.ts (imports, depth 2)

Initialization Flow — 3 symbols in 3 files

  • src/init/keep.ts:L1-L387 — keep.ts (imports, depth 1)
    28: import { repoPlace } from '../notes/home.js';
  • src/init/run.ts:L1-L445 — run.ts (imports, depth 1)
    34: import { repoPlace } from '../notes/home.js';
  • src/init/review-screen.ts:L1-L316 — review-screen.ts (imports, depth 2)

Cloud Collaboration — 3 symbols in 3 files

  • src/cloud/nudge.ts:L1-L505 — nudge.ts (imports, depth 1)
    181: if (!place.git || place.key.startsWith("local/") || !keepsNotes(repo, env)) return [];
  • src/cloud/lookup-activity.ts:L1-L60 — lookup-activity.ts (imports, depth 2)
    8: import { noteAuthor } from "../notes/git-facts.js";
  • src/cloud/pair.ts:L1-L368 — pair.ts (imports, depth 2)
    18: import { noteAuthor } from "../notes/git-facts.js";

Notes Storage — 2 symbols in 2 files

  • src/notes/memory-notes.ts:L1-L280 — memory-notes.ts (imports, depth 1)
    22: import { noteAuthor } from "./git-facts.js";
  • src/notes/pr-reviews.ts:L1-L577 — pr-reviews.ts (imports, depth 1)
    31: import { noteAuthor } from "./git-facts.js";

Event Telemetry — 2 symbols in 2 files

  • src/telemetry/sessions.ts:L1-L113 — sessions.ts (imports, depth 1)
    27: import { countBucket, savedTokensBucket, type AgentHost } from './contract.js';
  • src/telemetry/track.ts:L1-L162 — track.ts (imports, depth 1)
    21: import { EVENTS, allowedValue } from './contract.js';

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 : '');

CLI Entry Point — 1 symbol in 1 file

  • src/cli.ts:L1-L2844 — cli.ts (imports, depth 2)
    5: * graft/ and a clone has the graph. A workspace parent (≥2 git children)

Learning Sessions — 1 symbol in 1 file

  • src/learn/sessions.ts:L1-L310 — sessions.ts (imports, depth 1)
    30: import { noteAuthor } from "../notes/git-facts.js";

MCP Tools — 1 symbol in 1 file

  • src/mcp/tools.ts:L1-L337 — tools.ts (imports, depth 2)
Test signal per changed area — 1 ✓ · 1 ✗ · 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
  • ✗ Hook Learning — 0 of 1 reached · no test file reaches it
    • not reached: learningsOnTouch
  • ✓ Home Notes — 1 of 1 reached · 1 test file changed here: test/trail-learning-on-touch.test.ts
54 test suites also reference this code

54 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-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
  • test/init-b6-review.test.ts
  • …34 more

⚠️ 1 changed file not in the graph (TELEMETRY.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 6 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 30, 2026
@anirudhkumar-nanonets

Copy link
Copy Markdown
Collaborator Author

Folded into #738, which now carries this change as be2e0c1.

github-actions Bot added a commit that referenced this pull request Oct 30, 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