diff --git a/CHANGELOG.md b/CHANGELOG.md
index c93f3453..75b7312d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,24 @@ PyPI version — not the changelog header.
### Added
+- **`./setup.sh` chains into `aipass init run` — clone-first one-command install
+ (DPLAN-0234 Strand A).** Distribution is git-clone, not pip: the framework
+ changes constantly, so a PyPI snapshot goes stale while a clone is always
+ current HEAD. Now `git clone && cd AIPass && ./setup.sh` takes you from cold
+ clone to a working first project in one command: on interactive terminals,
+ setup ends by launching the guided `aipass init run` in a sibling directory
+ (default `~/aipass-project`, prompt to choose — init refuses to run inside the
+ engine tree). New flags mirror `aipass install`'s handoff rules: `--no-init`
+ skips, `--with-init` forces even headless (init chains `--non-interactive`),
+ `--project
` picks the target. CI and piped shells skip automatically
+ (`CI` env or no tty), so the windows/macos-test workflows that run bare
+ `bash setup.sh` are untouched. `install.py` now calls `setup.sh --no-init`
+ since install owns its own init handoff — no double-scaffold. Proven in
+ clean-room Docker, both runs exit 0: bare headless run skips init with a
+ hint; `--with-init` run chains init to `✓ Project initialized.` with the
+ project dir scaffolded. 39/39 install tests, seedgo 30/30. README Quick
+ Start updated to the one-command flow.
+
- **`aipass install` — one-command framework bootstrap.** The missing half of
`pip install aipass`: a single command resolves the install home (default
`~/AIPass`), git-clones the public repo, runs `setup.sh` (venv, editable
diff --git a/README.md b/README.md
index 20febaf5..20794d93 100644
--- a/README.md
+++ b/README.md
@@ -32,13 +32,10 @@ AIPass is a CLI-native scaffold that adds **persistent memory, identity, and coo
```bash
git clone https://github.com/AIOSAI/AIPass.git
-cd AIPass && ./setup.sh # installs the `aipass` + `drone` commands on your PATH
-
-cd ~ && mkdir my-project && cd my-project
-aipass init run
+cd AIPass && ./setup.sh # installs everything, then walks you into your first project
```
-A guided setup creates your project, your first agent, and opens a terminal where that agent is already running. Say "hi" — it knows who it is. Come back tomorrow — it remembers.
+Setup builds the environment, puts `aipass` + `drone` on your PATH, then chains straight into a guided init that creates your project, your first agent, and opens a terminal where that agent is already running. Say "hi" — it knows who it is. Come back tomorrow — it remembers.
This is the base framework. It gives your agents the infrastructure to persist, communicate, and organize — everything else you build on top.
@@ -87,7 +84,9 @@ git clone https://github.com/AIOSAI/AIPass.git
cd AIPass && ./setup.sh # Creates venv, installs, puts `aipass` + `drone` on your PATH, bootstraps 17 agents
```
-### 2. Your own project
+On an interactive terminal, setup ends by chaining into `aipass init run` — one command takes you from clone to a working first project. Pass `--no-init` to skip the chain, `--project ` to pick where the project lands (CI and piped shells skip automatically).
+
+### 2. Your own project (if you skipped the chain)
```bash
cd ~ && mkdir my-project && cd my-project
diff --git a/setup.sh b/setup.sh
index 83afe50d..d58a5267 100755
--- a/setup.sh
+++ b/setup.sh
@@ -2,6 +2,13 @@
#
# AIPass setup script
# Creates a venv, installs the package in editable mode, and verifies CLI entry points.
+# On interactive terminals it then chains into `aipass init run` to scaffold a first
+# project (DPLAN-0234: one command does setup + init).
+#
+# Usage: ./setup.sh [--no-init] [--with-init] [--project ]
+# --no-init skip the first-project init chain
+# --with-init force the init chain even headless (init runs --non-interactive)
+# --project first-project directory (default: ~/aipass-project)
#
set -euo pipefail
@@ -27,6 +34,27 @@ case "${OSTYPE:-}" in
;;
esac
+# --- Args ---
+# Mirrors the `aipass install` handoff rules: --no-init wins, --with-init forces,
+# default (auto) chains into init on interactive terminals only — CI/headless skip.
+RUN_INIT="auto"
+INIT_PROJECT=""
+PREV_ARG=""
+for arg in "$@"; do
+ if [ "$PREV_ARG" = "--project" ]; then
+ INIT_PROJECT="$arg"
+ PREV_ARG=""
+ continue
+ fi
+ case "$arg" in
+ --no-init) RUN_INIT="no" ;;
+ --with-init) RUN_INIT="yes" ;;
+ --project=*) INIT_PROJECT="${arg#--project=}" ;;
+ --project) PREV_ARG="--project" ;;
+ *) echo "WARN: unknown argument '$arg' (ignored)" ;;
+ esac
+done
+
echo "=== AIPass Setup ==="
echo "Repo root: $SCRIPT_DIR"
echo ""
@@ -982,6 +1010,77 @@ if [ "$FAIL" -eq 0 ]; then
echo " Claude Code: hooks installed to ~/.claude/settings.json"
command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml"
echo ""
+
+ # --- Chain into first-project init (DPLAN-0234: one command does setup + init) ---
+ # `aipass init` refuses to run inside the engine tree, so it always targets a
+ # sibling directory — never the repo itself. Decision mirrors install.py:
+ # --no-init wins, --with-init forces (headless chains --non-interactive),
+ # default = interactive terminals only (CI and piped shells skip).
+ AIPASS_BIN=""
+ if [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass.exe" ]; then
+ AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass.exe"
+ elif [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass" ]; then
+ AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass"
+ elif [ -f "$SCRIPT_DIR/.venv/bin/aipass" ]; then
+ AIPASS_BIN="$SCRIPT_DIR/.venv/bin/aipass"
+ elif command -v aipass &>/dev/null; then
+ AIPASS_BIN="$(command -v aipass)"
+ fi
+
+ LAUNCH_INIT=0
+ INIT_HEADLESS=0
+ if [ "$RUN_INIT" = "no" ]; then
+ echo "Skipping first-project init (--no-init). Run 'aipass init run' in a fresh directory when ready."
+ elif [ "$RUN_INIT" = "yes" ]; then
+ LAUNCH_INIT=1
+ if [ ! -t 0 ]; then
+ INIT_HEADLESS=1
+ fi
+ elif [ -t 0 ] && [ -z "${CI:-}" ]; then
+ LAUNCH_INIT=1
+ else
+ echo "Non-interactive shell — skipping first-project init. Run 'aipass init run' in a fresh directory when ready."
+ fi
+
+ if [ "$LAUNCH_INIT" -eq 1 ]; then
+ if [ -z "$AIPASS_BIN" ]; then
+ echo "WARN: aipass binary not found — open a new terminal and run 'aipass init run' in a fresh directory."
+ else
+ DEFAULT_PROJECT_DIR="$HOME/aipass-project"
+ PROJECT_DIR="$INIT_PROJECT"
+ if [ -z "$PROJECT_DIR" ] && [ "$INIT_HEADLESS" -eq 0 ] && [ -t 0 ]; then
+ echo "AIPass projects live in their own directory, never inside the engine repo."
+ read -r -p "Set up your first project now? [Y/n]: " INIT_REPLY
+ case "$INIT_REPLY" in
+ [nN]*)
+ PROJECT_DIR="-"
+ echo " Skipped. Run 'aipass init run' in a fresh directory when ready."
+ ;;
+ *)
+ read -r -p " Project directory [$DEFAULT_PROJECT_DIR]: " INIT_INPUT
+ PROJECT_DIR="${INIT_INPUT:-$DEFAULT_PROJECT_DIR}"
+ ;;
+ esac
+ elif [ -z "$PROJECT_DIR" ]; then
+ PROJECT_DIR="$DEFAULT_PROJECT_DIR"
+ fi
+
+ if [ "$PROJECT_DIR" != "-" ]; then
+ mkdir -p "$PROJECT_DIR"
+ INIT_CMD=("$AIPASS_BIN" init run)
+ if [ "$INIT_HEADLESS" -eq 1 ]; then
+ INIT_CMD+=(--non-interactive)
+ fi
+ echo ""
+ echo "Launching guided setup in $PROJECT_DIR ..."
+ if (cd "$PROJECT_DIR" && "${INIT_CMD[@]}"); then
+ echo "First project initialized at $PROJECT_DIR"
+ else
+ echo "Init didn't complete — run 'aipass init run' in $PROJECT_DIR when ready."
+ fi
+ fi
+ fi
+ fi
else
echo "=== Setup finished with errors ==="
echo "The venv was created and the package was installed, but one or more"
diff --git a/src/aipass/aipass/apps/modules/install.py b/src/aipass/aipass/apps/modules/install.py
index efcc6ad5..6e225738 100644
--- a/src/aipass/aipass/apps/modules/install.py
+++ b/src/aipass/aipass/apps/modules/install.py
@@ -131,7 +131,9 @@ def _run_setup(home: Path, dry_run: bool) -> bool:
return False
console.print("[cyan]Building environment[/cyan] [dim](venv, dependencies, hook wiring)…[/dim]")
try:
- proc = subprocess.run(["bash", str(setup)], cwd=str(home), timeout=_SETUP_TIMEOUT)
+ # --no-init: install owns the init handoff (_handoff_to_init) — without it,
+ # setup.sh's own init chain (DPLAN-0234) would scaffold the project twice.
+ proc = subprocess.run(["bash", str(setup), "--no-init"], cwd=str(home), timeout=_SETUP_TIMEOUT)
if proc.returncode == 0:
return True
logger.warning("[install] setup.sh exited %s", proc.returncode)