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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to `devloop` will be recorded in this file.

## [Unreleased]

## [0.9.3] - 2026-07-22

### Changed
- Clarified how to watch sibling directories by setting `root` to their
common parent, and documented the `restart = "never"` default plus the
wrapper exit-status behavior of `on_failure`.

### Fixed
- Made the cross-platform runtime smoke test use output-derived state
readiness instead of a fixture-local HTTP server, removing its flaky
loopback startup dependency while preserving workflow and watch coverage.

## [0.9.2] - 2026-07-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion 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 Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "devloop"
version = "0.9.2"
version = "0.9.3"
edition = "2024"

[dependencies]
Expand Down
6 changes: 6 additions & 0 deletions docs/behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ Managed processes are long-running child commands.
`devloop` is shutting down.
- `restart = "on_failure"` restarts only after unsuccessful exit.
- `restart = "never"` never restarts automatically.
- Restart policies use the managed command's exit status. A wrapper that
exits successfully after its long-running child dies is not restarted by
`on_failure`; the service remains down. For development-server wrappers,
use `restart = "always"` or propagate the child's exit status. A liveness
probe can detect a dead child only while its wrapper remains running; a
readiness probe only checks startup and does not supervise ongoing health.
- Managed child processes inherit the ambient environment unless the
process config explicitly overrides individual variables such as
`env.RUST_LOG`.
Expand Down
21 changes: 20 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ always watching the whole repository root recursively. `native` remains
the default backend; `poll` exists as a fallback for environments where
filesystem notifications are unreliable.

### Watching a sibling directory

Watch patterns cannot match paths outside `root`. If the configuration is
in `devloop/` and you need to watch its sibling `skills/` directory, make
their common parent the root and use a root-relative path:

```toml
root = ".."

[watch.skills]
paths = ["skills/"]
workflow = "skills"
```

`root` also resolves relative process commands, `cwd` values, and the
default state-file location. Adjust those paths if they were previously
relative to `devloop/`.

## Processes

Processes are long-running commands supervised by `devloop`.
Expand All @@ -96,7 +114,8 @@ output = { inherit = true, body_style = "plain" }
- `cwd`: working directory for the process. Relative paths are resolved
from `root`.
- `autostart`: whether to start the process before startup workflows.
- `restart`: one of `never`, `on_failure`, or `always`.
- `restart`: one of `never`, `on_failure`, or `always`. Defaults to
`never`, so a process without this key is not automatically restarted.
- `env`: extra environment variables for the process.
- `readiness`: optional readiness probe.
- `liveness`: optional liveness probe.
Expand Down
12 changes: 6 additions & 6 deletions fixtures/ci-smoke/devloop.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ paths = ["watched.txt"]
workflow = "content"

[process.server]
command = ["python3", "-m", "http.server", "18081", "--bind", "127.0.0.1"]
command = ["./scripts/emit-ready.sh"]
cwd = "."
autostart = false
restart = "never"
output = { inherit = false }
restart = "always"
output = { inherit = true, rules = [{ state_key = "server_ready", pattern = "^(ready)$", extract = "regex" }] }

[process.server.readiness]
kind = "http"
url = "http://127.0.0.1:18081/"
kind = "state_key"
key = "server_ready"
interval_ms = 100
timeout_ms = 5000

Expand All @@ -38,6 +38,6 @@ steps = [
[workflow.content]
steps = [
{ action = "run_hook", hook = "current_value" },
{ action = "write_state", key = "current_url", value = "http://127.0.0.1:18081/{{current_value}}" },
{ action = "write_state", key = "current_url", value = "devloop://{{current_value}}" },
{ action = "log", message = "changed value: {{current_value}}" },
]
5 changes: 5 additions & 0 deletions fixtures/ci-smoke/scripts/emit-ready.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail

printf 'ready\n'
exec tail -f /dev/null
17 changes: 3 additions & 14 deletions scripts/ci-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,7 @@ trap dump_log ERR

cp -R "${fixture_src}/." "${tmp_dir}/"
chmod +x "${tmp_dir}/scripts/read-watched.sh"
smoke_port="$(
python3 - <<'PY'
import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
PY
)"
sed -i.bak "s/18081/${smoke_port}/g" "${tmp_dir}/devloop.toml"
rm -f "${tmp_dir}/devloop.toml.bak"
chmod +x "${tmp_dir}/scripts/emit-ready.sh"

state_path="${tmp_dir}/.devloop/state.json"
devloop_bin="${repo_root}/target/debug/devloop"
Expand Down Expand Up @@ -118,7 +108,7 @@ while time.time() < deadline:
raise SystemExit("timed out waiting for watcher startup")
PY

python3 - "$state_path" "$log_path" "${tmp_dir}/watched.txt" "${smoke_port}" <<'PY'
python3 - "$state_path" "$log_path" "${tmp_dir}/watched.txt" <<'PY'
import json
import pathlib
import sys
Expand All @@ -127,7 +117,6 @@ import time
state_path = pathlib.Path(sys.argv[1])
log_path = pathlib.Path(sys.argv[2])
watched_path = pathlib.Path(sys.argv[3])
smoke_port = sys.argv[4]
deadline = time.time() + 15
next_write = 0.0
while time.time() < deadline:
Expand All @@ -139,7 +128,7 @@ while time.time() < deadline:
data = json.loads(state_path.read_text())
if (
data.get("current_value") == "updated"
and data.get("current_url") == f"http://127.0.0.1:{smoke_port}/updated"
and data.get("current_url") == "devloop://updated"
):
if "changed value: updated" in log_path.read_text():
sys.exit(0)
Expand Down