diff --git a/AGENTS.md b/AGENTS.md index 50f08b3f5..080af651b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,4 +37,5 @@ release-please owns it: land conventional commits on `main`, merge the generated - **After changes run `make review`** (typecheck/unit/integration + pi verdict; posts a PR comment). Build per change: landing → `cd packages/landing && bun run build`; `{core,server,agent-worker,cli}` → `make build-packages`; broad → `bun run typecheck` (`make dev` doesn't rebuild workspace pkgs). - **E2E before merge (hard gate)** for bug fixes: red→fix→green reproducer in the PR body; if you can't reproduce, BAIL (post the dead-end, don't open a PR). Native-app UI exempt (compile-check + draft, say so). - Bot behavior → `./scripts/test-bot.sh "@me ..."` (dev `@clawdotfreebot`, prod `@lobuaibot`; clear stale history via `chat_state_lists`). Prompt/behavior → promptfoo (`bun run evals`). Auth'd UI → `docs/BROWSER_TESTING.md`. +- **Owletto e2e (Chrome/Mac):** Chrome → `make e2e-browser` launches/reuses the stable `owletto` agent-browser harness (persistent `~/.config/lobu-dev/chrome` profile + `--extension` from this worktree); drive it with `agent-browser --session owletto `, `RESTART=1` to pick up extension/worktree changes. The extension's fixed manifest `key` pins its ID, so chrome.storage.local (gateway URL + auth) persists — pair once, reuse across sessions/worktrees, like the installed Mac app. Mac → use the installed `Owletto.app`; it reads `~/.config/lobu/config.json` each popover so task-setup-registered worktree contexts appear in its picker. Don't reintroduce a per-worktree extension symlink. - `.env` is the single source of truth for secrets (restart `make dev` after changes). Worker sessions persist via `./workspaces/{agentId}/`. Skills declare network/nix needs that merge into the allowlist — review skills before installing; destructive MCP calls need in-thread approval unless pre-approved in `lobu.toml`. diff --git a/Makefile b/Makefile index 724a50bb3..5ca87f7af 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Development Makefile for Lobu -.PHONY: help setup build test clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean task-use bump review +.PHONY: help setup build test clean dev build-packages ensure-submodule clean-workers test-unit test-integration test-e2e typecheck task-setup task-clean e2e-browser bump review # Default target help: @@ -16,7 +16,7 @@ help: @echo " make typecheck - Strict typecheck (same as Dockerfile) for server + owletto" @echo " make task-setup NAME= - Create a paired worktree at .claude/worktrees/ (lobu + submodule on real branch, .env copied, ports auto-assigned, Lobu context registered)" @echo " make task-clean NAME= [FORCE=1] - Remove the worktree, both branches, and the Lobu context (refuses if there's uncommitted/unpushed work unless FORCE=1)" - @echo " make task-use NAME= - Point Chrome ext / Mac app symlinks at this worktree (or 'main' for the canonical checkout)" + @echo " make e2e-browser [RESTART=1] - Launch/reuse the stable 'owletto' Chrome harness (extension from this worktree) for Chrome e2e" @echo " make bump SUBMODULE= [TARGET=] - Lightweight worktree + commit + PR for a trivial submodule pointer bump (skips bun install, .env, ports)" @echo " make review [BASE=] - Run local review (typecheck+unit+integration + pi); posts pi-review status and PR comment" @@ -89,9 +89,11 @@ task-clean: @: $${NAME?Usage: make task-clean NAME= [FORCE=1]} @./scripts/task-clean.sh "$(NAME)" $$( [ "$(FORCE)" = "1" ] && echo --force ) -task-use: - @: $${NAME?Usage: make task-use NAME=} - @./scripts/task-use.sh "$(NAME)" +# Stable Owletto Chrome harness for e2e: one persistent profile, paired once, +# reused from any agent session (mirrors the installed Mac app). Loads the +# extension from the current worktree; RESTART=1 forces a fresh launch. +e2e-browser: + @./scripts/e2e-browser.sh $$( [ "$(RESTART)" = "1" ] && echo --restart ) # Lightweight shortcut for "trivial submodule pointer bump" work. Creates a # minimal worktree (no bun install, no .env copy, no port allocation), advances diff --git a/scripts/e2e-browser.sh b/scripts/e2e-browser.sh new file mode 100755 index 000000000..2f5ba5887 --- /dev/null +++ b/scripts/e2e-browser.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# e2e-browser.sh — launch (or reuse) the stable Owletto Chrome harness for e2e. +# +# Mirrors the Mac-app model: one persistent browser profile, paired once, +# reused from every agent session. The extension's fixed manifest "key" pins +# its ID regardless of where it's loaded from, so the extension's +# chrome.storage.local (gateway URL + access/refresh tokens + workerId) lives +# in the --profile dir and survives restarts — and carries across worktrees, +# because the ID never changes. So you pair once and reuse forever, exactly +# like installing Owletto.app once. +# +# Usage: +# scripts/e2e-browser.sh # extension = current worktree, open its gateway +# scripts/e2e-browser.sh --restart # force a fresh launch (pick up extension edits or a new worktree) +# make e2e-browser # same, from a worktree root +# +# Stable handles — reuse these verbatim from any agent session: +# profile ~/.config/lobu-dev/chrome (--profile: persists pairing/cookies/storage) +# session owletto (--session: the daemon-managed browser) +# +# Drive it after launch: +# agent-browser --session owletto snapshot -i # refs for click/fill +# agent-browser --session owletto open # navigate +# agent-browser --session owletto close # shut it down +# +# The headed window may close when left idle, but the daemon keeps the +# session's launch config: the next `agent-browser --session owletto ` +# (or re-running this script) revives the browser WITH the extension. So a +# vanished window is not a re-pair — just poke the session. +# +# After editing extension source, reload it (chrome://extensions -> reload) or +# re-run with --restart (the --extension flag only applies at browser launch). +# Mac e2e needs no equivalent: the installed Owletto.app reads +# ~/.config/lobu/config.json on every popover, so worktree Lobu contexts +# registered by task-setup show up in its picker automatically. + +set -euo pipefail + +restart=0 +[[ "${1:-}" == "--restart" ]] && restart=1 + +command -v agent-browser >/dev/null 2>&1 || { + echo "error: agent-browser not on PATH (npm i -g agent-browser, or brew install agent-browser)" >&2 + exit 1 +} + +source_root="$(git rev-parse --show-toplevel)" +ext="$source_root/packages/owletto/apps/chrome" +[[ -d "$ext" ]] || { echo "error: no extension source at $ext" >&2; exit 1; } + +# Gateway URL = this worktree's PORT (.env.local), defaulting to the canonical 8787. +port=8787 +if [[ -f "$source_root/.env.local" ]]; then + p="$(awk -F= '/^PORT=/{print $2; exit}' "$source_root/.env.local" | tr -d '[:space:]')" + [[ -n "$p" ]] && port="$p" +fi +url="http://localhost:$port" + +profile="$HOME/.config/lobu-dev/chrome" +session="owletto" +mkdir -p "$profile" + +# Decide reuse vs (re)launch. A session can linger in the daemon's list after +# its browser window has died, so don't trust `session list` alone — probe the +# actual browser with a gateway-independent navigation (about:blank succeeds +# even when `make dev` is down). RESTART=1 always relaunches. +relaunch=1 +if [[ $restart -eq 0 ]] \ + && agent-browser session list 2>/dev/null | grep -qE "^[[:space:]]*${session}$"; then + if agent-browser --session "$session" open "about:blank" >/dev/null 2>&1; then + relaunch=0 + else + echo "-> '$session' session was stale (browser gone); relaunching" + agent-browser --session "$session" close >/dev/null 2>&1 || true + fi +elif [[ $restart -eq 1 ]]; then + agent-browser --session "$session" close >/dev/null 2>&1 || true +fi + +if [[ $relaunch -eq 0 ]]; then + echo "-> reusing running '$session' session" + echo " (use --restart to reload the extension after source/worktree changes)" + # Non-fatal: a down gateway shouldn't fail the harness — the browser is up. + agent-browser --session "$session" open "$url" >/dev/null 2>&1 || true +else + agent-browser --session "$session" --profile "$profile" --extension "$ext" --headed open "$url" >/dev/null 2>&1 || true + echo "-> launched '$session'" + echo " profile: $profile" + echo " extension: $ext" +fi + +echo "OK Owletto e2e browser ready" +echo " gateway: $url" +echo " sidepanel: set 'Server URL' to $url the first time you pair against this worktree" +echo " drive it: agent-browser --session $session snapshot -i" diff --git a/scripts/task-clean.sh b/scripts/task-clean.sh index 37f4cbc8f..ec5068c77 100755 --- a/scripts/task-clean.sh +++ b/scripts/task-clean.sh @@ -36,7 +36,7 @@ fi script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Resolve `repo` to the main checkout, not whatever worktree the script -# happens to live inside. Same fix as task-setup.sh (#900) / task-use.sh — +# happens to live inside. Same fix as task-setup.sh (#900) — # `--git-common-dir --path-format=absolute` returns the shared .git path # regardless of which worktree the call is made from, so invoking # `make task-clean` from inside a worktree targets the right paths. @@ -126,12 +126,4 @@ else echo "warning: 'lobu' CLI not on PATH; skipping context removal" >&2 fi -# If this worktree was the active target for the Chrome symlink, fall back -# to 'main' so the active/chrome symlink doesn't dangle into a deleted dir. -active_name_file="$HOME/.config/lobu-dev/active/.active-name" -if [[ -f "$active_name_file" ]] && [[ "$(cat "$active_name_file")" == "$name" ]]; then - "$repo/scripts/task-use.sh" main >/dev/null && \ - echo "→ active worktree was '$name'; reset to 'main'" -fi - echo "✓ cleaned up task '$name'" diff --git a/scripts/task-setup.sh b/scripts/task-setup.sh index 7c25c2b82..b81fb469c 100755 --- a/scripts/task-setup.sh +++ b/scripts/task-setup.sh @@ -174,10 +174,6 @@ echo "→ .env.local: PORT=$port WORKER_PROXY_PORT=$proxy" echo "$name" > "$worktree_dir/.task" -# Auto-activate this worktree for the Chrome extension / Mac app (symlink swap). -# Safe / idempotent — task-use.sh re-points existing symlinks each time. -"$repo/scripts/task-use.sh" "$name" || true - # Register the worktree as a Lobu CLI context so the Mac menubar can spawn # `lobu run` for it (lifecycle: managed) against the worktree's source. Safe to # re-run — `lobu context add` overwrites the existing entry. Non-fatal: the diff --git a/scripts/task-use.sh b/scripts/task-use.sh deleted file mode 100755 index 9336255b3..000000000 --- a/scripts/task-use.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# task-use.sh — point the Chrome extension at a specific worktree's source by -# retargeting a fixed symlink. -# -# Usage: -# scripts/task-use.sh # use the worktree at .claude/worktrees/ -# scripts/task-use.sh main # use the main checkout (no worktree) -# -# Symlink (created/updated each run): -# ~/.config/lobu-dev/active/chrome → /packages/owletto/apps/chrome -# -# Point Chrome's "Load unpacked" at ~/.config/lobu-dev/active/chrome ONCE. -# Then `task-use ` swaps which worktree it resolves to; reload the -# extension at chrome://extensions to pick up the new source. -# -# The Chrome extension's gateway URL is configured separately in the -# sidepanel ("Server URL"). Because each worktree's `make dev` runs on its -# own PORT (assigned in .env.local), the symlink retarget alone does NOT -# repoint the extension at the new server — re-open the sidepanel and update -# the URL to http://localhost: for the active worktree. -# -# Xcode/Mac is intentionally NOT symlinked. Open the .xcodeproj at the -# worktree path directly (`packages/owletto/apps/mac` inside the worktree). -# The Mac menubar reads ~/.config/lobu/config.json on every popover, so -# per-worktree Lobu contexts (registered by task-setup) appear in its picker -# automatically — no separate "active mac" indirection is required. - -set -euo pipefail - -usage() { - echo "usage: $0 " >&2 - exit 1 -} - -[[ $# -eq 1 ]] || usage -name="$1" - -if [[ "$name" != "main" ]] && ! [[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then - echo "error: name must be kebab-case or 'main': '$name'" >&2 - exit 1 -fi - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Resolve `repo` to the main checkout, not whatever worktree the script -# happens to live inside. Worktrees share the working tree (scripts/ -# included), so a naive `$script_dir/..` returns the calling worktree's -# root — and task-use would retarget the active/chrome symlink at -# `/.claude/worktrees//packages/...`, nested inside -# whatever worktree the operator happened to be in. Same fix as -# task-setup.sh (#899/#900): use git's shared .git path with -# --path-format=absolute so the resolution is invariant to cwd. -repo="$(dirname "$(git -C "$script_dir" rev-parse --path-format=absolute --git-common-dir)")" - -if [[ "$name" == "main" ]]; then - source_root="$repo" -else - source_root="$repo/.claude/worktrees/$name" - if [[ ! -d "$source_root" ]]; then - echo "error: no worktree at $source_root" >&2 - exit 1 - fi -fi - -active_dir="$HOME/.config/lobu-dev/active" -mkdir -p "$active_dir" - -set_link() { - local label="$1" target="$2" link="$3" - if [[ -d "$target" ]]; then - ln -sfn "$target" "$link" - echo "→ $label: $link → $target" - else - # Source path missing in this worktree (e.g. submodule not initialized). - # Leave the existing symlink alone rather than break it. - echo "(skip $label: source not present at $target)" - fi -} - -set_link "chrome" "$source_root/packages/owletto/apps/chrome" "$active_dir/chrome" - -# Record the active task name for tooling that wants it (and for task-clean -# to know whether to reset to 'main' when cleaning the active worktree). -echo "$name" > "$active_dir/.active-name" - -# Surface the worktree's PORT so the operator is reminded to update the -# Chrome extension's "Server URL" sidepanel setting — the source symlink -# and the gateway URL are independent switches, and stale URLs are the -# single most common footgun across worktree switches. -active_port="" -if [[ "$name" != "main" ]]; then - env_local="$source_root/.env.local" - if [[ -f "$env_local" ]]; then - active_port="$(awk -F= '/^PORT=/{print $2; exit}' "$env_local" | tr -d '[:space:]')" - fi -fi - -echo "✓ active worktree: $name" -if [[ -n "$active_port" ]]; then - echo " ↳ Chrome ext: set Server URL to http://localhost:$active_port" - echo " (open the owletto sidepanel; or reset via chrome://extensions → Reload)" -else - echo " ↳ Chrome ext: confirm Server URL points at this worktree's gateway" -fi -echo " MV3 service workers re-register on extension reload, not on symlink change."