Skip to content

Instantly share code, notes, and snippets.

@lolandese
Last active February 17, 2026 13:15
Show Gist options
  • Select an option

  • Save lolandese/7a8554a77d5769703a9fdec4c69a7410 to your computer and use it in GitHub Desktop.

Select an option

Save lolandese/7a8554a77d5769703a9fdec4c69a7410 to your computer and use it in GitHub Desktop.
AI Agent Guidelines for Drupal Module Porting (Dual-Branch Strategy) - D8/D9 → D10/D11. Covers branch management, TODO.md organization, API migrations, testing, demo submodules, gist sync, and porting workflow.

AI Agent Rules for Drupal Module Porting

Project Context

This workspace contains multiple Drupal versions (D7, D8, D9, D10, D11) for porting contrib modules maintained by lolandese. The goal is to modernize modules to Drupal 10/11 standards using AI assistance.

Core Principles

1. Dual-Version Strategy

  • Two version branches:
    • 1.0.x for Drupal 8/9 (maintenance mode, use latest stable D9)
    • 2.0.x for Drupal 10/11 (active development, use latest stable D11)
  • Port forward only: If a D8 version exists, do NOT create D7 versions
  • Version-specific targeting: Each branch targets its Drupal core versions appropriately
  • When to split versions: AI should recommend splitting when:
    • API changes are too disruptive for compatibility layers
    • New features require D10/11 exclusive APIs
    • Maintaining backward compatibility introduces technical debt
    • Security or performance significantly benefits from version split

2. Modernization Priority

  • Use Drupal best practices: Embrace services, dependency injection, and plugin systems
  • Refactor old patterns: Convert procedural code to OOP where appropriate
  • Configuration management: Use YAML configuration over database storage
  • Leverage modern APIs: Use entity API, form API improvements, and render arrays properly
  • Pragmatic approach: Balance modernization with version compatibility

3. API Migration Strategy

  • Deprecated functions are ACCEPTABLE in D8/9 branch when:
    • They work in the target Drupal 9.x stable version
    • Replacing them adds unnecessary complexity
    • Compatibility layers aren't available
    • MUST document with inline comments: Why deprecated function is used, what version deprecated it, what replaces it in D10/11
  • D10/11 branch should be fully modern: No deprecated functions, use current best practices
  • Document all API differences between branches in CHANGELOG
  • Use compatibility layers when available and sensible

Decision Criteria for Deprecated Functions (D8/9 branch):

  • Modern API requires 5+ lines of DI setup → OK to use deprecated in 8.x branch
  • Modern API is direct 1:1 replacement → Use modern API even in 8.x branch
  • Modern API available as compatibility layer → Use modern API
  • Document decision with inline comment explaining trade-off

Technical Standards

Dependency Management for Contrib

Development Version Dependencies

When adding dependencies that use development versions (e.g., dev-main, 3.0.x-dev), always pin to a specific commit hash to prevent unexpected changes during composer update.

Why This Matters:

  • Development branches change frequently
  • composer update can pull in breaking changes unexpectedly
  • Pinning ensures reproducible builds across environments
  • Prevents CI/testing failures due to upstream changes

Correct Format:

{
  "require": {
    "drupal/fivestar": "3.0.x-dev#abc123def456",
    "vendor/package": "dev-main#789xyz012345"
  }
}

Incorrect Format (Avoid):

{
  "require": {
    "drupal/fivestar": "3.0.x-dev",
    "vendor/package": "dev-main"
  }
}

How to Find Commit Hashes:

# Via Git Repository
git log --oneline -n 10
# Copy the commit hash from desired commit

# Via Composer Show
composer show vendor/package --all
# Look for commit hash in version information

# Via GitHub/GitLab
# Navigate to repository → branch → copy latest commit hash

When to Update Commit Hashes:

  • Before major releases - Update to latest stable commits
  • When fixing bugs - Update if upstream fixes are needed
  • During security updates - Update immediately if security fixes available
  • Regular maintenance - Review and update quarterly

Stable Version Dependencies

For stable releases, use semantic versioning:

{
  "require": {
    "drupal/core": "^10 || ^11",
    "drupal/votingapi": "^3.0"
  }
}

Essential Porting Tools

Before starting any port, install and use these critical tools:

Upgrade Status Module

composer require drupal/upgrade_status
drush en upgrade_status -y
# Visit /admin/reports/upgrade-status or:
drush upgrade_status:analyze MODULE_NAME
  • Provides deprecation dashboard for your module
  • Identifies D10/11 compatibility issues
  • Run BEFORE starting port and AFTER each major change
  • Should show all green before considering port complete

Drupal Rector (Automated Refactoring)

composer require --dev palantirnet/drupal-rector
vendor/bin/rector init  # Generates rector.php config

# Dry run to see what would change
vendor/bin/rector process web/modules/custom/YOUR_MODULE --dry-run

# Apply automated fixes
vendor/bin/rector process web/modules/custom/YOUR_MODULE
  • Automates many deprecated API replacements
  • Handles common transformations (entity_load, drupal_set_message, etc.)
  • Run this BEFORE manual porting work
  • Saves hours of repetitive refactoring

PHPCompatibility (PHP Version Checking)

composer require --dev phpcompatibility/php-compatibility
vendor/bin/phpcs --standard=PHPCompatibility \
  --runtime-set testVersion 8.1 \
  web/modules/custom/YOUR_MODULE
  • Critical for dual-branch strategy
  • 8.x branch: Check against PHP 7.4+
  • 10.x branch: Check against PHP 8.1+
  • Identifies PHP version conflicts

Examples for Developers Module

composer require --dev drupal/examples
  • Reference implementations of Drupal APIs
  • Location: web/modules/contrib/examples/modules/
  • Consult before implementing new patterns
  • Covers hooks, plugins, forms, entities, caching, and more
  • More reliable than web search results

Automated Refactoring Workflow

ALWAYS follow this sequence:

  1. Analyze - Run Upgrade Status to identify all issues
  2. Auto-fix - Run Drupal Rector to handle common migrations
  3. Verify - Run PHPStan and PHPCS on rector output
  4. Manual - Address remaining complex migrations by hand
  5. Test - Run full test suite
  6. Re-analyze - Run Upgrade Status again (should be green)
# Complete workflow
cd ~/port/D9/web/modules/custom/YOUR_MODULE  # or D11 for 10.x

# 1. Initial analysis
drush upgrade_status:analyze YOUR_MODULE

# 2. Auto-refactor
vendor/bin/rector process . --dry-run  # Review changes first
vendor/bin/rector process .            # Apply changes

# 3. Code quality check
vendor/bin/phpcs --standard=Drupal,DrupalPractice .
vendor/bin/phpstan analyze .

# 4. Test
vendor/bin/phpunit

# 5. Final verification
drush upgrade_status:analyze YOUR_MODULE  # Should be clean

Development Best Practices

Session Learning & AGENTS.md Evolution - WITH GIST SYNC

This guide evolves based on real porting experience. After major work sessions, propose additions:

What to Document:

  • Code patterns: Reusable architectural approaches with links to actual implementations
  • Error solutions: "When X error appears, try Y" with diagnostic commands
  • Workflow patterns: Development practices that prevent bugs
  • API migration patterns: Specific D7→D10/11 transformations that worked well

What NOT to Document:

  • One-off workarounds specific to a single module
  • Obvious Drupal best practices already in official docs
  • Temporary debugging steps

Documentation Format:

// Link to real code example
See themeless/src/Controller/ThemelessController.php#L45 for
event dispatcher injection pattern in D10/11 controllers

CRITICAL: After adding significant guidance to AGENTS.md:

  1. Commit to local git with clear message explaining additions
  2. Run security scan (see Sensitive Data Guard section)
  3. Update the GitHub Gist so future AI agents have access to the guidance
  4. Document in session logs what knowledge was captured

This ensures session learnings compound across multiple AI agent interactions rather than being lost or isolated to workspace.

TODO Management

Basic Workflow

Quick Capture Approach (Capture-Only Behavior): Use @TODO, @NOTE, or @FIXME prefixes in conversation to capture tasks directly without ceremony (see Quick Note Capture with Prefixes for syntax):

@TODO: Add validation for email format in contact form
@TODO (test): Create kernel test for cache invalidation

Plan Auto-Capture (Automatic Preservation): When implementing a multi-step plan, remaining tasks are automatically captured to TODO.md to prevent losing work (see Auto-Capture of Deferred Plan Tasks for details):

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Database Schema Updates
  - #9: Step 3: Add Unit Tests
  - #10: Step 4: Performance Optimization ⚠️

CRITICAL WORKFLOW RULE:

  • When prefix is detected (@TODO, @NOTE, @FIXME), ONLY the capture action is performed
  • NO implementation begins automatically - agent stops after writing to file
  • NO continuation or follow-up work - task is tracked and nothing else happens
  • To proceed with implementation, user must explicitly say "Start implementation" or similar instruction
  • This ensures user intent is respected: capture ≠ start work

Manual Entry For User-Initiated Requests: When user explicitly says "add to TODO":

  • Only add to TODO.md - do not start implementation
  • Wait for explicit "Start Implementation" before beginning work
  • Prevents premature implementation when user wants to track ideas

AI Budget Note: Most @TODO captures consume <0.33x effort (minimal AI input required)

Session State Symlink Architecture (CRITICAL)

🔗 All TODO.md, DONE.md, and NOTES.md files are SYMLINKS

This workspace uses Option B architecture: session-state as source of truth with automatic backup.

How it works:

SESSION-STATE REPO (Source of Truth):
~/session-state/D11/themeless/TODO.md    ← REAL FILE (backed up to GitHub)
~/session-state/D9/themeless/TODO.md     ← REAL FILE (backed up to GitHub)

WORKSPACE (Symlinks):
~/ddev-projects/port/D11/.../TODO.md → ~/session-state/D11/themeless/TODO.md
~/ddev-projects/port/D9/.../TODO.md  → ~/session-state/D9/themeless/TODO.md

For AI Agents:

  • Editing works transparently: When you read/write workspace/TODO.md, the OS follows the symlink
  • Changes auto-sync: Workspace edits write directly to session-state (no manual sync)
  • Backup is automatic: Cron/anacron commits session-state changes to private GitHub repo
  • Crash-proof: All file contents backed up to GitHub (not just symlink metadata)
  • Single source of truth: No duplication, no sync conflicts

What this means for you:

  • ✅ Edit TODO/DONE/NOTES files normally (symlinks are transparent)
  • ✅ Changes persist to GitHub automatically via scheduled backups
  • ✅ No special handling needed - workspace files work like regular files
  • ⚠️ If symlink is broken, check ~/session-state exists and run convert-to-option-b.sh

Backup Schedule:

  • Daily at 2am: Automatic commit and push if system is running
  • On boot: Catch-up backup 30 seconds after startup (handles laptop shutdown scenarios)
  • Manual: Run sescap alias (or ~/ddev-projects/port/backup-session-state.sh)

Bash Command Toolkit:

  • sescap - Quick backup to GitHub
  • sesstat - Show status and last backup
  • sesadd <module> - Add module to backup (creates templates + auto-updates .gitignore)
  • seslist - List all tracked modules
  • seshelp - Show full command reference
  • Note: sesadd automatically creates/updates .gitignore to exclude tracking files from module repository

Private Repository:

TODO.md Structure (Professional Template)

Purpose: LOCAL DEVELOPER USE ONLY session tracking document (excluded from git)

Workspace Versions - Which Get TODO.md Files

Per the dual-version strategy, only active development versions get TODO.md tracking:

  • D9 (1.0.x branch): ✅ Keep TODO.md - Active maintenance & bug fixes for D8/9
  • D10: ❌ Remove TODO.md - Reference/testing only, uses same 2.0.x code as D11
  • D11 (2.0.x branch): ✅ Keep TODO.md - Active development for D10/11
  • D7, D8: ❌ No TODO.md - Reference-only Drupal installations for bootstrapping

Why this matters: D9, D10, D11 are separate clones of the same drupal.org git repository. Each clone can checkout different branches (1.0.x, 2.0.x, etc.), but since D10 doesn't have a dedicated release branch (it tests D11's 2.0.x code), it shouldn't have independent development tracking. Use D11's TODO for 2.0.x feature work; use D10 only for Drupal 10-specific compatibility testing of D11's code.

File Location & Scope Organization: FOLDER LEVEL MATTERS

TODO.md files should be organized by project scope to keep work tracking aligned with code structure:

  1. Module-Specific TODO.md (Primary - recommended for most work)

    • Location: [module-root]/TODO.md (same level as .git, .gitignore, *.module files)
    • Example: /home/martinus/ddev-projects/port/D11/web/modules/custom/themeless/TODO.md
    • Scope: Tickets only for that specific module
    • Use case: Feature development, bugs, documentation for one module
  2. Version Branch TODO.md (Cross-module work)

    • Location: [drupal-version]/web/modules/custom/TODO.md (same level as all module folders)
    • Example: /home/martinus/ddev-projects/port/D11/web/modules/custom/TODO.md
    • Scope: Issues spanning multiple modules within one Drupal version
    • Use case: Version-wide refactoring, shared dependencies, compatibility issues
  3. Workspace Root TODO.md (Porting strategy & meta-work)

    • Location: /home/martinus/ddev-projects/port/TODO.md (highest level)
    • Scope: Workspace-wide porting decisions, version coordination, infrastructure
    • Use case: Meta-work about the porting process itself, decisions affecting all versions

Out-of-Scope Ticket Detection & Migration:

When capturing new tickets, detect scope mismatches and handle appropriately:

  1. Detect mismatch:

    • Ticket affects multiple modules → belongs in version-level, not module TODO.md
    • Ticket affects multiple versions → belongs in workspace root
    • Ticket is meta-work → doesn't belong in module TODO.md
  2. Agent behavior on detection:

    • Inform user: List out-of-scope tickets with current location and correct location
    • Ask permission: "Should I move these tickets to their correct TODO.md files?"
    • Migrate if approved: Move tickets to appropriate TODO.md, preserving all metadata
  3. Example notification:

⚠️ Out-of-Scope Tickets Detected in themeless/TODO.md:

- #15: Add JSON:API integration (affects multiple modules)
  Current: D11 themeless module TODO.md
  Suggested: D11/web/modules/custom/TODO.md
  Reason: Issue impacts multiple modules, not just themeless alone

- #22: Upgrade PHP 8.3 compatibility (workspace meta-work)
  Current: D11 themeless module TODO.md
  Suggested: /port/TODO.md
  Reason: Decision affects all Drupal versions

Move these tickets to correct locations? (y/n)

Create TODO.md & .gitignore if Missing:

When creating a new TODO.md at any scope level, always handle .gitignore:

  1. Create the TODO.md file at appropriate folder level
  2. Check for .gitignore at same folder level:
    • If exists: Add TODO.md, DONE.md, NOTES.md to ignore list
    • If missing: Create .gitignore with these exclusions
  3. Verify exclusion: Run git check-ignore TODO.md (should show path match)

Standard .gitignore snippet:

# Local development tracking (excluded from version control)
TODO.md
DONE.md
NOTES.md

Git Exclusion: Add to .gitignore:

TODO.md
NOTES.md

Required Sections:

  1. Warning Banner:
# [Module/Project] TODO

**⚠️ LOCAL DEVELOPER USE ONLY**

This file is for **local developer session tracking only** and is excluded from the remote repository via `.gitignore`.
  1. Guidelines Section:
  • Working notebook during active development sessions
  • Limit to current sprint or immediate work items
  • For persistent tracking, use project issue queue (drupal.org, GitHub Issues, etc.)
  • Before switching tasks, consider migrating items to issue queue
  • End of session: Clean up or keep as local reference
  1. Time & Budget Estimates Explanation:
**Time & Budget Estimates:**
- **Effort estimates** represent human developer session time (including AI interaction, code review, testing, iteration), not pure AI generation time
- **AI Budget %** estimates percentage of [AI Service] monthly limit (e.g., 300 requests/month for GitHub Copilot Pro)
- ⚠️ **Warning triangle** indicates tasks consuming >2.0% of monthly limit (>[threshold] requests)
  1. Impact & Compliance Points Explanation:
**Impact & Compliance Points:**
- **Impact** measures how completing an issue improves the module's adherence to AGENTS.md standards
- **Compliance points** are calculated by evaluating the module against AGENTS.md requirements across multiple areas:
  - Documentation quality (PHPDoc completeness, hook documentation, inline comments)
  - Test coverage (unit tests, kernel tests, functional tests, edge cases)
  - Code quality (type hints, dependency injection, coding standards)
  - Security practices (access control, input validation, test coverage)
  - Performance optimization (caching, database queries, render arrays)
- **Cumulative score** (e.g., "→ 94/100 cumulative") shows the total compliance after completing all issues in order
- Example: "+2 compliance points (→ 94/100 cumulative)" means this issue adds 2 points, resulting in 94/100 total if all previous issues are complete
- The baseline score (before TODO work) is shown in the Compliance Status section (if applicable)
  1. Categories with Emoji Tags:
**Categories:** Tag each item with one category for quick filtering:
- **🐛 bug** — Something broken or not working as expected
- **✨ feature** — New functionality to implement
- **📝 docs** — Documentation updates or guides
- **🧪 test** — Testing improvements or coverage
- **♻️ refactor** — Code quality, cleanup, or restructuring
- **⚡ performance** — Optimization and caching improvements

**Status Lifecycle:**
- **Not Started****In Progress****Blocked** (temporary)
- **solved** — Technical implementation completed
- **validated** — Implementation tested and verified
- **closed** — Issue resolved and ready for archival
  1. Table of Contents:
## Table of Contents

- [Recent Status Changes](#recent-status-changes-latest-first) (X items)
- [Outstanding Issues](#outstanding-issues) (X items)
- [Completed Tasks](#completed-tasks) (X sessions)
- [Compliance Status](#compliance-status) (if applicable)
- [For Next Session](#for-next-session)
  1. Recent Status Changes Section (NEW):
## Recent Status Changes (Latest First)

- **2026-02-16** - Issue #7: Performance Profiling & Caching Strategy → **closed**
- **2026-02-16** - Issue #6: Expand Event System Tests → **closed**
- **2026-02-15** - Issue #4: Kernel Tests for Security Features → **validated**
- **2026-02-15** - Issue #2: Helper Function Documentation → **closed**
- **2026-02-15** - Issue #1: Demo Module Hook Documentation → **closed**

Purpose: Minimal shortlist showing recent status transitions in descending chronological order for quick reference.

  1. Outstanding Issues Format:
### [Number]. [Issue Title]

- **Category:** 📝 docs
- **Module:** themeless_demo
- **Status:** Not Started | In Progress | Blocked | solved | validated | closed
- **Date Identified:** 2026-02-15
- **Priority:** High | Medium | Low
- **AGENTS.md Reference:** [Section Name] (lines X-Y)

- **Recommended Model:** 0.33x | **1x** | 3x
- **Estimated Effort:** X minutes/hours
- **AI Budget:** ~X.X% (Y requests) [add ⚠️ if >2.0%]
- **Impact:** +X compliance points (→ YY/100) or other measurable outcome

**Description:** Clear explanation of what needs to be done.

**Current State:** What exists now (optional).

**Required Changes:**
- Bullet list of specific actions
- With technical details
- And acceptance criteria

**Files to Update:**
- [path/to/file.php](path/to/file.php) - what changes
  1. DONE.md Archive Workflow (NEW):

When to Create:

  • When project has many completed items cluttering TODO.md
  • For historical reference and tracking accomplishments
  • To maintain clean active TODO while preserving implementation details

Archive Migration Rules:

  • Only closed status items move to DONE.md
  • solved and validated items remain in TODO.md Outstanding Issues for session visibility
  • Session narratives remain in TODO.md until related items are closed

DONE.md Structure:

# [Module/Project] DONE

**⚠️ LOCAL DEVELOPER USE ONLY**

This file is for **completed items archive** and is excluded from the remote repository via `.gitignore`.

**Archive Guidelines:**
- Contains all **closed** status items moved from TODO.md for historical reference
- **Streamlined format** focused on completed implementation details
- **For persistent task tracking**, use the [drupal.org issue queue] — this is the **source of truth for the community**

## Archived Issues (Latest First)

### [Number]. [Issue Title]

- **Status:** **closed** (YYYY-MM-DD)
- [Essential metadata: category, impact, files modified]

**Implementation Results:**
- Key accomplishments and outcomes
- Performance improvements or measurable benefits
- Files created/modified with brief description

## Implementation Sessions

- Preserve major session narratives from TODO.md
- Focus on implementation techniques and lessons learned
- Maintain chronological development story

Git Exclusion: Add to .gitignore:

TODO.md
DONE.md
NOTES.md
  1. Completed Tasks Format (Session-Based):
## Completed Tasks

### 2026-02-15 Session: [Session Title]

**Status:** ✅ Complete | 🚧 In Progress

**Scope:**
- What was accomplished
- Key decisions made
- Files modified count

**Changes Made:**
- [file/path.php](file/path.php) (+X lines) - what changed
- [another/file.js](another/file.js) - description

**Validation:**
- ✓ PHPCS passed
- ✓ Tests pass
- ✓ Manual testing results

**Impact:**
- Compliance: XX/100 → YY/100 (+Z points)
- Performance: Improved/No change
- Technical debt: Reduced/Added/None

**Pre-existing Issues Found:**
- Document any issues discovered but not fixed
- With quick fix estimates
  1. Compliance Status (Optional for AGENTS.md Compliance Tracking):
## Compliance Status

**Current Score:** XX/100

**Compliance Breakdown:**
- ✅ Category Name: 100% (fully compliant)
- ⚠️ Category Name: XX% (partially compliant with gaps)
- ❌ Category Name: 0% (not addressed)

**Path to 100% Compliance:**
- Issue #X + #Y: XX → YY (+Z points) - description
  1. For Next Session:
## For Next Session

**Recommended Priority:**
1. [Issue #X] - rationale (time, budget, impact)
2. [Issue #Y] - rationale

**Quick Wins (<1 hour, <2% budget):**
- Issue #X: [description] (time, budget)

**Substantial Work (>2% budget):**
- Issue #Y: [description] (time, budget) ⚠️

AI Budget Tracking Guidelines

When to Add AI Budget Estimates:

  • All issues in TODO.md should include AI budget percentage
  • Base calculation on your AI service's monthly limit (e.g., GitHub Copilot Pro: 300 requests/month)
  • 1 request ≈ 0.33% of 300-request budget
  • Include warning triangle (⚠️) for tasks >2.0% threshold

Budget Calculation Examples:

15-minute documentation task: ~3 requests = 1.0%
30-minute test creation: ~5 requests = 1.7%
1-hour implementation: ~7-8 requests = 2.3-2.7% ⚠️
2-hour refactoring: ~9 requests = 3.0% ⚠️

Threshold Recommendations:

  • 2.0% threshold: Good for separating quick wins (<30 min) from substantial work
  • Quick wins: No warning, suitable for short sessions
  • Substantial work: ⚠️ warning, requires dedicated time block

Benefits:

  • Session planning: Identify which tasks to combine in available time
  • Budget visibility: Prevent exceeding monthly AI service limits
  • Prioritization: Visual separation of quick vs complex work
  • Team coordination: Share cost estimates for collaborative planning

Example TODO.md Reference

See /home/martinus/ddev-projects/port/D11/web/modules/custom/themeless/TODO.md for complete implementation following this pattern.

LLM Model Selection Guidelines

GitHub Copilot Pro Model Tiers:

Tier selection affects actual AI request consumption against your monthly limit:

  • 0x (Free): Basic queries, syntax checks, documentation lookups - no cost to monthly limit
  • 0.33x (Economy): Documentation writing, code reading, minor refactoring - 3 requests = 1 equivalent (66% savings)
  • 1x (Standard): Code generation, tests, bug fixes, API migrations - baseline cost
  • 3x (Premium): Architecture decisions, complex refactoring, performance optimization - 3x cost per request

Model Selection by Task Complexity:

Task Type Tier Rationale
Documentation writing 0.33x PHPDoc, README updates - simpler models perform well
Code reading/explanation 0.33x Understanding existing code
Minor refactoring 0.33x Renames, formatting, simple cleanup
Feature implementation 1x New functionality requiring code generation
Test writing 1x Unit/kernel/functional tests with assertions
Bug fixes 1x Debugging and code corrections
API migrations 1x D7→D10 conversions, deprecated replacements
Architecture decisions 3x Service design, major technical choices
Complex refactoring 3x Multi-file restructuring, pattern changes
Performance optimization 3x Profiling analysis, caching strategies
Security analysis 3x Vulnerability assessment, access control review

Cost Impact Examples:

Documentation (0.33x): 9 requests = 3 equivalent = 1.0% of 300/month
Standard feature (1x): 7 requests = 7 equivalent = 2.3% of 300/month
Complex refactor (3x): 3 requests = 9 equivalent = 3.0% of 300/month

Strategy Tips:

  • Start with lower tier for initial exploration and code reading
  • Use 1x for most implementation work
  • Reserve 3x for critical decisions or when stuck after 3-4 attempts with lower tier
  • Upgrading tier after repeated failures saves budget vs. continued struggling

Quick Note Capture with Prefixes

Use @NOTE, @TODO, and @FIXME prefixes in conversation for immediate capture to local tracking files.

⚠️ AGENT BEHAVIOR: Capture-Only Rule

When an agent detects @TODO, @NOTE, or @FIXME prefix:

  1. STOP all other work immediately
  2. Capture the note to appropriate file (TODO.md, NOTES.md)
  3. DO NOT continue with implementation
  4. DO NOT start related work - wait for explicit user instruction
  5. Report only the capture action, nothing else

Examples of Correct Behavior:

CORRECT:

User: "@TODO: Create integration test for the payment workflow"
Agent: Captures to TODO.md and stops. No implementation begins.

⚠️ INCORRECT:

User: "@TODO: Create integration test for the payment workflow"
Agent: Captures to TODO.md AND THEN starts writing the test file

CORRECT:

Agent captures task. Later, user says: "Good, now let's implement that test."
Agent: Now begins implementation after explicit instruction.

Directive Reference

Prefix Destination Purpose Silent
@NOTE NOTES.md Quick observations, references, context ✓ No confirmation
@TODO TODO.md New tasks to track ✓ Auto-organized by category
@FIXME TODO.md Bugs or critical issues ✓ Marked with 🐛 category

Format

Basic usage:

@NOTE: The caching strategy works effectively here
@TODO: Add validation for email format in contact form
@FIXME: Database queries showing N+1 on user list page

With optional category tags (for @TODO/@FIXME):

@TODO (test): Create kernel test for cache invalidation
@TODO (docs): Document the event dispatch timing
@TODO (perf): Optimize entity loading in batch operations
@FIXME (bug): Fix isPublished() interface check on User entities
@FIXME (refactor): Reduce complexity in buildCacheMetadata()

Category Tags

Valid categories for @TODO/@FIXME:

  • test → 🧪 test
  • docs → 📝 docs
  • feature → ✨ feature
  • bug → 🐛 bug
  • refactor → ♻️ refactor
  • perf → ⚡ performance

Usage Examples During Development

Quick observation:

@NOTE: The service injection pattern here could be extracted into a trait

Feature suggestion:

@TODO (feature): Add webhook support for real-time updates

Performance issue:

@FIXME (perf): Avatar images loading synchronously - use lazy load

Testing gap:

@TODO (test): Add integration test for complete workflow across modules

Integration with Workflow

  1. Capture during development: Use prefixes naturally in conversation
  2. Auto-organization: Appended with timestamp and context
  3. Session tracking: Becomes part of session history in TODO.md
  4. Review before commit: Check TODO.md before pushing for items to address
  5. Migration: Move critical items to drupal.org issue queue for persistence

Notes.md vs TODO.md

  • NOTES.md: Transient observations, reference material, session notes (excluded from git)
  • TODO.md: Actionable work items, bugs, feature requests (local dev tracking, excluded from git)

Auto-Capture of Deferred Plan Tasks

Automatic preservation of planned work that isn't immediately started.

When an agent generates a multi-step plan and the user begins implementation of the first step without immediately proceeding to subsequent steps, the remaining planned tasks are automatically captured to TODO.md to prevent losing work.

When Auto-Capture Triggers

Auto-capture occurs when:

  1. Agent presents multi-step plan with numbered steps (1, 2, 3, 4...)
  2. User starts implementation of step #1 only (e.g., "Start implementing step 1", "Begin work on the first task")
  3. Remaining steps (#2, #3, #4...) were proposed but not selected
  4. Chat context preservation needed - subsequent steps would scroll above fold or be lost during execution

Auto-capture triggers automatically without requiring user approval to reduce friction.

Auto-capture does NOT trigger when:

  • All plan steps are started simultaneously
  • User explicitly defers entire plan ("Save for next session")
  • User explicitly skips remaining steps
  • Plan was rejected outright

Capture Format for Plan-Derived Tasks

Remaining plan steps are captured using YAML frontmatter format with auto-populated fields:

---
id: [auto-incremented from existing TODO.md issues]
title: [exact wording from plan step]
category: [inferred from description or explicit plan annotation]
status: Not Started
date_created: YYYY-MM-DD
effort: [preserved from plan estimate or inferred from description]
budget: [calculated from effort estimate]
impact: [preserved from plan or estimated]
blocked_by: [#1] (previous step, if dependent) or []
priority: [High|Medium|Low from plan or "Medium" default]
source: Auto-captured from plan step N
---

**Description:** Original plan step wording.

**Plan Context:** References to the parent plan for context during later execution.

Notification to User

After capturing deferred tasks, agent provides scannable notification:

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Database Schema Updates (effort: 1x)
  - #9: Step 3: Add Unit Tests (effort: 1x)
  - #10: Step 4: Performance Optimization (effort: 3x) ⚠️

View them anytime: cat TODO.md | grep -A 5 "^### [0-9]"

Notification content:

  • Task IDs and titles from plan steps
  • Effort estimate for each item
  • Destination: TODO.md with file path
  • Warning triangle (⚠️) for high-effort items (>2.0% budget)
  • Practical instruction for user to view captured tasks

Placement: After main response content, before closing remarks.

Task Dependency Handling

When plan steps have implicit dependencies:

  • Current step (#1): Captures blocked_by: [] (starts immediately)
  • Dependent steps (#2+): Captures blocked_by: [#1] or [#1, #2] if sequential
  • User can manually adjust dependencies in TODO.md if plan was unclear
  • When completing step #1, user can update dependent task status: blocked_by: []

User Workflow After Capture

  1. Begin step #1 - agent automatically captures remaining steps to TODO.md
  2. Complete step #1 - test, document, commit
  3. Review captured tasks - check TODO.md to see what's next
  4. Update blocking status - if pursuing step #2, change blocked_by: [#1]blocked_by: []
  5. Start step #2 - exact same format as step #1 execution
  6. Repeat for remaining steps

Example Workflow

Initial Plan Presented:

## Implementation Plan

1. Create database schema updates
2. Generate migration file
3. Add kernel tests
4. Performance optimization

User Response: "Start implementation"

Agent Action:

Implementing step 1: Create database schema updates...
[implementation code and output]

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Generate migration file (effort: 0.33x)
  - #9: Step 3: Add kernel tests (effort: 1x)
  - #10: Step 4: Performance optimization (effort: 3x) ⚠️

User Later: "Let's do step 2"

Agent: Retrieves task #8 from TODO.md, updates status/blocked_by, and implements step 2.

Prefer Drupal Built-in APIs (Critical for Contrib)

Before implementing any feature:

  1. Search Drupal's core modules and APIs for existing solutions
  2. Check contrib modules: drush pm:list | grep <feature>
  3. Consult Examples module before implementing new patterns
  4. Use Form API #states, #ajax instead of custom JavaScript
  5. Leverage service container and core APIs
  6. Document why when custom code is necessary

Comprehensive Error Messaging

When errors occur, provide context and solutions:

✗ Module installation failed: Configuration schema missing

See AGENTS.md section "Configuration Schema" for solutions
Quick checklist:
  1) Does config/schema/<module>.schema.yml exist?
  2) Run: drush config:inspect <module>.settings
  3) Check: /admin/reports/status for schema errors

Pattern:

  • Include diagnostic info (what failed, why, context)
  • Provide solution steps with exact commands
  • Link to documentation sections
  • Add quick self-diagnosis checklists

Form Action Button Pattern (for Admin UIs)

For forms with manual action triggers (e.g., "Generate Demo Content", "Migrate Now"):

// Status section first (weight -5)
$form['status'] = [
  '#type' => 'fieldset',
  '#title' => $this->t('Current Status'),
  '#weight' => -5,
];

$form['status']['info'] = [
  '#markup' => '<p>Demo content: 3 nodes created</p>',
];

// Action buttons in status section
$form['status']['actions'] = [
  '#type' => 'container',
  '#attributes' => ['class' => ['form-actions']],
];

$form['status']['actions']['create_demo'] = [
  '#type' => 'submit',
  '#value' => $this->t('Generate Demo Content'),
  '#submit' => ['::submitCreateDemo'],
  '#button_type' => 'primary',
];

// Submit handler
public function submitCreateDemo(array &$form, FormStateInterface $form_state) {
  try {
    $count = $this->demoService->createContent();
    $this->messenger()->addStatus($this->t('Created @count demo items', ['@count' => $count]));
    $form_state->setRebuild(TRUE); // Refresh form to show updated status
  }
  catch (\Exception $e) {
    $this->messenger()->addError($this->t('Failed: @message', ['@message' => $e->getMessage()]));
  }
}

Key points:

  • Status section shows current state before settings
  • Per-button submit handlers via #submit
  • Try-catch with messenger feedback
  • Call setRebuild(TRUE) to refresh form after action
  • Use action verbs in button labels

Drush Command Development

Why CLI Commands Are Essential for AI Agents

AI agents prefer CLI commands over UI interactions for module configuration and operations.

Benefits:

  • Automation: AI agents can invoke commands directly without browser automation
  • Testability: Commands can be unit/kernel tested independently
  • Reproducibility: Same command produces same result across environments
  • Speed: No browser overhead, faster iteration during development
  • Scripting: Commands can be chained in shell scripts or CI/CD pipelines

Command Development Pattern:

namespace Drupal\mymodule\Commands;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;

#[CLI\Command(name: 'mymodule:generate-demo')]
class MyModuleCommands extends DrushCommands {

  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct();
  }

  #[CLI\Command(name: 'mymodule:generate-demo', aliases: ['mymod:demo'])]
  #[CLI\Usage(name: 'mymodule:generate-demo', description: 'Generate demo content')]
  public function generateDemo(): void {
    $this->io()->title('Generating demo content');

    try {
      // Create entities
      $count = 0;
      foreach (range(1, 5) as $i) {
        $node = $this->entityTypeManager->getStorage('node')->create([
          'type' => 'article',
          'title' => "Demo Article $i",
        ]);
        $node->save();
        $count++;
      }

      $this->io()->success("Created $count demo articles");
    }
    catch (\Exception $e) {
      $this->io()->error($e->getMessage());
      throw $e;
    }
  }
}

Essential Command Patterns for Module Configuration:

# Instead of admin UI forms, provide commands for:
drush mymodule:set-api-key <key>           # Configure API credentials
drush mymodule:enable-feature <name>       # Toggle features
drush mymodule:import-data <file>          # Bulk operations
drush mymodule:status                      # Show current configuration
drush mymodule:validate                    # Check setup completeness

Command Registration Troubleshooting:

# If command not found:
drush cr                                   # Clear cache
drush list | grep mymodule                 # Verify registration
composer dump-autoload                     # Rebuild autoloader

Testing Strategy:

  • Unit tests: Test command logic independently of Drupal
  • Kernel tests: Test command with Drupal APIs but no full bootstrap
  • Functional tests: Test command output and side effects

Performance Considerations:

  • Use batch operations for large datasets
  • Provide progress bars: $this->io()->progressStart($total)
  • Allow limiting operations: drush mymodule:import --limit=100

Command Output Best Practices

For user-facing Drush commands:

#[CLI\Command(name: 'mymodule:import')]
public function import() {
  // Auto-apply cleanup after main action
  $imported = $this->importService->import();
  $cleaned = $this->importService->cleanup(); // Auto-run

  // Detailed reporting
  $this->io()->success(sprintf(
    'Import complete: %d items imported',
    $imported
  ));

  $this->io()->text(sprintf(
    'Cleanup: %d old items removed',
    $cleaned
  ));

  // Contextual help when config incomplete
  if (empty($this->config->get('api_key'))) {
    $this->logger()->warning('API key not configured');
    $this->logger()->warning('Set at /admin/config/mymodule/settings');
  }
}

Pattern:

  • Auto-apply related operations (backup → retention, import → cleanup)
  • Detailed reporting: before/after counts, sizes, protected items
  • Proper logging levels: success/error/warning/info
  • Link to admin pages when config incomplete

Configuration Export/Snapshot Workflow

MANDATORY Operations After Changes:

# After ANY configuration change (admin UI or code):
drush config:export  # Export to config/sync directory
git add config/
git commit -m "Update configuration"

# After ANY database change (content, config, state):
ddev snapshot      # Create restore point
# Or for non-DDEV:
drush sql:dump > backup-$(date +%Y%m%d-%H%M%S).sql

Why This Matters:

  • Configuration changes are lost without export
  • Database changes can't be reversed without snapshots
  • Team members need exported config to sync environments
  • CI/CD pipelines require exported configuration

Configuration Architecture Understanding:

Simple Configuration (in code):

  • Format: YAML files in config/install/ or config/optional/
  • Export: Automatically included in module
  • Examples: Module settings, system variables
  • Edit via: ConfigFactory API

Configuration Entities (in database):

  • Format: Exportable to YAML via drush config:export
  • Export: To config/sync/ directory (outside module)
  • Examples: Views, node types, field configs, image styles
  • Edit via: Entity API

Common Configuration Tasks:

# After creating view in UI:
drush config:export
mv config/sync/views.view.my_view.yml modules/mymodule/config/install/
git add modules/mymodule/config/install/views.view.my_view.yml

# After changing settings form:
drush config:export  # Exports mymodule.settings.yml
git add config/sync/mymodule.settings.yml

# After creating field via UI:
drush config:export
# Multiple files exported (field, storage, display, form display)
# Move to config/install/ if part of module requirements

Snapshot Workflow:

# Before risky operations:
ddev snapshot --name="before-major-refactor"

# List snapshots:
ddev snapshot --list

# Restore if needed:
ddev snapshot --restore --name="before-major-refactor"

Hook Documentation Standards

All hook implementations must include comprehensive documentation for other developers and AI agents.

Complete PHPDoc Template:

/**
 * Implements hook_node_presave().
 *
 * Automatically generates URL aliases for nodes based on title pattern.
 *
 * This hook runs before a node is saved to the database, allowing us to
 * modify the node object or perform validation before persistence.
 *
 * Dependencies:
 * - Requires pathauto module for alias generation
 * - Expects node type to have 'generate_alias' third-party setting enabled
 *
 * Performance:
 * - Executes on every node save operation (create and update)
 * - Minimal overhead: O(1) for alias generation
 * - Consider disabling for bulk imports via $node->pathauto_perform_alias
 *
 * @param \Drupal\node\NodeInterface $node
 *   The node entity being saved.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 *   If alias generation fails.
 *
 * @see \Drupal\pathauto\PathautoGenerator::createEntityAlias()
 * @see hook_node_insert()
 *
 * @code
 * // Example: Disable alias generation for programmatic creation
 * $node = Node::create(['type' => 'article', 'title' => 'Test']);
 * $node->pathauto_perform_alias = FALSE;
 * $node->save();
 * @endcode
 */
function mymodule_node_presave(NodeInterface $node) {
  // Implementation
}

Required Documentation Elements:

  1. "Implements hook_*": Standard Drupal convention
  2. One-line summary: What does this hook do?
  3. Detailed explanation: Why does this exist? What problem does it solve?
  4. Dependencies: Other modules or configuration required
  5. Performance notes: When does it run? Cost? Optimization tips?
  6. @param tags: Document all parameters with types
  7. @throws tags: Document exceptions that might be thrown
  8. @see tags: Link to related functions, hooks, documentation
  9. @code examples: Show how other developers can interact with this

AI Agent Benefits:

  • AI can understand hook purpose without reading implementation
  • Performance notes guide AI decisions about optimization
  • @code examples show AI how to properly invoke or bypass hook
  • Dependencies help AI understand module relationships

Event & Event Subscriber Documentation Standards

All custom events and event subscribers must include comprehensive documentation for other developers and AI agents.

Event Class Documentation Template

namespace Drupal\mymodule\Event;

use Drupal\Core\Entity\EntityInterface;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;

/**
 * Event fired when an API access occurs.
 *
 * This event allows other modules to log, modify, or react to API access
 * requests. It is dispatched after security checks pass but before the
 * response is generated.
 *
 * Use cases:
 * - Analytics and access logging
 * - Rate limiting or throttling
 * - Dynamic permission checks
 * - Response modification or caching
 *
 * @see \Drupal\mymodule\Controller\ApiController::viewEntity()
 */
final class MyModuleApiAccessEvent extends Event {

  /**
   * Constructs a MyModuleApiAccessEvent object.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being accessed via API.
   * @param string $format
   *   The requested format (json, xml, html).
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   */
  public function __construct(
    protected EntityInterface $entity,
    protected string $format,
    protected Request $request,
  ) {}

  /**
   * Gets the entity being accessed.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The entity.
   */
  public function getEntity(): EntityInterface {
    return $this->entity;
  }

  // Additional methods...
}

Event Subscriber Documentation Template

namespace Drupal\mymodule\EventSubscriber;

use Drupal\another_module\Event\MyModuleApiAccessEvent;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Logs API access events for analytics.
 *
 * This subscriber records all API access attempts to help administrators
 * monitor usage patterns and identify potential issues.
 *
 * Priority: 0 (default) - runs after security checks but before response
 * generation. Adjust priority if you need to run before/after other subscribers.
 *
 * Dependencies:
 * - another_module must be enabled
 * - Database logging must be configured
 *
 * Performance:
 * - Database write on every API access
 * - Consider batch logging for high-traffic sites
 * - Use queue worker for async processing if needed
 *
 * @see \Drupal\another_module\Event\MyModuleApiAccessEvent
 */
class ApiAccessLogger implements EventSubscriberInterface {

  /**
   * Constructs an ApiAccessLogger object.
   *
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory service.
   */
  public function __construct(
    protected LoggerChannelFactoryInterface $loggerFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Priority 0 means default order (after security, before response)
    return [
      MyModuleApiAccessEvent::class => ['onApiAccess', 0],
    ];
  }

  /**
   * Responds to API access events.
   *
   * Logs entity ID, type, format, and user information for analytics.
   *
   * @param \Drupal\another_module\Event\MyModuleApiAccessEvent $event
   *   The event object.
   */
  public function onApiAccess(MyModuleApiAccessEvent $event): void {
    $entity = $event->getEntity();
    $this->loggerFactory->get('mymodule.api')
      ->info('API access: @type @id in @format', [
        '@type' => $entity->getEntityTypeId(),
        '@id' => $entity->id(),
        '@format' => $event->getFormat(),
      ]);
  }
}

Event Dispatch Documentation

When dispatching events in controllers or services, document the event:

/**
 * Returns entity data in requested format.
 *
 * Dispatches MyModuleApiAccessEvent before generating response to allow
 * other modules to log access, modify output, or implement rate limiting.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity to return.
 * @param string $format
 *   The output format (json, xml, html).
 *
 * @return \Symfony\Component\HttpFoundation\Response
 *   The formatted response.
 *
 * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
 *   If access is denied by event subscriber.
 */
public function viewEntity(EntityInterface $entity, string $format): Response {
  // Dispatch event for logging/analytics
  $event = new MyModuleApiAccessEvent($entity, $format, $this->request);
  $this->eventDispatcher->dispatch($event);

  // Generate and return response
  return $this->formatEntity($entity, $format);
}

Required Documentation Elements

For Event Classes:

  1. Purpose statement: What triggers this event and why it exists
  2. Use cases: 3-5 specific examples of what subscribers might do
  3. Dispatch location: @see tag to where event is dispatched
  4. Constructor docs: Document all properties passed to event
  5. Getter methods: Document return types and what data they provide

For Event Subscribers:

  1. Purpose statement: What this subscriber does and why
  2. Priority explanation: Why this priority value was chosen
  3. Dependencies: Required modules, services, or configuration
  4. Performance notes: Cost, optimization strategies, scaling considerations
  5. Method docs: Document event parameter and implementation details

For Event Dispatch:

  1. Document in method PHPDoc: Mention event is dispatched
  2. Explain timing: When event fires (before/after what operation)
  3. List event capabilities: What can subscribers do with this event

AI Agent Benefits:

  • AI can identify extension points without reading all controller code
  • Performance notes guide decisions about subscriber priority and async processing
  • Use case examples help AI suggest relevant event subscriptions
  • Dependency documentation helps AI understand module relationships
  • Priority information guides AI when creating new subscribers

Multi-Remote Git Workflow

When maintaining contrib modules on both drupal.org and GitHub, always push to both remotes to prevent desynchronization.

Standard Push Workflow:

# After commits, push to both remotes:
git push origin 2.0.x          # Push to drupal.org
git push github 2.0.x          # Push to GitHub mirror

# For new branches:
git push -u origin 2.0.x       # Set upstream on drupal.org
git push -u github 2.0.x       # Set upstream on GitHub

Why Push to All Remotes:

  • Prevents diverging histories
  • Keeps GitHub mirror up to date for collaboration
  • Ensures drupal.org has canonical version
  • Allows GitHub-based CI/CD to run on latest code

Automated Push Script:

# .git/hooks/post-commit or git alias
#!/bin/bash
BRANCH=$(git branch --show-current)
git push origin "$BRANCH"
git push github "$BRANCH"
echo "Pushed to drupal.org and GitHub"

Verification:

# Check remote URLs:
git remote -v

# Verify both remotes have same commits:
git ls-remote origin
git ls-remote github

Remote Gist Backup (MANDATORY)

CRITICAL: Keep AGENTS.md synchronized to GitHub Gist for AI agent accessibility.

AGENTS.md is published and maintained at:

When to Update:

  1. After major AGENTS.md changes (new sections, significant rewrites)
  2. Before pushing to drupal.org (ensure gist is current)
  3. After sessions that add significant guidance or patterns
  4. When migrating workspace (keep backup accessible)

Rationale: The gist serves as:

  • Backup: Protected against accidental deletion or workspace loss
  • Reference: Shareable link for AI agents and new developers
  • History: Version tracking via GitHub gist revision history
  • Discovery: Publicly indexed for SEO and community reference

Update Command:

cd /home/martinus/ddev-projects/port

# Run security scan first (see Sensitive Data Guard below)
# Update the gist with latest AGENTS.md
gh gist edit 7a8554a77d5769703a9fdec4c69a7410 \
  --filename "AGENTS-drupal-porting.md" < AGENTS.md

# Verify the update
gh gist view 7a8554a77d5769703a9fdec4c69a7410 --raw | head -20

AI Agent Duty:

After implementing AGENTS.md changes:

  • Update the gist if:
    • New sections added (branch management, TODO structure, etc.)
    • Significant rewrites or restructuring
    • Critical guidance added from real porting experience
  • Skip update if:
    • Only minor typo fixes or formatting
    • Changes to workspace-specific file paths only
    • Temporary documentation tweaks

When requested by user:

  • Always update the gist immediately (after security check)
  • Verify the update completed successfully
  • Confirm with user that gist is now current

Post-Update Verification:

# Compare local vs remote
diff <(cat AGENTS.md) <(gh gist view 4b3e574fd3babe3b2d2dcc63c44b1877 --raw)

# If no output, files are identical ✓

See Also:

Sensitive Data Guard (MANDATORY FOR PUBLIC SHARING)

Before any public sharing, gist update, or external reference of AGENTS.md:

Run security scan:

# Check for passwords, tokens, API keys, credentials
grep -i "password\|secret\|token\|key\|credential\|api_key\|ssh\|private\|auth" AGENTS.md

# Check for email addresses
grep -E "[a-z]+@[a-z]" AGENTS.md

# Check for internal URLs
grep -E "127\.0\.0\.1|localhost:[0-9]{4,5}|http.*localhost" AGENTS.md

AI Agent Behavior:

  1. Always scan before gist update - Report findings to user

  2. Block update if sensitive data detected - Ask user to review and sanitize

  3. Approved data (workspace-specific):

    • Username: lolandese
    • Local paths: /home/martinus/ddev-projects/port/*
    • Generic examples with placeholders: <key>, <value>, [name]
  4. Report format:

✅ Security Check Complete
- No passwords, API keys, or tokens detected
- No email addresses beyond approved username
- No internal/private URLs found
- Safe to share publicly ✓
  1. If sensitive data found:
❌ Sensitive Data Detected - DO NOT UPDATE GIST

Found issues:
- [Line X]: Actual API key exposed
- [Line Y]: Database credentials

Action: User must review and sanitize before proceeding

Rationale: AGENTS.md is public and accessible. Workspace paths and username are acceptable; credentials are not.

Code Quality Standards

Required Standards:

  • Adhere to Drupal coding standards (PSR-12 with Drupal extensions)
  • Indentation: 2 spaces (no tabs)
  • Line length: ≤ 80 characters (flexible for readability)
  • Naming: CamelCase classes/methods, snake_case variables/functions
  • Braces: Always use braces; prefer early returns
  • PHPDoc: Full blocks with @param, @return, @throws
  • Type hints: Always use type hints for parameters and return values

Linting Commands:

# Check coding standards
vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/
vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/

# Auto-fix issues
vendor/bin/phpcbf --standard=Drupal src/

# Static analysis
vendor/bin/phpstan analyze src/

Reject any code that fails Drupal Coder sniffs.

Code Quality Examples

// ✅ GOOD: Modern Drupal 10/11 service injection
// Using PHP 8.0+ constructor property promotion
class MyModuleService {
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager, // Property declared and assigned
    protected MessengerInterface $messenger,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('messenger'),
    );
  }
}

// ✅ ACCEPTABLE in D8/9 branch: Deprecated function with documentation
function mymodule_do_something() {
  // Using entity_load() which is deprecated in D9, removed in D10.
  // Acceptable in 8.x branch as it works in latest D9.
  // D10/11 branch uses EntityTypeManager service instead.
  // @see https://www.drupal.org/node/2266845
  $entities = entity_load('node', [1, 2, 3]);

  // Using drupal_set_message() deprecated in D8.5, removed in D10.
  // Acceptable in 8.x branch.
  // D10/11 branch uses messenger service instead.
  // @see https://www.drupal.org/node/2774931
  drupal_set_message('Done!');
}

// ❌ BAD: Deprecated function without explanation
function mymodule_bad_example() {
  $entities = entity_load('node', [1, 2, 3]);
  drupal_set_message('Done!');
}

Deprecated Function Documentation Standard

When using deprecated functions in D8/9 branch:

/**
 * Process nodes using legacy API.
 *
 * Note: This implementation uses deprecated functions that work in Drupal 9
 * but are removed in Drupal 10. See 10.x branch for modern implementation.
 */
function mymodule_process_nodes(array $nids) {
  // @deprecated entity_load() is deprecated in Drupal 8.0.0, removed in 10.0.0.
  // Use \Drupal::entityTypeManager()->getStorage('node')->loadMultiple() instead.
  // Keeping this in 8.x branch as it works reliably in D9.
  // @see https://www.drupal.org/node/2266845
  $nodes = entity_load('node', $nids);

  foreach ($nodes as $node) {
    // Processing logic...

    // @deprecated drupal_set_message() deprecated in 8.5.0, removed in 10.0.0.
    // Use messenger service in 10.x branch.
    // @see https://www.drupal.org/node/2774931
    drupal_set_message(t('Processed @title', ['@title' => $node->label()]));
  }
}

Required Modernizations

1. Hook to Service Migration

  • Convert hook implementations to event subscribers where appropriate
  • Move business logic from .module files to services
  • Use dependency injection instead of global functions

2. Plugin System

  • Migrate custom hook systems to plugins (Block, Field, Action, etc.)
  • Use annotations for plugin discovery
  • Implement proper plugin interfaces

3. Configuration Schema

  • All configuration must have proper schema in config/schema/*.yml
  • Use configuration entities instead of variables
  • Implement proper config export/import

4. Entity API

  • Use entity type manager and storage handlers
  • Implement proper entity access control
  • Use field API for custom fields

File Structure

mymodule/
├── mymodule.info.yml          # Module metadata
├── mymodule.services.yml       # Service definitions
├── mymodule.routing.yml        # Route definitions
├── mymodule.permissions.yml    # Permission definitions
├── mymodule.module             # Hook implementations only
├── composer.json               # Dependencies
├── config/
│   ├── install/                # Default configuration
│   └── schema/                 # Configuration schemas
├── src/
│   ├── Controller/             # Controllers
│   ├── Form/                   # Forms
│   ├── Plugin/                 # Plugins
│   └── Service/                # Services
├── modules/
│   └── mymodule_demo/          # Demo submodule (REQUIRED)
│       ├── mymodule_demo.info.yml
│       ├── mymodule_demo.install
│       ├── mymodule_demo.module
│       └── config/
│           └── install/        # Demo-specific config
└── tests/
    ├── src/
    │   ├── Kernel/             # Kernel tests
    │   └── Unit/               # Unit tests
    └── modules/                # Test modules

Testing Requirements

Demo Submodule

REQUIRED for modules that:

  • Process Drupal core entities (nodes, users, taxonomy terms)
  • Provide Views that output user-facing content
  • Need realistic test data to demonstrate functionality

Optional but recommended for:

  • Utility modules without entity dependencies
  • API-only modules
  • Simple configuration modules

Demo submodules should:

  • Create temporary mock data (content, users, config) on install
  • Enable efficient manual and automated testing
  • Demonstrate module usage for new users
  • Clean up completely on uninstall

Purpose:

  1. Testing Efficiency: Pre-configured test data eliminates manual setup
  2. Development Speed: No need to recreate content after each code change
  3. User Onboarding: Living example of module capabilities
  4. Clean Removal: All demo entities tracked and removed on uninstall

Implementation:

  • Location: modules/[MODULE_NAME]_demo/
  • Track created entities in state storage
  • Use enforced dependencies in config
  • Create 2-5 demo content items initially
  • Expand progressively as features are added

See /DEMO_SUBMODULE.md for complete implementation patterns.

1. Kernel Tests

  • Required for: Core functionality, entity operations, configuration changes
  • Coverage: All service methods that interact with Drupal APIs
  • Example:
namespace Drupal\Tests\mymodule\Kernel;

use Drupal\KernelTests\KernelTestBase;

class MyModuleKernelTest extends KernelTestBase {
  protected static $modules = ['mymodule', 'node', 'user'];

  public function testMyModuleFunctionality() {
    // Test core functionality
  }
}

2. Unit Tests

  • Required for: Business logic, helper functions, data transformations
  • Coverage: Methods that don't require Drupal bootstrap
  • Example:
namespace Drupal\Tests\mymodule\Unit;

use Drupal\Tests\UnitTestCase;

class MyModuleUnitTest extends UnitTestCase {
  public function testDataProcessing() {
    // Test pure logic
  }
}

3. Test Coverage Goals

  • All public service methods must have tests
  • Critical path functionality must have kernel tests
  • Edge cases and error handling must be tested

4. Testing Best Practices

Test Organization:

tests/
├── src/
│   ├── Kernel/
│   │   ├── MyModuleServiceTest.php
│   │   └── MyModuleConfigTest.php
│   └── Unit/
│       ├── MyModuleHelperTest.php
│       └── MyModuleValidatorTest.php
└── modules/
    └── mymodule_test/  # Test-only submodule
        ├── mymodule_test.info.yml
        └── config/
            └── install/

Running Tests:

# Run all module tests
vendor/bin/phpunit web/modules/custom/mymodule

# Run specific test class
vendor/bin/phpunit web/modules/custom/mymodule/tests/src/Kernel/MyModuleServiceTest.php

# Run with coverage report
vendor/bin/phpunit --coverage-html coverage web/modules/custom/mymodule

CI/CD Integration:

  • Add .gitlab-ci.yml or .github/workflows/test.yml
  • Run tests on all supported PHP versions (8.1, 8.2, 8.3)
  • Test on both Drupal 10 and 11 if dual-version support
  • Include coding standards checks (phpcs)
  • Include static analysis (phpstan)

Git Workflow

Branch Management for Ports (CRITICAL)

⚠️ CRITICAL: Always change branches immediately after cloning/copying for a port or back-port.

When starting a port or back-port from an existing repository in another version, the first action must be to create/checkout a new branch. Failing to do this will result in commits being made to the wrong version branch.

Common Scenario:

# You're working on D11 (2.0.x branch)
cd ~/port/D11/web/modules/custom/themeless
git branch --show-current  # Shows: 2.0.x

# Copy to D9 for back-port
cp -r ~/port/D11/web/modules/custom/themeless ~/port/D9/web/modules/custom/themeless
cd ~/port/D9/web/modules/custom/themeless

# ⚠️ DANGER: Still on 2.0.x branch from D11!
git branch --show-current  # Still shows: 2.0.x

# ✅ REQUIRED: Change branch immediately
git checkout -b 1.0.x      # Create new branch for D8/9

Workflow:

  1. After cloning or copying module to different Drupal version directory
  2. Immediately check current branch: git branch --show-current
  3. Create appropriate branch for target version:
    • D8/9 ports: 1.0.x or 1.0.0-alpha1
    • D10/11 ports: 2.0.x or 2.0.0-alpha1
    • Issue-specific: issue-[NUMBER]-[DESCRIPTION]
  4. Verify branch changed before making any modifications
  5. Update .info.yml core_version_requirement to match target version

If Unsure About Branch Name:

  • Ask the user before creating branch
  • Consider: version scheme, drupal.org conventions, project history
  • Check existing branches on origin: git branch -r

Example Dialog:

Agent: "I need to create a branch for the D8/9 back-port. Should I use:
  1. 1.0.x (development branch)
  2. 1.0.0-alpha1 (tagged release)
  3. issue-[number]-d9-backport (issue-specific)
Which naming convention do you prefer?"

Why This Matters:

  • Prevents commits intended for D9 appearing in D11 branch
  • Avoids breaking production branches with wrong-version code
  • Maintains clear separation between version-specific development
  • Allows proper version comparison via git diff

Branch Naming

  • Format: issue-[ISSUE_NUMBER]-[SHORT_DESCRIPTION]
  • Example: issue-3456789-port-to-drupal-10

Commit Messages

Issue #3456789: Port module to Drupal 10

- Converted hook_menu to routing.yml
- Migrated variables to configuration API
- Implemented dependency injection in services
- Added kernel tests for core functionality
- Updated .info.yml with Drupal 10 compatibility

Commit Strategy

  • Atomic commits: Each commit should represent one logical change
  • Reference issues: Always include issue number in commit message
  • Explain why: Document the reason for changes, not just what changed

API Migration Patterns

Common D7 → D10/11 Migrations

All change records referenced below are from drupal.org/list-changes.

Variables to Configuration

// D7
$value = variable_get('mymodule_setting', 'default');
variable_set('mymodule_setting', $new_value);

// D10/11
$config = \Drupal::config('mymodule.settings');
$value = $config->get('setting') ?? 'default';

\Drupal::configFactory()->getEditable('mymodule.settings')
  ->set('setting', $new_value)
  ->save();

Change record: https://www.drupal.org/node/1667894

Entity Loading

// D7
$node = node_load($nid);
$nodes = node_load_multiple([1, 2, 3]);

// D10/11
$node = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->load($nid);

$nodes = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->loadMultiple([1, 2, 3]);

Change record: https://www.drupal.org/node/2266845

User Messages

// D7
drupal_set_message(t('Message'), 'status');
drupal_set_message(t('Error'), 'error');

// D10/11 (inject messenger service)
$this->messenger()->addStatus($this->t('Message'));
$this->messenger()->addError($this->t('Error'));

Change record: https://www.drupal.org/node/2774931

Forms

// D7
function mymodule_settings_form($form, &$form_state) {
  $form['setting'] = [
    '#type' => 'textfield',
    '#title' => t('Setting'),
    '#default_value' => variable_get('mymodule_setting'),
  ];
  return system_settings_form($form);
}

// D10/11
namespace Drupal\mymodule\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class SettingsForm extends ConfigFormBase {
  protected function getEditableConfigNames() {
    return ['mymodule.settings'];
  }

  public function getFormId() {
    return 'mymodule_settings_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('mymodule.settings');

    $form['setting'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Setting'),
      '#default_value' => $config->get('setting'),
    ];

    return parent::buildForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('mymodule.settings')
      ->set('setting', $form_state->getValue('setting'))
      ->save();

    parent::submitForm($form, $form_state);
  }
}

Menu to Routing

// D7 hook_menu()
function mymodule_menu() {
  $items['admin/config/mymodule'] = [
    'title' => 'My Module Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => ['mymodule_settings_form'],
    'access arguments' => ['administer mymodule'],
  ];
  return $items;
}

// D10/11 mymodule.routing.yml
mymodule.settings:
  path: '/admin/config/mymodule'
  defaults:
    _form: '\Drupal\mymodule\Form\SettingsForm'
    _title: 'My Module Settings'
  requirements:
    _permission: 'administer mymodule'

Change record: https://www.drupal.org/node/2122219

Security & Performance Guidelines

Security Requirements (Critical for Contrib)

  • Always sanitize user input: Use #plain_text for untrusted content, never raw #markup
  • CSRF protection: Forms automatically include tokens, verify in custom endpoints
  • Permissions: Implement proper access checks and route requirements (_permission)
  • SQL Injection: Use Entity Query or Database API with parameter binding, never raw SQL
  • XSS Prevention: Always use |e filter in Twig templates, validate #markup content
  • Configuration Security: Keep sensitive config out of version control
  • Access control: Check entity access before operations: $entity->access('view', $account)

Performance Best Practices

  • Render caching: Always add #cache array to render arrays
    $build['#cache'] = [
      'tags' => ['node:123', 'node_list'],
      'contexts' => ['user.roles', 'url.path'],
      'max-age' => 3600,
    ];
  • Cache tags: Use entity-based tags (node:123) or list-based tags (node_list)
  • Cache contexts: Apply user-specific contexts for personalized content
  • Lazy loading: Use #lazy_builder for expensive operations
  • Database queries: Use entity queries for better caching, avoid loading full entities when IDs suffice
  • Batch operations: Use Batch API for processing large datasets
  • Avoid premature optimization: Profile first with Webprofiler or XHProf

Code Review Checklist

Before submitting a port, verify:

For D8/9 Branch (8.x-1.x):

  • All .info files converted to .info.yml
  • core_version_requirement: ^8 || ^9 in .info.yml
  • Deprecated functions documented with inline comments
  • Works on latest stable Drupal 9.x
  • Services properly defined in .services.yml
  • Configuration has proper schema
  • Demo submodule created and tested
    • Installs cleanly with demo content
    • Uninstalls cleanly with no orphaned data
    • Demonstrates main module features
    • Used in tests where appropriate
  • Tests pass (vendor/bin/phpunit)
  • Code follows Drupal coding standards (vendor/bin/phpcs)
  • Dependencies declared in composer.json
  • README.md updated with D8/9 requirements
  • CHANGELOG.md notes which branch this is

For D10/11 Branch (10.x-1.x):

  • All .info files converted to .info.yml
  • core_version_requirement: ^10 || ^11 in .info.yml
  • No use of deprecated functions (check with PHPStan/Upgrade Status)
  • All hooks documented with proper @Hook attributes (D10+)
  • Works on latest stable Drupal 11.x
  • Services use modern patterns (constructor property promotion, etc.)
  • Configuration has proper schema
  • Demo submodule created and tested
    • Installs cleanly with demo content
    • Uninstalls cleanly with no orphaned data
    • Demonstrates main module features
    • Used in tests where appropriate
  • Tests pass (vendor/bin/phpunit)
  • Code follows Drupal coding standards (vendor/bin/phpcs)
  • Dependencies declared in composer.json
  • README.md updated with D10/11 requirements
  • CHANGELOG.md documents API differences from 8.x branch
  • UPGRADE.md created if migration from 8.x is complex

Both Branches:

  • Branch naming follows convention
  • Commits reference issue numbers
  • Ready for drupal.org review

Manual Review Flags

Tag the following for manual review with @todo MANUAL_REVIEW:

  1. Complex database queries: Custom queries may need optimization
  2. Permission changes: Ensure security is maintained
  3. Data migrations: Custom update hooks need careful testing
  4. Third-party integrations: API changes in external libraries
  5. Performance implications: Major architectural changes
  6. User-facing changes: UI/UX modifications

Version Split Recommendations

AI should recommend creating separate branches when encountering:

Definite Split Indicators:

  • Removed APIs: Required functionality removed in D10+ with no compatibility layer
  • PHP version conflicts: Code that can't work on both PHP 7.4 (D9) and PHP 8.3 (D11)
  • Major architecture changes: Significant refactoring that would break D8/9
  • Security improvements: D10/11-specific security features that can't be backported

Probable Split Indicators:

  • Hook attributes: Using D10+ hook attributes vs docblock hooks
  • Entity API changes: Significant entity handling differences
  • Service container changes: Major DI pattern differences
  • Configuration schema: Incompatible schema requirements

Hook Attributes Example (D10/11 only):

// Modern D10/11 approach with hook attributes
namespace Drupal\mymodule;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\node\NodeInterface;

class MyModuleHooks {

  #[Hook('node_presave')]
  public function nodePresave(NodeInterface $node): void {
    // Hook implementation
    $node->setTitle('Modified: ' . $node->getTitle());
  }

  #[Hook('help')]
  public function help(string $route_name): array|string {
    if ($route_name === 'help.page.mymodule') {
      return '<p>' . t('Help text here') . '</p>';
    }
    return [];
  }
}

Note: Hook attributes require Drupal 10.3+ and are not compatible with D8/9. Use traditional docblock hooks in 8.x branch.

Keep Single Branch If:

  • Compatibility layers exist: Functions work across D8-11 with proper checks
  • Minimal changes: Only .info.yml and minor updates needed
  • No breaking changes: All functionality works identically
  • Maintenance burden: Team prefers single codebase

Recommendation Format:

@todo VERSION_SPLIT: Consider splitting into 8.x and 10.x branches.

Reason: [Specific API/feature that causes issues]
Impact: [What breaks or becomes difficult]
Alternative: [If there's a way to keep single branch]

Example:
The hook_help() implementation uses hook attributes (D10+) which
aren't available in D8/9. We could:
1. Split branches (recommended) - use attributes in 10.x
2. Keep docblock hooks in both (works but not modern)

When AI recommends a split, it should:

  1. Document the reason clearly
  2. Suggest migration path from 8.x to 10.x
  3. Note which features differ between branches
  4. Propose UPGRADE.md content for users

Documentation Requirements

Code Documentation

  • All classes must have docblocks
  • All public methods must document parameters and return types
  • Complex logic must have inline comments

User Documentation

Update or create:

  • README.md: Installation and basic usage
  • INSTALL.txt: Detailed installation steps
  • CHANGELOG.md: Version history and breaking changes

Drupal.org Integration

Issue Queue Workflow

  1. Create or find existing issue for the port
  2. Comment with approach and plan
  3. Create branch: issue-[NUMBER]-[DESCRIPTION]
  4. Regular updates on progress
  5. Request review from maintainers
  6. Address feedback in new commits

Patch Generation

# Generate interdiff between versions
git diff 8.x-1.x..issue-3456789-port-to-drupal-10 > 3456789-2.patch

# Or use drupal.org's built-in comparison tools

Environment Assumptions

  • DDEV: All sites run on DDEV with consistent naming
  • SSH Access: Maintainer has push access to drupal.org repositories
  • GitHub Mirror: Parallel repository for collaboration
  • Git Remotes:
    • origin: drupal.org git repository
    • github: GitHub mirror

Debugging & Troubleshooting

Essential Debugging Commands

# View Drupal logs (primary debugging tool)
drush watchdog:show                    # Recent log messages
drush watchdog:show --severity=Error   # Filter by severity
drush watchdog:show --type=php         # Filter by type
drush watchdog:delete all              # Clear logs when huge

# System status checks
drush status                           # Verify Drupal root, DB connection
drush cr                              # Clear all caches
drush cache:rebuild                   # Alternative cache clear

# Configuration debugging
drush config:get <name>               # Show specific config
drush config:set <name> <key> <value> # Temporarily change config
drush config:export                   # Export active config
drush config:import                   # Import config

# Module and permission checks
drush pm:list --status=enabled        # List enabled modules
drush role:perm:add anonymous 'permission name'  # Grant permission
drush role:perm:list authenticated    # List role permissions

# Entity and field inspection
drush ev "\$node = \Drupal\node\Entity\Node::load(1); print_r(\$node->toArray());"

Common Issues and Solutions

Module won't enable:

  1. Check dependencies in .info.yml
  2. Run drush updatedb for schema updates
  3. Check logs: drush watchdog:show --severity=Error
  4. Verify composer dependencies installed

Configuration import fails:

  1. Check for schema errors: drush config:inspect <config.name>
  2. Ensure config/schema/*.yml exists and is valid
  3. Run drush cr before importing

Tests fail:

  1. Run specific test: vendor/bin/phpunit --group mymodule
  2. Check test environment: database, files directory permissions
  3. Verify test dependencies in composer.json
  4. Clear test cache: vendor/bin/phpunit --do-not-cache-result

Performance issues in demo:

  1. Check query count: Enable Database Logging module
  2. Profile with Webprofiler if available
  3. Verify render cache tags are set correctly
  4. Check for N+1 query problems (load entities in batch)

System Troubleshooting

When terminal commands hang:

  • Not a DDEV issue if even ls, pwd hang
  • Check for resource-intensive processes in other terminals
  • Monitor system load if accessible
  • Close competing processes
  • Restart Docker daemon: sudo systemctl restart docker

Common Pitfalls to Avoid

  1. Don't use \Drupal:: in services: Inject dependencies
  2. Don't hardcode paths: Use route generation and URL services
  3. Don't ignore configuration schema: All config needs validation
  4. Don't skip tests: They catch regressions early
  5. Don't mix concerns: Keep controllers thin, logic in services
  6. Don't forget access control: Always check permissions
  7. Don't use static caching improperly: Use cache services

Resources

Drupal.org Official Resources

AI Agent Files (General Drupal Development)

Note: Our AGENTS.md file is specifically designed for module porting and includes unique guidance not found in other resources (dual-branch strategy, deprecated function documentation, demo submodules, forward-only porting).

Version-Specific Notes

Drupal 10

  • PHP 8.1+ required
  • Symfony 6.2 components
  • CKEditor 5 (CKEditor 4 deprecated)
  • Hook event dispatcher pattern available

Drupal 11

  • PHP 8.3+ required
  • Symfony 7.0 components
  • Hook attributes (prefer over docblock hooks)
  • Strict typing encouraged
  • Constructor property promotion preferred

Success Criteria

A successful port means:

  1. ✅ All tests pass in target Drupal version
  2. ✅ No deprecated code warnings
  3. ✅ Follows modern Drupal architecture patterns
  4. ✅ Maintains or improves functionality
  5. ✅ Has comprehensive test coverage
  6. ✅ Documentation is updated
  7. ✅ Code passes quality checks (phpcs, phpstan)
  8. ✅ Ready for community review on drupal.org

AI Agent Rules for Drupal Module Porting

Project Context

This workspace contains multiple Drupal versions (D7, D8, D9, D10, D11) for porting contrib modules maintained by lolandese. The goal is to modernize modules to Drupal 10/11 standards using AI assistance.

Core Principles

1. Dual-Version Strategy

  • Two version branches:
    • 1.0.x for Drupal 8/9 (maintenance mode, use latest stable D9)
    • 2.0.x for Drupal 10/11 (active development, use latest stable D11)
  • Port forward only: If a D8 version exists, do NOT create D7 versions
  • Version-specific targeting: Each branch targets its Drupal core versions appropriately
  • When to split versions: AI should recommend splitting when:
    • API changes are too disruptive for compatibility layers
    • New features require D10/11 exclusive APIs
    • Maintaining backward compatibility introduces technical debt
    • Security or performance significantly benefits from version split

2. Modernization Priority

  • Use Drupal best practices: Embrace services, dependency injection, and plugin systems
  • Refactor old patterns: Convert procedural code to OOP where appropriate
  • Configuration management: Use YAML configuration over database storage
  • Leverage modern APIs: Use entity API, form API improvements, and render arrays properly
  • Pragmatic approach: Balance modernization with version compatibility

3. API Migration Strategy

  • Deprecated functions are ACCEPTABLE in D8/9 branch when:
    • They work in the target Drupal 9.x stable version
    • Replacing them adds unnecessary complexity
    • Compatibility layers aren't available
    • MUST document with inline comments: Why deprecated function is used, what version deprecated it, what replaces it in D10/11
  • D10/11 branch should be fully modern: No deprecated functions, use current best practices
  • Document all API differences between branches in CHANGELOG
  • Use compatibility layers when available and sensible

Decision Criteria for Deprecated Functions (D8/9 branch):

  • Modern API requires 5+ lines of DI setup → OK to use deprecated in 8.x branch
  • Modern API is direct 1:1 replacement → Use modern API even in 8.x branch
  • Modern API available as compatibility layer → Use modern API
  • Document decision with inline comment explaining trade-off

Technical Standards

Dependency Management for Contrib

Development Version Dependencies

When adding dependencies that use development versions (e.g., dev-main, 3.0.x-dev), always pin to a specific commit hash to prevent unexpected changes during composer update.

Why This Matters:

  • Development branches change frequently
  • composer update can pull in breaking changes unexpectedly
  • Pinning ensures reproducible builds across environments
  • Prevents CI/testing failures due to upstream changes

Correct Format:

{
  "require": {
    "drupal/fivestar": "3.0.x-dev#abc123def456",
    "vendor/package": "dev-main#789xyz012345"
  }
}

Incorrect Format (Avoid):

{
  "require": {
    "drupal/fivestar": "3.0.x-dev",
    "vendor/package": "dev-main"
  }
}

How to Find Commit Hashes:

# Via Git Repository
git log --oneline -n 10
# Copy the commit hash from desired commit

# Via Composer Show
composer show vendor/package --all
# Look for commit hash in version information

# Via GitHub/GitLab
# Navigate to repository → branch → copy latest commit hash

When to Update Commit Hashes:

  • Before major releases - Update to latest stable commits
  • When fixing bugs - Update if upstream fixes are needed
  • During security updates - Update immediately if security fixes available
  • Regular maintenance - Review and update quarterly

Stable Version Dependencies

For stable releases, use semantic versioning:

{
  "require": {
    "drupal/core": "^10 || ^11",
    "drupal/votingapi": "^3.0"
  }
}

Essential Porting Tools

Before starting any port, install and use these critical tools:

Upgrade Status Module

composer require drupal/upgrade_status
drush en upgrade_status -y
# Visit /admin/reports/upgrade-status or:
drush upgrade_status:analyze MODULE_NAME
  • Provides deprecation dashboard for your module
  • Identifies D10/11 compatibility issues
  • Run BEFORE starting port and AFTER each major change
  • Should show all green before considering port complete

Drupal Rector (Automated Refactoring)

composer require --dev palantirnet/drupal-rector
vendor/bin/rector init  # Generates rector.php config

# Dry run to see what would change
vendor/bin/rector process web/modules/custom/YOUR_MODULE --dry-run

# Apply automated fixes
vendor/bin/rector process web/modules/custom/YOUR_MODULE
  • Automates many deprecated API replacements
  • Handles common transformations (entity_load, drupal_set_message, etc.)
  • Run this BEFORE manual porting work
  • Saves hours of repetitive refactoring

PHPCompatibility (PHP Version Checking)

composer require --dev phpcompatibility/php-compatibility
vendor/bin/phpcs --standard=PHPCompatibility \
  --runtime-set testVersion 8.1 \
  web/modules/custom/YOUR_MODULE
  • Critical for dual-branch strategy
  • 8.x branch: Check against PHP 7.4+
  • 10.x branch: Check against PHP 8.1+
  • Identifies PHP version conflicts

Examples for Developers Module

composer require --dev drupal/examples
  • Reference implementations of Drupal APIs
  • Location: web/modules/contrib/examples/modules/
  • Consult before implementing new patterns
  • Covers hooks, plugins, forms, entities, caching, and more
  • More reliable than web search results

Automated Refactoring Workflow

ALWAYS follow this sequence:

  1. Analyze - Run Upgrade Status to identify all issues
  2. Auto-fix - Run Drupal Rector to handle common migrations
  3. Verify - Run PHPStan and PHPCS on rector output
  4. Manual - Address remaining complex migrations by hand
  5. Test - Run full test suite
  6. Re-analyze - Run Upgrade Status again (should be green)
# Complete workflow
cd ~/port/D9/web/modules/custom/YOUR_MODULE  # or D11 for 10.x

# 1. Initial analysis
drush upgrade_status:analyze YOUR_MODULE

# 2. Auto-refactor
vendor/bin/rector process . --dry-run  # Review changes first
vendor/bin/rector process .            # Apply changes

# 3. Code quality check
vendor/bin/phpcs --standard=Drupal,DrupalPractice .
vendor/bin/phpstan analyze .

# 4. Test
vendor/bin/phpunit

# 5. Final verification
drush upgrade_status:analyze YOUR_MODULE  # Should be clean

Development Best Practices

Session Learning & AGENTS.md Evolution - WITH GIST SYNC

This guide evolves based on real porting experience. After major work sessions, propose additions:

What to Document:

  • Code patterns: Reusable architectural approaches with links to actual implementations
  • Error solutions: "When X error appears, try Y" with diagnostic commands
  • Workflow patterns: Development practices that prevent bugs
  • API migration patterns: Specific D7→D10/11 transformations that worked well

What NOT to Document:

  • One-off workarounds specific to a single module
  • Obvious Drupal best practices already in official docs
  • Temporary debugging steps

Documentation Format:

// Link to real code example
See themeless/src/Controller/ThemelessController.php#L45 for
event dispatcher injection pattern in D10/11 controllers

CRITICAL: After adding significant guidance to AGENTS.md:

  1. Commit to local git with clear message explaining additions
  2. Run security scan (see Sensitive Data Guard section)
  3. Update the GitHub Gist so future AI agents have access to the guidance
  4. Document in session logs what knowledge was captured

This ensures session learnings compound across multiple AI agent interactions rather than being lost or isolated to workspace.

TODO Management

Basic Workflow

Quick Capture Approach (Capture-Only Behavior): Use @TODO, @NOTE, or @FIXME prefixes in conversation to capture tasks directly without ceremony (see Quick Note Capture with Prefixes for syntax):

@TODO: Add validation for email format in contact form
@TODO (test): Create kernel test for cache invalidation

Plan Auto-Capture (Automatic Preservation): When implementing a multi-step plan, remaining tasks are automatically captured to TODO.md to prevent losing work (see Auto-Capture of Deferred Plan Tasks for details):

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Database Schema Updates
  - #9: Step 3: Add Unit Tests
  - #10: Step 4: Performance Optimization ⚠️

CRITICAL WORKFLOW RULE:

  • When prefix is detected (@TODO, @NOTE, @FIXME), ONLY the capture action is performed
  • NO implementation begins automatically - agent stops after writing to file
  • NO continuation or follow-up work - task is tracked and nothing else happens
  • To proceed with implementation, user must explicitly say "Start implementation" or similar instruction
  • This ensures user intent is respected: capture ≠ start work

Manual Entry For User-Initiated Requests: When user explicitly says "add to TODO":

  • Only add to TODO.md - do not start implementation
  • Wait for explicit "Start Implementation" before beginning work
  • Prevents premature implementation when user wants to track ideas

AI Budget Note: Most @TODO captures consume <0.33x effort (minimal AI input required)

TODO.md Structure (Professional Template)

Purpose: LOCAL DEVELOPER USE ONLY session tracking document (excluded from git)

Workspace Versions - Which Get TODO.md Files

Per the dual-version strategy, only active development versions get TODO.md tracking:

  • D9 (1.0.x branch): ✅ Keep TODO.md - Active maintenance & bug fixes for D8/9
  • D10: ❌ Remove TODO.md - Reference/testing only, uses same 2.0.x code as D11
  • D11 (2.0.x branch): ✅ Keep TODO.md - Active development for D10/11
  • D7, D8: ❌ No TODO.md - Reference-only Drupal installations for bootstrapping

Why this matters: D9, D10, D11 are separate clones of the same drupal.org git repository. Each clone can checkout different branches (1.0.x, 2.0.x, etc.), but since D10 doesn't have a dedicated release branch (it tests D11's 2.0.x code), it shouldn't have independent development tracking. Use D11's TODO for 2.0.x feature work; use D10 only for Drupal 10-specific compatibility testing of D11's code.

File Location & Scope Organization: FOLDER LEVEL MATTERS

TODO.md files should be organized by project scope to keep work tracking aligned with code structure:

  1. Module-Specific TODO.md (Primary - recommended for most work)

    • Location: [module-root]/TODO.md (same level as .git, .gitignore, *.module files)
    • Example: /home/martinus/ddev-projects/port/D11/web/modules/custom/themeless/TODO.md
    • Scope: Tickets only for that specific module
    • Use case: Feature development, bugs, documentation for one module
  2. Version Branch TODO.md (Cross-module work)

    • Location: [drupal-version]/web/modules/custom/TODO.md (same level as all module folders)
    • Example: /home/martinus/ddev-projects/port/D11/web/modules/custom/TODO.md
    • Scope: Issues spanning multiple modules within one Drupal version
    • Use case: Version-wide refactoring, shared dependencies, compatibility issues
  3. Workspace Root TODO.md (Porting strategy & meta-work)

    • Location: /home/martinus/ddev-projects/port/TODO.md (highest level)
    • Scope: Workspace-wide porting decisions, version coordination, infrastructure
    • Use case: Meta-work about the porting process itself, decisions affecting all versions

Out-of-Scope Ticket Detection & Migration:

When capturing new tickets, detect scope mismatches and handle appropriately:

  1. Detect mismatch:

    • Ticket affects multiple modules → belongs in version-level, not module TODO.md
    • Ticket affects multiple versions → belongs in workspace root
    • Ticket is meta-work → doesn't belong in module TODO.md
  2. Agent behavior on detection:

    • Inform user: List out-of-scope tickets with current location and correct location
    • Ask permission: "Should I move these tickets to their correct TODO.md files?"
    • Migrate if approved: Move tickets to appropriate TODO.md, preserving all metadata
  3. Example notification:

⚠️ Out-of-Scope Tickets Detected in themeless/TODO.md:

- #15: Add JSON:API integration (affects multiple modules)
  Current: D11 themeless module TODO.md
  Suggested: D11/web/modules/custom/TODO.md
  Reason: Issue impacts multiple modules, not just themeless alone

- #22: Upgrade PHP 8.3 compatibility (workspace meta-work)
  Current: D11 themeless module TODO.md
  Suggested: /port/TODO.md
  Reason: Decision affects all Drupal versions

Move these tickets to correct locations? (y/n)

Create TODO.md & .gitignore if Missing:

When creating a new TODO.md at any scope level, always handle .gitignore:

  1. Create the TODO.md file at appropriate folder level
  2. Check for .gitignore at same folder level:
    • If exists: Add TODO.md, DONE.md, NOTES.md to ignore list
    • If missing: Create .gitignore with these exclusions
  3. Verify exclusion: Run git check-ignore TODO.md (should show path match)

Standard .gitignore snippet:

# Local development tracking (excluded from version control)
TODO.md
DONE.md
NOTES.md

Git Exclusion: Add to .gitignore:

TODO.md
NOTES.md

Required Sections:

  1. Warning Banner:
# [Module/Project] TODO

**⚠️ LOCAL DEVELOPER USE ONLY**

This file is for **local developer session tracking only** and is excluded from the remote repository via `.gitignore`.
  1. Guidelines Section:
  • Working notebook during active development sessions
  • Limit to current sprint or immediate work items
  • For persistent tracking, use project issue queue (drupal.org, GitHub Issues, etc.)
  • Before switching tasks, consider migrating items to issue queue
  • End of session: Clean up or keep as local reference
  1. Time & Budget Estimates Explanation:
**Time & Budget Estimates:**
- **Effort estimates** represent human developer session time (including AI interaction, code review, testing, iteration), not pure AI generation time
- **AI Budget %** estimates percentage of [AI Service] monthly limit (e.g., 300 requests/month for GitHub Copilot Pro)
- ⚠️ **Warning triangle** indicates tasks consuming >2.0% of monthly limit (>[threshold] requests)
  1. Impact & Compliance Points Explanation:
**Impact & Compliance Points:**
- **Impact** measures how completing an issue improves the module's adherence to AGENTS.md standards
- **Compliance points** are calculated by evaluating the module against AGENTS.md requirements across multiple areas:
  - Documentation quality (PHPDoc completeness, hook documentation, inline comments)
  - Test coverage (unit tests, kernel tests, functional tests, edge cases)
  - Code quality (type hints, dependency injection, coding standards)
  - Security practices (access control, input validation, test coverage)
  - Performance optimization (caching, database queries, render arrays)
- **Cumulative score** (e.g., "→ 94/100 cumulative") shows the total compliance after completing all issues in order
- Example: "+2 compliance points (→ 94/100 cumulative)" means this issue adds 2 points, resulting in 94/100 total if all previous issues are complete
- The baseline score (before TODO work) is shown in the Compliance Status section (if applicable)
  1. Categories with Emoji Tags:
**Categories:** Tag each item with one category for quick filtering:
- **🐛 bug** — Something broken or not working as expected
- **✨ feature** — New functionality to implement
- **📝 docs** — Documentation updates or guides
- **🧪 test** — Testing improvements or coverage
- **♻️ refactor** — Code quality, cleanup, or restructuring
- **⚡ performance** — Optimization and caching improvements

**Status Lifecycle:**
- **Not Started****In Progress****Blocked** (temporary)
- **solved** — Technical implementation completed
- **validated** — Implementation tested and verified
- **closed** — Issue resolved and ready for archival
  1. Table of Contents:
## Table of Contents

- [Recent Status Changes](#recent-status-changes-latest-first) (X items)
- [Outstanding Issues](#outstanding-issues) (X items)
- [Completed Tasks](#completed-tasks) (X sessions)
- [Compliance Status](#compliance-status) (if applicable)
- [For Next Session](#for-next-session)
  1. Recent Status Changes Section (NEW):
## Recent Status Changes (Latest First)

- **2026-02-16** - Issue #7: Performance Profiling & Caching Strategy → **closed**
- **2026-02-16** - Issue #6: Expand Event System Tests → **closed**
- **2026-02-15** - Issue #4: Kernel Tests for Security Features → **validated**
- **2026-02-15** - Issue #2: Helper Function Documentation → **closed**
- **2026-02-15** - Issue #1: Demo Module Hook Documentation → **closed**

Purpose: Minimal shortlist showing recent status transitions in descending chronological order for quick reference.

  1. Outstanding Issues Format:
### [Number]. [Issue Title]

- **Category:** 📝 docs
- **Module:** themeless_demo
- **Status:** Not Started | In Progress | Blocked | solved | validated | closed
- **Date Identified:** 2026-02-15
- **Priority:** High | Medium | Low
- **AGENTS.md Reference:** [Section Name] (lines X-Y)

- **Recommended Model:** 0.33x | **1x** | 3x
- **Estimated Effort:** X minutes/hours
- **AI Budget:** ~X.X% (Y requests) [add ⚠️ if >2.0%]
- **Impact:** +X compliance points (→ YY/100) or other measurable outcome

**Description:** Clear explanation of what needs to be done.

**Current State:** What exists now (optional).

**Required Changes:**
- Bullet list of specific actions
- With technical details
- And acceptance criteria

**Files to Update:**
- [path/to/file.php](path/to/file.php) - what changes
  1. DONE.md Archive Workflow (NEW):

When to Create:

  • When project has many completed items cluttering TODO.md
  • For historical reference and tracking accomplishments
  • To maintain clean active TODO while preserving implementation details

Archive Migration Rules:

  • Only closed status items move to DONE.md
  • solved and validated items remain in TODO.md Outstanding Issues for session visibility
  • Session narratives remain in TODO.md until related items are closed

DONE.md Structure:

# [Module/Project] DONE

**⚠️ LOCAL DEVELOPER USE ONLY**

This file is for **completed items archive** and is excluded from the remote repository via `.gitignore`.

**Archive Guidelines:**
- Contains all **closed** status items moved from TODO.md for historical reference
- **Streamlined format** focused on completed implementation details
- **For persistent task tracking**, use the [drupal.org issue queue] — this is the **source of truth for the community**

## Archived Issues (Latest First)

### [Number]. [Issue Title]

- **Status:** **closed** (YYYY-MM-DD)
- [Essential metadata: category, impact, files modified]

**Implementation Results:**
- Key accomplishments and outcomes
- Performance improvements or measurable benefits
- Files created/modified with brief description

## Implementation Sessions

- Preserve major session narratives from TODO.md
- Focus on implementation techniques and lessons learned
- Maintain chronological development story

Git Exclusion: Add to .gitignore:

TODO.md
DONE.md
NOTES.md
  1. Completed Tasks Format (Session-Based):
## Completed Tasks

### 2026-02-15 Session: [Session Title]

**Status:** ✅ Complete | 🚧 In Progress

**Scope:**
- What was accomplished
- Key decisions made
- Files modified count

**Changes Made:**
- [file/path.php](file/path.php) (+X lines) - what changed
- [another/file.js](another/file.js) - description

**Validation:**
- ✓ PHPCS passed
- ✓ Tests pass
- ✓ Manual testing results

**Impact:**
- Compliance: XX/100 → YY/100 (+Z points)
- Performance: Improved/No change
- Technical debt: Reduced/Added/None

**Pre-existing Issues Found:**
- Document any issues discovered but not fixed
- With quick fix estimates
  1. Compliance Status (Optional for AGENTS.md Compliance Tracking):
## Compliance Status

**Current Score:** XX/100

**Compliance Breakdown:**
- ✅ Category Name: 100% (fully compliant)
- ⚠️ Category Name: XX% (partially compliant with gaps)
- ❌ Category Name: 0% (not addressed)

**Path to 100% Compliance:**
- Issue #X + #Y: XX → YY (+Z points) - description
  1. For Next Session:
## For Next Session

**Recommended Priority:**
1. [Issue #X] - rationale (time, budget, impact)
2. [Issue #Y] - rationale

**Quick Wins (<1 hour, <2% budget):**
- Issue #X: [description] (time, budget)

**Substantial Work (>2% budget):**
- Issue #Y: [description] (time, budget) ⚠️

AI Budget Tracking Guidelines

When to Add AI Budget Estimates:

  • All issues in TODO.md should include AI budget percentage
  • Base calculation on your AI service's monthly limit (e.g., GitHub Copilot Pro: 300 requests/month)
  • 1 request ≈ 0.33% of 300-request budget
  • Include warning triangle (⚠️) for tasks >2.0% threshold

Budget Calculation Examples:

15-minute documentation task: ~3 requests = 1.0%
30-minute test creation: ~5 requests = 1.7%
1-hour implementation: ~7-8 requests = 2.3-2.7% ⚠️
2-hour refactoring: ~9 requests = 3.0% ⚠️

Threshold Recommendations:

  • 2.0% threshold: Good for separating quick wins (<30 min) from substantial work
  • Quick wins: No warning, suitable for short sessions
  • Substantial work: ⚠️ warning, requires dedicated time block

Benefits:

  • Session planning: Identify which tasks to combine in available time
  • Budget visibility: Prevent exceeding monthly AI service limits
  • Prioritization: Visual separation of quick vs complex work
  • Team coordination: Share cost estimates for collaborative planning

Example TODO.md Reference

See /home/martinus/ddev-projects/port/D11/web/modules/custom/themeless/TODO.md for complete implementation following this pattern.

LLM Model Selection Guidelines

GitHub Copilot Pro Model Tiers:

Tier selection affects actual AI request consumption against your monthly limit:

  • 0x (Free): Basic queries, syntax checks, documentation lookups - no cost to monthly limit
  • 0.33x (Economy): Documentation writing, code reading, minor refactoring - 3 requests = 1 equivalent (66% savings)
  • 1x (Standard): Code generation, tests, bug fixes, API migrations - baseline cost
  • 3x (Premium): Architecture decisions, complex refactoring, performance optimization - 3x cost per request

Model Selection by Task Complexity:

Task Type Tier Rationale
Documentation writing 0.33x PHPDoc, README updates - simpler models perform well
Code reading/explanation 0.33x Understanding existing code
Minor refactoring 0.33x Renames, formatting, simple cleanup
Feature implementation 1x New functionality requiring code generation
Test writing 1x Unit/kernel/functional tests with assertions
Bug fixes 1x Debugging and code corrections
API migrations 1x D7→D10 conversions, deprecated replacements
Architecture decisions 3x Service design, major technical choices
Complex refactoring 3x Multi-file restructuring, pattern changes
Performance optimization 3x Profiling analysis, caching strategies
Security analysis 3x Vulnerability assessment, access control review

Cost Impact Examples:

Documentation (0.33x): 9 requests = 3 equivalent = 1.0% of 300/month
Standard feature (1x): 7 requests = 7 equivalent = 2.3% of 300/month
Complex refactor (3x): 3 requests = 9 equivalent = 3.0% of 300/month

Strategy Tips:

  • Start with lower tier for initial exploration and code reading
  • Use 1x for most implementation work
  • Reserve 3x for critical decisions or when stuck after 3-4 attempts with lower tier
  • Upgrading tier after repeated failures saves budget vs. continued struggling

Quick Note Capture with Prefixes

Use @NOTE, @TODO, and @FIXME prefixes in conversation for immediate capture to local tracking files.

⚠️ AGENT BEHAVIOR: Capture-Only Rule

When an agent detects @TODO, @NOTE, or @FIXME prefix:

  1. STOP all other work immediately
  2. Capture the note to appropriate file (TODO.md, NOTES.md)
  3. DO NOT continue with implementation
  4. DO NOT start related work - wait for explicit user instruction
  5. Report only the capture action, nothing else

Examples of Correct Behavior:

CORRECT:

User: "@TODO: Create integration test for the payment workflow"
Agent: Captures to TODO.md and stops. No implementation begins.

⚠️ INCORRECT:

User: "@TODO: Create integration test for the payment workflow"
Agent: Captures to TODO.md AND THEN starts writing the test file

CORRECT:

Agent captures task. Later, user says: "Good, now let's implement that test."
Agent: Now begins implementation after explicit instruction.

Directive Reference

Prefix Destination Purpose Silent
@NOTE NOTES.md Quick observations, references, context ✓ No confirmation
@TODO TODO.md New tasks to track ✓ Auto-organized by category
@FIXME TODO.md Bugs or critical issues ✓ Marked with 🐛 category

Format

Basic usage:

@NOTE: The caching strategy works effectively here
@TODO: Add validation for email format in contact form
@FIXME: Database queries showing N+1 on user list page

With optional category tags (for @TODO/@FIXME):

@TODO (test): Create kernel test for cache invalidation
@TODO (docs): Document the event dispatch timing
@TODO (perf): Optimize entity loading in batch operations
@FIXME (bug): Fix isPublished() interface check on User entities
@FIXME (refactor): Reduce complexity in buildCacheMetadata()

Category Tags

Valid categories for @TODO/@FIXME:

  • test → 🧪 test
  • docs → 📝 docs
  • feature → ✨ feature
  • bug → 🐛 bug
  • refactor → ♻️ refactor
  • perf → ⚡ performance

Usage Examples During Development

Quick observation:

@NOTE: The service injection pattern here could be extracted into a trait

Feature suggestion:

@TODO (feature): Add webhook support for real-time updates

Performance issue:

@FIXME (perf): Avatar images loading synchronously - use lazy load

Testing gap:

@TODO (test): Add integration test for complete workflow across modules

Integration with Workflow

  1. Capture during development: Use prefixes naturally in conversation
  2. Auto-organization: Appended with timestamp and context
  3. Session tracking: Becomes part of session history in TODO.md
  4. Review before commit: Check TODO.md before pushing for items to address
  5. Migration: Move critical items to drupal.org issue queue for persistence

Notes.md vs TODO.md

  • NOTES.md: Transient observations, reference material, session notes (excluded from git)
  • TODO.md: Actionable work items, bugs, feature requests (local dev tracking, excluded from git)

Auto-Capture of Deferred Plan Tasks

Automatic preservation of planned work that isn't immediately started.

When an agent generates a multi-step plan and the user begins implementation of the first step without immediately proceeding to subsequent steps, the remaining planned tasks are automatically captured to TODO.md to prevent losing work.

When Auto-Capture Triggers

Auto-capture occurs when:

  1. Agent presents multi-step plan with numbered steps (1, 2, 3, 4...)
  2. User starts implementation of step #1 only (e.g., "Start implementing step 1", "Begin work on the first task")
  3. Remaining steps (#2, #3, #4...) were proposed but not selected
  4. Chat context preservation needed - subsequent steps would scroll above fold or be lost during execution

Auto-capture triggers automatically without requiring user approval to reduce friction.

Auto-capture does NOT trigger when:

  • All plan steps are started simultaneously
  • User explicitly defers entire plan ("Save for next session")
  • User explicitly skips remaining steps
  • Plan was rejected outright

Capture Format for Plan-Derived Tasks

Remaining plan steps are captured using YAML frontmatter format with auto-populated fields:

---
id: [auto-incremented from existing TODO.md issues]
title: [exact wording from plan step]
category: [inferred from description or explicit plan annotation]
status: Not Started
date_created: YYYY-MM-DD
effort: [preserved from plan estimate or inferred from description]
budget: [calculated from effort estimate]
impact: [preserved from plan or estimated]
blocked_by: [#1] (previous step, if dependent) or []
priority: [High|Medium|Low from plan or "Medium" default]
source: Auto-captured from plan step N
---

**Description:** Original plan step wording.

**Plan Context:** References to the parent plan for context during later execution.

Notification to User

After capturing deferred tasks, agent provides scannable notification:

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Database Schema Updates (effort: 1x)
  - #9: Step 3: Add Unit Tests (effort: 1x)
  - #10: Step 4: Performance Optimization (effort: 3x) ⚠️

View them anytime: cat TODO.md | grep -A 5 "^### [0-9]"

Notification content:

  • Task IDs and titles from plan steps
  • Effort estimate for each item
  • Destination: TODO.md with file path
  • Warning triangle (⚠️) for high-effort items (>2.0% budget)
  • Practical instruction for user to view captured tasks

Placement: After main response content, before closing remarks.

Task Dependency Handling

When plan steps have implicit dependencies:

  • Current step (#1): Captures blocked_by: [] (starts immediately)
  • Dependent steps (#2+): Captures blocked_by: [#1] or [#1, #2] if sequential
  • User can manually adjust dependencies in TODO.md if plan was unclear
  • When completing step #1, user can update dependent task status: blocked_by: []

User Workflow After Capture

  1. Begin step #1 - agent automatically captures remaining steps to TODO.md
  2. Complete step #1 - test, document, commit
  3. Review captured tasks - check TODO.md to see what's next
  4. Update blocking status - if pursuing step #2, change blocked_by: [#1]blocked_by: []
  5. Start step #2 - exact same format as step #1 execution
  6. Repeat for remaining steps

Example Workflow

Initial Plan Presented:

## Implementation Plan

1. Create database schema updates
2. Generate migration file
3. Add kernel tests
4. Performance optimization

User Response: "Start implementation"

Agent Action:

Implementing step 1: Create database schema updates...
[implementation code and output]

📋 Captured 3 remaining plan tasks to TODO.md:
  - #8: Step 2: Generate migration file (effort: 0.33x)
  - #9: Step 3: Add kernel tests (effort: 1x)
  - #10: Step 4: Performance optimization (effort: 3x) ⚠️

User Later: "Let's do step 2"

Agent: Retrieves task #8 from TODO.md, updates status/blocked_by, and implements step 2.

Prefer Drupal Built-in APIs (Critical for Contrib)

Before implementing any feature:

  1. Search Drupal's core modules and APIs for existing solutions
  2. Check contrib modules: drush pm:list | grep <feature>
  3. Consult Examples module before implementing new patterns
  4. Use Form API #states, #ajax instead of custom JavaScript
  5. Leverage service container and core APIs
  6. Document why when custom code is necessary

Comprehensive Error Messaging

When errors occur, provide context and solutions:

✗ Module installation failed: Configuration schema missing

See AGENTS.md section "Configuration Schema" for solutions
Quick checklist:
  1) Does config/schema/<module>.schema.yml exist?
  2) Run: drush config:inspect <module>.settings
  3) Check: /admin/reports/status for schema errors

Pattern:

  • Include diagnostic info (what failed, why, context)
  • Provide solution steps with exact commands
  • Link to documentation sections
  • Add quick self-diagnosis checklists

Form Action Button Pattern (for Admin UIs)

For forms with manual action triggers (e.g., "Generate Demo Content", "Migrate Now"):

// Status section first (weight -5)
$form['status'] = [
  '#type' => 'fieldset',
  '#title' => $this->t('Current Status'),
  '#weight' => -5,
];

$form['status']['info'] = [
  '#markup' => '<p>Demo content: 3 nodes created</p>',
];

// Action buttons in status section
$form['status']['actions'] = [
  '#type' => 'container',
  '#attributes' => ['class' => ['form-actions']],
];

$form['status']['actions']['create_demo'] = [
  '#type' => 'submit',
  '#value' => $this->t('Generate Demo Content'),
  '#submit' => ['::submitCreateDemo'],
  '#button_type' => 'primary',
];

// Submit handler
public function submitCreateDemo(array &$form, FormStateInterface $form_state) {
  try {
    $count = $this->demoService->createContent();
    $this->messenger()->addStatus($this->t('Created @count demo items', ['@count' => $count]));
    $form_state->setRebuild(TRUE); // Refresh form to show updated status
  }
  catch (\Exception $e) {
    $this->messenger()->addError($this->t('Failed: @message', ['@message' => $e->getMessage()]));
  }
}

Key points:

  • Status section shows current state before settings
  • Per-button submit handlers via #submit
  • Try-catch with messenger feedback
  • Call setRebuild(TRUE) to refresh form after action
  • Use action verbs in button labels

Drush Command Development

Why CLI Commands Are Essential for AI Agents

AI agents prefer CLI commands over UI interactions for module configuration and operations.

Benefits:

  • Automation: AI agents can invoke commands directly without browser automation
  • Testability: Commands can be unit/kernel tested independently
  • Reproducibility: Same command produces same result across environments
  • Speed: No browser overhead, faster iteration during development
  • Scripting: Commands can be chained in shell scripts or CI/CD pipelines

Command Development Pattern:

namespace Drupal\mymodule\Commands;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;

#[CLI\Command(name: 'mymodule:generate-demo')]
class MyModuleCommands extends DrushCommands {

  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct();
  }

  #[CLI\Command(name: 'mymodule:generate-demo', aliases: ['mymod:demo'])]
  #[CLI\Usage(name: 'mymodule:generate-demo', description: 'Generate demo content')]
  public function generateDemo(): void {
    $this->io()->title('Generating demo content');

    try {
      // Create entities
      $count = 0;
      foreach (range(1, 5) as $i) {
        $node = $this->entityTypeManager->getStorage('node')->create([
          'type' => 'article',
          'title' => "Demo Article $i",
        ]);
        $node->save();
        $count++;
      }

      $this->io()->success("Created $count demo articles");
    }
    catch (\Exception $e) {
      $this->io()->error($e->getMessage());
      throw $e;
    }
  }
}

Essential Command Patterns for Module Configuration:

# Instead of admin UI forms, provide commands for:
drush mymodule:set-api-key <key>           # Configure API credentials
drush mymodule:enable-feature <name>       # Toggle features
drush mymodule:import-data <file>          # Bulk operations
drush mymodule:status                      # Show current configuration
drush mymodule:validate                    # Check setup completeness

Command Registration Troubleshooting:

# If command not found:
drush cr                                   # Clear cache
drush list | grep mymodule                 # Verify registration
composer dump-autoload                     # Rebuild autoloader

Testing Strategy:

  • Unit tests: Test command logic independently of Drupal
  • Kernel tests: Test command with Drupal APIs but no full bootstrap
  • Functional tests: Test command output and side effects

Performance Considerations:

  • Use batch operations for large datasets
  • Provide progress bars: $this->io()->progressStart($total)
  • Allow limiting operations: drush mymodule:import --limit=100

Command Output Best Practices

For user-facing Drush commands:

#[CLI\Command(name: 'mymodule:import')]
public function import() {
  // Auto-apply cleanup after main action
  $imported = $this->importService->import();
  $cleaned = $this->importService->cleanup(); // Auto-run

  // Detailed reporting
  $this->io()->success(sprintf(
    'Import complete: %d items imported',
    $imported
  ));

  $this->io()->text(sprintf(
    'Cleanup: %d old items removed',
    $cleaned
  ));

  // Contextual help when config incomplete
  if (empty($this->config->get('api_key'))) {
    $this->logger()->warning('API key not configured');
    $this->logger()->warning('Set at /admin/config/mymodule/settings');
  }
}

Pattern:

  • Auto-apply related operations (backup → retention, import → cleanup)
  • Detailed reporting: before/after counts, sizes, protected items
  • Proper logging levels: success/error/warning/info
  • Link to admin pages when config incomplete

Configuration Export/Snapshot Workflow

MANDATORY Operations After Changes:

# After ANY configuration change (admin UI or code):
drush config:export  # Export to config/sync directory
git add config/
git commit -m "Update configuration"

# After ANY database change (content, config, state):
ddev snapshot      # Create restore point
# Or for non-DDEV:
drush sql:dump > backup-$(date +%Y%m%d-%H%M%S).sql

Why This Matters:

  • Configuration changes are lost without export
  • Database changes can't be reversed without snapshots
  • Team members need exported config to sync environments
  • CI/CD pipelines require exported configuration

Configuration Architecture Understanding:

Simple Configuration (in code):

  • Format: YAML files in config/install/ or config/optional/
  • Export: Automatically included in module
  • Examples: Module settings, system variables
  • Edit via: ConfigFactory API

Configuration Entities (in database):

  • Format: Exportable to YAML via drush config:export
  • Export: To config/sync/ directory (outside module)
  • Examples: Views, node types, field configs, image styles
  • Edit via: Entity API

Common Configuration Tasks:

# After creating view in UI:
drush config:export
mv config/sync/views.view.my_view.yml modules/mymodule/config/install/
git add modules/mymodule/config/install/views.view.my_view.yml

# After changing settings form:
drush config:export  # Exports mymodule.settings.yml
git add config/sync/mymodule.settings.yml

# After creating field via UI:
drush config:export
# Multiple files exported (field, storage, display, form display)
# Move to config/install/ if part of module requirements

Snapshot Workflow:

# Before risky operations:
ddev snapshot --name="before-major-refactor"

# List snapshots:
ddev snapshot --list

# Restore if needed:
ddev snapshot --restore --name="before-major-refactor"

Hook Documentation Standards

All hook implementations must include comprehensive documentation for other developers and AI agents.

Complete PHPDoc Template:

/**
 * Implements hook_node_presave().
 *
 * Automatically generates URL aliases for nodes based on title pattern.
 *
 * This hook runs before a node is saved to the database, allowing us to
 * modify the node object or perform validation before persistence.
 *
 * Dependencies:
 * - Requires pathauto module for alias generation
 * - Expects node type to have 'generate_alias' third-party setting enabled
 *
 * Performance:
 * - Executes on every node save operation (create and update)
 * - Minimal overhead: O(1) for alias generation
 * - Consider disabling for bulk imports via $node->pathauto_perform_alias
 *
 * @param \Drupal\node\NodeInterface $node
 *   The node entity being saved.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 *   If alias generation fails.
 *
 * @see \Drupal\pathauto\PathautoGenerator::createEntityAlias()
 * @see hook_node_insert()
 *
 * @code
 * // Example: Disable alias generation for programmatic creation
 * $node = Node::create(['type' => 'article', 'title' => 'Test']);
 * $node->pathauto_perform_alias = FALSE;
 * $node->save();
 * @endcode
 */
function mymodule_node_presave(NodeInterface $node) {
  // Implementation
}

Required Documentation Elements:

  1. "Implements hook_*": Standard Drupal convention
  2. One-line summary: What does this hook do?
  3. Detailed explanation: Why does this exist? What problem does it solve?
  4. Dependencies: Other modules or configuration required
  5. Performance notes: When does it run? Cost? Optimization tips?
  6. @param tags: Document all parameters with types
  7. @throws tags: Document exceptions that might be thrown
  8. @see tags: Link to related functions, hooks, documentation
  9. @code examples: Show how other developers can interact with this

AI Agent Benefits:

  • AI can understand hook purpose without reading implementation
  • Performance notes guide AI decisions about optimization
  • @code examples show AI how to properly invoke or bypass hook
  • Dependencies help AI understand module relationships

Event & Event Subscriber Documentation Standards

All custom events and event subscribers must include comprehensive documentation for other developers and AI agents.

Event Class Documentation Template

namespace Drupal\mymodule\Event;

use Drupal\Core\Entity\EntityInterface;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;

/**
 * Event fired when an API access occurs.
 *
 * This event allows other modules to log, modify, or react to API access
 * requests. It is dispatched after security checks pass but before the
 * response is generated.
 *
 * Use cases:
 * - Analytics and access logging
 * - Rate limiting or throttling
 * - Dynamic permission checks
 * - Response modification or caching
 *
 * @see \Drupal\mymodule\Controller\ApiController::viewEntity()
 */
final class MyModuleApiAccessEvent extends Event {

  /**
   * Constructs a MyModuleApiAccessEvent object.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being accessed via API.
   * @param string $format
   *   The requested format (json, xml, html).
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   */
  public function __construct(
    protected EntityInterface $entity,
    protected string $format,
    protected Request $request,
  ) {}

  /**
   * Gets the entity being accessed.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The entity.
   */
  public function getEntity(): EntityInterface {
    return $this->entity;
  }

  // Additional methods...
}

Event Subscriber Documentation Template

namespace Drupal\mymodule\EventSubscriber;

use Drupal\another_module\Event\MyModuleApiAccessEvent;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Logs API access events for analytics.
 *
 * This subscriber records all API access attempts to help administrators
 * monitor usage patterns and identify potential issues.
 *
 * Priority: 0 (default) - runs after security checks but before response
 * generation. Adjust priority if you need to run before/after other subscribers.
 *
 * Dependencies:
 * - another_module must be enabled
 * - Database logging must be configured
 *
 * Performance:
 * - Database write on every API access
 * - Consider batch logging for high-traffic sites
 * - Use queue worker for async processing if needed
 *
 * @see \Drupal\another_module\Event\MyModuleApiAccessEvent
 */
class ApiAccessLogger implements EventSubscriberInterface {

  /**
   * Constructs an ApiAccessLogger object.
   *
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory service.
   */
  public function __construct(
    protected LoggerChannelFactoryInterface $loggerFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Priority 0 means default order (after security, before response)
    return [
      MyModuleApiAccessEvent::class => ['onApiAccess', 0],
    ];
  }

  /**
   * Responds to API access events.
   *
   * Logs entity ID, type, format, and user information for analytics.
   *
   * @param \Drupal\another_module\Event\MyModuleApiAccessEvent $event
   *   The event object.
   */
  public function onApiAccess(MyModuleApiAccessEvent $event): void {
    $entity = $event->getEntity();
    $this->loggerFactory->get('mymodule.api')
      ->info('API access: @type @id in @format', [
        '@type' => $entity->getEntityTypeId(),
        '@id' => $entity->id(),
        '@format' => $event->getFormat(),
      ]);
  }
}

Event Dispatch Documentation

When dispatching events in controllers or services, document the event:

/**
 * Returns entity data in requested format.
 *
 * Dispatches MyModuleApiAccessEvent before generating response to allow
 * other modules to log access, modify output, or implement rate limiting.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity to return.
 * @param string $format
 *   The output format (json, xml, html).
 *
 * @return \Symfony\Component\HttpFoundation\Response
 *   The formatted response.
 *
 * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
 *   If access is denied by event subscriber.
 */
public function viewEntity(EntityInterface $entity, string $format): Response {
  // Dispatch event for logging/analytics
  $event = new MyModuleApiAccessEvent($entity, $format, $this->request);
  $this->eventDispatcher->dispatch($event);

  // Generate and return response
  return $this->formatEntity($entity, $format);
}

Required Documentation Elements

For Event Classes:

  1. Purpose statement: What triggers this event and why it exists
  2. Use cases: 3-5 specific examples of what subscribers might do
  3. Dispatch location: @see tag to where event is dispatched
  4. Constructor docs: Document all properties passed to event
  5. Getter methods: Document return types and what data they provide

For Event Subscribers:

  1. Purpose statement: What this subscriber does and why
  2. Priority explanation: Why this priority value was chosen
  3. Dependencies: Required modules, services, or configuration
  4. Performance notes: Cost, optimization strategies, scaling considerations
  5. Method docs: Document event parameter and implementation details

For Event Dispatch:

  1. Document in method PHPDoc: Mention event is dispatched
  2. Explain timing: When event fires (before/after what operation)
  3. List event capabilities: What can subscribers do with this event

AI Agent Benefits:

  • AI can identify extension points without reading all controller code
  • Performance notes guide decisions about subscriber priority and async processing
  • Use case examples help AI suggest relevant event subscriptions
  • Dependency documentation helps AI understand module relationships
  • Priority information guides AI when creating new subscribers

Multi-Remote Git Workflow

When maintaining contrib modules on both drupal.org and GitHub, always push to both remotes to prevent desynchronization.

Standard Push Workflow:

# After commits, push to both remotes:
git push origin 2.0.x          # Push to drupal.org
git push github 2.0.x          # Push to GitHub mirror

# For new branches:
git push -u origin 2.0.x       # Set upstream on drupal.org
git push -u github 2.0.x       # Set upstream on GitHub

Why Push to All Remotes:

  • Prevents diverging histories
  • Keeps GitHub mirror up to date for collaboration
  • Ensures drupal.org has canonical version
  • Allows GitHub-based CI/CD to run on latest code

Automated Push Script:

# .git/hooks/post-commit or git alias
#!/bin/bash
BRANCH=$(git branch --show-current)
git push origin "$BRANCH"
git push github "$BRANCH"
echo "Pushed to drupal.org and GitHub"

Verification:

# Check remote URLs:
git remote -v

# Verify both remotes have same commits:
git ls-remote origin
git ls-remote github

Remote Gist Backup (MANDATORY)

CRITICAL: Keep AGENTS.md synchronized to GitHub Gist for AI agent accessibility.

AGENTS.md is published and maintained at:

When to Update:

  1. After major AGENTS.md changes (new sections, significant rewrites)
  2. Before pushing to drupal.org (ensure gist is current)
  3. After sessions that add significant guidance or patterns
  4. When migrating workspace (keep backup accessible)

Rationale: The gist serves as:

  • Backup: Protected against accidental deletion or workspace loss
  • Reference: Shareable link for AI agents and new developers
  • History: Version tracking via GitHub gist revision history
  • Discovery: Publicly indexed for SEO and community reference

Update Command:

cd /home/martinus/ddev-projects/port

# Run security scan first (see Sensitive Data Guard below)
# Update the gist with latest AGENTS.md
gh gist edit 4b3e574fd3babe3b2d2dcc63c44b1877 \
  --filename "AGENTS-drupal-porting.md" < AGENTS.md

# Verify the update
gh gist view 4b3e574fd3babe3b2d2dcc63c44b1877 --raw | head -20

AI Agent Duty:

After implementing AGENTS.md changes:

  • Update the gist if:
    • New sections added (branch management, TODO structure, etc.)
    • Significant rewrites or restructuring
    • Critical guidance added from real porting experience
  • Skip update if:
    • Only minor typo fixes or formatting
    • Changes to workspace-specific file paths only
    • Temporary documentation tweaks

When requested by user:

  • Always update the gist immediately (after security check)
  • Verify the update completed successfully
  • Confirm with user that gist is now current

Post-Update Verification:

# Compare local vs remote
diff <(cat AGENTS.md) <(gh gist view 4b3e574fd3babe3b2d2dcc63c44b1877 --raw)

# If no output, files are identical ✓

See Also:

Sensitive Data Guard (MANDATORY FOR PUBLIC SHARING)

Before any public sharing, gist update, or external reference of AGENTS.md:

Run security scan:

# Check for passwords, tokens, API keys, credentials
grep -i "password\|secret\|token\|key\|credential\|api_key\|ssh\|private\|auth" AGENTS.md

# Check for email addresses
grep -E "[a-z]+@[a-z]" AGENTS.md

# Check for internal URLs
grep -E "127\.0\.0\.1|localhost:[0-9]{4,5}|http.*localhost" AGENTS.md

AI Agent Behavior:

  1. Always scan before gist update - Report findings to user

  2. Block update if sensitive data detected - Ask user to review and sanitize

  3. Approved data (workspace-specific):

    • Username: lolandese
    • Local paths: /home/martinus/ddev-projects/port/*
    • Generic examples with placeholders: <key>, <value>, [name]
  4. Report format:

✅ Security Check Complete
- No passwords, API keys, or tokens detected
- No email addresses beyond approved username
- No internal/private URLs found
- Safe to share publicly ✓
  1. If sensitive data found:
❌ Sensitive Data Detected - DO NOT UPDATE GIST

Found issues:
- [Line X]: Actual API key exposed
- [Line Y]: Database credentials

Action: User must review and sanitize before proceeding

Rationale: AGENTS.md is public and accessible. Workspace paths and username are acceptable; credentials are not.

Code Quality Standards

Required Standards:

  • Adhere to Drupal coding standards (PSR-12 with Drupal extensions)
  • Indentation: 2 spaces (no tabs)
  • Line length: ≤ 80 characters (flexible for readability)
  • Naming: CamelCase classes/methods, snake_case variables/functions
  • Braces: Always use braces; prefer early returns
  • PHPDoc: Full blocks with @param, @return, @throws
  • Type hints: Always use type hints for parameters and return values

Linting Commands:

# Check coding standards
vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/
vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/

# Auto-fix issues
vendor/bin/phpcbf --standard=Drupal src/

# Static analysis
vendor/bin/phpstan analyze src/

Reject any code that fails Drupal Coder sniffs.

Code Quality Examples

// ✅ GOOD: Modern Drupal 10/11 service injection
// Using PHP 8.0+ constructor property promotion
class MyModuleService {
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager, // Property declared and assigned
    protected MessengerInterface $messenger,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('messenger'),
    );
  }
}

// ✅ ACCEPTABLE in D8/9 branch: Deprecated function with documentation
function mymodule_do_something() {
  // Using entity_load() which is deprecated in D9, removed in D10.
  // Acceptable in 8.x branch as it works in latest D9.
  // D10/11 branch uses EntityTypeManager service instead.
  // @see https://www.drupal.org/node/2266845
  $entities = entity_load('node', [1, 2, 3]);

  // Using drupal_set_message() deprecated in D8.5, removed in D10.
  // Acceptable in 8.x branch.
  // D10/11 branch uses messenger service instead.
  // @see https://www.drupal.org/node/2774931
  drupal_set_message('Done!');
}

// ❌ BAD: Deprecated function without explanation
function mymodule_bad_example() {
  $entities = entity_load('node', [1, 2, 3]);
  drupal_set_message('Done!');
}

Deprecated Function Documentation Standard

When using deprecated functions in D8/9 branch:

/**
 * Process nodes using legacy API.
 *
 * Note: This implementation uses deprecated functions that work in Drupal 9
 * but are removed in Drupal 10. See 10.x branch for modern implementation.
 */
function mymodule_process_nodes(array $nids) {
  // @deprecated entity_load() is deprecated in Drupal 8.0.0, removed in 10.0.0.
  // Use \Drupal::entityTypeManager()->getStorage('node')->loadMultiple() instead.
  // Keeping this in 8.x branch as it works reliably in D9.
  // @see https://www.drupal.org/node/2266845
  $nodes = entity_load('node', $nids);

  foreach ($nodes as $node) {
    // Processing logic...

    // @deprecated drupal_set_message() deprecated in 8.5.0, removed in 10.0.0.
    // Use messenger service in 10.x branch.
    // @see https://www.drupal.org/node/2774931
    drupal_set_message(t('Processed @title', ['@title' => $node->label()]));
  }
}

Required Modernizations

1. Hook to Service Migration

  • Convert hook implementations to event subscribers where appropriate
  • Move business logic from .module files to services
  • Use dependency injection instead of global functions

2. Plugin System

  • Migrate custom hook systems to plugins (Block, Field, Action, etc.)
  • Use annotations for plugin discovery
  • Implement proper plugin interfaces

3. Configuration Schema

  • All configuration must have proper schema in config/schema/*.yml
  • Use configuration entities instead of variables
  • Implement proper config export/import

4. Entity API

  • Use entity type manager and storage handlers
  • Implement proper entity access control
  • Use field API for custom fields

File Structure

mymodule/
├── mymodule.info.yml          # Module metadata
├── mymodule.services.yml       # Service definitions
├── mymodule.routing.yml        # Route definitions
├── mymodule.permissions.yml    # Permission definitions
├── mymodule.module             # Hook implementations only
├── composer.json               # Dependencies
├── config/
│   ├── install/                # Default configuration
│   └── schema/                 # Configuration schemas
├── src/
│   ├── Controller/             # Controllers
│   ├── Form/                   # Forms
│   ├── Plugin/                 # Plugins
│   └── Service/                # Services
├── modules/
│   └── mymodule_demo/          # Demo submodule (REQUIRED)
│       ├── mymodule_demo.info.yml
│       ├── mymodule_demo.install
│       ├── mymodule_demo.module
│       └── config/
│           └── install/        # Demo-specific config
└── tests/
    ├── src/
    │   ├── Kernel/             # Kernel tests
    │   └── Unit/               # Unit tests
    └── modules/                # Test modules

Testing Requirements

Demo Submodule

REQUIRED for modules that:

  • Process Drupal core entities (nodes, users, taxonomy terms)
  • Provide Views that output user-facing content
  • Need realistic test data to demonstrate functionality

Optional but recommended for:

  • Utility modules without entity dependencies
  • API-only modules
  • Simple configuration modules

Demo submodules should:

  • Create temporary mock data (content, users, config) on install
  • Enable efficient manual and automated testing
  • Demonstrate module usage for new users
  • Clean up completely on uninstall

Purpose:

  1. Testing Efficiency: Pre-configured test data eliminates manual setup
  2. Development Speed: No need to recreate content after each code change
  3. User Onboarding: Living example of module capabilities
  4. Clean Removal: All demo entities tracked and removed on uninstall

Implementation:

  • Location: modules/[MODULE_NAME]_demo/
  • Track created entities in state storage
  • Use enforced dependencies in config
  • Create 2-5 demo content items initially
  • Expand progressively as features are added

See /DEMO_SUBMODULE.md for complete implementation patterns.

1. Kernel Tests

  • Required for: Core functionality, entity operations, configuration changes
  • Coverage: All service methods that interact with Drupal APIs
  • Example:
namespace Drupal\Tests\mymodule\Kernel;

use Drupal\KernelTests\KernelTestBase;

class MyModuleKernelTest extends KernelTestBase {
  protected static $modules = ['mymodule', 'node', 'user'];

  public function testMyModuleFunctionality() {
    // Test core functionality
  }
}

2. Unit Tests

  • Required for: Business logic, helper functions, data transformations
  • Coverage: Methods that don't require Drupal bootstrap
  • Example:
namespace Drupal\Tests\mymodule\Unit;

use Drupal\Tests\UnitTestCase;

class MyModuleUnitTest extends UnitTestCase {
  public function testDataProcessing() {
    // Test pure logic
  }
}

3. Test Coverage Goals

  • All public service methods must have tests
  • Critical path functionality must have kernel tests
  • Edge cases and error handling must be tested

4. Testing Best Practices

Test Organization:

tests/
├── src/
│   ├── Kernel/
│   │   ├── MyModuleServiceTest.php
│   │   └── MyModuleConfigTest.php
│   └── Unit/
│       ├── MyModuleHelperTest.php
│       └── MyModuleValidatorTest.php
└── modules/
    └── mymodule_test/  # Test-only submodule
        ├── mymodule_test.info.yml
        └── config/
            └── install/

Running Tests:

# Run all module tests
vendor/bin/phpunit web/modules/custom/mymodule

# Run specific test class
vendor/bin/phpunit web/modules/custom/mymodule/tests/src/Kernel/MyModuleServiceTest.php

# Run with coverage report
vendor/bin/phpunit --coverage-html coverage web/modules/custom/mymodule

CI/CD Integration:

  • Add .gitlab-ci.yml or .github/workflows/test.yml
  • Run tests on all supported PHP versions (8.1, 8.2, 8.3)
  • Test on both Drupal 10 and 11 if dual-version support
  • Include coding standards checks (phpcs)
  • Include static analysis (phpstan)

Git Workflow

Branch Management for Ports (CRITICAL)

⚠️ CRITICAL: Always change branches immediately after cloning/copying for a port or back-port.

When starting a port or back-port from an existing repository in another version, the first action must be to create/checkout a new branch. Failing to do this will result in commits being made to the wrong version branch.

Common Scenario:

# You're working on D11 (2.0.x branch)
cd ~/port/D11/web/modules/custom/themeless
git branch --show-current  # Shows: 2.0.x

# Copy to D9 for back-port
cp -r ~/port/D11/web/modules/custom/themeless ~/port/D9/web/modules/custom/themeless
cd ~/port/D9/web/modules/custom/themeless

# ⚠️ DANGER: Still on 2.0.x branch from D11!
git branch --show-current  # Still shows: 2.0.x

# ✅ REQUIRED: Change branch immediately
git checkout -b 1.0.x      # Create new branch for D8/9

Workflow:

  1. After cloning or copying module to different Drupal version directory
  2. Immediately check current branch: git branch --show-current
  3. Create appropriate branch for target version:
    • D8/9 ports: 1.0.x or 1.0.0-alpha1
    • D10/11 ports: 2.0.x or 2.0.0-alpha1
    • Issue-specific: issue-[NUMBER]-[DESCRIPTION]
  4. Verify branch changed before making any modifications
  5. Update .info.yml core_version_requirement to match target version

If Unsure About Branch Name:

  • Ask the user before creating branch
  • Consider: version scheme, drupal.org conventions, project history
  • Check existing branches on origin: git branch -r

Example Dialog:

Agent: "I need to create a branch for the D8/9 back-port. Should I use:
  1. 1.0.x (development branch)
  2. 1.0.0-alpha1 (tagged release)
  3. issue-[number]-d9-backport (issue-specific)
Which naming convention do you prefer?"

Why This Matters:

  • Prevents commits intended for D9 appearing in D11 branch
  • Avoids breaking production branches with wrong-version code
  • Maintains clear separation between version-specific development
  • Allows proper version comparison via git diff

Branch Naming

  • Format: issue-[ISSUE_NUMBER]-[SHORT_DESCRIPTION]
  • Example: issue-3456789-port-to-drupal-10

Commit Messages

Issue #3456789: Port module to Drupal 10

- Converted hook_menu to routing.yml
- Migrated variables to configuration API
- Implemented dependency injection in services
- Added kernel tests for core functionality
- Updated .info.yml with Drupal 10 compatibility

Commit Strategy

  • Atomic commits: Each commit should represent one logical change
  • Reference issues: Always include issue number in commit message
  • Explain why: Document the reason for changes, not just what changed

API Migration Patterns

Common D7 → D10/11 Migrations

All change records referenced below are from drupal.org/list-changes.

Variables to Configuration

// D7
$value = variable_get('mymodule_setting', 'default');
variable_set('mymodule_setting', $new_value);

// D10/11
$config = \Drupal::config('mymodule.settings');
$value = $config->get('setting') ?? 'default';

\Drupal::configFactory()->getEditable('mymodule.settings')
  ->set('setting', $new_value)
  ->save();

Change record: https://www.drupal.org/node/1667894

Entity Loading

// D7
$node = node_load($nid);
$nodes = node_load_multiple([1, 2, 3]);

// D10/11
$node = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->load($nid);

$nodes = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->loadMultiple([1, 2, 3]);

Change record: https://www.drupal.org/node/2266845

User Messages

// D7
drupal_set_message(t('Message'), 'status');
drupal_set_message(t('Error'), 'error');

// D10/11 (inject messenger service)
$this->messenger()->addStatus($this->t('Message'));
$this->messenger()->addError($this->t('Error'));

Change record: https://www.drupal.org/node/2774931

Forms

// D7
function mymodule_settings_form($form, &$form_state) {
  $form['setting'] = [
    '#type' => 'textfield',
    '#title' => t('Setting'),
    '#default_value' => variable_get('mymodule_setting'),
  ];
  return system_settings_form($form);
}

// D10/11
namespace Drupal\mymodule\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class SettingsForm extends ConfigFormBase {
  protected function getEditableConfigNames() {
    return ['mymodule.settings'];
  }

  public function getFormId() {
    return 'mymodule_settings_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('mymodule.settings');

    $form['setting'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Setting'),
      '#default_value' => $config->get('setting'),
    ];

    return parent::buildForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('mymodule.settings')
      ->set('setting', $form_state->getValue('setting'))
      ->save();

    parent::submitForm($form, $form_state);
  }
}

Menu to Routing

// D7 hook_menu()
function mymodule_menu() {
  $items['admin/config/mymodule'] = [
    'title' => 'My Module Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => ['mymodule_settings_form'],
    'access arguments' => ['administer mymodule'],
  ];
  return $items;
}

// D10/11 mymodule.routing.yml
mymodule.settings:
  path: '/admin/config/mymodule'
  defaults:
    _form: '\Drupal\mymodule\Form\SettingsForm'
    _title: 'My Module Settings'
  requirements:
    _permission: 'administer mymodule'

Change record: https://www.drupal.org/node/2122219

Security & Performance Guidelines

Security Requirements (Critical for Contrib)

  • Always sanitize user input: Use #plain_text for untrusted content, never raw #markup
  • CSRF protection: Forms automatically include tokens, verify in custom endpoints
  • Permissions: Implement proper access checks and route requirements (_permission)
  • SQL Injection: Use Entity Query or Database API with parameter binding, never raw SQL
  • XSS Prevention: Always use |e filter in Twig templates, validate #markup content
  • Configuration Security: Keep sensitive config out of version control
  • Access control: Check entity access before operations: $entity->access('view', $account)

Performance Best Practices

  • Render caching: Always add #cache array to render arrays
    $build['#cache'] = [
      'tags' => ['node:123', 'node_list'],
      'contexts' => ['user.roles', 'url.path'],
      'max-age' => 3600,
    ];
  • Cache tags: Use entity-based tags (node:123) or list-based tags (node_list)
  • Cache contexts: Apply user-specific contexts for personalized content
  • Lazy loading: Use #lazy_builder for expensive operations
  • Database queries: Use entity queries for better caching, avoid loading full entities when IDs suffice
  • Batch operations: Use Batch API for processing large datasets
  • Avoid premature optimization: Profile first with Webprofiler or XHProf

Code Review Checklist

Before submitting a port, verify:

For D8/9 Branch (8.x-1.x):

  • All .info files converted to .info.yml
  • core_version_requirement: ^8 || ^9 in .info.yml
  • Deprecated functions documented with inline comments
  • Works on latest stable Drupal 9.x
  • Services properly defined in .services.yml
  • Configuration has proper schema
  • Demo submodule created and tested
    • Installs cleanly with demo content
    • Uninstalls cleanly with no orphaned data
    • Demonstrates main module features
    • Used in tests where appropriate
  • Tests pass (vendor/bin/phpunit)
  • Code follows Drupal coding standards (vendor/bin/phpcs)
  • Dependencies declared in composer.json
  • README.md updated with D8/9 requirements
  • CHANGELOG.md notes which branch this is

For D10/11 Branch (10.x-1.x):

  • All .info files converted to .info.yml
  • core_version_requirement: ^10 || ^11 in .info.yml
  • No use of deprecated functions (check with PHPStan/Upgrade Status)
  • All hooks documented with proper @Hook attributes (D10+)
  • Works on latest stable Drupal 11.x
  • Services use modern patterns (constructor property promotion, etc.)
  • Configuration has proper schema
  • Demo submodule created and tested
    • Installs cleanly with demo content
    • Uninstalls cleanly with no orphaned data
    • Demonstrates main module features
    • Used in tests where appropriate
  • Tests pass (vendor/bin/phpunit)
  • Code follows Drupal coding standards (vendor/bin/phpcs)
  • Dependencies declared in composer.json
  • README.md updated with D10/11 requirements
  • CHANGELOG.md documents API differences from 8.x branch
  • UPGRADE.md created if migration from 8.x is complex

Both Branches:

  • Branch naming follows convention
  • Commits reference issue numbers
  • Ready for drupal.org review

Manual Review Flags

Tag the following for manual review with @todo MANUAL_REVIEW:

  1. Complex database queries: Custom queries may need optimization
  2. Permission changes: Ensure security is maintained
  3. Data migrations: Custom update hooks need careful testing
  4. Third-party integrations: API changes in external libraries
  5. Performance implications: Major architectural changes
  6. User-facing changes: UI/UX modifications

Version Split Recommendations

AI should recommend creating separate branches when encountering:

Definite Split Indicators:

  • Removed APIs: Required functionality removed in D10+ with no compatibility layer
  • PHP version conflicts: Code that can't work on both PHP 7.4 (D9) and PHP 8.3 (D11)
  • Major architecture changes: Significant refactoring that would break D8/9
  • Security improvements: D10/11-specific security features that can't be backported

Probable Split Indicators:

  • Hook attributes: Using D10+ hook attributes vs docblock hooks
  • Entity API changes: Significant entity handling differences
  • Service container changes: Major DI pattern differences
  • Configuration schema: Incompatible schema requirements

Hook Attributes Example (D10/11 only):

// Modern D10/11 approach with hook attributes
namespace Drupal\mymodule;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\node\NodeInterface;

class MyModuleHooks {

  #[Hook('node_presave')]
  public function nodePresave(NodeInterface $node): void {
    // Hook implementation
    $node->setTitle('Modified: ' . $node->getTitle());
  }

  #[Hook('help')]
  public function help(string $route_name): array|string {
    if ($route_name === 'help.page.mymodule') {
      return '<p>' . t('Help text here') . '</p>';
    }
    return [];
  }
}

Note: Hook attributes require Drupal 10.3+ and are not compatible with D8/9. Use traditional docblock hooks in 8.x branch.

Keep Single Branch If:

  • Compatibility layers exist: Functions work across D8-11 with proper checks
  • Minimal changes: Only .info.yml and minor updates needed
  • No breaking changes: All functionality works identically
  • Maintenance burden: Team prefers single codebase

Recommendation Format:

@todo VERSION_SPLIT: Consider splitting into 8.x and 10.x branches.

Reason: [Specific API/feature that causes issues]
Impact: [What breaks or becomes difficult]
Alternative: [If there's a way to keep single branch]

Example:
The hook_help() implementation uses hook attributes (D10+) which
aren't available in D8/9. We could:
1. Split branches (recommended) - use attributes in 10.x
2. Keep docblock hooks in both (works but not modern)

When AI recommends a split, it should:

  1. Document the reason clearly
  2. Suggest migration path from 8.x to 10.x
  3. Note which features differ between branches
  4. Propose UPGRADE.md content for users

Documentation Requirements

Code Documentation

  • All classes must have docblocks
  • All public methods must document parameters and return types
  • Complex logic must have inline comments

User Documentation

Update or create:

  • README.md: Installation and basic usage
  • INSTALL.txt: Detailed installation steps
  • CHANGELOG.md: Version history and breaking changes

Drupal.org Integration

Issue Queue Workflow

  1. Create or find existing issue for the port
  2. Comment with approach and plan
  3. Create branch: issue-[NUMBER]-[DESCRIPTION]
  4. Regular updates on progress
  5. Request review from maintainers
  6. Address feedback in new commits

Patch Generation

# Generate interdiff between versions
git diff 8.x-1.x..issue-3456789-port-to-drupal-10 > 3456789-2.patch

# Or use drupal.org's built-in comparison tools

Environment Assumptions

  • DDEV: All sites run on DDEV with consistent naming
  • SSH Access: Maintainer has push access to drupal.org repositories
  • GitHub Mirror: Parallel repository for collaboration
  • Git Remotes:
    • origin: drupal.org git repository
    • github: GitHub mirror

Debugging & Troubleshooting

Essential Debugging Commands

# View Drupal logs (primary debugging tool)
drush watchdog:show                    # Recent log messages
drush watchdog:show --severity=Error   # Filter by severity
drush watchdog:show --type=php         # Filter by type
drush watchdog:delete all              # Clear logs when huge

# System status checks
drush status                           # Verify Drupal root, DB connection
drush cr                              # Clear all caches
drush cache:rebuild                   # Alternative cache clear

# Configuration debugging
drush config:get <name>               # Show specific config
drush config:set <name> <key> <value> # Temporarily change config
drush config:export                   # Export active config
drush config:import                   # Import config

# Module and permission checks
drush pm:list --status=enabled        # List enabled modules
drush role:perm:add anonymous 'permission name'  # Grant permission
drush role:perm:list authenticated    # List role permissions

# Entity and field inspection
drush ev "\$node = \Drupal\node\Entity\Node::load(1); print_r(\$node->toArray());"

Common Issues and Solutions

Module won't enable:

  1. Check dependencies in .info.yml
  2. Run drush updatedb for schema updates
  3. Check logs: drush watchdog:show --severity=Error
  4. Verify composer dependencies installed

Configuration import fails:

  1. Check for schema errors: drush config:inspect <config.name>
  2. Ensure config/schema/*.yml exists and is valid
  3. Run drush cr before importing

Tests fail:

  1. Run specific test: vendor/bin/phpunit --group mymodule
  2. Check test environment: database, files directory permissions
  3. Verify test dependencies in composer.json
  4. Clear test cache: vendor/bin/phpunit --do-not-cache-result

Performance issues in demo:

  1. Check query count: Enable Database Logging module
  2. Profile with Webprofiler if available
  3. Verify render cache tags are set correctly
  4. Check for N+1 query problems (load entities in batch)

System Troubleshooting

When terminal commands hang:

  • Not a DDEV issue if even ls, pwd hang
  • Check for resource-intensive processes in other terminals
  • Monitor system load if accessible
  • Close competing processes
  • Restart Docker daemon: sudo systemctl restart docker

Common Pitfalls to Avoid

  1. Don't use \Drupal:: in services: Inject dependencies
  2. Don't hardcode paths: Use route generation and URL services
  3. Don't ignore configuration schema: All config needs validation
  4. Don't skip tests: They catch regressions early
  5. Don't mix concerns: Keep controllers thin, logic in services
  6. Don't forget access control: Always check permissions
  7. Don't use static caching improperly: Use cache services

Resources

Drupal.org Official Resources

AI Agent Files (General Drupal Development)

Note: Our AGENTS.md file is specifically designed for module porting and includes unique guidance not found in other resources (dual-branch strategy, deprecated function documentation, demo submodules, forward-only porting).

Version-Specific Notes

Drupal 10

  • PHP 8.1+ required
  • Symfony 6.2 components
  • CKEditor 5 (CKEditor 4 deprecated)
  • Hook event dispatcher pattern available

Drupal 11

  • PHP 8.3+ required
  • Symfony 7.0 components
  • Hook attributes (prefer over docblock hooks)
  • Strict typing encouraged
  • Constructor property promotion preferred

Success Criteria

A successful port means:

  1. ✅ All tests pass in target Drupal version
  2. ✅ No deprecated code warnings
  3. ✅ Follows modern Drupal architecture patterns
  4. ✅ Maintains or improves functionality
  5. ✅ Has comprehensive test coverage
  6. ✅ Documentation is updated
  7. ✅ Code passes quality checks (phpcs, phpstan)
  8. ✅ Ready for community review on drupal.org
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment