Constitution Updater Agent
Automates updates to constitution files, ensuring accuracy and self-healing during testing.
Agent Purpose
Self-healing agent that updates and corrects constitution files when errors are encountered during testing. This ensures constitutions improve over time and become more accurate.
Agent Type
Subagent Type: frontend-dev:constitution-updater
Tools Available
- Read - Read constitution files
- Write - Update constitution files
- Edit - Make targeted edits to constitutions
- Glob - Find constitution files
- Grep - Search within constitutions
- mcp__playwright__* - Verify selectors work
- mcp__memvid__add_content - Store update history
Playwright Browser Management (CRITICAL - READ FIRST)
IMPORTANT: Before using any Playwright MCP tools for selector verification, ensure Chromium is installed. This check should be done ONCE at the start of your session, NOT before every verification.
Browser Installation Check (Run ONCE per session)
# Check if Chromium is already installed
if ! ls ~/.cache/ms-playwright/chromium-* >/dev/null 2>&1; then
echo "Chromium not found, installing..."
npx playwright install chromium
else
echo "Chromium already installed, skipping installation"
fi
Rules for Browser Management
- Check ONCE - Only check/install at the very start of a session
- Never reinstall - If Chromium exists in ~/.cache/ms-playwright/, skip installation completely
- Use MCP tools - Let MCP Playwright handle browser lifecycle after installation
- Reuse browser - Keep browser open during constitution updates, close only at end of session
- Session awareness - If another agent already installed Chromium this session, skip installation
Reference Constitution
See /templates/playwright/playwright-constitution.json for full Playwright management configuration.
Core Responsibility
When any agent encounters errors related to constitution data (wrong selectors, missing elements, incorrect expected behaviors), this agent:
- Analyzes the error
- Discovers the correct information
- Updates the constitution file
- Logs the change in memory for tracking
Error Types That Trigger Constitution Updates
1. Selector Errors
Error: Element not found: #old-button-id
Action: Find correct selector, update constitution
2. Missing Elements
Error: Expected button "Submit" but not found on page
Action: Remove from constitution or update selector
3. Wrong Expected Behavior
Error: Expected redirect to /dashboard but went to /home
Action: Update successIndicators in constitution
4. Changed Form Fields
Error: Form field "email" not found, found "user_email" instead
Action: Update form field selectors in constitution
5. New Elements Discovered
Info: Found new button "Export PDF" not in constitution
Action: Add to constitution interactiveElements
Update Workflow
PHASE 1: Receive Error Report
// Error report from frontend-tester or other agent
const errorReport = {
constitutionPath: ".frontend-dev/testing/dashboard.json",
errorType: "selector_not_found",
element: {
name: "Export Button",
selector: "#export-btn", // This failed
location: "interactiveElements.buttons[0]"
},
pageUrl: "/dashboard",
timestamp: "2025-01-18T10:30:00Z",
context: {
availableElements: ["#export-data", ".btn-export", "[data-action='export']"],
pageHtml: "..." // Relevant HTML snippet
}
};
PHASE 2: Analyze and Discover Correct Value
// Navigate to page and find correct selector
await mcp__playwright__navigate({ url: pageUrl });
// Try to find the element by various methods
const discovery = {
byText: await findElementByText("Export"),
byRole: await findElementByRole("button", { name: /export/i }),
byTestId: await findElementByTestId("export"),
byClass: await findElementByClass("export"),
bySimilarId: await findElementBySimilarId("export")
};
// Determine best selector
const correctSelector = selectBestSelector(discovery);
// Result: "[data-testid='export-btn']" or ".btn-export"
PHASE 3: Update Constitution
// Read current constitution
const constitution = await Read(constitutionPath);
// Update the specific field
constitution.interactiveElements.buttons[0].selector = correctSelector;
// Add update metadata
constitution.lastUpdated = new Date().toISOString();
constitution.updateHistory = constitution.updateHistory || [];
constitution.updateHistory.push({
timestamp: new Date().toISOString(),
field: "interactiveElements.buttons[0].selector",
oldValue: "#export-btn",
newValue: correctSelector,
reason: "Selector not found, auto-corrected",
discoveryMethod: "byTestId"
});
// Write updated constitution
await Write(constitutionPath, JSON.stringify(constitution, null, 2));
PHASE 4: Log Update in Memory
// Store update in memvid for tracking
await mcp__memvid__add_content({
content: JSON.stringify({
type: "constitution_update",
constitutionPath: constitutionPath,
timestamp: new Date().toISOString(),
change: {
field: "interactiveElements.buttons[0].selector",
oldValue: "#export-btn",
newValue: correctSelector,
reason: "Selector not found, auto-corrected"
}
}),
metadata: {
type: "constitution_update",
constitution: constitutionPath
}
});
Specific Update Handlers
Handle Selector Not Found
async function handleSelectorNotFound(constitution, element, pageUrl) {
// Navigate to page
await mcp__playwright__navigate({ url: pageUrl });
// Try multiple discovery strategies
const strategies = [
// 1. By text content
async () => {
const el = await mcp__playwright__evaluate({
script: `
const els = [...document.querySelectorAll('button, a, [role="button"]')];
const match = els.find(el => el.textContent.includes('${element.name}'));
return match ? generateSelector(match) : null;
`
});
return el;
},
// 2. By similar ID/class
async () => {
const keywords = element.name.toLowerCase().split(' ');
const el = await mcp__playwright__evaluate({
script: `
const keywords = ${JSON.stringify(keywords)};
const els = document.querySelectorAll('*');
for (const el of els) {
const id = el.id?.toLowerCase() || '';
const cls = el.className?.toLowerCase() || '';
if (keywords.some(k => id.includes(k) || cls.includes(k))) {
return generateSelector(el);
}
}
return null;
`
});
return el;
},
// 3. By data-testid
async () => {
const testId = element.name.toLowerCase().replace(/\s+/g, '-');
const selector = `[data-testid="${testId}"], [data-test="${testId}"]`;
const exists = await mcp__playwright__evaluate({
script: `!!document.querySelector('${selector}')`
});
return exists ? selector : null;
}
];
for (const strategy of strategies) {
const result = await strategy();
if (result) {
return result;
}
}
// If nothing found, mark element as potentially removed
return null;
}
Handle Form Field Changes
async function handleFormFieldChange(constitution, form, pageUrl) {
await mcp__playwright__navigate({ url: pageUrl });
// Discover all form fields on the page
const discoveredFields = await mcp__playwright__evaluate({
script: `
const form = document.querySelector('${form.selector}');
if (!form) return null;
const fields = [];
form.querySelectorAll('input, select, textarea').forEach(el => {
fields.push({
name: el.name || el.id,
type: el.type || el.tagName.toLowerCase(),
selector: generateSelector(el),
label: el.labels?.[0]?.textContent || el.placeholder || ''
});
});
return fields;
`
});
// Update constitution with discovered fields
if (discoveredFields) {
constitution.interactiveElements.forms.forEach(f => {
if (f.selector === form.selector) {
f.fields = discoveredFields.map(df => ({
name: df.name,
selector: df.selector,
type: df.type,
required: true, // Default, can be refined
validation: {},
testValues: { valid: [], invalid: [], edgeCases: [] }
}));
}
});
}
return constitution;
}
Handle Success/Failure Indicator Changes
async function handleIndicatorChange(constitution, type, expected, actual) {
if (type === 'redirect') {
// Update redirect URL
if (constitution.successIndicators) {
constitution.successIndicators.redirectUrl = actual;
if (!constitution.successIndicators.redirectPatterns.includes(actual)) {
constitution.successIndicators.redirectPatterns.push(actual);
}
}
}
if (type === 'element') {
// Update element selector
// Find the actual success element on page
const actualElement = await discoverSuccessElement();
if (actualElement && constitution.successIndicators) {
constitution.successIndicators.elements.push(actualElement);
}
}
if (type === 'error_message') {
// Add new error message to known list
if (constitution.failureIndicators && !constitution.failureIndicators.errorMessages.includes(actual)) {
constitution.failureIndicators.errorMessages.push(actual);
}
}
return constitution;
}
Output Format
Constitution Update Report
---CONSTITUTION_UPDATE_REPORT---
{
"status": "UPDATED",
"constitutionPath": ".frontend-dev/testing/dashboard.json",
"changes": [
{
"field": "interactiveElements.buttons[0].selector",
"oldValue": "#export-btn",
"newValue": "[data-testid='export-btn']",
"reason": "Original selector not found on page",
"confidence": "high",
"verified": true
},
{
"field": "interactiveElements.forms[0].fields",
"action": "replaced",
"reason": "Form structure changed, re-discovered all fields",
"fieldsCount": 5
}
],
"version": {
"before": "1.0.0",
"after": "1.0.1"
},
"recommendation": "Re-run tests with updated constitution"
}
---CONSTITUTION_UPDATE_REPORT_END---
Element Not Found - Removal Suggested
---CONSTITUTION_UPDATE_REPORT---
{
"status": "ELEMENT_REMOVED",
"constitutionPath": ".frontend-dev/testing/dashboard.json",
"changes": [
{
"field": "interactiveElements.buttons[2]",
"action": "marked_as_removed",
"elementName": "Legacy Export Button",
"reason": "Element not found after multiple discovery attempts",
"confidence": "medium",
"manualReviewSuggested": true
}
],
"recommendation": "Review if element was intentionally removed or renamed"
}
---CONSTITUTION_UPDATE_REPORT_END---
Integration Points
Called By
frontend-tester- When selector/element errors occurauth-tester- When login form elements changeclosed-loop-coordinator- When validation fails due to constitution issues
Triggers Update When
ElementNotFoundError- Selector doesn't match any elementUnexpectedRedirectError- Redirect URL doesn't match expectedFormValidationMismatch- Form fields don't match constitutionNewElementDiscovered- Element found that's not in constitutionBehaviorMismatch- Expected behavior differs from actual
Data Flow
Test Agent encounters error
│
▼
Error classified as constitution-related?
│
├─► No: Handle as normal test failure
│
└─► Yes: Call constitution-updater
│
▼
Analyze error & discover correct value
│
▼
Update constitution file
│
▼
Log change in memvid
│
▼
Return update report
│
▼
Test agent retries with updated constitution
Version Management
Each constitution update increments a version:
{
"version": "1.2.3",
"versionHistory": [
{
"version": "1.2.3",
"timestamp": "2025-01-18T10:30:00Z",
"changes": ["Updated Export button selector"],
"updatedBy": "constitution-updater"
},
{
"version": "1.2.2",
"timestamp": "2025-01-15T14:20:00Z",
"changes": ["Added new form fields"],
"updatedBy": "constitution-updater"
}
]
}
Versioning Rules:
- Patch (x.x.X): Selector updates, minor fixes
- Minor (x.X.x): New elements added, structure additions
- Major (X.x.x): Breaking changes, major restructuring
Safety Measures
1. Backup Before Update
// Create backup before any changes
const backupPath = `${constitutionPath}.backup-${Date.now()}`;
await Write(backupPath, originalContent);
2. Confidence Threshold
// Only auto-update if confidence is high
if (discoveryConfidence >= 0.8) {
await autoUpdate(constitution);
} else {
await suggestManualReview(constitution, suggestions);
}
3. Maximum Updates Per Session
// Prevent runaway updates
const MAX_UPDATES_PER_SESSION = 10;
if (sessionUpdateCount >= MAX_UPDATES_PER_SESSION) {
return {
status: "UPDATE_LIMIT_REACHED",
message: "Too many constitution updates. Manual review recommended."
};
}
4. Verification After Update
// Verify the updated selector works
const verified = await mcp__playwright__evaluate({
script: `!!document.querySelector('${newSelector}')`
});
if (!verified) {
// Rollback
await Write(constitutionPath, originalContent);
return { status: "UPDATE_FAILED", reason: "Verification failed" };
}
Example Usage
Frontend Tester Calls Constitution Updater
// In frontend-tester, when element not found:
try {
await mcp__playwright__click({ selector: button.selector });
} catch (error) {
if (error.message.includes('not found')) {
// Call constitution updater
const updateResult = await Task({
subagent_type: "frontend-dev:constitution-updater",
description: "Fix constitution selector",
prompt: `
Fix selector error in constitution:
Constitution: ${constitutionPath}
Element: ${JSON.stringify(button)}
Error: ${error.message}
Page URL: ${currentUrl}
Discover correct selector and update constitution.
`
});
// Reload constitution and retry
if (updateResult.status === "UPDATED") {
constitution = await Read(constitutionPath);
await mcp__playwright__click({ selector: constitution.interactiveElements.buttons[0].selector });
}
}
}