diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52980660..8365327d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ env: # The binaries that make up a release. The daemon resolves adapters as # siblings of its own path (see `locate_binary`), so all of these must # ship together and land in the same directory. - BINS: "construct constructd construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" + BINS: "construct construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" jobs: verify: @@ -85,7 +85,7 @@ jobs: CC_aarch64_unknown_linux_gnu: ${{ matrix.cc }} run: | cargo build --release --locked --target "${{ matrix.target }}" \ - -p agentd -p agentd-cli -p construct-mcp \ + -p agentd-cli -p construct-mcp \ -p construct-adapter-shell -p construct-adapter-claude \ -p construct-adapter-codex -p construct-adapter-antigravity \ -p construct-adapter-smith diff --git a/AGENTS.md b/AGENTS.md index 794ccefc..f3521029 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ All code changes go through a branch, worktree, and PR — no exceptions. - **Release process lives in [`docs/RELEASING.md`](docs/RELEASING.md).** Use that guide for versioned releases and publishing prebuilt binaries. - **No `Co-Authored-By: Claude` trailer in commits.** Don't append model attribution to commit messages. `Co-authored-by:` for other humans is fine. - **Clean up after merge.** Remove the worktree (`git worktree remove `), delete the local branch (`git branch -d `), and delete the remote branch (e.g. via GitHub's "delete branch after merge", or `git push --delete `). -- **After merge, update and build the main worktree.** Once a PR is merged and the feature worktree is cleaned up, switch to the top-level checkout (`~/agentd`), pull latest `main`, and run `cargo build` there (debug profile). This keeps the user's main worktree binaries current so `/construct restart` can pick up the latest merged `construct` / `constructd` changes, especially when operating from a remote-control session. Report the updated main-worktree debug binary paths when relevant. +- **After merge, update and build the main worktree.** Once a PR is merged and the feature worktree is cleaned up, switch to the top-level checkout (`~/agentd`), pull latest `main`, and run `cargo build` there (debug profile). This keeps the user's main worktree binaries current so `/construct restart` can pick up the latest merged `construct` changes, especially when operating from a remote-control session. Report the updated main-worktree debug binary paths when relevant. - **When the change is testable, build all binaries in the worktree and report paths in the agent response.** Run `cargo build` inside the worktree (debug profile — much faster to iterate on than release; the binaries live under `.claude/worktrees//target/debug/`), then print the absolute path of every binary the workspace produces — `construct`, `constructd`, `construct-mcp`, and every `construct-adapter-*` — in the agent response so the user can copy and run them. Explicitly call out *which* binary the PR's code lives in so the user can run the right one without grepping the diff (e.g. "this PR only touches `crates/cli` → relevant binary is `construct`; the others are built but unchanged from main"). - **Record a video / screenshot when it helps the reviewer, and post accessible artifacts on the PR.** This is a judgment call: - Sometimes only an "after" recording makes sense (a brand-new pane / popup / view that didn't exist before). @@ -52,7 +52,7 @@ Use [vhs](https://github.com/charmbracelet/vhs) to capture deterministic mp4 / g export CONSTRUCT_SHELL_BIN="/bin/bash" # adapter discovery export PATH="$BIN_DIR:$PATH" - "$BIN_DIR/constructd" run >"/tmp/rain-${VARIANT}-daemon.log" 2>&1 & + "$BIN_DIR/construct" daemon run >"/tmp/rain-${VARIANT}-daemon.log" 2>&1 & DAEMON_PID=$! trap 'kill $DAEMON_PID 2>/dev/null || true; wait $DAEMON_PID 2>/dev/null || true' EXIT for _ in $(seq 1 50); do "$BIN_DIR/construct" ping >/dev/null 2>&1 && break; sleep 0.2; done diff --git a/README.md b/README.md index 1c0e6bbd..7f507979 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,7 @@ construct daemon run ``` It owns sessions, persists state, and exposes the local IPC socket used by -clients. (`constructd` is a back-compat alias — `constructd run` and -`construct daemon run` are equivalent.) +clients. ### 4. Start crack the matrix @@ -119,7 +118,6 @@ cargo build --workspace Debug binaries land in `target/debug/`: - `target/debug/construct` — TUI, control CLI, **and the daemon** (`construct daemon run`) -- `target/debug/constructd` — standalone daemon binary; a thin back-compat alias for `construct daemon run` - `target/debug/construct-mcp` — MCP bridge for agents - `target/debug/construct-adapter-*` — harness adapters diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 51938536..f0d180ac 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -6,7 +6,7 @@ license.workspace = true authors.workspace = true repository.workspace = true rust-version.workspace = true -description = "construct: TUI client for constructd" +description = "construct: TUI client and daemon for the agent fleet" [[bin]] name = "construct" @@ -16,9 +16,8 @@ path = "src/main.rs" tempfile = "3" agentd-protocol = { workspace = true } agentd-client = { workspace = true } -# The daemon, as a library, so the unified `construct` binary can run it -# directly via the `daemon` subcommand (the standalone `constructd` binary is -# a thin shim over the same crate, kept as a back-compat alias). +# The daemon, as a library, so the `construct` binary can run it directly via +# the `daemon` subcommand. The daemon has no standalone binary of its own. agentd = { path = "../daemon" } tokio = { workspace = true, features = ["full"] } serde = { workspace = true, features = ["derive"] } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index efb0f78e..adb9e19d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -34,10 +34,9 @@ enum Command { Tui, /// Run the construct daemon (session supervisor + IPC server). /// - /// This is the same daemon shipped as the standalone `constructd` - /// binary — `construct daemon run` and `constructd run` are - /// equivalent. One installed `construct` binary can run both the - /// client and the daemon. + /// One installed `construct` binary runs both the client and the daemon; + /// the TUI also auto-starts a daemon when none is running, so you rarely + /// need to invoke this directly (mainly servers / process supervisors). Daemon { #[command(subcommand)] command: Option, @@ -123,8 +122,7 @@ enum Command { }, } -/// Subcommands of `construct daemon`. Mirrors the standalone `constructd` -/// binary's CLI so the two entry points behave identically. +/// Subcommands of `construct daemon`. #[derive(Debug, Subcommand)] enum DaemonCommand { /// Run the daemon in the foreground (default). @@ -149,8 +147,7 @@ async fn main() -> Result<()> { let command = cli.command.unwrap_or(Command::Tui); // Daemon mode runs the supervisor in-process via the shared `agentd` - // library — the same code path as the standalone `constructd` binary. - // Handled before the client tracing init so the daemon's verbose filter + // library. Handled before the client tracing init so the daemon's verbose filter // applies, and before socket discovery (the daemon *owns* the socket // rather than connecting to it). The daemon's restart self-`exec()` // replays this argv (`construct daemon run …`) verbatim, so picking up an diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index e6ed6028..af943a04 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -8,9 +8,8 @@ repository.workspace = true rust-version.workspace = true description = "agentd daemon: supervises agent sessions across harnesses" -[[bin]] -name = "constructd" -path = "src/main.rs" +# Library-only: the daemon runtime is driven by `construct daemon` (see +# `crates/cli`). There is no standalone daemon binary. [dependencies] agentd-protocol = { workspace = true } diff --git a/crates/daemon/src/lib.rs b/crates/daemon/src/lib.rs index e1895930..c7185fc9 100644 --- a/crates/daemon/src/lib.rs +++ b/crates/daemon/src/lib.rs @@ -1,19 +1,14 @@ -//! construct daemon (`constructd` / `construct daemon`): session supervisor and -//! IPC server. +//! construct daemon: session supervisor and IPC server. //! -//! The daemon's entire runtime lives here as a library so it can be driven from -//! two binaries that share one code path: +//! The daemon's entire runtime lives here as a library. It is driven by the +//! `construct daemon` subcommand of the unified `construct` binary (see +//! `crates/cli`) — there is no standalone daemon binary. The TUI also calls +//! [`spawn_detached_daemon`] to auto-start one when none is running. //! -//! - `constructd` — the standalone daemon binary (thin shim in `main.rs`), -//! kept as a back-compat alias. -//! - `construct daemon …` — the unified `construct` binary's daemon subcommand -//! (see `crates/cli`), so a single installed binary can run both the TUI -//! client and the daemon. -//! -//! Both entry points call [`run`] after [`init_tracing`]. They each replay -//! their own argv on `daemon.restart` (see `session::request_daemon_restart`), -//! so the self-`exec()` restart path stays correct regardless of which name -//! launched the daemon. +//! The entry point calls [`run`] after [`init_tracing`]. On `daemon.restart` +//! the daemon replays its own argv (`construct daemon run …`; see +//! `session::request_daemon_restart`), so the self-`exec()` restart path picks +//! up an upgraded binary in place. use anyhow::{Context, Result}; use std::path::PathBuf; diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs deleted file mode 100644 index 5191f456..00000000 --- a/crates/daemon/src/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! `constructd` — standalone daemon binary. -//! -//! Thin shim over the `agentd` library crate, kept as a back-compat alias for -//! the unified `construct daemon …` entry point. Both share the same -//! [`agentd::run`] code path; this binary simply parses the legacy -//! `constructd ` CLI and dispatches. - -use anyhow::Result; -use clap::{Parser, Subcommand}; -use std::path::PathBuf; - -#[derive(Debug, Parser)] -#[command(name = "constructd", about = "construct daemon", version)] -struct Cli { - #[command(subcommand)] - command: Option, -} - -#[derive(Debug, Subcommand)] -enum Command { - /// Run the daemon in the foreground (default). - Run { - /// Override the socket path. - #[arg(long)] - socket: Option, - }, - /// Print resolved paths and exit. - Paths, - /// Print the embedded default config and exit. - DefaultConfig, -} - -#[tokio::main] -async fn main() -> Result<()> { - agentd::init_tracing(); - let cli = Cli::parse(); - match cli.command.unwrap_or(Command::Run { socket: None }) { - Command::Run { socket } => agentd::run(socket).await, - Command::Paths => { - agentd::print_paths(); - Ok(()) - } - Command::DefaultConfig => { - println!("{}", agentd::DEFAULT_CONFIG_TOML); - Ok(()) - } - } -} diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 259a08f3..d721e87e 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -1,6 +1,6 @@ -//! End-to-end test harness for `constructd`. +//! End-to-end test harness for the construct daemon. //! -//! Spawns the **real** `constructd` binary out of the workspace's +//! Spawns the **real** daemon (`construct daemon run`) out of the workspace's //! `target/debug/` against a fresh tempdir for every test (so //! the test never touches the developer's actual `$CONSTRUCT_*_DIR` //! state), waits for the IPC socket to come up, and returns a @@ -32,7 +32,7 @@ use tokio::process::{Child, Command}; use agentd_client::Client; -/// One isolated `constructd` instance + a connected IPC client. +/// One isolated daemon instance + a connected IPC client. /// `Drop` kills the daemon (via tokio's `kill_on_drop`) and /// cleans up the tempdir (via `TempDir`). pub struct Daemon { @@ -45,10 +45,10 @@ pub struct Daemon { /// passing to the TUI helper. pub socket: PathBuf, /// Path the daemon binary was launched from. For `spawn()` - /// this is the workspace `target/debug/constructd`. For - /// `spawn_relocatable()` it's a private copy under the - /// tempdir that the test can swap to exercise the - /// "exec picks up an upgraded binary" path. + /// this is the workspace `target/debug/construct` (run as + /// `construct daemon run`). For `spawn_relocatable()` it's a + /// private copy under the tempdir that the test can swap to + /// exercise the "exec picks up an upgraded binary" path. pub binary_path: PathBuf, /// Pre-connected IPC client. Tests start using it directly — /// no separate "connect" step. @@ -57,8 +57,8 @@ pub struct Daemon { } impl Daemon { - /// Spawn a fresh `constructd` against a tempdir and wait for its - /// IPC socket to come up. Always sets + /// Spawn a fresh daemon (`construct daemon run`) against a tempdir and wait + /// for its IPC socket to come up. Always sets /// `CONSTRUCT_REMOTE_NO_TUNNEL=1` because the e2e tests should /// never spawn a real cloudflared subprocess (would publish a /// real tunnel URL and the CI runner can't reach a `*.try @@ -71,10 +71,11 @@ impl Daemon { Self::spawn_inner(false).await } - /// Like `spawn`, but copies the `constructd` binary into the - /// tempdir and launches that copy instead of the workspace - /// binary. The copy lives at `Daemon::binary_path`, so a - /// test can atomically swap it (write-then-rename) and then + /// Like `spawn`, but copies the `construct` binary into the + /// tempdir and launches that copy (as `construct daemon run`) + /// instead of the workspace binary. The copy lives at + /// `Daemon::binary_path`, so a test can atomically swap it + /// (write-then-rename) and then /// `daemon.restart` to verify the daemon exec()s the new /// on-disk bytes. Running from a private copy also keeps the /// swap from disturbing other tests that share the workspace @@ -137,8 +138,8 @@ impl Daemon { let binary_path = if relocatable { let bin_dir = dir.path().join("bin"); std::fs::create_dir_all(&bin_dir)?; - let src = constructd_bin_path()?; - let dst = bin_dir.join("constructd"); + let src = construct_bin_path()?; + let dst = bin_dir.join("construct"); std::fs::copy(&src, &dst) .with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?; copy_executable_perms(&dst)?; @@ -162,7 +163,7 @@ impl Daemon { } dst } else { - constructd_bin_path()? + construct_bin_path()? }; let mut cmd = Command::new(&binary_path); @@ -173,7 +174,7 @@ impl Daemon { // Skip cloudflared in every e2e test — its absence // from CI runners is not a test failure. .env("CONSTRUCT_REMOTE_NO_TUNNEL", "1") - .args(["run", "--socket"]) + .args(["daemon", "run", "--socket"]) .arg(&socket) // Silence the daemon's stderr / stdout in tests by // default — flip these to `inherit()` while debugging. @@ -523,14 +524,10 @@ fn copy_executable_perms(path: &Path) -> Result<()> { Ok(()) } -/// Locate the `constructd` binary in the workspace `target/` +/// Locate the `construct` binary in the workspace `target/` /// directory. Honors `CARGO_TARGET_DIR` first, then falls back /// to walking up two levels from `CARGO_MANIFEST_DIR` /// (`crates/e2e` → workspace root). -fn constructd_bin_path() -> Result { - bin_path("constructd") -} - fn construct_bin_path() -> Result { bin_path("construct") } diff --git a/crates/protocol/src/adapter.rs b/crates/protocol/src/adapter.rs index 32722445..a343b5b1 100644 --- a/crates/protocol/src/adapter.rs +++ b/crates/protocol/src/adapter.rs @@ -179,10 +179,10 @@ fn parse_command_words(raw: &str) -> Option { pub fn missing_bin_hint(bin: &str, source: &std::io::Error) -> String { format!( "Failed to start `{bin}`: {source}.\n\ - Make sure `{bin}` is on PATH in the shell you started constructd from \ - (try `which {bin}` there). If you use a version manager (nvm, asdf, \ - pyenv, …), activate it in your shell's startup file so PATH is set \ - before launching constructd." + Make sure `{bin}` is on PATH in the shell you started the construct \ + daemon from (try `which {bin}` there). If you use a version manager \ + (nvm, asdf, pyenv, …), activate it in your shell's startup file so \ + PATH is set before launching the daemon." ) } diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 3d9cada2..8a44f378 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -8,9 +8,8 @@ checksums, and publishes a GitHub Release with auto-generated release notes. ## Versioning The single source of truth is `version` under `[workspace.package]` in the -root `Cargo.toml`. `constructd --version` and -`construct --version` always report the workspace version. Use [semver](https://semver.org/) -(`MAJOR.MINOR.PATCH`). +root `Cargo.toml`. `construct --version` always reports the workspace version. +Use [semver](https://semver.org/) (`MAJOR.MINOR.PATCH`). The release workflow's `verify` job refuses to build unless the pushed tag (minus its leading `v`) exactly matches the Cargo version — a mistyped tag can @@ -45,8 +44,12 @@ never publish a mislabelled binary. - `constructd-aarch64-unknown-linux-gnu.tar.gz` (Linux arm64) - `SHA256SUMS` - Each tarball contains all release binaries (`construct`, `constructd`, - `construct-mcp`, `construct-adapter-*`) plus `README.md` and `LICENSE`. + The tarballs keep their historical `constructd-` name (the download + URL `install.sh` and `construct upgrade` expect) even though there is no + longer a `constructd` binary inside. + + Each tarball contains all release binaries (`construct`, `construct-mcp`, + `construct-adapter-*`) plus `README.md` and `LICENSE`. 4. Review the release notes. The workflow passes `generate_release_notes: true` to the release step, so GitHub fills the release body automatically from the diff --git a/docs/architecture.md b/docs/architecture.md index a000c646..904fe5e0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,18 +18,18 @@ Five layers, each replaceable: └──────────────────────────────────────────────┘ ``` -- **Daemon** owns sessions, spawns adapters, persists transcripts. Speaks JSON-RPC over a Unix socket to clients. Run it with `construct daemon run` (or the back-compat `constructd` alias — same binary code). +- **Daemon** owns sessions, spawns adapters, persists transcripts. Speaks JSON-RPC over a Unix socket to clients. Run it with `construct daemon run` (the TUI also auto-starts one when none is running). - **Client** (`construct`) is the TUI plus a set of one-shot subcommands. Multiple clients can attach concurrently. - **Adapter** binaries are independent processes. They implement the AHP over stdio. Anyone can ship one in any language. -The daemon and client ship as **one binary**: `construct` runs the TUI by default and the daemon under `construct daemon`. The daemon's runtime lives in the `agentd` library crate; both `construct daemon` and the standalone `constructd` binary are thin entry points over it. They are not merged into one *process* — the daemon stays a separate long-lived process that many clients attach to — only into one shipped executable. See [`specs/0026-single-binary-daemon-and-client.md`](../specs/0026-single-binary-daemon-and-client.md). +The daemon and client ship as **one binary**: `construct` runs the TUI by default and the daemon under `construct daemon`. The daemon's runtime lives in the `agentd` library crate; there is no standalone daemon binary. The daemon and client are not merged into one *process* — the daemon stays a separate long-lived process that many clients attach to — only into one shipped executable. See [`specs/0026-single-binary-daemon-and-client.md`](../specs/0026-single-binary-daemon-and-client.md). ## Crates | Crate | Binary | Purpose | |---|---|---| | `crates/protocol` | — (lib) | AHP + IPC types, transport, adapter SDK | -| `crates/daemon` | `agentd` (lib) + `constructd` (bin) | Session supervisor, IPC server. Runtime lives in the lib; `constructd` is a thin alias for `construct daemon` | +| `crates/daemon` | `agentd` (lib only) | Session supervisor + IPC server runtime. No standalone binary — driven by `construct daemon` | | `crates/cli` | `construct` | TUI client + control subcommands + `construct daemon` (runs the daemon via the `agentd` lib) | | `crates/adapter-shell` | `construct-adapter-shell` | Generic shell command runner | | `crates/adapter-claude` | `construct-adapter-claude` | Wraps the `claude` CLI | diff --git a/examples/config.toml b/examples/config.toml index 8f286af3..0097acea 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -4,7 +4,7 @@ # ~/.config/construct/config.toml). All adapter entries here are optional — # `shell`, `claude`, and `codex` are registered automatically. Override the # binary path when the adapter binary is not on $PATH and not next to the -# `constructd` binary. +# `construct` binary. # [adapters.shell] # binary = "construct-adapter-shell" diff --git a/install.sh b/install.sh index 9cf3677f..b1dd1bff 100755 --- a/install.sh +++ b/install.sh @@ -19,7 +19,7 @@ set -eu REPO="zarvis-ai/agentd" VERSION="${CONSTRUCT_VERSION:-latest}" BIN_DIR="${CONSTRUCT_BIN_DIR:-$HOME/.local/bin}" -BINS="construct constructd construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" +BINS="construct construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" say() { printf '%s\n' "$*"; } err() { printf 'error: %s\n' "$*" >&2; exit 1; } @@ -73,7 +73,7 @@ fi tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT -say "Installing constructd ($VERSION) for $target" +say "Installing construct ($VERSION) for $target" if ! fetch "${base}/${asset}" "${tmp}/${asset}"; then err "could not download ${base}/${asset} diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 118ca7ed..c1c409ec 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -19,12 +19,10 @@ export CONSTRUCT_DATA_DIR="$SANDBOX/data" export CONSTRUCT_CONFIG_DIR="$SANDBOX/config" export CONSTRUCT_RUNTIME_DIR="$SANDBOX/runtime" -CONSTRUCTD="$ROOT/target/debug/constructd" CONSTRUCT_CLI="$ROOT/target/debug/construct" -[ -x "$CONSTRUCTD" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } [ -x "$CONSTRUCT_CLI" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } -"$CONSTRUCTD" run >"$SANDBOX/daemon.log" 2>&1 & +"$CONSTRUCT_CLI" daemon run >"$SANDBOX/daemon.log" 2>&1 & DAEMON_PID=$! trap 'kill $DAEMON_PID 2>/dev/null || true' EXIT sleep 0.4 diff --git a/scripts/test_agentd.sh b/scripts/test_agentd.sh index abd938b2..94a0e08b 100755 --- a/scripts/test_agentd.sh +++ b/scripts/test_agentd.sh @@ -5,9 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WT="$(cd "$SCRIPT_DIR/.." && pwd)" BIN_DIR="$WT/target/debug" CLIENT_BIN="$BIN_DIR/construct" -CONSTRUCTD_BIN="$BIN_DIR/constructd" -if [[ ! -x "$CONSTRUCTD_BIN" || ! -x "$CLIENT_BIN" ]]; then +if [[ ! -x "$CLIENT_BIN" ]]; then echo "missing $BIN_DIR binaries; run: cargo build" >&2 exit 1 fi @@ -72,7 +71,7 @@ if [[ "${CONSTRUCT_TEST_KEEP:-0}" != "1" ]]; then fi mkdir -p "$DEMO_DIR/run" "$DEMO_DIR/state" "$DEMO_DIR/data" "$DEMO_DIR/config" -"$CONSTRUCTD_BIN" run >"$LOG" 2>&1 & +"$CLIENT_BIN" daemon run >"$LOG" 2>&1 & PID=$! echo "$PID" >"$PID_FILE" diff --git a/specs/0026-single-binary-daemon-and-client.md b/specs/0026-single-binary-daemon-and-client.md index 1498582b..450faf18 100644 --- a/specs/0026-single-binary-daemon-and-client.md +++ b/specs/0026-single-binary-daemon-and-client.md @@ -3,18 +3,15 @@ Status: accepted Date: 2026-06-07 Area: architecture -Scope: The daemon and the TUI/CLI client ship as one executable; the daemon's runtime is a library with thin, interchangeable entry points. +Scope: The daemon and the TUI/CLI client ship as one executable; the daemon's runtime is a library driven by the `construct daemon` subcommand, with no standalone daemon binary. ## Decision The daemon and the client are **one shipped binary**, not two. The `construct` binary runs the TUI by default and runs the daemon under a `daemon` subcommand (`construct daemon run`). The daemon's entire runtime lives in a library crate -so it can be driven from more than one entry point. - -A standalone `constructd` binary remains as a **back-compat alias** that is a -thin shim over the same library. `constructd run` and `construct daemon run` -are equivalent and share 100% of their code path. +(`agentd`) that the `construct` binary drives — there is **no standalone daemon +binary**. This consolidation is about the *executable*, not the *process*. The daemon is still a single long-lived process that many clients attach to over the IPC @@ -30,9 +27,10 @@ multiple daemons. - The daemon's logic was bin-only and unreachable as a library; promoting it to a library lets the unified binary call it directly with no duplication and no subprocess hop. -- Keeping `constructd` as an alias avoids breaking existing installs, scripts, - muscle memory, and the atomic-rename upgrade layout, which replaces binaries - in place at fixed paths. +- A single binary is the whole point: a standalone `constructd` was kept only + briefly as a transition alias, then dropped once `construct daemon` and TUI + auto-start covered every way it was used. Fewer binaries to build, ship, and + install. ## Consequences @@ -41,15 +39,14 @@ multiple daemons. call. Don't push entry-point-only logic back into a `main`. - **Self-restart re-execs by replaying argv verbatim.** The daemon picks up an upgraded binary by `exec()`ing its startup-captured executable path with its - original arguments. This is why a symlink/`argv[0]`-multiplex approach was - rejected: resolving a `constructd`→`construct` symlink would drop the name - while the replayed args still assume daemon dispatch, breaking restart. Each - real entry point must replay an argv that re-enters the *same* mode - (`construct daemon run …` re-execs `construct daemon run …`; `constructd run` - re-execs `constructd run`). -- **The unified binary links both dependency sets** (TUI rendering + daemon - server/tunnel). Accept the larger single binary in exchange for shipping one - file instead of two; most heavy dependencies are already shared. + original arguments (`construct daemon run …` re-execs `construct daemon run + …`). This is also why a standalone `constructd` was *not* kept as a symlink to + `construct`: resolving the symlink would drop the `constructd` name while the + replayed args still assume daemon dispatch, breaking restart. With a single + real binary the replayed argv always re-enters daemon mode correctly. +- **The single binary links every layer** (TUI rendering + daemon + server/tunnel). Accept the larger binary in exchange for shipping one file; + most heavy dependencies are already shared. - **Daemon mode owns the socket; client mode connects to it.** Mode selection must happen before socket discovery and before tracing init, so the daemon's verbose log filter applies in daemon mode and the client's quiet filter @@ -67,6 +64,14 @@ multiple daemons. unlinking and stealing the live socket. The lock is held for the process lifetime and released across the restart `exec()` (CLOEXEC fd), so the re-execed image re-acquires it. Daemons on *different* sockets don't contend. +- **The release tarball keeps its historical `constructd-` name** even + though it no longer contains a `constructd` binary, because already-released + `install.sh` / `construct upgrade` builds fetch that exact asset name. + Renaming it would break the upgrade path. Dropping `constructd` from the + packaged binary set is itself a one-time upgrade break: an `install.sh` baked + into an older binary lists `constructd` in its `BINS` and aborts when it's + absent from the new tarball, so that single hop must re-run the install + one-liner instead of `construct upgrade`. ## Non-Goals @@ -83,6 +88,6 @@ multiple daemons. - Start the system explicitly: run the daemon in one place (`construct daemon run`), attach one or more clients elsewhere (`construct`). - Upgrade in place, then restart the running daemon: the daemon re-execs itself - at the same path and rebinds the socket without losing sessions, regardless of - whether it was launched as `construct daemon` or `constructd`. -- A legacy script or habit that runs `constructd run` keeps working unchanged. + at the same path (`construct daemon run …`) and rebinds the socket without + losing sessions. +- Migration: anything that ran `constructd run` now runs `construct daemon run`.