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

## [Unreleased]

## [0.11.1] - 2026-09-02

### Fixed

- Made transactional artifact guidance discoverable from root help, the bare
`devloop docs` index, and artifact validation errors.
- Made the runtime smoke test select its required informational log level
instead of inheriting a caller setting that could hide its readiness signal.

## [0.11.0] - 2026-09-02

### Added
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.11.0"
version = "0.11.1"
edition = "2024"

[dependencies]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The tool will:
Built-in reference docs are also available from the CLI:

```bash
devloop docs
devloop docs config
devloop docs behavior
devloop docs artifacts
Expand Down
29 changes: 18 additions & 11 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Docs
# Devloop Documentation

- [Behavior Reference](behavior.md)
- [Configuration Reference](configuration.md)
- [Transactional Artifact Generations](artifacts.md)
- [Development Guide](development.md)
- [Security Notes](security.md)
Run `devloop docs` to print this index in a terminal. Choose a topic according
to the work in front of you:

This directory holds detailed reference material for `devloop`.
Keep the top-level `README.md` focused on purpose, installation, and the
main development loop; put configuration detail here. Session-log behaviour
is documented in the Behavior Reference, and its path follows the state-file
configuration in the Configuration Reference.
- [Configuration Reference](configuration.md) — run `devloop docs config` when
writing or validating `devloop.toml`.
- [Behavior Reference](behavior.md) — run `devloop docs behavior` when reasoning
about supervision, workflows, state, or failure handling.
- [Transactional Artifact Generations](artifacts.md) — run
`devloop docs artifacts` when a build replaces files that a managed process
is serving.
- [Development Guide](development.md) — run `devloop docs development` when
changing or validating devloop itself.
- [Security Notes](security.md) — run `devloop docs security` when configuring
control surfaces, tokens, hooks, or inherited environments.

This directory holds devloop's detailed reference material. The top-level
README remains a synopsis of the tool, installation, and the main development
loop.
2 changes: 1 addition & 1 deletion scripts/ci-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ devloop_bin="${repo_root}/target/debug/devloop"

(cd "${repo_root}" && cargo build --bins >/dev/null)

"${devloop_bin}" run --config "${tmp_dir}/devloop.toml" >"${log_path}" 2>&1 &
RUST_LOG=info "${devloop_bin}" run --config "${tmp_dir}/devloop.toml" >"${log_path}" 2>&1 &
devloop_pid=$!
start_watchdog

Expand Down
7 changes: 4 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ impl Config {
}
}
for (name, artifact) in &self.artifact {
artifact
.validate(self, name)
.with_context(|| format!("invalid artifact '{name}'"))?;
artifact.validate(self, name).with_context(|| {
format!("invalid artifact '{name}'; for guidance, run `devloop docs artifacts`")
})?;
}
self.event_server.validate()?;
self.browser_reload_server.validate()?;
Expand Down Expand Up @@ -1094,6 +1094,7 @@ steps = [{ action = "notify_reload" }]
.expect_err("artifact consumer must not start before recovery");

assert!(format!("{error:#}").contains("must set autostart = false"));
assert!(format!("{error:#}").contains("devloop docs artifacts"));
}

#[test]
Expand Down
67 changes: 50 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const SESSION_LOG_SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
author,
version,
about = "Run config-driven local development workflows",
long_about = "devloop watches a client repository, supervises its processes, and executes ordered workflows defined in a TOML config file."
long_about = "devloop watches a client repository, supervises its processes, and executes ordered workflows defined in a TOML config file.",
after_help = "Serving generated files that are replaced during rebuilds? Use transactional artifacts. Run `devloop docs artifacts`."
)]
struct Cli {
#[command(subcommand)]
Expand All @@ -65,7 +66,7 @@ enum Command {
/// Print built-in reference documentation.
Docs {
#[arg(value_enum)]
topic: DocsTopic,
topic: Option<DocsTopic>,
},
}

Expand Down Expand Up @@ -322,17 +323,18 @@ fn resolve_config_path(config: Option<PathBuf>) -> Result<PathBuf> {
}
}

fn docs_text(topic: DocsTopic) -> &'static str {
fn docs_text(topic: Option<DocsTopic>) -> &'static str {
match topic {
DocsTopic::Config => include_str!("../docs/configuration.md"),
DocsTopic::Behavior => include_str!("../docs/behavior.md"),
DocsTopic::Artifacts => include_str!("../docs/artifacts.md"),
DocsTopic::Development => include_str!("../docs/development.md"),
DocsTopic::Security => include_str!("../docs/security.md"),
None => include_str!("../docs/README.md"),
Some(DocsTopic::Config) => include_str!("../docs/configuration.md"),
Some(DocsTopic::Behavior) => include_str!("../docs/behavior.md"),
Some(DocsTopic::Artifacts) => include_str!("../docs/artifacts.md"),
Some(DocsTopic::Development) => include_str!("../docs/development.md"),
Some(DocsTopic::Security) => include_str!("../docs/security.md"),
}
}

fn render_docs_text(topic: DocsTopic) -> String {
fn render_docs_text(topic: Option<DocsTopic>) -> String {
render_markdown_for_terminal(docs_text(topic))
}

Expand Down Expand Up @@ -524,7 +526,7 @@ mod tests {
};
use crate::session_log::SessionLog;
use crate::test_support::RustLogGuard;
use clap::Parser;
use clap::{CommandFactory, Parser};
use tempfile::tempdir;

#[test]
Expand Down Expand Up @@ -639,7 +641,7 @@ mod tests {

#[test]
fn docs_text_uses_embedded_configuration_reference() {
let rendered = docs_text(DocsTopic::Config);
let rendered = docs_text(Some(DocsTopic::Config));

assert!(rendered.starts_with("# Configuration Reference"));
assert!(rendered.contains("startup_workflows"));
Expand All @@ -649,7 +651,7 @@ mod tests {

#[test]
fn docs_text_uses_embedded_session_log_behavior_reference() {
let rendered = docs_text(DocsTopic::Behavior);
let rendered = docs_text(Some(DocsTopic::Behavior));

assert!(rendered.starts_with("# Behavior Reference"));
assert!(rendered.contains("### Session logs"));
Expand All @@ -658,15 +660,15 @@ mod tests {

#[test]
fn docs_text_uses_embedded_development_reference() {
let rendered = docs_text(DocsTopic::Development);
let rendered = docs_text(Some(DocsTopic::Development));

assert!(rendered.starts_with("# Development Guide"));
assert!(rendered.contains("DEVLOOP_RUN_WATCH_FLAKE_SMOKE"));
}

#[test]
fn docs_text_exposes_agent_safe_artifact_workflow() {
let rendered = docs_text(DocsTopic::Artifacts);
let rendered = docs_text(Some(DocsTopic::Artifacts));

assert!(rendered.starts_with("# Transactional Artifact Generations"));
assert!(rendered.contains("## Agent rule"));
Expand All @@ -676,7 +678,7 @@ mod tests {

#[test]
fn rendered_docs_drop_markdown_heading_markers() {
let rendered = render_docs_text(DocsTopic::Config);
let rendered = render_docs_text(Some(DocsTopic::Config));

assert!(rendered.starts_with("CONFIGURATION REFERENCE"));
assert!(!rendered.contains("# Configuration Reference"));
Expand Down Expand Up @@ -712,7 +714,9 @@ mod tests {
let cli = Cli::try_parse_from(["devloop", "docs", "security"]).expect("parse cli");

match cli.command {
super::Command::Docs { topic } => assert!(matches!(topic, DocsTopic::Security)),
super::Command::Docs { topic } => {
assert!(matches!(topic, Some(DocsTopic::Security)))
}
_ => panic!("expected docs subcommand"),
}
}
Expand All @@ -722,8 +726,37 @@ mod tests {
let cli = Cli::try_parse_from(["devloop", "docs", "development"]).expect("parse cli");

match cli.command {
super::Command::Docs { topic } => assert!(matches!(topic, DocsTopic::Development)),
super::Command::Docs { topic } => {
assert!(matches!(topic, Some(DocsTopic::Development)))
}
_ => panic!("expected docs subcommand"),
}
}

#[test]
fn cli_root_help_points_agents_to_artifact_guidance() {
let help = Cli::command().render_long_help().to_string();

assert!(help.contains("Serving generated files that are replaced during rebuilds?"));
assert!(help.contains("devloop docs artifacts"));
}

#[test]
fn cli_accepts_docs_without_a_topic() {
let cli = Cli::try_parse_from(["devloop", "docs"]).expect("parse docs index command");

match cli.command {
super::Command::Docs { topic } => assert!(topic.is_none()),
_ => panic!("expected docs subcommand"),
}
}

#[test]
fn docs_index_explains_when_to_use_artifacts() {
let rendered = render_docs_text(None);

assert!(rendered.starts_with("DEVLOOP DOCUMENTATION"));
assert!(rendered.contains("devloop docs artifacts"));
assert!(rendered.contains("replaces files that a managed process is serving"));
}
}
28 changes: 28 additions & 0 deletions tests/cli_docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::process::Command;

#[test]
fn root_help_names_the_artifact_use_case_and_guide() {
let output = Command::new(env!("CARGO_BIN_EXE_devloop"))
.arg("--help")
.output()
.expect("run root help");

assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("help is UTF-8");
assert!(stdout.contains("Serving generated files that are replaced during rebuilds?"));
assert!(stdout.contains("devloop docs artifacts"));
}

#[test]
fn bare_docs_prints_the_topic_index() {
let output = Command::new(env!("CARGO_BIN_EXE_devloop"))
.arg("docs")
.output()
.expect("run docs index");

assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("docs index is UTF-8");
assert!(stdout.starts_with("DEVLOOP DOCUMENTATION"));
assert!(stdout.contains("devloop docs artifacts"));
assert!(stdout.contains("replaces files that a managed process is serving"));
}