First commit of the image + Instagram A/B pipeline workstream. Ships the Cloudflare Workers AI image provider, its pricing + validator helpers, four new settings fields in a new "Images" admin section, and unit tests with mocked HTTP. Nothing calls the provider yet from the article pipeline — integration lands in commit 1b. Why: CEO-approved on 2026-04-15 (see convo/prautoblogger/threads/2026-04-image-pipeline/01-cto-handoff.md). Style is locked to the 90s infomercial pastiche; default model is FLUX.1 [schnell] at ~$0.0011/MP (~$0.01/article for 8 images across 4 placements x 2 variants). Risk flags: - Direct call to Cloudflare Workers AI bypassing AI Gateway (decision D-001, documented in convo/prautoblogger/decisions/). Gateway route to Workers AI currently 403s; OpenRouter gateway path unaffected. - prab_event_log structured writes deferred to commit 1b when the table lands (decision D-002). Logger-based logging is in place now. - No schema changes. No existing code paths touched outside settings. - Backward compatible: provider is instantiable but not wired to any consumer yet. Test plan: - CI runs PHPUnit including the new CloudflareImageProviderTest. - After merge: verify the new Images section appears on the settings page, the style-suffix textarea pre-fills with the infomercial prompt, and entering the existing CLOUDFLARE_AI_TOKEN + account UUID lets a future "Test Connection" button return OK (button ships in a later commit; the underlying validate_credentials_detailed() works today). Files: - includes/providers/interface-image-provider.php (adopted from CTO draft) - includes/providers/class-cloudflare-image-provider.php (283 lines) - includes/providers/class-cloudflare-image-pricing.php (78 lines) - includes/providers/class-cloudflare-image-validator.php (107 lines) - includes/admin/class-settings-fields.php (+ Images section, 4 fields) - prautoblogger.php (+ PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, _STYLE_SUFFIX) - tests/unit/Providers/CloudflareImageProviderTest.php - ARCHITECTURE.md (file tree, integrations table, options table, decision #16) - CONVENTIONS.md (How To: Add a New Image Provider) - CHANGELOG.md (Unreleased section) Agent-Session: cowork-2026-04-15-image-pipeline-commit-1a
14 KiB
Executable File
PRAutoBlogger — Conventions
This document codifies the naming patterns, coding conventions, and step-by-step guides for extending PRAutoBlogger. Update this file whenever a new pattern is introduced.
Naming Conventions
PHP Classes
- Format:
PRAutoBlogger_{PascalCase}(e.g.,PRAutoBlogger_Content_Generator) - File naming:
class-{kebab-case}.php(e.g.,class-content-generator.php) - Interfaces:
interface-{kebab-case}.phpwithPRAutoBlogger_{Name}_Interfaceclass name
Hooks (Actions & Filters)
- Prefix: All hooks start with
prautoblogger_ - Actions:
prautoblogger_{event}(e.g.,prautoblogger_before_content_generation) - Filters:
prautoblogger_filter_{what}(e.g.,prautoblogger_filter_article_idea_score) - Hook callbacks registered in:
class-prautoblogger.php(central loader), never in the classes themselves
Database
- Table prefix:
{$wpdb->prefix}prautoblogger_(e.g.,wp_prautoblogger_source_data) - Option prefix:
prautoblogger_(e.g.,prautoblogger_openrouter_api_key) - Post meta prefix:
_prautoblogger_(underscore prefix hides from custom fields UI) - Transient prefix:
prautoblogger_(e.g.,prautoblogger_reddit_token)
Constants
- Format:
PRAUTOBLOGGER_{UPPER_SNAKE}(e.g.,PRAUTOBLOGGER_MAX_RETRIES) - Defined in:
prautoblogger.php(bootstrap file) for global constants
Hook Callback Methods
- Action callbacks: Prefixed with
on_(e.g.,on_daily_generation_triggered) - Filter callbacks: Prefixed with
filter_(e.g.,filter_content_quality)
Error Handling & Logging
Structured Logger
All logging uses PRAutoBlogger_Logger (singleton) instead of raw error_log(). The logger writes to the prab_event_log custom table and forwards errors/warnings to PHP's error_log() for server-level monitoring. The verbosity threshold is user-configurable via Settings → Publishing → Log Level.
Never call error_log() directly. Always use the Logger:
$logger = PRAutoBlogger_Logger::instance();
$logger->error( 'API call failed: ' . $error_message, 'openrouter' );
$logger->warning( 'Budget nearing limit.', 'cost-tracker' );
$logger->info( 'Pipeline complete: 3 posts generated.', 'pipeline' );
$logger->debug( 'Token counts: prompt=1200, completion=800', 'openrouter' );
Log levels (ascending verbosity)
| Level | Numeric | Use for |
|---|---|---|
| error | 0 | Failures: API errors, parse failures, DB write failures |
| warning | 1 | Degraded behavior: budget approaching, missing providers, fallback pricing |
| info | 2 | Key events: pipeline start/stop, articles generated, metrics collected |
| debug | 3 | Verbose detail: token counts, timing, skipped posts, intermediate values |
Context strings
The second argument to every log call is a short context tag identifying the origin:
pipeline, scheduler, collector, analyzer, editor, openrouter, reddit, ga4, metrics, cost-tracker, encryption.
What gets shown to the user
- API key issues → admin notice (persistent until resolved)
- Budget exceeded → admin notice + email to site admin
- Generation failures → logged to
prab_generation_logwithresponse_status = 'error' - Generation failures also shown in the admin dashboard widget
- All log entries visible in PRAutoBlogger → Activity Log (filterable by level)
What gets retried
- LLM API calls: exponential backoff, max 3 retries, then fail loudly
- Reddit API calls: exponential backoff, max 3 retries, then skip this collection cycle
- Never retry silently. Every retry is logged. Every final failure is surfaced.
Error severity levels
- Fatal: Plugin cannot function (missing API key, budget exhausted) → admin notice, stop all scheduled jobs
- Retriable: Transient API failure → retry with backoff, log each attempt
- Skippable: Single post generation failed → log, skip this post, continue with others
How To: Add a New LLM Provider
- Create
includes/providers/class-{name}-provider.php - Implement
PRAutoBlogger_LLM_Provider_Interface(seeinterface-llm-provider.php) - Required methods:
send_chat_completion(),get_available_models(),estimate_cost() - Add provider option to the settings page in
admin/class-admin-page.php - Register the provider in
class-prautoblogger.phploader - Update ARCHITECTURE.md external API integrations table
Interface contract:
interface PRAutoBlogger_LLM_Provider_Interface {
public function send_chat_completion(array $messages, string $model, array $options = []): array;
public function get_available_models(): array;
public function estimate_cost(string $model, int $prompt_tokens, int $completion_tokens): float;
public function get_provider_name(): string;
}
How To: Add a New Source Provider (Social Platform)
- Create
includes/providers/class-{platform}-provider.php - Implement
PRAutoBlogger_Source_Provider_Interface(seeinterface-source-provider.php) - Required methods:
collect_data(),get_source_type(),validate_credentials() - Add API credentials fields to the settings page in
admin/class-admin-page.php - Register the provider in
class-prautoblogger.phploader - Add the source type string to the
ab_source_data.source_typeallowed values - Update ARCHITECTURE.md file tree and external API integrations table
Interface contract:
interface PRAutoBlogger_Source_Provider_Interface {
public function collect_data(array $config): array; // Returns PRAutoBlogger_Source_Data[]
public function get_source_type(): string; // e.g., 'reddit', 'tiktok'
public function validate_credentials(): bool;
public function get_rate_limit_status(): array;
}
How To: Add a New Image Provider
- Create
includes/providers/class-{name}-image-provider.php - Implement
PRAutoBlogger_Image_Provider_Interface(seeinterface-image-provider.php) - Required methods:
generate_image(),estimate_cost(),get_provider_name(),validate_credentials_detailed() - Split helpers into companions if you'd exceed the 300-line cap. The Cloudflare provider splits into provider + pricing + validator — mirror that shape.
- Add API credentials fields to
admin/class-settings-fields.phpunder theprautoblogger_imagessection - Register the provider in
class-prautoblogger.phploader once the image pipeline lands (commit 1b of the image workstream) - Update
ARCHITECTURE.mdfile tree, external API integrations table, and the options table
Interface contract:
interface PRAutoBlogger_Image_Provider_Interface {
public function generate_image( string $prompt, int $width, int $height, array $options = [] ): array; // {bytes, mime_type, width, height, model, seed, cost_usd, latency_ms}
public function estimate_cost( int $width, int $height, array $options = [] ): float;
public function get_provider_name(): string; // e.g., 'cloudflare_workers_ai'
public function validate_credentials_detailed(): array; // {status, message, debug?}
}
Retry + cost rules (non-negotiable)
- 5xx / 429 / network errors: retry with exponential backoff up to
PRAUTOBLOGGER_MAX_RETRIES, respectRetry-Afterwhen present. - 4xx other than 429: fail loudly on the first response — never retry a deterministic bad request.
- Validation check must be non-destructive (no image generation).
- Every attempt logs via
PRAutoBlogger_Loggerwith context{provider-slug}-image.
How To: Add a New Admin Setting
- Define the option key in the settings array in
admin/class-admin-page.php→get_settings_fields()method - Format: add an entry to the appropriate section array:
[ 'id' => 'prautoblogger_new_setting', 'label' => __('Setting Label', 'prautoblogger'), 'type' => 'text', // text, number, select, textarea, checkbox, password 'default' => '', 'description' => __('Help text shown below the field.', 'prautoblogger'), 'sanitize' => 'sanitize_text_field', // sanitization callback ] - The settings page renderer handles registration, rendering, and sanitization automatically from this array.
- Access the setting anywhere with:
get_option('prautoblogger_new_setting', $default) - For encrypted settings (API keys): use
PRAutoBlogger_Encryption::encrypt()/decrypt()
How To: Add a New Pipeline Stage
The writing pipeline is an ordered array of stages. Each stage is a class method on Content_Generator.
- Add the stage method to
class-content-generator.php:protected function stage_{name}(PRAutoBlogger_Content_Request $request): string - Register the stage in the
get_pipeline_stages()method for the appropriate mode (single_pass or multi_step) - Each stage receives the accumulated context and returns its output
- Each stage logs its cost via
Cost_Tracker::log_api_call() - If a stage fails after retries, the entire pipeline fails (no partial publishes)
How To: Add a New Frontend Component
Frontend components use wp.element (WordPress-bundled React) — no JSX, no build step.
-
Create the PHP class in
includes/frontend/class-{name}.php:- Register a shortcode via
add_shortcode()in anon_register_shortcode()method - Register a REST endpoint if the component needs async data (in
on_register_rest_route()) - Enqueue JS (
wp-elementdependency) and CSS viawp_enqueue_script/style() - Pass config to JS via
wp_localize_script()
- Register a shortcode via
-
Create JS in
assets/js/{name}.js:- Use
wp.element.createElement(aliased asel) for rendering - Import hooks:
var useState = wp.element.useState; - Read config from
window.{localizedObjectName} - Handle all states: loading, loaded, empty, error
- Mount into the shortcode's
<div id="...">mount point
- Use
-
Create CSS in
assets/css/{name}.css:- Use Peptide Starter theme CSS custom properties with fallbacks:
var(--color-text-primary, #f9fafb) - Prefix all classes with
prab-to avoid collisions - Include responsive breakpoints, focus-visible states, and loading skeletons
- Use Peptide Starter theme CSS custom properties with fallbacks:
-
Wire hooks in
class-prautoblogger.php:- Instantiate the PHP class in
register_frontend_hooks() - Register shortcode on
init, REST routes onrest_api_init
- Instantiate the PHP class in
-
Add
'frontend/'to the autoloader's$directoriesarray (already done for the first component) -
Update ARCHITECTURE.md file tree and add a data flow section
REST endpoint conventions:
- Namespace:
prautoblogger/v1 - Permission callback: use a filter hook for extensibility (e.g.,
prautoblogger_rest_{name}_public) - Sanitize all params with
sanitize_callbackin arg registration - Return only the fields the frontend needs — no unnecessary data
- Set
Cache-Controlheader for cacheable responses
Code Quality Rules
declare(strict_types=1);in every PHP file- Type declarations on all method parameters, return types, and class properties
- No file exceeds 300 lines. Split when approaching this limit
- Every class has a 3-question preamble docblock: What / Who triggers / Dependencies
- Every public method has a docblock with
@param,@return, and side effects @seereferences at the top of files that participate in multi-class flows- All strings translatable via
__()/_e()withprautobloggertext domain - No magic methods without explicit justification documented in code
- No
echoof raw data — always escape withesc_html(),esc_attr(), etc. - Nonce on every form/AJAX —
wp_nonce_field()/check_ajax_referer()
Git Workflow — PR-Gated, Soft-Enforced
This repo is private on GitHub's free plan, which does not support branch protection or rulesets. The review gate is enforced at the agent layer, not server-side. Every agent and human contributor follows these rules; the .github/workflows/main-push-audit.yml tripwire opens an audit issue on any direct push to main that did not come from a merged PR.
Rules
-
Never push to
maindirectly. Every change lands via a pull request that the maintainer merges. No self-merging. No force-pushing tomain. -
Branch naming:
claude/<scope>-<YYYYMMDD>for agent-authored work,fix/<scope>orfeat/<scope>for human-authored. The scope is a 1–3 word kebab-case description. -
Commit trailer: every commit authored by an agent must end with an
Agent-Session:trailer so commits can be correlated back to the conversation that produced them, even though all commits share thepeptiderepobot identity.feat: add per-request cost tracking Agent-Session: cowork-2026-04-14-cost-audit -
PR description template: every PR description covers
- What changed (one paragraph)
- Why (motivation or incident link)
- Risk flags (schema changes, API contract changes, cost impact, compatibility)
- Test plan (what was run locally, what to smoke-test after merge)
-
Emergency push exception: if a situation genuinely requires pushing to
mainwithout a PR (site down, CI broken, one-line hotfix), surface it in the chat before doing it. Every emergency push gets a follow-up PR that commits the same changes through the normal flow so git stays the source of truth. The tripwire will open an audit issue — close it with a comment explaining why.
Opening a PR from an agent session
The gh CLI is not installed in the Cowork sandbox. Use curl with the PAT from the workspace .env.credentials:
GH_TOKEN=$(grep "^GITHUB_PAT=" "$WORKSPACE/.env.credentials" | cut -d= -f2)
git push -u origin HEAD
curl -s -X POST -H "Authorization: token $GH_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/peptiderepo/<repo>/pulls" \
-d "$(jq -cn --arg title "<title>" --arg head "<branch>" --arg body "<body>" \
'{title:$title, head:$head, base:"main", body:$body}')"
When this changes
If the peptiderepo GitHub account is ever upgraded to Pro (or the repo goes public), replace this soft-enforcement section with a note pointing to the real branch protection rules in repo settings.