From 39a637410c7a010139a3b3d7f98cc1c5460c5c97 Mon Sep 17 00:00:00 2001 From: iWP Claudy Date: Sun, 2 Aug 2026 20:08:29 +0200 Subject: [PATCH] Initial skill library: 9 skills for delegate briefs WordPress plugin rebrand/conventions/remote-CLI patterns, Gitea release workflow, bastille jail provisioning, remote shell quoting safety, server fleet map, delegate brief writing, and verification discipline -- all derived from real incidents this session, plus two skills adapted (MIT license, attributed) from obra/superpowers and andrej-karpathy-skills. --- README.md | 62 +++++++- skills/bastille-jail-provisioning/SKILL.md | 82 +++++++++++ skills/delegate-brief-writing/SKILL.md | 68 +++++++++ skills/gitea-release-workflow/SKILL.md | 98 +++++++++++++ skills/karpathy-guidelines/SKILL.md | 67 +++++++++ skills/remote-shell-quoting-safety/SKILL.md | 69 +++++++++ skills/server-fleet-map/SKILL.md | 43 ++++++ .../verification-before-completion/SKILL.md | 65 +++++++++ .../wordpress-cli-remote-execution/SKILL.md | 69 +++++++++ skills/wordpress-plugin-conventions/SKILL.md | 123 ++++++++++++++++ skills/wordpress-plugin-rebrand/SKILL.md | 137 ++++++++++++++++++ 11 files changed, 882 insertions(+), 1 deletion(-) create mode 100644 skills/bastille-jail-provisioning/SKILL.md create mode 100644 skills/delegate-brief-writing/SKILL.md create mode 100644 skills/gitea-release-workflow/SKILL.md create mode 100644 skills/karpathy-guidelines/SKILL.md create mode 100644 skills/remote-shell-quoting-safety/SKILL.md create mode 100644 skills/server-fleet-map/SKILL.md create mode 100644 skills/verification-before-completion/SKILL.md create mode 100644 skills/wordpress-cli-remote-execution/SKILL.md create mode 100644 skills/wordpress-plugin-conventions/SKILL.md create mode 100644 skills/wordpress-plugin-rebrand/SKILL.md diff --git a/README.md b/README.md index ca1fb22..d2e43cc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ # agent-skills -Reusable skill library for AI coding delegates (kimi, agy, vibe, codex, session Claude) -- self-contained process definitions so delegate briefs can reference a skill instead of re-explaining the process every time. \ No newline at end of file +Reusable skill library for AI coding delegates (session Claude, kimi, agy, +vibe, codex) working on the CloudHost/iWP.es projects. Goal: capture +process knowledge once, so a delegate brief can reference a skill instead +of re-explaining the same process from scratch every time. + +## Format + +Each skill is a directory under `skills/` containing a `SKILL.md` with +YAML frontmatter (`name`, `description`, optional `license`/`source`) +followed by the actual instructions in markdown. This is the same format +Claude Code's native `Skill` tool consumes directly, and matches the +convention used by other cross-platform skill projects (see Provenance +below) -- Gemini CLI's `activate_skill` tool and some Codex plugin setups +can potentially consume the same format natively, though that hasn't +been separately verified for this repo yet. + +**For delegates that don't natively discover skills** (kimi, vibe, and +any codex/agy invocation not specifically configured for plugin +discovery): read the relevant `SKILL.md` yourself before writing the +delegate's brief, and fold its content into the prompt directly rather +than assuming the delegate can fetch it itself. + +## Skills in this repo + +- `wordpress-plugin-rebrand` -- forking/rebranding an existing WP plugin + under a new brand prefix. +- `wordpress-plugin-conventions` -- baseline WP plugin coding standards. +- `wordpress-cli-remote-execution` -- running wp-cli against a site inside + a remote jail/container. +- `gitea-release-workflow` -- tagging releases on self-hosted Gitea, + including a real release-asset-unreachable gotcha and its workaround. +- `bastille-jail-provisioning` -- FreeBSD jail creation via clone-from-base, + including a real recurring IP-conflict gotcha. +- `remote-shell-quoting-safety` -- the base64-push pattern for landing + `$`/quote-heavy content through nested shell layers without corruption. +- `server-fleet-map` -- which host is for what (dev/corporate/affiliate/ + customers) and how to decide where new work belongs. +- `delegate-brief-writing` -- what a self-contained brief for a memoryless + CLI delegate needs to contain. +- `verification-before-completion` -- never relay a delegate's self-report + as fact; how to actually re-verify. +- `karpathy-guidelines` -- general LLM-coding behavioral guidelines + (simplicity, surgical changes, surfacing assumptions). + +## Provenance + +Some skills here are original (derived directly from real incidents on +this project); some are adapted from external open-source skill +libraries under their original MIT licenses, with attribution kept in +each skill's frontmatter: +- [obra/superpowers](https://github.com/obra/superpowers) (MIT) +- [SuperClaude-Org/SuperClaude](https://github.com/SuperClaude-Org/SuperClaude_Framework) (MIT) +- andrej-karpathy-skills (MIT) + +## Adding a new skill + +When a real, non-obvious pattern or gotcha comes up more than once, +write it down here rather than re-discovering it next time. Keep each +skill focused on one concern, include the *why* (not just the *what*) so +future edge cases can be judged sensibly, and prefer concrete confirmed +incidents over generic advice. \ No newline at end of file diff --git a/skills/bastille-jail-provisioning/SKILL.md b/skills/bastille-jail-provisioning/SKILL.md new file mode 100644 index 0000000..629dd5f --- /dev/null +++ b/skills/bastille-jail-provisioning/SKILL.md @@ -0,0 +1,82 @@ +--- +name: bastille-jail-provisioning +description: Use when creating a new FreeBSD bastille jail (a new site/service on a shared-LAN jail host) via clone-from-known-good-base. Covers the IP-conflict gotcha that has caused real, time-costly incidents twice. +--- + +# Bastille Jail Provisioning + +Pattern for standing up a new jail on a FreeBSD host running Bastille, +by cloning a known-working base jail (already has nginx+php-fpm+packages +configured) rather than building from scratch. + +## CRITICAL FIRST STEP: arp-scan before picking an IP + +**Confirmed real incident, twice, on two different hosts on the same +shared LAN**: an IP address that looked free by every host-local check +(`bastille list all`, `ifconfig` aliases, a manually-tracked "already +used" list) turned out to already belong to a **different physical +device, or a jail on a different host sharing the same LAN segment**. +Symptom: the new jail's DNS resolution and outbound TCP connections +silently fail or time out with no useful error — this looks exactly like +a jail networking/pf config bug and can cost real time chasing the wrong +theory before the actual cause (an IP conflict) is found. + +**Before assigning ANY candidate IP, always:** +```bash +pkg install -y arp-scan # if not already present +arp-scan --interface=vtnet0 +``` +Zero responses = genuinely free. **Any** response (even from a device +with no recognizable vendor match) means it's taken — pick a different +IP and re-check. Do not rely on ping (misses devices with ICMP +disabled/filtered — a real false-negative that happened once already) or +on a per-host "what's already in use" list (each host only knows about +its own jails, not other physical devices or other hosts' jails on the +same shared subnet). + +If you inherit a jail already provisioned on a conflicting IP: stop the +jail, remove the bad alias, update `jail.conf`'s `ip4.addr`, start the +jail again (this re-adds the alias on the new IP), then propagate the IP +change everywhere else it's referenced (DB user grants scoped by IP, +host-level nginx `proxy_pass` target, etc.) — an IP conflict fixed in +`jail.conf` alone but not in the DB grant/nginx config leaves the site +broken even though the jail itself is now healthy. + +## Provisioning steps (once the IP is confirmed free) + +1. **Clone from a known-good base jail** rather than building from + scratch — inherits working nginx/php-fpm/package configuration: + ```bash + bastille clone -a + ``` +2. **Wipe cloned content, do a fresh install** if this is a brand-new + site (not a migration) — the cloned base's site files are a *template* + for the stack config, not content you want to keep. +3. **Create the database** on the host's local MariaDB, with the grant + scoped to the **jail's own IP**, not the host's IP: + ```sql + CREATE DATABASE `dbname` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + CREATE USER 'dbuser'@'' IDENTIFIED BY ''; + GRANT ALL PRIVILEGES ON `dbname`.* TO 'dbuser'@''; + ``` + The application's `DB_HOST` config value is the **host's** own LAN IP + (where MariaDB actually listens), NOT the jail's IP — the jail IP is + only used for the GRANT's scope. Confirmed gotcha across multiple + real migrations: mixing these two up is an easy, non-obvious mistake. +4. **Wire the host-level nginx reverse-proxy vhost**, `proxy_pass`ing to + the jail's IP. +5. **Do NOT touch DNS or the reverse-proxy manager's public routing** as + part of automated provisioning — that's a deliberate, manual cutover + step for a human to do once the site is verified working over a + direct IP/Host-header test. Build and verify fully "dark" first. +6. **Smoke test** both directly to the jail IP and via the host nginx + proxy (with the right `Host` header and, if the site checks for HTTPS + via `X-Forwarded-Proto`, that header too) before considering + provisioning done. + +## Persist credentials immediately + +Write the generated DB password (and any other generated secrets) to a +durable location the moment they're generated — if a later provisioning +step fails, an unrecorded generated password is otherwise lost with no +way to recover it short of resetting it. diff --git a/skills/delegate-brief-writing/SKILL.md b/skills/delegate-brief-writing/SKILL.md new file mode 100644 index 0000000..df3cfb5 --- /dev/null +++ b/skills/delegate-brief-writing/SKILL.md @@ -0,0 +1,68 @@ +--- +name: delegate-brief-writing +description: Use when handing a task off to a CLI-based coding delegate (kimi, agy, vibe, codex, or a fresh subagent) that has no memory of the current conversation. Covers what a self-contained brief needs. +--- + +# Delegate Brief Writing + +A delegate CLI (kimi, agy, vibe, codex) or a fresh subagent has **zero +memory** of anything discussed so far — not the goal, not what's already +been ruled out, not which files matter. Everything it needs has to be in +the prompt itself. + +## What a self-contained brief needs + +1. **Background**: what is being built and why, in enough detail that + the delegate can make small judgment calls correctly without asking. +2. **Exact scope boundaries**: what's in scope, and just as important, + what's explicitly OUT of scope (a delegate that doesn't know a + boundary exists will happily wander past it). +3. **Confirmed facts, stated as confirmed** — not "probably" or "should + be": if you already checked something (a slug mapping, a config + value, a file's actual current content), say so explicitly and tell + the delegate not to re-derive it. Re-deriving already-known facts + wastes the delegate's budget and can introduce a *different*, + incorrect answer. +4. **Exact commands/patterns to follow**, not just a description of the + goal — if there's an established pattern (see e.g. the + `wordpress-plugin-rebrand` skill), give the exact steps, don't make + the delegate reinvent the process from a one-line goal statement. +5. **Credentials handling**: tell the delegate exactly how to read a + credential (e.g. "read the token fresh from `/path/to/token-file`") + and explicitly forbid printing/writing it anywhere. +6. **What to verify before reporting done**, concretely (lint, a specific + grep, a specific functional test) — see the + `verification-before-completion` skill. +7. **Exact report format expected back**, with a length cap. An + unconstrained delegate report is often much longer than useful. + +## Wrapper-agent pattern: make it wait for the real result + +If dispatching via an intermediate "wrapper" agent whose job is to +invoke the CLI tool and relay the result: **explicitly instruct it to +run the command synchronously and wait for actual completion before +ending its turn.** Confirmed real failure mode: a wrapper agent +repeatedly backgrounded the CLI process itself and ended its own turn +immediately, producing a stream of "still running, will report back" +notifications that never actually contained a result — because the +wrapper's own turn had already ended, it wasn't actually watching +anything. If this happens, don't keep re-prompting the same wrapper — +find the actual underlying process directly and monitor it yourself. + +## Splitting work across multiple parallel delegates + +When one deliverable naturally splits into N independent pieces (e.g. +"rebrand these 5 plugins"): do ONE piece yourself (or with one delegate) +first as a pilot, nail down the exact pattern including any gotchas +discovered along the way, THEN write the remaining N-1 briefs +incorporating everything learned from the pilot — including the specific +mistakes that happened during the pilot and how they were fixed, so the +parallel delegates don't repeat them. A brief written *after* the pilot +is meaningfully more reliable than N briefs all written from a cold +start. + +## Always re-verify independently afterward + +See `verification-before-completion`. A delegate's own final report, +however detailed and confident-sounding, is a claim — re-derive the key +facts yourself before treating the task as actually done. diff --git a/skills/gitea-release-workflow/SKILL.md b/skills/gitea-release-workflow/SKILL.md new file mode 100644 index 0000000..dc40d17 --- /dev/null +++ b/skills/gitea-release-workflow/SKILL.md @@ -0,0 +1,98 @@ +--- +name: gitea-release-workflow +description: Use when tagging a release for a plugin/package on a self-hosted Gitea instance, especially when the release needs to be downloadable by an external system (an auto-updater, a CI job, etc.). +--- + +# Gitea Release Workflow + +## Creating a release + +```bash +curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d '{"tag_name": "v1.0.0", "name": "v1.0.0", "body": "", "draft": false, "prerelease": false}' \ + "$HOST/api/v1/repos/$OWNER/$REPO/releases" +``` + +Verify it landed: +```bash +curl -s -H "Authorization: token $TOKEN" "$HOST/api/v1/repos/$OWNER/$REPO/releases/latest" +``` +Note: on at least one real self-hosted instance, `/releases/latest` 404s +for unauthenticated requests even on a public repo — always send the +token, don't assume public-repo endpoints are auth-free. + +## KNOWN GOTCHA: release-asset downloads can be unreachable + +**Confirmed live on a real self-hosted Gitea instance**: uploading a zip +as a release asset and then trying to fetch it via the asset's own +`browser_download_url` field returned an **internal address** +(`http://localhost:3000/...`) instead of the instance's real public +hostname — a `ROOT_URL` misconfiguration server-side. This makes the +uploaded asset **completely unreachable from outside the Gitea server +itself**, even though the release object and its metadata (version, +changelog) are fine. + +**Symptom**: `update-check`-style API calls that only read release +metadata work fine; the actual **download** step 404s or times out. + +**Workaround that reliably works**: don't rely on the release-asset +upload/download mechanism at all for the actual file bytes. Instead use +Gitea's **repo-archive endpoint**, which zips the tagged git tree +on-the-fly and is served from the same working API path pattern as +everything else: + +```bash +curl -s -H "Authorization: token $TOKEN" -o output.zip \ + "$HOST/api/v1/repos/$OWNER/$REPO/archive/$TAG.zip" +``` + +This means: the release object still gets created (for its version/ +changelog metadata, which downstream consumers do need), but you do +**not** need to upload a zip as a release asset at all — the actual +distributable content is whatever's in the tagged git tree, fetched via +`/archive/{tag}.zip` at request time. + +**Before trusting the asset-download path on a NEW Gitea instance**: test +it once (`curl` the `browser_download_url` directly) rather than +assuming it works — if it does work on a given instance, the asset-upload +approach is simpler and fine to use; the archive-endpoint workaround is +only needed on instances that have this specific misconfiguration. Don't +apply the workaround blindly without confirming the actual failure mode +exists on the instance you're using. + +## Archive-endpoint folder-naming gotcha + +The `/archive/{tag}.zip` endpoint names the zip's top-level folder based +on the repo (exact naming varies by Gitea version — sometimes the repo +name, sometimes lowercased). If the repo's own files live in a +subdirectory rather than at repo root, the resulting zip gets a +**double-nested** top-level folder. Keep single-purpose repos (one +plugin/package per repo) flat at the root to avoid this — see the +`wordpress-plugin-rebrand` skill's flatten-to-root step. + +## Forking into a new org + +```bash +curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d '{"organization": "TargetOrg", "name": "new-repo-name"}' \ + "$HOST/api/v1/repos/$SOURCE_OWNER/$SOURCE_REPO/forks" +``` +The `name` field renames the fork on creation — you don't have to accept +the source repo's own name. Fork inherits the **source repo's full git +history**; if you're about to do a wholesale rebrand and don't need that +history preserved, a `git init` + single clean commit + `git push --force` +is often cleaner than carrying an unrelated-looking history forward — but +only force-push a repo that's genuinely fresh with nothing else depending +on its prior state. + +## Verify a release actually works end-to-end + +Don't stop at "the release object exists" — actually fetch the archive +and inspect it: +```bash +curl -s -H "Authorization: token $TOKEN" -o /tmp/verify.zip \ + "$HOST/api/v1/repos/$OWNER/$REPO/archive/$TAG.zip" +unzip -l /tmp/verify.zip | head -20 +``` +Confirm: single non-double-nested top-level folder, expected files +present, no truncation. diff --git a/skills/karpathy-guidelines/SKILL.md b/skills/karpathy-guidelines/SKILL.md new file mode 100644 index 0000000..6a62d04 --- /dev/null +++ b/skills/karpathy-guidelines/SKILL.md @@ -0,0 +1,67 @@ +--- +name: karpathy-guidelines +description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria. +license: MIT +--- + +# Karpathy Guidelines + +Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/skills/remote-shell-quoting-safety/SKILL.md b/skills/remote-shell-quoting-safety/SKILL.md new file mode 100644 index 0000000..705b60c --- /dev/null +++ b/skills/remote-shell-quoting-safety/SKILL.md @@ -0,0 +1,69 @@ +--- +name: remote-shell-quoting-safety +description: Use when a command or file content needs to pass through multiple nested shell layers (e.g. local shell -> ssh -> a jail/container exec wrapper -> su -> the target interpreter). Avoids silent corruption of $, quotes, and special characters. +--- + +# Remote Shell Quoting Safety + +## The problem + +Content containing `$`, quotes, or other shell-special characters gets +progressively mangled the more shell layers it passes through (local +shell → `ssh` → a container/jail exec wrapper like `bastille cmd` → +`su -c` → the actual target interpreter). Each layer's own quoting rules +interact, and by 3+ layers deep it becomes extremely hard to reason about +correctly — and failures are often **silent**: no error, just corrupted +content (e.g. `$_SERVER['KEY']` silently becoming `['KEY']`, `$` and the +variable name simply eaten). + +**Confirmed real incident**: a `wp-config.php` snippet containing +`$_SERVER['HTTP_X_FORWARDED_PROTO']` passed through +`ssh → bastille cmd → su -m www -c '...'` came out the other side as +bare `["HTTP_X_FORWARDED_PROTO"]` — no error at the point of corruption, +the resulting fatal PHP error only surfaced two steps later at a +completely different command, making the actual cause much harder to +trace back. + +## The fix: base64, not nested quoting + +Don't try to escape correctly through N layers. Instead: + +1. Write the target file's **complete final content** locally (a real + file, not a shell variable). +2. Base64-encode it locally: `base64 -w0 localfile > /tmp/payload.b64` + (or pipe directly). +3. `cat`/pipe the base64 text through the SSH/exec layers (base64 output + is alphanumeric-only — nothing for any shell layer to misinterpret, + regardless of how many layers it passes through). +4. Decode back to the real file **on the far side, in ONE final step**: + `base64 -d payload.b64 > /real/target/path`. + +```bash +B64=$(base64 -w0 local_file.php) +echo "$B64" | ssh user@host "cat > /tmp/upload.b64" +ssh user@host "base64 -d /tmp/upload.b64 > /real/target/path && rm /tmp/upload.b64" +``` + +For a jail/container layer on top of SSH (e.g. `bastille cmd `), +land the base64 payload on the **host** filesystem first (which the jail +can usually see via a shared mount or a copy step), then decode from +**inside** the jail in one command — don't try to thread the base64 +string itself through the jail-exec wrapper's own quoting too; one +decode step per real filesystem boundary crossed is enough. + +## When to reach for this + +- Any file content containing `$`, backticks, mixed quotes, or PHP/shell + code being pushed through 2+ nested shell contexts. +- Whenever a "the file exists but does something subtly wrong" bug + appears after a multi-hop deployment step, and the content involves + special characters — check for quoting corruption before looking + elsewhere. + +## What NOT to do + +Don't respond to a quoting failure by adding more layers of escaping +(`\\\$`, `'"'"'`, etc.) — this usually makes the problem harder to +reason about, not easier, and is exactly the kind of fragile fix that +looks like it works until the next slightly-different payload breaks it +again. Switch to the base64 pattern instead of debugging escape depth. diff --git a/skills/server-fleet-map/SKILL.md b/skills/server-fleet-map/SKILL.md new file mode 100644 index 0000000..ddf423f --- /dev/null +++ b/skills/server-fleet-map/SKILL.md @@ -0,0 +1,43 @@ +--- +name: server-fleet-map +description: Use when deciding WHICH host a piece of work belongs on, or when you need a host's connection details. Reference map of the FreeBSD/bastille jail fleet and what each host is actually for. +--- + +# Server Fleet Map + +All hosts reachable over Tailscale (`root@`, SSH key +`/root/.ssh/id_ed25519` unless noted). Each host has a distinct +**purpose** — check this before assuming a new site/service belongs on +whichever host you happen to already be connected to. + +| Host | Tailscale IP | Purpose | Notes | +|---|---|---|---| +| **staging** | `100.104.61.54` | Dev/staging — pre-production testing, throwaway experiments | New jail IPs here need the same arp-scan discipline as everywhere else on this shared LAN (see `bastille-jail-provisioning` skill) — this is where that gotcha was first found. | +| **venus** | `100.109.251.127` (LAN `192.168.0.33`) | Corporate — internal business sites/tools, the iWP.es plugin-subscription platform, and other CloudHost-org first-party projects | Bastille jails, combined web+db (local MariaDB, no shared external DB host). New jails typically created via `bastille clone -a ` rather than from scratch. | +| **granja** | `100.98.197.28` | Affiliate network — the travel/tourism site network (menorca.ro, palma.ro, and similar) | No public interface configured directly; sits behind a reverse-proxy manager (NPM) over Tailscale. See `docs/server-granja.md` for the local-curl-simulates-NPM testing trick. | +| **funky** | `100.127.21.100` (LAN `192.168.0.218`) | Customers — sites migrated from the old Linux affiliate fleet, actual paying-customer properties (e.g. news.easycut.es, photomouse.ro) | Combined web+db+valkey, bastille jails. Migration target that absorbed several now-decommissioned Linux hosts (maagar, shoe, proton). | +| **zamolxis** | `100.115.128.41` | Standalone (non-jailed) FreeBSD site host | Not part of the granja/external-fleet jail families — a single site (Newspaper/tagDiv theme, PHP 8.5) running directly on the host, not in a jail. | +| **cabrera** | `100.100.108.19`, SSH port `79` | External Linux affiliate-network host (BTPanel-style) | Part of the older Linux fleet (alongside raptor/maagar/formentor/moonie/spunky/proton/shoe — several since decommissioned/migrated to funky). Hosts easycut.es's main WooCommerce e-commerce site directly (not jailed). | + +## How to decide where new work belongs + +1. **What kind of thing is it?** A corporate/internal tool → venus. A + customer's production site → funky. A new affiliate-network travel + site → granja. A quick experiment or something not ready for anything + resembling production → staging. +2. **Does it already have an established host from a related sibling + project?** (e.g. another CloudHost-org first-party tool already lives + on venus) — match that, don't scatter related things across hosts + without a reason. +3. **When genuinely unsure, ask** rather than guessing — which host + something lives on affects DNS, backup scope, and who has access to + it later. + +## Gitea (separate from any of the above) + +`https://devops.cloudhost.es` — not one of the site-hosting fleet, this +is the internal git/CI host. Orgs seen so far: `CloudHost` (main +internal projects + this skills repo), `iWP.es` (the plugin-subscription +product and its forked/rebranded plugins), `InformatiQ` (the security +plugin), `External` (mirrors of third-party repos for internal +reference). diff --git a/skills/verification-before-completion/SKILL.md b/skills/verification-before-completion/SKILL.md new file mode 100644 index 0000000..30b76e9 --- /dev/null +++ b/skills/verification-before-completion/SKILL.md @@ -0,0 +1,65 @@ +--- +name: verification-before-completion +description: Use before reporting any task as done, especially when the work was done by a delegate/subagent. Never relay a self-report as fact. +license: MIT +source: adapted from obra/superpowers (MIT license), condensed for delegate-brief reuse +--- + +# Verification Before Completion + +**A delegate's "done" is a claim, not a fact.** This applies whether the +delegate is another AI agent, a subagent, or your own past self in an +earlier step. Re-derive the result yourself before reporting completion +to whoever is waiting on it. + +## The rule + +Never report "done" based solely on: +- A delegate saying it's done. +- A test that wasn't actually re-run by you. +- Code that "should work" based on reading it, when running it was possible. + +Instead, for each claim, do the cheapest check that would actually catch +a wrong claim: + +| Claim | Real check | +|---|---| +| "The file was created" | `ls`/`cat` it yourself | +| "The syntax is valid" | Actually run the linter yourself, don't trust a self-report of having run it | +| "The tests pass" | Run them yourself, read the actual output | +| "The API returns X" | Make the actual call yourself | +| "The deploy succeeded" | Check the live system, not the deploy log's exit code alone | +| "The old code is gone" | grep for it yourself | + +## Why this matters (not just process theater) + +Across a real multi-delegate session, every single delegate's first +self-report claimed success. Independent re-verification (re-running the +linter, re-grepping, and — critically — testing the actual live +end-to-end pipeline rather than just checking that files existed) is +what actually confirmed correctness. In an earlier phase of the same +session, a wrapper-agent pattern (spawn an agent whose only job is to +invoke one CLI tool and report back) repeatedly ended its own turn the +instant it *started* a background process, before that process had +produced any real result — producing a loop of confident-sounding "still +running" reports that carried zero information. The fix was to stop +trusting the wrapper's self-reported status and directly monitor the +actual underlying process. + +## Escalating verification for higher-stakes changes + +- Low-stakes (a text file, a doc change): read it back. +- Medium-stakes (application code): lint + a targeted functional test. +- High-stakes (something that serves real traffic, handles money, or + changes shared/production state): the *actual* end-to-end path a real + user would hit — not a unit test in isolation, the real live pipeline. + If you cannot exercise the real path yourself (e.g. no browser + available for a checkout flow), say so explicitly rather than reporting + success on a partial check. + +## What NOT to do + +Don't pad out a report with hedging language as a substitute for actually +checking ("this should work", "this is likely correct"). Either verify +it, or clearly state it's unverified and why — don't blur the line +between the two. diff --git a/skills/wordpress-cli-remote-execution/SKILL.md b/skills/wordpress-cli-remote-execution/SKILL.md new file mode 100644 index 0000000..fab0c12 --- /dev/null +++ b/skills/wordpress-cli-remote-execution/SKILL.md @@ -0,0 +1,69 @@ +--- +name: wordpress-cli-remote-execution +description: Use when running wp-cli commands against a WordPress site living inside a remote jail/container (via ssh + a jail-exec wrapper). Covers the output-prefix stripping gotcha and the correct user/path invocation pattern. +--- + +# WordPress CLI Remote Execution + +## Invocation pattern + +```bash +ssh user@host "bastille cmd sh -c 'cd /path/to/site && su -m www -c \"wp --path=/path/to/site\"'" +``` + +Key points: +- Run wp-cli as the **web server user** (`www`, `www-data`, etc.), not + root — WordPress file ownership assumptions and some plugin behavior + depend on this. +- Always pass `--path=` explicitly rather than relying on `cd` alone + propagating through every nested layer correctly. + +## GOTCHA: jail-exec wrappers prefix their own output + +A wrapper like `bastille cmd ` prints a `[jailname]:` header line +before the actual command output — this is the wrapper's own framing, +not part of the real output. Any script that captures this output for +further parsing must filter it out: + +```bash +OUTPUT=$(ssh user@host "bastille cmd " 2>&1 | grep -v "^\[\]:$") +``` + +Forgetting this is a common, easy-to-miss source of "the command +mysteriously failed to parse" bugs when scripting on top of jail-exec +wrappers. + +## GOTCHA: `error_reporting(0)` in bootstrap files can mask real errors + +Some CMS/application bootstrap files set `error_reporting(0)` globally +near the very top (often to suppress noisy legacy warnings). If you're +writing a standalone diagnostic/one-off script that `require`s such a +bootstrap file, your own errors after that point are ALSO silently +suppressed — a script can fail with **zero output and just a bad exit +code**, giving no clue why. Fix: explicitly +`error_reporting(E_ALL); ini_set('display_errors', '1');` again +**after** requiring the bootstrap, and add a +`register_shutdown_function` that dumps `error_get_last()` — this turns +a silent failure into an actual, readable error message. + +## GOTCHA: glob patterns fail silently under some remote shells + +`bastille cmd grep -rln 'pattern' /some/path/*.php` can fail with +`zsh: no matches found: ...` if the jail's default shell is zsh and the +glob doesn't expand as expected in that invocation context — with no +useful indication that this is a shell-globbing issue rather than "no +files matched." Fix: drop the glob and grep the directory recursively +instead (`grep -rln 'pattern' /some/path/`, no trailing `/*.ext`) — this +avoids the shell needing to expand anything at all. + +## When you need to run PHP with full application bootstrap, standalone + +If you need to call an application's own PHP functions/classes outside +the normal web request flow (e.g. to directly test a code path), don't +guess at which files to `require` — trace the **actual real bootstrap +sequence** the application's own front controller uses (e.g. its +`index.php`) by reading it, and replicate that exact require order in +your standalone script. Skipping steps because "this part probably isn't +needed" reliably produces a cascade of "undefined function/class" errors +that have to be debugged one require statement at a time — reading the +real bootstrap sequence once up front is faster than that cascade. diff --git a/skills/wordpress-plugin-conventions/SKILL.md b/skills/wordpress-plugin-conventions/SKILL.md new file mode 100644 index 0000000..7c174bf --- /dev/null +++ b/skills/wordpress-plugin-conventions/SKILL.md @@ -0,0 +1,123 @@ +--- +name: wordpress-plugin-conventions +description: Use when writing or modifying a WordPress plugin's PHP code. Baseline structural/security conventions to follow without being told each time. +--- + +# WordPress Plugin Conventions + +Baseline conventions for first-party WordPress plugin code. Follow these +without needing to be told in every brief. + +## File structure + +``` +plugin-slug/ + plugin-slug.php — main file: header, constants, bootstrap class + includes/ + class--*.php — one class per concern, class-based not procedural + admin/ — admin-only UI (settings pages, dashboards) + assets/{css,js}/ + languages/ — .pot/.po/.mo if the plugin is translatable +``` + +## Main file skeleton + +```php +prepare()` for every query with a variable in it — no string- + interpolated SQL, ever, no exceptions. + +## Database tables + +If a plugin needs its own tables (not just options), use `dbDelta()`: +```php +require_once ABSPATH . 'wp-admin/includes/upgrade.php'; +$charset = $wpdb->get_charset_collate(); +$sql = "CREATE TABLE {$wpdb->prefix}prefix_thing (...) {$charset};"; +dbDelta($sql); +``` +Track a `DB_VERSION` constant + a stored option so future plugin updates +can detect and re-run `dbDelta()` for schema changes. + +## PHP 7.4 compatibility + +Unless a specific brief says otherwise, target PHP 7.4+ (a large fraction +of real WordPress hosting is still on 7.4). Avoid PHP 8-only syntax +(named arguments, enums, readonly properties, `match`). If a codebase +needs `str_contains`/`str_starts_with`/`str_ends_with` (PHP 8.0+) on an +environment that might run 7.4, guard with `function_exists()` +polyfills rather than assuming they exist. + +## Don't build what WordPress core already gives you + +Before writing custom code for: cron scheduling (`wp_schedule_event`), +REST endpoints (`register_rest_route`), settings UI +(`register_setting`/`add_settings_field`/`woocommerce_admin_fields` if +WooCommerce context), object caching (`wp_cache_*` functions), HTTP +requests (`wp_remote_get`/`wp_remote_post`, not raw `curl`) — check +whether a core API already does it. Reinventing these is both wasted +effort and a common source of subtle bugs core already solved correctly. diff --git a/skills/wordpress-plugin-rebrand/SKILL.md b/skills/wordpress-plugin-rebrand/SKILL.md new file mode 100644 index 0000000..f24a604 --- /dev/null +++ b/skills/wordpress-plugin-rebrand/SKILL.md @@ -0,0 +1,137 @@ +--- +name: wordpress-plugin-rebrand +description: Use when forking and rebranding an existing WordPress plugin under a new name/prefix (e.g. for a white-label or iWP-branded resale product). Covers identifier renaming, repo flattening, updater bundling, and license-field wiring. +--- + +# WordPress Plugin Rebrand + +Process for taking an existing WordPress plugin and rebranding it under a +new name/prefix, without breaking anything or missing a spot. Validated +live across 5 real plugins in one session (WooList, WooBC, WooSol, +SlickWoo, MetAI → iWP-branded forks), each independently re-verified +end-to-end afterward with zero defects found. + +## Inputs you need before starting + +- Source repo (clone URL + credentials). +- New brand prefix (e.g. `iWP`), new product slug (e.g. `iwp-woolist`) — + **get the exact slug from wherever the catalog/product list already + defines it, never invent your own.** +- The updater/license-client file to bundle, if the target platform has + one (e.g. a shared `IWP_Updater` class), and its exact integration + snippet (api_url, plugin_slug, license_option names). + +## Step 1 — Learn the actual existing convention, don't assume + +Read the plugin's main file first. Every plugin has its own prefix +scheme — don't assume it matches a previous rebrand. Identify: +- Constant prefix (`PLUGINNAME_...`) +- Class prefix (`PluginName_...`) +- Function prefix (`pluginname_...`) +- Text domain string (usually lowercase-hyphenated, e.g. `pluginname`) +- Whether the plugin lives at the repo root or in a subdirectory + +## Step 2 — Flatten to repo root + +If the plugin's files live in a subdirectory (not the repo root), move +everything up to root. Reason: many git hosts' repo-archive endpoint +(`/archive/{tag}.zip`-style) names the zip's top-level folder after the +**repo**, so a nested subdirectory inside the repo produces a +double-nested folder in the resulting distributable zip +(`reponame/plugin-subdir/...` instead of `reponame/...`). Flattening +avoids this categorically rather than requiring special-case handling +downstream. + +## Step 3 — Rename, in this order + +1. **Text-domain / hyphenated contexts first**: every `oldname-` (CSS + classes, script/style handles, asset file paths, admin page slugs) → + `newprefix-oldname-`. A safe global find/replace, catches the vast + majority of hyphenated identifiers correctly in one pass. +2. **Constants**: `OLDNAME_` → `NEWPREFIX_OLDNAME_`. +3. **Classes**: `OldName_` → `NewPrefix_OldName_`. +4. **Functions / hooks / underscore contexts**: `oldname_` → + `newprefix_oldname_`. +5. **camelCase-after-underscore contexts** (JS localize-object names are + the common case: `oldnameData` used as the 2nd arg to + `wp_localize_script()`) → `newprefix_oldnameData`. **Grep the JS files + for the exact existing spelling before finalizing — don't guess the + casing, the PHP `wp_localize_script()` call must match byte-for-byte + or the JS breaks silently at runtime.** +6. **Remaining bare prose occurrences of the product name** (page titles, + UI labels, doc-comments) → the new display name (e.g. `MetAI` → + `iWP MetAI`). Do this LAST, after class renaming, or you'll double- + prefix the class name. +7. **File/directory renames** to match: `class-oldname-admin.php` → + `class-newprefix-oldname-admin.php`, etc. +8. Update the plugin header block: `Plugin Name`, `Author`, `Author URI`, + `Plugin URI`, `Text Domain` (always hyphenated, never underscored — + WordPress text domains are always hyphen-form even when everything + else in that context uses underscores). + +## Step 4 — Cross-check enqueue calls against actual filenames + +Every `wp_enqueue_script()`, `wp_enqueue_style()`, `wp_localize_script()` +handle/path argument must match the ACTUAL renamed filenames on disk +exactly, or the admin UI silently fails to load its JS/CSS with no error +message anywhere obvious. Check this explicitly, don't assume the +renames were consistent. + +## Step 5 — Bundle the updater / license client (if applicable) + +Copy the shared updater class unchanged into the plugin's `includes/` +directory. Wire it in the main file (near the top, after constants): + +```php +require_once . 'includes/class-.php'; +add_action( 'plugins_loaded', function () { + new ( [ + 'api_url' => '', + 'plugin_file' => __FILE__, + 'plugin_slug' => '', + 'version' => , + 'license_option' => '__license_key', + ] ); +}, 5 ); +``` + +Then add ONE settings field for the license key (option name matching +`license_option` above) into wherever the plugin already has an admin +settings UI — follow that file's existing code style/pattern for how +other fields are registered/rendered, don't introduce a different +pattern. If the plugin has no settings UI at all, add a minimal one +rather than skipping this step — license-gated updates need somewhere +for the customer to enter the key. + +## Step 6 — Verify before pushing + +1. `php -l` every `.php` file. Zero tolerance for syntax errors. +2. Case-insensitive grep the whole tree for the bare old name: + `grep -rniE '(^|[^a-z_-])oldname' --include='*.php' --include='*.js' --include='*.css' .` + — every hit must be part of a correctly-prefixed token + (`newprefix-oldname`, `newprefix_oldname`, `NewPrefix_OldName`, or the + new display name). Zero unprefixed exceptions, except possibly an + original-authorship credit comment if one genuinely exists. +3. Sanity-check for double-prefixing bugs (`iWP iWP`, `IWP_IWP`, + `iwp-iwp`, `iwp_iwp` or equivalent for your prefix) — a real mistake + that happens when step 3.6 runs before 3.3, or runs twice. + +## Step 7 — Ship + +Commit, push (force-push is fine for a freshly-created fork with no +other history depending on it — confirm that's actually the case before +force-pushing anything else). Tag a release. See the +`gitea-release-workflow` skill for the release/verification steps if the +git host is Gitea. + +## Common failure modes (all hit at least once during validation) + +- **Forgetting the flatten-to-root step** → double-nested zip. +- **JS localize-object name mismatch** (hyphen where an underscore is + required for a valid JS identifier) → admin UI JS silently breaks with + no visible error. +- **Trusting the delegate's self-report instead of re-verifying** — every + single delegate run in the validating session initially reported + success; independent re-verification (re-running `php -l` yourself, a + fresh grep, an actual functional test through the real serving + pipeline) is what actually confirms it, not the report.