feat: add stdio-only LSP server for agentic workflow files#17184
feat: add stdio-only LSP server for agentic workflow files#17184
Conversation
Implements a Language Server Protocol server accessible via `gh aw lsp --stdio` that provides IDE support for agentic workflow Markdown files: - JSON-RPC 2.0 transport over stdio with Content-Length framing - Diagnostics: YAML syntax errors and schema validation - Hover: schema descriptions for frontmatter keys - Completions: top-level keys, nested keys, enum values, and workflow snippets - Stateless session-only memory (no daemon, no disk state) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
pkg/cli/lsp_command.go
Outdated
| }, | ||
| } | ||
|
|
||
| cmd.Flags().Bool("stdio", true, "Use stdio transport (default, only supported mode)") |
pkg/lsp/server.go
Outdated
| }, | ||
| ServerInfo: &ServerInfo{ | ||
| Name: "gh-aw-lsp", | ||
| Version: "0.1.0", |
pkg/lsp/server.go
Outdated
| }, | ||
| }, | ||
| ServerInfo: &ServerInfo{ | ||
| Name: "gh-aw-lsp", |
- Fix staticcheck: use tagged switch instead of if-else chain - Fix testifylint: use require for error assertions, avoid float comparisons - Fix schema provider to preserve local descriptions when following $ref - Fix diagnostics to validate empty frontmatter against schema - Fix multiple frontmatter detection test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Simplify Content-Length header construction in server tests (use fmt.Sprintf) - Fix padIndex to handle values ≥100 correctly (use fmt.Sprintf with zero-padding) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| github.com/sourcegraph/conc v0.3.0 | ||
| github.com/spf13/cobra v1.10.2 | ||
| github.com/stretchr/testify v1.11.1 | ||
| go.yaml.in/yaml/v3 v3.0.4 |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot keep working. validate that the vscode config is valid |
|
@copilot make command hidden |
There was a problem hiding this comment.
Pull request overview
This pull request adds a Language Server Protocol (LSP) server for agentic workflow Markdown files, enabling IDE support with diagnostics, hover information, and completions. The implementation is comprehensive, well-tested, and follows the LSP specification correctly.
Changes:
- Adds a new
pkg/lsp/package with stdio-based LSP server supporting JSON-RPC 2.0 over Content-Length framing - Implements diagnostics (frontmatter validation, YAML syntax, schema validation), hover (schema descriptions), and completions (context-aware keys/values plus workflow snippets)
- Integrates with existing
pkg/parserschema validation for compile-consistent error reporting - Adds
gh aw lspCLI command in the development group - Promotes
go.yaml.in/yaml/v3from indirect to direct dependency - Includes 40 unit tests covering all major components
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/lsp/transport.go |
JSON-RPC 2.0 message transport with Content-Length framing |
pkg/lsp/transport_test.go |
Tests for message read/write with proper framing |
pkg/lsp/protocol.go |
Minimal hand-rolled LSP protocol types (Position, Range, Diagnostic, Hover, Completion, etc.) |
pkg/lsp/document.go |
Thread-safe in-memory document store with frontmatter region parsing |
pkg/lsp/document_test.go |
Tests for document management and frontmatter detection |
pkg/lsp/schema.go |
Schema provider extracting properties, enums, and descriptions from embedded JSON schema; YAML path resolution with AST and fallback |
pkg/lsp/schema_test.go |
Tests for schema navigation and YAML path computation |
pkg/lsp/diagnostics.go |
Diagnostic computation: frontmatter presence, YAML syntax, schema validation via parser.ValidateMainWorkflowFrontmatterWithSchema |
pkg/lsp/diagnostics_test.go |
Tests for various diagnostic scenarios |
pkg/lsp/hover.go |
Hover provider returning markdown-formatted schema information |
pkg/lsp/hover_test.go |
Tests for hover at different positions |
pkg/lsp/completion.go |
Context-aware completion provider with property/enum/snippet suggestions |
pkg/lsp/completion_test.go |
Tests for completion in various contexts |
pkg/lsp/server.go |
Main LSP server with message routing and lifecycle management |
pkg/lsp/server_test.go |
Integration tests for server request/response flows |
pkg/cli/lsp_command.go |
CLI command to start LSP server on stdio |
cmd/gh-aw/main.go |
Wires lsp command into development group |
go.mod |
Moves yaml.v3 from indirect to direct dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }) | ||
|
|
||
| var stdout bytes.Buffer | ||
| server, err := NewServer(strings.NewReader(input), &stdout, &bytes.Buffer{}) |
There was a problem hiding this comment.
All test calls to NewServer are missing the required version parameter. The function signature expects 4 parameters (stdin, stdout, stderr, version) but only 3 are passed.
For consistency with production code, add an empty string or a test version as the fourth parameter:
server, err := NewServer(strings.NewReader(input), &stdout, &bytes.Buffer{}, "test-version")| server, err := NewServer(strings.NewReader(input), &stdout, &bytes.Buffer{}) | |
| server, err := NewServer(strings.NewReader(input), &stdout, &bytes.Buffer{}, "test-version") |
| gh aw lsp # Start LSP server on stdio | ||
| echo '...' | gh aw lsp # Pipe LSP messages`, |
There was a problem hiding this comment.
The second example "echo '...' | gh aw lsp" is misleading. LSP servers don't work well with simple piped input - they need to engage in a full JSON-RPC 2.0 request/response protocol over stdio.
Consider removing this example or replacing it with a more realistic one, such as:
Examples:
gh aw lsp # Start LSP server on stdio (for IDE integration)
Or provide a more accurate example of how to configure it in an IDE (as shown in the PR description with the VS Code configuration).
| 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)`, |
| func RunLSP() error { | ||
| lspLog.Print("Starting LSP server on stdio") | ||
|
|
||
| server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr) |
There was a problem hiding this comment.
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()) |
Adds a Language Server Protocol server (
gh aw lsp --stdio) providing IDE support for agentic workflow Markdown files — diagnostics, hover, and completions driven by the existing embedded JSON schema.LSP Server (
pkg/lsp/)parser.ValidateMainWorkflowFrontmatterWithSchemafor compile-consistent errors)on/tools/safe-outputs, enum values forengine, plus snippet templates for common workflow patternsCLI (
pkg/cli/lsp_command.go)Wired as
gh aw lsp --stdioin the development command group.VS Code consumption
Example wire interaction
40 unit tests covering transport, document management, schema extraction, diagnostics, hover, completions, and full server request/response flows.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/graphql/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GO111MODULE 64/bin/go git remo�� add origin ache/node/24.13.0/x64/bin/node -json GO111MODULE 64/bin/go ache/node/24.13.0/x64/bin/node(http block)https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1/usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha -json GO111MODULE 5220588/b364/vet.cfg GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 5220588/b414/vet.cfg GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v3/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go env 3054-66663/test-2705419959/.github/workflows GO111MODULE .cfg GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha blog-auditor.md GO111MODULE ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile env 5220588/b414/_pkg_.a GO111MODULE 5220588/b414=> GOINSECURE b/gh-aw/pkg/reporev-parse GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha bot-detection.md GO111MODULE /opt/hostedtoolcache/go/1.25.0/x-lang=go1.25 GOINSECURE GOMOD GOMODCACHE go env runs/20260220-153054-66663/test-go1.25.0 GO111MODULE 5220588/b293/vet.cfg l GOMOD GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha user.email test@example.com /usr/bin/git -json GO111MODULE 64/bin/go git rev-�� --show-toplevel sh /usr/bin/git "prettier" --chegit GOPROXY 64/bin/go git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v5/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha 252110038/.github/workflows GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha 5220588/b420/_pkg_.a GOPROXY e/git GOSUMDB b/gh-aw/pkg/strirev-parse 64/bin/go e/git -1 Ycqe/hwuS1ICFz_ibRTSsYcqe go /usr/bin/git heck '**/*.cjs' git GO111MODULE 64/bin/go 5220588/b420/importcfg(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --git-dir sh /usr/bin/git "prettier" --chegit GOPROXY 64/bin/go git init�� 64/bin/go go /usr/bin/git -json GO111MODULE 64/bin/go /usr/bin/git(http block)https://api.github.com/repos/actions/github-script/git/ref/tags/v8/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha GOSUMDB GOWORK 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ck 'scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD erignore go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD erignore go env -json GO111MODULE 64/bin/go ngci-lint" ]; thgit GOMOD GOMODCACHE go(http block)https://api.github.com/repos/actions/setup-go/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha /ref/tags/v8 GO111MODULE ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile env 5220588/b415/_pkg_.a GO111MODULE 5220588/b415=> GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/actions/setup-node/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha 57 5220588/b125/vet.cfg Name,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle GOINSECURE GOMOD GOMODCACHE go env runs/20260220-153054-66663/test-252110038/.github/workflows GO111MODULE(http block)https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts/usr/bin/gh gh run download 1 --dir test-logs/run-1 GO111MODULE x_amd64/link GOINSECURE GOMOD GOMODCACHE x_amd64/link env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE -j/NEbzTRrGx3Hf7l9Ai1Xn/DcrjArqgtest@example.com(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts/usr/bin/gh gh run download 12345 --dir test-logs/run-12345 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts/usr/bin/gh gh run download 12346 --dir test-logs/run-12346 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts/usr/bin/gh gh run download 2 --dir test-logs/run-2 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts/usr/bin/gh gh run download 3 --dir test-logs/run-3 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts/usr/bin/gh gh run download 4 --dir test-logs/run-4 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env hub/workflows GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts/usr/bin/gh gh run download 5 --dir test-logs/run-5 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env ty-test.md GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/workflows/usr/bin/gh gh workflow list --json name,state,path heck '**/*.cjs' GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go m/_n�� -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 GOMOD GOMODCACHE x_amd64/link env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE 3s/1zjxUpocWPETRTest User(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env 2705419959/.github/workflows GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/nonexistent/repo/actions/runs/12345/usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE x_amd64/link GOINSECURE GOMOD GOMODCACHE x_amd64/link(http block)https://api.github.com/repos/owner/repo/actions/workflows/usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/owner/repo/contents/file.md/tmp/go-build1435220588/b382/cli.test /tmp/go-build1435220588/b382/cli.test -test.testlogfile=/tmp/go-build1435220588/b382/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true GOINSECURE GOMOD GOMODCACHE go k/gh�� -json GO111MODULE 64/bin/go GOINSECURE GOMOD erignore go(http block)https://api.github.com/repos/test-owner/test-repo/actions/secrets/usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name heck '**/*.cjs' GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go m/_n�� -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
Below is a concrete, repo-oriented implementation plan for adding stdio-only LSP support to github/gh-aw (Go), specifically for agentic workflow Markdown files (your “aw .mds” dialect). The goal is a nice IDE experience (VS Code + Monaco) with hover (“hiver”) info, diagnostics, stateless behavior, and completion.
⸻
Scope
• Language: Agentic Workflow Markdown files (Markdown with YAML frontmatter; compiled by gh aw compile). Docs confirm the split: YAML frontmatter + markdown body. 
• Transport: stdio only (JSON-RPC 2.0 over stdin/stdout).
• Runtime: no daemon. The LSP server runs as a process inside the same container as the IDE.
• Stateless: no disk state, no background indexing; only in-memory document snapshots for the active LSP session.
• Core LSP features
• textDocument/publishDiagnostics for schema/parse errors
• textDocument/hover (your “hiver”)
• textDocument/completion (requested)
• (Optional but cheap) textDocument/documentSymbol for headings/frontmatter sections
CLI entrypoint
Add a command that exposes the protocol:
• gh aw lsp (or gh aw language-server) that speaks LSP on stdio
• Example: gh aw lsp --stdio (even if stdio is the only supported mode)
This matches the repo’s existing CLI orientation (gh aw ...). 
⸻
Why this matters
You want diagnostics that match what gh aw compile would reject. The docs explicitly say compilation includes parsing + validation against JSON schemas. 
Source of truth
• The frontmatter schema is already present and extensive (example excerpt in generated docs). 
• The raw JSON schema used by the compiler exists in-repo at pkg/parser/schemas/main_workflow_schema.json. 
Key principle: the LSP server should call the same parser/validator packages that compilation uses, so the IDE diagnostics stay consistent with CI.
⸻
Packages to add (suggested)
1. pkg/lsp/protocol
• Minimal LSP types you need (or generate from an LSP Go library, but minimal hand-rolled is fine to reduce dependency surface).
2. pkg/lsp/jsonrpc
• Reads/writes stdio messages with Content-Length: framing.
3. pkg/lsp/server
• Request router + handlers.
4. pkg/lsp/aw
• AW-specific language services: parsing frontmatter spans, schema validation → diagnostics, completion/hover providers.
Document model (session-only)
Maintain:
map[protocol.DocumentURI]*DocumentSnapshot
Where DocumentSnapshot contains:
• full text
• parsed frontmatter range (byte offsets + line/col mapping)
• parsed YAML AST (if available)
• last diagnostics hash (to suppress duplicates)
This is still “stateless” in the sense of no daemon, no persisted index; it’s just session memory.
Handler set (MVP)
Implement:
• initialize, initialized
• textDocument/didOpen
• textDocument/didChange (full sync first; incremental later if desired)
• textDocument/didClose
• textDocument/hover
• textDocument/completion
• shutdown, exit
⸻
What diagnostics should cover
1. Frontmatter block detection
• Missing opening/closing ---
• Multiple frontmatter blocks
2. YAML parsing errors
• indentation, invalid scalars, duplicate keys
3. Schema validation errors (the big win)
• Validate parsed YAML object against main_workflow_schema.json 
• Produce:
• message
• severity (Error/Warning)
• range: best-effort mapping to YAML node locations
Mapping errors to source ranges
If your current validator returns JSON-pointer-ish paths (common), add a YAML AST walker:
• Path properties.on → find the on: key node range
• Path properties.tools.properties.github... → find nested mapping node
If exact node range can’t be found: attach to the frontmatter block start line (still useful).
When to publish
• On didOpen and didChange.
• Also on save if you later add textDocument/didSave.
⸻
Hover content sources
Use schema descriptions + docstrings:
• The schema includes rich description fields for keys like on, imports, etc. 
• The generated frontmatter reference is also derived from schema and provides user-friendly explanation. 
Hover behavior
• If cursor is over a frontmatter key, show:
• Key name
• Schema description
• Constraints (required/enum/pattern/min/max) if easily available
• If cursor is over certain well-known values (e.g., on.slash_command.events), show enum meaning.
This gives immediate “what does this do” help without opening docs.
⸻
Completions should be context-aware with three main contexts:
A) Frontmatter key completions (top-level)
When cursor is inside frontmatter mapping at indentation 0:
Suggest schema properties such as name, description, imports, on, engine, tools, safe-outputs, etc. (the schema is the canonical list). 
Implementation:
• Load schema once at startup (embed it).
...
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.