Skip to content

Spec audit: turn specification.website findings into suggested tasks - #767

Draft
ilicfilip wants to merge 20 commits into
developfrom
filip/spec-audit
Draft

Spec audit: turn specification.website findings into suggested tasks#767
ilicfilip wants to merge 20 commits into
developfrom
filip/spec-audit

Conversation

@ilicfilip

@ilicfilip ilicfilip commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a website-spec audit that runs against the site's public URL and turns each failing rule into a Progress Planner suggested task, throttled to 1/day. Two engines share all plugin-side task-mapping code:

  • Deterministic PHP checks (always on): 5 starter rules — doctype, html-lang, meta-charset, meta-description, xml-sitemaps. Operates on a single shared homepage fetch.
  • WP 7.0 AI client (optional, requires a configured connector): asks Claude/GPT/Gemini to evaluate the homepage against https://specification.website/llms.txt. PHP wins on overlapping rule_ids.

Designed so the audit engine can later move from the plugin to the progressplanner.com SaaS without touching task-creation code (phase B — Remote_Audit_Source is a stub today, a regression test guards the contract).

Key design points

  • Zero outbound HTTP from admin_init. The audit runs only from CLI, AJAX-shutdown (with fastcgi_finish_request()), or a dedicated cron hook. The data collector's update_cache() is a no-op unless an explicit caller has opted in. This caused FPM pool starvation during development and is now structurally prevented.
  • Self-healing for retired rules. Tasks store prpl_source. If a PHP-check task's rule_id is no longer in the live registry, it auto-completes — so a future rule rename/retire doesn't strand orphan tasks. Legacy tasks without source meta backfill to php-check.
  • Throttle is deferred + survival-counted. The per-window counter only increments at shutdown for tasks that actually survived the request (so a same-request auto-completion doesn't burn the slot).
  • Canonical slugs. PHP and LLM both use spec.website's own URL slugs (doctype, html-lang, meta-charset, meta-description, xml-sitemaps). Each finding's doc_url points at the real spec page.

Full architecture, decisions, and open questions are in HANDOFF-spec-audit.md on this branch.

Verified live

On a local WP 7.0 + Yoast + Woo + Anthropic Connector site (planner.test):

  • wp prpl audit run produces ~5 PHP findings + ~10 LLM findings in ~18s.
  • ✅ Severity-prioritized throttle picks the most important rule for the daily slot.
  • ✅ Fix → re-audit → auto-complete loop works end-to-end.
  • ✅ Zero admin-pageload HTTP after the cache exists.

Not yet verified

  • Production WP 7.0 stable (testing was on nightly).
  • Phase-B SaaS endpoint (the stub returns [] until progressplanner.com/wp-json/progress-planner-saas/v1/audit exists).
  • Multisite.

Test plan

  • Pull the branch on a WP 7.0 install.
  • Read HANDOFF-spec-audit.md for the why-behind-each-decision context.
  • Run composer test (425/425 should pass — includes 27 spec-audit tests).
  • Run composer phpstan (clean).
  • wp prpl audit run on a fresh site — expect 1 task injected (the highest-severity failing rule).
  • Open WP admin → Progress Planner — task should appear with the spec.website "Why is this important?" link.
  • Fix the flagged issue, run again — rule flips to pass, next admin pageload auto-completes the task.
  • If you have an AI Connector configured: confirm mcp-llm source findings appear in wp eval dump of spec_audit_findings.

Open follow-ups (handoff doc lists them in priority order)

  1. (medium) Local-dev false positive on https-tls for .test/.local URLs — small filter.
  2. (medium) Rename Spec_Mcp_ClientSpec_Ai_Client (the "MCP" in the name was aspirational; core's AI client can't act as an MCP client).
  3. (low) --dry-run flag on the CLI command.
  4. (low) UI button for "Run audit now" (AJAX endpoint already exists).
  5. (low) Deactivation cleanup should unschedule the cron hook.

🤖 Generated with Claude Code

ilicfilip and others added 10 commits May 29, 2026 17:47
Introduce the audit layer that checks a site against specification.website.
Defines the swappable Audit_Source contract + normalized finding schema
(Audit_Runner), five deterministic PHP checks (doctype, lang, charset,
robots.txt, sitemap) behind a filterable registry, a Local source that merges
PHP checks with an optional AI pass (PHP wins on overlap), a Remote SaaS source
stub for the future server-side engine, and Spec_Mcp_Client which drives WP 7.0's
core AI client.

Note: WP 7.0's AI client cannot act as an MCP client, so Spec_Mcp_Client feeds
the spec checklist + HTML to wp_ai_client_prompt() instead. The whole AI path is
guarded by is_available() and degrades to PHP-only checks; WP7-specific calls are
marked TODO(wp7-verify) as they can't be exercised without a live WP 7.0 connector.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the Spec_Audit data collector (caches the audit; runs the expensive
checks/LLM only on cache refresh, never on admin_init) and the Spec_Audit task
provider that turns failing findings into recommendations. The provider releases
at most one task per window (default daily), overridable via
progress_planner_spec_audit_max_tasks_per_window and _window filters; each
failing rule maps to one durable task and is auto-completed when a re-audit shows
it passing. Register both in their managers, and add a `wp prpl audit run` CLI
command plus a progress_planner_run_spec_audit AJAX trigger for on-demand runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover the deterministic PHP checks, Audit_Runner schema normalization/dedup,
graceful degradation to PHP-only findings when the AI layer is unavailable, the
per-window injection throttle and per-rule completion, and a shape-equality test
asserting the local and remote sources produce identical finding shapes (the
guard that keeps the phase-C and phase-B engines interchangeable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified on a live WP 7.0 install: the AI builder (WP_AI_Client_Prompt_Builder)
delegates SDK methods through __call, so the earlier method_exists() guards on
using_max_tokens/as_json_response/is_supported_for_text_generation silently
skipped those calls. Call them directly and gate availability on wp_supports_ai()
plus is_supported_for_text_generation(). Confirmed end-to-end via `wp prpl audit
run`: detects failing rules, injects one task, and the throttle blocks the second
run; the AI path correctly reports unavailable when no provider is configured and
degrades to PHP-only checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the inject-time recheck I added in ba74aa5 called wp_remote_get
from get_tasks_to_inject(), which runs on every admin_init. Each admin
pageview then triggered 1+ loopback HTTP requests back into the same
PHP-FPM pool serving the user — pinning workers and starving the whole
pool until nginx returned 502s for unrelated Valet sites.

Three structural changes prevent that class of bug entirely:

1. The Spec_Audit data collector's update_cache() is a no-op unless an
   explicit caller has opted in via Spec_Audit_Data_Collector::with_explicit_refresh().
   The Data_Collector_Manager's admin_init sweep therefore cannot trigger
   the audit. Sanctioned callers: CLI command, cron hook, AJAX shutdown.
2. collect() never falls back to calculate_data() on cache miss — a missing
   cache returns []. is_specific_task_completed() now distinguishes "no
   cache" from "rule passed", so an object-cache flush can't mass-complete
   every audit task.
3. The AJAX "run now" handler defers the audit to shutdown and calls
   fastcgi_finish_request() so the user's worker is released to the pool
   before the outbound HTTP starts. A daily wp-cron hook also drives
   refreshes from a non-web context.

Reverted is_still_failing()'s live recheck — it's the wrong place for HTTP.
Kept the deferred throttle counter (pure-PHP, no HTTP). Updated tests:
dropped the live-recheck test; added tests for the cache-empty completion
guard and the no-explicit-refresh no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The robots.txt check turned out to be the wrong rule for a suggested task:
WordPress generates a virtual robots.txt automatically, and when it fails
(e.g. status 404 with a real body, as seen on a Yoast + Woo install), the
fix is at the nginx/Valet routing level — not something a WordPress user
can address from wp-admin. Suggested tasks should be actionable inside
WordPress.

Replace with a meta-description check that operates on the homepage HTML
we already fetch (zero extra outbound HTTP). The user-facing fix is real
and in-scope: install/configure an SEO plugin or set a description in the
theme. Yoast/RankMath both supply it out of the box, so the recommendation
naturally guides users to a known good path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tasks are keyed by rule_id. When we rename, retire, or filter out a check
(as just happened with robots-txt), the old task lives on in users' DBs
forever — its completion logic only knew how to react to the rule still
being audited.

Two changes:

1. On injection, persist the finding's source as prpl_source meta so the
   provider can later tell deterministic (php-check) tasks from
   probabilistic (mcp-llm / saas) ones. Legacy tasks without the meta are
   backfilled to 'php-check' (the original starter set was all PHP checks),
   so existing installs self-heal on upgrade too.

2. In is_specific_task_completed(), short-circuit to "complete" for any
   php-check task whose rule_id is no longer present in the live
   Checks_Registry. Does NOT apply to LLM/SaaS tasks — their rule space
   is open-ended and a rule missing from one audit just means the model
   didn't mention it this run, not that it was retired.

Pure in-memory work; no outbound HTTP. The existing cache-empty guard in
is_specific_task_completed() still prevents mass-completion from an
object-cache flush.

Tests use synthetic rule IDs (rule-a etc.) that the registry never knew
about, so the test setUp now registers no-op stub checks for them; the
new retired-rule tests remove all audit_checks filters to simulate
removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs blocked the AI path on a live WP 7.0 + Anthropic connector,
found via foreground reflection probing rather than running the audit live:

1. JSON schema rejected. Anthropic's structured-output API requires
   `additionalProperties:false` on every `type:object`. Without it the
   call 400s and the WP_Error is swallowed, leaving no diagnostic.

2. Checklist URL was wrong. /mcp/ returns the HTML page describing the
   MCP server, not spec content — the model was fed 22KB of irrelevant
   HTML. Switched to /llms.txt, the canonical LLM-oriented Markdown
   index (~37KB) the spec publishes for exactly this purpose.

3. Errors were silent. run_prompt() returned null on any failure, and
   audit_url() caught Throwables and json_decode failures the same way.
   Added log_error() that writes to error_log under WP_DEBUG so future
   failures show up in debug.log without changing the public contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the architecture, decisions, what's verified vs not, open questions,
and how to test. The top priority follow-up is using spec.website's canonical
slugs as rule_ids so PHP and LLM findings dedupe naturally and doc_urls point
at real spec pages — written up in detail at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PHP checks were inventing slugs (html-doctype, html-lang-attribute,
charset-meta, xml-sitemap) and the LLM was inventing its own (doctype,
html-lang, meta-charset, etc.). They covered the same rules with
different identifiers, so the "PHP wins on overlap" dedupe never fired
and doc_urls were generic /specification.website/ pointers.

Adopt the spec's own URL slugs as canonical rule_ids:
  html-doctype          → doctype          (/spec/foundations/doctype/)
  html-lang-attribute   → html-lang        (/spec/foundations/html-lang/)
  charset-meta          → meta-charset     (/spec/foundations/meta-charset/)
  meta-description      → meta-description (/spec/foundations/meta-description/)
  xml-sitemap           → xml-sitemaps     (/spec/seo/xml-sitemaps/)

Also align categories to the spec ('foundations' for the HTML-baseline
checks, 'seo' for sitemaps). Each finding's doc_url now points at the
actual spec page so the "Why is this important?" link is genuinely
useful.

Update the AI prompt to instruct the model to use canonical slugs derived
from the spec URL pattern, with concrete examples. PHP and LLM findings
will now dedupe naturally where they cover the same rule.

Existing tasks with the old rule_ids will be auto-completed by the
self-heal logic (commit bbfaf44) on the next admin pageload — no manual
migration needed. Drop the canonical-slug TODO from the handoff doc and
renumber remaining open questions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Test on Playground
Test this pull request on the Playground
or download the zip

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

✅ Code Coverage Report

Metric Value
Total Coverage 32.49% 📉
Base Coverage 31.62%
Difference 📈 0.87%

⚠️ Coverage below recommended 40% threshold

🎉 Great job maintaining/improving code coverage!

📊 File-level Coverage Changes (18 files)

🆕 New Files

Class Coverage Lines
🟡 Progress_Planner\Suggested_Tasks\Audit\Audit_Runner 78.57% 33/42
🟢 Progress_Planner\Suggested_Tasks\Audit\Checks\Charset_Check 93.75% 15/16
🔴 Progress_Planner\Suggested_Tasks\Audit\Checks\Checks_Registry 26.83% 11/41
🟢 Progress_Planner\Suggested_Tasks\Audit\Checks\Doctype_Check 100.00% 14/14
🟢 Progress_Planner\Suggested_Tasks\Audit\Checks\Lang_Attribute_Check 92.86% 13/14
🟢 Progress_Planner\Suggested_Tasks\Audit\Checks\Meta_Description_Check 100.00% 19/19
🔴 Progress_Planner\Suggested_Tasks\Audit\Checks\Sitemap_Check 3.70% 1/27
🟢 Progress_Planner\Suggested_Tasks\Audit\Local_Audit_Source 87.50% 14/16
🔴 Progress_Planner\Suggested_Tasks\Audit\Remote_Audit_Source 37.21% 16/43
🔴 Progress_Planner\Suggested_Tasks\Audit\Spec_Mcp_Client 0.00% 0/123
🔴 Progress_Planner\Suggested_Tasks\Data_Collector\Spec_Audit 55.56% 10/18
🟡 Progress_Planner\Suggested_Tasks\Providers\Spec_Audit 68.84% 95/138
🔴 Progress_Planner\WP_CLI\Audit_Command 0.00% 0/28

📈 Coverage Improved

Class Before After Change
Progress_Planner\Suggested_Tasks\Providers\Tasks 36.59% 38.41% +1.82%
Progress_Planner\Suggested_Tasks\Data_Collector\Data_Collector_Manager 64.29% 65.52% +1.23%
Progress_Planner\Suggested_Tasks_DB 90.11% 90.66% +0.55%
Progress_Planner\Suggested_Tasks\Tasks_Manager 62.83% 63.16% +0.33%

📉 Coverage Decreased

Class Before After Change
Progress_Planner\Base 45.40% 45.12% -0.28%
ℹ️ About this report
  • All tests run in a single job with Xdebug coverage
  • Security tests excluded from coverage to prevent output issues
  • Coverage calculated from line coverage percentages

ilicfilip and others added 5 commits June 1, 2026 14:40
is_specific_task_completed() treated "rule no longer reported in a
populated audit" as completion for any source. Because the mcp-llm
engine is non-deterministic, a rule simply being absent from a later
audit run made its task self-complete even though the user fixed
nothing — contradicting the documented design (only php-check tasks
should complete on rule-absence; LLM/SaaS tasks complete only on an
explicit pass).

Guard the rule-absence branch on the php-check source. Add a
regression test asserting an mcp-llm task is not completed on omission
but is completed on an explicit pass, and clarify the existing
php-check test. Update the handoff doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_audit_now() returned only the tasks its own get_tasks_to_inject()
call created. When the bootstrap inject_tasks() sweep already consumed
the daily throttle slot earlier in the same request, the CLI command
reported "0 task(s) injected" even though a task had been injected.
Return pending_release_ids (all tasks injected this request) so the
count reflects reality.

Also fix the handoff's clean-state snippet: it iterated the cached
get_tasks_by() result while delete_recommendation() flushed that cache
group mid-loop, skipping tasks and leaving survivors. Snapshot the IDs
with a raw get_posts() + provider tax_query instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two independent upstream breakages stack in this job:

1. `wp package install` fails with "Your github oauth token for github.com
   contains invalid characters". The stable WP-CLI phar bundles an old
   Composer that rejects the current GitHub Actions token format (hyphens) —
   composer#12076. setup-php exports the token via COMPOSER_AUTH, so pinning
   setup-php or clearing GITHUB_TOKEN does not help (both were tried in
   #766 and failed). The nightly phar bundles a Composer with relaxed
   validation, which fixed this step in #766.

2. The plugin check step then failed with "Environment not initialized. Run
   `wp-env start` first." plugin-check-action generated a .wp-env.json that
   loaded plugin-check from a download URL; on newer runner images wp-env
   exits 0 without initializing. #766 worked around it by pinning
   @wordpress/env to 11.5.0, which did not work and left that PR blocked.

Upstream fixed (2) in plugin-check-action v1.1.7, so no @wordpress/env pin
is needed — pinning to 11.5.0 against a fixed action would be
counterproductive. v1.1.9 also picks up the v1.1.8 bundle-regression fix.

Ref: WordPress/plugin-check-action#590

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… WP-CLI

Reverts the WP-CLI nightly step added in the previous commit and documents
why the surrounding pins exist.

The nightly was a May workaround for Composer rejecting the GitHub Actions
token ("contains invalid characters", composer#12076). Two things changed:

- composer#12076 was fixed in Composer 2.10.0 (2026-05-28, one day after
  that debugging session) and setup-php now ships 2.10.2, so the stable
  WP-CLI phar no longer hits the token error. Confirmed on CI run
  31781466704: Composer reached dependency resolution, no token error.
- The nightly has since become wp-cli 3.0.0-alpha, and every released
  dist-archive-command requires wp-cli ^2 / ^2.13, so `wp package install`
  can no longer resolve against it. The nightly is now a dead end.

dist-archive-command stays on v3.1.0: it is the newest release accepting
wp-cli ^2. v3.2.x needs ^2.13, which has no stable release (latest is
2.12.0).

The remaining real fix is the action bump. v1.1.7 fixed the wp-env
silent-startup failure ("Environment not initialized") that blocked #766;
v1.1.9 picks up the v1.1.8 bundle-regression fix. Since that landed
upstream, the @wordpress/env 11.5.0 pin from #766 is not carried over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
11 0 11

⚠️ Warnings (11)

📁 classes/suggested-tasks/providers/class-spec-audit.php (1 warning)
📍 Line 🔖 Check 💬 Message
518 Squiz.PHP.DiscouragedFunctions.Discouraged The use of function set_time_limit() is discouraged
📁 classes/suggested-tasks/providers/class-content-review.php (4 warnings)
📍 Line 🔖 Check 💬 Message
232 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.
377 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.
381 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.
388 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.
📁 classes/suggested-tasks/data-collector/class-unpublished-content.php (1 warning)
📍 Line 🔖 Check 💬 Message
103 WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.
📁 classes/activities/class-query.php (2 warnings)
📍 Line 🔖 Check 💬 Message
71 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $table_name used in $wpdb->query()\n$table_name assigned unsafely at line 58.
163 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $where_args used in $wpdb->get_results()\n$where_args assigned unsafely at line 153.
📁 classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php (1 warning)
📍 Line 🔖 Check 💬 Message
111 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_row()\n$query assigned unsafely at line 98.
📁 classes/suggested-tasks/data-collector/class-terms-without-posts.php (1 warning)
📍 Line 🔖 Check 💬 Message
120 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 118.
📁 classes/suggested-tasks/data-collector/class-terms-without-description.php (1 warning)
📍 Line 🔖 Check 💬 Message
108 PluginCheck.Security.DirectDB.UnescapedDBParameter Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 106.

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

ilicfilip and others added 2 commits August 14, 2026 10:02
Tested against the WP 7.1 RC ahead of its release. Clears the Plugin Check
outdated_tested_upto_header error (readme declared 6.9).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Security workflow (symfonycorp/security-checker-action) failed on
squizlabs/php_codesniffer 3.13.5: CVE-2026-67434, HIGH, OS command injection
in the gitblame report via a crafted filename. Patched in 3.13.6.

GHSA-hmqg-cxww-wqhq

Dev-only, transitive (pulled by wpcs/yoastcs/phpcsextra et al). Every
constraint already allows ^3.13.x, so this is a lockfile-only bump with no
composer.json change. Composer also refreshed the packages locked alongside
it: phpcodesniffer-composer-installer v1.2.1, phpcsutils 1.2.3,
phpcsextra 1.5.1, wpcs 3.4.1. `composer check-cs` passes on the new set.

Note this does not touch the 3 open composer/composer advisories
(CVE-2026-59946/59947/59948) — a separate package, not flagged by this check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor
Composer package changes
Dev Packages Operation Base Target
squizlabs/php_codesniffer Upgraded 3.13.5 3.13.6

ilicfilip and others added 3 commits August 14, 2026 10:17
…POSER_AUTH

The "Your github oauth token for github.com contains invalid characters"
failure is INTERMITTENT, not fixed. Proof: run 31782281682 (df71e84) passed,
and a re-run of that identical commit minutes later failed at the same step
with the same error. Composer 2.10.2 and wp-cli 2.12.0 in both runs, so the
Composer version was never the deciding factor — whether the ephemeral token
GitHub mints for a given run contains a rejected character is.

This supersedes the claim in c96d1ca that composer#12076 being fixed in
2.10.0 made the stable phar safe. It did not; that run was luck.

dist-archive-command is a public package, so this install needs no auth at
all. Clear BOTH token sources: wp-cli reads GITHUB_TOKEN directly, and
setup-php separately exports the same token via COMPOSER_AUTH. b0ec35d
cleared only GITHUB_TOKEN, leaving COMPOSER_AUTH to reintroduce it — which is
why that attempt failed and why single-variable fixes look like they work
until the next unlucky token.

Uses `unset` rather than env: '' because wp-cli writes the token whenever
getenv() returns a string, and an empty string qualifies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unset-only fix in 3f52e7a was insufficient: run 31783342116 shows
`unset GITHUB_TOKEN COMPOSER_AUTH` executing and the token error still
occurring. So the credential is not only in the environment — setup-php also
persists it into Composer's global auth config on the runner, which no env
change can remove.

Remove the stored credential (global github-oauth entry + auth.json, incl.
wp-cli's own package home) before unsetting the env vars, so nothing can
rewrite it. Also logs which auth locations are populated, so the next run
identifies the real source instead of another guess.

Still intermittent by nature: it only fails when the run's ephemeral token
happens to contain a rejected character, so a single green run proves
nothing here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Workflow: replace the diagnostic block with the one line that actually
matters. Run 31784727273 proved GITHUB_TOKEN and COMPOSER_AUTH are already
empty at that point and the rejected credential lives in
~/.composer/auth.json, so deleting that file is the whole fix — the unset
and the extra rm targets were noise from narrowing the cause. Comments now
state the verified mechanism rather than the intermediate guesses.

`rm -f` exits 0 on a missing path, so no `|| true` is needed under `bash -e`.

HANDOFF-spec-audit.md: purpose served (it says "Not pushed. No PR." while the
branch is pushed with PR #767 open), and .distignore lists docs individually,
so it was shipping inside the built plugin zip — Plugin Check was scanning it.
Full content stays in git history from deb03b1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant