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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`), delete the local branch (`git branch -d <name>`), and delete the remote branch (e.g. via GitHub's "delete branch after merge", or `git push <remote> --delete <name>`).
- **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/<branch>/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).
Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
7 changes: 3 additions & 4 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
Expand Down
13 changes: 5 additions & 8 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DaemonCommand>,
Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions crates/daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
23 changes: 9 additions & 14 deletions crates/daemon/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
48 changes: 0 additions & 48 deletions crates/daemon/src/main.rs

This file was deleted.

41 changes: 19 additions & 22 deletions crates/e2e/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)?;
Expand All @@ -162,7 +163,7 @@ impl Daemon {
}
dst
} else {
constructd_bin_path()?
construct_bin_path()?
};

let mut cmd = Command::new(&binary_path);
Expand All @@ -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.
Expand Down Expand Up @@ -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<PathBuf> {
bin_path("constructd")
}

fn construct_bin_path() -> Result<PathBuf> {
bin_path("construct")
}
Expand Down
8 changes: 4 additions & 4 deletions crates/protocol/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,10 @@ fn parse_command_words(raw: &str) -> Option<CommandOverride> {
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."
)
}

Expand Down
13 changes: 8 additions & 5 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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-<target>` 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
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Loading
Loading