diff --git a/src/agent/mcp-server.ts b/src/agent/mcp-server.ts index 621fcad..28a0b45 100644 --- a/src/agent/mcp-server.ts +++ b/src/agent/mcp-server.ts @@ -15,6 +15,7 @@ import { createSourceTools } from "./tools/sources.js"; import { createSessionTools } from "./tools/sessions.js"; import { createWebTools } from "./tools/web.js"; import { createDoctorTools } from "./tools/doctor.js"; +import { createConceptTools } from "../methodology/tools.js"; import type { NavGroup } from "../web/templates/layout.js"; export interface McpServerOptions { @@ -50,6 +51,7 @@ export function createMarvinMcpServer( manifest: options?.manifest, marvinDir: options?.marvinDir, }), + ...createConceptTools(options?.config), ]; return createSdkMcpServer({ diff --git a/src/methodology/concepts/artifact-types.ts b/src/methodology/concepts/artifact-types.ts new file mode 100644 index 0000000..e30a348 --- /dev/null +++ b/src/methodology/concepts/artifact-types.ts @@ -0,0 +1,300 @@ +import type { ConceptDefinition } from "../types.js"; + +export const featureConcept: ConceptDefinition = { + id: "feature", + name: "Feature", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Product capability defined by the Product Owner (F-xxx).", + definition: + "A product capability defined and prioritized by the Product Owner. Features describe what to build at a business level. They progress through draft → approved → done. Approved features are broken into implementation epics by the Tech Lead. Features have priority levels (critical, high, medium, low) to communicate business value.", + whenToUse: + "When defining product capabilities that need to be built. Features are the primary unit of scope from the business perspective.", + relatedArtifacts: ["epic", "decision", "discovery"], + relatedTools: ["create_feature", "list_features", "get_feature", "update_feature"], + relatedPersonas: ["po"], + relatedConcepts: ["epic", "sprint", "product-owner"], + source: "plugin:generic-agile#feature", +}; + +export const epicConcept: ConceptDefinition = { + id: "epic", + name: "Epic", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Implementation work package created by the Tech Lead, linked to a feature (E-xxx).", + definition: + "An implementation work package created by the Tech Lead, linked to an approved feature. Epics break features into deliverable chunks with effort estimates and target dates. They progress through planned → in-progress → done. Epics must link to approved features — the system enforces this.", + whenToUse: + "When breaking approved features into implementation work. Each epic should have a clear scope and definition of done.", + relatedArtifacts: ["feature", "task", "sprint"], + relatedTools: ["create_epic", "list_epics", "get_epic", "update_epic"], + relatedPersonas: ["tl"], + relatedConcepts: ["feature", "task", "sprint", "tech-lead"], + source: "plugin:generic-agile#epic", +}; + +export const taskConcept: ConceptDefinition = { + id: "task", + name: "Task", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Concrete implementation item linked to an epic (T-xxx).", + definition: + "A concrete implementation item created by the Tech Lead, linked to an epic. Tasks are the smallest unit of trackable work. They progress through backlog → ready → in-progress → review → done. Tasks include acceptance criteria, complexity estimates, and estimated points.", + whenToUse: + "When breaking epics into concrete implementation items that individual developers can pick up.", + relatedArtifacts: ["epic"], + relatedTools: ["create_task", "list_tasks", "get_task", "update_task"], + relatedPersonas: ["tl"], + relatedConcepts: ["epic", "sprint", "tech-lead"], + source: "plugin:generic-agile#task", +}; + +export const sprintConcept: ConceptDefinition = { + id: "sprint", + name: "Sprint", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Time-boxed iteration that groups epics and work items (SP-xxx).", + definition: + "A time-boxed iteration that groups epics and work items with delivery dates. Sprints have start/end dates, goals, and linked epics. They progress through planned → active → completed (or cancelled). Work items are associated with sprints via sprint:SP-xxx tags. The Delivery Manager owns sprint planning and tracking.", + whenToUse: + "When organizing work into delivery iterations. Use Sprint 0 for project bootstrapping.", + relatedArtifacts: ["epic", "action", "task"], + relatedTools: [ + "create_sprint", + "list_sprints", + "get_sprint", + "update_sprint", + "generate_sprint_progress", + ], + relatedPersonas: ["dm"], + relatedConcepts: ["sprint-0", "epic", "delivery-manager"], + source: "plugin:generic-agile#sprint", +}; + +export const decisionConcept: ConceptDefinition = { + id: "decision", + name: "Decision", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Recorded project decision with rationale and status (D-xxx).", + definition: + "A recorded project decision with rationale, status, and optional alternatives considered. Decisions provide an audit trail of why choices were made. They progress through proposed → accepted (or rejected/deferred). All personas can create decisions, but the DM ensures they are properly documented.", + whenToUse: + "When the team makes a significant choice that should be recorded for traceability — architectural decisions, tool choices, process changes, scope changes.", + relatedArtifacts: ["action", "question"], + relatedTools: ["create_decision", "list_decisions", "get_decision", "update_decision"], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["action", "question"], + source: "core#decision", +}; + +export const actionConcept: ConceptDefinition = { + id: "action", + name: "Action", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Tracked work item with owner and due date (A-xxx).", + definition: + "A tracked work item with an owner, due date, and optional sprint assignment. Actions are the general-purpose unit for tracking work that needs to happen. They progress through open → in-progress → done (or cancelled). Actions support sprint assignment via the sprints parameter, which auto-generates sprint:SP-xxx tags. Overdue actions are flagged in GAR reports.", + whenToUse: + "When tracking any work that needs to happen — setup tasks, follow-ups, risk mitigations, meeting outcomes. More general than tasks (which are implementation-specific).", + relatedArtifacts: ["decision", "sprint"], + relatedTools: [ + "create_action", + "list_actions", + "get_action", + "update_action", + "suggest_sprints_for_action", + ], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["sprint", "decision"], + source: "core#action", +}; + +export const questionConcept: ConceptDefinition = { + id: "question", + name: "Question", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Open question requiring resolution (Q-xxx).", + definition: + "An open question requiring resolution, with optional assignee and answer. Questions track uncertainties and knowledge gaps. They progress through open → answered (or deferred). Unresolved questions are surfaced in risk registers. Discovery session gaps can auto-spawn linked questions.", + whenToUse: + "When there is an uncertainty or knowledge gap that needs to be resolved before work can proceed.", + relatedArtifacts: ["decision", "discovery"], + relatedTools: ["create_question", "list_questions", "get_question", "update_question"], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["decision", "discovery"], + source: "core#question", +}; + +export const meetingConcept: ConceptDefinition = { + id: "meeting", + name: "Meeting", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Meeting record with attendees, agenda, and extracted outcomes (M-xxx).", + definition: + "A meeting record with attendees, date, agenda, and notes. Meetings can be analyzed to extract decisions, actions, and questions as governance artifacts. The meeting date is required metadata. Meetings progress through scheduled → completed → cancelled.", + whenToUse: + "When recording meeting discussions, decisions, and action items for audit trail and follow-up.", + relatedArtifacts: ["decision", "action", "question"], + relatedTools: [ + "create_meeting", + "list_meetings", + "get_meeting", + "update_meeting", + "analyze_meeting", + ], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["action", "decision"], + source: "plugin:generic-agile#meeting", +}; + +export const reportConcept: ConceptDefinition = { + id: "report", + name: "Report", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Persisted project report for audit trail (R-xxx).", + definition: + "A persisted project report generated from current artifact state. Report types include status reports, risk registers, GAR (Green-Amber-Red) reports, epic progress, feature progress, and sprint progress. Reports are generated on-demand and optionally saved as documents for historical tracking.", + whenToUse: + "When creating stakeholder updates, tracking project health, or building an audit trail of project status over time.", + relatedArtifacts: ["sprint", "epic", "feature", "action"], + relatedTools: [ + "generate_status_report", + "generate_risk_register", + "generate_gar_report", + "save_report", + ], + relatedPersonas: ["dm"], + relatedConcepts: ["sprint", "delivery-manager"], + source: "plugin:generic-agile#report", +}; + +export const contributionConcept: ConceptDefinition = { + id: "contribution", + name: "Contribution", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Structured persona input outside of meetings (C-xxx).", + definition: + "A structured input from a persona outside of meetings — such as action results, spike findings, stakeholder feedback, or risk findings. Contributions are analyzed to produce governance effects (new decisions, updated actions, etc.). Each persona has specific contribution types available.", + whenToUse: + "When a persona has structured input to provide outside of a meeting context — test results, research findings, stakeholder feedback, risk assessments.", + relatedArtifacts: ["action", "decision"], + relatedTools: [ + "create_contribution", + "list_contributions", + "get_contribution", + "update_contribution", + ], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["action", "decision"], + source: "plugin:generic-agile#contribution", +}; + +export const useCaseConcept: ConceptDefinition = { + id: "use-case", + name: "Use Case", + category: "artifact-type", + methodology: ["aem"], + summary: "Business scenario requiring SAP BTP extension (UC-xxx).", + definition: + "A business scenario requiring extension on SAP BTP, defined by the Product Owner in AEM Phase 1. Each use case is classified by extension type (in-app, side-by-side, hybrid) and priority. Use cases progress through draft → assessed → approved → deferred. Approved use cases move to technology assessment in Phase 2.", + whenToUse: + "In AEM projects, when defining business scenarios that need SAP BTP extension capabilities.", + relatedArtifacts: ["tech-assessment", "feature", "discovery"], + relatedTools: ["create_use_case", "list_use_cases", "get_use_case", "update_use_case"], + relatedPersonas: ["po"], + relatedConcepts: ["assess-use-case", "tech-assessment", "product-owner"], + source: "plugin:sap-aem#use-case", +}; + +export const techAssessmentConcept: ConceptDefinition = { + id: "tech-assessment", + name: "Tech Assessment", + category: "artifact-type", + methodology: ["aem"], + summary: "Technology evaluation linked to a use case (TA-xxx).", + definition: + "A technology evaluation linked to an assessed/approved use case, created by the Tech Lead in AEM Phase 2. Evaluates BTP services, extension points, and feasibility. Tech assessments progress through draft → evaluated → recommended → rejected. The system enforces that the linked use case must be assessed or approved.", + whenToUse: "In AEM Phase 2, when evaluating BTP technologies for approved use cases.", + relatedArtifacts: ["use-case", "extension-design"], + relatedTools: [ + "create_tech_assessment", + "list_tech_assessments", + "get_tech_assessment", + "update_tech_assessment", + ], + relatedPersonas: ["tl"], + relatedConcepts: ["assess-technology", "use-case", "extension-design", "tech-lead"], + source: "plugin:sap-aem#tech-assessment", +}; + +export const extensionDesignConcept: ConceptDefinition = { + id: "extension-design", + name: "Extension Design", + category: "artifact-type", + methodology: ["aem"], + summary: "Architecture design linked to a tech assessment (XD-xxx).", + definition: + "An architecture design linked to a recommended tech assessment, created by the Tech Lead in AEM Phase 3. Defines architecture pattern, BTP services, and integration points. Extension designs progress through draft → designed → validated → approved. The system enforces that the linked tech assessment must be recommended.", + whenToUse: + "In AEM Phase 3, when designing the extension architecture for recommended technologies.", + relatedArtifacts: ["tech-assessment", "epic"], + relatedTools: [ + "create_extension_design", + "list_extension_designs", + "get_extension_design", + "update_extension_design", + ], + relatedPersonas: ["tl"], + relatedConcepts: ["define-solution", "tech-assessment", "epic", "tech-lead"], + source: "plugin:sap-aem#extension-design", +}; + +export const discoveryConcept: ConceptDefinition = { + id: "discovery", + name: "Discovery Session", + category: "artifact-type", + methodology: ["generic-agile", "aem"], + summary: "Stakeholder elicitation session for requirements validation (DS-xxx).", + definition: + "A structured stakeholder elicitation session for validating requirements and identifying gaps. Discoveries capture findings and gaps during sessions. Gaps can auto-spawn linked questions (Q-xxx). Sessions progress through draft → in-review → needs-input → accepted (or parked). Sessions can be chained via parent references to carry forward open gaps.", + whenToUse: + "When validating requirements with stakeholders, identifying gaps in understanding, or refining features/use cases before committing to implementation.", + relatedArtifacts: ["question", "feature", "use-case"], + relatedTools: [ + "start_discovery", + "record_finding", + "record_gap", + "complete_discovery", + "list_discoveries", + "get_discovery", + ], + relatedPersonas: ["po", "dm", "tl"], + relatedConcepts: ["feature", "use-case", "question"], + source: "plugin:generic-agile#discovery", +}; + +export const artifactTypeConcepts: ConceptDefinition[] = [ + featureConcept, + epicConcept, + taskConcept, + sprintConcept, + decisionConcept, + actionConcept, + questionConcept, + meetingConcept, + reportConcept, + contributionConcept, + useCaseConcept, + techAssessmentConcept, + extensionDesignConcept, + discoveryConcept, +]; diff --git a/src/methodology/concepts/index.ts b/src/methodology/concepts/index.ts new file mode 100644 index 0000000..fffae84 --- /dev/null +++ b/src/methodology/concepts/index.ts @@ -0,0 +1,17 @@ +import type { ConceptDefinition } from "../types.js"; +import { phaseConcepts } from "./phases.js"; +import { ritualConcepts } from "./rituals.js"; +import { artifactTypeConcepts } from "./artifact-types.js"; +import { roleConcepts } from "./roles.js"; + +export const ALL_CONCEPTS: ConceptDefinition[] = [ + ...phaseConcepts, + ...ritualConcepts, + ...artifactTypeConcepts, + ...roleConcepts, +]; + +export { phaseConcepts } from "./phases.js"; +export { ritualConcepts } from "./rituals.js"; +export { artifactTypeConcepts } from "./artifact-types.js"; +export { roleConcepts } from "./roles.js"; diff --git a/src/methodology/concepts/phases.ts b/src/methodology/concepts/phases.ts new file mode 100644 index 0000000..947647c --- /dev/null +++ b/src/methodology/concepts/phases.ts @@ -0,0 +1,54 @@ +import type { ConceptDefinition } from "../types.js"; + +export const assessUseCase: ConceptDefinition = { + id: "assess-use-case", + name: "Assess Use Case", + category: "phase", + methodology: ["aem"], + summary: "First AEM phase — validate the extension scenario with business stakeholders.", + definition: + "The first phase of the SAP Application Extension Methodology (AEM). The team defines and justifies business scenarios needing extension on SAP BTP. The Product Owner creates use cases (UC-xxx) classified by extension type (in-app, side-by-side, hybrid) and priority. Use cases progress from draft to assessed to approved before the project can advance to Phase 2.", + whenToUse: + "At the start of any AEM extension project, before evaluating technologies or designing solutions.", + relatedArtifacts: ["use-case", "feature", "discovery"], + relatedTools: ["create_use_case", "get_current_phase", "advance_phase"], + relatedPersonas: ["po"], + relatedConcepts: ["assess-technology", "phase-gate", "use-case"], + source: "plugin:sap-aem#phase-1", +}; + +export const assessTechnology: ConceptDefinition = { + id: "assess-technology", + name: "Assess Technology", + category: "phase", + methodology: ["aem"], + summary: "Second AEM phase — evaluate BTP technologies and extension points.", + definition: + "The second phase of AEM. The Tech Lead evaluates BTP technologies and extension points for each approved use case. Tech assessments (TA-xxx) are created, linking to assessed/approved use cases and documenting BTP service feasibility. Assessments progress from draft to evaluated to recommended (or rejected) before the project can advance to Phase 3.", + whenToUse: + "After use cases are assessed/approved in Phase 1. Requires approved use cases before tech assessments can be created.", + relatedArtifacts: ["tech-assessment", "use-case"], + relatedTools: ["create_tech_assessment", "get_current_phase", "advance_phase"], + relatedPersonas: ["tl"], + relatedConcepts: ["assess-use-case", "define-solution", "phase-gate", "tech-assessment"], + source: "plugin:sap-aem#phase-2", +}; + +export const defineSolution: ConceptDefinition = { + id: "define-solution", + name: "Define Solution", + category: "phase", + methodology: ["aem"], + summary: "Third AEM phase — design the extension architecture.", + definition: + "The third and final phase of AEM. The Tech Lead designs the extension architecture for recommended technologies. Extension designs (XD-xxx) are created, linking to recommended tech assessments and documenting architecture patterns, BTP services, and integration points. Designs progress from draft to designed to validated to approved.", + whenToUse: + "After tech assessments are recommended in Phase 2. Requires recommended tech assessments before extension designs can be created.", + relatedArtifacts: ["extension-design", "tech-assessment", "epic"], + relatedTools: ["create_extension_design", "get_current_phase", "advance_phase"], + relatedPersonas: ["tl"], + relatedConcepts: ["assess-technology", "phase-gate", "extension-design"], + source: "plugin:sap-aem#phase-3", +}; + +export const phaseConcepts: ConceptDefinition[] = [assessUseCase, assessTechnology, defineSolution]; diff --git a/src/methodology/concepts/rituals.ts b/src/methodology/concepts/rituals.ts new file mode 100644 index 0000000..027604b --- /dev/null +++ b/src/methodology/concepts/rituals.ts @@ -0,0 +1,82 @@ +import type { ConceptDefinition } from "../types.js"; + +export const sprint0: ConceptDefinition = { + id: "sprint-0", + name: "Sprint 0", + category: "ritual", + methodology: ["generic-agile", "aem"], + summary: "Variable-duration bootstrapping phase before regular sprints begin.", + definition: + "A variable-duration bootstrapping phase that runs before regular sprints begin. Not time-boxed like a normal sprint. Sprint 0 ends when the team is ready to start Sprint 1 with a refined backlog and working infrastructure.", + whenToUse: + "When a project has work items (features, actions) but no sprints recorded yet, OR when starting a new AEM extension project.", + checklist: [ + { + category: "infrastructure-provisioning", + items: ["CI/CD setup", "repositories", "cloud services", "dev environments"], + }, + { + category: "backlog-refinement", + items: ["transition features to epics", "acceptance criteria", "estimates"], + }, + { + category: "ceremony-scheduling", + items: ["standup cadence", "refinement sessions", "reviews"], + }, + { + category: "integration-setup", + items: ["Jira", "Confluence", "other tool connections"], + }, + { + category: "aem-addendum", + items: [ + "phase gate checklists", + "BTP service access", + "extension design templates", + "iterative loop definitions", + ], + appliesWhen: "methodology=aem", + }, + ], + relatedArtifacts: ["sprint", "action", "task", "decision"], + relatedTools: ["create_sprint", "bootstrap_sprint_zero", "get_started"], + relatedPersonas: ["dm"], + relatedConcepts: ["assess-use-case", "phase-gate", "iterative-loop"], + source: "persona:dm#sprint-0", +}; + +export const phaseGate: ConceptDefinition = { + id: "phase-gate", + name: "Phase Gate", + category: "gate", + methodology: ["aem"], + summary: "Soft checkpoint between AEM phases that validates artifact readiness.", + definition: + "A soft checkpoint between AEM phases. Before advancing from one phase to the next, the system checks that prerequisite artifacts are in the expected status. Phase gates use soft enforcement — they warn about incomplete artifacts but do not block progression. The Delivery Manager is responsible for managing phase gate transitions.", + whenToUse: + "When the team believes they have completed the work for the current AEM phase and want to advance to the next one.", + relatedArtifacts: ["use-case", "tech-assessment", "extension-design"], + relatedTools: ["advance_phase", "get_current_phase", "generate_phase_status"], + relatedPersonas: ["dm"], + relatedConcepts: ["assess-use-case", "assess-technology", "define-solution"], + source: "plugin:sap-aem#phase-gate", +}; + +export const iterativeLoop: ConceptDefinition = { + id: "iterative-loop", + name: "Iterative Loop", + category: "loop", + methodology: ["aem"], + summary: "Feedback loops between AEM phases allowing revisiting earlier work.", + definition: + "The iterative loops between AEM phases that allow teams to revisit earlier work as new information emerges. Unlike a strict waterfall, AEM phases can feed back: findings in Assess Technology may require revisiting use cases, and Define Solution work may reveal technology gaps. These loops are mapped during Sprint 0 and managed by the Delivery Manager.", + whenToUse: + "When work in a later AEM phase reveals gaps or changes that affect earlier-phase artifacts (e.g., a tech assessment reveals a use case needs revision).", + relatedArtifacts: ["use-case", "tech-assessment", "extension-design"], + relatedTools: ["get_current_phase"], + relatedPersonas: ["dm"], + relatedConcepts: ["assess-use-case", "assess-technology", "define-solution", "phase-gate"], + source: "plugin:sap-aem#iterative-loop", +}; + +export const ritualConcepts: ConceptDefinition[] = [sprint0, phaseGate, iterativeLoop]; diff --git a/src/methodology/concepts/roles.ts b/src/methodology/concepts/roles.ts new file mode 100644 index 0000000..bcae483 --- /dev/null +++ b/src/methodology/concepts/roles.ts @@ -0,0 +1,70 @@ +import type { ConceptDefinition } from "../types.js"; + +export const productOwnerConcept: ConceptDefinition = { + id: "product-owner", + name: "Product Owner", + category: "role", + methodology: ["generic-agile", "aem"], + summary: "Defines product vision, manages backlog, and prioritizes features.", + definition: + "The Product Owner defines product vision, manages the backlog, and prioritizes features (or use cases in AEM). Responsible for ensuring the team builds the right things. In AEM, the PO is the Business Process Owner who defines and justifies extension use cases. The PO creates features, conducts discovery sessions with stakeholders, and approves scope.", + whenToUse: + "When defining what to build, prioritizing work, validating requirements with stakeholders, or approving features for implementation.", + relatedArtifacts: ["feature", "use-case", "discovery", "decision"], + relatedTools: ["create_feature", "create_use_case", "start_discovery", "set_persona"], + relatedPersonas: ["po"], + relatedConcepts: ["feature", "use-case", "discovery", "delivery-manager", "tech-lead"], + source: "persona:product-owner", +}; + +export const deliveryManagerConcept: ConceptDefinition = { + id: "delivery-manager", + name: "Delivery Manager", + category: "role", + methodology: ["generic-agile", "aem"], + summary: "Manages delivery, risks, sprints, and governance processes.", + definition: + "The Delivery Manager ensures the project is delivered on time, within scope, and with managed risks. Tracks progress, manages sprints, coordinates between team members, and ensures governance processes are followed. In AEM, the DM also manages phase gate transitions and generates extension portfolio reports. Responsible for Sprint 0 bootstrapping.", + whenToUse: + "When planning sprints, tracking progress, managing risks, running health checks, generating reports, or facilitating governance processes.", + relatedArtifacts: ["sprint", "action", "report", "meeting"], + relatedTools: [ + "create_sprint", + "generate_status_report", + "generate_risk_register", + "bootstrap_sprint_zero", + "set_persona", + ], + relatedPersonas: ["dm"], + relatedConcepts: ["sprint", "sprint-0", "product-owner", "tech-lead"], + source: "persona:delivery-manager", +}; + +export const techLeadConcept: ConceptDefinition = { + id: "tech-lead", + name: "Tech Lead", + category: "role", + methodology: ["generic-agile", "aem"], + summary: "Owns technical architecture, breaks features into epics and tasks.", + definition: + "The Tech Lead owns technical architecture, code quality, and technical decisions. Breaks approved features into implementation epics and tasks. In AEM, the TL is the Solution Architect who evaluates BTP technologies (Phase 2) and designs extension architecture (Phase 3). Creates tech assessments and extension designs.", + whenToUse: + "When making architectural decisions, breaking features into implementation work, evaluating technologies, or designing solutions.", + relatedArtifacts: ["epic", "task", "tech-assessment", "extension-design", "decision"], + relatedTools: [ + "create_epic", + "create_task", + "create_tech_assessment", + "create_extension_design", + "set_persona", + ], + relatedPersonas: ["tl"], + relatedConcepts: ["epic", "task", "product-owner", "delivery-manager"], + source: "persona:tech-lead", +}; + +export const roleConcepts: ConceptDefinition[] = [ + productOwnerConcept, + deliveryManagerConcept, + techLeadConcept, +]; diff --git a/src/methodology/registry.ts b/src/methodology/registry.ts new file mode 100644 index 0000000..c2e4068 --- /dev/null +++ b/src/methodology/registry.ts @@ -0,0 +1,82 @@ +import type { ConceptDefinition, ConceptSummary, ConceptFilter, Methodology } from "./types.js"; +import { ALL_CONCEPTS } from "./concepts/index.js"; + +export class ConceptRegistry { + private readonly concepts: Map; + + constructor(definitions: ConceptDefinition[] = ALL_CONCEPTS) { + this.concepts = new Map(); + for (const def of definitions) { + if (this.concepts.has(def.id)) { + throw new Error(`Duplicate concept ID: ${def.id}`); + } + this.concepts.set(def.id, def); + } + } + + get(id: string): ConceptDefinition | undefined { + return this.concepts.get(id); + } + + list(filter?: ConceptFilter): ConceptSummary[] { + let results = [...this.concepts.values()]; + + if (filter?.category) { + results = results.filter((c) => c.category === filter.category); + } + + if (filter?.methodology) { + const meth = filter.methodology; + results = results.filter((c) => c.methodology.includes(meth)); + } + + return results.map(toSummary); + } + + /** Returns the full concept definition, filtering AEM-only checklist items when methodology is not AEM. */ + explain(id: string, methodology?: Methodology): ConceptDefinition | undefined { + const concept = this.concepts.get(id); + if (!concept) return undefined; + + if (!concept.checklist || methodology === "aem") { + return { ...concept }; + } + + // Filter out checklist items that require AEM when methodology is not AEM + const filteredChecklist = concept.checklist.filter( + (item) => !item.appliesWhen?.includes("methodology=aem"), + ); + + return { ...concept, checklist: filteredChecklist }; + } + + /** All registered concept IDs. */ + ids(): string[] { + return [...this.concepts.keys()]; + } + + /** Total count of registered concepts. */ + get size(): number { + return this.concepts.size; + } +} + +function toSummary(concept: ConceptDefinition): ConceptSummary { + return { + id: concept.id, + name: concept.name, + category: concept.category, + methodology: concept.methodology, + summary: concept.summary, + }; +} + +/** Singleton registry instance used by tools. */ +let defaultRegistry: ConceptRegistry | undefined; + +export function getConceptRegistry(): ConceptRegistry { + if (!defaultRegistry) { + defaultRegistry = new ConceptRegistry(); + } + return defaultRegistry; +} diff --git a/src/methodology/tools.ts b/src/methodology/tools.ts new file mode 100644 index 0000000..d7b27c6 --- /dev/null +++ b/src/methodology/tools.ts @@ -0,0 +1,171 @@ +import { z } from "zod/v4"; +import { tool, type SdkMcpToolDefinition } from "@anthropic-ai/claude-agent-sdk"; +import type { MarvinProjectConfig } from "../core/config.js"; +import type { Methodology, ConceptCategory } from "./types.js"; +import { normalizeMethodology } from "./types.js"; +import { getConceptRegistry } from "./registry.js"; + +export function createConceptTools(config?: MarvinProjectConfig): SdkMcpToolDefinition[] { + const methodology = normalizeMethodology(config?.methodology); + + return [ + tool( + "list_concepts", + "List all methodology concepts Marvin knows about, optionally filtered by category or methodology. Returns concept summaries (id, name, category, methodology, summary).", + { + category: z + .enum(["phase", "artifact-type", "ritual", "role", "gate", "loop"]) + .optional() + .describe("Filter by concept category"), + methodology: z + .enum(["aem", "generic-agile"]) + .optional() + .describe("Filter by methodology. Defaults to the project's configured methodology."), + }, + async (args) => { + const registry = getConceptRegistry(); + const filterMethodology = args.methodology ?? methodology; + const concepts = registry.list({ + category: args.category as ConceptCategory | undefined, + methodology: filterMethodology as Methodology, + }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ concepts }, null, 2), + }, + ], + }; + }, + { annotations: { readOnlyHint: true } }, + ), + + tool( + "explain_concept", + "Get the canonical structured definition for a methodology concept. Returns definition, when_to_use, optional checklist, and related artifacts/tools/personas/concepts. Use list_concepts to discover available concept IDs.", + { + id: z.string().describe("Concept ID (kebab-case, e.g. 'sprint-0', 'assess-use-case')"), + }, + async (args) => { + const registry = getConceptRegistry(); + const concept = registry.explain(args.id, methodology); + + if (!concept) { + const available = registry.ids().join(", "); + return { + content: [ + { + type: "text" as const, + text: `Concept "${args.id}" not found. Use list_concepts() to see available concepts. Available IDs: ${available}`, + }, + ], + isError: true, + }; + } + + // Build response omitting undefined optional fields + const response: Record = { + id: concept.id, + name: concept.name, + category: concept.category, + methodology: concept.methodology, + definition: concept.definition, + }; + + if (concept.whenToUse) response.whenToUse = concept.whenToUse; + if (concept.checklist?.length) response.checklist = concept.checklist; + if (concept.relatedArtifacts?.length) response.relatedArtifacts = concept.relatedArtifacts; + if (concept.relatedTools?.length) response.relatedTools = concept.relatedTools; + if (concept.relatedPersonas?.length) response.relatedPersonas = concept.relatedPersonas; + if (concept.relatedConcepts?.length) response.relatedConcepts = concept.relatedConcepts; + response.source = concept.source; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + { annotations: { readOnlyHint: true } }, + ), + + tool( + "explain_phase", + "Convenience shortcut for explain_concept filtered to AEM phases. Accepts phase ID or human-readable name (e.g. 'assess-use-case' or 'Assess Use Case').", + { + name: z.string().describe("Phase ID or human name"), + }, + async (args) => { + const registry = getConceptRegistry(); + + // Try direct ID match first + let concept = registry.explain(args.name, methodology); + + // Try matching by human name (case-insensitive) + if (!concept) { + const phases = registry.list({ category: "phase", methodology }); + const match = phases.find((p) => p.name.toLowerCase() === args.name.toLowerCase()); + if (match) { + concept = registry.explain(match.id, methodology); + } + } + + if (!concept) { + const phases = registry.list({ category: "phase", methodology }); + const available = phases.map((p) => `${p.id} ("${p.name}")`).join(", "); + return { + content: [ + { + type: "text" as const, + text: `Phase "${args.name}" not found. Available phases: ${available}`, + }, + ], + isError: true, + }; + } + + if (concept.category !== "phase") { + return { + content: [ + { + type: "text" as const, + text: `"${args.name}" is a ${concept.category}, not a phase. Use explain_concept("${concept.id}") instead.`, + }, + ], + isError: true, + }; + } + + const response: Record = { + id: concept.id, + name: concept.name, + category: concept.category, + methodology: concept.methodology, + definition: concept.definition, + }; + + if (concept.whenToUse) response.whenToUse = concept.whenToUse; + if (concept.relatedArtifacts?.length) response.relatedArtifacts = concept.relatedArtifacts; + if (concept.relatedTools?.length) response.relatedTools = concept.relatedTools; + if (concept.relatedPersonas?.length) response.relatedPersonas = concept.relatedPersonas; + if (concept.relatedConcepts?.length) response.relatedConcepts = concept.relatedConcepts; + response.source = concept.source; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + { annotations: { readOnlyHint: true } }, + ), + ]; +} diff --git a/src/methodology/types.ts b/src/methodology/types.ts new file mode 100644 index 0000000..9a796ec --- /dev/null +++ b/src/methodology/types.ts @@ -0,0 +1,57 @@ +export type ConceptCategory = "phase" | "artifact-type" | "ritual" | "role" | "gate" | "loop"; + +export type Methodology = "generic-agile" | "aem"; + +export interface ChecklistItem { + category: string; + items: string[]; + appliesWhen?: string; +} + +export interface ConceptDefinition { + id: string; + name: string; + category: ConceptCategory; + methodology: Methodology[]; + summary: string; + definition: string; + whenToUse?: string; + checklist?: ChecklistItem[]; + relatedArtifacts?: string[]; + relatedTools?: string[]; + relatedPersonas?: string[]; + relatedConcepts?: string[]; + source: string; +} + +export interface ConceptSummary { + id: string; + name: string; + category: ConceptCategory; + methodology: Methodology[]; + summary: string; +} + +export interface ConceptFilter { + category?: ConceptCategory; + methodology?: Methodology; +} + +import { ConfigError } from "../core/errors.js"; + +const METHODOLOGY_ALIASES: Record = { + "sap-aem": "aem", + aem: "aem", + "generic-agile": "generic-agile", +}; + +/** Normalize a config methodology ID (e.g. "sap-aem") to a concept-registry Methodology value ("aem"). */ +export function normalizeMethodology(configValue?: string): Methodology { + if (!configValue) return "generic-agile"; + const mapped = METHODOLOGY_ALIASES[configValue]; + if (!mapped) { + const valid = Object.keys(METHODOLOGY_ALIASES).join(", "); + throw new ConfigError(`Unknown methodology "${configValue}". Valid values: ${valid}`); + } + return mapped; +} diff --git a/test/methodology/registry.test.ts b/test/methodology/registry.test.ts new file mode 100644 index 0000000..ea1fa18 --- /dev/null +++ b/test/methodology/registry.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect } from "vitest"; +import { ConceptRegistry } from "../../src/methodology/registry.js"; +import { ALL_CONCEPTS } from "../../src/methodology/concepts/index.js"; +import type { ConceptDefinition } from "../../src/methodology/types.js"; + +describe("ConceptRegistry", () => { + it("loads all built-in concepts without duplicates", () => { + const registry = new ConceptRegistry(); + expect(registry.size).toBe(ALL_CONCEPTS.length); + expect(registry.size).toBeGreaterThanOrEqual(20); + }); + + it("rejects duplicate concept IDs", () => { + const dupe: ConceptDefinition = { + id: "sprint-0", + name: "Duplicate", + category: "ritual", + methodology: ["generic-agile"], + summary: "dupe", + definition: "dupe", + source: "test", + }; + expect(() => new ConceptRegistry([...ALL_CONCEPTS, dupe])).toThrow("Duplicate concept ID"); + }); + + describe("get()", () => { + it("returns a concept by ID", () => { + const registry = new ConceptRegistry(); + const sprint0 = registry.get("sprint-0"); + expect(sprint0).toBeDefined(); + expect(sprint0!.name).toBe("Sprint 0"); + expect(sprint0!.category).toBe("ritual"); + }); + + it("returns undefined for unknown ID", () => { + const registry = new ConceptRegistry(); + expect(registry.get("nonexistent-id")).toBeUndefined(); + }); + }); + + describe("list()", () => { + it("returns all concepts with no filter", () => { + const registry = new ConceptRegistry(); + const all = registry.list(); + expect(all.length).toBe(ALL_CONCEPTS.length); + }); + + it("filters by category", () => { + const registry = new ConceptRegistry(); + const phases = registry.list({ category: "phase" }); + expect(phases.length).toBe(3); + for (const p of phases) { + expect(p.category).toBe("phase"); + } + }); + + it("filters by methodology", () => { + const registry = new ConceptRegistry(); + const aemOnly = registry.list({ methodology: "aem" }); + for (const c of aemOnly) { + expect(c.methodology).toContain("aem"); + } + + const genericOnly = registry.list({ methodology: "generic-agile" }); + for (const c of genericOnly) { + expect(c.methodology).toContain("generic-agile"); + } + + // AEM-only concepts should not appear in generic-agile list + const aemExclusive = aemOnly.filter((c) => !c.methodology.includes("generic-agile")); + expect(aemExclusive.length).toBeGreaterThan(0); + for (const c of aemExclusive) { + expect(genericOnly.find((g) => g.id === c.id)).toBeUndefined(); + } + }); + + it("combines category and methodology filters", () => { + const registry = new ConceptRegistry(); + const aemPhases = registry.list({ category: "phase", methodology: "aem" }); + expect(aemPhases.length).toBe(3); + + // Phases are AEM-only, so generic-agile should return none + const agilePhases = registry.list({ category: "phase", methodology: "generic-agile" }); + expect(agilePhases.length).toBe(0); + }); + + it("returns summary shape (no definition field)", () => { + const registry = new ConceptRegistry(); + const summaries = registry.list(); + for (const s of summaries) { + expect(s).toHaveProperty("id"); + expect(s).toHaveProperty("name"); + expect(s).toHaveProperty("category"); + expect(s).toHaveProperty("methodology"); + expect(s).toHaveProperty("summary"); + expect(s).not.toHaveProperty("definition"); + expect(s).not.toHaveProperty("checklist"); + } + }); + }); + + describe("explain()", () => { + it("returns full concept definition", () => { + const registry = new ConceptRegistry(); + const sprint0 = registry.explain("sprint-0"); + expect(sprint0).toBeDefined(); + expect(sprint0!.definition).toBeTruthy(); + expect(sprint0!.checklist).toBeDefined(); + expect(sprint0!.checklist!.length).toBeGreaterThanOrEqual(4); + }); + + it("returns undefined for unknown ID", () => { + const registry = new ConceptRegistry(); + expect(registry.explain("nonexistent")).toBeUndefined(); + }); + + it("includes AEM addendum when methodology is aem", () => { + const registry = new ConceptRegistry(); + const sprint0 = registry.explain("sprint-0", "aem"); + expect(sprint0).toBeDefined(); + const aemSection = sprint0!.checklist!.find((c) => c.category === "aem-addendum"); + expect(aemSection).toBeDefined(); + expect(aemSection!.items).toContain("phase gate checklists"); + }); + + it("excludes AEM addendum when methodology is generic-agile", () => { + const registry = new ConceptRegistry(); + const sprint0 = registry.explain("sprint-0", "generic-agile"); + expect(sprint0).toBeDefined(); + const aemSection = sprint0!.checklist!.find((c) => c.category === "aem-addendum"); + expect(aemSection).toBeUndefined(); + expect(sprint0!.checklist!.length).toBe(4); + }); + + it("excludes AEM addendum when no methodology specified", () => { + const registry = new ConceptRegistry(); + const sprint0 = registry.explain("sprint-0"); + expect(sprint0).toBeDefined(); + // Without methodology, we filter out AEM-only items (default behavior) + const aemSection = sprint0!.checklist!.find((c) => c.category === "aem-addendum"); + expect(aemSection).toBeUndefined(); + }); + }); + + describe("AC1.1 — required concepts exist", () => { + const registry = new ConceptRegistry(); + + const requiredConcepts = [ + "sprint-0", + "assess-use-case", + "assess-technology", + "define-solution", + "phase-gate", + "iterative-loop", + ]; + + const requiredArtifactTypes = [ + "feature", + "epic", + "task", + "sprint", + "decision", + "action", + "question", + "meeting", + "report", + "use-case", + "tech-assessment", + "extension-design", + "discovery", + "contribution", + ]; + + const requiredRoles = ["product-owner", "delivery-manager", "tech-lead"]; + + for (const id of requiredConcepts) { + it(`has concept: ${id}`, () => { + expect(registry.get(id)).toBeDefined(); + }); + } + + for (const id of requiredArtifactTypes) { + it(`has artifact type: ${id}`, () => { + const concept = registry.get(id); + expect(concept).toBeDefined(); + expect(concept!.category).toBe("artifact-type"); + }); + } + + for (const id of requiredRoles) { + it(`has role: ${id}`, () => { + const concept = registry.get(id); + expect(concept).toBeDefined(); + expect(concept!.category).toBe("role"); + }); + } + }); + + describe("concept data integrity", () => { + const registry = new ConceptRegistry(); + + it("all concepts have unique IDs", () => { + const ids = registry.ids(); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("all concept IDs are kebab-case", () => { + for (const id of registry.ids()) { + expect(id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/); + } + }); + + it("all concepts have a non-empty source", () => { + for (const id of registry.ids()) { + const concept = registry.get(id)!; + expect(concept.source).toBeTruthy(); + } + }); + + it("all concepts have at least one methodology", () => { + for (const id of registry.ids()) { + const concept = registry.get(id)!; + expect(concept.methodology.length).toBeGreaterThan(0); + } + }); + }); +}); diff --git a/test/methodology/tools.test.ts b/test/methodology/tools.test.ts new file mode 100644 index 0000000..6eabec8 --- /dev/null +++ b/test/methodology/tools.test.ts @@ -0,0 +1,208 @@ +import { describe, it, expect } from "vitest"; +import { createConceptTools } from "../../src/methodology/tools.js"; +import { phaseConcepts } from "../../src/methodology/concepts/index.js"; +import type { MarvinProjectConfig } from "../../src/core/config.js"; + +function extractHandler(tools: any[], name: string): (args: any) => Promise { + const t = tools.find((tool) => tool.name === name); + if (!t) throw new Error(`Tool "${name}" not found`); + return (t as any).handler; +} + +function parseResult(result: any): any { + return JSON.parse(result.content[0].text); +} + +describe("Concept tools", () => { + describe("list_concepts", () => { + it("returns all concepts for the project methodology", async () => { + const tools = createConceptTools({ + name: "test", + methodology: "generic-agile", + } as MarvinProjectConfig); + const handler = extractHandler(tools, "list_concepts"); + + const result = await handler({}); + const data = parseResult(result); + expect(data.concepts).toBeDefined(); + expect(data.concepts.length).toBeGreaterThan(0); + + // All returned concepts should include generic-agile + for (const c of data.concepts) { + expect(c.methodology).toContain("generic-agile"); + } + }); + + it("filters by category", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "list_concepts"); + + const result = await handler({ category: "phase" }); + const data = parseResult(result); + expect(data.concepts.length).toBe(phaseConcepts.length); + for (const c of data.concepts) { + expect(c.category).toBe("phase"); + } + }); + + it("filters by explicit methodology override", async () => { + const tools = createConceptTools({ + name: "test", + methodology: "generic-agile", + } as MarvinProjectConfig); + const handler = extractHandler(tools, "list_concepts"); + + const result = await handler({ methodology: "aem" }); + const data = parseResult(result); + // AEM filter should return AEM-specific concepts + const aemOnly = data.concepts.filter((c: any) => !c.methodology.includes("generic-agile")); + expect(aemOnly.length).toBeGreaterThan(0); + }); + + it("returns summary shape without definition field", async () => { + const tools = createConceptTools({ name: "test" } as MarvinProjectConfig); + const handler = extractHandler(tools, "list_concepts"); + + const result = await handler({}); + const data = parseResult(result); + for (const c of data.concepts) { + expect(c).toHaveProperty("id"); + expect(c).toHaveProperty("name"); + expect(c).toHaveProperty("summary"); + expect(c).not.toHaveProperty("definition"); + } + }); + }); + + describe("explain_concept", () => { + it("returns full definition for a valid concept", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_concept"); + + const result = await handler({ id: "sprint-0" }); + expect(result.isError).toBeFalsy(); + + const data = parseResult(result); + expect(data.id).toBe("sprint-0"); + expect(data.name).toBe("Sprint 0"); + expect(data.definition).toBeTruthy(); + expect(data.whenToUse).toBeTruthy(); + expect(data.checklist).toBeDefined(); + expect(data.relatedArtifacts).toBeDefined(); + expect(data.relatedTools).toBeDefined(); + expect(data.relatedPersonas).toBeDefined(); + expect(data.relatedConcepts).toBeDefined(); + expect(data.source).toBe("persona:dm#sprint-0"); + }); + + it("includes AEM addendum when methodology is aem", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_concept"); + + const result = await handler({ id: "sprint-0" }); + const data = parseResult(result); + const aemSection = data.checklist.find((c: any) => c.category === "aem-addendum"); + expect(aemSection).toBeDefined(); + }); + + it("excludes AEM addendum when methodology is generic-agile", async () => { + const tools = createConceptTools({ + name: "test", + methodology: "generic-agile", + } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_concept"); + + const result = await handler({ id: "sprint-0" }); + const data = parseResult(result); + const aemSection = data.checklist.find((c: any) => c.category === "aem-addendum"); + expect(aemSection).toBeUndefined(); + }); + + it("returns error for nonexistent concept", async () => { + const tools = createConceptTools({ name: "test" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_concept"); + + const result = await handler({ id: "nonexistent-id" }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("not found"); + expect(result.content[0].text).toContain("list_concepts"); + }); + + it("omits undefined optional fields", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_concept"); + + const result = await handler({ id: "phase-gate" }); + const data = parseResult(result); + // phase-gate has no checklist + expect(data.checklist).toBeUndefined(); + }); + }); + + describe("explain_phase", () => { + it("finds phase by ID", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_phase"); + + const result = await handler({ name: "assess-use-case" }); + expect(result.isError).toBeFalsy(); + const data = parseResult(result); + expect(data.id).toBe("assess-use-case"); + expect(data.category).toBe("phase"); + }); + + it("finds phase by human name (case-insensitive)", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_phase"); + + const result = await handler({ name: "Assess Use Case" }); + expect(result.isError).toBeFalsy(); + const data = parseResult(result); + expect(data.id).toBe("assess-use-case"); + }); + + it("finds phase by lowercase human name", async () => { + const tools = createConceptTools({ name: "test", methodology: "aem" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_phase"); + + const result = await handler({ name: "define solution" }); + expect(result.isError).toBeFalsy(); + const data = parseResult(result); + expect(data.id).toBe("define-solution"); + }); + + it("returns error for non-phase concept", async () => { + const tools = createConceptTools({ name: "test" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_phase"); + + const result = await handler({ name: "sprint-0" }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("ritual"); + expect(result.content[0].text).toContain("not a phase"); + }); + + it("returns error for unknown phase", async () => { + const tools = createConceptTools({ name: "test" } as MarvinProjectConfig); + const handler = extractHandler(tools, "explain_phase"); + + const result = await handler({ name: "nonexistent-phase" }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("not found"); + }); + }); + + describe("tool registration", () => { + it("creates exactly 3 tools", () => { + const tools = createConceptTools(); + expect(tools.length).toBe(3); + }); + + it("all tools have correct names", () => { + const tools = createConceptTools(); + const names = tools.map((t) => t.name); + expect(names).toContain("list_concepts"); + expect(names).toContain("explain_concept"); + expect(names).toContain("explain_phase"); + }); + }); +});