From c217d5620c4742d329f795d611fb1c94118a2451 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:39:46 +0200 Subject: [PATCH 1/4] docs(skills): unify .claude and .agents skill contents Make each shipped skill file byte-identical across both trees (fuller claude variant wins): rl, rl_page_title, rl_menu_link. On sites where one tree links to the other, divergent variants made the installer overwrite itself and the freshness check fail permanently. Replace a few em dashes with colons/semicolons per repo policy. Add a CI check that fails when any pair differs. --- .agents/skills/rl/SKILL.md | 156 +++++++++++++---- .claude/skills/rl/SKILL.md | 6 +- .github/workflows/skills-pair-check.yml | 29 ++++ .../.agents/skills/rl_menu_link/SKILL.md | 157 +++++++++++++----- .../.claude/skills/rl_menu_link/SKILL.md | 4 +- .../.agents/skills/rl_page_title/SKILL.md | 149 +++++++++++++---- .../.claude/skills/rl_page_title/SKILL.md | 4 +- 7 files changed, 397 insertions(+), 108 deletions(-) create mode 100644 .github/workflows/skills-pair-check.yml diff --git a/.agents/skills/rl/SKILL.md b/.agents/skills/rl/SKILL.md index 89c375c..26fde07 100644 --- a/.agents/skills/rl/SKILL.md +++ b/.agents/skills/rl/SKILL.md @@ -1,36 +1,136 @@ -# RL — Reinforcement Learning Experiments (Drush CLI) +--- +name: rl +version: 1.0.0 +description: > + Manage Reinforcement Learning (A/B testing) experiments via Drush CLI. + Thompson Sampling multi-armed bandit experiments: create, analyze, + configure, and optimize content variants. +triggers: + - /rl + - experiment + - a/b test + - ab test + - bandit + - thompson sampling + - conversion rate + - variant +--- -Manage A/B testing experiments powered by Thompson Sampling via Drush. +# RL: Reinforcement Learning Experiments -## Commands +You are managing A/B testing experiments powered by Thompson Sampling. +The RL module tracks impressions (turns) and conversions (rewards) across +content variants and automatically optimizes traffic distribution. -### Discovery & Analysis -- `drush rl:list` — List all experiments with stats -- `drush rl:status ` — Experiment phase, confidence, distribution -- `drush rl:performance ` — Arm-level conversion rates and labels -- `drush rl:trends ` — Historical trends (--period=daily|weekly|monthly) -- `drush rl:export ` — Export full data (--snapshots for history) -- `drush rl:analyze ` — Full analysis with recommendations +## Preamble: Auto-discover Current State -### Experiment CRUD -- `drush rl:experiment:create --module=X --name="Y"` — Create experiment -- `drush rl:experiment:update --name="Y"` — Update metadata -- `drush rl:experiment:delete ` — Delete experiment + all data +```bash +# List all active experiments +drush rl:list --format=yaml + +# Get current module configuration +drush rl:config:list + +# If experiments exist, get status of the most active one +# drush rl:status --format=yaml +``` + +## Commands Reference + +### Discovery & Analysis (read-only) + +| Command | Alias | Purpose | +|---|---|---| +| `rl:list` | `rll` | List all experiments with summary stats | +| `rl:status ` | `rlst` | Detailed status: phase, confidence, traffic distribution | +| `rl:performance ` | `rlp` | Arm-level performance with human-readable labels | +| `rl:trends ` | `rlt` | Historical trends with period aggregation | +| `rl:export ` | `rle` | Export complete experiment data | +| `rl:analyze ` | `rla` | Full analysis with actionable recommendations | + +### Experiment Lifecycle + +| Command | Alias | Purpose | +|---|---|---| +| `rl:experiment:create ` | `rl-ec` | Create experiment (`--module`, `--name`, `--dry-run`) | +| `rl:experiment:update ` | `rl-eu` | Update name/module (`--module`, `--name`, `--dry-run`) | +| `rl:experiment:delete ` | `rl-ed` | Delete experiment + all data (`--dry-run`) | ### Configuration -- `drush rl:config:list` — List all settings -- `drush rl:config:get ` — Get one setting -- `drush rl:config:set ` — Set with validation -- `drush rl:config:reset` — Reset to defaults -- `drush rl:event-log:clear` — Clear all snapshots - -### Settings Keys -| Key | Type | Default | + +| Command | Alias | Purpose | +|---|---|---| +| `rl:config:get [key]` | `rl-cg` | Get one or all settings | +| `rl:config:set ` | `rl-cs` | Set a setting with validation (`--dry-run`) | +| `rl:config:list` | `rl-cl` | List all settings with current values | +| `rl:config:reset` | `rl-cr` | Reset all settings to defaults (`--dry-run`) | +| `rl:event-log:clear` | `rl-elc` | Clear all snapshots (`--dry-run`) | + +### Setup + +| Command | Alias | Purpose | |---|---|---| -| debug_mode | boolean | false | -| enable_event_log | boolean | true | -| event_log_max_rows | integer | 100000 | -| chart_line_threshold | integer | 9 | +| `rl:setup-ai` | `rl-sa` | Install AI skill files (`--host`, `--check`) | + +## Configuration Keys + +| Key | Type | Default | Description | +|---|---|---|---| +| `debug_mode` | boolean | false | Log Thompson Sampling scores | +| `enable_event_log` | boolean | true | Record snapshots for historical charts | +| `event_log_max_rows` | integer | 100000 | Max snapshot rows (1000–10000000) | +| `chart_line_threshold` | integer | 9 | Max variants for 2D line chart (2–50) | + +## Workflow Examples + +### Create and monitor a new A/B test +```bash +# Create experiment +drush rl:experiment:create hero_cta_test \ + --module=my_module \ + --name="Hero CTA Button Color Test" + +# Check status after data collection +drush rl:status hero_cta_test + +# Get detailed performance breakdown +drush rl:performance hero_cta_test --format=yaml + +# Full analysis with recommendations +drush rl:analyze hero_cta_test +``` + +### Configure the module +```bash +# Enable debug logging temporarily +drush rl:config:set debug_mode true + +# Increase snapshot storage +drush rl:config:set event_log_max_rows 500000 + +# Check all settings +drush rl:config:list +``` + +## Key Concepts + +- **Experiment**: A multi-armed bandit test comparing content variants +- **Arm**: A single variant (identified by arm_id, often a node ID) +- **Turn/Impression**: One display of a variant to a visitor +- **Reward/Conversion**: A successful outcome (click, form submit, etc.) +- **Thompson Sampling**: Bayesian algorithm that balances + exploration vs exploitation +- **Phase**: exploration, learning, exploitation, conclusive +- **Confidence**: Statistical confidence that the top + performer is truly best + +## Notes -All state-changing commands support `--dry-run`. -Analysis commands support `--format=json|yaml|table`. +- Experiments are tracked in custom database tables, + not Drupal config entities +- The `rl.php` endpoint provides high-performance + tracking with minimal Drupal bootstrap +- Arms are typically node IDs; the analyzer resolves + them to node titles automatically +- Views management is handled by `drush_webmaster` + (`wm:view:*` commands); do not duplicate diff --git a/.claude/skills/rl/SKILL.md b/.claude/skills/rl/SKILL.md index b3b5bb5..26fde07 100644 --- a/.claude/skills/rl/SKILL.md +++ b/.claude/skills/rl/SKILL.md @@ -16,13 +16,13 @@ triggers: - variant --- -# RL — Reinforcement Learning Experiments +# RL: Reinforcement Learning Experiments You are managing A/B testing experiments powered by Thompson Sampling. The RL module tracks impressions (turns) and conversions (rewards) across content variants and automatically optimizes traffic distribution. -## Preamble — Auto-discover Current State +## Preamble: Auto-discover Current State ```bash # List all active experiments @@ -133,4 +133,4 @@ drush rl:config:list - Arms are typically node IDs; the analyzer resolves them to node titles automatically - Views management is handled by `drush_webmaster` - (`wm:view:*` commands) — do not duplicate + (`wm:view:*` commands); do not duplicate diff --git a/.github/workflows/skills-pair-check.yml b/.github/workflows/skills-pair-check.yml new file mode 100644 index 0000000..d847bfd --- /dev/null +++ b/.github/workflows/skills-pair-check.yml @@ -0,0 +1,29 @@ +name: Skills pair check + +on: + pull_request: + paths: + - '.claude/**' + - '.agents/**' + - '**/.claude/**' + - '**/.agents/**' + +jobs: + pairs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Fail when paired skill files differ + run: | + fail=0 + while IFS= read -r claude_file; do + agents_file="${claude_file//\/.claude\//\/.agents\/}" + if [ -f "$agents_file" ]; then + if ! cmp -s "$claude_file" "$agents_file"; then + echo "DIFFER: $claude_file vs $agents_file" + fail=1 + fi + fi + done < <(find . -type f -path '*/.claude/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + exit $fail diff --git a/modules/rl_menu_link/.agents/skills/rl_menu_link/SKILL.md b/modules/rl_menu_link/.agents/skills/rl_menu_link/SKILL.md index 11a3b84..4b9474c 100644 --- a/modules/rl_menu_link/.agents/skills/rl_menu_link/SKILL.md +++ b/modules/rl_menu_link/.agents/skills/rl_menu_link/SKILL.md @@ -1,57 +1,138 @@ -# RL Menu Link — A/B test menu link labels via Drush CLI +--- +name: rl_menu_link +version: 1.0.0 +description: > + A/B test Drupal menu link labels using Thompson Sampling. Works for + both menu_link_content entities and YAML-defined links. Per-language + scoping. Manage experiments via Drush CLI. +triggers: + - /rl-menu-link + - menu link test + - menu label test + - menu variant + - a/b test menu + - test menu link + - rl_menu_link +--- -A/B test Drupal menu link labels for menu_link_content entities and -YAML-defined links using Thompson Sampling. Per-language scoping. +# RL Menu Link: Drush CLI -## Commands +You are managing A/B testing experiments for Drupal menu link labels. +The rl_menu_link module is a content-entity-backed integration on top +of the parent rl module's Thompson Sampling engine. -### Discovery -- `drush rl:menu-link:list` — List experiments (`--enabled=yes|no|all`) -- `drush rl:menu-link:get ` — Full details + live arm stats + original label +## Preamble: Auto-discover Current State -### Lifecycle -- `drush rl:menu-link:create --variants="A,B,C"` — Create -- `drush rl:menu-link:update --label="X"` — Update -- `drush rl:menu-link:delete ` — Delete + purge analytics +```bash +# List all menu link experiments with stats. +drush rl:menu-link:list --format=yaml -### Common options -- `--variants="A,B,C"` or `--variants=A --variants=B` — Alternative labels -- `--label="..."` — Human-readable label -- `--langcode=es` — Per-language scope (default `und` = all languages) -- `--disabled` — Create as disabled -- `--dry-run` — Preview without applying +# Filter to active experiments only. +drush rl:menu-link:list --enabled=yes --format=yaml +``` -All commands output YAML. All state-changing commands support `--dry-run`. +## Commands Reference -## Plugin ID format +| Command | Alias | Purpose | +|---|---|---| +| `rl:menu-link:list` | `rl-mll` | List experiments (`--enabled=yes\|no\|all`) | +| `rl:menu-link:get ` | `rl-mlg` | Show full details + live arm stats + original label | +| `rl:menu-link:create ` | `rl-mlc` | Create experiment (`--variants`, `--label`, `--langcode`, `--disabled`, `--dry-run`) | +| `rl:menu-link:update ` | `rl-mlu` | Update label / variants / enable / disable (`--dry-run`) | +| `rl:menu-link:delete ` | `rl-mld` | Delete experiment AND purge RL analytics (`--dry-run`) | -- User-created menu links: `menu_link_content:abc-uuid` -- YAML-defined links: machine name like `system.admin_content`, - `user.page`, `system.admin_structure` +All state-changing commands support `--dry-run`. All commands output YAML. ## Concepts -- The original label is always tested as variant `v0` (read live from - the menu link manager). -- Stored variants are `v1`, `v2`, etc. -- Each `(plugin_id, langcode)` pair is its own experiment with - independent Thompson Sampling state. -- Reward signal: user clicked the tracked menu link. -- Lookups are indexed; tested at 10K+ experiments per site. +- **Plugin ID**: every menu link in Drupal has a plugin ID. For + user-created `menu_link_content` entities it looks like + `menu_link_content:abc-uuid` (the entity UUID). For YAML-defined links + from contrib/custom modules it is the link's machine name (e.g., + `system.admin_content`, `user.page`). +- **Variant**: an alternative label text. The original label (whatever + the menu link normally renders) is always tested as variant `v0`. + Stored variants are `v1`, `v2`, etc. +- **Langcode**: experiments are scoped per language. Use `und` (the + default, `LANGCODE_NOT_SPECIFIED`) for "all languages". Lookup tries + language-specific match first, falls back to "all languages". +- **Reward**: a click on the tracked menu link. + +## Workflow Examples + +### Test alternatives for a content menu link + +```bash +# Find the plugin ID first via the menu UI or: +drush ev "echo \\Drupal::entityTypeManager()->getStorage('menu_link_content')->load(1)->getPluginId();" + +# Then create the experiment. +drush rl:menu-link:create menu_link_content:abc-uuid \ + --variants="Services,What We Do,Solutions" \ + --label="Services menu link test" +``` -## Example +### Test a core admin menu link ```bash -# A/B test the core "Content" admin link with alternatives. drush rl:menu-link:create system.admin_content \ - --variants="Content,Manage Content,Site Content" + --variants="Content,Manage Content,Edit Site" +``` -# Check progress. -drush rl:menu-link:get +### Test a Spanish-only variant + +```bash +drush rl:menu-link:create menu_link_content:abc-uuid \ + --variants="Servicios,Lo Que Hacemos" \ + --langcode=es +``` + +### Use multiple --variants flags -# Delete + purge. -drush rl:menu-link:delete +```bash +drush rl:menu-link:create system.admin_structure \ + --variants="Structure" \ + --variants="Site Structure" \ + --variants="Layout" +``` + +### Preview before applying + +```bash +drush rl:menu-link:create system.admin_content \ + --variants="One,Two" \ + --dry-run +``` + +### Check current state of an experiment + +```bash +drush rl:menu-link:get +# Returns: id, label, plugin id, original_label (read live from menu +# link manager), langcode, enabled, variants list, rl_experiment_id, +# total_turns, per-arm turns/rewards/rate. ``` -The same admin UI is available at /admin/config/services/rl-menu-link -via Views. +## How variants reach end users + +1. The module's preprocess_menu hook walks the menu tree on every + render and looks up an experiment by each item's plugin ID. +2. Matching items get their `title` swapped with the Thompson Sampling + winner, and `data-rl-ml-experiment-id` / `data-rl-ml-arm-id` data + attributes are injected onto the rendered anchor. +3. The bundled tracking JS uses IntersectionObserver to record an + impression when the link enters the viewport, and a click handler + to record a reward. +4. Tens of thousands of experiments scale via indexed `(plugin_id, + langcode)` lookups (content entities, not config). + +## Notes + +- The same menu link can have separate experiments per language. +- Vertical tabs on `menu_link_content` edit forms also create menu link + experiments; the Drush commands operate on the same content entities. +- The admin UI is at `/admin/config/services/rl-menu-link` (Views). +- Analytics are managed by the parent `rl` module; use `drush rl:list` + and the `rl:experiment:*` commands to inspect raw turn/reward data. +- Deleting an experiment via this command purges the RL analytics + tables (turns, rewards, snapshots, registry) atomically. diff --git a/modules/rl_menu_link/.claude/skills/rl_menu_link/SKILL.md b/modules/rl_menu_link/.claude/skills/rl_menu_link/SKILL.md index f536ded..4b9474c 100644 --- a/modules/rl_menu_link/.claude/skills/rl_menu_link/SKILL.md +++ b/modules/rl_menu_link/.claude/skills/rl_menu_link/SKILL.md @@ -15,13 +15,13 @@ triggers: - rl_menu_link --- -# RL Menu Link — Drush CLI +# RL Menu Link: Drush CLI You are managing A/B testing experiments for Drupal menu link labels. The rl_menu_link module is a content-entity-backed integration on top of the parent rl module's Thompson Sampling engine. -## Preamble — Auto-discover Current State +## Preamble: Auto-discover Current State ```bash # List all menu link experiments with stats. diff --git a/modules/rl_page_title/.agents/skills/rl_page_title/SKILL.md b/modules/rl_page_title/.agents/skills/rl_page_title/SKILL.md index 8e6cc4a..0ec4fdf 100644 --- a/modules/rl_page_title/.agents/skills/rl_page_title/SKILL.md +++ b/modules/rl_page_title/.agents/skills/rl_page_title/SKILL.md @@ -1,54 +1,133 @@ -# RL Page Title — A/B test page titles via Drush CLI +--- +name: rl_page_title +version: 1.0.0 +description: > + A/B test page titles for any Drupal page (nodes, Views displays, custom + controllers, any path) using Thompson Sampling. Per-language scoping. + Manage experiments via Drush CLI. +triggers: + - /rl-page-title + - page title test + - title experiment + - title variant + - a/b test page title + - test page title + - rl_page_title +--- -A/B test Drupal page titles for any page (nodes, Views displays, custom -controllers, any path) using Thompson Sampling. Per-language scoping. +# RL Page Title: Drush CLI -## Commands +You are managing A/B testing experiments for Drupal page titles. The +rl_page_title module is a content-entity-backed integration on top of +the parent rl module's Thompson Sampling engine. -### Discovery -- `drush rl:page-title:list` — List experiments (`--enabled=yes|no|all`) -- `drush rl:page-title:get ` — Full details + live arm stats +## Preamble: Auto-discover Current State -### Lifecycle -- `drush rl:page-title:create --variants="A,B,C"` — Create -- `drush rl:page-title:update --label="X"` — Update label/variants/enable/disable -- `drush rl:page-title:delete ` — Delete + purge analytics +```bash +# List all page title experiments with stats. +drush rl:page-title:list --format=yaml + +# Filter to active experiments only. +drush rl:page-title:list --enabled=yes --format=yaml +``` + +## Commands Reference -### Common options -- `--variants="A,B,C"` or `--variants=A --variants=B` — Alternative titles -- `--label="..."` — Human-readable label -- `--langcode=es` — Per-language scope (default `und` = all languages) -- `--disabled` — Create as disabled -- `--dry-run` — Preview without applying +| Command | Alias | Purpose | +|---|---|---| +| `rl:page-title:list` | `rl-ptl` | List experiments (`--enabled=yes\|no\|all`) | +| `rl:page-title:get ` | `rl-ptg` | Show full details + live RL stats per arm | +| `rl:page-title:create ` | `rl-ptc` | Create experiment (`--variants`, `--label`, `--langcode`, `--disabled`, `--dry-run`) | +| `rl:page-title:update ` | `rl-ptu` | Update label / variants / enable / disable (`--dry-run`) | +| `rl:page-title:delete ` | `rl-ptd` | Delete experiment AND purge RL analytics (`--dry-run`) | -All commands output YAML. All state-changing commands support `--dry-run`. +All state-changing commands support `--dry-run`. All commands output YAML. ## Concepts -- The original title is always tested as variant `v0` (read live). -- Stored variants are `v1`, `v2`, etc. -- Each `(path, langcode)` pair is its own experiment with independent - Thompson Sampling state. -- Reward signal: user stayed on page 10 seconds (bounce-rate proxy). -- Lookups are indexed; tested at 10K+ experiments per site. +- **Path**: the internal Drupal path to test (e.g., `/node/42`, `/blog`, + `/user/login`). Aliases are accepted and resolved on save. +- **Variant**: an alternative title text. The original title (whatever + Drupal renders normally) is always tested as variant `v0`. Stored + variants are `v1`, `v2`, etc. +- **Langcode**: experiments are scoped per language. Use `und` (the + default, `LANGCODE_NOT_SPECIFIED`) for "all languages". Lookup tries + language-specific match first, falls back to "all languages". +- **Reward**: a successful outcome (the user stayed on the page for 10 + seconds, default reward strategy). Tracked client-side via the + bundled JS. -## Example +## Workflow Examples + +### Test alternatives for a node title ```bash -# A/B test the /blog page title with two alternatives. -drush rl:page-title:create /blog \ - --variants="News & Insights,Latest Articles" \ - --label="Blog index test" +# Create with two alternatives. The original /node/42 title is v0. +drush rl:page-title:create /node/42 \ + --variants="Learn About Our Team,Meet ACME" -# Check progress. -drush rl:page-title:get +# Wait for traffic, then check progress. +drush rl:page-title:get -# Disable temporarily. +# Pause without deleting if results are inconclusive. drush rl:page-title:update --disable -# Delete + purge. +# Delete and purge analytics when done. drush rl:page-title:delete ``` -The same admin UI is available at /admin/config/services/rl-page-title -via Views. +### Test a Views page title in Spanish only + +```bash +drush rl:page-title:create /blog \ + --variants="Nuestro Blog,Últimos Artículos" \ + --langcode=es \ + --label="Blog index (Spanish)" +``` + +### Use multiple --variants flags instead of comma-separated + +```bash +drush rl:page-title:create /user/login \ + --variants="Sign In" \ + --variants="Log In to Continue" \ + --variants="Welcome Back" +``` + +### Test a custom module page (the form page itself) + +```bash +drush rl:page-title:create /node/add/article \ + --variants="Create New Article,Write a New Story" +``` + +### Preview before applying + +```bash +drush rl:page-title:create /blog \ + --variants="One,Two" \ + --dry-run +``` + +## How variants reach end users + +1. The module's preprocess_page_title hook intercepts the rendered title + for the matching path on every request. +2. The Thompson Sampling selector picks a winning variant based on + accumulated turns and rewards. +3. The bundled tracking JS records a turn on page load and a reward if + the user stays past the threshold (10s default). +4. Tens of thousands of experiments scale via indexed `(path, langcode)` + lookups (content entities, not config). + +## Notes + +- The same page can have separate experiments per language, with + independent Thompson Sampling state. +- Vertical tabs on entity edit forms also create page title experiments; + the Drush commands operate on the same content entities. +- The admin UI is at `/admin/config/services/rl-page-title` (Views). +- Analytics are managed by the parent `rl` module; use `drush rl:list` + and the `rl:experiment:*` commands to inspect raw turn/reward data. +- Deleting an experiment via this command purges the RL analytics + tables (turns, rewards, snapshots, registry) atomically. diff --git a/modules/rl_page_title/.claude/skills/rl_page_title/SKILL.md b/modules/rl_page_title/.claude/skills/rl_page_title/SKILL.md index 772b934..0ec4fdf 100644 --- a/modules/rl_page_title/.claude/skills/rl_page_title/SKILL.md +++ b/modules/rl_page_title/.claude/skills/rl_page_title/SKILL.md @@ -15,13 +15,13 @@ triggers: - rl_page_title --- -# RL Page Title — Drush CLI +# RL Page Title: Drush CLI You are managing A/B testing experiments for Drupal page titles. The rl_page_title module is a content-entity-backed integration on top of the parent rl module's Thompson Sampling engine. -## Preamble — Auto-discover Current State +## Preamble: Auto-discover Current State ```bash # List all page title experiments with stats. From f7a7dde892574c36aff51dae78268efe100c2677 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:02:21 +0200 Subject: [PATCH 2/4] chore: rephrase dashes in command output, comments and docs Use colons, commas and participles instead of em dashes and double dashes, per repo policy. --- modules/rl_page_title/README.md | 2 +- src/Drush/Commands/RlCommandsBase.php | 2 +- src/Drush/Commands/RlSetupCommands.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/rl_page_title/README.md b/modules/rl_page_title/README.md index 7302a13..0a0ca5a 100644 --- a/modules/rl_page_title/README.md +++ b/modules/rl_page_title/README.md @@ -34,7 +34,7 @@ experiment entity. Stored variants are arms v1, v2, ... vN. The RL experiment ID is a deterministic hash: `rl_page_title-{12-char-sha1-of-path-pipe-langcode}`. The hash includes the -langcode so each language gets its own Thompson Sampling state -- an English +langcode so each language gets its own Thompson Sampling state: an English experiment for `/blog` and a Spanish experiment for `/blog` accumulate separate turns and rewards. diff --git a/src/Drush/Commands/RlCommandsBase.php b/src/Drush/Commands/RlCommandsBase.php index 9c6fb61..aa3232c 100644 --- a/src/Drush/Commands/RlCommandsBase.php +++ b/src/Drush/Commands/RlCommandsBase.php @@ -18,7 +18,7 @@ abstract class RlCommandsBase extends DrushCommands { /** * Switches to admin user for the duration of the Drush process. * - * Does not call switchBack() — relies on process termination after + * Does not call switchBack(), relying on process termination after * command execution. */ protected function switchToAdmin(): void { diff --git a/src/Drush/Commands/RlSetupCommands.php b/src/Drush/Commands/RlSetupCommands.php index 5a42f6b..536239b 100644 --- a/src/Drush/Commands/RlSetupCommands.php +++ b/src/Drush/Commands/RlSetupCommands.php @@ -180,15 +180,15 @@ protected function checkSkillFiles(array $modules, string $projectRoot, string $ continue; } if (!file_exists($dest)) { - $results[] = sprintf('%s — NOT INSTALLED', $relativePath); + $results[] = sprintf('%s: NOT INSTALLED', $relativePath); $outdated = TRUE; } elseif (md5_file($source) !== md5_file($dest)) { - $results[] = sprintf('%s — OUTDATED', $relativePath); + $results[] = sprintf('%s: OUTDATED', $relativePath); $outdated = TRUE; } else { - $results[] = sprintf('%s — up to date', $relativePath); + $results[] = sprintf('%s: up to date', $relativePath); } } } From b505fb7615392dbcad7f3125892d41e872237ea7 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:26:44 +0200 Subject: [PATCH 3/4] Require both SKILL.md counterparts in pair check The pair check used to skip a skill silently when either counterpart file was missing. Require both SKILL.md files to exist (when the skill lives in both trees) before comparing contents. Skills living in only one tree stay out of scope by design, as do host-specific files such as agents/openai.yaml. --- .github/workflows/skills-pair-check.yml | 31 ++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/skills-pair-check.yml b/.github/workflows/skills-pair-check.yml index d847bfd..b4dff50 100644 --- a/.github/workflows/skills-pair-check.yml +++ b/.github/workflows/skills-pair-check.yml @@ -17,13 +17,38 @@ jobs: - name: Fail when paired skill files differ run: | fail=0 + # Every SKILL.md whose skill exists in both trees must exist in + # both trees and be byte-identical. Skills living in only one + # tree (e.g. Claude-only helpers) are skipped by design. while IFS= read -r claude_file; do agents_file="${claude_file//\/.claude\//\/.agents\/}" - if [ -f "$agents_file" ]; then - if ! cmp -s "$claude_file" "$agents_file"; then + agents_dir=$(dirname "$agents_file") + if [ -d "$agents_dir" ]; then + if [ ! -f "$agents_file" ]; then + echo "MISSING: $agents_file (no .agents counterpart)" + fail=1 + elif ! cmp -s "$claude_file" "$agents_file"; then echo "DIFFER: $claude_file vs $agents_file" fail=1 fi fi - done < <(find . -type f -path '*/.claude/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + done < <(find . -type f -name 'SKILL.md' -path '*/.claude/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + while IFS= read -r agents_file; do + claude_file="${agents_file//\/.agents\//\/.claude\/}" + claude_dir=$(dirname "$claude_file") + if [ -d "$claude_dir" ] && [ ! -f "$claude_file" ]; then + echo "MISSING: $claude_file (no .claude counterpart)" + fail=1 + fi + done < <(find . -type f -name 'SKILL.md' -path '*/.agents/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + # All other files are compared only when both counterparts + # exist; host-specific files such as agents/openai.yaml stay + # optional. + while IFS= read -r claude_file; do + agents_file="${claude_file//\/.claude\//\/.agents\/}" + if [ -f "$agents_file" ] && ! cmp -s "$claude_file" "$agents_file"; then + echo "DIFFER: $claude_file vs $agents_file" + fail=1 + fi + done < <(find . -type f -path '*/.claude/*' -not -name 'SKILL.md' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') exit $fail From 511145f5c5bb7313658112206bd58a4c6974bc12 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:32:57 +0200 Subject: [PATCH 4/4] Wrap long lines in pair check --- .github/workflows/skills-pair-check.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/skills-pair-check.yml b/.github/workflows/skills-pair-check.yml index b4dff50..a5a8057 100644 --- a/.github/workflows/skills-pair-check.yml +++ b/.github/workflows/skills-pair-check.yml @@ -32,7 +32,9 @@ jobs: fail=1 fi fi - done < <(find . -type f -name 'SKILL.md' -path '*/.claude/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + done < <(find . -type f -name 'SKILL.md' -path '*/.claude/*' \ + -not -path './.git/*' -not -path '*/node_modules/*' \ + -not -path '*/vendor/*' -not -path '*worktrees*') while IFS= read -r agents_file; do claude_file="${agents_file//\/.agents\//\/.claude\/}" claude_dir=$(dirname "$claude_file") @@ -40,15 +42,21 @@ jobs: echo "MISSING: $claude_file (no .claude counterpart)" fail=1 fi - done < <(find . -type f -name 'SKILL.md' -path '*/.agents/*' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + done < <(find . -type f -name 'SKILL.md' -path '*/.agents/*' \ + -not -path './.git/*' -not -path '*/node_modules/*' \ + -not -path '*/vendor/*' -not -path '*worktrees*') # All other files are compared only when both counterparts # exist; host-specific files such as agents/openai.yaml stay # optional. while IFS= read -r claude_file; do agents_file="${claude_file//\/.claude\//\/.agents\/}" - if [ -f "$agents_file" ] && ! cmp -s "$claude_file" "$agents_file"; then + if [ -f "$agents_file" ] \ + && ! cmp -s "$claude_file" "$agents_file"; then echo "DIFFER: $claude_file vs $agents_file" fail=1 fi - done < <(find . -type f -path '*/.claude/*' -not -name 'SKILL.md' -not -path './.git/*' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*worktrees*') + done < <(find . -type f -path '*/.claude/*' \ + -not -name 'SKILL.md' -not -path './.git/*' \ + -not -path '*/node_modules/*' -not -path '*/vendor/*' \ + -not -path '*worktrees*') exit $fail