diff --git a/AGENTS.md b/AGENTS.md index dbb829ba..d55cae5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ job projection protocol). `scripts/` holds build/release/verify tooling. ## Environment - Node 22.22.2+ (22.x) or 24.15.0+ (24.x) — the test runner refuses other majors. - On the primary Mac: `. "$HOME/.nvm/nvm.sh" && nvm use 22`. + On the Linux workstation: `. "$HOME/.nvm/nvm.sh" && nvm use 22` (its nvm + default is 24). On the Mac, Node 22 comes from Homebrew. - Rust/cargo required for `check:core`, `build:core*`, and the Tauri shell (`. "$HOME/.cargo/env"`). - Bun **exactly 1.4.0** for `*.bun.test.ts` and `*.dom.bun.test.tsx` files (pinned in @@ -33,23 +34,44 @@ job projection protocol). `scripts/` holds build/release/verify tooling. dependency files. Unknown local modifications must fail rather than be replaced. - Server binds loopback by default (fail-closed; it can run shell commands). `SERVER_PORT` defaults to 3001, Vite dev on 5173. Do not export `SERVER_PORT=0`. +- **Machines (owner setup, 2026-09-27).** Day-to-day work happens on the + Linux workstation `home-server` (Ubuntu 24.04, x86_64, checkout at + `~/workspace/gajae-code-app`). Everything except the macOS desktop shell + builds and tests there; CI is Ubuntu too. The MacBook (`macbookpro` on the + tailnet) is the macOS build host, driven over SSH (next item). The Linux + host's global git identity is a test placeholder, so the checkout carries a + repo-local `user.name`/`user.email`; keep it when re-cloning. +- **macOS lanes from Linux**: `scripts/macos-remote.sh -- ''` runs a + command on the Mac (`GAJAE_MAC_HOST=macbookpro`) at the current commit, which + must be pushed first. It uses the disposable detached worktree + `.gjc-worktrees/mac-build` of the Mac's checkout, re-runs `npm ci` only when + `package-lock.json` changes, and runs in a zsh login shell (a plain + non-interactive SSH shell lacks Homebrew's Node). Desktop shell gate: + `scripts/macos-remote.sh -- 'npm run server:payload:macos && cargo fmt --manifest-path src-tauri/Cargo.toml -- --check && cargo test --locked --manifest-path src-tauri/Cargo.toml'`. + The Mac stays awake on AC through the LaunchAgent `local.mac-awake` + (`caffeinate -s -i`); it must be logged in for GUI work. Screenshots and + menu clicks over SSH (`screencapture`, `osascript`) need Screen Recording + and Accessibility granted to `sshd-keygen-wrapper` on the Mac. - A long-lived dev stack may already be running in tmux session `gajae-dev` - (check `tmux ls` and `lsof -nP -iTCP:3001 -iTCP:5173 -sTCP:LISTEN`; its log is + (check `tmux ls` and `ss -ltn | grep -E ':(3101|5174) '`; its log is mirrored to `/tmp/gjc-dev/dev.log` and the address/operating notes live in `/tmp/gjc-dev/README.md`). Reuse it rather than starting a second - `npm run dev` on the same ports. On the primary Mac it serves the tailnet - from its own database: - `HOST=$(tailscale ip -4) GAJAE_ALLOW_UNAUTH_REMOTE=1 DATABASE_PATH=$HOME/.gajae-app-dev/auth.db npm run dev`. - The unauth override disables authentication on the bound address, so never - combine it with a bind that is reachable outside the tailnet. + `npm run dev` on the same ports. On `home-server` it serves the tailnet + from its own database on **3101/5174**: + `HOST=$(tailscale ip -4) SERVER_PORT=3101 VITE_PORT=5174 GAJAE_ALLOW_UNAUTH_REMOTE=1 DATABASE_PATH=$HOME/.gajae-app-dev/auth.db npm run dev`. + There, chatmux owns 3001 and `tailscale serve` already proxies + `127.0.0.1:3001` and `127.0.0.1:5173` with Funnel on `:443`, so never run a + dev stack on loopback 3001/5173 on that host. The unauth override disables + authentication on the bound address, so never combine it with a bind that + is reachable outside the tailnet, or put it behind `serve`/Funnel. - **Never run a dev server on `~/.gajae-app` while the desktop app is installed.** Without `DATABASE_PATH` the server uses `~/.gajae-app`, and `gajae-core jobs` takes an exclusive lock on `jobs.sqlite3` there. Whichever instance starts second runs without its jobs authority: jobs are unavailable and the desktop updater's **Restart to install** is refused (`updater_runtime_unknown`, journal blockers `orchestrator owner_unknown` / - `native-jobs owner_failed`). On the primary Mac the dev stack uses - `~/.gajae-app-dev`; the desktop app owns `~/.gajae-app`. + `native-jobs owner_failed`). Dev stacks use `~/.gajae-app-dev` on every + machine; on the Mac the desktop app owns `~/.gajae-app`. - Tauri builds choke on `CI=1`: use `env -u CI npm run tauri -- build`. - A release-profile macOS build refuses to guess its updater mode: set `GJC_UPDATE_MODE=disabled` for ad-hoc/manual bundles, or the full production @@ -76,6 +98,7 @@ npm run check:core # cargo fmt --check + clippy -D warnings + cargo test npm run verify # FULL GATE: audit + typecheck + check:core + test + test:e2e:gjc + lint + check:identity + build npm run test:e2e:gjc # 8 GJC wire/browser e2e tests (also part of verify; not part of npm test) npm run desktop:dev # Tauri dev shell +scripts/macos-remote.sh -- '' # run a macOS-only command on the Mac over SSH (pushed commit) npm run server:payload:macos # embedded macOS server payload + sidecar (prerequisite for src-tauri cargo test) GJC_UPDATE_MODE=disabled env -u CI npm run tauri -- build --bundles app # ad-hoc macOS app bundle (unsigned, no updater) npm run server:payload:linux # Linux x64 self-host payload + pinned runtimes diff --git a/scripts/macos-remote.sh b/scripts/macos-remote.sh new file mode 100755 index 00000000..80d85593 --- /dev/null +++ b/scripts/macos-remote.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Runs a command in a macOS build checkout over SSH, at a commit already on +# origin. For working from a Linux machine: the desktop shell (Tauri, the +# server payload, `src-tauri` cargo tests) only builds on macOS. +# +# scripts/macos-remote.sh [--ref ] -- '' +# +# GAJAE_MAC_HOST ssh destination of the Mac (required) +# GAJAE_MAC_CHECKOUT build checkout on the Mac, relative to its home or +# absolute (default: workspace/gajae-code-app/.gjc-worktrees/mac-build) +# +# The ref (default HEAD) is resolved here and must already be on an origin +# branch. The checkout is disposable: it is force-checked-out to that commit +# and cleaned of untracked files (ignored build outputs are kept, so builds +# stay incremental), and `npm ci` runs again whenever package-lock.json +# differs from its last install. +# The command runs in a zsh login shell from the checkout root, e.g. +# scripts/macos-remote.sh -- 'npm run server:payload:macos && cargo test --locked --manifest-path src-tauri/Cargo.toml' +set -euo pipefail + +ref=HEAD +while [ $# -gt 0 ]; do + case "$1" in + --ref) ref="$2"; shift 2 ;; + --) shift; break ;; + *) echo "usage: $0 [--ref ] -- ''" >&2; exit 64 ;; + esac +done +command="$*" +if [ -z "$command" ]; then echo "usage: $0 [--ref ] -- ''" >&2; exit 64; fi +host="${GAJAE_MAC_HOST:?set GAJAE_MAC_HOST to the ssh destination of the Mac}" +checkout="${GAJAE_MAC_CHECKOUT:-workspace/gajae-code-app/.gjc-worktrees/mac-build}" +sha="$(git rev-parse --verify "$ref^{commit}")" + +remote_script=$(cat <<'REMOTE' +set -e +cd "$HOME" +cd "$GAJAE_CHECKOUT" +git fetch --quiet --prune origin +# Reachable from a remote-tracking branch, not merely present in the Mac's +# object store, which also holds the Mac's own unpushed work. +if [ -z "$(git branch -r --contains "$GAJAE_SHA" 2>/dev/null)" ]; then + print -u2 "macos-remote: $GAJAE_SHA is not on origin; push it first." + exit 65 +fi +git checkout --quiet --force --detach "$GAJAE_SHA" +# Untracked leftovers go; ignored build outputs (node_modules, cargo target, +# the server payload) stay so builds remain incremental. +git clean -fdq +lock="$(shasum -a 256 package-lock.json | cut -d' ' -f1)" +if [ "$(cat node_modules/.macos-remote-lock 2>/dev/null)" != "$lock" ]; then + npm ci --no-audit --no-fund + print -r -- "$lock" > node_modules/.macos-remote-lock +fi +print -r -- "macos-remote: $(git log --oneline -1) on $(hostname -s)" +eval "$GAJAE_COMMAND" +REMOTE +) + +ssh -o BatchMode=yes "$host" /bin/zsh -l -s <