# Agent and WordPress Scaffold Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.

**Goal:** Create a safe WordPress rebuild repository with local development services and disabled production-access templates.

**Architecture:** Track only custom WordPress source, Agent configuration, tests, and documentation. Use shell guards for remote tools and Docker Compose for isolated local services. Run repository checks in GitHub Actions without production credentials.

**Tech Stack:** Git, GitHub CLI, Docker Compose, WordPress, MariaDB, Bash, Node.js, Playwright, YAML

---

## File map

- `AGENTS.md`: Define Agent scope and production safety rules.
- `README.md`: Explain setup, validation, and approved workflows.
- `.env.example`: Define the complete environment variable contract.
- `.gitignore`: Exclude secrets, WordPress runtime data, and dependencies.
- `docker-compose.yml`: Run isolated local WordPress and MariaDB services.
- `package.json`: Define Playwright and repository check commands.
- `playwright.config.js`: Configure public-site and local-site browser tests.
- `tests/playwright/public-site.spec.js`: Check the public site without authentication.
- `tests/playwright/local-site.spec.js`: Check the local WordPress service.
- `agent/wordpress-mcp/README.md`: Define the disabled WordPress MCP setup.
- `agent/wordpress-mcp/server.example.json`: Provide a disabled server configuration template.
- `agent/rest-api/README.md`: Define read-only REST API access.
- `agent/ssh/README.md`: Define restricted SSH access.
- `agent/wp-cli/README.md`: Define guarded WP-CLI access.
- `agent/database/README.md`: Define read-only database access.
- `docs/ACCESS.md`: List connection owners and activation requirements.
- `docs/SAFETY.md`: Define prohibited production actions.
- `docs/SITE-INVENTORY.md`: Record the initial public site scope.
- `scripts/validate-environment.sh`: Validate one access-specific environment.
- `scripts/with-environment.sh`: Load reviewed values for one access mode.
- `scripts/rest-public-read.sh`: Read the public REST index without credentials.
- `scripts/check-committed-diff.sh`: Check committed CI changes for whitespace errors.
- `scripts/wp-read.sh`: Allow an explicit set of remote WP-CLI read commands.
- `scripts/db-read.sh`: Allow read-only SQL statements.
- `tests/shell/guards.bats`: Test the shell guards without remote connections.
- `tests/unit/ci-workflow.test.js`: Test the offline workflow contract.
- `tests/unit/committed-diff.test.js`: Test CI commit-range selection.
- `.github/workflows/validate.yml`: Run offline repository validation.
- `wp-content/themes/custom-theme/.gitkeep`: Keep the custom theme directory.
- `wp-content/plugins/custom-plugin/.gitkeep`: Keep the custom plugin directory.
- `wp-content/mu-plugins/custom-mu-plugin.php`: Load custom must-use plugins.
- `wp-content/mu-plugins/custom-mu-plugin/.gitkeep`: Keep the custom must-use plugin directory.
- `scripts/validate-mu-plugin-loader.sh`: Validate the loader mount and PHP syntax.

### Task 1: Repository contract and documentation

**Files:**

- Create: `AGENTS.md`
- Create: `README.md`
- Create: `.env.example`
- Create: `.gitignore`
- Create: `docs/ACCESS.md`
- Create: `docs/SAFETY.md`
- Create: `docs/SITE-INVENTORY.md`

- [x] **Step 1: Write the repository contract test**

Run:

```bash
test -f AGENTS.md
test -f README.md
test -f .env.example
test -f .gitignore
test -f docs/ACCESS.md
test -f docs/SAFETY.md
test -f docs/SITE-INVENTORY.md
```

Expected: The command fails because the files do not exist.

- [x] **Step 2: Create the Agent policy**

Create `AGENTS.md` with these rules:

```markdown
# Agent instructions

Use ASB-STE100 style for all new prose.

## Scope

Work only in this repository.
Use `https://phyllisschlafly.com` as the production site URL.
Keep all access through this scaffold read-only.
Use a separate access method and change procedure for an approved production write.

## Required safety rules

- Do not store secrets, private keys, database exports, or uploads in Git.
- Do not change production files, content, settings, users, or database records.
- Do not run a WP-CLI command that changes production state.
- Do not run SQL other than `SELECT`, `SHOW`, `DESCRIBE`, or `EXPLAIN`.
- Do not disable TLS verification.
- Do not add production credentials to GitHub Actions.
- Stop when a required host, path, or credential is not available.

## Validation

Run `npm test` before each commit.
Run `docker compose config` after each Docker Compose change.
```

- [x] **Step 3: Create the environment contract**

Create `.env.example`:

```dotenv
SITE_URL=https://phyllisschlafly.com
LOCAL_SITE_URL=http://localhost:8080

WP_LOCAL_DB_NAME=wordpress
WP_LOCAL_DB_USER=wordpress
WP_LOCAL_DB_PASSWORD=local-wordpress-only
WP_LOCAL_DB_ROOT_PASSWORD=local-root-only

WP_REST_USER=replace-with-wordpress-user
WP_REST_APPLICATION_PASSWORD=replace-with-application-password

WP_SSH_HOST=replace-with-host
WP_SSH_PORT=22
WP_SSH_USER=replace-with-restricted-user
WP_SSH_KEY_PATH=replace-with-absolute-key-path
WP_SSH_KNOWN_HOSTS_PATH=replace-with-absolute-known-hosts-path
WP_REMOTE_PATH=replace-with-wordpress-path

WP_DB_HOST=replace-with-read-only-database-host
WP_DB_PORT=3306
WP_DB_NAME=replace-with-database-name
WP_DB_USER=replace-with-read-only-user
WP_DB_PASSWORD=replace-with-read-only-password
WP_DB_SSL_MODE=VERIFY_IDENTITY
WP_DB_SSL_CA=/absolute/path/to/production-database-ca.crt
```

- [x] **Step 4: Create exclusions**

Create `.gitignore`:

```gitignore
.env
.env.*
!.env.example
node_modules/
playwright-report/
test-results/
.DS_Store
*.log
*.sql
*.sql.gz
*.pem
*.key
*.crt
wp-admin/
wp-includes/
wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-content/backup*/
```

- [x] **Step 5: Write setup and safety documentation**

Write `README.md` with the purpose, prerequisites, local setup commands, validation commands, and access activation sequence.
Include `git diff --cached` and `git diff --cached --check` in the validation commands.
Write `docs/ACCESS.md` with separate sections for Playwright, WordPress MCP, REST API, SSH, WP-CLI, and database access.
Require `WP_DB_SSL_MODE=VERIFY_IDENTITY` and the CA certificate path in the database access section.
Require certificate and hostname verification in the database access section.
Write `docs/SAFETY.md` with the prohibited operations from `AGENTS.md`.
Add separate steps to run `git diff --cached` and review the complete staged diff.
Write `docs/SITE-INVENTORY.md` with the public URL and these public sections: About, Events, Media, Archives, Contact, Shop, Phyllis Schlafly Report, Weekly Column, Education Reporter, and Radio Broadcasts.

- [x] **Step 6: Run the contract test**

Run the command from Step 1.

Expected: PASS with exit code `0`.

- [x] **Step 7: Commit**

```bash
git add AGENTS.md README.md .env.example .gitignore docs
git commit -m "Add repository safety contract"
```

### Task 2: Local WordPress environment

**Files:**

- Create: `docker-compose.yml`
- Create: `wp-content/themes/custom-theme/.gitkeep`
- Create: `wp-content/plugins/custom-plugin/.gitkeep`
- Create: `wp-content/mu-plugins/custom-mu-plugin.php`
- Create: `wp-content/mu-plugins/custom-mu-plugin/.gitkeep`
- Create: `scripts/validate-mu-plugin-loader.sh`

- [x] **Step 1: Write the failing configuration check**

Run:

```bash
docker compose --env-file .env.example config --quiet
```

Expected: FAIL because `docker-compose.yml` does not exist.

- [x] **Step 2: Create `docker-compose.yml`**

```yaml
services:
  database:
    image: mariadb:11.4.12@sha256:a794d9eb009e20de605858a11f32f63b4075cbd197c650436f0e3b457e4caed7
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: ${WP_LOCAL_DB_NAME}
      MARIADB_USER: ${WP_LOCAL_DB_USER}
      MARIADB_PASSWORD: ${WP_LOCAL_DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${WP_LOCAL_DB_ROOT_PASSWORD}
    volumes:
      - wordpress_database:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  wordpress:
    image: wordpress:6.9.1-php8.3-apache@sha256:ffef0dca1f0fc4357bfef3856ebd1ba18f7b394378277122eaa4524ca2619d43
    restart: unless-stopped
    depends_on:
      database:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_DB_HOST: database:3306
      WORDPRESS_DB_NAME: ${WP_LOCAL_DB_NAME}
      WORDPRESS_DB_USER: ${WP_LOCAL_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WP_LOCAL_DB_PASSWORD}
    volumes:
      - ./wp-content/mu-plugins/custom-mu-plugin.php:/var/www/html/wp-content/mu-plugins/custom-mu-plugin.php:ro
      - ./wp-content/themes/custom-theme:/var/www/html/wp-content/themes/custom-theme
      - ./wp-content/plugins/custom-plugin:/var/www/html/wp-content/plugins/custom-plugin
      - ./wp-content/mu-plugins/custom-mu-plugin:/var/www/html/wp-content/mu-plugins/custom-mu-plugin
    healthcheck:
      test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1/wp-login.php') === false);"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s

volumes:
  wordpress_database:
```

- [x] **Step 3: Keep the source directories**

Create empty `.gitkeep` files in:

```text
wp-content/themes/custom-theme/.gitkeep
wp-content/plugins/custom-plugin/.gitkeep
wp-content/mu-plugins/custom-mu-plugin/.gitkeep
```

Create `wp-content/mu-plugins/custom-mu-plugin.php`.
Load each readable PHP file from `custom-mu-plugin` in name order.
Reject direct loader execution outside WordPress.

Create `scripts/validate-mu-plugin-loader.sh`.
Confirm that Docker Compose mounts the loader at the must-use plugin root.
Run `php -l` when PHP is available.

- [x] **Step 4: Validate the configuration**

Run:

```bash
docker compose --env-file .env.example config --quiet
scripts/validate-mu-plugin-loader.sh
```

Expected: PASS with exit code `0`.

- [x] **Step 5: Run the local startup check**

Start the isolated local stack:

```bash
docker compose --env-file .env.example up -d --wait
docker compose --env-file .env.example ps
curl --fail --silent --show-error --output /dev/null http://127.0.0.1:8080/wp-login.php
find wp-content -mindepth 1 -maxdepth 4 -print
docker compose --env-file .env.example exec -T wordpress \
  php -l /var/www/html/wp-content/mu-plugins/custom-mu-plugin.php
docker compose --env-file .env.example down --volumes
```

Expected: Both containers become healthy.
Expected: WordPress returns an HTTP response.
Expected: The loader has valid PHP syntax.
Expected: Each custom component directory contains only its tracked source.

- [x] **Step 6: Commit**

```bash
git add .env.example README.md docker-compose.yml docs/superpowers scripts wp-content
git commit -m "Add local WordPress environment"
```

### Task 3: Browser and Playwright checks

**Files:**

- Create: `package.json`
- Create: `playwright.config.js`
- Create: `tests/playwright/public-site.spec.js`
- Create: `tests/playwright/local-site.spec.js`

- [x] **Step 1: Create the package contract**

Create `package.json`:

```json
{
  "name": "ps-site-rebuild",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "test": "npm run test:offline",
    "test:offline": "bash -n scripts/*.sh && docker compose --env-file .env.example config --quiet",
    "test:public": "playwright test tests/playwright/public-site.spec.js",
    "test:local": "playwright test tests/playwright/local-site.spec.js"
  },
  "devDependencies": {
    "@playwright/test": "^1.54.0"
  }
}
```

- [x] **Step 2: Install the locked dependencies**

Run:

```bash
npm install
```

Expected: npm creates `package-lock.json` and exits with code `0`.

- [x] **Step 3: Create Playwright configuration**

Create `playwright.config.js`:

```javascript
const { defineConfig } = require("@playwright/test");

module.exports = defineConfig({
  testDir: "./tests/playwright",
  timeout: 30000,
  retries: 1,
  use: {
    trace: "retain-on-failure"
  }
});
```

- [x] **Step 4: Write the public-site test**

Create `tests/playwright/public-site.spec.js`:

```javascript
const { test, expect } = require("@playwright/test");

test("production home page is publicly readable", async ({ page }) => {
  const siteUrl = process.env.SITE_URL || "https://phyllisschlafly.com";
  const response = await page.goto(siteUrl, { waitUntil: "domcontentloaded" });
  expect(response).not.toBeNull();
  expect(response.ok()).toBeTruthy();
  await expect(page).toHaveTitle(/Eagle Forum|Phyllis Schlafly/i);
});
```

- [x] **Step 5: Write the local-site test**

Create `tests/playwright/local-site.spec.js`:

```javascript
const { test, expect } = require("@playwright/test");

test("local WordPress responds", async ({ page }) => {
  const siteUrl = process.env.LOCAL_SITE_URL || "http://localhost:8080";
  const response = await page.goto(siteUrl, { waitUntil: "domcontentloaded" });
  expect(response).not.toBeNull();
  expect(response.status()).toBeLessThan(500);
});
```

- [x] **Step 6: Run the public check**

Run:

```bash
npx playwright install chromium
npm run test:public
```

Expected: PASS for `production home page is publicly readable`.

- [x] **Step 7: Commit**

```bash
git add package.json package-lock.json playwright.config.js tests/playwright
git commit -m "Add Playwright site checks"
```

### Task 4: Connection templates

**Files:**

- Create: `agent/wordpress-mcp/README.md`
- Create: `agent/wordpress-mcp/server.example.json`
- Create: `agent/rest-api/README.md`
- Create: `agent/ssh/README.md`
- Create: `agent/wp-cli/README.md`
- Create: `agent/database/README.md`

- [x] **Step 1: Write the failing template check**

Run:

```bash
test -f agent/wordpress-mcp/server.example.json
test -f agent/wordpress-mcp/README.md
test -f agent/rest-api/README.md
test -f agent/ssh/README.md
test -f agent/wp-cli/README.md
test -f agent/database/README.md
```

Expected: FAIL because the templates do not exist.

- [x] **Step 2: Create the disabled WordPress MCP template**

Create `agent/wordpress-mcp/server.example.json`:

```json
{
  "manifestType": "repository-access-documentation",
  "enabled": false,
  "client": {
    "selected": false,
    "name": null
  },
  "server": {
    "selected": false,
    "name": null
  },
  "transport": "stdio",
  "requiredEnvironmentVariables": [
    "SITE_URL",
    "WP_REST_USER",
    "WP_REST_APPLICATION_PASSWORD"
  ]
}
```

- [x] **Step 3: Write connection instructions**

For each access method, document:

1. The variables from `.env.example`.
2. The required least-privilege account.
3. A verification command when the access method permits zero production writes.
4. The prohibited write operations.
5. The operator approval needed to enable access.

Use an unauthenticated public REST check.
Keep MCP disabled.
Do not use the application-password placeholders through this scaffold.
Use these permitted verification commands:

```bash
scripts/with-environment.sh rest .env scripts/rest-public-read.sh
printf -v remote_command 'test -d %q' "$WP_REMOTE_PATH"
ssh -F none \
  -p "${WP_SSH_PORT}" -i "${WP_SSH_KEY_PATH}" \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o PreferredAuthentications=publickey \
  -o PasswordAuthentication=no \
  -o KbdInteractiveAuthentication=no \
  -o ClearAllForwardings=yes \
  -o ForwardAgent=no \
  -o PermitLocalCommand=no \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile="${WP_SSH_KNOWN_HOSTS_PATH}" \
  "${WP_SSH_USER}@${WP_SSH_HOST}" \
  "$remote_command"
./scripts/wp-read.sh core version
./scripts/db-read.sh "SHOW TABLES"
```

- [x] **Step 4: Run the template check**

Run the command from Step 1.

Expected: PASS with exit code `0`.

- [x] **Step 5: Commit**

```bash
git add agent
git commit -m "Add production access templates"
```

### Task 5: Guarded remote inspection scripts

**Files:**

- Create: `scripts/validate-environment.sh`
- Create: `scripts/with-environment.sh`
- Create: `scripts/rest-public-read.sh`
- Create: `scripts/wp-read.sh`
- Create: `scripts/db-read.sh`
- Create: `tests/shell/guards.bats`

- [x] **Step 1: Confirm the standalone Bash test design**

Run:

```bash
bash --version
```

Expected: Bash prints its installed version.

`tests/shell/guards.bats` is a standalone Bash harness.
The `.bats` suffix remains for repository compatibility.
The harness does not require the Bats executable.
Do not install Bats locally or in CI.

- [x] **Step 2: Write failing standalone guard tests**

Create `tests/shell/guards.bats`:

```bash
#!/usr/bin/env bash
set -u

test_count=0
failure_count=0

run_test() {
  local name="$1"
  shift

  test_count=$((test_count + 1))
  if "$@"; then
    printf 'ok %s - %s\n' "$test_count" "$name"
  else
    failure_count=$((failure_count + 1))
    printf 'not ok %s - %s\n' "$test_count" "$name"
  fi
}

test_wp_rejects_write_command() {
  output="$(./scripts/wp-read.sh plugin install akismet 2>&1)"
  status=$?
  [[ "$status" -eq 64 ]]
  [[ "$output" == *"Command is not in the read-only allowlist."* ]]
}

run_test 'WP guard rejects a write command' test_wp_rejects_write_command

if ((failure_count > 0)); then
  exit 1
fi
```

Also test these controls:

- Reject newline input followed by `\!`, `\.`, `\r`, or `\q`.
- Confirm that rejected backslash input does not run `mysql`.
- Confirm that `--no-defaults` and `--no-login-paths` are the first MySQL options.
- Confirm that `-F none` are the first SSH options.

Run:

```bash
chmod +x tests/shell/guards.bats
tests/shell/guards.bats
```

Expected: FAIL because the scripts do not exist.

- [x] **Step 3: Create the environment validator**

Create `scripts/validate-environment.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

mode="${1:-}"
local_vars=(LOCAL_SITE_URL WP_LOCAL_DB_NAME WP_LOCAL_DB_USER WP_LOCAL_DB_PASSWORD WP_LOCAL_DB_ROOT_PASSWORD)
rest_vars=(SITE_URL WP_REST_USER WP_REST_APPLICATION_PASSWORD)
ssh_vars=(WP_SSH_HOST WP_SSH_PORT WP_SSH_USER WP_SSH_KEY_PATH WP_SSH_KNOWN_HOSTS_PATH WP_REMOTE_PATH)
database_vars=(WP_DB_HOST WP_DB_PORT WP_DB_NAME WP_DB_USER WP_DB_PASSWORD WP_DB_SSL_MODE WP_DB_SSL_CA)

case "$mode" in
  local) required=("${local_vars[@]}") ;;
  rest) required=("${rest_vars[@]}") ;;
  ssh) required=("${ssh_vars[@]}") ;;
  database) required=("${database_vars[@]}") ;;
  *)
    printf 'Usage: %s <local|rest|ssh|database>\n' "$0" >&2
    exit 64
    ;;
esac

missing=()
for name in "${required[@]}"; do
  if [[ -z "${!name:-}" ]]; then
    missing+=("$name")
  fi
done

if ((${#missing[@]})); then
  printf 'Missing environment variable: %s\n' "${missing[@]}" >&2
  exit 65
fi

printf 'Environment validation passed for %s mode.\n' "$mode"
```

Create `scripts/with-environment.sh`.
Accept only `local`, `rest`, `ssh`, and `database`.
Clear every repository contract variable before file parsing.
Accept only known `NAME=VALUE` entries from the reviewed file.
Export only variables for the selected access mode.
Reject the old combined mode.

- [x] **Step 4: Create the WP-CLI guard**

Create `scripts/wp-read.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

if (($# < 1)); then
  printf 'Usage: %s <read-only-wp-command> [arguments]\n' "$0" >&2
  exit 64
fi

command_key="${1}${2:+ $2}"
case "$command_key" in
  "core version"|"core is-installed"|"plugin list"|"theme list"|"option get"|"post list"|"user list"|"site list")
    ;;
  *)
    printf 'Command is not in the read-only allowlist.\n' >&2
    exit 64
    ;;
esac

: "${WP_SSH_HOST:?WP_SSH_HOST is required.}"
: "${WP_SSH_PORT:?WP_SSH_PORT is required.}"
: "${WP_SSH_USER:?WP_SSH_USER is required.}"
: "${WP_SSH_KEY_PATH:?WP_SSH_KEY_PATH is required.}"
: "${WP_SSH_KNOWN_HOSTS_PATH:?WP_SSH_KNOWN_HOSTS_PATH is required.}"
: "${WP_REMOTE_PATH:?WP_REMOTE_PATH is required.}"

printf -v remote_command '%q ' wp "--path=${WP_REMOTE_PATH}" "$@"
ssh -F none \
  -p "$WP_SSH_PORT" -i "$WP_SSH_KEY_PATH" \
  -o BatchMode=yes \
  -o IdentitiesOnly=yes \
  -o PreferredAuthentications=publickey \
  -o PasswordAuthentication=no \
  -o KbdInteractiveAuthentication=no \
  -o ClearAllForwardings=yes \
  -o ForwardAgent=no \
  -o PermitLocalCommand=no \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile="$WP_SSH_KNOWN_HOSTS_PATH" \
  "${WP_SSH_USER}@${WP_SSH_HOST}" \
  "$remote_command"
```

- [x] **Step 5: Create the database guard**

Create `scripts/db-read.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

sql="${1:-}"
if [[ -z "$sql" ]]; then
  printf 'Usage: %s "SELECT ..."\n' "$0" >&2
  exit 64
fi

trimmed="${sql#"${sql%%[![:space:]]*}"}"
if [[ "$sql" == *\\* ]]; then
  printf 'MySQL client commands and backslash escapes are not allowed.\n' >&2
  exit 64
fi
if [[ "$trimmed" == *";"* && "${trimmed%;}" == *";"* ]]; then
  printf 'Multiple SQL statements are not allowed.\n' >&2
  exit 64
fi

if [[ "$trimmed" == *"/*"* || "$trimmed" == *"#"* || "$trimmed" =~ --[[:space:]] ]]; then
  printf 'SQL comments are not allowed.\n' >&2
  exit 64
fi

if [[ ! "$trimmed" =~ ^(SELECT|SHOW|DESCRIBE|EXPLAIN)[[:space:]] ]]; then
  printf 'Only read-only SQL is allowed.\n' >&2
  exit 64
fi

if [[ "$trimmed" =~ INTO[[:space:]]+(OUTFILE|DUMPFILE) ]]; then
  printf 'SQL file writes are not allowed.\n' >&2
  exit 64
fi

: "${WP_DB_HOST:?WP_DB_HOST is required.}"
: "${WP_DB_PORT:?WP_DB_PORT is required.}"
: "${WP_DB_NAME:?WP_DB_NAME is required.}"
: "${WP_DB_USER:?WP_DB_USER is required.}"
: "${WP_DB_PASSWORD:?WP_DB_PASSWORD is required.}"
: "${WP_DB_SSL_MODE:?WP_DB_SSL_MODE is required.}"
: "${WP_DB_SSL_CA:?WP_DB_SSL_CA is required.}"

if [[ "$WP_DB_SSL_MODE" != "VERIFY_IDENTITY" ]]; then
  printf 'WP_DB_SSL_MODE must be VERIFY_IDENTITY.\n' >&2
  exit 65
fi

MYSQL_PWD="$WP_DB_PASSWORD" mysql \
  --no-defaults \
  --no-login-paths \
  --host="$WP_DB_HOST" \
  --port="$WP_DB_PORT" \
  --user="$WP_DB_USER" \
  --database="$WP_DB_NAME" \
  --ssl-mode=VERIFY_IDENTITY \
  --ssl-ca="$WP_DB_SSL_CA" \
  --batch \
  --execute="$sql"
```

- [x] **Step 6: Run the guard tests**

Run:

```bash
chmod +x scripts/*.sh
tests/shell/guards.bats
bash -n scripts/*.sh
```

Expected: The standalone guard tests pass. Bash reports no syntax errors.

- [x] **Step 7: Commit**

```bash
git add scripts tests/shell
git commit -m "Add read-only production guards"
```

### Task 6: Offline continuous integration

**Files:**

- Create: `.github/workflows/validate.yml`
- Create: `scripts/check-committed-diff.sh`
- Create: `tests/unit/ci-workflow.test.js`
- Create: `tests/unit/committed-diff.test.js`
- Modify: `package.json`

- [x] **Step 1: Add the offline test command**

Change the `test:offline` script in `package.json` to:

```json
"test:offline": "node --test tests/unit/*.test.js && bash -n scripts/*.sh tests/shell/*.sh tests/shell/*.bats && docker compose --env-file .env.example config --quiet && playwright test --list && scripts/validate-mu-plugin-loader.sh && scripts/validate-access-templates.sh && tests/shell/read-only-guards.sh && tests/shell/guards.bats && git diff --check"
```

Run `tests/shell/guards.bats` directly as a standalone Bash harness.
Do not install Bats or add a root package installation.

- [x] **Step 2: Write failing committed-range tests**

Add unit tests for these cases:

- A pull request uses `<base>...HEAD`.
- A normal push uses `<before>..HEAD`.
- An initial push compares Git's empty tree to the complete `HEAD` tree.
- A missing base compares Git's empty tree to the complete `HEAD` tree.
- A whitespace error from an earlier initial-push commit fails the check.
- Invalid commit text stops before Git runs.

Run:

```bash
node --test tests/unit/ci-workflow.test.js tests/unit/committed-diff.test.js
```

Expected: FAIL because the workflow controls and committed-diff script do not exist.

- [x] **Step 3: Add committed-range validation**

Create `scripts/check-committed-diff.sh`.
Read the event name and commit values from environment variables.
Validate each nonempty commit value as a hexadecimal commit SHA.
Pass each validated range to Git as one quoted argument.
Do not evaluate event text as shell syntax.

```bash
#!/usr/bin/env bash
set -euo pipefail

check_complete_tree() {
  local empty_tree
  empty_tree="$(git hash-object -t tree /dev/null)"
  git diff --check "$empty_tree" HEAD
}

check_range() {
  local base_sha="$1"
  local separator="$2"

  if [[ -z "$base_sha" ]]; then
    check_complete_tree
    return
  fi

  if [[ ! "$base_sha" =~ ^[0-9a-fA-F]{40,64}$ ]]; then
    printf 'The event value is not a valid commit SHA.\n' >&2
    exit 64
  fi

  if [[ "$base_sha" =~ ^0+$ ]]; then
    check_complete_tree
    return
  fi

  git diff --check "${base_sha}${separator}HEAD"
}

case "${CI_EVENT_NAME:-}" in
  pull_request)
    check_range "${CI_PR_BASE_SHA:-}" '...'
    ;;
  push)
    check_range "${CI_PUSH_BEFORE_SHA:-}" '..'
    ;;
  *)
    printf 'CI_EVENT_NAME must be pull_request or push.\n' >&2
    exit 64
    ;;
esac
```

- [x] **Step 4: Create the validation workflow**

Create `.github/workflows/validate.yml`:

```yaml
name: Validate

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Check committed changes
        env:
          CI_EVENT_NAME: ${{ github.event_name }}
          CI_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
          CI_PUSH_BEFORE_SHA: ${{ github.event.before }}
        run: scripts/check-committed-diff.sh
      - name: Set up Node.js
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 22
          cache: npm
      - name: Install dependencies
        run: npm ci
      - name: Run offline tests
        run: npm test
```

- [x] **Step 5: Run offline checks**

Run:

```bash
npm test
```

Expected: Unit tests, Bash syntax, guard tests, Docker Compose validation, template validation, and Playwright discovery pass.

- [x] **Step 6: Confirm that CI has no production connection**

Run:

```bash
rg -n 'SITE_URL|WP_SSH_|WP_DB_|test:public|test:local' .github/workflows
```

Expected: No matches.

- [x] **Step 7: Commit**

```bash
git add package.json .github/workflows/validate.yml scripts/check-committed-diff.sh tests/unit
git commit -m "Add offline repository validation"
```

### Task 7: Final verification and private GitHub repository

**Files:**

- Verify: All tracked files
- Modify: Git remote configuration

- [x] **Step 1: Run the full local verification**

Run:

```bash
npm test
git diff --check
git status --short
git ls-files | rg '(^|/)(\\.env$|.*\\.pem$|.*\\.key$|.*\\.sql(\\.gz)?$)' && exit 1 || true
```

Expected: All tests pass. Git reports no uncommitted changes. The secret-file scan reports no tracked secret files.

- [x] **Step 2: Record the repository location**

The private repository exists at:

`https://github.com/Eagle-Forum-Education-Legal/PS-Site-Rebuild`

- [x] **Step 3: Confirm the private repository**

The repository is private.
The default branch is `main`.

- [x] **Step 4: Verify remote properties**

Run:

```bash
gh repo view Eagle-Forum-Education-Legal/PS-Site-Rebuild \
  --json nameWithOwner,visibility,defaultBranchRef,url \
  --jq '{nameWithOwner, visibility, defaultBranch: .defaultBranchRef.name, url}'
```

Expected:

```json
{
  "defaultBranch": "main",
  "nameWithOwner": "Eagle-Forum-Education-Legal/PS-Site-Rebuild",
  "url": "https://github.com/Eagle-Forum-Education-Legal/PS-Site-Rebuild",
  "visibility": "PRIVATE"
}
```

- [x] **Step 5: Verify the pushed branch**

Run:

```bash
git status --short --branch
git log --oneline --decorate -7
```

Expected: `main` tracks `origin/main`. The branch has no uncommitted changes.

## Completion

All plan tasks are complete.
The final repository is `https://github.com/Eagle-Forum-Education-Legal/PS-Site-Rebuild`.
The final environment modes are `local`, `rest`, `ssh`, and `database`.
The loader clears ambient contract values before it loads reviewed values.
This plan remains as historical implementation documentation.
