Skip to content

Commit 99fdedd

Browse files
committed
fix: correct OpenCode format from skills to commands
Per OpenCode docs (https://opencode.ai/docs/commands/): - Changed directory from .opencode/skills/ to .opencode/commands/ - Changed file format from <name>/SKILL.md to <name>.md (flat structure) - Changed frontmatter from 'name' + 'description' to just 'description' Updated: - Initializer to create commands in correct format - Doctor checks to validate commands structure - All tests to match new format - Documentation in TUTORIAL.md and CHANGELOG.md
1 parent 9878c62 commit 99fdedd

8 files changed

Lines changed: 205 additions & 283 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- **OpenCode Support**: Full integration with OpenCode AI assistant
1313
- Bootstrap creates `opencode.json` at project root with MCP server configuration
14-
- Skills directory `.opencode/skills/` with TaskWing slash commands (tw-next, tw-done, tw-brief, etc.)
14+
- Commands directory `.opencode/commands/` with TaskWing slash commands (tw-next, tw-done, tw-brief, etc.)
1515
- Plugin hooks `.opencode/plugins/taskwing-hooks.js` for autonomous task execution using Bun's ctx.$ API
16-
- Doctor health checks validate OpenCode configuration (MCP, skills, plugins)
16+
- Doctor health checks validate OpenCode configuration (MCP, commands, plugins)
1717
- Integration tests and CI job for OpenCode-specific validation
18-
- Documentation in TUTORIAL.md with opencode.json example, skill structure, and plugin format
18+
- Documentation in TUTORIAL.md with opencode.json example, command structure, and plugin format
1919
- **Workspace-Aware Knowledge Scoping**: Full monorepo support for knowledge management
2020
- New `tw workspaces` command to list detected workspaces in a monorepo
2121
- `--workspace` and `--all` flags for `tw list` and `tw context` commands

cmd/doctor.go

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ func checkCodexMCP() DoctorCheck {
362362
// 1. Checks if opencode.json exists at project root
363363
// 2. Validates JSON structure and taskwing-mcp entry
364364
// 3. Verifies command is JSON array and type is "local"
365-
// 4. Validates .opencode/skills/*/SKILL.md files
365+
// 4. Validates .opencode/commands/*.md files
366366
func checkOpenCodeMCP(cwd string) []DoctorCheck {
367367
checks := []DoctorCheck{}
368368

@@ -468,108 +468,91 @@ func checkOpenCodeMCP(cwd string) []DoctorCheck {
468468
Message: fmt.Sprintf("%s registered in opencode.json", serverName),
469469
})
470470

471-
// Check 7: Validate skills (optional - warn if issues)
472-
skillsChecks := checkOpenCodeSkills(cwd)
473-
checks = append(checks, skillsChecks...)
471+
// Check 7: Validate commands (optional - warn if issues)
472+
commandsChecks := checkOpenCodeCommands(cwd)
473+
checks = append(checks, commandsChecks...)
474474

475475
return checks
476476
}
477477

478-
// checkOpenCodeSkills validates .opencode/skills/*/SKILL.md files
479-
func checkOpenCodeSkills(cwd string) []DoctorCheck {
478+
// checkOpenCodeCommands validates .opencode/commands/*.md files
479+
// OpenCode commands use flat structure: .opencode/commands/<name>.md with description frontmatter
480+
// See: https://opencode.ai/docs/commands/
481+
func checkOpenCodeCommands(cwd string) []DoctorCheck {
480482
checks := []DoctorCheck{}
481483

482-
skillsDir := filepath.Join(cwd, ".opencode", "skills")
483-
if _, err := os.Stat(skillsDir); os.IsNotExist(err) {
484-
// No skills directory - not an error, skills are optional
484+
commandsDir := filepath.Join(cwd, ".opencode", "commands")
485+
if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
486+
// No commands directory - not an error, commands are optional
485487
return checks
486488
}
487489

488-
// Find all SKILL.md files
489-
pattern := filepath.Join(skillsDir, "*", "SKILL.md")
490+
// Find all .md files in commands directory (flat structure)
491+
pattern := filepath.Join(commandsDir, "*.md")
490492
matches, err := filepath.Glob(pattern)
491493
if err != nil || len(matches) == 0 {
492-
// No skills found - not an error
494+
// No commands found - not an error
493495
return checks
494496
}
495497

496-
validSkills := 0
497-
invalidSkills := []string{}
498+
validCommands := 0
499+
invalidCommands := []string{}
498500

499-
for _, skillPath := range matches {
500-
// Get the skill directory name
501-
skillDirName := filepath.Base(filepath.Dir(skillPath))
501+
for _, cmdPath := range matches {
502+
// Skip marker file
503+
if filepath.Base(cmdPath) == ".taskwing-managed" {
504+
continue
505+
}
506+
507+
// Get the command name from filename (without .md extension)
508+
cmdName := strings.TrimSuffix(filepath.Base(cmdPath), ".md")
502509

503-
// Read and validate SKILL.md
504-
content, err := os.ReadFile(skillPath)
510+
// Read and validate command file
511+
content, err := os.ReadFile(cmdPath)
505512
if err != nil {
506-
invalidSkills = append(invalidSkills, skillDirName+": unreadable")
513+
invalidCommands = append(invalidCommands, cmdName+": unreadable")
507514
continue
508515
}
509516

510517
// Check for frontmatter markers
511518
contentStr := string(content)
512519
if !strings.HasPrefix(contentStr, "---") {
513-
invalidSkills = append(invalidSkills, skillDirName+": missing YAML frontmatter")
520+
invalidCommands = append(invalidCommands, cmdName+": missing YAML frontmatter")
514521
continue
515522
}
516523

517524
// Extract frontmatter
518525
parts := strings.SplitN(contentStr, "---", 3)
519526
if len(parts) < 3 {
520-
invalidSkills = append(invalidSkills, skillDirName+": incomplete frontmatter")
527+
invalidCommands = append(invalidCommands, cmdName+": incomplete frontmatter")
521528
continue
522529
}
523530

524531
frontmatter := parts[1]
525532

526-
// Check for required fields (simple validation - name and description)
527-
hasName := strings.Contains(frontmatter, "name:")
533+
// Check for required field (OpenCode only requires description)
528534
hasDescription := strings.Contains(frontmatter, "description:")
529535

530-
if !hasName || !hasDescription {
531-
missing := []string{}
532-
if !hasName {
533-
missing = append(missing, "name")
534-
}
535-
if !hasDescription {
536-
missing = append(missing, "description")
537-
}
538-
invalidSkills = append(invalidSkills, skillDirName+": missing "+strings.Join(missing, ", "))
536+
if !hasDescription {
537+
invalidCommands = append(invalidCommands, cmdName+": missing description")
539538
continue
540539
}
541540

542-
// Extract name from frontmatter and verify it matches directory
543-
// Simple extraction - look for "name: value" pattern
544-
for _, line := range strings.Split(frontmatter, "\n") {
545-
line = strings.TrimSpace(line)
546-
if strings.HasPrefix(line, "name:") {
547-
nameValue := strings.TrimSpace(strings.TrimPrefix(line, "name:"))
548-
// Remove quotes if present
549-
nameValue = strings.Trim(nameValue, "\"'")
550-
if nameValue != skillDirName {
551-
invalidSkills = append(invalidSkills, fmt.Sprintf("%s: name mismatch (name: %q != dir: %q)", skillDirName, nameValue, skillDirName))
552-
continue
553-
}
554-
break
555-
}
556-
}
557-
558-
validSkills++
541+
validCommands++
559542
}
560543

561-
if len(invalidSkills) > 0 {
544+
if len(invalidCommands) > 0 {
562545
checks = append(checks, DoctorCheck{
563-
Name: "Skills (OpenCode)",
546+
Name: "Commands (OpenCode)",
564547
Status: "warn",
565-
Message: fmt.Sprintf("%d valid, %d invalid skills", validSkills, len(invalidSkills)),
566-
Hint: "Invalid: " + strings.Join(invalidSkills, "; ") + ". For development, use taskwing-local-dev-mcp",
548+
Message: fmt.Sprintf("%d valid, %d invalid commands", validCommands, len(invalidCommands)),
549+
Hint: "Invalid: " + strings.Join(invalidCommands, "; ") + ". For development, use taskwing-local-dev-mcp",
567550
})
568-
} else if validSkills > 0 {
551+
} else if validCommands > 0 {
569552
checks = append(checks, DoctorCheck{
570-
Name: "Skills (OpenCode)",
553+
Name: "Commands (OpenCode)",
571554
Status: "ok",
572-
Message: fmt.Sprintf("%d skills configured", validSkills),
555+
Message: fmt.Sprintf("%d commands configured", validCommands),
573556
})
574557
}
575558

0 commit comments

Comments
 (0)