fix(web): setup script step 2 checks pwd, no auto-mkdir (#344)

The /home onboarding page already has a visible manual "Step 3 — create
your workspace folder" instructing the user to `mkdir -p ~/<dir> && cd
~/<dir>` BEFORE pasting the install script into Claude Code. The pasted
script's step 2 then re-ran the same mkdir+cd, which silently overrode
an intentional alternate install path (e.g. user cd'd to
~/work/agnes-prod on purpose) and was redundant on the default path.

Step 2 now verifies the user is in `$HOME/<workspace_dir>` via `pwd`.
On mismatch it stops and asks the user to either re-paste from the
correct folder or reply `install here` to accept the current cwd.
Never auto-creates a folder.

Step 9 (restart Claude Code) references the install directory confirmed
in step 2 instead of a hardcoded `~/<workspace_dir>`, so users on a
custom path see accurate guidance.
This commit is contained in:
Vojtech 2026-05-19 13:20:33 +04:00 committed by GitHub
parent c6c72b9c00
commit bd90485dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 152 additions and 17 deletions

View file

@ -11,6 +11,15 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
## [Unreleased]
### Changed
- Setup script no longer auto-creates the workspace folder. Step 2 of
the pasted prompt now runs `pwd`, compares it to `$HOME/<workspace_dir>`
(the folder the /home page's visible Step 3 told the user to create
manually), and on mismatch warns + asks the user to either re-paste
from the right folder or reply `install here` to accept the current
cwd. Respects an intentional alternate install path instead of
silently switching the user back to the default. Step 9 (restart
Claude Code) now references the install directory confirmed in step 2
rather than a hardcoded `~/<workspace_dir>`.
- **BREAKING (marketplace identifier)**: synthetic plugin bundling flea
skills + agents renamed from `agnes-store-bundle` to `flea`. The
served `marketplace.json` now lists `flea` (previously

View file

@ -324,19 +324,29 @@ def _install_cli_lines(*, has_ca: bool, server_url_placeholder: str = "{server_u
def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
"""Steps 2-4 — workspace folder bootstrap, then `agnes init` + smoke verify.
"""Steps 2-4 — workspace folder check, then `agnes init` + smoke verify.
Step 2 (new) explicitly creates the workspace folder and cd's into it.
Previously the script assumed Claude Code was already cd'd to a sensible
location and ran `agnes init --workspace .` against whatever that
happened to be. With the new explicit mkdir+cd the workspace path is
deterministic `~/{workspace_dir}` and the visible /home step block
+ this scripted step stay in lockstep.
Step 2 verifies the user is already cd'd into the workspace folder
that the /home onboarding page's visible "Step 3 — create your
workspace folder" told them to create manually (`mkdir -p
~/{workspace_dir} && cd ~/{workspace_dir}`). The pasted script
DOES NOT auto-create the folder that would silently override an
intentional choice to install at a different path (e.g. the user
cd'd to `~/work/agnes-prod` on purpose). Instead we `pwd`, compare
to `$HOME/{workspace_dir}`, and on mismatch warn loudly and ask the
user to either re-paste from the right folder or explicitly confirm
"install here" in the current cwd.
`{workspace_dir}` and `{instance_brand}` are placeholders pre-substituted
by :func:`resolve_lines` from the operator-configured brand. Defaults
keep `~/Agnes` behavior for instances that don't set the brand knob.
`agnes init --workspace .` continues to use the current cwd, so once
step 2 has confirmed (or the user has explicitly accepted) the
install dir, step 3 lands in the right place. Step 9's restart-claude
cue references "this same directory" so users on a custom path see
accurate guidance.
`agnes init` is the workspace-rails delivery mechanism for everyone:
it authenticates with the PAT, fetches CLAUDE.md (RBAC-filtered),
writes AGNES_WORKSPACE.md (human-facing docs), installs Claude Code
@ -352,14 +362,36 @@ def _init_lines(server_url_placeholder: str = "{server_url}") -> list[str]:
"""
return [
"",
"2) Create the workspace folder in the user's home directory and cd into it:",
" # POSIX (macOS / Linux / WSL):",
" mkdir -p \"$HOME/{workspace_dir}\" && cd \"$HOME/{workspace_dir}\"",
" # Windows (PowerShell) — only run if the user is on Windows:",
" # New-Item -ItemType Directory -Force -Path \"$HOME\\{workspace_dir}\" | Out-Null",
" # Set-Location \"$HOME\\{workspace_dir}\"",
"2) Verify the user is already in the workspace folder.",
" The /home page's visible \"Step 3 — create your workspace folder\"",
" already asked the user to run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}",
" in their terminal BEFORE pasting this script. Do NOT silently",
" re-create the folder here — the user may have intentionally cd'd",
" to a different location to install {instance_brand} there instead.",
"",
" The remaining steps run inside this directory.",
" Run:",
" pwd",
" Expected: $HOME/{workspace_dir} (i.e. ~/{workspace_dir})",
"",
" If `pwd` matches the expected path: continue silently to step 3.",
"",
" If `pwd` does NOT match, STOP and tell the user verbatim:",
"",
" \"You are in <current-pwd>, but {instance_brand} is normally",
" installed in ~/{workspace_dir} (see /home Step 3). Either run",
" mkdir -p ~/{workspace_dir} && cd ~/{workspace_dir}",
" in your terminal now and re-paste this setup script, OR reply",
" 'install here' to install {instance_brand} in <current-pwd>",
" instead. Reply 'abort' to stop.\"",
"",
" Wait for the user's reply.",
" - 'install here' → continue to step 3 in the current cwd.",
" The cwd you saw from `pwd` is the install",
" directory; remember it for step 9.",
" - 'abort' / anything else → stop without making any changes.",
" Do NOT run `mkdir`, do NOT `cd`, do NOT",
" continue to step 3.",
"",
"3) Bootstrap your {instance_brand} workspace in this directory:",
f" agnes init --server-url \"{server_url_placeholder}\" --token \"{{token}}\" --workspace .",
@ -490,7 +522,7 @@ def _restart_claude_lines(step_num: str) -> list[str]:
return [
"",
f"{step_num}) Restart Claude Code so every plugin, MCP server, and SessionStart hook installed above actually loads:",
" Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same `~/{workspace_dir}` directory.",
" Tell me to type `/exit` (or close the Claude Code session entirely), then run `claude` again from this same directory — the install dir confirmed in step 2 (`~/{workspace_dir}` on the default path, or whatever cwd the user explicitly accepted with 'install here').",
" The next session boots with all marketplace plugins, every connector's keychain entries / OAuth grants, and the agnes-welcome + refresh-marketplace SessionStart hooks active. This is the last action before the Confirm summary — once I'm back in Claude Code, setup is complete.",
]

View file

@ -135,7 +135,10 @@ class TestInstanceBrand:
workspace_dir="FoundryAI",
))
assert "Set up the Foundry AI CLI on this machine." in joined
assert "mkdir -p \"$HOME/FoundryAI\"" in joined
# Step 2 is a pwd-check now (no auto-mkdir); brand + workspace_dir
# thread through the warning copy + expected-path string.
assert "$HOME/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined
assert "Bootstrap your Foundry AI workspace" in joined
assert "Foundry AI workspace is ready" in joined
# No raw placeholders survive substitution.
@ -150,7 +153,10 @@ class TestInstanceBrand:
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl"))
assert "Set up the Agnes CLI on this machine." in joined
assert "mkdir -p \"$HOME/Agnes\"" in joined
# Step 2 is a pwd-check (no auto-mkdir); default path threads
# through as `$HOME/Agnes` + the warning's manual-mkdir example.
assert "$HOME/Agnes" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined
assert "Bootstrap your Agnes workspace" in joined
assert "Agnes workspace is ready" in joined
mod._instance_config = None

View file

@ -1000,3 +1000,91 @@ def test_gws_prompt_emits_pass_fail_contract():
body = gws_prompt(gws_oauth_configured=False)
assert "✅ Google Workspace ready" in body
assert "❌ Google Workspace setup failed" in body
# ---------------------------------------------------------------------------
# Step 2 — workspace folder check (no auto-mkdir).
# ---------------------------------------------------------------------------
def test_step_2_checks_pwd_does_not_auto_mkdir():
"""Step 2 must verify the user is already in the workspace folder
(the /home page Step 3 manual mkdir+cd) instead of silently re-running
`mkdir -p "$HOME/<dir>" && cd ` which would override an intentional
alternate install path.
Contract:
- Step 2 runs `pwd` and compares against `$HOME/<workspace_dir>`.
- Emits a warning message naming both the expected path and the
manual mkdir line the user already saw on /home.
- Does NOT emit `mkdir -p "$HOME/<workspace_dir>"` or the equivalent
PowerShell `New-Item -ItemType Directory -Force` line.
- Asks the user to reply 'install here' to proceed in a non-default
cwd, or 'abort' to stop. No automatic folder creation.
"""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl"))
# The new check-only step header.
assert "2) Verify the user is already in the workspace folder." in joined
# `pwd` check + expected-path comparison must be present.
assert "pwd" in joined
assert "$HOME/Agnes" in joined # default workspace_dir
assert "~/Agnes" in joined
# Warning copy that references the /home Step 3 manual mkdir line.
assert "/home Step 3" in joined
assert "mkdir -p ~/Agnes && cd ~/Agnes" in joined
# Explicit "install here" / "abort" decision tree.
assert "'install here'" in joined
assert "'abort'" in joined
# The auto-mkdir lines from the previous step 2 must be GONE.
assert 'mkdir -p "$HOME/Agnes"' not in joined
assert 'mkdir -p "$HOME/{workspace_dir}"' not in joined
assert 'New-Item -ItemType Directory -Force -Path "$HOME\\Agnes"' not in joined
assert "Set-Location \"$HOME\\Agnes\"" not in joined
def test_step_2_warning_substitutes_custom_brand():
"""Custom brand + workspace_dir must thread through the new step 2
warning copy no leftover placeholders, expected path matches the
operator's configured workspace dir."""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines(
"agnes.whl",
instance_brand="Foundry AI",
workspace_dir="FoundryAI",
))
assert "2) Verify the user is already in the workspace folder." in joined
assert "$HOME/FoundryAI" in joined
assert "~/FoundryAI" in joined
assert "mkdir -p ~/FoundryAI && cd ~/FoundryAI" in joined
# Brand + workspace_dir thread through the warning copy (the text
# wraps across two lines so we check the substrings separately).
assert "but Foundry AI is normally" in joined
assert "installed in ~/FoundryAI" in joined
# No placeholders survive into the rendered text.
assert "{workspace_dir}" not in joined
assert "{instance_brand}" not in joined
def test_step_9_restart_references_install_dir_not_hardcoded():
"""Step 9 must describe the restart cwd as the directory confirmed in
step 2 (mentioning the default path) rather than a bare hardcoded
`~/{workspace_dir}`. This keeps the wording accurate when the user
chose an alternate install path via 'install here' in step 2."""
from app.web.setup_instructions import resolve_lines
joined = "\n".join(resolve_lines("agnes.whl"))
assert "9) Restart Claude Code" in joined
# Wording references the step-2 confirmation.
assert "install dir confirmed in step 2" in joined
# Default path still mentioned as the expected baseline.
assert "~/Agnes" in joined
# The "install here" callout in the restart step keeps the user-flow
# connection visible.
assert "'install here'" in joined