Skip to main content
πŸ“’ New post: Editor Superpowers for Phobos β€” Introducing the Language Server and VS Code Extension β€” Read more β†’

Language Server

The Phobos Language Server (phobos-language-server) is a standalone Go binary implementing the Language Server Protocol for Phobos HCL pipeline templates, release lifecycle templates, and Phobos variables files. It powers the Phobos VS Code extension but works with any LSP-capable editor β€” Neovim, Helix, Zed, Emacs, Sublime Text, or any custom client.

Using VS Code?

If you're using VS Code or a fork (VSCodium, Cursor, Kiro), install the Phobos VS Code extension instead β€” it manages the language server for you (bundled in GitLab-releases VSIXes, downloaded on first launch for marketplace installs) and adds activity-bar views, status-bar integration, and webviews on top. This page is for editors that don't have a dedicated Phobos extension.

What it provides​

Every feature below works on pipeline.hcl and release_lifecycle.hcl files at the template level β€” no plugin installation required. When local plugins are installed via phobos plugin install, action-body features light up for the configured actions too. Variables files (*.pbvars) get a tighter set of cross-file features against their sibling template β€” see Variables files.

LSP methods​

AreaLSP methods
Lifecycleinitialize, initialized, shutdown, exit
SynctextDocument/didOpen, didChange (incremental), didClose
DiagnosticstextDocument/publishDiagnostics (push), textDocument/diagnostic + workspace/diagnostic (pull β€” LSP 3.17+)
CompletiontextDocument/completion, textDocument/signatureHelp
NavigationtextDocument/definition, textDocument/references, textDocument/documentHighlight, textDocument/documentSymbol, workspace/symbol
Call hierarchytextDocument/prepareCallHierarchy, callHierarchy/incomingCalls, callHierarchy/outgoingCalls
RenametextDocument/prepareRename, textDocument/rename (with cross-file propagation for workspace-level declarations)
HovertextDocument/hover
StructuretextDocument/foldingRange, textDocument/selectionRange
DecorationtextDocument/documentLink, textDocument/semanticTokens/full, textDocument/inlayHint
FormattingtextDocument/formatting, textDocument/rangeFormatting, textDocument/onTypeFormatting (triggered on })
Code actionstextDocument/codeAction (install-plugin, declare-missing-plugin-block, declare-aliased-plugin-block, set-plugin-ref per declared alias, deprecation rewrites, extract-to-variable / inline-variable refactors, source.fixAll)

Diagnostics​

  • Parse errors, missing required attributes, disallowed enum values, reserved-keyword / name-format violations, block count limits, duplicate stage / task / action names.
  • Cross-field constraints: interval requires attempts, pipeline_type = "deployment" requires environment, mount_point volumes must be declared.
  • stage_order correctness (drift between stage_order entries and declared stages).
  • Return-type validation of HCL built-in functions against the field they populate.
  • Dependency-name validation β€” warns on dependencies = ["X"] when X isn't a sibling task / deployment / nested pipeline in the same stage and phase.
  • Dead action-reference validation β€” warns on this.action.<X>.outputs.* when X isn't declared inside the same task. Covers every expression inside the task body, including task-level attributes like success_condition, if, and when.
  • Undefined-reference validation β€” errors on var.<X>, jwt.<X>, vcs_token.<X>, volume.<X> when the named value isn't declared, and on any traversal whose root isn't a known Phobos namespace (catches typos like pipelne.stage.*). Local bindings from dynamic blocks are exempted.
  • Namespace-structure validation β€” walks pipeline.*, this.*, and phobos.* traversals segment-by-segment against a fixed grammar. Typos in structural keywords (pipeline.stage.<s>.task.<t>.outp.<o> where outp should be outputs) produce a targeted diagnostic on the specific wrong segment.
  • Plugin-configuration-required validation β€” errors when a task uses action "<plugin>_<act>" but no matching plugin "<plugin>" { ... } block is declared at the template root. Every plugin including exec requires a declaration.
  • Plugin-alias validation β€” when a template declares aliased plugin instances:
    • The plugin attribute on an action is a symbolic traversal expression (e.g. plugin = tharsis.primary). action { plugin = <type>.<alias> } refs that don't resolve to a declared plugin instance produce an error with a concrete "declare plugin \"<type>\" { alias = \"<alias>\" } or change plugin to match a declared name" hint.
    • When a type has only aliased instances (no unaliased default), actions of that type MUST set plugin = .... The diagnostic lists every available qualified name so the fix is a copy-paste.
  • Deprecation markers on phobos.* and action_outputs.* references (as DiagnosticTag.Deprecated); hovers and completion items point at the pipeline.* / this.action.* replacements.
  • Unused-declaration diagnostics for variable, jwt, vcs_token, and volume. Default severity is Information; clients can opt into Hint via configuration.
  • Enum-typed attributes (on_error, when, pipeline_type, ...) accept variable references without false positives β€” the value resolves at runtime and is enforced there.

Completion​

Context-aware completion for:

  • Block types, attribute names, and enumerated string values.
  • HCL built-in function names and signatures.
  • Named-value namespaces: var.*, jwt.*, vcs_token.*, system.*, pipeline.*, this.*.
  • All four output-reference shapes, including this.alias.* and typed-output field navigation.
  • Sibling task / deployment / nested-pipeline names inside dependencies = [...].
  • Declared plugin-instance names (qualified <type>.<alias>) inside action { plugin = <cursor> }.

Variables files (*.pbvars)​

Phobos variables files supply concrete inputs to a sibling pipeline or release-lifecycle template β€” the analog of Terraform's .tfvars. The CLI loads them in a defined precedence order at pipeline-creation time (see Variables guide); the language server adds editor support on top.

  • Body shape β€” top-level attribute assignments only (name = value). Any block at this scope fires an error pointing at the block keyword (a clear sign the author confused a vars file for a template).
  • Cross-file diagnostics against the sibling template's variable "<name>" declarations, found by walking the .pbvars file's directory for pipeline.hcl then release_lifecycle.hcl:
    • Unknown variable name β†’ variable "x" is not declared in the sibling template; remove the assignment or add a 'variable "x" { ... }' block to the template.
    • Type mismatch β†’ variable "replicas" has type number but was assigned a value of type string. The check parses the variable's type = ... expression via typeexpr.TypeConstraint and runs convert.Convert, so structured types (object({...}), list(...), etc.) are checked field-by-field.
    • Unresolved references inside a value β†’ HCL's own evaluation diagnostic (e.g. Variables not allowed: …). Vars files don't carry an eval context, so references are diagnosed where they appear.
    • Stays silent when the directory has no canonical sibling β€” false-flagging every attribute as "unknown" would be hostile.
  • Completion β€” declared variable names from the sibling, filtered by typed prefix and suppressed for already-assigned names. Each item inserts a typed snippet shaped to the variable's declared cty.Type:
    • string β†’ name = "${1}" (cursor between the quotes)
    • number β†’ name = ${1:0} (placeholder selected for one-keystroke overwrite)
    • bool β†’ name = ${1|true,false|} (choice dropdown)
    • list / set / tuple β†’ name = [${1}]
    • object({...}) β†’ multi-line, one tab stop per declared attribute, sorted alphabetically with = aligned and each field's value sized to its declared type
    • map(...) β†’ name = {\n ${1}\n} (single tab stop, since keys are dynamic)
  • Hover β€” surfaces the sibling variable's description, declared type, and default-state ("Has a default…" vs "No default declared…").
  • Live cross-file β€” the editor integration plumbs an in-memory document store into the LSP so unsaved sibling buffers propagate to .pbvars diagnostics, completions, and hover live, without the user saving first. Vars-file analysis bypasses the analysis cache for this reason.

Plugin-aware features​

When a Phobos plugin is installed at ~/.phobos.d/plugins/<host>/<org>/<name>/<version>/<os>_<arch>/, the language server fetches the plugin's schema and extends diagnostics, completion, and hover to cover action bodies for every action the plugin exposes.

  • Built-in schemas β€” The exec plugin's schema is bundled, so action "exec_command" works without installing anything.
  • Schema cache β€” Plugin schemas are cached on disk (<binpath_dir>/schema.cache.json) so the next cold start is fast even when the plugin binary is missing.
  • Live reload β€” A filesystem watcher on ~/.phobos.d/plugins picks up newly-installed plugins (debounced at 500 ms) without any explicit editor action.
  • Install quick-fix β€” The Plugin not installed diagnostic carries structured data so editors can emit a "run phobos plugin install …" code action, including any --version constraint the template pins.

Installation​

Pre-built binaries for the common platforms are published on the releases page. Each release ships with a sha256sums.txt manifest alongside the binaries.

Linux / macOS​

Install phobos-language-server on Linux / macOS
# Replace VERSION with the release tag and PLATFORM with one of:
# linux_amd64, linux_arm64, linux_arm, linux_386,
# darwin_amd64, darwin_arm64
VERSION=0.1.0
PLATFORM=linux_amd64

BASE="https://gitlab.com/infor-cloud/martian-cloud/phobos/phobos-language-server/-/releases/${VERSION}/downloads"
curl -L -o phobos-language-server "${BASE}/phobos-language-server_${VERSION}_${PLATFORM}"
curl -L -o sha256sums.txt "${BASE}/sha256sums.txt"

# Verify before installing.
sha256sum -c --ignore-missing sha256sums.txt

chmod +x phobos-language-server
sudo mv phobos-language-server /usr/local/bin/

Windows​

Download phobos-language-server_<version>_windows_amd64.exe (or windows_386.exe) from the releases page, verify its SHA-256 against the manifest, and place it on your PATH.

Building from source​

git clone https://gitlab.com/infor-cloud/martian-cloud/phobos/phobos-language-server.git
cd phobos-language-server
make build
# Produces bin/phobos-language-server

Requires Go 1.26 or newer.

Running​

The server speaks LSP over stdio by default β€” every mainstream LSP client uses stdio, so the usual phobos-language-server command suffices.

phobos-language-server # stdio (default)
phobos-language-server --version
phobos-language-server --port 7777 # TCP mode, for dev / debugging
TCP mode

TCP mode is primarily for language-server developers β€” run the server in one terminal, connect from an editor that supports TCP LSP transport. It's equally usable from any LSP client, but the stdio default is simpler and is what every editor defaults to.

Editor setup​

The language server activates on pipeline.hcl, release_lifecycle.hcl, and any *.pbvars file. Configure your editor to:

  1. Associate those filename patterns with a Phobos-specific language ID (we use phobos in the VS Code extension; other editors can pick any ID).
  2. Launch phobos-language-server via stdio.

Example snippets below are sketches β€” refer to each client's own documentation for the authoritative configuration format.

Neovim (nvim-lspconfig)​

init.lua snippet
vim.filetype.add({
filename = {
['pipeline.hcl'] = 'phobos',
['release_lifecycle.hcl'] = 'phobos',
},
pattern = {
['.*%.pbvars'] = 'phobos',
},
})

require('lspconfig.configs').phobos = {
default_config = {
cmd = { 'phobos-language-server' },
filetypes = { 'phobos' },
root_dir = require('lspconfig').util.root_pattern('pipeline.hcl', 'release_lifecycle.hcl', '.git'),
},
}

require('lspconfig').phobos.setup({})

Helix​

languages.toml snippet
[[language]]
name = "phobos"
scope = "source.hcl"
file-types = ["hcl"] # narrow further via roots if you share HCL with other tools
language-servers = ["phobos-language-server"]

[language-server.phobos-language-server]
command = "phobos-language-server"

Zed​

Zed's extension API exposes LSP integrations as custom extensions. The server launches via stdio the same way β€” see the Zed extension docs for the current wrapper format.

Emacs (eglot / lsp-mode)​

eglot configuration
(add-to-list 'auto-mode-alist '("pipeline\\.hcl\\'" . phobos-mode))
(add-to-list 'auto-mode-alist '("release_lifecycle\\.hcl\\'" . phobos-mode))
(add-to-list 'auto-mode-alist '("\\.pbvars\\'" . phobos-mode))

(add-to-list 'eglot-server-programs
'(phobos-mode . ("phobos-language-server")))

Configuration​

Feature toggles are sent to the server via LSP initializationOptions (on initialize) and workspace/didChangeConfiguration (at runtime). The VS Code extension sends these automatically from phobos.* settings; other clients must wire them through in their own configuration.

OptionDefaultDescription
inlayHints.enabledtrueMaster switch for inlay hints.
inlayHints.variableTypestrueRender : string, : number, etc. after untyped variable defaults.
inlayHints.variableDefaultstrueRender the resolved default value next to var.<name> references where the default is a literal.
inlayHints.actionPluginstrueRender the resolved plugin instance next to each action "<plugin>_<name>" label β€” : docker.builder when plugin = docker.builder is set, : docker otherwise.
inlayHints.actionCountstrueRender per-task action + output counts (: 2 actions, 1 output). The outputs half is hidden when the task has none.
inlayHints.deploymentstrueRender environment = "…" after deployment blocks in lifecycle templates.
inlayHints.stagestrueRender a children-count summary after each stage "<name>" label (: 3 tasks, : 2 tasks, 1 pipeline). Main-phase direct children only.
documentLinks.enabledtrueMaster switch for document links.
documentLinks.pluginDocstrueTurn plugin source strings in plugin_requirements into clickable registry links.
semanticTokens.enabledtrueEnable Phobos semantic tokens layered on top of the client's TextMate grammar.
diagnostics.unusedAsHintfalseRender unused-declaration diagnostics as Hint instead of Information. Quieter but less discoverable.

Example initializationOptions payload:

{
"inlayHints": { "enabled": true, "actionPlugins": false },
"diagnostics": { "unusedAsHint": true }
}

Workspace trust​

The server spawns plugin binaries under ~/.phobos.d/plugins/ to introspect their schemas. When running the server on behalf of a workspace whose content you don't trust β€” e.g. a client's untrusted-workspace mode β€” set the PHOBOS_LSP_DISABLE_PLUGINS environment variable to 1 before launching:

PHOBOS_LSP_DISABLE_PLUGINS=1 phobos-language-server

With the variable set, the server skips plugin discovery and schema introspection entirely. Syntax-level diagnostics, template-level diagnostics, completion, hover, formatting, and every other non-plugin-aware feature remain available. Action-body completion and validation silently degrade to "no plugin schemas".

Custom workspace/executeCommand endpoints​

The server exposes several custom commands through workspace/executeCommand. Editor implementors can call these to light up integrations the VS Code extension uses.

CommandPurpose
phobos.reloadPluginsRe-run plugin discovery and republish diagnostics. Rate-limited to one run per 750 ms.
phobos.listPluginsReturn the plugin inventory (names, versions, sources, built-in flag).
phobos.getPluginSchemaFetch one plugin's schema on demand.
phobos.pluginHealthPer-plugin ready / failed report for status-bar indicators.
phobos.migrateDeprecationsCompile a WorkspaceEdit applying every preferred deprecation fix in a document.
phobos.evalSnapshotReturn the evaluator namespace tree at a position β€” what VS Code's Evaluated Context panel renders.
phobos.documentKindReturn "pipeline" or "lifecycle" for a document.
phobos.pipelineGraphReturn a layered-DAG structure (stages + child nodes + edges) for a template.
phobos.runShellCommandClient-side target for install quick-fixes β€” the server emits Command descriptors, the client runs the shell.

Troubleshooting​

The server crashes on startup​

Check the client's language-server log output. The server panics are caught per-request (recoverHandler logs the stack and returns an error instead of killing the process), but a crash during startup β€” before the handler wrapper installs β€” surfaces in the client's transport log. Most startup crashes are bad Go build flags; a clean make build or a fresh release binary fixes them.

Plugin schemas aren't loading​

  • Verify the plugin is installed at ~/.phobos.d/plugins/<host>/<org>/<name>/<version>/<os>_<arch>/ and that the binary is a regular file (not a symlink β€” the server rejects symlinks for security).
  • Check the Phobos Output channel or equivalent for subprocess failed / timed out messages. Plugin subprocesses have a 10-second Schema() deadline.
  • Try phobos.reloadPlugins (via your client's command surface) or restart the language server.
  • If PHOBOS_LSP_DISABLE_PLUGINS=1 is set in the server's environment, plugin discovery is disabled by design β€” clear the variable to re-enable it.

Diagnostics appear twice in the Problems panel​

Your client may be listening on both push (publishDiagnostics) and pull (textDocument/diagnostic) channels without deduplication. The server suppresses push when the client declares pull support during initialize, so check that your client is sending textDocument: { diagnostic: {...} } in its capabilities payload. Otherwise, disable pull support in the client to fall back to push-only.

source.fixAll doesn't trigger on save​

Make sure the client advertises textDocument.codeAction.codeActionLiteralSupport.codeActionKind.valueSet covering source.fixAll, and that the editor's on-save action invokes the source.fixAll kind specifically. VS Code's editor.codeActionsOnSave uses this pattern; other clients vary.

  • VS Code Extension β€” managed-server UX (bundled or runtime-installed) for VS Code and compatible editors.
  • HCL Reference β€” template syntax the language server validates.
  • Plugin reference β€” plugin schemas that drive action-body features.