-
Notifications
You must be signed in to change notification settings - Fork 253
feat: add stdio-only LSP server for agentic workflow files #17184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6a464be
5c1d649
a802b2a
bb31125
714aa30
b2f467a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||
| package cli | ||||||||
|
|
||||||||
| import ( | ||||||||
| "fmt" | ||||||||
| "os" | ||||||||
|
|
||||||||
| "github.com/github/gh-aw/pkg/console" | ||||||||
| "github.com/github/gh-aw/pkg/logger" | ||||||||
| "github.com/github/gh-aw/pkg/lsp" | ||||||||
| "github.com/spf13/cobra" | ||||||||
| ) | ||||||||
|
|
||||||||
| var lspLog = logger.New("cli:lsp") | ||||||||
|
|
||||||||
| // NewLSPCommand creates the lsp command | ||||||||
| func NewLSPCommand() *cobra.Command { | ||||||||
| cmd := &cobra.Command{ | ||||||||
| Use: "lsp", | ||||||||
| Short: "Start the Language Server Protocol server for workflow files", | ||||||||
| Long: `Start a Language Server Protocol (LSP) server that provides IDE support | ||||||||
| for agentic workflow Markdown files. | ||||||||
|
|
||||||||
| The LSP server communicates over stdio using JSON-RPC 2.0 and provides: | ||||||||
| - Diagnostics (YAML syntax errors and schema validation) | ||||||||
| - Hover information (schema descriptions for frontmatter keys) | ||||||||
| - Completions (frontmatter keys, values, and workflow snippets) | ||||||||
|
|
||||||||
| The server is stateless (session-only memory, no daemon, no disk state). | ||||||||
|
|
||||||||
| Examples: | ||||||||
| gh aw lsp # Start LSP server on stdio | ||||||||
| echo '...' | gh aw lsp # Pipe LSP messages`, | ||||||||
|
Comment on lines
+31
to
+32
|
||||||||
| gh aw lsp # Start LSP server on stdio | |
| echo '...' | gh aw lsp # Pipe LSP messages`, | |
| gh aw lsp # Start LSP server on stdio (for IDE integration)`, |
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / mcp-server-compile-test
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / actions-build
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Compiler
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Misc Part 2
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / update
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Completion & Other
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Rendering & Bundling
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Progress Flag
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI HTTP MCP Connect
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Tools & MCP
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI MCP Other
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Audit Logs & Firewall
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Features
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CMD Tests
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration Add Workflows
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI MCP Connectivity
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Compile Workflows
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / audit
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Parser Remote Fetch & Cache
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Add & List Commands
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Compile & Poutine
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Validation
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Update Command
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Parser Location & Validation
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: Workflow Actions & Containers
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Build & Test on macos-latest
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Alpine Container Test
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Docker Build
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / build
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / security
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Build & Test on windows-latest
not enough arguments in call to lsp.NewServer
Check failure on line 45 in pkg/cli/lsp_command.go
GitHub Actions / Integration: CLI Security Tools
not enough arguments in call to lsp.NewServer
Copilot
AI
Feb 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The NewServer function signature expects 4 parameters including a version string, but RunLSP() only passes 3 parameters (stdin, stdout, stderr). The version parameter should be passed using cli.GetVersion().
Change line 45 to:
server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr, cli.GetVersion())This will ensure the LSP server can report its version in the initialize response.
| server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr) | |
| server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr, GetVersion()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| package lsp | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/github/gh-aw/pkg/logger" | ||
| ) | ||
|
|
||
| var completionLog = logger.New("lsp:completion") | ||
|
|
||
| // HandleCompletion computes completion items for a position in a document. | ||
| func HandleCompletion(snap *DocumentSnapshot, pos Position, sp *SchemaProvider) *CompletionList { | ||
| if snap == nil || sp == nil { | ||
| return &CompletionList{} | ||
| } | ||
|
|
||
| // If no frontmatter, suggest the skeleton snippet | ||
| if !snap.HasFrontmatter { | ||
| return &CompletionList{ | ||
| Items: snippetCompletions(), | ||
| } | ||
| } | ||
|
|
||
| // Only provide completions inside frontmatter | ||
| if !snap.PositionInFrontmatter(pos) { | ||
| return &CompletionList{} | ||
| } | ||
|
|
||
| // Compute the YAML path at the cursor position | ||
| yamlLine := pos.Line - snap.FrontmatterStartLine - 1 | ||
| path, currentKey := YAMLPathAtPosition(snap.FrontmatterYAML, yamlLine) | ||
|
|
||
| completionLog.Printf("Completion at line %d: path=%v, key=%s", pos.Line, path, currentKey) | ||
|
|
||
| var items []CompletionItem | ||
|
|
||
| if len(path) == 0 && currentKey == "" { | ||
| // At top level with no context — suggest top-level keys | ||
| items = topLevelKeyCompletions(sp) | ||
| } else if len(path) == 0 && currentKey != "" { | ||
| // Typing a top-level key — filter top-level keys | ||
| items = filterCompletions(topLevelKeyCompletions(sp), currentKey) | ||
| } else { | ||
| // Nested context — first check if there are enum values for the value | ||
| if currentKey != "" { | ||
| enums := sp.EnumValues(append(path, currentKey)) | ||
| if len(enums) > 0 { | ||
| items = enumCompletions(enums) | ||
| } | ||
| } | ||
|
|
||
| // Also suggest nested keys at this path | ||
| if len(items) == 0 { | ||
| props := sp.NestedProperties(path) | ||
| if len(props) > 0 { | ||
| items = propertyCompletions(props) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Always include snippets when at top level | ||
| if len(path) == 0 { | ||
| items = append(items, snippetCompletions()...) | ||
| } | ||
|
|
||
| return &CompletionList{ | ||
| IsIncomplete: false, | ||
| Items: items, | ||
| } | ||
| } | ||
|
|
||
| // topLevelKeyCompletions returns completion items for all top-level frontmatter keys. | ||
| func topLevelKeyCompletions(sp *SchemaProvider) []CompletionItem { | ||
| props := sp.TopLevelProperties() | ||
| return propertyCompletions(props) | ||
| } | ||
|
|
||
| // propertyCompletions converts PropertyInfo items into CompletionItem items. | ||
| func propertyCompletions(props []PropertyInfo) []CompletionItem { | ||
| sort.Slice(props, func(i, j int) bool { | ||
| // Required first, then alphabetical | ||
| if props[i].Required != props[j].Required { | ||
| return props[i].Required | ||
| } | ||
| return props[i].Name < props[j].Name | ||
| }) | ||
|
|
||
| items := make([]CompletionItem, 0, len(props)) | ||
| for i, p := range props { | ||
| item := CompletionItem{ | ||
| Label: p.Name, | ||
| Kind: CompletionItemKindProperty, | ||
| Detail: p.Type, | ||
| Deprecated: p.Deprecated, | ||
| } | ||
|
|
||
| if p.Description != "" { | ||
| item.Documentation = &MarkupContent{ | ||
| Kind: "markdown", | ||
| Value: p.Description, | ||
| } | ||
| } | ||
|
|
||
| if p.Required { | ||
| item.Detail = p.Type + " (required)" | ||
| } | ||
|
|
||
| // Insert text with colon and space | ||
| item.InsertText = p.Name + ": " | ||
| item.InsertTextFormat = InsertTextFormatPlainText | ||
|
|
||
| // Sort required items first using prefix | ||
| if p.Required { | ||
| item.SortText = "0_" + p.Name | ||
| } else { | ||
| item.SortText = "1_" + padIndex(i) | ||
| } | ||
|
|
||
| items = append(items, item) | ||
| } | ||
|
|
||
| return items | ||
| } | ||
|
|
||
| // enumCompletions returns completion items for enum values. | ||
| func enumCompletions(values []string) []CompletionItem { | ||
| items := make([]CompletionItem, 0, len(values)) | ||
| for _, v := range values { | ||
| items = append(items, CompletionItem{ | ||
| Label: v, | ||
| Kind: CompletionItemKindEnum, | ||
| InsertText: v, | ||
| InsertTextFormat: InsertTextFormatPlainText, | ||
| }) | ||
| } | ||
| return items | ||
| } | ||
|
|
||
| // snippetCompletions returns pre-built snippet completions for common workflow patterns. | ||
| func snippetCompletions() []CompletionItem { | ||
| return []CompletionItem{ | ||
| { | ||
| Label: "aw: Minimal workflow", | ||
| Kind: CompletionItemKindSnippet, | ||
| Detail: "New agentic workflow skeleton", | ||
| Documentation: &MarkupContent{ | ||
| Kind: "markdown", | ||
| Value: "Creates a minimal agentic workflow with issue trigger, Copilot engine, and safe outputs.", | ||
| }, | ||
| InsertText: `--- | ||
| on: | ||
| issues: | ||
| types: [opened] | ||
| engine: copilot | ||
| permissions: read-all | ||
| safe-outputs: | ||
| add-comment: | ||
| --- | ||
| # $1 | ||
|
|
||
| $0`, | ||
| InsertTextFormat: InsertTextFormatSnippet, | ||
| SortText: "2_snippet_minimal", | ||
| }, | ||
| { | ||
| Label: "aw: Slash command workflow", | ||
| Kind: CompletionItemKindSnippet, | ||
| Detail: "Workflow triggered by slash command", | ||
| Documentation: &MarkupContent{ | ||
| Kind: "markdown", | ||
| Value: "Creates a workflow triggered by a slash command in issue or PR comments.", | ||
| }, | ||
| InsertText: `--- | ||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
| slash_command: | ||
| name: $1 | ||
| engine: copilot | ||
| permissions: read-all | ||
| safe-outputs: | ||
| add-comment: | ||
| --- | ||
| # $2 | ||
|
|
||
| $0`, | ||
| InsertTextFormat: InsertTextFormatSnippet, | ||
| SortText: "2_snippet_slash", | ||
| }, | ||
| { | ||
| Label: "aw: Workflow with imports", | ||
| Kind: CompletionItemKindSnippet, | ||
| Detail: "Workflow with imported shared components", | ||
| Documentation: &MarkupContent{ | ||
| Kind: "markdown", | ||
| Value: "Creates a workflow that imports shared workflow components.", | ||
| }, | ||
| InsertText: `--- | ||
| on: | ||
| issues: | ||
| types: [opened] | ||
| engine: copilot | ||
| imports: | ||
| - $1 | ||
| permissions: read-all | ||
| safe-outputs: | ||
| add-comment: | ||
| --- | ||
| # $2 | ||
|
|
||
| $0`, | ||
| InsertTextFormat: InsertTextFormatSnippet, | ||
| SortText: "2_snippet_imports", | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // filterCompletions filters completion items by prefix. | ||
| func filterCompletions(items []CompletionItem, prefix string) []CompletionItem { | ||
| if prefix == "" { | ||
| return items | ||
| } | ||
| prefix = strings.ToLower(prefix) | ||
| var filtered []CompletionItem | ||
| for _, item := range items { | ||
| if strings.HasPrefix(strings.ToLower(item.Label), prefix) { | ||
| filtered = append(filtered, item) | ||
| } | ||
| } | ||
| return filtered | ||
| } | ||
|
|
||
| func padIndex(i int) string { | ||
| return fmt.Sprintf("%04d", i) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot use existing yaml libraries