feat: narrate worktree setup — echo first, typed phase progress, setup script on the eager path - #452
Merged
Merged
Conversation
A first turn on a fresh thread cannot start until the repository is fetched, a worktree is added and the repo's post-checkout hook has run. `thread.turn.start` emits the user's message only after all of that, so the transcript stays empty for minutes and the send reads as lost. `thread.message.user.echo` records the message on its own, ahead of provisioning, and marks it `provisional`. The turn start that follows carries the same messageId, and every projection upserts messages by id, so this costs one event and no duplicate row. The flag exists because consumers key work off "a user turn is starting". The pre-turn checkpoint baseline is the dangerous one: at echo time the thread's worktree may not exist, so capturing there would snapshot the project checkout instead and then suppress the real baseline as already present. The reactor now skips provisional messages and captures on the turn start, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setup progress was reported through three producer-local strings (`setup-script.requested/started/failed`) with an untyped payload and no success kind, so a client could not tell a finished install from one that never reported back, and nothing at all was said about the fetch or the worktree checkout that precede it. Declare the kinds and one payload shape in contracts, and build the records from a single server-side module: producers choose only *when* a phase starts and ends, while the kind, tone, wording and payload live in one place the mac and mobile clients can render off. `phase` is what lets a client collapse a phase's lifecycle onto one row instead of stacking start and finish; `exitCode` is deliberately absent rather than zero when the status was never observed, since "exit 0" would read as success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`createWorktree` hides two separately unbounded operations behind one call: a `git fetch` against the remote (only when the base is a remote-tracking ref) and a `git worktree add` that checks the tree out and runs the repository's post-checkout hook — in this repo, a full dependency install. A caller keeping a user waiting on it had no way to say which one was running. The optional reporter is observation only: a reporter that fails is logged and ignored rather than failing a worktree the caller needs, and the fetch phase is announced only on the path that actually contacts the remote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…status The setup script is typed into an interactive PTY so the user can watch it and keep the terminal afterwards. That means the shell, not the script, owns the process: the PTY's exit event never fires when the script ends, so the server could say "started" and then never mention it again. A failed `pnpm install` was indistinguishable from a healthy thread whose agent had simply gone strange. The command is now submitted together with a `printf` of `$?`. The line the PTY echoes back carries the printf *format* (`exit status %s`) while the shell's own output carries the substituted status, so the scanner cannot mistake the echo for the result, and a per-run token keeps a reused terminal from resolving an earlier run's marker. The watch is registered before the command is submitted (a fast script must not beat it) and unsubscribes itself the moment the outcome is known. Windows shells get the bare command: cmd.exe and PowerShell do not carry `$?` through a POSIX printf, and there the runner reports no completion signal rather than a guessed one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects with one shape — the app looks frozen between send and the agent's first token: The bootstrap program dispatched the user's message only after git fetch, worktree add and the setup script, so nothing appeared for the minutes those take. The echo now goes first, and setup activities are stamped no earlier than the message they belong under: the message carries the client's clock and the activities the server's, so a client running ahead used to produce a transcript that reordered itself on reload. Every setup phase now reports itself as a typed activity — fetch, worktree creation (which is also where the post-checkout hook runs), and the setup script through to its exit status. Turn start is deliberately not gated on the script, so its outcome is reported from a detached fiber. Eager provisioning — the client creating a thread's worktree ahead of the first message so opening a thread is instant — never ran the setup script at all. Whenever it won the race against first-turn bootstrap the agent landed in a checkout with no dependencies installed and no sign of it. Attaching a worktree to a thread that has no messages and no turns is exactly that case, so setup runs there too, through the same program. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new setup kinds would otherwise fall through to the tone fallback: a bare notice for progress and a generic failed tool call for a script that exited non-zero, with the exit status nowhere on screen. Each phase gets one row keyed by the phase rather than the activity id, so "Fetching origin/main" becomes "Fetched repository (1.2s)" in place instead of stacking a second entry, the same upsert-by-id contract tool calls already use. A failed setup script keeps its exit status in the detail, because "the agent is confused" and "your install exited 1" are the same symptom without it. Activities persisted before the typed payload existed fall back to the kind, so they still land on the right row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The "app looks frozen during worktree setup" cluster (inventory findings 270, 89, 90, 87):
thread.message.user.echocommand persists the user's message immediately afterthread.create— before git fetch, worktree add, and the setup script. The final turn start re-emits the same messageId; every projection dedupes by id. (The client-side optimistic echo PR keys the same id — one row across all three.)packages/contracts/src/threadSetup.tsdeclares the phase kinds (repository fetch, worktree create, setup script — started/completed/failed with duration + exit code).GitWorkflowService.createWorktreereports real phase boundaries (fetch events fire only when the remote is actually contacted); the setup script's exit code is observed via a completion marker in the terminal stream. The mac renders each phase as a tool-style row that replaces its running predecessor; failures show the exit status.CheckpointReactor's pre-turn baseline so the baseline still captures in the worktree, not the project checkout (test-proven).Deliberate skips, documented in commits: post-checkout hook is not narrated as its own phase (git runs it inside
worktree add; fabricating boundaries would be dishonest — worktree-create covers it), and baseline capture is not gated on script completion (needs its own design; the new completed/failed activity is the event it would key on). Mobile mapping for the new kinds noted as follow-up.Area
apps/mac— native macOS appapps/windowsapps/mobileapps/server— backend serverpackages/contracts)Release size
size:XSsize:Ssize:Msize:Lsize:XLVerification
pnpm run verify(vp check + typecheck + related tests + Swift suite): pass🤖 Generated with Claude Code