Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 121 additions & 39 deletions internal/cmd/mcp_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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"
}

Expand All @@ -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*
Expand All @@ -491,20 +474,119 @@ 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)
2. Fix any violations found
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 이거 원래 이렇게 직접 만들어야 함? cobra에서 제공 안하고?

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
}
50 changes: 20 additions & 30 deletions internal/cmd/survey_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
}
28 changes: 8 additions & 20 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions internal/policy/templates/demo-template.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 6 additions & 0 deletions internal/policy/templates/react-template.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 6 additions & 0 deletions internal/policy/templates/typescript-template.json
Original file line number Diff line number Diff line change
@@ -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)"}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category array defines "error_handling", "naming", "formatting", and "documentation", but rules with id "5" (line 47) and id "7" (line 57) use category "performance" which is not defined in the category array. This will result in these rules being assigned to a non-existent category. Add the "performance" category to the category array or change these rules' categories to one of the defined categories.

Suggested change
{"name": "documentation", "description": "Documentation rules (JSDoc, type annotations)"}
{"name": "documentation", "description": "Documentation rules (JSDoc, type annotations)"},
{"name": "performance", "description": "Performance optimization and best practices"}

Copilot uses AI. Check for mistakes.
],
"defaults": {
"languages": ["typescript"],
"severity": "error",
Expand Down
6 changes: 6 additions & 0 deletions internal/policy/templates/vue-template.json
Original file line number Diff line number Diff line change
@@ -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"}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category array defines "naming", "formatting", "error_handling", and "performance", but rule with id "3" (line 38) uses category "documentation" which is not defined in the category array. This will result in the rule being assigned to a non-existent category. Add the "documentation" category to the category array or change the rule's category to one of the defined categories.

Suggested change
{"name": "performance", "description": "Performance optimization and computed properties"}
{"name": "performance", "description": "Performance optimization and computed properties"},
{"name": "documentation", "description": "Prop types, default values, and code documentation"}

Copilot uses AI. Check for mistakes.
],
"defaults": {
"languages": ["javascript", "typescript", "vue"],
"severity": "error",
Expand Down
2 changes: 1 addition & 1 deletion internal/server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ <h3 class="text-xl font-semibold text-slate-900 flex items-center gap-2">

<!-- Import Modal -->
<div id="import-modal" class="fixed inset-0 modal-overlay flex items-center justify-center p-4 z-50 hidden">
<div class="w-full max-w-lg bg-white rounded-lg shadow-xl border border-gray-300">
<div class="w-[28rem] max-w-[calc(100vw-2rem)] bg-white rounded-lg shadow-xl border border-gray-300">
<header class="p-6 flex items-center justify-between border-b border-gray-300">
<h3 class="text-xl font-semibold text-slate-900">📥 컨벤션 가져오기</h3>
<button id="close-import-modal" class="text-slate-500 hover:text-slate-800 text-2xl">&times;</button>
Expand Down
Loading