diff --git a/src/modules/bmm/agents/dev.agent.yaml b/src/modules/bmm/agents/dev.agent.yaml index fbc5505c..82bc9116 100644 --- a/src/modules/bmm/agents/dev.agent.yaml +++ b/src/modules/bmm/agents/dev.agent.yaml @@ -35,14 +35,14 @@ agent: - "NEVER lie about tests being written or passing - tests must actually exist and pass 100%" 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" 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" 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" - 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" diff --git a/src/modules/bmm/agents/quick-flow-solo-dev.agent.yaml b/src/modules/bmm/agents/quick-flow-solo-dev.agent.yaml index 23a6a813..34363f63 100644 --- a/src/modules/bmm/agents/quick-flow-solo-dev.agent.yaml +++ b/src/modules/bmm/agents/quick-flow-solo-dev.agent.yaml @@ -18,10 +18,10 @@ agent: - If `**/project-context.md` exists, follow it. If absent, proceed without. 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" 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" description: "[QD] Implement the tech spec end-to-end solo (Core of Quick Flow)" diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml new file mode 100644 index 00000000..7d511299 --- /dev/null +++ b/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml new file mode 100644 index 00000000..3208b39f --- /dev/null +++ b/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/compound-wrong-shortcut.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/compound-wrong-shortcut.agent.yaml new file mode 100644 index 00000000..aa73e8ae --- /dev/null +++ b/test/fixtures/agent-schema/invalid/menu-triggers/compound-wrong-shortcut.agent.yaml @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml b/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml new file mode 100644 index 00000000..5ce29421 --- /dev/null +++ b/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml @@ -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 diff --git a/tools/schema/agent.js b/tools/schema/agent.js index cafff7c0..e8e9dc73 100644 --- a/tools/schema/agent.js +++ b/tools/schema/agent.js @@ -4,6 +4,56 @@ const { z } = require('zod'); const COMMAND_TARGET_KEYS = ['workflow', 'validate-workflow', 'exec', 'action', 'tmpl', 'data']; 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: " or or fuzzy match on " + * @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 --------------------------------------------------------------- @@ -52,8 +102,21 @@ function agentSchema(options = {}) { // Handle legacy format with trigger field if (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({ code: 'custom', path: ['agent', 'menu', index, 'trigger'], @@ -62,16 +125,16 @@ function agentSchema(options = {}) { return; } - if (seenTriggers.has(triggerValue)) { + if (seenTriggers.has(canonicalTrigger)) { ctx.addIssue({ code: 'custom', 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; } - seenTriggers.add(triggerValue); + seenTriggers.add(canonicalTrigger); } // Handle multi format with triggers array (new format) else if (item.triggers && Array.isArray(item.triggers)) {