feat(schema): add compound trigger format validation

This commit is contained in:
Alex Verkhovsky 2025-12-23 06:55:20 -08:00
parent 0edcebc517
commit 15d34d7ea5
7 changed files with 175 additions and 10 deletions

View File

@ -35,14 +35,14 @@ agent:
- "NEVER lie about tests being written or passing - tests must actually exist and pass 100%" - "NEVER lie about tests being written or passing - tests must actually exist and pass 100%"
menu: menu:
- trigger: DS or dev-story or fuzzy match on dev story - trigger: DS or dev-story or fuzzy match on dev-story
workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml" workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml"
description: "[DS] Execute Dev Story workflow (full BMM path with sprint-status)" description: "[DS] Execute Dev Story workflow (full BMM path with sprint-status)"
- trigger: CR or code-review or fuzzy match on code review - trigger: CR or code-review or fuzzy match on code-review
workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml" workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml"
description: "[CR] Perform a thorough clean context code review (Highly Recommended, use fresh context and different LLM)" description: "[CR] Perform a thorough clean context code review (Highly Recommended, use fresh context and different LLM)"
- trigger: PS or party-mode or fuzzy match on party mode - trigger: PM or party-mode or fuzzy match on party-mode
exec: "{project-root}/_bmad/core/workflows/party-mode/workflow.md" exec: "{project-root}/_bmad/core/workflows/party-mode/workflow.md"
description: "[PS] Bring the whole team in to chat with other expert agents from the party" description: "[PM] Bring the whole team in to chat with other expert agents from the party"

View File

@ -18,10 +18,10 @@ agent:
- If `**/project-context.md` exists, follow it. If absent, proceed without. - If `**/project-context.md` exists, follow it. If absent, proceed without.
menu: menu:
- trigger: TS or tech-spec or fuzzy match on tech spec - trigger: TS or tech-spec or fuzzy match on tech-spec
workflow: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/create-tech-spec/workflow.yaml" workflow: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/create-tech-spec/workflow.yaml"
description: "[TS] Architect a technical spec with implementation-ready stories (Required first step)" description: "[TS] Architect a technical spec with implementation-ready stories (Required first step)"
- trigger: QD or quick-dev or fuzzy match on quick dev - trigger: QD or quick-dev or fuzzy match on quick-dev
workflow: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml" workflow: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.yaml"
description: "[QD] Implement the tech spec end-to-end solo (Core of Quick Flow)" description: "[QD] Implement the tech spec end-to-end solo (Core of Quick Flow)"

View File

@ -0,0 +1,24 @@
# Test: Compound trigger with invalid format
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger compound format error: invalid compound trigger format
agent:
metadata:
id: compound-invalid-format
name: Invalid Format
title: Invalid Format Test
icon: 🧪
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: TS or tech-spec
description: Missing fuzzy match clause
action: test

View File

@ -0,0 +1,24 @@
# Test: Compound trigger with mismatched kebab portions
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger compound format error: kebab-case trigger mismatch: "tech-spec" vs "other-thing"
agent:
metadata:
id: compound-mismatched-kebab
name: Mismatched Kebab
title: Mismatched Kebab Test
icon: 🧪
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: TS or tech-spec or fuzzy match on other-thing
description: Kebab portions do not match
action: test

View File

@ -0,0 +1,24 @@
# Test: Compound trigger with wrong shortcut
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger compound format error: shortcut "XX" does not match expected "TS" for "tech-spec"
agent:
metadata:
id: compound-wrong-shortcut
name: Wrong Shortcut
title: Wrong Shortcut Test
icon: 🧪
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: XX or tech-spec or fuzzy match on tech-spec
description: Shortcut does not match kebab trigger
action: test

View File

@ -0,0 +1,30 @@
# Test: Valid compound triggers
# Expected: PASS
agent:
metadata:
id: compound-triggers
name: Compound Triggers
title: Compound Triggers Test
icon: 🧪
persona:
role: Test agent with compound triggers
identity: I test compound trigger validation.
communication_style: Clear
principles:
- Test compound format
menu:
- trigger: TS or tech-spec or fuzzy match on tech-spec
description: Two-word compound trigger
action: tech_spec
- trigger: DS or dev-story or fuzzy match on dev-story
description: Another two-word compound trigger
action: dev_story
- trigger: WI or workflow-init-process or fuzzy match on workflow-init-process
description: Three-word compound trigger (uses first 2 words for shortcut)
action: workflow_init
- trigger: H or help or fuzzy match on help
description: Single-word compound trigger (1-letter shortcut)
action: help

View File

@ -4,6 +4,56 @@ const { z } = require('zod');
const COMMAND_TARGET_KEYS = ['workflow', 'validate-workflow', 'exec', 'action', 'tmpl', 'data']; const COMMAND_TARGET_KEYS = ['workflow', 'validate-workflow', 'exec', 'action', 'tmpl', 'data'];
const TRIGGER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const TRIGGER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const COMPOUND_TRIGGER_PATTERN = /^([A-Z]{1,2}) or ([a-z0-9]+(?:-[a-z0-9]+)*) or fuzzy match on ([a-z0-9]+(?:-[a-z0-9]+)*)$/;
/**
* Derive the expected shortcut from a kebab-case trigger.
* - Single word: first letter (e.g., "help" "H")
* - Multi-word: first letter of first two words (e.g., "tech-spec" "TS")
* @param {string} kebabTrigger The kebab-case trigger name.
* @returns {string} The expected uppercase shortcut.
*/
function deriveShortcutFromKebab(kebabTrigger) {
const words = kebabTrigger.split('-');
if (words.length === 1) {
return words[0][0].toUpperCase();
}
return (words[0][0] + words[1][0]).toUpperCase();
}
/**
* Parse and validate a compound trigger string.
* Format: "<SHORTCUT> or <kebab-case> or fuzzy match on <kebab-case>"
* @param {string} triggerValue The trigger string to parse.
* @returns {{ valid: boolean, kebabTrigger?: string, error?: string }}
*/
function parseCompoundTrigger(triggerValue) {
const match = COMPOUND_TRIGGER_PATTERN.exec(triggerValue);
if (!match) {
return { valid: false, error: 'invalid compound trigger format' };
}
const [, shortcut, kebabTrigger, fuzzyKebab] = match;
// Validate both kebab instances are identical
if (kebabTrigger !== fuzzyKebab) {
return {
valid: false,
error: `kebab-case trigger mismatch: "${kebabTrigger}" vs "${fuzzyKebab}"`,
};
}
// Validate shortcut matches derived value
const expectedShortcut = deriveShortcutFromKebab(kebabTrigger);
if (shortcut !== expectedShortcut) {
return {
valid: false,
error: `shortcut "${shortcut}" does not match expected "${expectedShortcut}" for "${kebabTrigger}"`,
};
}
return { valid: true, kebabTrigger };
}
// Public API --------------------------------------------------------------- // Public API ---------------------------------------------------------------
@ -52,8 +102,21 @@ function agentSchema(options = {}) {
// Handle legacy format with trigger field // Handle legacy format with trigger field
if (item.trigger) { if (item.trigger) {
const triggerValue = item.trigger; const triggerValue = item.trigger;
let canonicalTrigger = triggerValue;
if (!TRIGGER_PATTERN.test(triggerValue)) { // Check if it's a compound trigger (contains " or ")
if (triggerValue.includes(' or ')) {
const result = parseCompoundTrigger(triggerValue);
if (!result.valid) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'trigger'],
message: `agent.menu[].trigger compound format error: ${result.error}`,
});
return;
}
canonicalTrigger = result.kebabTrigger;
} else if (!TRIGGER_PATTERN.test(triggerValue)) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
path: ['agent', 'menu', index, 'trigger'], path: ['agent', 'menu', index, 'trigger'],
@ -62,16 +125,16 @@ function agentSchema(options = {}) {
return; return;
} }
if (seenTriggers.has(triggerValue)) { if (seenTriggers.has(canonicalTrigger)) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
path: ['agent', 'menu', index, 'trigger'], path: ['agent', 'menu', index, 'trigger'],
message: `agent.menu[].trigger duplicates "${triggerValue}" within the same agent`, message: `agent.menu[].trigger duplicates "${canonicalTrigger}" within the same agent`,
}); });
return; return;
} }
seenTriggers.add(triggerValue); seenTriggers.add(canonicalTrigger);
} }
// Handle multi format with triggers array (new format) // Handle multi format with triggers array (new format)
else if (item.triggers && Array.isArray(item.triggers)) { else if (item.triggers && Array.isArray(item.triggers)) {