Refactor Trail Models for Project Migration

can you review https://entire.io/gh/entireio/trails/2074/changes/cli/1482
That link points at project trail #2074, which wraps repo trail cli#1482 (branch dip/project-trails-cli, head b3e7e92d). The change is 42 files and about +3.2k/−0.6k lines. CI is green and there are no open findings. I built the branch and ran its read-only commands against prod. Two of the problems below showed up in those runs, not just in reading the code. I haven't posted anything.
Bugs
1. Findings, approvals, watch, checkout and resume all fail for any trail whose branch has merged.
trail_working_context.go:110rejects the branch unlessout.Branch == selected.Branch.- For a merged branch, the Change read through
GET …/trails/<id>/changes/<cid>returns"branch": nulland puts the name inoriginal_branch. The parent trail'schanges[]summary still shows the name, so the two never match. - I reproduced it:
trail finding list 1482returnstrail branch response does not match the selected repository/branch. That trail's only cli branch issecurity/investigate-fix-file-read, which has merged. Reading findings after a merge is a normal thing to do, and the error doesn't say why it failed. - Fix: compare against
out.Branch, falling back toout.OriginalBranch. Add a test with a merged Change fixture.
2. Looking up a trail by number already fails in gh/entireio.
resolveSelector(project_trail_target.go:252) pages through the collection sorted byupdated. It stops aftertrailFindMaxPages=20pages of 100, so it can see at most 2,000 trails.gh/entireiohas 2,099 trails today.trail show 2 --project gh/entireiofails after about 8.6s withexceeded its page budget, so roughly 99 trails can't be reached by number, and that grows every day.- Sorting by
updatedalso means a trail edited while the lookup is paging can move to the top and be skipped. - The server already resolves numbers on the detail route:
GET /api/v1/gh/entireio/trails/2returns trail #2 with an ETag. Fix: use that single GET instead of the scan. The design doc says number lookups go through/trails, so update the doc too.
3. Old trail numbers now silently resolve to unrelated trails.
- Repo numbers and project numbers use the same integer range, and they have already diverged (1482 ≠ 2074).
trail show 1482now shows "fix: stop entire investigate fix from following an untrusted file path", a different trail.- Write commands have the same problem.
trail update 1482 --status closedwould close the wrong trail.trail approve 1482would approve the wrong trail's branch whenever it has exactly one branch in this repo. The confirmation text names the trail only after the write. - Repo numbers are still everywhere people look: the web URL
…/changes/cli/1482, CLAUDE.md:56 (entire trail finding list <n>), and anything people or agents remember. - Fix: before a write, check whether the number is also a repo-local trail number with a different parent, and refuse or warn if so. At minimum, require
--projectfor numeric selectors on write commands.
Regressions outside the PR's stated scope
4. The first-turn injection drops safety guidance (lifecycle.go:~461).
- The new text no longer contains "commits auto-capture checkpoints, never create them by hand" or "leave enable/disable/clean/auth to the user". Those were the reasons this injection existed.
- It adds per-task advice instead: run
trail show, otherwise create a trail, keep its description updated. The deleted comment recorded a 963-transcript census showing that kind of advice only costs tokens. trail createpublishes the branch, so this now nudges every agent session toward pushing.- Fix: keep the two invariant sentences. Add at most one line about trails.
5. Old command forms break with no migration hint.
trail listwithout--projectand--status anyboth error now (-nis gone too).trail_review_cmd.go:516–528still prints--status anyand<number|id|branch>hints.- CLAUDE.md:56 still documents the repo-number usage.
- Requiring
--projecteven inside a clone is a product decision. Per the repo rules it should be flagged for review, not just done.
Cleanup
- 6. Dead code: the old
newTrailShowCmd,newTrailUpdateCmd,newTrailCreateCmd,newTrailDeleteCmd(merged in #2666 and now unregistered),runTrailListAll, and most ofresolveTrailReviewTargetare no longer registered. Only tests andreview_bridge.go:67still call them. The tests keep them looking live to the linter, so they should either be deleted or have a stated reason to stay. - 7. Agent-help visibility: removing
agentHelpRequiresTrailsAnnotationnow advertisestrailto agents in repos where trails are disabled. The finding, approve and checkout subcommands still need repo trails there. - 8. Comment output:
comment listprintsNo discussions on trail 01M…(the ULID) instead of#N. - 9. Old repo-trail discussions: the old repo-scoped
trail commentis no longer registered. I couldn't confirm whether existing repo-trail discussions are visible through the project/discussionsroute; that's worth asking the author.
Context for the author
Trail #2074 also links dip/trails-legacy-project-opt-in ("Keep legacy trails default; opt into project trails via environment"). If that branch is meant to land first, it would limit #3 and #5 to people who opt in. #1, #2 and #4 still apply either way.
The parts that worked well:
- Fail-closed routing: no fallback to a catalog or default cell.
- ETag handling: a 412 never turns into an unconditional retry.
- Checking that response identity matches the request.
- The idempotency key is printed before the request is sent.
- Live listing and pagination against prod worked.
If you'd like, I can post #1–#4 as trail findings or comments, or draft the fixes for #1 and #2.
The only new commit merges #2659 (dip/trails-legacy-project-opt-in). Project trails are now behind ENTIRE_PROJECT_TRAILS=1, and without it the CLI registers the old repo-scoped commands again. I rebuilt the new head (a111d6c58d) and ran it in both modes. CI and the agent review were still running when I checked.
Fixed by the opt-in
- #3 (old numbers resolving to the wrong trail) is fixed for default users. Without the variable,
trail show/finding list/approvals/resume 1482all reach cli#1482 again. It still applies to anyone who setsENTIRE_PROJECT_TRAILS=1. - #5 (old command forms breaking):
list --status any,-nanddeleteare back, along with the old help and hints. - #6 (dead code): the legacy commands are registered again, so the code is live.
- #7 (agent-help visibility): in legacy mode
trailagain requires trails to be enabled before agent-help lists it.
Legacy mode behaves like main in what I ran. The "checkpoint not found" warning from trail resume 1482 also appears with the main build, so this PR didn't cause it.
Still open
#4 is now worse. entireTrailContextInjection in lifecycle.go didn't change, and it isn't gated on ENTIRE_PROJECT_TRAILS.
- Every agent session in a trails-enabled repo is told "a trail captures project-level intent across repositories and branches". By default, though, the CLI runs in legacy mode, where a trail is per-branch.
- The two invariants are still missing: "commits auto-capture checkpoints, never create them by hand" and "leave enable/disable/clean/auth to the user".
- Fix: put back the invariant text from main. Add the project-trails sentence only when the variable is set.
#1 still reproduces in project mode. ENTIRE_PROJECT_TRAILS=1 trail finding list 1482 returns trail branch response does not match the selected repository/branch. The check at trail_working_context.go:116 still compares out.Branch, which is null once a branch has merged. Fix: fall back to out.OriginalBranch.
#2 still reproduces in project mode. trail show 2 --project gh/entireio returns exceeded its page budget (project_trail_target.go:263). The fix is still a single GET /api/v1/{host}/{project}/trails/{number}, which the server already supports.
#8 (minor): comment list still names the trail by its ULID (project_trail_comment_cmd.go:103).
New in this commit
- Legacy
checkoutandresumenow reject trails that have no number.resolveLegacyTrailContextgoes throughresolveNumberedTrailAtPath, which errors withtrail has no number yet. On main, only approvals and comments used that resolver; checkout and resume worked without a number. This is low impact, but it's a regression in the mode meant to keep old behaviour. Fix: useresolveTrailBySelectorAtPathfor thelocalOnlycallers. - Help text is rewritten by searching for exact strings.
configureLegacyTrailHelp(trail_legacy_cmd.go) finds substrings like"The trail may be given"and"<trail> is a project"in each command'sLongand splices in replacements. If anyone rewords those help texts, the rewrite silently stops happening; only thepanicon a missing command is caught. Better to have each command's constructor choose its help text by mode, or add a test that asserts the legacy wording for all seven commands. - Mode is decided once, when the command tree is built.
usesProjectTrailsreads the annotation set at construction, so it can't flip partway through a run, which is good. The cost is that hidden code paths don't see the setting: thereview_bridge.go:67call toresolveTrailReviewTargetand the injection both stay legacy-only or mode-unaware. That's fine as long as it's intended; the injection is where it shows (see #4).
Before merging, I'd still fix #4. #1 and #2 only affect people who opt in, so they could become follow-ups if the variable is treated as an internal preview.
Finding #4 is about one function: entireTrailContextInjection in cmd/entire/cli/lifecycle.go. It builds the text Entire injects into an agent's context on the first turn of each session. This conversation got the main-branch version: it's the "Entire is enabled for this repo. Run entire agent-help…" block at the top of your first message here.
When it fires (unchanged)
The PR doesn't change when the text is injected. It's still once per session, and only when the cached check says trails are enabled for the repo (lifecycle.go:~552, decision != trailEnablementCacheEnabled → return). Only the wording changes, and it changes for every agent session in every trails-enabled repo.
The text, before and after
main:
Entire is enabled for this repo. Run
entire agent-helpto see what entire does and which subcommand to use, thenentire agent-help <command>for that command's exact, current flags. Commits automatically capture the AI session as a checkpoint, so never create checkpoints by hand — just commit normally. Leave setup and destructive commands (enable, disable, clean, auth) to the user. This repo is auto-detected from the git origin remote as gh/entireio/cli; you are already inside it, so never ask the user for the repo name.
PR:
Entire Trails is enabled. A trail captures project-level intent across repositories and branches—not just one branch. Start with
entire trail showto find the current branch's trail. Reuse an existing trail when the work shares its intent; otherwise create one. Keep its description current withentire trail update. Useentire agent-help trailto discover commands and flags. This repo is auto-detected from the git origin remote as gh/entireio/cli; you are already inside it, so never ask the user for the repo name.
Only the closing repo sentence is the same in both.
What changes in practice
-
Two rules are removed.
- "Commits automatically capture the AI session as a checkpoint, so never create checkpoints by hand" is what stops agents from running
entire checkpoint create. - "Leave setup and destructive commands (enable, disable, clean, auth) to the user" is the only place in the injection that tells agents not to run those commands. agent-help repeats it in its audience grouping, but only if the agent goes and reads agent-help.
- The new test (
trail_injection_text_test.go) asserts the text must not contain"checkpoint"or"Leave setup and destructive commands". So the removal is deliberate and locked in, not an oversight.
- "Commits automatically capture the AI session as a checkpoint, so never create checkpoints by hand" is what stops agents from running
-
The pointer to agent-help gets narrower.
entire agent-help(the whole CLI) becomesentire agent-help trail(one command group). The deleted comment recorded that the agent-help pointer is the part agents actually use: 25 calls in a 963-transcript census. Pointing it attrailalone means agents no longer get sent to the full command list. -
It adds a workflow for every session. "Start with
entire trail show… otherwise create one… keep its description current withentire trail update." The deleted comment said this is the kind of text to keep out: per-task advice cost tokens on every first turn, and agents never followed it. Two specific effects:- Every session now starts with a trail lookup, which is a network call, even for unrelated work.
- "otherwise create one" pushes agents toward
trail create, which pushes the branch to the remote. That's an outward-facing action, and the agent is now prompted to take it without the user asking.
-
It describes the wrong model in default mode. After the #2659 merge, the CLI runs in legacy mode unless
ENTIRE_PROJECT_TRAILS=1is set, and in legacy mode a trail belongs to one branch. The injection has no mode check, so every default user's agent is told trails are "project-level intent across repositories… not just one branch", whileentire trailbehaves per-branch. -
The design note is deleted. The roughly 20-line comment explaining why the text is kept minimal (the census, and why advice belongs in agent-help) is replaced with a 3-line comment. The next person to edit this function loses that reasoning.
Suggested fix
- Restore main's text unchanged, including the agent-help pointer and both rules.
- When
ENTIRE_PROJECT_TRAILS=1is set, append at most one sentence, for example: "Trails here are project-scoped: one trail can span several repositories and branches; seeentire agent-help trail." - Drop the "otherwise create one / keep it updated" workflow, or move it into the
trailguidance inagentHelpGuidance. CLAUDE.md says that's where agent guidance belongs. - Change the test back to asserting the rules are present.
The injection runs in the agent-hook path, which can't read the mode annotation from the cobra command tree. The simplest gate is reading os.Getenv(projectTrailsEnv) there, the same check newTrailCmd uses.
This trail (#2074, branch dip/project-trails-cli) moves the CLI's entire trail commands onto a new server model called project trails, behind an opt-in switch.
The model change
Today (repo trails): a trail belongs to one branch in one repo. cli#1482 is "the trail for branch dip/project-trails-cli in gh/entireio/cli". Numbers count up per repository.
New (project trails): a trail is intent at the project level (a GitHub owner like gh/entireio, or a native et/<project>). One trail can span several repositories and branches. Each linked branch becomes a "change" under it, and numbers count up per project.
The server already has both, and every repo trail now has a project parent. The URL you sent shows that: trails/2074/changes/cli/1482 is project trail #2074, which holds the cli branch work #1482. This trail itself has two changes in cli: dip/project-trails-cli and dip/trails-legacy-project-opt-in.
What the CLI gains (with ENTIRE_PROJECT_TRAILS=1)
- Intent-level commands:
trail show,update,createandlistoperate on the project trail: title, description, status (draft, open or closed only; "merged" belongs to a branch), assignees and priority.updateuses an ETag so it refuses to overwrite someone else's concurrent edit.createpushes your local branch, then creates the trail with that branch linked in one request. It prints an idempotency key so a failed request can be retried safely.
- Linking branches: the new
trail link/trail unlinkadd or remove a repo branch from an existing trail. That's how a trail grows across repos. - Listing:
trail listneeds an explicit--project gh/entireio, even inside a clone.--repoonly filters within that project. Requests go straight to the project's assigned cell, and pagination uses the server's own page tokens. The old cross-cell fan-out is gone. - Branch work is selected inside a trail:
checkout,resume,finding,approve,approvalsandwatchstill act on a single branch.- You name the project trail, and
--repo/--branchpick which branch within it. Without them it uses your current branch, or the only branch in this repo; if that's ambiguous it asks you to choose. - Approvals and findings apply to that one branch, not the whole trail.
- Discussions:
trail commentmoves to trail-wide discussions, so comments belong to the trail, not one repo's branch. - Routing safety: the project's cell comes from Core's project resolution. If that fails, the command errors; it never falls back to a guessed or default cell.
What happens without the variable
After the #2659 merge, nothing changes for default users. entire trail registers the old repo-scoped commands with their old help: repo-local numbers, list --status any, delete, and so on. Project mode is a preview you opt into.
The one exception is the first-turn agent injection (#4 from my review). It changes for everyone, describes project trails even in default mode, and drops the checkpoint and destructive-command rules.
Also in the diff
docs/architecture/project-trails-cli.md: a design doc for the new surface.- Updates to the API-routing and CLI-conventions docs.
- agent-help classification for the new commands.
trail resumesuggests follow-up commands; in project mode they now includeENTIRE_PROJECT_TRAILS=1 … --project … --branch ….
In short, it's the CLI half of moving trails from "one branch's trail" to "a project-level trail that collects branches across repos". It's off by default, and the review issues I raised (merged branches, number lookup, the injection) are mostly in the opt-in path.
Mostly yes. The server already has one data model; the two modes only differ in how the CLI reads its arguments. I checked this against prod before answering.
Why one mode is possible
- Every repo trail already has a project parent. cli#1213, a merged branch from September, returns
parent: {number: 1482, …}, and cli#1482 sits under project #2074. There are no "legacy trails" on the server, only branch work that has a project parent. The env var switches CLI behaviour, not data. - Several commands already act on one branch in both modes.
finding,approve,approvals,checkout,resumeandwatchwork on a single repo branch either way. Only how the CLI locates that branch differs.
Where the two actually conflict
- What a bare number means. Repo numbers and project numbers overlap and have drifted apart: 1482 is this branch in cli, and also an unrelated project trail. You can't try both and pick one, because almost every number exists in both spaces. You need a rule.
- There are two descriptions. The repo trail and its project trail store separate bodies. The title of #1482/#2074 matches, but the bodies differ: the repo body is the PR-style "Summary…" text, the project body is the intent text. So
trail update --bodyhas two possible targets. - Status values differ. A branch can be
merged; a project trail is only draft/open/closed. listscope. The old default is this repo's trails; the new one requires--project.- Discussions. I couldn't confirm whether repo-trail comments and project discussions are the same store.
How I'd unify it
| Command | Single behaviour |
|---|---|
| Selectors | A bare number inside a clone means the repo number, so it keeps working with existing URLs, CLAUDE.md and habits. A project number needs --project gh/entireio 2074 or a qualified form like gh/entireio#2074. ULIDs work everywhere. Output always shows both, e.g. Trail #2074 · cli#1482 (dip/project-trails-cli). |
finding / approve / approvals / checkout / resume / watch | Resolve the selector to one branch. A repo number gives the branch directly. A project selector gives the trail, and --branch or the current branch picks the branch within it. This is the PR's working-context code plus a repo-number case (plus the merged-branch fix). |
show | One view: the branch work plus its parent's intent and its other repos and branches. The JSON includes both, with parent added. |
list | No --project inside a clone lists this repo's trails, as today. --project lists the project collection. --status any is accepted in both. |
create | One command. The default creates the project trail with the current branch linked, which the server already treats the same as a repo trail with a parent. --no-branch and --project are extra options, not a different mode. |
link / unlink | Always available; these are new features, not a conflict. |
update | Needs a decision because of the two bodies (see below). |
delete | Keep it for branch work; project trails get close. |
With this, the env var, the usesProjectTrails checks, configureLegacyTrailHelp's string patching and the duplicate show/list/create/update/comment code all go away. The first-turn injection also no longer needs a mode check.
What needs a product or server answer first
update --body: which description does it edit by default? I'd edit the project intent when you name a project trail, and the branch description when you use a repo number or the current branch. That only works if the server keeps both bodies on purpose; if it plans to merge them, the question disappears.- Discussions: are repo-trail comments visible through the project
/discussionsroute? If yes,commentcan switch over completely. If not, old comments need a read path. - A number-lookup route:
GET /{host}/{project}/trails/{number}already works. It should be a documented contract so the CLI can depend on it (this also fixes #2).
If those three answers come back as I'd expect, unifying is mostly removing code, not writing new code. I can turn this into a comment for the author, or prototype the selector rule on a branch.
That changes my recommendation. The canonical number for cli trails is now the project number, and the PR as merged gets the default backwards for exactly this repo.
What breaks now
The trail page shows trails/2074. Using the installed CLI (same behaviour as the PR's default legacy mode):
That error is the harmless case. cli's repo numbers and gh/entireio's project numbers cover overlapping ranges (1482 exists in both, as different trails). So a number copied from the web UI often lands on an unrelated repo trail with no error. Default users hit this on reads and on writes like approve and update.
With ENTIRE_PROJECT_TRAILS=1 you get the reverse: old repo numbers from habit, CLAUDE.md:56 or earlier output land on the wrong project trail.
So the env var lets each user pick a numbering scheme, but which numbers are correct depends on the repo, not the user. In a migrated repo, legacy mode is wrong for everyone.
So my earlier rule was wrong
I suggested "a bare number inside a clone means the repo number". For a migrated repo that contradicts the UI. It should be:
- Bare number = project number. That matches the web, the trail page, and what people will paste.
- Old repo numbers only in a qualified form, like
cli#1482, or through--branch. - A cheap safety check for old habits: with a bare number in a clone, if the current branch's Change has number N but its parent has a different number, warn "#N is this repo's branch number; its trail is #M" before running any write.
What decides the mode
The mode should follow the repo's migration state, not an env var.
- If every repo is being migrated (every Change I read already has a
parent, including old merged ones), drop the env var and legacy mode. Ship project semantics only, keeping the qualified repo form and the warning above for old numbers. - If migration is per project, the CLI needs a signal from the server.
/projects/resolve/gh/entireiocurrently returns no such field; I checked. A flag liketrails: "project"on that response, or on the repo placement the CLI already fetches, would let the CLI choose per project automatically. Guessing from "has a parent" doesn't work, because parents already exist everywhere.
Either way, the four open items from the review stay the same, and fixing #1 (merged branches) and #2 (number lookup) becomes required before release rather than follow-up. Both are on the path every cli user would take.
One question for the author or server team decides most of this: is migration all-at-once or per project? I can draft that as a comment on the trail together with the flipped selector rule.
ok, maybe different approach: what if we separate this on a higher level, like duplicate the whole trail_ files to project_trails and then do the conidition in the least amount of places, once we ship project trails, the normal trails can be thrown away (could also do trails to legacy_trails and project becomes trails assuming we keep the later)
Yes. Splitting at the top is cleaner than what the PR does now. I'd change one thing: fork the commands whose meaning differs, and pass a resolver into the rest instead of copying their files.
Where the mode is checked today
On the current head, the mode check appears about 10 times across 7 files:
usesProjectTrails(cmd)intrail_working_context.go,trail_review_cmd.go,trail_watch_cmd.go, and twice inagent_help_cmd.go.selected.Target != nilchecks in resume, approvals anddescription().configureLegacyTrailHelp, which rewrites the legacy help by searching for exact strings.
Every shared command has to know about both modes. Removing legacy later means finding and unpicking all of these.
Why I wouldn't copy every trail_* file
trail_cmd.go, trail_review_cmd.go and trail_resume_cmd.go alone are about 5,500 lines, and most of that is identical between modes. Fetching findings, rendering the dashboard, applying a finding, the checkout and worktree logic, resume pickers, and the SSE watch loop all don't care which kind of trail they were given. Copying them means:
- Every bug fix lands twice until legacy is removed.
- The repo's duplication check (
mise run dup, part of lint) flags the copies.
The split I'd use
All of the difference between modes is in how a command finds its target. What it does once it has one is the same. So split there:
| Kind | Commands | Approach |
|---|---|---|
| Different meaning | show, list, create, update, comment, delete (legacy), link/unlink (project) | Separate command trees. Legacy stays exactly as on main in trail_*.go; project lives in project_trail_*.go. The PR already has these files. |
| Same work, different lookup | finding, approve, request-changes, approvals, checkout, resume, watch | One command each, given its resolver. E.g. newTrailFindingCmd(resolve trailTargetResolver). The legacy resolver is main's code; the project resolver is the working-context code. |
Then the mode is checked in one place:
Agent-help and the first-turn injection read the same projectTrailsEnabled(), so they stay consistent with the tree that was built.
Each mode's help text sits in its own constructor, so configureLegacyTrailHelp goes away. The resolver also returns what each mode needs to display (repo #1482 or project #2074 / branch), which removes the Target != nil checks in output code.
Naming
Keep legacy where it is now, and rename at removal time. Renaming trail_*.go to legacy_trail_*.go now would touch about 40 files and conflict with every open branch that edits trail code, which is a lot of branches. Once project trails ship:
- Delete the legacy tree and the legacy resolver.
- Rename
project_trail_*.gototrail_*.goin a separate commit that only renames files. - Delete
projectTrailsEnabled().
That cleanup is mostly deletion, and nothing in the shared commands needs changing.
The switch itself
Because it's one function, it can start as the env var and later become whatever decides migration, as discussed above. Since cli is already migrated, it should probably read a per-project state rather than a per-user setting. The existing per-repo trails-enablement cache (the one that gates agent-help and the injection) is a natural place to store it, because the CLI can read it at startup without network calls.
On the current PR, about half of this is a refactor of what's already there. The project_trail_* files already exist and the working context is already a resolver. What's left is reverting the shared files to main, adding the resolver parameter to the seven shared commands, and moving the mode check into newTrailCmd.
If you want, I can sketch the resolver interface against the current head so the author can see how much code actually moves.
The split is implemented and lint, Windows vet and the trail tests pass, but the full suite (mise run test:ci) is still running. Nothing is committed or pushed yet. It's on local branch soph/project-trails-split, built on top of the PR head a111d6c58d.
What changed:
- One place picks the model.
newTrailCmdForModeregisters either the legacy commands (show,list,create,update,delete,comment) or the project ones (show,list,create,update,link,unlink,comment). - The seven shared commands take the mode as a parameter. The mode, in the new
trail_mode.go, says how a trail number or--branchfinds the branch and which help text to show. That applies tocheckout,resume,finding,watch,approve,request-changesandapprovals. Their code no longer checks which mode it's in. configureLegacyTrailHelpis deleted. Legacy help, flag descriptions and error hints are main's wording again; I confirmedtrail finding --helpmatches main.- The first-turn injection is main's text again, including the checkpoint and destructive-command rules. Only with
ENTIRE_PROJECT_TRAILS=1does it add one sentence saying trails are project-scoped. A test now requires both rules to be present. - Legacy
checkoutandresumework again for trails without a number, as on main; only approvals need a number. There's a new test for this, and it fails if I undo the fix. - Docs:
project-trails-cli.mdhas a short layout section and the steps to remove legacy later.
Three places still check whether a project trail was found, to choose the output wording in resume, approvals and the trail description. That's based on the result, not the mode, and it goes away when legacy is removed. Agent-help still reads the mode from the command tree it was built from.
The change is 20 files, +317/−187 against the PR head.
Verified so far:
mise run linthas 0 issues, andGOOS=windows go vetpasses.- All tests matching trail, project, agent-help, injection, approval, finding, review, resume, checkout or watch pass.
- Against prod, read-only:
- Without the variable:
finding list 1482andapprovals 1482work, and agent-help describes per-branch trails. - With it:
finding list 2074 --branch …andapprovals 2074 --branch …work, and agent-help describes project trails.
- Without the variable:
Not in this change: the merged-branch bug (#1), the number-lookup budget (#2) and the comment ULID (#8) are still there in project mode. I kept this change to the restructuring.
When the suite finishes I'll report the result. I'll then ask whether to push to dipree's branch (dip/project-trails-cli) or open a separate PR on top of it; it's their PR, so I'd rather not push to it without you saying so.
The full suite (mise run test:ci) had two failures. Neither is caused by the split:
TestProbeConfigDir_AsksClaude(agent/claudecode) passes when run alone. In the full run it hit a 5s timeout, most likely because the machine was busy with everything else.TestOpenCodeSeedRepoPlantsDeps(e2e/agents) fails the same way on cleanorigin/main, because a local OpenCode setup directory is missing on this machine. It's the "unrelated OpenCode fixture failure" the PR description mentions.
Everything else passed, including unit, integration and canary tests across the trail packages. Lint is clean, the Windows vet passes, and both modes behave correctly against prod.
The split is still uncommitted on local branch soph/project-trails-split. How do you want it delivered?
- Push to
dip/project-trails-cliso it lands in dipree's PR directly. That rewrites their branch, so check with them first. - Open a separate PR on top of their branch. They can review and merge it into theirs; this is the least intrusive option.
- Just commit locally so you can look at it first.
The split is up as PR #2683 and is on trail #2074, next to the other two branches.
- PR: https://github.com/entireio/cli/pull/2683. Its base is
dip/project-trails-cli, the same way #2659 was stacked, so it shows only the split and merges into dipree's branch. - Trail #2074 now lists three branches in cli:
dip/project-trails-cli(open),dip/trails-legacy-project-opt-in(merged) andsoph/project-trails-split(open). - Branch and commit: pushed as
soph/project-trails-split, one commit,1812915cc9.
The new branch's entry on the trail is titled just "soph/project-trails-split", because linking a branch takes the branch name as its title. There's no command yet to rename a linked branch, so it will stay that way unless the server picks up the PR title.
The PR description lists what changed, the three project-mode bugs left for follow-up (merged branches, the number lookup and the comment ULID), and the verification. That includes the two test:ci failures that aren't from this change: one passed when run alone, and the other fails the same way on main.
The automated agent review hasn't run on this push yet. Want me to check the trail's findings once it finishes and fix any real ones?
can you link this to this trail still: https://entire.io/gh/entireio/trails/2074/changes/cli/1483
I haven't changed anything, because I'm not sure what you want here. That link is not a separate trail; it's one branch on the same trail #2074, and our PR is already on it.
| Branch on trail #2074 | Branch name | Status |
|---|---|---|
cli/1482 | dip/project-trails-cli | open, the main PR |
cli/1483 (your link) | dip/trails-legacy-project-opt-in (PR #2659) | merged, branch deleted |
cli/1503 | soph/project-trails-split (PR #2683) | open, the split we just pushed |
Each entry is one branch, so our branch can't also be linked to 1483. It would have to replace dipree's merged branch there.
Did you mean one of these?
- Mention it in the PR: say in #2683's description that the split restructures the opt-in from #2659 (cli/1483). This is the closest to "attach it to that change".
- Comment on cli/1483 or #2659 pointing at #2683, so anyone reading the opt-in PR finds the follow-up.
- Something else: if you meant a different trail or link, send it and I'll attach the branch there.
The one finding on our branch (cli/1503) was real but minor. I fixed it, pushed the fix, and marked the finding resolved.
The finding (project_trails.go:169, medium): when a request has no body, ProjectTrailRequest still wraps an empty slice in a bytes.Reader instead of passing nil. This code comes from dipree's base PR, not the split, but it shows up in our PR's diff.
What actually happens: the request body is already empty on the wire, because Go's HTTP library treats an empty reader as no body. The real effect is a header. The client adds Content-Type: application/json whenever it gets any non-nil reader. So every bodyless request in project mode was sending that header: reads, number lookups, unlink, and deleting a discussion message. Prod accepted these reads earlier, so nothing visibly broke. Some servers do reject an empty body labelled as JSON, though.
Fix (bc18726ff7, pushed to #2683):
- Pass a nil reader when there's no body.
- New test
TestProjectTrailRequestSendsBodyOnlyWhenGiven: GET and DELETE send no Content-Type and no body, while PATCH sends JSON. It failed before the fix and passes now. - API and trail tests pass,
mise run linthas 0 issues, and the Windows vet passes.
On the trail: I resolved finding 01M48XFFA6V1 with a note giving the commit and the reason.
The repo's rule is that the job isn't done until the review of the new commit also comes back clean. That review hasn't run yet. Should I check the findings again once it finishes?
It's in the repo's CLAUDE.md (which AGENTS.md links to), in the Verification table at CLAUDE.md:56:
| After every push to an open PR | check the trail's findings (
entire trail finding list <n>); fix real ones, resolve with-m, and re-check after the fix lands. Not done until findings are clear |
It's the instruction file checked into the repo, and I'm told to follow it. That's why I resolved the finding with an -m note and offered to check again after the next review.
This is the same line I flagged in the review: <n> there means the repo trail number (cli#1503). With ENTIRE_PROJECT_TRAILS=1 the same number would be read as a project trail number and point at a different trail.
In project mode it can't. A repo trail number like 1503 can't be typed in project mode at all: a bare number is always read as a project trail number, and there's no prefixed form like cli#1503. You reach the same branch by naming the project trail and the branch instead:
You can also leave out the selector and let the CLI follow the current branch to its project trail:
I ran the selector-less --branch form against prod during the review and it worked. A trail ULID doesn't help either: project mode reads any ULID as a project trail ID, so the branch-work ID for cli#1503 fails the identity check.
The branch is the one name that works in both modes. entire trail finding list --branch <pr-branch> reads the right findings with or without ENTIRE_PROJECT_TRAILS=1:
- Legacy mode finds the repo trail for that branch.
- Project mode finds that branch on its parent trail.
So CLAUDE.md:56 would hold up better if it said:
check the trail's findings (
entire trail finding list --branch <pr-branch>)
rather than <n>. Spelling out --branch instead of relying on the current branch also avoids a known trap: finding list with no target checks whatever branch you happen to be on. If that's the wrong branch, "No findings" doesn't mean the PR has none.
Want me to add that wording change to #2683?
No. In project mode, finding always works on a single branch and never combines findings across a trail's branches. The help text says so: "findings apply only to that branch, not the whole trail." So entire trail finding list 2074 picks one branch first:
--branch, if you pass it.- Otherwise, your current branch, if it's one of the trail's branches in this repo.
- Otherwise, the trail's only branch in this repo, if there's exactly one.
- Otherwise, it errors: "trail has multiple branches in this repository; select --branch: …".
It only considers branches in the current repository, or the one named with --repo. Branches the trail has in other repos are never included.
For #2074 this means that if you're on a branch that isn't one of its three cli branches, you get the "select --branch" error, not a combined list. If you pick the merged dip/trails-legacy-project-opt-in, you hit the merged-branch bug from my review instead.
The server has no trail-wide findings endpoint either. I tried …/trails/<project-trail-id>/reviews/comments, /findings and /reviews on #2074, and all three return 404. Findings exist only per branch. A trail-wide view would have to call each branch's findings itself, across every repo and cell, and handle the case where some of them fail. The design doc lists project-wide aggregation as follow-up work, and so far only for watch.
So for #2683 the per-branch command is the correct one: finding list --branch soph/project-trails-split.
Agreed, I think a change selector is the right addition. It's also the cleanest fix for the number problem we keep running into.
Why it fits
- It matches what people see. The web URL is
trails/2074/changes/cli/1503. A--change cli/1503flag uses exactly the parts of that URL, so people can copy straight from the link. - It's unambiguous. A bare number means the project trail (2074).
--changemeans one branch's work (cli/1503). Nobody has to guess which kind of number they typed, which removes the collision problem. - It's more reliable than
--branch.- Branch names can be reused, and a merged branch's name is gone from the server (
branch: null, as we saw). - A change number never changes and never gets reused.
- So it works for merged work, where
--branchcurrently fails, and it works without being on the branch.
- Branch names can be reused, and a merged branch's name is gone from the server (
- It can mean the same thing in both modes. In legacy mode
--change 1503is just the repo trail number. So the line in CLAUDE.md can becomeentire trail finding list --change <n>and work whether or not the variable is set. - It works across repos.
--change entire-api/170names another repo's work inside the same project, matchingchanges/entire-api/170in the URL.
How it would resolve
--change cli/1503→ the repo is cli, inside the project from--projector origin's owner. A bare--change 1503uses this repo.- Fetch the change by its number on the repo route (the same lookup legacy uses today). Its
parentgives the project trail. - If a trail selector is also given, check that the change's parent is that trail.
- Read the change through the project route to confirm it belongs to that trail. This check should compare IDs, not branch names, which also fixes the merged-branch bug.
--changeand--branchcan't be used together.
It would apply to the seven shared commands (checkout, resume, finding, watch, approve, request-changes, approvals). After the split it's one flag on each plus one branch in each mode's resolver.
One thing to settle first
The design doc currently rules this out explicitly: "there is no trail change subgroup or change selector." So this reverses a stated decision, and dipree should agree before it's built. The reasoning behind that line seems to be keeping one user-facing kind of thing (the trail). A --change flag fits that: it's a way to select within a trail, the same as --branch, not a second kind of trail.
Do you want me to raise it as a comment on the trail, or prototype it on #2683 (or a new stacked branch) so there's something concrete to react to?
Yes, and I'd prefer that to a flag. In project mode it's unambiguous, and it makes pasting from the web work. The grammar would be:
| Positional selector | Meaning |
|---|---|
2074 | project trail number |
01M462… (ULID) | project trail ID |
cli/1503 | branch work 1503 in repo cli, inside the project from --project or origin's owner |
the full URL …/trails/2074/changes/cli/1503 | the same, with the project and trail checked against the URL |
Accepting the full URL is cheap, because it already contains the project, the trail number and the repo/number pair. It's what people actually copy.
Why it's safe in project mode
Project mode already refuses branch names as selectors; branches only come in through --branch. So <repo>/<digits> can't be mistaken for anything else there.
What each command does with it:
- Branch-level commands (
finding,approve,checkout, …) act on that branch directly. They skip the current-branch guessing, and it works for merged work. show/updateact on its parent trail.showcould also mark which branch you came in through.
The catch is legacy mode
In legacy mode a positional selector can be a branch name, and branch names contain slashes. fix/1503 and issue/42 are normal branch names. So in legacy, cli/1503 already means "the branch called cli/1503". Two ways to handle that:
- Only in project mode. Legacy keeps working as it does, where bare
1503already means that same change. This is simplest, and legacy is going away. The downside is that the form differs by mode:1503in legacy,cli/1503in project. - Both modes, with a strict rule. In legacy, read
<seg>/<digits>as a change only when<seg>is exactly the current repo's name. Anything else stays a branch. If a branch with that exact name also exists, refuse and ask for--branch. Thencli/1503works the same in both modes, and the CLAUDE.md line can sayentire trail finding list cli/<n>.
I'd go with option 2. The only collision is a branch in cli literally named cli/<number>, and in that case the CLI refuses rather than guessing. It also gives one command that works before, during and after the switch to project trails.
The same steps as before still apply:
- Look up the change by repo number.
- Follow its
parentto the project trail. - Confirm through the project route, matching by ID rather than branch name, which also fixes the merged-branch bug.
The design doc's "no change selector" line would change to say that changes can be addressed, not that they become a second kind of trail.
Want me to build option 2 on top of #2683?
cli/1503 now works as a selector in project mode and is pushed to #2683 as commit a3ca5c1215. Legacy mode is unchanged.
What it does:
- Any project-mode selector, positional or
--trail, can be<repo>/<number>, the end of a change's web URL (…/trails/2074/changes/cli/1503). - The repo is looked up in the project from
--project, or origin's owner if you don't pass one. Itsparentgives the trail, so no list lookup is needed. finding,approve,request-changes,approvals,watch,checkout,resumeandunlinkact on that branch's work.show,updateandcommentact on its trail.- It can't be combined with
--branch, and--repo, if given, must name the same repo. checkoutandresumerequire it to be this clone's repo.
It also fixes merged branches (bug #1 from the review). Selecting merged work failed in every branch-level command. The check now uses original_branch when branch is null, so cli/1483 works.
Verified against prod (read-only, project mode):
finding list cli/1503,finding list cli/1483(merged),approvals cli/1482andshow cli/1503all reach trail #2074 and the right branch.cli/99999, combining with--branch, and a--repofor another repo each fail with a specific error.finding list cli/1503 --status any --freshness anyin project mode andfinding list 1503 …in legacy mode show the same resolved finding.
Tests:
- New tests cover the selector's syntax, open and merged work, and the refused combinations. The merged-work test fails without the fix.
- The CLI and API package tests and the uncached integration tests pass.
mise run linthas 0 issues and the Windows vet passes.
Docs and description:
- The design doc's selector paragraph is updated and there's a new usage example. I removed the hard wrapping in the paragraph I rewrote, per your no-hard-wrap rule, so that one paragraph is now a single line while the rest of the file is still wrapped.
- The help text for the project-mode commands now lists the new form.
- The PR description is updated; it previously still listed the merged-branch bug as not fixed.
Still open in project mode: number lookup still fails past 2,000 trails, and comment list still shows the trail's ULID instead of #N.
The review of the new commit hasn't run yet. Want me to check the findings when it does?
No trail finding is open right now. Our branch only ever had the one I fixed.
| Branch | Finding | Status |
|---|---|---|
| cli#1503 (ours, #2683) | 01M48XFFA6V1 (medium): bodyless requests sent a JSON Content-Type | resolved (fixed in bc18726ff7) |
| cli#1482 (dipree's base) | 01M48TFVW2PZ (low): trail approvals output in project mode | resolved |
| cli#1482 | 01M46Q5M9YH3 (low): comment … --json printed the raw API resource, including ETags | resolved |
The findings gate on #1503 passes and CI shows 17/17 passed.
That status is from the review of the previous commit. The review of a3ca5c1215 (the cli/1503 selector) may not have run yet, so I'd check again once it does.
If you meant the issues from my own review, two are still open, both in project mode only:
- Number lookup (#2):
trail show <n>gives up after 20 pages, andgh/entireioalready has 2,099 trails. The fix is to useGET /{host}/{project}/trails/{number}, which the server already supports. comment list(#8): it names the trail by its ULID instead of#N.
Of the issues from my two review passes, five are fixed (four by #2683, one by dipree's own reply PR), three were resolved when dipree merged the opt-in (#2659), and four are still open.
First review
| # | Issue | Status |
|---|---|---|
| 1 | Merged branches fail every branch-level command (a merged branch's branch comes back null) | Fixed in #2683 (a3ca5c1215): falls back to original_branch |
| 2 | Number lookup fails past 2,000 trails (trail show 2 in gh/entireio errors after ~8.6s) | Open. Fix is the single GET /{host}/{project}/trails/{number}, which the server already supports |
| 3 | Old repo trail numbers silently resolve to unrelated project trails | Partly addressed. The default legacy mode avoids it, and cli/1503 now gives a clear way to name branch work. A bare number in project mode still collides. Since cli is already migrated, the mode should follow the repo's migration state, not an env var; that's still a product decision |
| 4 | First-turn injection dropped the checkpoint and destructive-command rules | Fixed in #2683 (1812915cc9) |
| 5 | Old command forms break (list --status any, -n, delete, old hints) | Resolved by the opt-in. CLAUDE.md:56 still says entire trail finding list <n>, and I suggested --branch <pr-branch>; with the new selector, cli/<n> also works. Not changed yet |
| 6 | Legacy commands kept only as dead code | Resolved by the opt-in: they're live again |
| 7 | Agent-help listing trail even in repos where trails are disabled | Resolved by the opt-in for legacy mode |
| 8 | comment list shows the trail's ULID instead of #N | Open |
| 9 | Are old repo-trail comments visible through the project /discussions route? | Open question, never verified |
Second review (after #2659 merged in)
| Issue | Status |
|---|---|
Legacy checkout/resume rejected trails with no number | Fixed in #2683 (1812915cc9) |
configureLegacyTrailHelp rewrote help by searching for exact strings | Fixed: removed, and each mode now supplies its own help text |
| The mode was fixed when commands were built, so the injection didn't know about it | Fixed: the injection now checks projectTrailsEnabled(). entire review stays legacy-only on purpose |
Also from the trail
01M48XFFA6V1 (bodyless requests sent a JSON Content-Type) is fixed in bc18726ff7. Dipree resolved the two low findings on their branch.
So what's still open: #2, #8, the per-project mode switch (#3), the CLAUDE.md:56 wording (#5), and the discussions question (#9). #2 and #8 are small code fixes I can add to #2683 if you want. #3 and #9 need answers from dipree or the server team.
Here's what the lookup does today, why it breaks, and the replacement.
What happens today
To turn trail show 42 into a trail ID, resolveSelector (project_trail_target.go:263) scans the whole project:
GET /api/v1/trails?projectId=…&pageSize=100&sort=updated- Look for
number == 42on that page. - If it isn't there, follow
nextPageTokenand repeat, up totrailFindMaxPages = 20pages.
That gives three problems:
- A hard ceiling. 20 pages × 100 trails means at most 2,000 trails are searchable.
gh/entireiohas 2,099, sotrail show 2already fails with "exceeded its page budget". - It's slowest where it's needed most. Results are sorted by last update, so the trails people look up by number are usually old ones near the end of the list.
trail show 2made 20 sequential requests and took 8.6s before failing. - A race. If a trail is edited while the scan is paging, it moves to the top of the list. The scan has already passed that point, so it reports "not found" even though the trail exists.
Filtering doesn't help. I tried ?number=2 on the list endpoint, and the server ignores it and returns the newest trails.
The fix
The detail route already accepts a number where the ID would go. I checked against prod:
So resolveSelector becomes a single request:
Later requests (show, update, /changes/…, discussions) keep using the ULID path, as they do now.
What that gets us:
- One request instead of up to 20, about 0.3s instead of 8.6s, and the time no longer depends on project size or how recently the trail was edited.
- No ceiling and no paging race.
- The
trailFindMaxPagesloop and the repeated-page-token guard can be deleted.
The checks on Number and ProjectID keep the current safety: if the server returned something other than what was asked for, the CLI refuses instead of using it.
The open question
The design doc deliberately says numeric lookups go through the /trails list, and that GET /{host}/{project}/trails/{number} is "never used". That may only be because the author didn't know the detail route takes numbers. It could also mean the server team treats number lookup on that route as an accident rather than a supported contract.
So before changing this, someone on the server side should confirm the route officially supports numbers. If it does, the fix is about 20 lines plus a test, and the doc's sentence changes to match. If it doesn't, the fallback is to ask the server for a real number lookup. A larger page budget would only move the ceiling.
That route is part of the new project-trails API, and its number support is documented. I checked the cell's OpenAPI spec (/api/v1/openapi.json):
This isn't the old repo API. The /{host}/{project}/trails/… routes are the project-trails API: create, detail, update, changes, and discussions all live there. The spec says outright that the detail route takes a project number. That answers what I left open last time: it's a documented contract, not an accident.
The spec is also precise about the limits. Only GET detail accepts a number. PATCH, /changes/{changeId} and /discussions are all "Globally unique Trail ULID" only. That matches the fix: one GET …/trails/{number} to get the ULID, then use the ULID for everything else, which the CLI already does after lookup.
The routes that might not survive the switch are the ones project mode currently depends on. Findings, approvals, reviews and the event stream only exist on the old repo routes, /trails/{host}/{owner}/{repo}/{number}/… (trailDetail, trailsApprovals, reviewStart, trailEvents). There are no project-scoped versions in the spec. So in project mode, finding, approve, approvals and watch all reach a branch's work through the old repo route by its repo number. The project trail's branch entries (/changes/{changeId}) only support detail and detach.
Two old routes matter here:
trailDetail(GET by repo number), which the newcli/1503selector also uses.trailsListwith its branch filter, which is how the CLI finds the trail for the current branch.
One of the old routes is already marked deprecated: DELETE /trails/{host}/{owner}/{repo}/{number}, which matches the PR turning trail delete into an error.
So for the server team, the useful question isn't whether the number lookup survives; it does, by design. It's whether the per-branch features (reviews/findings, approvals, events, and finding a branch's trail) will move under /{host}/{project}/trails/{trailId}/changes/{changeId}/…, or stay on the repo routes. If they move, the project-mode resolver in this PR needs a second pass. If they stay, it's fine as is.
With that settled, I'd go ahead with fix #2 on #2683. It's a single request against a documented route, and it updates the design doc's "never used" sentence.
The CLI talks to one backend server (each region has its own copy of it). That server exposes two sets of trail URLs:
- Repo routes, the older set:
/api/v1/trails/{host}/{owner}/{repo}/…. Each trail there is one branch, numbered per repo (cli#1503). - Project routes, the newer set:
/api/v1/{host}/{project}/trails/…. Each trail there is project-level intent, numbered per project (#2074), with linked branches listed as "changes".
Legacy mode uses only the repo routes. Project mode uses both.
Legacy mode (the default)
| Command | Calls |
|---|---|
list | GET /api/v1/trails/gh/entireio/cli |
show 1503, update, create, delete | GET/PATCH/POST/DELETE /api/v1/trails/gh/entireio/cli/1503 (+ /body) |
finding | /api/v1/trails/gh/entireio/cli/1503/reviews/comments |
approve, approvals | /api/v1/trails/gh/entireio/cli/1503/approvals |
watch | /api/v1/trails/<id>/events (mapped onto the repo route) |
comment | /api/v1/trails/gh/entireio/cli/1503/discussions |
Project mode (ENTIRE_PROJECT_TRAILS=1)
Commands about the trail as a whole use project routes:
| Command | Calls |
|---|---|
| (first) | Core /projects/resolve/gh/entireio, to find which cell hosts the project |
list | GET /api/v1/trails?projectId=… |
show 2074 | number lookup (below), then GET /api/v1/gh/entireio/trails/<ULID> |
update, create | PATCH/POST /api/v1/gh/entireio/trails[/<ULID>] |
link, unlink | POST/DELETE /api/v1/gh/entireio/trails/<ULID>/changes[/<changeId>] |
comment | /api/v1/gh/entireio/trails/<ULID>/discussions/… |
Commands about one branch use both. Take finding list 2074 --branch soph/project-trails-split:
- Project route: read trail #2074 to see its branches (
GET /api/v1/gh/entireio/trails/<ULID>). - Project route: confirm the branch belongs to it (
GET …/trails/<ULID>/changes/<changeId>), which also gives its repo number, 1503. - Repo route: get the actual findings:
GET /api/v1/trails/gh/entireio/cli/1503/reviews/comments.
approve, approvals and watch work the same way, ending on /approvals or /events. Without a selector, the CLI first finds the current branch's trail on a repo route (GET /api/v1/trails/gh/entireio/cli?branch=…) and follows its parent to the project trail. The new cli/1503 selector starts from GET /api/v1/trails/gh/entireio/cli/1503 the same way.
How this connects to the last two answers
- Fix #2 is about step 1's number lookup. Today
show 2074finds the ULID by paging throughGET /api/v1/trails?projectId=…. The project detail routeGET /api/v1/gh/entireio/trails/2074is documented to accept the number directly, so one call replaces the paging. - The "survive the switch" question is about step 3. Findings, approvals and events only exist on the repo routes; there's no project-route version. If the server later moves them under
…/trails/<ULID>/changes/<changeId>/…, project mode's branch-level commands need updating. The number lookup in fix #2 is already on the new routes, so the switch doesn't put it at risk.
2 - ok, switch the url 3 - this is separated now throught the commands, when project trails are enabled on a repo the trails move to project for the xisting numbering 5 - use cli/<n> 8 - this is new as part of this PR? which #n would be shown 9 - can we check this on the cli? they are not yet for not migrated repos but an old trail on the cli that has comments should work, please look at ~/Work/entire/devenv/entire-api
Items 2, 5 and 8 are done and pushed to #2683 as commit ae78f427e2. Item 9 is answered: old comments don't carry over to project mode. Lint, the Windows vet, the CLI tests and the uncached integration tests all pass.
2: number lookup switched. trail show 2074 is now one GET /{host}/{project}/trails/2074, and the CLI checks that the reply has that number and project.
trail show 2used to fail after 8.6s. Against prod it now works in 3.1s, most of which is finding the project's server, not the lookup.trail show 999999says "project trail #999999 not found in gh/entireio".- The tests and the design and routing docs are updated.
3: Understood. With cli/<n>, the existing repo numbers stay usable in project mode, so I didn't change anything for this.
5: CLAUDE.md:56 updated, with one addition. cli/<n> only works with ENTIRE_PROJECT_TRAILS=1. Without the variable the CLI is in legacy mode, where cli/1503 is read as a branch name and isn't found. Since that's the default for most agents, the line now says to use cli/<n> with project trails and <n> otherwise. If you'd rather it said only cli/<n>, I can change it.
8: yes, this was new in the PR. It's in dipree's project comment command; legacy already printed #1503. Now:
- After
--trail 2074, acli/1503, or following the current branch, it prints "No discussions on trail #2074", the project number. - After a ULID, it still shows the ULID, because that's what you typed and the number isn't known without an extra request.
- New test added; it fails without the fix.
9: old comments are not visible in project mode.
- Server (entire-api): repo-trail comments are stored in
trail_threads, project discussions in a separatetrail_discussionstable (internal/store/thread_store.go). Nothing copies between them, anddocs/architecture.md:256says "Existing repository discussions … remain independent; no existing discussion data is moved." - Against prod: cli#1483 has 3 discussions. Legacy
comment list --trail 1483 --allshows all three. Project mode'scomment list --trail cli/1483reads trail #2074's project discussions and shows none. - Effect: in project mode the old repo comment command isn't registered, so nothing can read those comments. Comments made on cli trails before migration disappear from the CLI once the variable is set.
The PR description now covers this under "Not in this PR". One possible fix: when comment is given a cli/<n> selector, show that branch's repo comments. Or ask the server team to move the old comments into the project table. Which do you prefer, or should it just stay documented?
Okay, I'll leave the old comments alone. Pre-migration comments on repo trails stay readable only in legacy mode.
The note about it is still in #2683's description under "Not in this PR". I'll leave it there as a record of the known gap unless you want it removed.
Both findings you pasted were real, and both are fixed and pushed to #2683. A third, new trail finding is fixed and resolved too.
1. finding apply could patch the wrong repository (c7d04b0613). My cli/<n> selector caused this. apply already refuses --repo, but from a cli checkout a selector like api/7 named another repo's work anyway. The fix:
- The finding resolvers now have a "must be this clone's repository" option, and only
applyturns it on, the same waycheckoutandresumealready work. - In project mode,
apply api/7from a cli clone now fails with "api/7 is not in this clone's repository" before any request is sent. - Legacy mode didn't need a change, because its selectors can't name another repo.
2. Worktree directories collided (c7d04b0613). This came from the base PR (27dac09c2b): checkout --worktree named the directory after the trail's number, which every branch on the trail shares. It now uses each branch's own repo number again, so feature/x and feature-x under trail #42 get trail-7-feature-x and trail-8-feature-x.
New trail finding 01M498XYVS4J (low, from the review of the previous commit; 5ee7461ca6): for merged work reached through cli/<n>, messages printed trail #42 (branch ). They now name the original branch, the same way the merged-branch fix does. I resolved the finding with a note.
Each fix has a new test that fails without it. Lint has 0 issues, the Windows vet passes, and the full CLI tests and uncached integration tests pass.
The two you pasted aren't on the trail, so there was nothing to resolve. If they came from a PR review, tell me where and I can reply there. The agent review of the latest commit (5ee7461ca6) hasn't run yet. I can check its findings once it finishes.
Yes. The trail has two new open findings on the latest commit (5ee7461c), and Copilot left a review on the PR with three inline comments. There are no other PR comments and no approvals yet. CI passed, 17/17, and the findings gate passes because nothing is marked blocking.
Trail findings
| Finding | Claim | My read |
|---|---|---|
01M49B4D22F4 (medium), trail_working_context.go:151 | For a merged change reached by cli/<n>, the trail's branch list has no branch name, so the containment check fails | Wrong for merged work, possibly right for unlinked work. On prod, the trail's branch list does carry the name for merged branches (trail #1482's merged security/investigate-fix-file-read), and finding list cli/1483 (merged) worked. An unlinked branch might really have an empty name there; I haven't verified that. A cheap guard is to skip the branch-name comparison when the list entry has none, since the ID, trail and repo checks already prove it's the right work. |
01M49B4D1KS2 (low), trail_watch_cmd.go:101 | watch prints … / with no branch name for merged work | Real. It's the same gap I fixed in the last commit, in one place I missed. A one-line fix. |
Copilot on the PR
| Comment | My read |
|---|---|
trail_legacy_cmd.go:56: legacy resume on a trail with no number sends the findings request to …/0/reviews/comments, which warns and drops the findings | Real, but main already does this. Main's resume registered the /0 path the same way; my change only restored main's behavior. The fix would be to skip the findings request for unnumbered trails. It's rare, since a trail normally gets its number right away. |
trail_mode_test.go:180: the new test calls the resolver directly instead of running checkout/resume end to end | Fair, but limited. The test covers what the commit fixed, which is resolution. Running resume end to end in legacy mode needs a git repo with sessions, and it would mainly expose the issue in the comment above. |
lifecycle.go:459: two stray lines describing handleLifecycleTurnStart sit above the injection function | Real. The misplaced comment is on main. Dipree's PR had removed it, and my restore of main's injection text put it back. Trivial to delete. |
The watch fix, the unlinked-branch guard and the stray comment are small, and I can fix them in one commit and resolve the two findings. For Copilot's first two comments, do you want me to fix legacy resume as well, even though main already behaves that way, or just reply on the PR that it's existing behavior? I won't post anything there without your go-ahead.
Restored. Confirming those two files differ from the last commit only by the intended changes, then running the full checks.