(Originally published March 2nd, 2025. Updated March 14th, see end of post for the latest)
I’ve been experimenting with new AI tools pretty heavily over the last two years (see my experiments with custom agents or AI-assisted data analytics for more info).
As of this writing (end of February 2025) we’ve also heavily integrated AI tools into our development workflows at WP Fusion and EchoDash.
I would estimate now that AI generates about 95% of my code output.
Despite this, I don’t think software developers are in danger of losing their jobs, the nature of the work has just changed.
You need to understand how to configure the tools, review their output, and ask the right questions in order to get the results you want.
So, while AI is ultimately writing the code, I still spend most of my day in conversation with various bots and agents, updating docs and resources so they have accurate training materials, testing new tools and workflows, etc.
A lot of people are asking me how we use AI in development and how they can get started too, so I’m publishing this guide. Keep in mind that this stuff is changing really quickly— our current workflow was only implemented a few days ago. So you can assume it will be out of date again in a week or two 😅. At the end of the post I’ll share some resources I’ve been following to keep up on the latest changes.
I thought it might be fun to do a little history tour of how my use of AI and LLMS has evolved over the past year, to highlight the improvement in output you get from fine-tuning the system.
Test case
Let’s use an example feature request for WP Fusion. WP Fusion supports syncing Paid Memberships Pro membership data with custom fields in your CRM or email marketing platform.
This lets you automate email sequences and other marketing interactions— for example, to send a message before someone’s upcoming plan renewal, or to send a survey after a subscription cancellation.
At the moment, mapping custom fields with your CRM is configured globally, in the WP Fusion settings.

The 3.0 release of Paid Memberships Pro added the ability to have multiple active memberships. In the context of WP Fusion, this means that if a user signed up for a second membership plan, the details of the first membership plan would be overwritten in the CRM.
This customer wants the ability to configure individual membership products with their own field mappings, so for example, Membership A Expiration Date would sync to Expiration Date A and Membership B Expiration Date would sync to Expiration Date B.
With this change, you could run parallel automation sequences for members with multiple memberships, with each email personalized and timed to the purchased product. Let’s dig in.
Approach 1: ChatGPT
Everyone knows ChatGPT. It was the first tool I started using for software development, and thanks to WP Fusion’s extensive documentation, it knows our software pretty well. ChatGPT was the first tool I used to assist in software development.
I use a basic custom instruction set for coding to try and ensure the output confirms to the WordPress code and documentation standards.

For the traits, I use:
When you respond with a PHP code snippet, please make sure to use the WordPress PHP Coding Standards and PHP Documentation standards. Text should be translatable via __() using the textdomain wp-fusion, and all output should be sanitized and escaped following WordPress security standards.
Let’s try a prompt with the ChatGPT 4o model:
Our WP Fusion integration with Paid Memberships Pro (https://wpfusion.com/documentation/membership/paid-memberships-pro/ ) currently allows syncing fields like Membership Status and Expiration Date with the customer’s CRM. This is currently configured sitewide, which means it’s not compatible with Paid Memberships Pro’s new feature that allows members to have multiple concurrent memberships. We’d like to extend the integration to support adding a new settings panel to the PMPro membership configuration page where users can map fields with their CRM specific to that membership level. When a member signs up at that level, the fields should be synced to their contact record in your CRM. You can find the Paid Memberships Pro plugin here: https://github.com/strangerstudios/paid-memberships-pro . I have uploaded WP Fusion’s existing PMPro integration class. Can you please propose a solution, or let me know if you require more information or code examples before proceeding?
(I uploaded class-pmpro.php
to the chat context so ChatGPT can understand our existing code structure).
I’ll include screenshots of the most relevant parts of the output as much as possible, in case the links don’t last forever.
Here’s the resulting code:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WPF_PMPro {
public function __construct() {
// Hooks for sitewide sync
add_action( 'pmpro_after_change_membership_level', [ $this, 'sync_membership_fields' ], 10, 2 );
// Hooks for membership level settings UI
add_action( 'pmpro_membership_level_after_other_settings', [ $this, 'add_level_settings' ] );
add_action( 'pmpro_save_membership_level', [ $this, 'save_level_settings' ] );
}
/**
* Add CRM field mapping settings to the membership level settings page.
*/
public function add_level_settings( $level ) {
$settings = get_option( "wpf_pmpro_level_{$level->id}_settings", [] );
?>
<h3><?php esc_html_e( 'WP Fusion Field Mapping', 'wp-fusion' ); ?></h3>
<table class="form-table">
<tr>
<th scope="row"><label for="wpf_crm_fields"> <?php esc_html_e( 'CRM Field Mappings', 'wp-fusion' ); ?></label></th>
<td>
<textarea name="wpf_crm_fields" id="wpf_crm_fields" class="large-text" rows="5"><?php echo esc_textarea( json_encode( $settings ) ); ?></textarea>
<p class="description"> <?php esc_html_e( 'Enter JSON key-value pairs for CRM field mappings.', 'wp-fusion' ); ?></p>
</td>
</tr>
</table>
<?php
}
/**
* Save the membership-level-specific CRM field mappings.
*/
public function save_level_settings( $level_id ) {
if ( isset( $_POST['wpf_crm_fields'] ) ) {
$settings = json_decode( stripslashes( $_POST['wpf_crm_fields'] ), true );
update_option( "wpf_pmpro_level_{$level_id}_settings", $settings );
}
}
/**
* Sync membership fields when a member joins or leaves a level.
*/
public function sync_membership_fields( $level_id, $user_id ) {
$settings = get_option( "wpf_pmpro_level_{$level_id}_settings", [] );
if ( empty( $settings ) ) {
return; // No level-specific settings, exit early.
}
// Prepare data for syncing
$contact_data = [];
foreach ( $settings as $crm_field => $wp_field ) {
$contact_data[ $crm_field ] = get_user_meta( $user_id, $wp_field, true );
}
// Send data to WP Fusion CRM integration
if ( function_exists( 'wpf_update_contact' ) ) {
wpf_update_contact( $user_id, $contact_data );
}
}
}
new WPF_PMPro();
It used the correct class name and hooks for Paid Memberships Pro ( pmpro_membership_level_after_other_settings
, etc), so this code is perfectly workable.
However there are a few problems:
- It’s created a textarea for writing the field mapping as a JSON object. WP Fusion includes functions that render a visual CRM field dropdown, but it hasn’t picked up on that.
wpf_update_contact()
is not a real function, and wouldn’t apply in this case anyway, since the member is a registered user. The correct method would bewp_fusion()->user->push_user_meta( $user_id, $contact_data )
(see doc)- The documentation blocks don’t conform to the WordPress documentation standards, despite my asking for this (they’re missing the
@since
and@param
tags). - The values for the PMPro membership data aren’t stored in usermeta, they’re accessed off of the
$membership_level
object. This is how we do it already in the class I uploaded to ChatGPT, but it failed to read or understand it:
$update_data = array(
'pmpro_status' => $this->get_membership_level_status( $user_id, $membership_level->id ),
'pmpro_start_date' => gmdate( get_option( 'date_format' ), intval( $membership_level->startdate ) ),
'pmpro_next_payment_date' => gmdate( get_option( 'date_format' ), intval( $nextdate ) ),
'pmpro_membership_level' => $membership_level->name,
'pmpro_subscription_price' => $membership_level->billing_amount,
);
In summary:
It’s useful to get quick access to the hooks and add the settings panel, but ultimately the code doesn’t follow the conventions of the rest of the class, and only about 10% of it is usable.
Approach 2: Anthropic
Around September of 2024 I started using Claude.ai for coding and it improved things considerably. I was using the Claude 3.5 Sonnet.
(In this test I am using the free plan, since I let my Pro account expire, but the Pro account generally works better since you can create “Projects” with multiple files uploaded, that persist between conversations, rather than uploading the files one at a time into each session.)
Let’s use the same prompt and see what we get.
Anthropic response: https://claude.ai/share/92837c16-1db2-4358-9fb4-0ccafc9397dc
You can see the class-pmpro.php
file was too big to upload, so I had to explain the relevant parts of the integration to Claude. Here’s the output code:
/**
* Add per-level CRM field mappings to the membership level settings
*
* @since 3.40.0 (or your next version number)
*
* @param int $level_id The membership level ID
*/
public function add_level_settings( $level_id ) {
// Continue showing existing settings and global message
echo '<tr class="wpf-settings-row">
<th scope="row" valign="top"><label>' . __( 'WP Fusion Settings', 'wp-fusion' ) . '</label></th>
<td><p class="description">' . sprintf( __( 'For more information on these settings, %1$ssee our documentation%2$s.', 'wp-fusion' ), '<a href="https://wpfusion.com/documentation/membership/paid-memberships-pro/" target="_blank">', '</a>' ) . '</p></td>
</tr>';
// Add new section for per-level CRM field mappings
echo '<tr class="wpf-settings-row">
<th scope="row" valign="top"><label>' . __( 'Field Mapping', 'wp-fusion' ) . '</label></th>
<td><p class="description">' . __( 'These fields will be synced to the CRM when a member registers for this specific level.', 'wp-fusion' ) . '</p>';
$level_fields = $this->get_membership_level_fields();
$saved_mappings = get_option( 'wpf_pmpro_level_' . $level_id . '_fields', array() );
echo '<table class="form-table wpf-level-fields-table">';
foreach ( $level_fields as $key => $field ) {
$field_id = $key . '_level_' . $level_id;
$saved_value = isset( $saved_mappings[$key] ) ? $saved_mappings[$key] : false;
echo '<tr>
<th scope="row"><label for="' . esc_attr( $field_id ) . '">' . esc_html( $field['name'] ) . '</label></th>
<td>';
wpf_render_crm_field_select(
$saved_value,
'wpf_pmpro_level_fields',
$field_id
);
echo '</td>
</tr>';
}
echo '</table></td>
</tr>';
}
/**
* Get available membership level fields for mapping
*
* @since 3.40.0
*
* @return array The available fields
*/
public function get_membership_level_fields() {
return array(
'pmpro_status' => array(
'name' => __( 'Membership Status', 'wp-fusion' ),
),
'pmpro_start_date' => array(
'name' => __( 'Start Date', 'wp-fusion' ),
),
'pmpro_expiration_date' => array(
'name' => __( 'Expiration Date', 'wp-fusion' ),
),
'pmpro_next_payment_date' => array(
'name' => __( 'Next Payment Date', 'wp-fusion' ),
),
'pmpro_membership_level' => array(
'name' => __( 'Membership Level Name', 'wp-fusion' ),
),
'pmpro_subscription_price' => array(
'name' => __( 'Subscription Price', 'wp-fusion' ),
),
'pmpro_approval' => array(
'name' => __( 'Approval Status', 'wp-fusion' ),
),
);
}
/**
* Save the membership level field mappings
*
* @since 3.40.0
*
* @param int $level_id The membership level ID
*/
public function save_level_settings( $level_id ) {
// Save the field mappings
if ( isset( $_POST['wpf_pmpro_level_fields'] ) ) {
$field_mappings = array();
foreach ( $_POST['wpf_pmpro_level_fields'] as $field_key => $crm_field ) {
// Extract the field name from the combined key (field_level_id)
$parts = explode('_level_', $field_key);
if ( count($parts) == 2 && $parts[1] == $level_id ) {
if ( ! empty( $crm_field ) ) {
$field_mappings[$parts[0]] = $crm_field;
}
}
}
update_option( 'wpf_pmpro_level_' . $level_id . '_fields', $field_mappings );
}
}
/**
* Modify the membership level sync function to support per-level field mappings
*
* @since 3.40.0
*
* @param int $user_id The user ID
* @param object $membership_level The membership level object
*/
public function sync_membership_level_fields( $user_id, $membership_level ) {
if ( ! empty( $membership_level ) ) {
if ( ! isset( $membership_level->startdate ) ) {
$membership_level->startdate = time(); // sometimes this isn't set yet during signup.
} elseif ( ! is_numeric( $membership_level->startdate ) ) {
$membership_level->startdate = strtotime( $membership_level->startdate );
}
$nextdate = strtotime( '+' . $membership_level->cycle_number . ' ' . $membership_level->cycle_period, intval( $membership_level->startdate ) );
// Prepare all the available data
$all_data = array(
'pmpro_status' => $this->get_membership_level_status( $user_id, $membership_level->id ),
'pmpro_start_date' => gmdate( get_option( 'date_format' ), intval( $membership_level->startdate ) ),
'pmpro_next_payment_date' => gmdate( get_option( 'date_format' ), intval( $nextdate ) ),
'pmpro_membership_level' => $membership_level->name,
'pmpro_subscription_price' => $membership_level->billing_amount,
);
if ( ! empty( $membership_level->enddate ) ) {
if ( ! is_numeric( $membership_level->enddate ) ) {
$membership_level->enddate = strtotime( $membership_level->enddate );
}
$all_data['pmpro_expiration_date'] = gmdate( get_option( 'date_format' ), intval( $membership_level->enddate ) );
} else {
// Never expires.
$all_data['pmpro_expiration_date'] = null;
}
// Approvals
$approval_status = get_user_meta( $user_id, 'pmpro_approval_' . $membership_level->id, true );
if ( ! empty( $approval_status ) ) {
$all_data['pmpro_approval'] = $approval_status['status'];
}
// Get per-level field mappings
$level_mappings = get_option( 'wpf_pmpro_level_' . $membership_level->id . '_fields', array() );
// Get global field mappings
$fields = wp_fusion()->settings->get( 'contact_fields' );
$update_data = array();
// First check level-specific mappings
if ( ! empty( $level_mappings ) ) {
foreach ( $level_mappings as $field_key => $crm_field ) {
if ( isset( $all_data[$field_key] ) && ! empty( $crm_field ) ) {
$update_data[$field_key] = $all_data[$field_key];
}
}
}
// Then check global mappings for any fields not already set
foreach ( $all_data as $field_key => $value ) {
// If we already have a level-specific mapping, skip
if ( isset( $update_data[$field_key] ) ) {
continue;
}
// Check if there's a global mapping
if ( isset( $fields[$field_key] ) && ! empty( $fields[$field_key]['crm_field'] ) ) {
$update_data[$field_key] = $value;
}
}
} else {
// No level, use global settings
$update_data = array(
'pmpro_membership_level' => null,
'pmpro_expiration_date' => null,
'pmpro_subscription_price' => null,
'pmpro_next_payment_date' => null,
'pmpro_status' => $this->get_membership_level_status( $user_id ),
);
}
// Send the updated data
wp_fusion()->user->push_user_meta( $user_id, $update_data );
}
/**
* Set up hooks to add and save settings
*
* @since 3.40.0
*/
public function init() {
// Add existing hooks
// Add new hooks for per-level settings
add_action( 'pmpro_membership_level_before_content_settings', array( $this, 'add_level_settings' ) );
add_action( 'pmpro_save_membership_level', array( $this, 'save_level_settings' ) );
}
This is mostly right. It uses the wpf_render_crm_field_select()
function to render the UI, and saves it properly (though the save function is a bit cumbersome and could be simplified).
The output is properly escaped, and translatable using our textdomain, and the documentation follows the standards.
In summary:
This would work 👍. Claude got a boost because I pointed it directly to the relevant functions, but it works about the same in the Pro plan when the files are uploaded into the project context. I’d say 90% of this is usable.
However, it’s still cumbersome to have to have this conversation in a chat context, then copy the code into the project.
Approach 3: Cursor
Cursor has been a game changer for us since it integrates the Anthropic (aka Claude) LLMs directly into your IDE.
Cursor is a fork of VS Code, so if you’re already using VS Code migration is instant— all your extensions, settings, keyboard bindings, etc., are copied over automatically. When I first opened Cursor it even knew which files I had open when I last closed VS Code. It’s effortless.
Rather than going through how I started with Cursor and how it’s working now, I’ll just show you everything put together. Here’s my prompt, slightly modified to work best with my Cursor workflow:
TASK: Our WP Fusion integration with Paid Memberships Pro currently allows syncing fields like Membership Status and Expiration Date with the customer’s CRM. This is configured sitewide, which means it’s not compatible with concurrent memberships. Please extend the integration to support adding a new settings panel to the PMPro membership configuration page where users can map fields with their CRM specific to that membership level, similar to the change we’ve recently made in the WooCommerce Subscriptions integration (`class-woo-subscriptions.php` is attached).
When a member signs up at that level, the fields should be synced to their contact record in your CRM. The fields should be saved in such a way that they can be edited from the membership level settings, or via the global Contact Fields list (the same way as the Woo Subscriptions integration works).

You’ll notice I’m using Cursor in Agent mode, with claude-3.7-sonnet
as the model. I’ve added class-pmpro.php
and class-woo-subscriptions.php
to the project context.
Here’s a video of the agent in action:
And here’s the finished field mapping UI on the membership plan configuration:

I did have a problem with the new nonce verification not passing.
I pointed it out to the agent, and it navigated to my plugins directory, and searched the Paid Memberships Pro plugin for the relevant admin template files.

It found the PMPro nonce name and how it’s verified, and corrected the code.
I found a couple of other issues too. For example it was using get_the_title()
to get the title of the membership level, but Paid Memberships Pro levels aren’t normal posts, so this won’t work.
For small changes like this I’ll switch back to Chat mode (instead of Agent) to get a quick fix for a specific line. It solved it without issue:

After those two fixes, everything worked quite nicely. I especially like this implementation because it saves the field mappings to our global settings, meaning they can be edited there in addition to on the membership level.

I’d like to point out that this is a difficult task— even a couple of months ago this would have been impossible.
LLMs struggle with this for a couple of reasons:
- This file is large. At 1700 lines, it’s way bigger than what would normally fit in the “context window” of a single chat. The Woo Subscriptions integration we used for inspiration is also large, at 1500 lines.
- The integration is old— it dates back to 2016, so it doesn’t follow our code guidelines, or WordPress development best practices. You’ll notice in the screencast I disabled automatic linting in the Cursor settings— this feature works great with new integrations, but because there are so many errors in the file, the agent gets hung up on fixing them and never completes the task.
The amazing thing is that despite this, the agent is able to search the files for relevant code, break them into manageable blocks of a few hundred lines, and load them into context where they can be used effectively.
I wanted to use something challenging as an example of the capabilities of working with agents, but for something more representative of our current workflow, watch this example of completing a feature request for a customer in our Memberoni integration.
This file is smaller, newer, and already conforms to our code and documentation standards.
You can see it’s much faster, and with Iterate on Lints enabled the Agent automatically goes back and corrects any new code that doesn’t conform to our standards.
It’s a nice implementation because it correctly handles lessons that are inside of modules, and removing all the complete tags from the course content when a course’s progress is reset— something I probably wouldn’t have bothered with if I were doing this manually.
Current setup
Here are the tricks / tools we’re using at the moment:
- Cursor IDE
- Claude 3.7 Sonnet, in Chat mode for planning, or Agent mode for coding
- It’s also sometimes worthwhile to switch to a “reasoning” model like
r1
oro3
in chat mode for planning projects
- It’s also sometimes worthwhile to switch to a “reasoning” model like
- “YOLO mode” — enabling this in the Cursor settings allows agents to run terminal commands, like searching for files, running unit tests, creating git branches, etc.
4. “Iterate on lints”
This Cursor setting can go a long way towards enforcing your code and documentation standards with the agent, but it can cause problems if used on large files that aren’t already compliant (you’ll note I turned it off when working with Paid Memberships Pro).
We use PHPStan for static analysis, and PHPCS with the WordPress.org coding standards ruleset to ensure our work meets the official code and documentation standards.

With both of these enabled, if the agent generates code that doesn’t pass any of the enabled checks and standards, it will see the error message, and go back and “iterate” on its work until everything passes.
Usually these fixes end up in the context of the current conversation so it won’t make the same mistake twice.
5. Instructions.md
We include a file called instructions.md
in the project root as overall instructions for LLMs.
This file isn’t included in every request, but it is indexed by Cursor so it’s part of the general project context, and when we find an agent is going off the rails a bit we can remind it of our standards by referencing the file using @instructions.md
.
TBH I’m not sure how big a difference this makes overall but I see it recommended in many places across Reddit and YouTube so I figured it couldn’t hurt to have.
You can find our current instructions.md
here.
instructions.md
can also be referenced by Rules:
6. Cusor Rules
Cursor has a rules system where you can create project-wide rules which apply to specific file patterns or directives in the chat. Rules can also be referenced by tagging @rule-name.mdc
.
For WP Fusion we use several rules. The main one is agent-workflow.md
, this tells the system how to perform when we’re working with an agent. The contents of that file are:
---
description: Description: High-level guidance for AI agents on how to prioritize rules and manage workflows. This is a meta-rule that helps organize the application of other rules.
globs:
alwaysApply: true
---
# Agent Workflow Guidelines
## Priority Order
1. "TASK:" directives (see [task-directives.mdc](mdc:wp-fusion-lite/.cursor/rules/task-directives.mdc))
2. Code generation requests (follow patterns in relevant rule files)
3. General inquiries (reference [instructions.md](mdc:wp-fusion-lite/instructions.md) )
## Scratchpad Management
Always maintain the scratchpad at [scratchpad.md](mdc:wp-fusion-lite/.cursor/scratchpad.md) for any non-trivial task.
## Documentation References
- For WordPress standards: See [wordpress.mdc](mdc:wp-fusion-lite/.cursor/rules/wordpress.mdc)
- For CRM integrations: See [crm-integration.mdc](mdc:wp-fusion-lite/.cursor/rules/crm-integration.mdc)
- For core functionality: See [core.mdc](mdc:wp-fusion-lite/.cursor/rules/core.mdc)
The alwaysApply
sets this rule to run with every request. It’s prepended to the conversation and helps the agent understand how to interact with the request.
It then links to instructions.md
and several other rule files depending on the nature of the request. The other rule files we use are:
- task-directives.mdc: This rule is designed to kick in when a request starts with “TASK:”, it provides a more structured approach to handling a feature request or bug fix. You can see me trying to invoke the TASK directive in my change with Paid Memberships Pro, but so far I’ve only been able to get it to work inconsistently.
- core.mdc: This rule provides an overview of our project and development patterns, and APIs. It’s designed to run with any
*.php
files in the project. - wordpress.mdc: This rule provides a more generic overview of WordPress development best practices (not unique to WP Fusion).
- crm-integration.mdc: This rule runs on PHP files in the
/includes/crms/
folder. It helps the agents understand our CRM integration class structure when creating new CRM integrations.
These files are all version controlled and shared among the team.
7. Scratchpad.md
For larger tasks, I find it helps the agent to use a scratchpad in ./cursor/scratchpad.md
to first break down the objectives, and then request my approval before it starts to work.
I’ve tried to make this automatic any time I start a request with “TASK:” but so far it’s been hit or miss. However if I specifically request that the agent use the scratchpad at the start of a session, it will use it.
Here’s the scratchpad.md
from our Paid Memberships Pro feature request:
current_task: "Extend PMPro integration to support membership-level specific field mapping"
status: complete
description: Added membership-level specific field mapping to the PMPro integration, similar to the WooCommerce Subscrptions implementation
steps:
[X] Created a method to define available CRM fields for PMPro memberships
[X] Added a field mapping section to the membership level settings UI
[X] Updated the save_level_settings method to save field mappings
[X] Modified the sync_membership_level_fields method to use level-specific field mappings
[X] Updated the prepare_meta_fields method to include level-specific fields
[X] Added proper nonce verification for security
reflections:
- Maintained backward compatibility with existing global field mapping
- Added level-specific fields that include the level name for clarity
- Implemented similar pattern to the WooCommerce Subscriptions integration
- Fixed Yoda condition in the after_change_membership_level method
decisions:
- Used the format 'pmpro_fieldname_levelid' for level-specific fields
- Kept the global fields for backward compatibility
- Added proper nonce verification for security
In the case of longer tasks that take multiple requests, this lets the agent pick up where it left off.
The scratchpad.md
is not version controlled (it’s included in .gitignore
) which means Cursor will also ignore it by default.
We solve that with the .cursorignore
file.
8. .cursorignore
By default Cursor ignores all files in your .gitignore
. In our case we want Cursor to have access to scratchpad.md
without committing it to Git (since it’s just a scratchpad).
This is solved with a .cursorignore
file in the project’s root directory:
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
vendor/
node_modules/
languages/
build/
!.cursor/scratchpad.md
9. Documentation
Cursor supports adding custom documentation to the project context. Just point in the base URL to the documentation, and Cursor will index it and make it available to the AI.

10. @ commands
Cursor has a command palette that can accept a variety of inputs, such as URLs, references to documentation (see above), links to the web, and your current git state.
For example I’ll often use an Ask / Chat session to review PRs, and then switch to an agent to make changes if needed.

Or if I’m working with a new API, I’ll @url
the API docs so the agent can read the page and make sure my code properly formats data and handles responses.
11. Use small files
Historically we’ve just bundled all the functionality for a single integration into one big class. Cursor has a hard time dealing with these.
I’m also using Cursor to work on EchoDash, which is built in Ruby on Rails, and follows a Model View Controller convention. Because each file is small (less than 100 lines) and descriptively named, the agent can quickly jump between 5+ files to make the changes it needs. It’s much faster and more accurate.
Whereas when we point an agent at our WP Fusion + WooCommerce integration (at 4,000+ lines), it quite often gets confused and forgets where it’s working.
To fix that, we’re breaking up our integrations into descriptively named smaller classes with related functionality (admin interfaces, membership level changes, transactions, etc). This has sped up development with Cursor significantly.
12. Clean up first
If you’re working on old code that doesn’t meet your current standards, clean it up before running an agent on it.
The problem is, if you ask, “Add this new feature” to a file that doesn’t match the style you’ve asked for in your rules, the agent will spend most of the time trying to fix the file, and forget about the feature.
Instead, what I like to do is start by asking “Can you please review this file against our latest code and documentation standards, and update it accordingly before we begin work on a new feature”.
Once this is done and all lints / tests pass, the agent tends to work a lot more efficiently.
What I’m interested in next
I’ve heard exciting things about MCP (Model Context Protocol) servers with Cursor. These let you connect Cursor agents to outside services (usually self-hosted) that can help them accomplish a specific task.
I’ve been recommended the Sequential Thinking MCP Server, but haven’t had a chance to try it out yet.
I’m still learning the nuances of the rules directives. Ideally what I want is, when I ask for a TASK:, the agent will:
- Search the relevant files to understand the task to be done.
- Write a summary into a
scratchpad.md
and wait for me for review. - Once I approve it, create a Git branch off of
main
with the name of the feature of fix. - Work through the tasks in the
scratchpad.md
until they are complete, updating the scratchpad with any challenges or observations. - Update the
readme.txt
with a summary of the changes once the task is complete.
I’ve been able to get this to work if I spell it out at the start of the conversation, but haven’t been able to get it to work consistently by saying “TASK:” (you can see in my screencasts it does update the readme, sometimes uses the scratchpad, and does not create a git branch).
Are LLMs making life easier?
When I said AI generates about 95% of my code, you’d be forgiven for picturing me waking up in the morning, assigning a few tasks to agents, and then heading to the beach 🏖️
I’m still working just as much, but my output has increased significantly. We used to say no to requests from just one or two customers, because we didn’t have the resources.
Now, if it’s a simple request, and an LLM can accomplish it in a few minutes, I’m happy to make changes.
It’s also made it easier to open-source a lot of tools we’d developed internally.
Previously we’d hack together something in a minimal way to solve a problem we were having (let’s say, some EDD license activation issue), and keep it private. It’s a lot of work to turn a single function into a plugin someone else can use.
Now I can say, “Take this X we’re using on our site, put it into a new plugin, create a new Github repo for it with a readme, and make sure it conforms to all best practices in terms of security, extensibility, and accessibility before requesting a final review from me.”, and it basically does all the heavy lifting.
Further reading
This stuff is changing really quickly and it can be hard to keep on top of.
I recommend checking:
- The Cursor changelog: if they add a new feature, spend some time playing with it.
- r/cursor on Reddit: Some great tips and tricks here for writing prompts and rules.
- YouTube: I don’t have any particular channels to recommend, I’ve just been clicking through to videos from Reddit and now I get some good recommendations for Cursor hacks on my home page.
Update – March 14th
I said this stuff changes quickly 😅. We’ve incorporated (adapted to?) three changes since I originally wrote this:
Cursor 0.47
Claude 3.7 in Cursor v0.46 tended to go “off the rails” a bit. I found it would start making changes I didn’t ask for, or get stuck in a loop on linter errors. You can read more on this Reddit thread.
Updating to the Cursor 0.47 pre-release has smoothed over a lot of those issues. 0.47 also includes an “auto-select” mode for the agents, which will switch between LLM models (in the same session) depending on the task at hand. Some agents do better at reasoning, some are better at running tools, and some are better at writing code.
I wish we could see which model is in use at a given time but generally I’ve had a better experience with 0.47 vs the initial Claude 3.7 rollout in 0.46.
Linting
I mentioned in the initial post needing to turn off Iterate On Lints when working with older files. We’ve since fine-tuned PHPCS, PHPCBF, and PHPStan for WP Fusion, meaning most linter errors can be fixed automatically before we assign a task to an agent.
By customizing the PHPCS rules to the project we’ve also stopped Cursor from making changes we don’t want. For example a class named WPF_WooCommerce
should have a filename class-wpf-woocommerce.php
(based on the WordPress coding standards), but we use class-woocommerce.php
. When working with agents, Cursor was renaming files to match the WordPress PHPCS standard, which was annoying and needed to be fixed manually.
MCPs
Everyone’s been telling me the Sequential Thinking MCP Server is an essential companion to Cursor agents, but I looked at the docs a few times, felt overwhelmed, and didn’t know how to get started.
In essence, the project
implements an advanced Sequential Thinking server using the Model Context Protocol (MCP). It provides a structured and flexible approach to problem-solving and decision-making through a series of thought steps, incorporating stages, scoring, and tagging.
Eventually I just opened up a Cursor agent, pointed it at the URL for the MCP server, and asked it to do whatever it needed to get it working.
Of course it had no problem installing the required packages, connecting it to Cursor, and demonstrating that it was working. Took all of two minutes 💁♂️

With these three changes, we’re getting even better results with larger files than even two weeks ago.
A project we’re working on at the moment is splitting WP Fusion’s larger integrations into smaller classes, organized around specific functionality (frontend, admin, hooks, export tools, etc).
As a test, I opened up our Paid Memberships Pro integration again and asked an agent to break it into multiple classes.
The MCP was called up first and broke the project down into steps:

At the end of about 8 MCP calls, the agent had come up with the following plan:
{
"thought": "Now I'm ready to implement the refactored PMPro integration. I'll start by creating the directory structure and then implement each class one by one.
First, I'll create the directory structure:
1. Create a new directory: `includes/integrations/pmpro/`
2. Create three files:
- `class-pmpro.php` (Main class)
- `class-pmpro-admin.php` (Admin class)
- `class-pmpro-hooks.php` (Hooks class)
Then I'll implement each class with proper documentation and following the WordPress coding standards. I'll make sure to:
1. Add proper file documentation headers
2. Add proper class and method documentation
3. Fix any linting issues from the original code
4. Update the code to use the latest WordPress coding standards
5. Ensure backward compatibility
Let's start with implementing the main class (WPF_PMPro), then the admin class (WPF_PMPro_Admin), and finally the hooks class (WPF_PMPro_Hooks).",
}
It then proceeded to implement the plan: creating the directory, creating the three files, and splitting up the functionality.
In the process, it refactored all of our code to meet the PHPCS standards for code and documentation. The agent worked for about five minutes. At the end everything was completed perfectly— no changes were required.
There are a ton of MCPs out there— BrowserTools allows agents to interact with your web browser, Package Version lets agents look up Node packages. There’s even MCP Server, an MCP that lets agents install more MCP servers (this last one scares me 😅).
You can find a ton of other MCPs (and additional Cursor resources) at cursor.directory.