diff --git a/internal/cmd/mcp_register.go b/internal/cmd/mcp_register.go index 9b222a0..460a0c3 100644 --- a/internal/cmd/mcp_register.go +++ b/internal/cmd/mcp_register.go @@ -88,27 +88,13 @@ func promptMCPRegistration() { } } - // Use custom template to hide "type to filter" and typed characters - restore := useMultiSelectTemplateNoFilter() - defer restore() - fmt.Println() printTitle("MCP", "Register Symphony as an MCP server") fmt.Println(indent("Symphony MCP provides code convention tools for AI assistants")) - fmt.Println(indent("(Use arrows to move, space to select, enter to submit)")) fmt.Println() - // Multi-select prompt for tools - var selectedTools []string - prompt := &survey.MultiSelect{ - Message: "Select vibe coding tools to integrate:", - Options: mcpToolOptions, - } - - if err := survey.AskOne(prompt, &selectedTools); err != nil { - fmt.Println("Skipped MCP registration") - return - } + // Use Select with toggle behavior - Enter toggles selection, "Submit" confirms + selectedTools := selectToolsWithEnterToggle(mcpToolOptions) // If no tools selected, skip if len(selectedTools) == 0 { @@ -414,9 +400,12 @@ func getClaudeCodeInstructions() string { **Check MCP Status**: Verify Symphony MCP server is active. If unavailable, warn the user and do not proceed. -**Query Conventions**: Use ` + "`mcp__symphony__list_convention`" + ` to retrieve relevant rules. -- Select appropriate category: security, style, documentation, error_handling, architecture, performance, testing +**Query Categories First**: Use ` + "`mcp__symphony__list_category`" + ` to get available categories. +- **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category. + +**Query Conventions**: Use ` + "`mcp__symphony__list_convention`" + ` with a category from list_category. - Filter by languages as needed + **After Updating Rules/Categories**: If you add/edit/remove conventions or categories, run ` + "`mcp__symphony__convert`" + ` to regenerate derived policy and linter configs (then re-run validation if needed). ### 2. After Writing Code @@ -428,11 +417,12 @@ func getClaudeCodeInstructions() string { ## Workflow 1. Verify Symphony MCP is active -2. Query conventions for your task -3. Write code -4. Validate with Symphony -5. Fix violations -6. Commit +2. Query categories (list_category) +3. Query conventions with valid category (list_convention) +4. Write code +5. Validate with Symphony +6. Fix violations +7. Commit ` + symphonySectionEnd + "\n" } @@ -453,22 +443,15 @@ alwaysApply: true ### Before Code Generation 1. **Verify Symphony MCP is active** - If not available, stop and warn user -2. **Query conventions** - Use ` + "`symphony/list_convention`" + ` with appropriate category and language -3. **After updating conventions/categories** - Use ` + "`symphony/convert`" + ` to regenerate derived policy and linter configs +2. **Query categories first** - Use ` + "`symphony/list_category`" + ` to get available categories + - **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category. +3. **Query conventions** - Use ` + "`symphony/list_convention`" + ` with a category from step 2 +4. **After updating conventions/categories** - Use ` + "`symphony/convert`" + ` to regenerate derived policy and linter configs ### After Code Generation 1. **Validate all changes** - Use ` + "`symphony/validate_code`" + ` 2. **Fix violations** - Address issues before committing -## Convention Categories -- security -- style -- documentation -- error_handling -- architecture -- performance -- testing - --- *Auto-generated by Symphony* @@ -491,10 +474,11 @@ This project uses Symphony MCP for automated code convention management. ### Before Writing Code 1. Verify Symphony MCP server is active. If not available, warn user and stop. -2. Query relevant conventions using symphony/list_convention tool. - - Categories: security, style, documentation, error_handling, architecture, performance, testing - - Filter by programming language -3. If you add/edit/remove conventions or categories, run symphony/convert (then validate again if needed). +2. Query available categories using symphony/list_category tool. + - **IMPORTANT**: Do NOT invent category names. Only use categories returned by list_category. +3. Query relevant conventions using symphony/list_convention tool with a category from step 2. + - Filter by programming language as needed +4. If you add/edit/remove conventions or categories, run symphony/convert (then validate again if needed). ### After Writing Code 1. Always validate changes using symphony/validate_code tool (validates all git changes) @@ -502,9 +486,107 @@ This project uses Symphony MCP for automated code convention management. 3. Only commit after validation passes ## Workflow -Check MCP → Query Conventions → Write Code → Validate → Fix → Commit +Check MCP → Query Categories → Query Conventions → Write Code → Validate → Fix → Commit --- Auto-generated by Symphony ` } + +// selectToolsWithEnterToggle allows users to select tools using Enter key to toggle +// and "Submit" option to confirm selection +func selectToolsWithEnterToggle(tools []string) []string { + selected := make(map[string]bool) + lastChoice := "" // Track last selected option to maintain cursor position + + // Use custom template to hide message output + restore := useSelectTemplateNoMessage() + defer restore() + + // Print header once with cyan hint + fmt.Printf("Select tools to integrate: %s\n", colorize(cyan, "[Enter: toggle]")) + + for { + // Count selected items + count := 0 + for _, v := range selected { + if v { + count++ + } + } + + // Build submit option with count + var submitOption string + if count > 0 { + submitOption = fmt.Sprintf("✓ Submit (%d selected)", count) + } else { + submitOption = "✓ Submit" + } + + // Build options with selection indicators + options := make([]string, 0, len(tools)+1) + for _, tool := range tools { + if selected[tool] { + options = append(options, fmt.Sprintf("[x] %s", tool)) + } else { + options = append(options, fmt.Sprintf("[ ] %s", tool)) + } + } + options = append(options, submitOption) + + // Find default option index based on last choice + defaultOption := options[0] + if lastChoice != "" { + for _, opt := range options { + // Match by tool name (ignore [x]/[ ] prefix and submit option changes) + if strings.HasPrefix(lastChoice, "✓") && strings.HasPrefix(opt, "✓") { + defaultOption = opt + break + } + for _, tool := range tools { + if strings.Contains(lastChoice, tool) && strings.Contains(opt, tool) { + defaultOption = opt + break + } + } + } + } + + // Show selection prompt + var choice string + prompt := &survey.Select{ + Message: "", + Options: options, + Default: defaultOption, + } + + if err := survey.AskOne(prompt, &choice); err != nil { + // User cancelled + return nil + } + + lastChoice = choice + + // Check if Submit was selected + if strings.HasPrefix(choice, "✓ Submit") { + break + } + + // Toggle the selected tool + for _, tool := range tools { + if strings.Contains(choice, tool) { + selected[tool] = !selected[tool] + break + } + } + } + + // Collect selected tools + var result []string + for _, tool := range tools { + if selected[tool] { + result = append(result, tool) + } + } + return result +} diff --git a/internal/cmd/survey_templates.go b/internal/cmd/survey_templates.go index 611c539..5feb7ac 100644 --- a/internal/cmd/survey_templates.go +++ b/internal/cmd/survey_templates.go @@ -24,29 +24,6 @@ var selectTemplateNoFilter = ` {{- end}} {{- end}}` -// Custom MultiSelect template that: -// 1. Removes "type to filter" hint -// 2. Hides typed characters (removes .FilterMessage) -// 3. Shows clear control instructions -var multiSelectTemplateNoFilter = ` -{{- define "option"}} - {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}} - {{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}} - {{- color "reset"}} - {{- " "}}{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{color "reset"}}{{end}} -{{end}} -{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }}{{color "reset"}} -{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} -{{- else }} - {{- " "}}{{- color "cyan"}}[Arrow keys: move, Space: toggle, Enter: confirm]{{color "reset"}} - {{- "\n"}} - {{- range $ix, $option := .PageEntries}} - {{- template "option" $.IterateOption $ix $option}} - {{- end}} -{{- end}}` - // useSelectTemplateNoFilter temporarily overrides the global Select template // to hide "type to filter" and prevent typed characters from showing. // Returns a restore function that must be called to restore the original template. @@ -58,13 +35,26 @@ func useSelectTemplateNoFilter() func() { } } -// useMultiSelectTemplateNoFilter temporarily overrides the global MultiSelect template -// to hide "type to filter" and prevent typed characters from showing. -// Returns a restore function that must be called to restore the original template. -func useMultiSelectTemplateNoFilter() func() { - original := survey.MultiSelectQuestionTemplate - survey.MultiSelectQuestionTemplate = multiSelectTemplateNoFilter +// Custom Select template with no message output - only shows options +var selectTemplateNoMessage = ` +{{- define "option"}} + {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} + {{- .CurrentOpt.Value}} + {{- color "reset"}} +{{end}} +{{- if .ShowAnswer}}{{/* hide answer line */}} +{{- else}} + {{- range $ix, $option := .PageEntries}} + {{- template "option" $.IterateOption $ix $option}} + {{- end}} +{{- end}}` + +// useSelectTemplateNoMessage temporarily overrides the global Select template +// to hide message and answer output. Only shows options. +func useSelectTemplateNoMessage() func() { + original := survey.SelectQuestionTemplate + survey.SelectQuestionTemplate = selectTemplateNoMessage return func() { - survey.MultiSelectQuestionTemplate = original + survey.SelectQuestionTemplate = original } } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index e1a0d77..0104f3b 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -853,48 +853,36 @@ func (s *Server) convertUserPolicy(userPolicyPath, codePolicyPath string) error return ConvertPolicyWithLLM(userPolicyPath, codePolicyPath) } -// getRBACInfo returns RBAC information for the current user +// getRBACInfo returns RBAC information for the current role func (s *Server) getRBACInfo() string { - // Try to get current user - username, err := git.GetCurrentUser() - if err != nil { - // Not in a git environment or user not configured + // Get current role from .env + userRole, err := roles.GetCurrentRole() + if err != nil || userRole == "" { + // No role selected return "" } - // Get user's role - userRole, err := roles.GetUserRole(username) - if err != nil { - // Roles not configured - return "" - } - - if userRole == "none" { - return fmt.Sprintf("⚠️ RBAC: User '%s' has no assigned role. You may not have permission to modify files.", username) - } - // Load user policy to get RBAC details userPolicy, err := roles.LoadUserPolicyFromRepo() if err != nil { // User policy not available - return fmt.Sprintf("🔐 RBAC: Current user '%s' has role '%s'", username, userRole) + return fmt.Sprintf("🔐 RBAC: Current role '%s'", userRole) } // Check if RBAC is defined if userPolicy.RBAC == nil || userPolicy.RBAC.Roles == nil { - return fmt.Sprintf("🔐 RBAC: Current user '%s' has role '%s' (no restrictions defined)", username, userRole) + return fmt.Sprintf("🔐 RBAC: Current role '%s' (no restrictions defined)", userRole) } // Get role configuration roleConfig, exists := userPolicy.RBAC.Roles[userRole] if !exists { - return fmt.Sprintf("⚠️ RBAC: User '%s' has role '%s', but role is not defined in policy", username, userRole) + return fmt.Sprintf("⚠️ RBAC: Role '%s' is not defined in policy", userRole) } // Build RBAC info message var rbacMsg strings.Builder rbacMsg.WriteString("🔐 RBAC Information:\n") - rbacMsg.WriteString(fmt.Sprintf(" User: %s\n", username)) rbacMsg.WriteString(fmt.Sprintf(" Role: %s\n", userRole)) if len(roleConfig.AllowWrite) > 0 { diff --git a/internal/policy/templates/demo-template.json b/internal/policy/templates/demo-template.json index 51868ed..bb17668 100644 --- a/internal/policy/templates/demo-template.json +++ b/internal/policy/templates/demo-template.json @@ -1,5 +1,9 @@ { "version": "1.0.0", + "category": [ + {"name": "naming", "description": "Naming conventions for classes, methods, and variables"}, + {"name": "error_handling", "description": "Error handling and exception management rules"} + ], "defaults": { "languages": ["java"], "severity": "error" diff --git a/internal/policy/templates/react-template.json b/internal/policy/templates/react-template.json index 82d0d6b..02848b0 100644 --- a/internal/policy/templates/react-template.json +++ b/internal/policy/templates/react-template.json @@ -1,5 +1,11 @@ { "version": "1.0.0", + "category": [ + {"name": "naming", "description": "Naming conventions for components, variables, and functions"}, + {"name": "error_handling", "description": "Error handling and React Hooks best practices"}, + {"name": "formatting", "description": "Code formatting and component structure rules"}, + {"name": "performance", "description": "Performance optimization and rendering efficiency"} + ], "defaults": { "languages": ["javascript", "typescript", "jsx", "tsx"], "severity": "error", diff --git a/internal/policy/templates/typescript-template.json b/internal/policy/templates/typescript-template.json index f460f19..83a3df0 100644 --- a/internal/policy/templates/typescript-template.json +++ b/internal/policy/templates/typescript-template.json @@ -1,5 +1,11 @@ { "version": "1.0.0", + "category": [ + {"name": "error_handling", "description": "Type safety and error handling rules"}, + {"name": "naming", "description": "Naming conventions for types, interfaces, and variables"}, + {"name": "formatting", "description": "Code formatting and module structure"}, + {"name": "documentation", "description": "Documentation rules (JSDoc, type annotations)"} + ], "defaults": { "languages": ["typescript"], "severity": "error", diff --git a/internal/policy/templates/vue-template.json b/internal/policy/templates/vue-template.json index 2dd1f9d..061615c 100644 --- a/internal/policy/templates/vue-template.json +++ b/internal/policy/templates/vue-template.json @@ -1,5 +1,11 @@ { "version": "1.0.0", + "category": [ + {"name": "naming", "description": "Naming conventions for components and files"}, + {"name": "formatting", "description": "Code formatting and Composition API structure"}, + {"name": "error_handling", "description": "Error handling and reactive state management"}, + {"name": "performance", "description": "Performance optimization and computed properties"} + ], "defaults": { "languages": ["javascript", "typescript", "vue"], "severity": "error", diff --git a/internal/server/static/index.html b/internal/server/static/index.html index e74cf8b..160d3ba 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -216,7 +216,7 @@