1. The blast radius is capped before any code is written
Born from: run 1, where a spec proposed a change I would never have approved.
The scoper's only output is a build spec, and the spec is invalid unless the work is
purely additive. “Additive” is defined by files, not by line count — a new
integration directory, new JS components, generated assets are all fine; touching a shared
base class is not. If the feature genuinely cannot be built additively, the agent is
required to stop rather than write a smaller-sounding spec.
# .claude/agents/wpf-feature-scoper.md
blast_radius MUST be `additive`. Additive means NO edits to
includes/crms/class-base.php, includes/integrations/class-base.php, any
shared base class, or the singleton bootstrap beyond a single registration
line — it is NOT a file count. […] If the only viable implementation
breaks this, output `ESCALATE: <reason>` and STOP. Do not emit a spec.
Core Feature Enhancements (e.g. "Expiring tags") usually escalate —
that is correct, not a failure.
That last line is doing real work. Without it, an agent measured on completion
rate learns to squeeze core changes into an additive-shaped spec.
2. No claim of absence without a citation
Born from: run 1, three separate confidently-wrong “this hook never fires” claims.
The calibration run for this pipeline was to point the brand-new scoper at an
integration I had already built by hand, blind, and diff its spec against my real
implementation. It got five things wrong. Two were ordinary misreadings. The other three
were all the same species: the agent asserted something did not exist, having looked
somewhat but not systematically. So now every negative claim has to carry its receipt
— the search pattern, the scope searched, and the number of hits. “I found no
renewal hook” is inadmissible. “grep -rn 'renewal' includes/ → 0 hits”
is admissible. That one rule removed the entire class.
3. Verification drives the real product, never the hooks directly
Born from: run 1, a hand-fired hook that passed while the real code path was broken.
This is the interlock I would most encourage your team to copy, because it is where
almost all fake green comes from. It is trivially easy for an agent to “test” an
integration by calling do_action() with hand-rolled arguments. That proves the
listener runs. It proves nothing about whether the host plugin ever calls it, with those
arguments, in that order.
# .claude/agents/wpf-integration-verifier.md
Drive the host plugin's real machinery — its own status setters,
processors, renewal/payment paths. NEVER fire the host's hooks or WP
Fusion's integration hooks manually: hand-rolled do_action() calls fake
the argument shapes and ordering where real bugs live.
Every test-plan item ends OK-with-evidence or BLOCKED-with-reason.
Evidence is captured output: a tags dump, log rows, a screenshot path, a
response body. No partial pass — "3 of 4" is not a pass. Ambiguous
output is a failure with the raw capture attached; don't interpret it away.
“Don't interpret it away” is the most valuable four words in our
entire agent corpus. Ambiguity resolves to failure, never to success.
4. A green test run that ran no tests is a failure
Born from: run 3, a suite that exited 0 having executed nothing.
PHPUnit will happily exit successfully when a configuration change means it collected
zero tests. An agent reading only the exit code reports a pass, truthfully and uselessly.
So we stopped reading the exit code alone.
# tests/run-phpunit.sh
if ! grep -Eq 'OK \([1-9][0-9]* tests?|Tests: [1-9][0-9]*' "$OUTPUT_FILE"; then
echo "PHPUnit exited successfully but did not execute any tests." >&2
exit 1
fi
Four lines. Closes an entire category of false confidence.
5. The test harness physically cannot reach production
Born from: run 1, where composer test turned out to be destructive.
Our PHPUnit config points at the live local development database — there is no
separate test database. We discovered this the hard way when a tearDown()
blanked a setting and silently disconnected the dev site's CRM connection. The fix was two
rules (capture in setup, restore in teardown, never blank) and one interlock: the runtime
harness boots WordPress and then checks where it landed before doing anything.
// tests/runtime/wp-run.php
$_SERVER['HTTP_HOST'] = 'dev.local';
require dirname( __DIR__, 5 ) . '/wp-load.php';
if ( 'https://dev.local' !== untrailingslashit( home_url() ) ) {
fwrite( STDERR, "wp-run.php: refusing to run outside https://dev.local.\n" );
exit( 1 );
}
I cannot type a production hostname into this by mistake at midnight. It
simply exits.
6. Mutations are bracketed, and a round cannot start dirty
Born from: the same incident — state left behind between runs.
Verification necessarily changes things: it flips settings, creates users, writes tags.
Every option the harness touches is backed up first and restored at the end of the round.
More importantly, a new round refuses to begin if a previous round left backups
unrestored, rather than starting on top of unknown state.
// tests/runtime/lib.php
function wpf_rt_begin_round( $round_id = '' ) {
if ( wpf_rt_has_pending_option_backups() ) {
return new WP_Error(
'wpf_rt_pending_option_backups',
'Runtime verification has pending option backups. Run
tests/runtime/run.sh --restore-options before starting a new round.'
);
}
// ... fresh round state
}
Fails closed. The interesting design decision is that it refuses rather than
auto-cleaning — auto-cleaning would hide the fact that the last round died badly.
7. Credentials are stripped from evidence mechanically
Born from: run 1, a bearer token that appeared in a log dump.
The verifier's evidence includes raw rows from our logging table, which contain API
traffic. During the first run an Authorization header made it into a report.
Nothing bad came of it, but “the agent was careful” is not a security control, so
redaction became a function that every log dump passes through.
// tests/runtime/lib.php
function wpf_rt_redact_sensitive_log_data( $message ) {
$message = preg_replace(
'/\b(Authorization)\b(\s*[:=>]\s*)[^\r\n<]+/i',
'$1$2[REDACTED]', $message );
$message = preg_replace(
'/\b(Basic|Bearer|OAuth|Token)\s+[A-Za-z0-9+\/=._~:-]+/i',
'$1 [REDACTED]', $message );
// ... further passes strip cookies, access_token, api_key,
// client_secret, password and friends from query strings and JSON
return $message;
}
This is the concrete answer to “is it more secure than you?”. I redact
when I remember to. The function redacts every time.
8. A different vendor's model reviews the work
Standing policy, and the part I would least want to give up.
Once the branch is pushed, control passes to a review loop that requires an
OpenAI Codex review on the current head of the PR. Claude wrote the code;
Codex looks for what is wrong with it. Findings must be fixed or answered on the thread with
a concrete reason — and crucially, a fixed thread is not sufficient on its own: Codex
must have reviewed the current head, CI must pass, and the PR must be non-draft and
conflict-free before the gate opens.
Two models from two vendors with different training data disagree in useful places. A
model reviewing its own output mostly agrees with itself. This is also, in practice, where
the genuine bugs get caught — run 3's most instructive defect was found here.
9. Nothing merges, releases, or publishes itself
Design constraint from day one.
The orchestrator ends with a request for review from a named human, and then a single
instruction with no exceptions clause:
# .claude/skills/wpf-feature-pipeline/SKILL.md
Never merge. Never mark a request `done`; those are operator-only actions.
The docs agent has the same shape — it can create drafts and propose diffs against
live pages, but status: publish is forbidden outright. Both gates cost me a few
minutes per run and have repeatedly been worth it.