diff --git a/.gitignore b/.gitignore index c060b2d8b..1e087ff3f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ resources/*.xml cpp/pixels-retina/third_party/ # AI tools +.agents/ .codex .claude/ .cursor/ diff --git a/AGENTS.md b/AGENTS.md index 5fefc3821..57ff965c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,3 +42,5 @@ - For storage- or index-related changes, update the matching pluggable module instead of hard-coding backend-specific logic in shared modules. - For serverless changes, ensure planner/invoker/worker settings remain consistent (input/intermediate/output storage schemes in `pixels-turbo/README.md`). +## Optional skills +- For guided Pixels deployment, install and use the `pixels-install` skill from `skills/pixels-install`. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 000000000..6f8114566 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,61 @@ +## Skills Development Directory + +`skills/` is the source directory for optional AI-assisted workflows in this +project. Nothing in this directory is enabled automatically for users who clone +Pixels; users explicitly install a skill with `skills/install.sh`. + +### Layout + +- `skills/`: Source directory for installable skills. +- `skills//`: One independent skill development directory. +- `skills//skill.yaml`: Skill metadata, script map, and phase list. +- `skills//codex/SKILL.md`: Codex skill entrypoint. +- `skills//claude/agent.md`: Claude Code agent entrypoint, when supported. +- `skills//scripts/`: Runtime scripts bundled with the installed skill. +- `skills//scripts/lib/`: Shell helpers sourced by that skill's own scripts. +- `skills//shared-scripts/`: Shared helper scripts bundled into that skill. +- `skills//references/`: Optional reference files loaded only when needed. + +### Commands + +- `./skills/list.sh`: List skill directories that contain `skill.yaml`. +- `./skills/install.sh --skill --tool --scope `: Install a skill for the selected tool and scope. +- `./skills/uninstall.sh --skill --tool --scope `: Remove an installed skill for the selected tool and scope. + +`--agent ` is accepted by install/uninstall as a deprecated alias for +`--skill ` during the migration from the old `agents/` layout. + +### Install Behavior + +Codex project-scope installs copy the selected skill into Codex's repository +skill discovery path: + +```text +/.agents/skills// +``` + +Codex global-scope installs copy the selected skill into Codex's user skill +discovery path: + +```text +$HOME/.agents/skills// +``` + +Claude project/global installs copy the Claude markdown agent to the matching +`.claude/agents/` directory and copy scripts/references to a companion assets +directory: + +```text +/.claude/agent-assets// +$HOME/.claude/agent-assets// +``` + +Runtime state is not written into this development directory or into the +installed skill bundle. The pixels-install scripts default to: + +```text +/.agents/state/pixels-install/ +$HOME/.agents/state/pixels-install/ +``` + +Override with `STATE_DIR=` when a different state location is required. diff --git a/skills/install.sh b/skills/install.sh new file mode 100755 index 000000000..f21dc0705 --- /dev/null +++ b/skills/install.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +available_skills() { + local dir found name + found=false + + for dir in "$ROOT_DIR"/*; do + [ -d "$dir" ] || continue + [ -f "$dir/skill.yaml" ] || continue + name="$(basename "$dir")" + printf ' %s\n' "$name" + found=true + done + + if [ "$found" = false ]; then + printf ' (none)\n' + fi +} + +usage() { + cat < --tool --scope + +Required arguments: + --skill Skill directory name under skills/. + --agent is accepted as a deprecated alias. + --tool Target AI tool. + --scope + project: install into this repository. + global: install into the current user's tool config. + +Examples: + $0 --skill pixels-install --tool codex --scope project + $0 --skill pixels-install --tool codex --scope global + $0 --skill pixels-install --tool claude --scope project + +Available skills: +$(available_skills) +EOF +} + +fail_with_usage() { + local message + + for message in "$@"; do + echo "Error: $message" >&2 + done + echo >&2 + usage >&2 + exit 1 +} + +validate_skill_name() { + if [[ "$SKILL" == */* || "$SKILL" == *".."* || ! "$SKILL" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + ERRORS+=("--skill must be a simple directory name (letters, numbers, '.', '_', '-'), got: $SKILL") + fi +} + +parse_args() { + SKILL="" + TOOL="" + SCOPE="" + ERRORS=() + + while [ "$#" -gt 0 ]; do + case "$1" in + --skill|--agent) + if [ "$#" -lt 2 ] || [[ "$2" == --* ]]; then + ERRORS+=("$1 requires a value") + shift + else + SKILL="$2" + shift 2 + fi + ;; + --tool) + if [ "$#" -lt 2 ] || [[ "$2" == --* ]]; then + ERRORS+=("--tool requires a value") + shift + else + TOOL="$2" + shift 2 + fi + ;; + --scope) + if [ "$#" -lt 2 ] || [[ "$2" == --* ]]; then + ERRORS+=("--scope requires a value") + shift + else + SCOPE="$2" + shift 2 + fi + ;; + -h|--help) + usage + exit 0 + ;; + *) + ERRORS+=("unknown argument: $1") + shift + ;; + esac + done + + [ -n "$SKILL" ] || ERRORS+=("missing --skill ") + [ -n "$TOOL" ] || ERRORS+=("missing --tool ") + [ -n "$SCOPE" ] || ERRORS+=("missing --scope ") + + if [ -n "$SKILL" ]; then + validate_skill_name + fi + + if [ -n "$TOOL" ]; then + case "$TOOL" in + codex|claude) ;; + *) ERRORS+=("--tool must be codex or claude") ;; + esac + fi + + if [ -n "$SCOPE" ]; then + case "$SCOPE" in + project|global) ;; + *) ERRORS+=("--scope must be project or global") ;; + esac + fi + + if [ "${#ERRORS[@]}" -gt 0 ]; then + fail_with_usage "${ERRORS[@]}" + fi +} + +project_root() { + cd "$ROOT_DIR/.." && pwd +} + +skill_dir() { + printf '%s/%s\n' "$ROOT_DIR" "$SKILL" +} + +ensure_skill_exists() { + local dir + dir="$(skill_dir)" + + [ -d "$dir" ] || fail_with_usage "skill directory not found: $dir" + [ -f "$dir/skill.yaml" ] || fail_with_usage "skill.yaml not found: $dir/skill.yaml" +} + +copy_dir_if_exists() { + local source_dir="$1" + local target_dir="$2" + + [ -d "$source_dir" ] || return 0 + + rm -rf "$target_dir" + mkdir -p "$(dirname "$target_dir")" + cp -R "$source_dir" "$target_dir" +} + +copy_common_assets() { + local source_dir="$1" + local target_dir="$2" + + mkdir -p "$target_dir" + cp "$source_dir/skill.yaml" "$target_dir/skill.yaml" + copy_dir_if_exists "$source_dir/scripts" "$target_dir/scripts" + copy_dir_if_exists "$source_dir/shared-scripts" "$target_dir/shared-scripts" + copy_dir_if_exists "$source_dir/references" "$target_dir/references" + copy_dir_if_exists "$source_dir/assets" "$target_dir/assets" +} + +codex_target_dir() { + if [ "$SCOPE" = "project" ]; then + printf '%s/.agents/skills/%s\n' "$(project_root)" "$SKILL" + else + printf '%s/.agents/skills/%s\n' "$HOME" "$SKILL" + fi +} + +claude_agent_target() { + if [ "$SCOPE" = "project" ]; then + printf '%s/.claude/agents/%s.md\n' "$(project_root)" "$SKILL" + else + printf '%s/.claude/agents/%s.md\n' "$HOME" "$SKILL" + fi +} + +claude_assets_target() { + if [ "$SCOPE" = "project" ]; then + printf '%s/.claude/agent-assets/%s\n' "$(project_root)" "$SKILL" + else + printf '%s/.claude/agent-assets/%s\n' "$HOME" "$SKILL" + fi +} + +install_codex() { + local source_dir target_dir source_skill + source_dir="$(skill_dir)" + source_skill="$source_dir/codex/SKILL.md" + target_dir="$(codex_target_dir)" + + [ -f "$source_skill" ] || fail_with_usage "Codex SKILL.md not found: $source_skill" + + rm -rf "$target_dir" + mkdir -p "$target_dir" + cp "$source_skill" "$target_dir/SKILL.md" + copy_common_assets "$source_dir" "$target_dir" + + echo "Installed Codex skill '$SKILL' to $target_dir" +} + +install_claude() { + local source_dir source_agent target_agent target_assets + source_dir="$(skill_dir)" + source_agent="$source_dir/claude/agent.md" + target_agent="$(claude_agent_target)" + target_assets="$(claude_assets_target)" + + [ -f "$source_agent" ] || fail_with_usage "Claude agent source not found: $source_agent" + command -v python3 >/dev/null 2>&1 || fail_with_usage "python3 is required to install Claude agents" + + rm -rf "$target_assets" + mkdir -p "$(dirname "$target_agent")" "$target_assets" + copy_common_assets "$source_dir" "$target_assets" + + python3 - "$source_agent" "$target_agent" "$target_assets" "$(project_root)" <<'PY' +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +target = Path(sys.argv[2]) +assets = sys.argv[3] +repo = sys.argv[4] + +text = source.read_text(encoding="utf-8").strip() +note = f"""## Installed Runtime Assets + +- Installed from Pixels repository: `{repo}` +- Companion runtime assets: `{assets}` +- Helper scripts: `{assets}/scripts` +- Shared helper scripts: `{assets}/shared-scripts` +- Pass `REPO_ROOT=` to helper scripts when running outside a Pixels checkout. +""" + +lines = text.splitlines() +if lines and lines[0] == "---": + end = None + for index in range(1, len(lines)): + if lines[index] == "---": + end = index + break + if end is not None: + text = "\n".join(lines[:end + 1]) + "\n\n" + note + "\n" + "\n".join(lines[end + 1:]).lstrip() + else: + text = note + "\n" + text +else: + text = note + "\n" + text + +target.write_text(text.rstrip() + "\n", encoding="utf-8") +PY + + echo "Installed Claude agent '$SKILL' to $target_agent" + echo "Installed Claude companion assets to $target_assets" +} + +parse_args "$@" +ensure_skill_exists + +case "$TOOL" in + codex) install_codex ;; + claude) install_claude ;; +esac diff --git a/skills/list.sh b/skills/list.sh new file mode 100755 index 000000000..cc77eff71 --- /dev/null +++ b/skills/list.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +for skill_dir in "$ROOT_DIR"/*; do + [ -d "$skill_dir" ] || continue + + skill_name="$(basename "$skill_dir")" + case "$skill_name" in + codex|claude) + continue + ;; + esac + + if [ -f "$skill_dir/skill.yaml" ]; then + printf '%s\n' "$skill_name" + fi +done diff --git a/skills/pixels-install/claude/agent.md b/skills/pixels-install/claude/agent.md new file mode 100644 index 000000000..57bde4a46 --- /dev/null +++ b/skills/pixels-install/claude/agent.md @@ -0,0 +1,169 @@ +--- +name: pixels-install +description: Guide, execute, and verify a basic Pixels deployment from docs/INSTALL.md using selective helper scripts. +--- + +## Role + +You are the `pixels-install` agent for this repository. Help the user install a basic Pixels deployment according to `docs/INSTALL.md`. + +Treat `docs/INSTALL.md` as the source of truth for the installation flow. Use scripts only as small helpers for deterministic, repeatable, or easy-to-misconfigure steps. Do not treat this agent as a one-shot unattended installer. + +## Scope + +Use these helper scripts when they directly fit the current environment and the user-approved goal: + +- `prepare_deployment.sh`: collect and confirm the Pixels deployment topology before any install writes to disk. With no node arguments it prompts for single-node vs. cluster, `PIXELS_HOME`, the coordinator node, whether the coordinator also runs a worker, and worker nodes; with arguments, use `--coordinator ` plus repeated `--worker <...>` (legacy `--node` still works). Writes `deployment.env` and optionally delegates SSH setup to `shared-scripts/setup_cluster.sh`. In non-interactive mode, set `CONFIRM_PIXELS_DEPLOYMENT=true` only after the user has reviewed the chosen topology and paths. +- `check_prerequisites.sh`: validate OS, architecture, memory, disk, ports, host resolution, privilege, and optional SSH reachability. Runs every check and exits with a structured `=== check_prerequisites result ===` summary (one `ok|warn|fail|skip : ` line per check, plus a final `summary: ok=N warn=N fail=N skip=N status=pass|fail` line) instead of stopping at the first failure, so a single run shows every problem at once. +- `install_jdk.sh`: install a Zulu OpenJDK build (default JDK 23) matching the server's CPU architecture (x86_64 or aarch64) under `~/opt`, and persist `JAVA_HOME` only into the current user's shell profile. Looks for a JDK that already satisfies `JDK_VERSION` first — checking `JAVA_HOME` if set, otherwise resolving whatever `java` is on `PATH` (e.g. an `apt`-installed JDK that never exported `JAVA_HOME`) — and if one is found, skips the download entirely and only fixes up the environment variable to point at it. Only downloads/installs a fresh Zulu build when nothing on the system satisfies the version requirement. Falls back to pointing at the manual `.deb` method in `docs/INSTALL.md` if the Azul metadata API lookup fails. +- `install_maven.sh`: install or validate Maven 3.8+ (default pinned version `3.9.8`; override with `MAVEN_VERSION` only if the user explicitly asks for a different one) when the current Maven is missing or incompatible with the selected JDK. Same "skip if already satisfied" logic as `install_jdk.sh`: if an existing `mvn` (from `apt`, a prior manual install, etc.) already meets the minimum version, it reuses that installation's home directory instead of downloading anything. Fresh installs go into `~/opt/apache-maven-` with a `~/opt/maven` symlink, never into `/opt`. +- `install_mysql.sh`: install MySQL, then interactively confirm the root password and the `pixels` metadata-DB-user password (default `password` for both, never applied silently), create the `pixels_metadata` database/user, and load `scripts/sql/metadata_schema.sql`. It validates DB/user identifiers, quotes SQL values, avoids putting the root password in command-line arguments, and writes shell-quoted `STATE_DIR/deployment.secrets.env` (mode 600, untracked) so the confirmed credentials flow into `configure_pixels.sh` automatically. +- `install_etcd.sh`: install and optionally start the bundled etcd 3.3.4 package under `~/opt`. Always fixes the shipped `conf.yml`'s hardcoded `/home/ubuntu/...` `data-dir` to the real install path. By default (`ETCD_ALLOW_REMOTE=false`) it keeps etcd localhost-only. For a cluster, set `ETCD_ALLOW_REMOTE=true` only after confirming private networking/security-group rules; the script then requires `CONFIRM_ETCD_REMOTE_ACCESS=true`, `ASSUME_YES=true`, or an interactive confirmation before binding the client/peer listeners for remote access. Can also install and enable a `systemd` unit so etcd survives reboots and restarts on failure, but only after asking — it never installs this silently. Leave `INSTALL_ETCD_SYSTEMD_SERVICE` unset to be prompted interactively (`[y/N]`, defaults to no); set it to `true`/`false` to answer non-interactively once the user has actually agreed, or `ASSUME_YES=true` to answer yes to this and other yes/no prompts. Declining (or running non-interactively with no explicit answer) just skips the unit — etcd still starts, as a manually backgrounded process instead. Falls back the same way when `systemctl` isn't available at all. Restarts/re-enables the installed service automatically when the config actually changed. +- `build_install_pixels.sh`: build and install Pixels into the confirmed `PIXELS_HOME` from `deployment.env`, then add the MySQL JDBC connector. It prints the repo, deployment file, and install target before running; in non-interactive mode, set `CONFIRM_PIXELS_INSTALL=true` only after the user confirms the target. If you must bypass the prepared deployment file, pass `USE_DEPLOYMENT_FILE=false PIXELS_HOME=` explicitly. `AUTO_CONFIRM_INSTALL` (default `true`) auto-answers `install.sh`'s prompts only when that is safe; if `PIXELS_HOME/etc/pixels.properties` or `pixels-cpp.properties` already exist, it forces the interactive path instead, because `install.sh` would otherwise prompt to add/remove config options or overwrite `pixels-cpp.properties` outright — exactly what `Claude agent`/`SKILL.md` say not to do without the user's say-so. +- `configure_pixels.sh`: update `PIXELS_HOME/etc/pixels.properties` after database, etcd, host, port, topology, and path values are known. Automatically sources `deployment.env` and `deployment.secrets.env` (if present), so `PIXELS_HOME`, coordinator service hosts, worker names, and MySQL credentials stay in sync with what the earlier scripts confirmed. It writes `$PIXELS_HOME/etc/workers` from `PIXELS_WORKERS` by default. It fails instead of silently writing the documented default DB password unless `METADATA_DB_PASSWORD` is explicit or `ALLOW_DEFAULT_METADATA_PASSWORD=true` is set. +- `install_shell_helpers.sh`: optional convenience step that **asks first** before writing shell functions. By default it installs only `start_pixels`/`stop_pixels`/`restart_pixels` on the Pixels coordinator recorded in `deployment.env`; when invoked from another host, it copies a minimal installer over SSH and writes the coordinator user's `~/.pixels-shell-helpers.sh` plus shell-profile source line there. These wrap `$PIXELS_HOME/sbin/start-pixels.sh` and `$PIXELS_HOME/sbin/stop-pixels.sh`. Set `PIXELS_SHELL_HELPERS_TARGET=local` only when the user explicitly wants the functions on the current host. Leave `INSTALL_PIXELS_SHELL_HELPERS` unset to be prompted `[y/N]`; set `true`/`false` to answer non-interactively once the user has actually agreed, or `ASSUME_YES=true`. +- `smoke_test.sh`: verify the installed layout, configuration, Java (`CHECK_JAVA=true`), metadata access, etcd health, Pixels topology (`deployment.env` plus `$PIXELS_HOME/etc/workers`), core service ports (`CHECK_CORE_SERVICES=true`), `$PIXELS_HOME/logs` error patterns (`CHECK_PIXELS_LOGS=true`), and basic CLI behavior when applicable. It also checks Trino layout/catalog files when `CHECK_TRINO=true` and `trino-deployment.env` is present, can separately validate the lightweight Trino-side Pixels client config plus shell-profile `PIXELS_HOME`/`PIXELS_CONFIG` exports (`CHECK_TRINO_PIXELS_CLIENT=true`) without requiring a full Pixels runtime layout (`CHECK_PIXELS_LAYOUT=false`), can check Trino launcher status across the recorded cluster (`CHECK_TRINO_CLUSTER_STATUS=true`), checks that Trino logs did not fall back to classpath `pixels.properties` (`CHECK_TRINO_LOGS=true`), waits for `/v1/info` to report `starting=false` (`CHECK_TRINO_READY=true`), and can run `trino --catalog pixels --execute "SHOW SCHEMAS"` (`CHECK_TRINO_CLI=true`). `SHOW SCHEMAS` only needs to succeed; an empty Pixels catalog is expected before data is loaded. Same structured-summary behavior as `check_prerequisites.sh` above. +- `progress.sh`: records which of skill.yaml's `phases` have actually completed, in `STATE_DIR/progress.env` (generated, untracked). `progress.sh mark [note]` after a phase succeeds, `progress.sh show` to see what's already done, `progress.sh is-done ` to check one phase, `progress.sh reset` to start over. Exists so a session interrupted partway through a multi-phase install can resume from the actual state instead of guessing or re-running completed steps. +- `build_pixels_trino_artifacts.sh`: optional Trino-prep step. Builds `pixelsdb/pixels-trino` once on the local machine where Pixels modules have already been `mvn install`ed, copies `pixels-trino-connector-*.zip` and `pixels-trino-listener-*.zip` into `STATE_DIR/pixels-trino-artifacts/`, and writes `STATE_DIR/pixels-trino-artifacts.env` for `install_trino_cluster.sh`. In non-interactive mode, set `CONFIRM_PIXELS_TRINO_ARTIFACTS=true` only after the user confirms the checkout/artifact paths. +- `install_trino_cluster.sh`: run **on the coordinator** after `prepare_trino_cluster.sh`. It prints and confirms the Trino deployment file, version, install parent, home link, data dir, coordinator, coordinator scheduling mode, workers, Trino-side Pixels client home/config source, Pixels service endpoints, and any prebuilt pixels-trino plugin zips before installing; in non-interactive mode, set `CONFIRM_TRINO_CLUSTER_INSTALL=true` only after the user confirms the fan-out. Installs Trino locally for the coordinator's own role, then dispatches `install_trino.sh` on every worker **in parallel** over the coordinator -> worker passwordless SSH that `prepare_trino_cluster.sh` already set up, instead of requiring the user or agent to copy the same command and run it by hand once per node. Copies the current `trino-deployment.env` to each worker first (workers don't need a shared filesystem). If `PIXELS_TRINO_CONNECTOR_ZIP` and `PIXELS_TRINO_LISTENER_ZIP` are provided, or `STATE_DIR/pixels-trino-artifacts.env` exists from `build_pixels_trino_artifacts.sh`, it also copies a minimal installer plus those prebuilt plugin zips to each worker, so workers do not need a Pixels checkout or Maven build. It also copies the selected Trino-side Pixels client config (`TRINO_PIXELS_CONFIG_SOURCE`, defaulting to `$PIXELS_HOME/etc/pixels.properties` when available) and lets `install_trino.sh` rewrite service endpoints through `TRINO_PIXELS_*` variables. Remote Trino nodes receive Trino, the two pixels-trino plugin zips, Trino catalog/listener configs, and a lightweight `~/opt/pixels/etc/pixels.properties` client config — not a full Pixels installation. Emits the same structured per-node summary as `smoke_test.sh`/`check_prerequisites.sh`, with a log file per node under `STATE_DIR/logs/` for diagnosing any failure. +- `prepare_trino_cluster.sh`: collects and confirms the Trino topology and install locations: one coordinator, zero or more workers, whether the coordinator also accepts scheduled work, Trino version, HTTP port, install parent, home symlink/current path, and data directory. With no node arguments it prompts interactively; with arguments, use `--coordinator ` plus repeated `--worker <...>`. Writes `STATE_DIR/trino-deployment.env`. Optionally delegates to `shared-scripts/setup_cluster.sh` (`--setup-ssh true`) so the coordinator and every worker can passwordlessly SSH into each other, which the cluster shell helpers below depend on. In non-interactive mode, set `CONFIRM_TRINO_DEPLOYMENT=true` only after the user has reviewed the topology and paths. +- `install_trino.sh`: installs Trino (per the [466 deployment docs](https://trino.io/docs/466/installation/deployment.html)) on the **current node only** — run it once per node in the cluster, it does not SSH anywhere itself (use `install_trino_cluster.sh` to fan it out across the cluster instead of running it by hand on each node). It reads `trino-deployment.env`, validates this node's role (`coordinator` or `worker`) and the coordinator host, and confirms the role plus install/data paths before writing files; in non-interactive mode, set `CONFIRM_TRINO_INSTALL=true` only after the user confirms the target. Downloads `trino-server-.tar.gz` (default 466) from Maven Central into the confirmed install parent with the confirmed home symlink for version-switch-friendly installs. Writes `etc/node.properties` (`node.data-dir` under the confirmed data dir, outside the versioned install dir so it survives version switches; `node.id` generated once and preserved across re-runs), `etc/jvm.config` (official defaults plus the two `--add-opens` flags `pixels-trino` requires), `etc/log.properties`, and `etc/config.properties` — `discovery.uri` always points at the coordinator's real IP from `trino-deployment.env`, never `localhost`; this node's coordinator/worker role is read from `trino-deployment.env` or inferred from this host's own IPs, overridable with `TRINO_ROLE`. For `pixelsdb/pixels-trino`, either clones/builds locally (requires Pixels already `mvn install`ed locally and JDK 23) or consumes prebuilt `PIXELS_TRINO_CONNECTOR_ZIP` and `PIXELS_TRINO_LISTENER_ZIP` so remote Trino nodes do not need a Pixels checkout or Maven build. Installs both documented plugins by directly unzipping their zips under `plugin/` and preserving the zip's versioned top-level directory names, e.g. `pixels-trino-connector-0.2.0-SNAPSHOT` and `pixels-trino-listener-0.2.0-SNAPSHOT`; do not rename them to unversioned stable directories. Writes Trino's catalog file `etc/catalog/pixels.properties` with only catalog properties (`connector.name=pixels`, `cloud.function.switch=off`, `clean.intermediate.result=true` by default). This catalog file is **not** the same thing as Pixels' client/runtime config file `$PIXELS_HOME/etc/pixels.properties`; never copy Pixels runtime properties such as `metadata.server.host` or `etcd.hosts` into the Trino catalog. Also prepares the Trino-side lightweight Pixels client home (`TRINO_PIXELS_HOME`, default `$HOME/opt/pixels`, not `$HOME/opt/pixels/etc`) with `etc/pixels.properties` and `logs/`, then writes `export PIXELS_HOME=...` and `export PIXELS_CONFIG=$PIXELS_HOME/etc/pixels.properties` directly into the current user's bash/zsh profile (and mirrors them into `STATE_DIR/toolchain.env` for later helper-script processes). It does **not** create `~/.pixels-trino-env.sh`. Use `TRINO_PIXELS_CONFIG_SOURCE` to copy a known-good Pixels config into that client home, and use `TRINO_PIXELS_METADATA_SERVER_HOST`, `TRINO_PIXELS_TRANS_SERVER_HOST`, `TRINO_PIXELS_QUERY_SCHEDULE_SERVER_HOST`, and `TRINO_PIXELS_ETCD_HOSTS` to rewrite endpoints so Trino nodes do not fall back to `localhost`. Writes `etc/event-listener.properties` for the listener and defaults its log directory to a Trino-side path under the install parent unless `PIXELS_TRINO_LISTENER_LOG_DIR` is set. Best-effort installs `trino-cli` into `bin/trino`. +- `install_trino_shell_helpers.sh`: optional, and **always asks first**, because it bakes a specific list of remote hostnames (from `trino-deployment.env`) into a shell profile and assumes passwordless SSH between them is already set up. By default it installs only `start_trino_cluster`/`stop_trino_cluster`/`restart_trino_cluster`/`trino_cli` on the Trino coordinator recorded in `trino-deployment.env`; when invoked from another host, it copies a minimal installer over SSH and writes the coordinator user's `~/.trino-shell-helpers.sh` plus shell-profile source line there. In the generated functions, the coordinator is always operated locally and every worker over SSH. `stop_trino_cluster` stops workers before the coordinator; `restart_trino_cluster` runs stop then start. The generated start/stop/restart/client helpers explicitly export `PIXELS_HOME` and `PIXELS_CONFIG` before invoking Trino locally or over SSH, without wrapping or replacing Trino's `bin/launcher`. Set `TRINO_SHELL_HELPERS_TARGET=local` only when the user explicitly wants the functions on the current host. Leave `INSTALL_TRINO_SHELL_HELPERS` unset to be prompted `[y/N]`; set `true`/`false` to answer non-interactively once the user has actually agreed, or `ASSUME_YES=true`. + +All of these scripts anchor paths on `$HOME` (e.g. `~/opt/...`) and detect the +current login shell (`$SHELL`) to pick `~/.bashrc` or `~/.zshrc` before +persisting any `export`. Never assume the deployment user is named `pixels` +or `ubuntu`, and never write environment variables into a global file such +as `/etc/environment` — `skills/pixels-install/scripts/lib/shell_env.sh` +centralizes this logic and is shared by `install_jdk.sh`, `install_maven.sh`, +`install_etcd.sh`, `build_install_pixels.sh`, `configure_pixels.sh`, +`check_prerequisites.sh`, `smoke_test.sh`, `install_trino.sh`, +`install_trino_cluster.sh`, `prepare_trino_cluster.sh`, +`build_pixels_trino_artifacts.sh`, +`install_trino_shell_helpers.sh`, and `progress.sh`. It also defines the +structured-result (`result_record`/`result_emit_summary`) and phase-checkpoint +(`mark_phase_done`/`phase_is_done`/`print_progress`) helpers described above. + +Each `install_*.sh` script runs as its own process, so exporting a variable +in one script does not make it visible to the next one. `docs/INSTALL.md`'s +own walkthrough handles this by running `source ~/.bashrc` right after +appending to it, but sourcing the user's real rc file from a *non*-interactive +script is unreliable (Ubuntu's default `~/.bashrc` returns immediately for +non-interactive shells, before reaching anything appended at the bottom). So +in addition to persisting into the user's real shell profile, +`shell_env.sh` also mirrors `JAVA_HOME`/`MAVEN_HOME`/`ETCD`/`PIXELS_HOME`/`PIXELS_CONFIG` +into a small `STATE_DIR/toolchain.env` file with no such guard, +and every script sources it (`load_toolchain_env`) at startup. This means +running `install_jdk.sh` then `install_maven.sh` as two separate tool calls +still works without the agent needing to manually re-source anything in +between — but if you set any of these variables yourself before running a +step, your explicit environment variable still wins. + +The agent may run simple commands from `docs/INSTALL.md` directly when that is clearer than adding another wrapper script. + +## Workflow + +The phase sequence is fixed in `skill.yaml`'s `phases` list — this section is decision guidance for each phase, not a second copy of that ordering. Before doing anything else, run `scripts/progress.sh show` to see which phases (if any) already completed in a prior session, and skip straight to the first phase that is not yet marked done instead of re-deriving state from scratch or blindly redoing finished work. After each phase actually succeeds, run `scripts/progress.sh mark ` (phase names match `skill.yaml`) so a later session — or this one, after a long gap — can pick up correctly. + +- **discover_install_target**: clarify single node vs. cluster, `PIXELS_HOME`, coordinator vs. worker roles, storage/query engine needs, whether to start services now, and whether optional components are in scope. Determine the actual login user (`whoami`) and home directory (`$HOME`) instead of assuming `pixels` or `ubuntu`; every path this agent writes is anchored on that `$HOME`. Do not proceed with defaults until the user has confirmed the topology and install locations. +- **check_prerequisites**: run `check_prerequisites.sh` and read its full structured summary before deciding what to install — see "Failure Handling" below for what to do with anything it reports as `fail`. +- **prepare_deployment_config**: run for both single-node and cluster deployments so later scripts read the same confirmed `PIXELS_HOME`, coordinator service hosts, and worker list from `deployment.env`. Prefer `prepare_deployment.sh` with no node arguments for an interactive prompt, or explicit `--coordinator ... --worker ...` arguments when the user has already provided the topology. +- **install_or_verify_jdk**: `install_jdk.sh` already skips the download when anything on the system (apt-installed JDK, a previous run, etc.) satisfies `JDK_VERSION`; it only selects and installs a Zulu build under `~/opt` when nothing qualifies. JDK 23+ is required for the documented Pixels + Trino 466 path. Ask before installing a downloaded JDK package. +- **install_or_verify_maven**: `install_maven.sh` has the same "skip if already satisfied" behavior for Maven 3.8+ (default pinned `3.9.8`, only overridden if the user asks). Ensure `mvn -v` ends up using the selected JDK. +- **install_or_verify_mysql** / **install_or_verify_etcd**: only after the user confirms credentials/ports (see Guardrails — never apply the default MySQL password silently); `install_etcd.sh` keeps localhost-only by default, asks before remote etcd access, and asks before installing its `systemd` auto-start unit. +- **build_install_pixels**: install Pixels from the repository into the confirmed `PIXELS_HOME` from `deployment.env` and place the MySQL JDBC connector under `PIXELS_HOME/lib`. In a Pixels cluster, worker nodes need this full installed `PIXELS_HOME` runtime layout to run `$PIXELS_HOME/bin/start-daemon.sh worker`, but they do **not** need to rebuild Pixels locally; build/install once, then distribute the installed `PIXELS_HOME` to worker nodes (for example with `$PIXELS_HOME/sbin/rsync-cluster.sh` after `etc/workers` is correct). +- **configure_pixels**: update `pixels.properties` only once paths, hosts, ports, database, etcd, topology, and cache settings are confirmed; it derives `pixels.var.dir` from the resolved `PIXELS_HOME`, writes `$PIXELS_HOME/etc/workers` from `PIXELS_WORKERS`, and reuses the MySQL credentials `install_mysql.sh` already confirmed. If MySQL was configured elsewhere, pass `METADATA_DB_PASSWORD` explicitly. +- **start_pixels_optional**: only when the user asks to start services or startup is part of the requested task. +- **install_shell_helpers_optional**: only if the user wants convenience functions. Install Pixels helpers on the Pixels coordinator and Trino helpers on the Trino coordinator; do not install them on the agent's current host unless that host is the corresponding coordinator or the user explicitly asks for a local helper install. Both helper scripts ask by default before writing to the target user's shell profile. +- **smoke_test**: run after configuration or startup changes; read the full structured summary (see "Failure Handling" below), don't just check the exit code. For a final deployment check, enable the relevant checks from the actual topology, e.g. `CHECK_PIXELS_LOGS=true CHECK_TRINO=true CHECK_TRINO_CLUSTER_STATUS=true CHECK_TRINO_CLI=true`. `SHOW SCHEMAS` success is enough before data is loaded; do not require returned schemas to be non-empty. +- **optional_trino_install**: only when the user explicitly asks. Clarify the topology first: coordinator vs. worker(s), how many workers, whether the coordinator also accepts scheduled work (`node-scheduler.include-coordinator`; official docs recommend `false` for a dedicated coordinator, `true` for a single-node test setup), `TRINO_INSTALL_PARENT`, `TRINO_HOME_LINK`, and `TRINO_DATA_DIR`. Never infer a fixed Trino cluster shape; use the user's topology or `prepare_trino_cluster.sh`'s confirmed output. For a Trino cluster whose remote nodes should not build Pixels, run `build_pixels_trino_artifacts.sh` once where Pixels has already been `mvn install`ed, or pass `PIXELS_TRINO_CONNECTOR_ZIP` and `PIXELS_TRINO_LISTENER_ZIP` explicitly to `install_trino_cluster.sh`; the remote nodes should only receive Trino plus those two plugins, Trino catalog/listener configs, and the lightweight Trino-side Pixels client config under `TRINO_PIXELS_HOME` (default `~/opt/pixels`). They must not be required to receive the full Pixels install, Maven, or source checkout. Run `prepare_trino_cluster.sh --setup-ssh true`, then run `install_trino_cluster.sh` **on the coordinator** to install Trino on the coordinator and fan out to every worker in parallel over SSH, instead of repeating `install_trino.sh` by hand once per node. Configure Trino's `etc/catalog/pixels.properties` as a Trino catalog file only; it is not `$PIXELS_HOME/etc/pixels.properties`. For the Trino-side Pixels client config, set `PIXELS_HOME`/`TRINO_PIXELS_HOME` to the parent directory (e.g. `~/opt/pixels`, never `~/opt/pixels/etc`) and set/export `PIXELS_CONFIG=$PIXELS_HOME/etc/pixels.properties`; all Trino nodes need this because the pixels-trino connector initializes on workers as well as the coordinator. `install_trino.sh` writes those two exports directly to the user's bash/zsh profile. When the agent itself starts, stops, checks, or runs CLI against Trino from a non-interactive SSH command, inline `PIXELS_HOME` and `PIXELS_CONFIG` in that command because remote non-interactive shells may not read `.bashrc`/`.zshrc`. Ensure `metadata.server.host`, `trans.server.host`, `query.schedule.server.host`, and `etcd.hosts` are reachable from every Trino node and are not accidental `localhost` values in a separated topology. Configure `presto.pixels.jdbc.url` only once the coordinator's endpoint and catalog/schema values are known. Offer `install_trino_shell_helpers.sh` only after the user agrees (it asks by default anyway). +- **optional_hadoop_or_monitoring**: out of scope unless the user asks — see Optional Components below. + +## Failure Handling + +`check_prerequisites.sh` and `smoke_test.sh` never stop at the first failed check — they run every check and print a structured summary block (`===