Skip to content

Commit 6bf1653

Browse files
committed
Add option to run agent tasks without cli interaction
1 parent c9ae95d commit 6bf1653

14 files changed

Lines changed: 803 additions & 36 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ vp run codex
4545
vp run vibe # alias of devstral
4646
```
4747

48+
Run in the background and retrieve collected output by task ID:
49+
50+
```bash
51+
vp run <agent> --detached --prompt "Write TEST.md"
52+
vp logs show <task-id>
53+
```
54+
55+
Use `--prompt` to run a single non-interactive task. VibePod maps it to each
56+
agent's documented prompt mode.
57+
4858
Extra arguments after the agent are forwarded to the agent process. Use `--`
4959
before agent flags so VibePod does not parse them as its own options:
5060

docs/agents/index.md

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,24 @@ agents:
153153
MY_VAR: value
154154
```
155155

156-
## Passing arguments to the agent
156+
## Running one prompt
157+
158+
Use `--prompt` to run a single task in the selected agent's non-interactive mode:
159+
160+
```bash
161+
vp run codex --prompt "Write TEST.md"
162+
vp run claude --prompt "Summarize this repository"
163+
```
164+
165+
Combine it with detached mode to run the same non-interactive task in the background:
166+
167+
```bash
168+
vp run codex --detached --prompt "Write TEST.md"
169+
```
170+
171+
VibePod maps `--prompt` to each agent's documented prompt mode (`claude -p`, `codex exec`, `opencode run`, and so on).
172+
173+
## Passing raw arguments to the agent
157174

158175
Any extra arguments after the agent name are appended to the agent command inside the container:
159176

@@ -206,21 +223,36 @@ vp run codex --ikwid
206223

207224
## Detached mode
208225

209-
Use `-d` / `--detach` to start an agent container in the background without attaching your terminal. The agent process starts immediately inside the container — `-d` only controls whether VibePod attaches your terminal to it.
226+
Use `-d`, `--detach`, or `--detached` to start an agent container in the background without attaching your terminal. The agent process starts immediately inside the container and VibePod returns a task ID that can be used to retrieve logs later.
210227

211228
### Basic usage
212229

213230
```bash
214-
vp run claude -d
215-
# ✓ Started vibepod-claude-a1b2c3d4
231+
vp run codex --detached --prompt "Write TEST.md"
232+
# ✓ Started vibepod-codex-a1b2c3d4
233+
# ✓ Task ID: 9f2c7d4e8a1b4c6f9a0d2e3f4b5c6d7e
216234
```
217235

218-
The command prints the container name and returns immediately. You can also find it later with:
236+
The command prints the container name and task ID, then returns immediately. You can also find running task IDs later with:
219237

220238
```bash
221239
vp list --running
222240
```
223241

242+
### Retrieving detached output
243+
244+
Detached container output is collected in the local VibePod logs database while the task runs.
245+
246+
```bash
247+
vp logs show 9f2c7d4e8a1b4c6f9a0d2e3f4b5c6d7e
248+
```
249+
250+
For a still-running task, you can attach by task ID:
251+
252+
```bash
253+
vp logs attach 9f2c7d4e8a1b4c6f9a0d2e3f4b5c6d7e
254+
```
255+
224256
### Interacting with a detached container
225257

226258
The agent is already running inside the container. You can exec into it to inspect state, install extra tools, or interact with the agent alongside its running process:
@@ -254,9 +286,8 @@ vp stop --all # stop every VibePod container
254286

255287
### Caveats
256288

257-
- **`auto_remove` (default: `true`)** — By default, containers are automatically removed when they stop. This means you cannot restart a stopped detached container; you need to `vp run` again. Set `auto_remove: false` in your [configuration](../configuration.md) if you want stopped containers to persist.
258-
- **No built-in re-attach** — VibePod does not currently have a command to re-attach your terminal to a detached container. Use `docker attach <container>` or `docker exec -it <container> bash` directly.
259-
- **Session logging** — Sessions started with `--detach` are not recorded in the VibePod session log since VibePod does not capture the interactive I/O. If you need session logging, run without `--detach`.
289+
- **`auto_remove` (default: `true`)** — Foreground containers are automatically removed when they stop by default. Detached containers are kept so their Docker logs remain available through the task ID.
290+
- **Session logging disabled** — If `logging.enabled` is `false`, VibePod still starts the detached container and prints a task ID, but it does not collect detached output in SQLite.
260291

261292
## Connecting to a Docker Compose network
262293

@@ -464,3 +495,15 @@ vp run copilot # or: vp p
464495
```bash
465496
vp run codex # or: vp x
466497
```
498+
499+
For non-interactive detached tasks, use Codex's `exec` subcommand:
500+
501+
```bash
502+
vp run codex --detached -- exec "Write a TEST.md file with details to our test strategy"
503+
```
504+
505+
For unattended edits, combine it with IKWID mode:
506+
507+
```bash
508+
vp run codex --detached --ikwid -- exec "Write a TEST.md file with details to our test strategy"
509+
```

docs/quickstart.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ Use `-w` / `--workspace` to target any directory:
5353
vp run claude -w ~/other-project
5454
```
5555

56-
## Pass arguments to the agent
56+
## Run one prompt
5757

58-
Arguments after the agent name are forwarded to the agent command inside the container:
58+
Use `--prompt` to run a single non-interactive task. VibePod maps it to the selected agent's documented prompt mode:
5959

6060
```bash
61-
vp run <agent> <agent-args>
61+
vp run codex --prompt "Write TEST.md"
6262
```
6363

64-
When forwarding flags to the agent, use `--` to stop VibePod option parsing:
64+
For raw agent-specific arguments, use `--` to stop VibePod option parsing:
6565

6666
```bash
6767
vp run <agent> -- <agent-flag> <value>
@@ -91,10 +91,16 @@ If that agent is already configured under `agents`, the command exits without ch
9191

9292
## Run in the background
9393

94-
Use `-d` / `--detach` to start the container without attaching your terminal:
94+
Use `-d`, `--detach`, or `--detached` to start the container without attaching your terminal:
9595

9696
```bash
97-
vp run claude -d
97+
vp run codex --detached --prompt "Write TEST.md"
98+
```
99+
100+
Detached runs print a task ID. Use it to inspect collected output later:
101+
102+
```bash
103+
vp logs show <task-id>
98104
```
99105

100106
Check which agents are running:

src/vibepod/cli.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,15 @@ def _alias(
4040
bool, typer.Option("--pull", help="Pull latest image before run")
4141
] = False,
4242
detach: Annotated[
43-
bool, typer.Option("-d", "--detach", help="Run container in background")
43+
bool, typer.Option("-d", "--detach", "--detached", help="Run container in background")
4444
] = False,
45+
prompt: Annotated[
46+
str | None,
47+
typer.Option(
48+
"--prompt",
49+
help="Run a single prompt in the agent's non-interactive mode",
50+
),
51+
] = None,
4552
env: Annotated[
4653
list[str] | None,
4754
typer.Option("-e", "--env", help="Environment variable KEY=VALUE", show_default=False),
@@ -76,6 +83,7 @@ def _alias(
7683
workspace=workspace,
7784
pull=pull,
7885
detach=detach,
86+
prompt=prompt,
7987
env=env,
8088
name=name,
8189
network=network,

src/vibepod/commands/list_cmd.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def _running_rows(containers: list[Any]) -> list[dict[str, str]]:
3939
"agent": agent,
4040
"container": getattr(container, "name", "-"),
4141
"context": labels.get("vibepod.workspace", "-"),
42+
"task_id": labels.get("vibepod.session_id", "-"),
4243
}
4344
)
4445
return sorted(rows, key=lambda row: (row["agent"], row["container"]))
@@ -75,11 +76,12 @@ def list_agents(
7576
running_table = Table(title="Running Agents", title_justify="left")
7677
running_table.add_column("AGENT", style="cyan")
7778
running_table.add_column("CONTAINER", style="magenta")
79+
running_table.add_column("TASK ID")
7880
running_table.add_column("CONTEXT")
7981

8082
if running_rows:
8183
for row in running_rows:
82-
running_table.add_row(row["agent"], row["container"], row["context"])
84+
running_table.add_row(row["agent"], row["container"], row["task_id"], row["context"])
8385
console.print(running_table)
8486
else:
8587
console.print("No running agents.")

src/vibepod/commands/logs.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
import urllib.request
88
import webbrowser
99
from pathlib import Path
10-
from typing import Annotated
10+
from typing import Annotated, Any
1111

1212
import typer
1313

1414
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING
1515
from vibepod.core.config import get_config
1616
from vibepod.core.docker import DockerClientError, DockerManager, _is_latest_tag
17+
from vibepod.core.session_logger import SessionLogger
1718
from vibepod.utils.console import error, info, success, warning
1819

1920
app = typer.Typer(help="View logs and traffic UI")
@@ -37,6 +38,11 @@ def _wait_for_datasette(port: int) -> bool:
3738
return False
3839

3940

41+
def _log_db_path(config: dict[str, Any]) -> Path:
42+
log_cfg = config.get("logging", {})
43+
return Path(str(log_cfg.get("db_path", "~/.config/vibepod/logs.db"))).expanduser().resolve()
44+
45+
4046
@app.command("start")
4147
def logs_start(
4248
port: Annotated[int | None, typer.Option("--port", help="Datasette host port")] = None,
@@ -125,6 +131,108 @@ def logs_status() -> None:
125131
info(f"Datasette container: {existing.name} ({existing.status})")
126132

127133

134+
@app.command("show")
135+
def logs_show(
136+
task_id: Annotated[str, typer.Argument(help="Task/run ID returned by `vp run --detach`")],
137+
) -> None:
138+
"""Print persisted logs for a detached task."""
139+
db_path = _log_db_path(get_config())
140+
session = SessionLogger.get_session(db_path, task_id)
141+
if session is None:
142+
error(f"Unknown task ID: {task_id}")
143+
raise typer.Exit(1)
144+
145+
outputs = SessionLogger.get_outputs(db_path, task_id)
146+
if outputs:
147+
for row in outputs:
148+
print(row["content"], end="")
149+
return
150+
151+
try:
152+
docker_logs = DockerManager().container_logs(str(session["container_id"]))
153+
except DockerClientError as exc:
154+
warning(f"No persisted output has been collected for this task: {exc}")
155+
return
156+
157+
if docker_logs:
158+
print(docker_logs, end="")
159+
return
160+
161+
info("No output has been collected for this task yet.")
162+
163+
164+
@app.command("attach")
165+
def logs_attach(
166+
task_id: Annotated[str, typer.Argument(help="Task/run ID returned by `vp run --detach`")],
167+
) -> None:
168+
"""Attach to a running detached task by task ID."""
169+
db_path = _log_db_path(get_config())
170+
session = SessionLogger.get_session(db_path, task_id)
171+
if session is None:
172+
error(f"Unknown task ID: {task_id}")
173+
raise typer.Exit(1)
174+
175+
try:
176+
manager = DockerManager()
177+
container = manager.get_container(str(session["container_id"]))
178+
except DockerClientError as exc:
179+
error(str(exc))
180+
raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc
181+
182+
info(f"Attached to {session['container_name']}. Use Ctrl+C to detach/stop.")
183+
try:
184+
manager.attach_interactive(container)
185+
except KeyboardInterrupt:
186+
info("Detached")
187+
188+
189+
@app.command("collect", hidden=True)
190+
def logs_collect(
191+
task_id: Annotated[str, typer.Argument(help="Task/run ID")],
192+
db_path: Annotated[Path | None, typer.Option("--db-path", help="Logs DB path")] = None,
193+
) -> None:
194+
"""Persist Docker logs for a detached task until the container exits."""
195+
config = get_config()
196+
resolved_db_path = (
197+
db_path.expanduser().resolve() if db_path is not None else _log_db_path(config)
198+
)
199+
session = SessionLogger.get_session(resolved_db_path, task_id)
200+
if session is None:
201+
raise typer.Exit(1)
202+
203+
try:
204+
manager = DockerManager()
205+
container = manager.get_container(str(session["container_id"]))
206+
for chunk in container.logs(stream=True, follow=True, stdout=True, stderr=True):
207+
if isinstance(chunk, bytes):
208+
content = chunk.decode("utf-8", errors="replace")
209+
else:
210+
content = str(chunk)
211+
SessionLogger.append_output(
212+
resolved_db_path,
213+
session_id=task_id,
214+
content=content,
215+
)
216+
try:
217+
result = container.wait()
218+
status_code = result.get("StatusCode") if isinstance(result, dict) else None
219+
exit_reason = f"exit_{status_code}" if status_code is not None else "normal"
220+
except Exception:
221+
exit_reason = "normal"
222+
SessionLogger.close_session_by_id(
223+
resolved_db_path,
224+
session_id=task_id,
225+
exit_reason=exit_reason,
226+
)
227+
except Exception:
228+
SessionLogger.close_session_by_id(
229+
resolved_db_path,
230+
session_id=task_id,
231+
exit_reason="collector_error",
232+
)
233+
raise typer.Exit(1)
234+
235+
128236
@app.command("ui", hidden=True)
129237
def logs_ui(
130238
port: Annotated[int | None, typer.Option("--port", help="Datasette host port")] = None,

0 commit comments

Comments
 (0)