Skills / Design

build-modify

Session-aware per-step execution engine for /modify - surgical theme edits, WP-CLI content/plugin changes, lazy Docker startup, and versioned output on session completion

Source Shaisolaris/solaris-dev-shopLicense MITSafety scan Clear

No fetch-and-execute, destructive, credential, exfiltration or rules-override patterns in 1 files, checked on Oct 20, 2026. This is not a guarantee of safety. How the scan works.

npx degit Shaisolaris/solaris-dev-shop/employees/specialized/wordpress-master/diagnostics/build-modify .claude/skills/build-modify

12 alternatives · Best design skills · Author? Prove this skill

SKILL.md
# Build Modify Skill

Session-aware execution engine for the `/modify` command. COMMAND.md owns the interactive session loop and invokes this skill's sections per step. Each atomic modification step produces its own git commit and build.json entry. No zip or SETUP.md regeneration occurs per step - those are deferred to session completion (Section 7).

This skill expects the following variables to be set by the calling command (`/modify`) before invocation:

| Variable | Source | Description |
|---|---|---|
| `WP_DIR` | /modify argument | Path to WordPress directory (set once at session start) |
| `MODIFY_MODE` | /modify session loop | `"nl"` for natural language, `"visual"` for design re-export (set per step) |
| `NL_REQUEST` | /modify session loop | Natural language modification request (set per NL step) |
| `VISUAL_PATH` | /modify session loop | Path to updated design export directory (set per visual step) |

---

## Section 0: Prerequisites and Setup

Set up lazy Docker/WP-CLI helpers, declare the `git_commit_stage` helper function, and register an EXIT trap for MySQL container cleanup. Docker is NOT checked upfront - it is started lazily only when a content or plugin step requires MySQL.

```bash
# ── Lazy Docker/WP-CLI helpers ───────────────────────────────────────────────
# Docker and WP-CLI are only needed for content and plugin steps.
# Theme-only sessions (theme-token, template-edit) work without Docker entirely.

WP_CLI_MODE=""
WP=""
DOCKER_CHECKED=false

ensure_wp_cli() {
  if [ "$DOCKER_CHECKED" = "true" ]; then
    return 0
  fi

  # Check Docker is running
  if ! docker info > /dev/null 2>&1; then
    echo "[Modify] ERROR: Docker is required for content and plugin modifications."
    echo "[Modify] Start Docker Desktop and try again."
    echo "[Modify] Theme-only modifications (theme-token, template-edit) work without Docker."
    return 1
  fi

  # Check for local WP-CLI first (preferred), Docker fallback
  if which wp > /dev/null 2>&1; then
    WP_CLI_MODE="local"
    WP="wp --path=$BUILD_DIR"
    echo "[Modify] WP-CLI ready (local)"
  else
    if docker run --rm wordpress:cli wp --version > /dev/null 2>&1; then
      WP_CLI_MODE="docker"
      WP="docker run --rm -v \"$BUILD_DIR:/var/www/html\" --network host wordpress:cli wp --allow-root"
      echo "[Modify] WP-CLI ready (docker)"
    else
      echo "[Modify] ERROR: WP-CLI is required for content/plugin modifications but was not found."
      return 1
    fi
  fi

  DOCKER_CHECKED=true
  return 0
}

ensure_docker_mysql() {
  # Ensure WP-CLI is available first
  if ! ensure_wp_cli; then
    STEP_SKIPPED=true
    return 1
  fi

  # Check database.sql exists
  if [ ! -f "$BUILD_DIR/database.sql" ]; then
    echo "[Modify] ERROR: database.sql not found in build directory."
    echo "[Modify] Cannot perform content modifications without a database."
    echo "[Modify] Theme-only modifications (theme-token, template-edit) are still possible."
    STEP_SKIPPED=true
    return 1
  fi

  # If MySQL is already running, reuse it
  if [ "$MYSQL_RUNNING" = "true" ]; then
    return 0
  fi

  # Read existing credentials from wp-config.php
  EXISTING_DB_PASS=$($WP config get DB_PASSWORD 2>/dev/null || echo "")
  EXISTING_DB_USER=$($WP config get DB_USER 2>/dev/null || echo "wp")

  if [ -z "$EXISTING_DB_PASS" ]; then
    EXISTING_DB_PASS="$(openssl rand -hex 12)"
    EXISTING_DB_USER="wp"
    echo "[Modify] NOTE: Could not read existing DB credentials - using generated credentials"
  fi

  # Port 3307 conflict check
  if lsof -i :3307 > /dev/null 2>&1; then
    echo "[Modify] ERROR: Port 3307 is already in use. Close the conflicting process and try again."
    STEP_SKIPPED=true
    return 1
  fi

  MYSQL_ROOT_PASS="wpmod_root_$(openssl rand -hex 8)"

  # Start MySQL container with credentials matching wp-config.php
  docker run -d \
    --name "$MYSQL_CONTAINER" \
    -e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASS" \
    -e MYSQL_DATABASE=wordpress \
    -e MYSQL_USER="$EXISTING_DB_USER" \
    -e MYSQL_PASSWORD="$EXISTING_DB_PASS" \
    -p 127.0.0.1:3307:3306 \
    mysql:8.0 \
    --default-authentication-plugin=mysql_native_password \
    > /dev/null

  MYSQL_RUNNING=true
  echo "[Modify] Starting MySQL container..."

  # Wait for ready (up to 30 seconds)
  WAIT_COUNT=0
  until docker exec "$MYSQL_CONTAINER" mysqladmin ping --silent 2>/dev/null; do
    WAIT_COUNT=$((WAIT_COUNT + 1))
    if [ "$WAIT_COUNT" -ge 30 ]; then
      echo "[Modify] ERROR: MySQL container failed to start within 30 seconds."
      STEP_SKIPPED=true
      return 1
    fi
    sleep 1
  done

  echo "[Modify] Database ready."

  # Import existing database.sql
  if ! $WP db import "$BUILD_DIR/database.sql" 2>&1; then
    echo "[Modify] ERROR: Database import failed. Cannot proceed with content modification."
    STEP_SKIPPED=true
    return 1
  fi

  echo "[Modify] Database imported into ephemeral container."
  return 0
}

# ── MySQL container EXIT trap ────────────────────────────────────────────────

MYSQL_CONTAINER="wpmodify-mysql-$$"
MYSQL_RUNNING=false

cleanup_modify_mysql() {
  if [ "$MYSQL_RUNNING" = "true" ]; then
    echo "[Modify] Cleaning up Docker MySQL container..."
    docker rm -f "$MYSQL_CONTAINER" 2>/dev/null || true
    echo "[Modify] MySQL cleanup complete."
  fi
}
trap cleanup_modify_mysql EXIT

# ── git_commit_stage function ────────────────────────────────────────────────
# Redeclared from build-git Section 4 - skills are separate execution contexts.
# Usage: git_commit_stage "commit subject" ["commit body"]
# Failures are warn-and-continue - git failures never abort the modification.

git_commit_stage() {
  local SUBJECT="$1"
  local BODY="${2:-}"

  if [ "${GIT_INITIALIZED:-true}" != "true" ]; then
    echo "[Modify] WARNING: Git not initialized - skipping commit: $SUBJECT"
    return 0
  fi

  echo "[Modify] Git commit: $SUBJECT"

  # Stage all changes
  if ! git -C "$BUILD_DIR" add -A > /dev/null 2>&1; then
    echo "[Modify] WARNING: git add failed for commit: $SUBJECT"
    return 0
  fi

  # Check if there is anything to commit
  if git -C "$BUILD_DIR" diff --cached --quiet 2>/dev/null; then
    echo "[Modify] NOTE: Nothing to commit for: $SUBJECT (no changes staged)"
    return 0
  fi

  # Commit - use -m twice for multiline commits (subject + body)
  if [ -n "$BODY" ]; then
    if git -C "$BUILD_DIR" commit -m "$SUBJECT" -m "$BODY" > /dev/null 2>&1; then
      echo "[Modify] Committed: $SUBJECT"
    else
      echo "[Modify] WARNING: git commit failed for: $SUBJECT"
    fi
  else
    if git -C "$BUILD_DIR" commit -m "$SUBJECT" > /dev/null 2>&1; then
      echo "[Modify] Committed: $SUBJECT"
    else
      echo "[Modify] WARNING: git commit failed for: $SUBJECT"
    fi
  fi

  echo ""
}
```

## Section 1: WordPress Directory Validation and State Loading

Use the WordPress directory path (WP_DIR) directly. Validate wp-content/ exists, load build.json if present, or create a minimal one for external WordPress directories. This section runs once at session start.

```bash
BUILD_DIR="$WP_DIR"
echo "[Modify] WordPress directory: $BUILD_DIR"
echo ""

# ── Validate wp-content/ exists (belt-and-suspenders - COMMAND.md already checked) ──

if [ ! -d "$BUILD_DIR/wp-content" ]; then
  echo "ERROR: wp-content/ not found in $BUILD_DIR"
  echo "This does not appear to be a WordPress installation."
  exit 1
fi

# ── Load or create build.json ────────────────────────────────────────────────

BUILD_JSON_PATH="$BUILD_DIR/build.json"

if [ -f "$BUILD_JSON_PATH" ]; then
  echo "[Modify] Loading state from build.json..."

  STATE_JSON=$(python3 << 'PYEOF'
import json, os, sys

build_json_path = os.path.join(os.environ.get("BUILD_DIR", ""), "build.json")
try:
    with open(build_json_path) as f:
        state = json.load(f)
    print(json.dumps(state))
except Exception:
    print("{}")
PYEOF
  )

  # Extract key state values
  THEME_SLUG=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('theme',{}).get('slug',''))" 2>/dev/null)
  CREATION_MODE=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('creation_mode', d.get('mode','')))" 2>/dev/null)
  EXISTING_MODS=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('modifications',[])))" 2>/dev/null)
else
  echo "[Modify] No build.json found - this appears to be an external WordPress directory."
  echo "[Modify] Creating build.json to track modifications."

  # Auto-detect theme slug: first non-twentytwenty* theme in wp-content/themes/
  THEME_SLUG=$(python3 << PYEOF
import os, sys

themes_dir = os.path.join("$BUILD_DIR", "wp-content", "themes")
if not os.path.isdir(themes_dir):
    print("")
    sys.exit(0)

themes = sorted(os.listdir(themes_dir))
# Prefer non-core themes
for t in themes:
    if os.path.isdir(os.path.join(themes_dir, t)) and not t.startswith("twentytwenty"):
        print(t)
        sys.exit(0)
# Fallback to first theme
for t in themes:
    if os.path.isdir(os.path.join(themes_dir, t)):
        print(t)
        sys.exit(0)
print("")
PYEOF
  )

  CREATION_MODE="external"
  EXISTING_MODS=0

  # Create minimal build.json
  python3 << PYEOF
import json, os
from datetime import datetime, timezone

build_dir = "$BUILD_DIR"
theme_slug = "$THEME_SLUG"
build_id = os.path.basename(os.path.abspath(build_dir))

state = {
    "build_id": build_id,
    "creation_mode": "external",
    "created_at": datetime.now(timezone.utc).isoformat(),
    "build_dir": os.path.abspath(build_dir),
    "theme": {"slug": theme_slug} if theme_slug else {},
    "modifications": []
}

build_json_path = os.path.join(build_dir, "build.json")
with open(build_json_path, "w") as f:
    json.dump(state, f, indent=2)
    f.write("\n")

print(f"[Modify] Created build.json (build_id: {build_id}, theme: {theme_slug or 'unknown'})")
PYEOF
fi

# Derive BUILD_ID from build.json for downstream sections (commit messages, output naming)
BUILD_ID=$(python3 -c "import json; print(json.load(open('$BUILD_DIR/build.json')).get('build_id',''))" 2>/dev/null)

echo "[Modify] Theme: ${THEME_SLUG:-unknown}"
echo "[Modify] Creation mode: ${CREATION_MODE:-unknown}"
echo "[Modify] Existing modifications: ${EXISTING_MODS:-0}"

# ── Compute next version ─────────────────────────────────────────────────────

# Version starts at 1 for the first modification
NEXT_VERSION=$((${EXISTING_MODS:-0} + 1))

echo "[Modify] Next version: v${NEXT_VERSION}"
echo ""

# ── Detect git repo ──────────────────────────────────────────────────────────

if git -C "$BUILD_DIR" rev-parse --git-dir > /dev/null 2>&1; then
  echo "[Modify] Git repo detected - appending modification commits"
  GIT_INITIALIZED=true
else
  echo "[Modify] No git repo found - initializing for modification tracking"
  # Initialize git with the same pattern as build-git Section 1
  if git -C "$BUILD_DIR" init -b main > /dev/null 2>&1; then
    # Set fallback git identity if needed
    GIT_USER_NAME=$(git config --global user.name 2>/dev/null || echo "")
    GIT_USER_EMAIL=$(git config --global user.email 2>/dev/null || echo "")
    if [ -z "$GIT_USER_NAME" ] || [ -z "$GIT_USER_EMAIL" ]; then
      git -C "$BUILD_DIR" config user.name "CoWork Build"
      git -C "$BUILD_DIR" config user.email "build@cowork.local"
    fi

    # Write .gitignore from build-git Section 2 pattern
    cat > "$BUILD_DIR/.gitignore" << 'GITIGNORE'
# WordPress Build - Generated .gitignore
# Tracks: custom-* prefixed themes and plugins, README.md, SETUP.md, build.json
# Excludes: WP core, ephemeral dirs, database, MCP adapter, WP.org-sourced components

# ── WordPress Core Root Files ──────────────────────────────────────────────
index.php
license.txt
readme.html
wp-activate.php
wp-blog-header.php
wp-comments-post.php
wp-config.php
wp-config-sample.php
wp-cron.php
wp-links-opml.php
wp-load.php
wp-login.php
wp-mail.php
wp-settings.php
wp-signup.php
wp-trackback.php
xmlrpc.php

# ── WordPress Core Directories ─────────────────────────────────────────────
wp-admin/
wp-includes/

# ── Ephemeral wp-content Directories ──────────────────────────────────────
wp-content/cache/
wp-content/upgrade/
wp-content/uploads/
wp-content/backups/
wp-content/w3tc-config/
wp-content/wflogs/
wp-content/updraft/

# ── Database and Debug Logs ────────────────────────────────────────────────
database.sql
wp-content/debug.log

# ── MCP Adapter (bundled, reinstallable from CoWork plugin) ───────────────
wp-content/plugins/mcp-adapter/

# ── WordPress Core Bundled Themes (reinstallable) ─────────────────────────
wp-content/themes/twentytwentyone/
wp-content/themes/twentytwentytwo/
wp-content/themes/twentytwentythree/
wp-content/themes/twentytwentyfour/
wp-content/themes/twentytwentyfive/
GITIGNORE

    # Create initial commit capturing current state
    git -C "$BUILD_DIR" add -A > /dev/null 2>&1
    git -C "$BUILD_DIR" commit -m "build(init): capture existing state for modification tracking" > /dev/null 2>&1
    GIT_INITIALIZED=true
    echo "[Modify] Git initialized with initial state commit"
  else
    GIT_INITIALIZED=false
    echo "[Modify] WARNING: git init failed - modifications will proceed without git history"
  fi
fi

echo ""
```

## Section 2: NL Request Decomposition (NL mode only)

the coding agent reads the NL_REQUEST and decomposes it into a sequence of atomic steps. Each step has a type from the modification taxonomy. Compound requests (e.g. "change the color and add a blog page") are split into separate steps, each getting its own git commit and build.json entry.

**Skip this section entirely for visual mode** - visual modifications are handled as a single atomic step in Section 5.

```
Decomposition is a the coding agent in-context reasoning step, not automated parsing.

the coding agent reads the NL_REQUEST and the current build state (theme slug, installed plugins,
existing content from build.json) to produce an ordered list of atomic steps.

Step Type Taxonomy:
┌──────────────────┬─────────────────────────────────────────────────────────────┐
│ Step Type        │ Description                                                 │
├──────────────────┼─────────────────────────────────────────────────────────────┤
│ theme-token      │ Change a value in theme.json (color, font, spacing)         │
│ template-edit    │ Edit a specific .html template or part file                 │
│ content-edit     │ Modify an existing page or post via WP-CLI (requires MySQL) │
│ content-create   │ Create a new page, post, or menu item (requires MySQL)      │
│ plugin-add       │ Install and activate a WP.org plugin (requires MySQL)       │
│ plugin-remove    │ Deactivate and uninstall a plugin (requires MySQL)          │
└──────────────────┴─────────────────────────────────────────────────────────────┘

Output: an ordered list of atomic steps with type, target, and specific change description.

Example decomposition:

  User request: "change the primary color to forest green and add a blog page"

  Step 1: [theme-token] theme.json → settings.color.palette → primary → #2d5a27
  Step 2: [content-create] Create new page "Blog" with wp post create, link in navigation menu

  Each step produces:
  - One git commit: build(modify): "change the primary color to forest green and add a blog page" (step 1/2)
  - One build.json modifications entry

For a single-step request, omit the "(step N/M)" suffix from the commit message.

IMPORTANT: Content steps (content-edit, content-create, plugin-add, plugin-remove) all
require MySQL. These steps call ensure_docker_mysql() which handles lazy Docker startup.
If Docker is unavailable, content/plugin steps are skipped with a warning - theme-only
steps still succeed.
```

## Section 3: NL Modification Execution

Execute each atomic step from the decomposition. Each sub-section handles a different step type. The per-step finalization (Section 4) runs after each step completes.

### Section 3a: theme-token Steps

Read `theme.json` with Python `json.load`, locate the target token, apply the change, validate, and write back. Never use sed/awk for JSON editing. No Docker required.

```bash
# theme-token modification pattern
# Variables: THEME_SLUG, TARGET_SLUG (e.g. "primary"), NEW_VALUE (e.g. "#2d5a27"), TOKEN_PATH (e.g. "color.palette")

THEME_JSON_PATH="$BUILD_DIR/wp-content/themes/$THEME_SLUG/theme.json"

if [ ! -f "$THEME_JSON_PATH" ]; then
  echo "[Modify] WARNING: theme.json not found at $THEME_JSON_PATH - skipping theme-token step"
  # warn-and-continue
else
  # Read, modify, validate, write - all in Python for JSON safety
  python3 << PYEOF
import json, sys, os, shutil

theme_json_path = "$THEME_JSON_PATH"
target_slug = "$TARGET_SLUG"
new_value = "$NEW_VALUE"
token_path = "$TOKEN_PATH"

# Read current theme.json
with open(theme_json_path) as f:
    theme = json.load(f)

# Back up original for rollback on validation failure
backup_path = theme_json_path + ".bak"
shutil.copy2(theme_json_path, backup_path)

old_value = None
modified = False

# Route by token path type
if token_path.startswith("color.palette"):
    palette = theme.get("settings", {}).get("color", {}).get("palette", [])
    for entry in palette:
        if entry.get("slug") == target_slug:
            old_value = entry.get("color")
            entry["color"] = new_value
            modified = True
            break

elif token_path.startswith("typography.fontFamilies"):
    font_families = theme.get("settings", {}).get("typography", {}).get("fontFamilies", [])
    for entry in font_families:
        if entry.get("slug") == target_slug:
            old_value = entry.get("fontFamily")
            entry["fontFamily"] = new_value
            modified = True
            break

elif token_path.startswith("spacing"):
    spacing = theme.get("settings", {}).get("spacing", {})
    if target_slug in spacing:
        old_value = spacing[target_slug]
        spacing[target_slug] = new_value
        modified = True

if not modified:
    print(f"[Modify] WARNING: Token '{target_slug}' not found in {token_path} - skipping")
    os.remove(backup_path)
    sys.exit(0)

# Validate JSON is still valid
try:
    json.dumps(theme)
except Exception as e:
    print(f"[Modify] ERROR: JSON validation failed after edit - restoring backup")
    shutil.move(backup_path, theme_json_path)
    sys.exit(1)

# Write updated theme.json
with open(theme_json_path, "w") as f:
    json.dump(theme, f, indent=2)
    f.write("\n")

# Verify written file is valid JSON
try:
    with open(theme_json_path) as f:
        json.load(f)
except Exception:
    print(f"[Modify] ERROR: Written file is not valid JSON - restoring backup")
    shutil.move(backup_path, theme_json_path)
    sys.exit(1)

# Clean up backup
os.remove(backup_path)

print(f"[Modify] theme.json updated: {target_slug} {old_value} → {new_value}")
PYEOF

  STEP_FILES_CHANGED="wp-content/themes/$THEME_SLUG/theme.json"
  STEP_TYPE="theme-token"
  STEP_DESCRIPTION="Changed ${target_slug} from ${old_value} to ${new_value}"
  BEFORE_JSON="{\"${target_slug}\": \"${old_value}\"}"
  AFTER_JSON="{\"${target_slug}\": \"${new_value}\"}"
fi
```

### Section 3b: template-edit Steps

Read the target `.html` template file, apply the the coding agent-interpreted change to the specific block comment section, and write back. Preserve all other blocks verbatim - surgical edit, not full rewrite. No Docker required.

```
Template editing is a the coding agent in-context judgment step.

the coding agent reads the current content of the target template file (from templates/ or parts/)
and the user's modification request. the coding agent then:

1. Identifies which block comment section(s) need to change
2. Rewrites only the affected block(s) using WordPress block comment syntax (<!-- wp:* -->)
3. Preserves all other blocks verbatim
4. Writes the updated template file

The template file is at:
  $BUILD_DIR/wp-content/themes/$THEME_SLUG/templates/{template-name}.html
  OR
  $BUILD_DIR/wp-content/th

Shown under the source repo’s MIT license. Read the full folder on GitHub.