Closes the two M4 P2s: LIMIT 100 safety cap on the per-run stage-I/O query; implements the previously-stubbed get_page() null-wpdb test. QA APPROVE; CI green.
Agent-Session: pipeline-p2-cleanup-merge
Reworks the board into Direction C Mission Brief: status-grouped run list + persistent inspector reusing M4 Stage I/O; stage-list-driven; preserves New Article + polling + filters; column-limit setting restored. QA APPROVE 1034e19; CI green.
Agent-Session: pipeline-board-m5-merge
Relocates Content/AI Models/Sources fields into the per-step Pipeline panels; adds a Global Content Context block; retires the 3 duplicate Settings tabs. Option keys unchanged (no migration). QA APPROVE for 80d8e5f; CI green.
Agent-Session: pipeline-settings-m2-merge
Board submenu now registers at admin_menu priority 11 (parent toplevel at 10)
so the page hook lands under prautoblogger_page_prautoblogger-board; $submenu
reordered defensively to keep Board the first item / top-level click target.
Enqueue gate now matches; board.css/board.js load for the first time on prod.
Regression test: BoardMenuRegistrationTest (fails on pre-fix main).
QA: APPROVE-W-P2 qa-reviews/prautoblogger/2026-06-12-6168520.md
Thread: convo/prautoblogger/threads/2026-06-pipeline-v2-phase2-admin/ seq 11-14
Agent-Session: cto-board-404-hotfix-merge-20260612
Runware retired taskType:models; all 42 daily catalog 401s come from this
deprecated endpoint. Migrated to auth+modelSearch pair per Runware's
current API. Paginator fetches up to 5 pages x 100 = 500 models, filters
client-side to io:text-to-image in capabilities[]. Uses air identifier as
model id. Cache key bumped to v2 (prautoblogger_runware_model_cache_v2)
to invalidate v1 rows that used old taskType/requiresImage shape.
Failure cooldown: sync() records prautoblogger_runware_catalog_last_failure_at
on every error path. get_models() gates re-trigger via is_in_failure_cooldown()
which reads prautoblogger_runware_catalog_failure_cooldown_seconds (default
3600s). Success clears the timestamp. Caps cascade to ~24 errors/day.
300-line rule: fetching/normalization extracted to new companion class
PRAutoBlogger_Runware_Catalog_Fetcher (class-runware-catalog-fetcher.php,
205 lines). Catalog class: 261 lines. Autoloader picks up the new file.
Tests: normalize_models against real modelSearch fixture (redacted key);
cooldown behavior (failure->active->expired); sync clears timestamp; fallback
list always served when cache empty. 328/328 PHPUnit green.
Agent-Session: prab-v0183-20260612
R1: move phpcs:ignore out of SQL string in class-analysis-prompts.php:81
(same pattern as v0.18.2 cost-reporter fix). GA4 self-improvement loop
has returned empty context on every pipeline run since v0.16.0 — this
restores get_performance_context() to the LLM. Sibling occurrence also
fixed in class-post-assembler.php:112.
R2(a): update_generation_stage() now renews STATUS_TTL on every stage call
so the transient outlasts long LLM calls during background runs.
R2(b): on_ajax_generation_status() detects lock-held-but-transient-gone
when lock age > STATUS_TTL (= Hostinger background-process kill).
Marks stuck run failed, releases lock, returns status:error with
'infrastructure timeout' message. Daily reaper remains the backstop.
R3: when transient absent but lock held within TTL, return status:running
(with started_at from lock) instead of status:idle.
Executor split: class-executor.php was 351 lines (>300). AJAX generation
handlers extracted to new class-generation-status-poller.php (260 lines);
executor now 226 lines. Public hook wiring unchanged via delegation.
Generation_Lock gains get_acquired_at() for R2b/R3 lock-age checks.
Tests: AnalysisPromptsTest (regression SQL + behavioral context shaping),
GenerationStatusPollerTest (R2a TTL renewal, R2b abort, R3 running,
idle path).
Closes: R1 (analysis-prompts SQL), R2 (manual-run resilience), R3 (honest status)
Version: 0.18.3
Agent-Session: prab-v0183-20260612
QA REJECT-P1 remediation (seq 29). start()/done()/is_done()/get_output()
were asymmetric: start() passed the mapped role but done()/is_done()/
get_output() passed '' via the default, addressing a different unique key
(run_id, stage, agent_role, item_key). On every normal run this orphaned
the start() row in 'running'; on resume is_done() always returned false
→ duplicate-post vector.
Fix: add resolve_role(stage, agent_role) private static seam in
Run_Stage_State. When the caller passes '' the role is derived from
Stage_Display_Map::default_agent_role(stage) before any DB operation.
Non-empty roles (Phase-2 fan-out) pass through unchanged. All four
write/lookup methods (start, done, fail, get) call resolve_role.
is_done() and get_output() go through get() so they benefit too.
Mid-run-upgrade compat (v0.18.1 → v0.18.2): get() falls back to a
legacy role='' row on miss so a run started under v0.18.1 (all rows
have agent_role='') can still resume under v0.18.2. One extra indexed
query, only on miss, only for known stages.
Tests: two new RunStageStateTest cases —
test_role_tagged_start_and_done_are_symmetric: start+done with
'editor', assert is_done('editor')=true and is_done('')=false.
test_legacy_empty_role_row_is_found_on_fallback: resolve to 'editor',
primary miss, legacy role='' hit → is_done=true.
ARCHITECTURE.md §22: agent_role column and #22 Phase 1 description
updated — no longer 'Phase-2 only / '' default'; now documents Phase 1
population from Stage_Display_Map.
Agent-Session: prab-v0182-maintenance-20260612
1. Cost-reporter SQL: phpcs:ignore annotations were embedded inside two SQL
string literals (get_daily_spend, get_spend_by_stage), causing MariaDB to
reject both queries and break the Metrics & Costs admin page. Moved annotations
above the prepare() calls. Regression tests added to CostReporterTest.
2. Collector failure observability: catch block in collect_from_all_sources() now
logs structured meta (model slug, HTTP status parsed from exception message,
first 200 chars of error body) so outage incidents are diagnosable post-hoc.
3. run_stages.agent_role back-write: Pipeline_Runner, Article_Worker, and
Content_Generator were calling Run_Stage_State::start() with agent_role=''.
All start() call sites now pass Stage_Display_Map::default_agent_role(stage).
Role written at INSERT only; ON DUPLICATE KEY UPDATE does not include
agent_role — idempotency-key semantics on resume are unchanged.
4. Runware float deprecation: normalize_seed() applied % to a float
(microtime(true) * 1000), triggering PHP 8 deprecation on every image
generation. Fixed with (int) round(...) before the modulo operator.
Refs: convo/prautoblogger/threads/2026-06-pipeline-v2-phase1/25-cto-v0182-maintenance.md
Agent-Session: prab-v0182-maintenance-20260612
QA F4 (P3): prautoblogger_reasoning_max_tokens was missing from the
sanitize_field() numeric allowlist, so it saved as raw text (absint at
read kept runtime safe). Saves as numeric-or-0 now, matching every other
number field. CHANGELOG 0.18.1 entry notes the server-side sanitize (no
new version — 0.18.1 is still unreleased).
Agent-Session: prab-empty-draft-guard-20260611
Reasoning models at high effort could spend the entire max_tokens budget
on thinking and return zero visible content with finish_reason=length;
the empty article flowed writer -> editor (reject) -> empty draft post,
with the cost booked as success (2026-06-11 incident: prod run acf24029,
draft post 921; diagnosis: convo pipeline-v2-phase1 seq 12).
- Empty-completion guard at the provider seam (new
OpenRouter_Completion_Guard): empty/whitespace content is a failure;
one retry with reasoning disabled (per-call override) when
finish_reason=length or reasoning was active, then the stage fails.
- finish_reason=length always warning-logged (model, stage, tokens).
- Failed/retried attempts booked to generation_log with
response_status=error + real token burn; run_decisions verdict rows
(retry_reasoning_disabled / failed_empty_completion) for v0.18.0 runs.
- Writer-path guard in Content_Generator::execute_stage and
belt-and-braces empty check in Publisher::create_post.
- Reasoning budget sanity: reasoning.max_tokens cap + completion
headroom via new prautoblogger_reasoning_max_tokens setting (default
constant 2048, 0 = legacy uncapped effort mode); cap replaces effort
when active; governor reservations use the effective ceiling.
- Body assembly moved to Request_Builder::build_body (300-line cap).
- Tests: guard verdict matrix, builder budget shaping, writer/publisher
guards, full provider retry path.
Agent-Session: prab-empty-draft-guard-20260611
OpenRouter sometimes returns a dated snapshot ID as the response model
(request deepseek/deepseek-v4-flash -> response model
deepseek/deepseek-v4-flash-20260423). The pricing chain missed the
registry on those IDs and booked the conservative $10/$30 per-M
estimate: today's prod single-pass run logged $0.582 against a true
flash-tier cost of roughly 1-2 cents. Inflated actuals eat the
Cost_Governor per-run ceiling (default $0.50; today's run would have
halted) and the monthly $5 budget gate (~9 days to self-block).
Fix at the single pricing seam (OpenRouter_Pricing::estimate_cost):
on a lookup miss, strip a trailing 8-digit 20-prefixed -YYYYMMDD
snapshot suffix and retry the hardcoded table + cached models list
once with the base ID. The conservative fallback is unchanged when
the base ID is also unknown. Governor reserve/settle and
generation-log booking all price through this seam, so every path
survives snapshot IDs.
Tests: snapshot ID resolves to the base price when only the base ID
is registered; an unknown ID without a suffix still gets the
conservative estimate; a non-date trailing digit run (foo-202612345)
is never stripped.
Agent-Session: pipeline-v2-phase1-pricing-fix-20260611
A cron-triggered generation can run right after a deploy, before any
admin_init migration pass — inserts carrying the new agent_role /
prompt_version columns would fail silently on the old schema and lose
the cost row. Two-sided heal: (1) log_api_call/log_image_generation
retry the insert without the v0.18.0 columns when it fails; (2) the
db-version check now also runs on cron requests (init + wp_doing_cron),
so the schema migrates on the first cron hit, not the first admin visit.
Agent-Session: pipeline-v2-phase1-engineer-20260612
Pipeline v2 Phase 1, commit 4/4. Run_Stage_State persists per-run
per-stage state keyed run_id + stage + agent_role + item_key (item_key
scopes articles because one run_id spans a whole batch; agent_role is
the Phase-2 quorum dimension, schema-only). Done stages snapshot output
and are reused on re-entry — writer stages via
Content_Generator::set_run_item, the review via its stored verdict, a
published item skipped outright — never re-run, never re-charged. Post
creation is keyed by _prautoblogger_run_id + new _prautoblogger_idea_hash
(check-before-insert in Publisher) so retries cannot duplicate posts.
Run-level research/analysis checkpoints + done/failed lifecycle marks
ride Pipeline_Runner/Executor (incl. orphan-abort + budget-abort paths).
Run_Reaper rides the EXISTING #19 daily cron: stages stuck running >2x
expected stage wall-clock and runs silent >2x expected run wall-clock ->
failed (filterable), open stages reaped, queryable as 'incomplete'.
Retention: request_json + stage output payloads NULLed after R days, R
from the new Audit Payload Retention setting (default constant 14,
0 = keep forever, no literal at point of use). Audit_Writer records
editorial verdicts (run_decisions) and per-article kept sources
(run_sources) for new runs.
Under-300: status-transient/summary helpers extracted verbatim to
Pipeline_Status (Pipeline_Runner 271; removed the worker's duplicate).
Agent-Session: pipeline-v2-phase1-engineer-20260612
Pipeline v2 Phase 1, commit 3/4. Cost_Governor reserves each call's
worst-case estimate (prompt chars/4 + max_tokens via the #18 pricing
chain) against the run ledger row with one atomic conditional UPDATE
(the #10 lock discipline) BEFORE dispatch, then settles to actuals after
each response; failed calls release their hold. Integrated inside
OpenRouter_Provider::send_chat_completion so EVERY text-LLM call path is
guarded with zero call-site changes; the image curl_multi batch reserves
its summed estimate in Image_Pipeline before generate_image_batch.
Breach: run halted (sticky), overage recorded, Cost_Ceiling_Exception
aborts the un-dispatched call, process_next_queued_article aborts the
remaining queued articles of halted/failed runs, and the Review Queue
shows a 'Halted runs' notice (runs halted pre-post have no draft to
list). Ceiling = prautoblogger_per_run_cost_ceiling_usd setting (default
constant $0.50, snapshotted at run start, 0 = disabled). Standalone
no-run calls, the monthly cap, and the AI Gateway path are unchanged.
Under-300: provider API-key fetch+validation moved verbatim to
OpenRouter_Request_Builder::resolve_api_key().
Agent-Session: pipeline-v2-phase1-engineer-20260612
Pipeline v2 Phase 1, commit 2/4. Prompt_Registry (read) +
Prompt_Registry_Writer (immutable create/activate/seed) over the new
prompts table; canonical v1 bodies extracted verbatim into
Prompt_Defaults(+Editorial) — single source for BOTH the seed migration
(Migrate_Prompt_Seed_V0180, flag-gated, self-healing) and the in-code
fallback, so a missing table renders byte-identical prompts and never
fatals. Call sites swapped: Content_Prompts, Analysis_Prompts,
Chief_Editor, LLM_Research_Provider. Includes the image-composer seam
keys image.rewriter_system + image.style_template (composer PR consumes,
must not add its own prompt storage).
Pinning: Cost_Tracker::set_run_id() registers the process run context,
ensures the runs ledger row, and pins active versions once per run;
log_api_call/log_image_generation stamp prompt_version (run pins) +
agent_role (stage map) on every generation_log row. Single-pass keeps
stage 'draft' and stamps content.single_pass explicitly.
Under-300 refactors: Content_Generator's four duplicated span/log blocks
consolidated into execute_stage() (301 -> 224 lines, behavior
identical); get_avg_tokens_for_stages body moved to Cost_Reporter with a
delegating wrapper kept for API compatibility.
Tests: byte-identical fallback renders (single_pass/outline/polish/
editor.system), token filling, key coverage incl. stage-map integrity.
Agent-Session: pipeline-v2-phase1-engineer-20260612
* Commit 1: editorial image prompt + style-template setting
Pivot the image pipeline from the single-panel comic style to a text-free
editorial scientific illustration, and replace the Style Suffix setting with
an editable Style Template setting. Commit 1 of the in-plugin editorial image
pipeline (no composer yet; pipeline otherwise unchanged).
- New constant PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE (brief §7 editorial
template with the single {{ topic_summary }} token).
- New setting prautoblogger_image_style_template (textarea) replaces the
Style Suffix field; single-token validation on save (brief A5); old
prautoblogger_image_style_suffix mirrored to _deprecated for one cycle and
not deleted (one-time migration prautoblogger_migrated_style_template_v0160).
- REWRITER_SYSTEM_PROMPT now emits a 1-2 sentence editorial topic/mechanism
summary as the SCENE (no comic gag), keeping the CAPTION line for the
unchanged HTML-caption-below-image path; scene/caption parsing contract
preserved.
- New PRAutoBlogger_Image_Template_Filler substitutes the scene into the
template's {{ topic_summary }} slot across all three entry points; strips
control chars + clamps length (A5); degrades safely on missing token or
empty summary so a blank/token-only prompt is never sent to the provider
(A5/A6).
- Version bump 0.15.1 -> 0.16.0 + CHANGELOG + ARCHITECTURE.md updates.
Agent-Session: image-editorial-commit1
* style: auto-fix PHPCS violations via phpcbf
---------
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Orphaned array entry for runware_catalog_sync field was placed after the
closing ); of the fields array instead of inside it. PHP parse error on
line 268. Moved entry to correct position inside the array.
Replace hardcoded model registry with live-sync system.
New PRAutoBlogger_Runware_Model_Catalog class fetches the Runware model
catalog daily, caches in WP options (24h TTL), and falls back to the
hardcoded 15-model list if the API is unavailable. Sync Now button
in PRAdmin for on-demand refresh. Eliminates the drift that caused
silent model fallback bugs (v0.13.8, v0.14.0).
All Runware model selections except schnell and dev were silently falling
back to runware:100@1 because COST_PER_IMAGE only had 2 entries while the
image model registry was expanded to 15 models in v0.13.8.
Fix: add all 15 model IDs to COST_PER_IMAGE and DEFAULT_STEPS.
35% title + 255px fixed PRAB cols left ~50px per auto column.
Replace with a balanced budget across all columns so Author,
Categories, Tags, and Date all get enough room to render horizontally:
title 22%, author 6%, categories 7%, tags 9%, date 8%,
writing_model/image_model 85px each, cost 50px, peptide_topics 75px
table-layout: fixed ignores min-width — the WP posts list table uses
fixed layout so min-width: 80px had no effect. Changed to width: 80px
which is honoured under fixed-layout tables.
Two bugs in class-post-list-columns.php:
1. Cost always showed $0.00: a PHP '// phpcs:ignore' comment was
placed inside the SQL string literal, causing MariaDB syntax error
on every render_cost_cell() query. Moved comment to the line above
wpdb->prepare() where it belongs.
2. Peptide Topics column header compressed to near-zero width by the
three fixed-width PRAB columns (Writing Model + Image Model + Cost =
255px). Added min-width: 80px to .column-taxonomy-peptide_topic so
the header renders horizontally.
class-autoloader.php listed services/ but not services/opik/. All four Opik classes live in that subdirectory. Composer classmap covered it (tests passed); WP runtime autoloader would fatal in production on first Opik class reference. Adds services/opik/ to the scan list. Bumps to v0.12.3.
Opik requires version 7 UUIDs. wp_generate_uuid4() was producing v4
which the API rejected with 422. Replaced both call sites in
class-opik-trace-context.php with generate_uuid7(), a private static
method implementing RFC 9562 §5.7.
Fixes: smoke test 422 {"errors":["Trace id must be a version 7 UUID"]}
File-level sniffs like WordPress.Files.FileName are evaluated at the file level,
not at code level. They require phpcs:ignore directives to be placed within the
file-level docblock (/** ... */), not in code comments.
Moved all 81 phpcs:ignore pragmas from code comments to the file-level docblocks.
This applies to:
- 79 class files with class-naming convention ignores
- prautoblogger.php with strict-types docblock ignore
- uninstall.php with strict-types docblock ignore
- Added phpcs:ignore for class file naming convention (79 violations)
WordPress.Files.FileName.InvalidClassFileName is a known architectural
issue — class files use short names without prautoblogger- prefix
- Added phpcs:ignore for ->prepare() table interpolation (56 violations)
WordPress.DB.PreparedSQL.InterpolatedNotPrepared: dynamic table names
(prab_generation_logs, prab_cost_logs) require interpolation; ->prepare()
does not support table name placeholders, only column values
- Fixed file-level docblock positioning (2 violations)
prautoblogger.php and uninstall.php: moved declare(strict_types=1) before
file docblock per PHP best practices