Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.57.0] - 2026-09-28

### Added

- **`aid resume` reopens an agent session in one command.** After a restart, `aid resume`
opens the workspace picker, starts the workspace, and starts the agent with its own
resume words: `claude --resume` and `codex resume` open their session pickers, and
`gemini --resume` reopens the latest session. The agent is started as a fresh `aid`
launch starts it, so full auto, Remote Control named after the workspace id, `--rm`
and `--no-remote` all still apply. `aid resume <workspace>` skips the workspace picker.

## [0.56.0] - 2026-09-28

### Added
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ one argument instead of a clone, a config file and a build command.
[![GitHub pull-requests merged](https://badgen.net/github/merged-prs/blooop/devlaunch)](https://github.com/blooop/devlaunch/pulls?q=is%3Amerged)
[![GitHub release](https://img.shields.io/github/release/blooop/devlaunch.svg)](https://GitHub.com/blooop/devlaunch/releases/)
[![PyPI](https://img.shields.io/pypi/v/devlaunch)](https://pypi.org/project/devlaunch/)
[![Conda](https://img.shields.io/badge/conda-v0.56.0-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
[![Conda](https://img.shields.io/badge/conda-v0.57.0-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
[![License](https://img.shields.io/github/license/blooop/devlaunch)](https://opensource.org/license/mit/)
[![Platform](https://img.shields.io/badge/platform-linux--64-blue)](https://github.com/blooop/devlaunch/releases)
[![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
Expand Down Expand Up @@ -341,7 +341,7 @@ clone, and [docs/cleanup.md](docs/cleanup.md) says what it carries one past and

```bash
$ dl --version
dl 0.56.0
dl 0.57.0
```

`--devcontainer <variant|path>` picks a non-default `devcontainer.json`. A bare name means
Expand Down Expand Up @@ -378,6 +378,7 @@ precedence order and what a profile does not change.

```bash
aid <user/repo>[@branch|@<pr-link>] [prompt...]
aid resume [<workspace>]
```

`aid` is a shortcut, not a second launcher. It rewrites its command line into a `dl` one, so
Expand Down Expand Up @@ -440,6 +441,12 @@ typed.

The agent's CLI has to be in the container already. `aid` runs it; it does not install it.

After a restart, `aid resume` is the way back into an agent session in one command. It
opens `dl`'s picker, starts the workspace you pick, and starts the agent with its own
resume picker, Remote Control and full auto included. `aid resume <workspace>` skips the
first picker. [docs/cli.md](docs/cli.md#aid-resume-back-into-a-session-after-a-restart)
has the words per agent.

## What every workspace gets

`dl` launches arbitrary repos, so none of this can depend on the image, and no repo has to add
Expand Down
42 changes: 42 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,48 @@ claude all take it offline immediately. The entry can sit in the claude.ai list
roughly 4 hours after that before it clears, which is the web side timing out rather
than anything still running on your machine.

## `aid resume`: back into a session after a restart

```bash
aid resume [<workspace>]
```

A machine that turns off ends every agent session and stops every container. The
conversation is not lost: the agent keeps it on disk inside the container, and a
stopped container keeps its disk.
`aid resume` is the way back in, in one command. With no workspace it opens the same
picker `dl` does. Then it starts the workspace and hands the agent its own resume
words, so for claude it is exactly

```bash
dl <picked-id> -- CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 IS_SANDBOX=1 claude --dangerously-skip-permissions --remote-control=<picked-id> --resume
```

and claude's picker lists the sessions that were run in that workspace. Everything a
fresh `aid` launch does still happens: the same full-auto flag, the same Remote Control
name, `--no-remote` and `--rm` from either end of the line, and the agent flags. The
words per agent are these:

| Agent | Words | What the agent does |
|---|---|---|
| `claude` | `--resume` | Opens its session picker |
| `codex` | `resume` | Opens its session picker |
| `gemini` | `--resume` | Reopens its latest session. Its picker is `/resume`, inside the session |

The claude row was checked against `claude --help`. The codex and gemini rows follow
those CLIs' documentation and were not run.

`resume` is a verb only in the first positional slot, the way `dl stop` is. After the
workspace it is prompt text like any other word, so `aid owner/repo resume the work`
still sends a prompt. A workspace that is itself called `resume` is `aid resume
resume`. Words after the workspace on a resume line are refused before anything
boots, because the three agents would each read them as something different: a
search term, a session id, a session index.

The picker is asked for before the line is built, not handed to `dl -- <command>` to
open. The line names the Remote Control session after the workspace id, and the id is
not known until the pick is made.

## `kill`: the workspace that will not answer

`dl <ws> stop` asks devpod to stop a workspace, and it is the right thing to type
Expand Down
11 changes: 6 additions & 5 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
# The single source of the version (docs/rust-rewrite-plan.md: cutover ships
# 0.1.0, version read from Cargo.toml).
[workspace.package]
version = "0.56.0"
version = "0.57.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/blooop/devlaunch"
Expand Down
2 changes: 2 additions & 0 deletions rust/aid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dl = { path = "../dl" }
dev-build = ["dl/dev-build"]

[dev-dependencies]
# `setsid` for the tests that need `aid` to run with no controlling terminal.
libc = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
# The harness crate, and only ever that: `KeepingCoverage`, so the boundary tests
Expand Down
41 changes: 37 additions & 4 deletions rust/aid/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@
// No arguments is the help *and* a failure, which is Python's pair of endings for
// one body: somebody who typed `aid` asked for a workspace and named none, and
// somebody who typed `aid --help` got what they asked for.
let asked_for_help = argv
.first()
.is_some_and(|word| word == "--help" || word == "-h");
let is_help = |word: &String| word == "--help" || word == "-h";
let asked_for_help = match argv {
[first, second, ..] if first == rewrite::RESUME_WORD => is_help(second),
[first, ..] => is_help(first),
[] => false,
};
if argv.is_empty() || asked_for_help {
print!("{}", help());
return if argv.is_empty() { 1 } else { 0 };
Expand Down Expand Up @@ -139,7 +142,14 @@
remote_control: remote_control.as_deref(),
};
let parsed = match rewrite::parse_aid_args(argv, environment) {
Ok(parsed) => parsed,
Ok(rewrite::Line::Ready(parsed)) => parsed,
// `aid resume` with no workspace. The pick comes before everything below,
// which is all about one named workspace, so from here on this line is an
// `aid resume <id>` like any other.
Ok(rewrite::Line::Unpicked(unpicked)) => match dl::pick_workspace() {
Ok(workspace_id) => unpicked.picked(workspace_id),

Check warning on line 150 in rust/aid/src/main.rs

View check run for this annotation

Codecov / codecov/patch

rust/aid/src/main.rs#L150

Added line #L150 was not covered by tests
Err(code) => return code,
},
Err(refused) => {
eprintln!("{}", refusal(&refused));
return 1;
Expand Down Expand Up @@ -260,6 +270,11 @@
dl::python_repr(value),
rewrite::remote_control_values().join(", ")
),
UsageError::ResumeTakesNoPrompt { words } => format!(
"aid resume takes a workspace and nothing after it, not {}: the agent's own \
picker chooses the session. Use aid resume [<workspace>].",
dl::python_repr(words)
),
}
}

Expand Down Expand Up @@ -293,6 +308,14 @@
Usage:
aid <user/repo>[@branch] [prompt...] Open the workspace and start the agent
aid <workspace> [prompt...] Same, for an existing workspace or ./path
aid resume [<workspace>] Reopen an earlier agent session in the
workspace. With no workspace, pick one
from dl's picker first

`aid resume` starts the agent the way a fresh launch does (same agent flag,
full-auto, Remote Control named after the workspace) and hands it its own
resume words: claude and codex open their session picker, and gemini
reopens its latest session.

With no prompt on a terminal, aid boots the workspace in the background and
asks for the prompt while it does: type it free of shell quoting and press
Expand Down Expand Up @@ -348,6 +371,8 @@
aid blooop/devlaunch@fix/42 fix the bug # Open the branch, hand over the prompt
aid --gemini ./my-project explain this # Pick a different agent
aid --no-remote blooop/devlaunch # Nothing but the session in front of you
aid resume # Pick a workspace, then a session in it
aid resume blooop/devlaunch@fix/42 # Pick a session in that workspace
aid blooop/devlaunch@fix/42 fix the bug --rm
# The line above, recalled, with the
# workspace deleted once the agent is
Expand Down Expand Up @@ -538,6 +563,14 @@
assert!(help.contains("DEVLAUNCH_NO_TTY=1"), "{help}");
}

#[test]
fn the_help_names_resume_and_what_it_opens() {
let help = help();

assert!(help.contains("aid resume [<workspace>]"), "{help}");
assert!(help.contains("from dl's picker"), "{help}");
}

#[test]
fn the_help_names_remote_control_and_what_it_needs() {
let help = help();
Expand Down
Loading
Loading