Skip to content

feat: narrate worktree setup — echo first, typed phase progress, setup script on the eager path - #452

Merged
SergeSerb2 merged 6 commits into
mainfrom
surgecode/worktree-setup-progress
Aug 2, 2026
Merged

feat: narrate worktree setup — echo first, typed phase progress, setup script on the eager path#452
SergeSerb2 merged 6 commits into
mainfrom
surgecode/worktree-setup-progress

Conversation

@SergeSerb2

Copy link
Copy Markdown
Owner

Summary

The "app looks frozen during worktree setup" cluster (inventory findings 270, 89, 90, 87):

  • Echo before provisioning: a new internal thread.message.user.echo command persists the user's message immediately after thread.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.)
  • Typed setup progress: packages/contracts/src/threadSetup.ts declares the phase kinds (repository fetch, worktree create, setup script — started/completed/failed with duration + exit code). GitWorkflowService.createWorktree reports 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.
  • Event ordering (finding 89): setup activities clamp to (message createdAt + 1ms) so live order == replay order; test-asserted.
  • Eager path runs the setup script (finding 87): eager provisioning (worktree attached via meta-update before the first turn) previously skipped the setup script entirely — the agent landed in a worktree with no dependencies and zero indication. It now runs the same setup program with the same typed activities; re-materializing a used thread is excluded.
  • Baseline-capture hazard closed: the provisional echo is skipped by 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 app
  • apps/windows
  • apps/mobile
  • apps/server — backend server
  • Shared packages (packages/contracts)
  • Build, CI, or release tooling
  • Docs

Release size

  • size:XS
  • size:S
  • size:M
  • size:L
  • size:XL

Verification

  • pnpm run verify (vp check + typecheck + related tests + Swift suite): pass
  • server.test.ts 109/109 (2 new eager-provisioning tests, ordering assertion), CheckpointReactor 14/14 (+1), ProjectSetupScriptRunner 6/6 (+3), new marker/activities/decider/contracts suites, Swift ActivityRowTests extended

🤖 Generated with Claude Code

SergeSerb2 and others added 6 commits August 2, 2026 15:54
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>
@SergeSerb2 SergeSerb2 added the size:M Normal feature or meaningful behavior change label Aug 2, 2026
@SergeSerb2
SergeSerb2 merged commit 8e19cae into main Aug 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M Normal feature or meaningful behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant