Skip to content

fix: a warm launch says what it skipped (#560 sections 1 and 2) - #561

Merged
blooop merged 3 commits into
mainfrom
fix/560-a-warm-launch-says-what-it-skipped
Sep 4, 2026
Merged

fix: a warm launch says what it skipped (#560 sections 1 and 2)#561
blooop merged 3 commits into
mainfrom
fix/560-a-warm-launch-says-what-it-skipped

Conversation

@blooop

@blooop blooop commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Addresses sections 1 and 2 of #560 — A warm attach reports neither a stale checkout nor a skipped setup pass, and an arm in a workspace id kills the launch with exit 126, and gives section 3 the diagnosis it can have without a design decision. Section 3's code change is deliberately not here; see the last part of this description for why.

The two sections in scope share the root the issue names: a launch of a workspace devpod already knows skips both the git work and the setup pass, both skips are deliberate and documented, and neither was visible at the moment it mattered.

1. A warm launch says how far behind its checkout is

dl owner/repo@branch against a workspace devpod already has now prints one line before the attach banner, when the checkout is behind:

Workspace devlaunch-main-3j1t: its checkout is 37 commits behind origin/main as this
workspace last fetched it, and a launch of a workspace devpod already has fetches
nothing. Run 'git fetch' inside it, or 'dl <workspace> rm' and launch again, if you
meant to run against the branch as it is now.

No network call, and the wording is scoped to what that buys. The count is git rev-list --left-right --count HEAD...refs/remotes/origin/<branch> against dl's own clone. A fetch on attach would have traded away the decision #144 settled and #149 / #150 built, to fix a reporting problem, and it was never needed: the fact was local the whole time. So the sentence says how the checkout stands against a ref of whatever age the last fetch left it, and claims nothing about the remote now.

The symmetric difference rather than HEAD..<ref>, so a diverged checkout is two facts: local commits are named as the reader's own ((and 3 of its own it has not pushed)) instead of being counted as staleness.

Five shapes stay silent, each pinned by a test:

  • a checkout that agrees with its ref (a hot attach gains no sentence for saying nothing is wrong)
  • one that is only ahead
  • a workspace whose clone is gone (one stat, no spawn)
  • dl <workspace-id> by bare name: Plan::Existing carries no triple, so there is no branch to name and no clone path to derive, and the triple the picker recovers is allowed to name the tab and nothing else
  • a warm resolution answered under an id metadata.json recorded rather than the derived one (#88's arm): the clone directory is a function of the derived id, and the recorded path is only readable by opening the store, which this arm may not do (#145)

cold.opens.get() == 0 is asserted on the reporting path, so a_launch_that_matches_its_own_record_attaches_and_reads_no_machinery keeps meaning what it meant.

And reset says it too, and still leaves the checkout alone. That was the half that cost the reporter a session: reset reads like the answer to an out-of-date checkout and is not one, because devpod's --reset removes a devpod-managed source and dl hands devpod its own local folder. reset_reports_the_stale_checkout_it_is_about_to_rebuild_over_and_leaves_it_alone asserts the line, the --reset flag, and that the clone is exactly as far behind afterwards as before, read out of the clone rather than taken on trust.

reset's help line and the README table stopped calling themselves a clean slate; docs/cli.md and docs/workspaces.md gained the sentence that rm is the only verb which refreshes git state.

2. Every devpod up says whether it forwarded dotfiles

dotfiles: https://github.com/you/dotfiles (devpod context options), passed to devpod up; devpod installs them when it creates the container.
dotfiles: none set in devpod context options, so this up asked for none. 'devpod context set-options DOTFILES_URL=<repo>' is the only place dl reads it from.

dl reads DOTFILES_URL from devpod context options and from nowhere else, and it forwarded or omitted the two flags in silence either way. That silence is what made the report a fortnight rather than an afternoon: three plausible causes and no observation to cut between them. an_up_says_which_dotfiles_it_asked_devpod_for_and_the_argv_agrees asserts the sentence and the devpod up argv in one test, so the printed line and the flags cannot drift.

Said on the up and only on the up: an attach that runs no up asked devpod for nothing either way, and a line there would suggest it had. docs/workspace-tools.md gained the section on where the setting comes from, including that context-options.json is dl's cache of devpod's answer rather than an input.

This is one new line on every up. Two goldens changed to carry it. The negative arm is the one that answers the reported case, so it is not gated on the positive.

3. Not in this PR, and why

The issue's own framing is that section 3 can be picked up on its own, and both halves of it need something this PR should not decide alone.

The upstream PR is a link, from a branch in blooop/devpod to skevetter/devpod, in a repository this change does not touch. That is yours to open.

The dl-side half rests on a premise that does not hold. The issue proposes matching devpod's stderr for inject agent plus exit status 126, citing clients/devpod.rs:331 as dl already reading devpod's stderr as it arrives. That reader is devpod::run_watching_stderr, and its only caller is flows/lifecycle/delete.rs. devpod up goes through devpod::run, which is a passthrough that captures nothing, deliberately: an image build's progress belongs on the user's terminal. Making the 126 legible as specified therefore means piping devpod up's stderr through dl on the main build path, which changes what devpod sees of the terminal and what it prints. That is a real trade on the path every cold launch takes, and it is not the trade the issue was weighing.

What is here instead is the diagnosis, in docs/cli.md: the uname -a glob, the one-line check for any running container, the docker cp that unblocks a container already in that state, and the note that a recreate wipes it. It also records that dl's own setup pass writes the workspace id into the container's hostname, so dl is one of the ways arm gets there.

Testing

  • cargo test --workspace, cargo clippy --locked --all-targets -- -D warnings, cargo fmt --check, and pytest test/ (707 passed) all clean.
  • One pre-existing failure, present on main in this devcontainer and untouched by this change: flows::provision::tests::the_probe_reports_the_config_facts_when_it_actually_runs expects claudemounts to be empty and this container's own mount table makes the scan answer /.
  • rust/devlaunch-core/public-api.api.txt was hand-edited, in sorted position for the four new LaunchNotice rows in each of the two paths the generator renders them at. cargo public-api needs a nightly toolchain the devcontainer does not carry, so the CI public-api job's diff is the first real check of it; if it disagrees, regenerating on a host is the fix.

New fixtures in launch_scenario.py: --stale-checkout (two commits on origin.git's main, fetched into the warm workspace's clone and not checked out) and --dotfiles (devpod context options naming a repository, in devpod's own nested shape rather than the flat shape dl's cache holds).

Divergence rows 33 and 34 record both new lines, since neither has a Python precedent.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dn4qJGhkW4KdwQnMuNSXsN

Summary by Sourcery

Make warm launches and provisioning diagnostics explicit without adding network fetches or changing the established warm-attach behavior.

New Features:

  • Report stale checkouts during warm workspace launches using the locally known remote-tracking ref without fetching.
  • Explain the dotfiles configuration forwarded to each devpod up.
  • Document the cause and recovery steps for agent injection failures caused by hostnames containing arm.

Bug Fixes:

  • Make warm launches and resets visibly communicate that they skip git refresh, including how far behind a checkout is.
  • Prevent dotfiles configuration from being forwarded without a corresponding diagnostic, and avoid forwarding a dotfiles script without a repository.

Enhancements:

  • Clarify that rm is the only workspace verb that refreshes git state and that reset recreates container resources without changing the checkout.

Documentation:

  • Update CLI, workspace, workspace-tools, README, changelog, and rewrite-plan documentation with checkout freshness, dotfiles configuration, reset semantics, and agent injection diagnosis.

Tests:

  • Add unit and integration coverage for checkout divergence reporting, silent cases, reset behavior, dotfiles diagnostics, matching command arguments, and new launch fixtures.

Two reports from #560, and they share a root: a launch of a
workspace devpod already knows skips both the git work and the setup pass, and
the terminal said nothing about either.

A warm launch of `owner/repo@branch` now says how far behind its checkout is,
when it is behind. The count comes out of dl's own clone -- one `rev-list`
against a local repository, no network -- so the claim is narrow on purpose:
how the checkout stands against the `origin/<branch>` that clone last fetched,
of whatever age, and never a claim about the remote now. A fetch on attach was
the other candidate and would have traded away devlaunch#144's decision to fix
a reporting problem. Silent when the counts agree, when the checkout is only
ahead, when there is no clone on disk, for a bare workspace name, and for a
warm resolution addressed by a recorded id rather than the derived one.

Every `devpod up` now says whether it forwarded dotfiles, naming the repository
and script it passed or saying that devpod's context options name none. dl
reads DOTFILES_URL from `devpod context options` and from nowhere else, and it
used to forward or omit the flags in silence either way -- which is what turned
one report of "the dotfiles never landed" into a fortnight of three plausible
causes and no observation to cut between them.

`reset`'s help line and the README table stopped saying "Clean slate: remove
everything, recreate", which reads as a promise about the checkout that reset
does not keep: `rm` is the only verb that refreshes git state, and docs/cli.md
and docs/workspaces.md now say so.

docs/cli.md also gained the diagnosis for `inject agent ... exit status 126`
(section 3 of the issue): devpod globs `uname -a` for `arm` to pick its agent
binary, and `uname -a` carries the container hostname. The match is devpod's
and is not fixed here.

Claude-Session: https://claude.ai/code/session_01Dn4qJGhkW4KdwQnMuNSXsN

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 7 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds local, no-network stale-checkout diagnostics to warm and reset launches, emits an auditable dotfiles-forwarding notice for every devpod up, and documents the arm-related agent-injection diagnosis while intentionally deferring the stderr-handling code change. It adds unit, integration, fixture, golden, and API-snapshot updates covering the new notice paths and their required silent cases.

Sequence diagram for a warm launch checkout freshness notice

sequenceDiagram
    participant User
    participant Launch
    participant WorkspaceClone
    participant Git
    participant Devpod

    User->>Launch: run owner/repo@branch
    Launch->>Devpod: status workspace
    Devpod-->>Launch: workspace already running
    Launch->>WorkspaceClone: checkout_freshness(repos_dir, workspace)
    WorkspaceClone->>Git: ahead_behind(clone, refs/remotes/origin/branch)
    Git-->>WorkspaceClone: rev-list counts
    WorkspaceClone-->>Launch: ahead and behind counts
    alt checkout is behind
        Launch-->>User: CheckoutBehind notice
    end
    Launch->>Devpod: ssh workspace
    Devpod-->>User: attach session
Loading

Sequence diagram for dotfiles forwarding during devpod up

sequenceDiagram
    participant Launch
    participant Devpod
    participant Notices

    Launch->>Devpod: context options --output json
    Devpod-->>Launch: DOTFILES_URL and script options
    Launch->>Notices: dotfiles_notice()
    alt DOTFILES_URL configured
        Launch->>Devpod: up with --dotfiles and optional --dotfiles-script
        Notices-->>Launch: DotfilesForwarded
    else DOTFILES_URL not configured
        Launch->>Devpod: up without dotfiles flags
        Notices-->>Launch: DotfilesNotConfigured
    end
    Launch-->>Notices: render notice
Loading

Flow diagram for checkout refresh semantics

flowchart LR
    Launch[Workspace command] --> Existing{Workspace devpod exists?}
    Existing -->|yes| Container[Act on existing container]
    Existing -->|no| Cold[Cold launch]
    Container --> GitCheck[Optional local checkout freshness check]
    GitCheck -->|behind| Notice[Report checkout behind last-fetched ref]
    GitCheck -->|agreeing, ahead, missing, or bare name| Silent[No freshness notice]
    Container --> Reset[restart, recreate, or reset]
    Reset --> Unchanged[Checkout remains unchanged]
    Cold --> Fetch[Fetch or clone git state]
    Rm[rm] --> Delete[Delete workspace and clone]
    Delete --> Fetch
Loading

File-Level Changes

Change Details Files
Report stale checkouts during warm launches without fetching or opening cold-launch machinery.
  • Add local symmetric-difference computation against the clone’s last fetched remote-tracking ref.
  • Emit a warning only for branch-addressed warm launches with commits behind, including locally ahead commits when diverged.
  • Keep reporting silent for missing clones, bare workspace IDs, recorded-ID mismatches, and non-stale checkouts.
  • Apply the same notice to reset while preserving the checkout and update reset documentation.
rust/devlaunch-core/src/clients/git.rs
rust/devlaunch-core/src/clients/git/tests.rs
rust/devlaunch-core/src/flows/workspace_clone.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/workspace_clone.rs
rust/dl/src/render.rs
rust/dl/tests/launch.rs
rust/dl/tests/launch_scenario.py
rust/dl/src/cli.rs
README.md
docs/cli.md
docs/workspaces.md
docs/rust-rewrite-plan.md
CHANGELOG.md
Make every devpod up disclose the dotfiles configuration it forwarded.
  • Add launch notices for configured and absent DOTFILES_URL values from devpod context options.
  • Emit the notice at the single up-argument construction path so it stays consistent with the forwarded argv.
  • Cover positive, negative, and failure-path output with integration fixtures and tests.
rust/devlaunch-core/src/flows/launch.rs
rust/dl/src/render.rs
rust/dl/tests/launch.rs
rust/dl/tests/launch_scenario.py
docs/workspace-tools.md
docs/rust-rewrite-plan.md
CHANGELOG.md
Document the known cause and recovery steps for devpod agent injection failures involving arm in workspace hostnames.
  • Add diagnosis of devpod’s uname-based arm substring match and its interaction with dl-written hostnames.
  • Document inspection and docker cp recovery commands, plus the limitation that recreate removes the workaround.
  • Explicitly leave the upstream and dl stderr-forwarding fix out of this PR.
docs/cli.md
CHANGELOG.md
Update public API metadata and user-facing release documentation for the new notices and semantics.
  • Record the four LaunchNotice API additions in sorted public API output.
  • Clarify that rm, not reset or other container verbs, refreshes git state.
  • Add changelog and rewrite-plan entries for the implemented behavior.
rust/devlaunch-core/public-api.api.txt
README.md
docs/cli.md
docs/workspaces.md
docs/rust-rewrite-plan.md
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.64286% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.89%. Comparing base (01f71c7) to head (a472802).

Files with missing lines Patch % Lines
rust/dl/src/render.rs 97.91% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 95.16% <99.64%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 95.16% <99.64%> (+0.02%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

# Conflicts:
#	CHANGELOG.md
#	rust/devlaunch-core/public-api.api.txt
…d a script

`up_args` pushed `--dotfiles-script` on DOTFILES_SCRIPT alone, and
`dotfiles_notice` branched on DOTFILES_URL alone. So

    devpod context set-options DOTFILES_SCRIPT=install.sh   # and no DOTFILES_URL

put `--dotfiles-script install.sh` in the argv and printed

    dotfiles: none set in devpod context options, so this up asked for none.

That is precisely the drift this notice exists to catch. Its own doc comment says
so -- derived from the same two readers rather than from the argv, because "the
disagreement worth catching is exactly a reader that stopped feeding the argv" --
and the notice was the half that was right.

Fixed at the argv rather than by teaching the notice a third state, because the
flag was meaningless: `--dotfiles-script` names a file *inside* what `--dotfiles`
cloned, so with no repository it asks devpod to run a script in a repository
nobody named. Nothing is lost by not sending it, and the two readings agree again
without a variant describing a configuration that cannot work.

The existing argv test sets both options, so it never saw this.

Claude-Session: https://claude.ai/code/session_01U4SpX4ixqQG5R8hErsPMot
@blooop
blooop merged commit b5d0300 into main Sep 4, 2026
15 checks passed
@blooop
blooop deleted the fix/560-a-warm-launch-says-what-it-skipped branch September 4, 2026 12:55
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