From 20a690f57601835e82180259c4a7ec760517dde1 Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:22:57 +0530 Subject: [PATCH 01/10] feat(meta): add devsper type library for completions and hover docs Co-Authored-By: Claude Sonnet 4.6 --- meta/3rd/devsper/library/devsper.lua | 133 +++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 meta/3rd/devsper/library/devsper.lua diff --git a/meta/3rd/devsper/library/devsper.lua b/meta/3rd/devsper/library/devsper.lua new file mode 100644 index 000000000..1f5922a94 --- /dev/null +++ b/meta/3rd/devsper/library/devsper.lua @@ -0,0 +1,133 @@ +---@meta devsper + +--- Result returned by devsper.exec(). +---@class devsper.ExecResult +---@field code integer Exit code of the process. +---@field stdout string Standard output captured from the process. +---@field stderr string Standard error captured from the process. + +--- HTTP response returned by devsper.http methods. +---@class devsper.HttpResponse +---@field status integer HTTP status code (e.g. 200, 404). +---@field body string Response body as a string. +---@field headers table Response headers keyed by header name. + +--- HTTP client library available at devsper.http. +---@class devsper.HttpLib +local HttpLib = {} + +--- Perform an HTTP GET request. +---@param url string The URL to request. +---@param opts table? Optional request options (e.g. headers). +---@return devsper.HttpResponse +function HttpLib.get(url, opts) end + +--- Perform an HTTP POST request. +---@param url string The URL to post to. +---@param body string Request body. +---@param opts table? Optional request options (e.g. headers, content-type). +---@return devsper.HttpResponse +function HttpLib.post(url, body, opts) end + +--- Context object passed to tool run functions. +---@class devsper.ToolCtx +---@field workflow_id string ID of the parent workflow. +---@field run_id string ID of the current run. +local ToolCtx = {} + +--- Emit a structured log message from within a tool. +---@param level string Log level: "debug", "info", "warn", or "error". +---@param msg string Log message text. +function ToolCtx:log(level, msg) end + +--- Parameter specification for a tool parameter. +---@class devsper.ParamSpec +---@field type "string"|"number"|"boolean"|"object" JSON-schema-style type. +---@field required boolean? Whether the parameter is required. Defaults to false. +---@field default any? Default value when the parameter is omitted. + +--- Configuration for the evolutionary/speculative execution engine. +---@class devsper.EvolutionConfig +---@field allow_mutations boolean? Allow the swarm to mutate task prompts at runtime. Defaults to false. +---@field max_depth integer? Maximum recursion/mutation depth. Defaults to 3. +---@field speculative boolean? Enable speculative (parallel branch) execution. Defaults to false. + +--- Top-level configuration passed to devsper.workflow(). +---@class devsper.WorkflowConfig +---@field name string Human-readable workflow name (required). +---@field model string? Default LLM model ID for tasks in this workflow. +---@field workers integer? Number of parallel worker goroutines/processes. +---@field bus "memory"|"redis"|"kafka"? Message bus backend. Defaults to "memory". +---@field evolution devsper.EvolutionConfig? Optional evolutionary execution configuration. + +--- Specification for a single workflow task. +---@class devsper.TaskSpec +---@field prompt string Prompt sent to the LLM for this task (required). +---@field model string? Override the workflow-level model for this task. +---@field can_mutate boolean? Allow this task's prompt to be mutated by the evolution engine. +---@field depends_on string[]? List of task IDs that must complete before this task runs. +--- Use `"*"` as a wildcard meaning "all other tasks must finish first". + +--- Specification for a workflow plugin. +---@class devsper.PluginSpec +---@field source string Path or import specifier for the plugin module (required). + +--- Specification for a workflow input parameter. +---@class devsper.InputSpec +---@field type "string"|"number"|"boolean"|"object"? Type of the input value. +---@field required boolean? Whether this input must be supplied at run time. +---@field default any? Default value used when the input is not supplied. + +--- Specification passed to devsper.tool(). +---@class devsper.ToolSpec +---@field description string Human-readable description of the tool (required). +---@field params table? Map of parameter name to its spec. +---@field run fun(ctx: devsper.ToolCtx, args: table): any, string? Tool implementation (required). Returns a result and an optional error string. + +--- Fluent builder returned by devsper.workflow(). +---@class devsper.WorkflowBuilder +local WorkflowBuilder = {} + +--- Register a task in the workflow. +---@param id string Unique task identifier within the workflow. +---@param spec devsper.TaskSpec Task configuration. +---@return devsper.WorkflowBuilder Returns the builder for chaining. +function WorkflowBuilder.task(id, spec) end + +--- Register a plugin in the workflow. +---@param name string Plugin name used to reference it within tasks. +---@param spec devsper.PluginSpec Plugin configuration. +---@return devsper.WorkflowBuilder Returns the builder for chaining. +function WorkflowBuilder.plugin(name, spec) end + +--- Declare an input parameter for the workflow. +---@param name string Input parameter name. +---@param spec devsper.InputSpec Input specification. +---@return devsper.WorkflowBuilder Returns the builder for chaining. +function WorkflowBuilder.input(name, spec) end + +--- The devsper global injected into every .devsper file. +---@class devsper +---@field http devsper.HttpLib HTTP client for use inside tools or workflows. +devsper = {} + +--- Define and register a workflow. Returns a builder for adding tasks, plugins, and inputs. +---@param config devsper.WorkflowConfig Top-level workflow configuration. +---@return devsper.WorkflowBuilder +function devsper.workflow(config) end + +--- Define and register a tool that the swarm can invoke. +---@param name string Unique tool name. +---@param spec devsper.ToolSpec Tool specification including description, params, and run function. +function devsper.tool(name, spec) end + +--- Execute a subprocess and return its result. +---@param cmd string Executable to run. +---@param args string[]? Arguments to pass to the executable. +---@return devsper.ExecResult +function devsper.exec(cmd, args) end + +--- Emit a structured log message at the workflow/global level. +---@param level string Log level: "debug", "info", "warn", or "error". +---@param msg string Log message text. +function devsper.log(level, msg) end From 52b0f6e1dbf58f92070dcc0fb3b1190d339b6902 Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:33:01 +0530 Subject: [PATCH 02/10] fix(meta): fix WorkflowBuilder method syntax, HttpLib anchoring, union types Co-Authored-By: Claude Sonnet 4.6 --- meta/3rd/devsper/library/devsper.lua | 33 ++++++++++------------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/meta/3rd/devsper/library/devsper.lua b/meta/3rd/devsper/library/devsper.lua index 1f5922a94..f09fc2b10 100644 --- a/meta/3rd/devsper/library/devsper.lua +++ b/meta/3rd/devsper/library/devsper.lua @@ -12,22 +12,11 @@ ---@field body string Response body as a string. ---@field headers table Response headers keyed by header name. ---- HTTP client library available at devsper.http. ---@class devsper.HttpLib -local HttpLib = {} - ---- Perform an HTTP GET request. ----@param url string The URL to request. ----@param opts table? Optional request options (e.g. headers). ----@return devsper.HttpResponse -function HttpLib.get(url, opts) end - ---- Perform an HTTP POST request. ----@param url string The URL to post to. ----@param body string Request body. ----@param opts table? Optional request options (e.g. headers, content-type). ----@return devsper.HttpResponse -function HttpLib.post(url, body, opts) end +---Fetch a URL and return the response. +---@field get fun(url: string, opts?: { headers?: table, timeout?: integer }): devsper.HttpResponse +---Perform an HTTP POST request. +---@field post fun(url: string, body: string|table, opts?: { headers?: table, timeout?: integer }): devsper.HttpResponse --- Context object passed to tool run functions. ---@class devsper.ToolCtx @@ -36,7 +25,7 @@ function HttpLib.post(url, body, opts) end local ToolCtx = {} --- Emit a structured log message from within a tool. ----@param level string Log level: "debug", "info", "warn", or "error". +---@param level "debug"|"info"|"warn"|"error" Log level. ---@param msg string Log message text. function ToolCtx:log(level, msg) end @@ -65,8 +54,7 @@ function ToolCtx:log(level, msg) end ---@field prompt string Prompt sent to the LLM for this task (required). ---@field model string? Override the workflow-level model for this task. ---@field can_mutate boolean? Allow this task's prompt to be mutated by the evolution engine. ----@field depends_on string[]? List of task IDs that must complete before this task runs. ---- Use `"*"` as a wildcard meaning "all other tasks must finish first". +---@field depends_on? (string|"*")[] Task IDs that must complete first. Use "*" to depend on all other tasks. --- Specification for a workflow plugin. ---@class devsper.PluginSpec @@ -92,19 +80,19 @@ local WorkflowBuilder = {} ---@param id string Unique task identifier within the workflow. ---@param spec devsper.TaskSpec Task configuration. ---@return devsper.WorkflowBuilder Returns the builder for chaining. -function WorkflowBuilder.task(id, spec) end +function WorkflowBuilder:task(id, spec) end --- Register a plugin in the workflow. ---@param name string Plugin name used to reference it within tasks. ---@param spec devsper.PluginSpec Plugin configuration. ---@return devsper.WorkflowBuilder Returns the builder for chaining. -function WorkflowBuilder.plugin(name, spec) end +function WorkflowBuilder:plugin(name, spec) end --- Declare an input parameter for the workflow. ---@param name string Input parameter name. ---@param spec devsper.InputSpec Input specification. ---@return devsper.WorkflowBuilder Returns the builder for chaining. -function WorkflowBuilder.input(name, spec) end +function WorkflowBuilder:input(name, spec) end --- The devsper global injected into every .devsper file. ---@class devsper @@ -119,6 +107,7 @@ function devsper.workflow(config) end --- Define and register a tool that the swarm can invoke. ---@param name string Unique tool name. ---@param spec devsper.ToolSpec Tool specification including description, params, and run function. +---@return nil function devsper.tool(name, spec) end --- Execute a subprocess and return its result. @@ -128,6 +117,6 @@ function devsper.tool(name, spec) end function devsper.exec(cmd, args) end --- Emit a structured log message at the workflow/global level. ----@param level string Log level: "debug", "info", "warn", or "error". +---@param level "debug"|"info"|"warn"|"error" Log level. ---@param msg string Log message text. function devsper.log(level, msg) end From af4d5e68482375f64923aadf183cb6044cf5cb4b Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:37:21 +0530 Subject: [PATCH 03/10] feat(diag): add devsper schema validation diagnostic Adds script/core/diagnostics/devsper-schema.lua which fires warnings on .devsper files for missing required spec fields (prompt, source, run, description) and unknown builtin: plugin names. Registered as the 'devsper-schema' diagnostic in the 'devsper' group via proto/diagnostic.lua. --- script/core/diagnostics/devsper-schema.lua | 122 +++++++++++++++++++++ script/proto/diagnostic.lua | 8 ++ 2 files changed, 130 insertions(+) create mode 100644 script/core/diagnostics/devsper-schema.lua diff --git a/script/core/diagnostics/devsper-schema.lua b/script/core/diagnostics/devsper-schema.lua new file mode 100644 index 000000000..7cdc9efa9 --- /dev/null +++ b/script/core/diagnostics/devsper-schema.lua @@ -0,0 +1,122 @@ +local files = require 'files' +local guide = require 'parser.guide' + +-- Returns the value node of a named field in a table AST node, or nil. +local function tableGet(tblNode, key) + for i = 1, #tblNode do + local child = tblNode[i] + if child.type == 'tablefield' + and child.field + and child.field[1] == key then + return child.value + end + end + return nil +end + +-- Returns true if `node` is a getfield call like `.methodName(...)` +local function isLocalMethod(callNode, methodName) + local callee = callNode.node + if not callee then return false end + if callee.type ~= 'getfield' then return false end + if not callee.field then return false end + if callee.field[1] ~= methodName then return false end + -- receiver must be a local variable (getlocal) + local receiver = callee.node + if not receiver then return false end + return receiver.type == 'getlocal' +end + +-- Returns true if `callNode` is `devsper.(...)` +local function isDevsperGlobalMethod(callNode, methodName) + local callee = callNode.node + if not callee then return false end + if callee.type ~= 'getfield' then return false end + if not callee.field then return false end + if callee.field[1] ~= methodName then return false end + local receiver = callee.node + if not receiver then return false end + if receiver.type ~= 'getglobal' then return false end + return receiver[1] == 'devsper' +end + +local validBuiltins = { + git = true, + search = true, + http = true, + code = true, + filesystem = true, +} + +return function (uri, callback) + -- Only run on .devsper files + if not uri:match('%.devsper$') then return end + + local state = files.getState(uri) + if not state then return end + + guide.eachSourceType(state.ast, 'call', function (source) + local args = source.args + if not args then return end + + -- wf.task(id, spec) — spec must have a `prompt` key + if isLocalMethod(source, 'task') then + local spec = args[2] + if spec and spec.type == 'table' then + if not tableGet(spec, 'prompt') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: wf.task() spec is missing required field "prompt"', + } + end + end + + -- wf.plugin(name, spec) — spec must have a `source` key; builtin: prefix validated + elseif isLocalMethod(source, 'plugin') then + local spec = args[2] + if spec and spec.type == 'table' then + local sourceVal = tableGet(spec, 'source') + if not sourceVal then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: wf.plugin() spec is missing required field "source"', + } + elseif sourceVal.type == 'string' then + local strVal = sourceVal[1] + if strVal then + local builtinName = strVal:match('^builtin:(.+)$') + if builtinName and not validBuiltins[builtinName] then + callback { + start = sourceVal.start, + finish = sourceVal.finish, + message = ('devsper: unknown builtin plugin %q (valid: git, search, http, code, filesystem)'):format(builtinName), + } + end + end + end + end + + -- devsper.tool(name, spec) — spec must have `description` and `run` keys + elseif isDevsperGlobalMethod(source, 'tool') then + local spec = args[2] + if spec and spec.type == 'table' then + if not tableGet(spec, 'description') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: devsper.tool() spec is missing required field "description"', + } + end + if not tableGet(spec, 'run') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: devsper.tool() spec is missing required field "run"', + } + end + end + end + end) +end diff --git a/script/proto/diagnostic.lua b/script/proto/diagnostic.lua index 72761493e..4c226399b 100644 --- a/script/proto/diagnostic.lua +++ b/script/proto/diagnostic.lua @@ -227,6 +227,14 @@ m.register { status = 'Any', } +m.register { + 'devsper-schema', +} { + group = 'devsper', + severity = 'Warning', + status = 'Any', +} + ---@return table function m.getDefaultSeverity() local severity = {} From 13a992b5a435d99fe2ad330e0fb9fdc7728cc16d Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:39:36 +0530 Subject: [PATCH 04/10] feat(meta): add workspace config template for .devsper projects --- meta/3rd/devsper/.luarc.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 meta/3rd/devsper/.luarc.json diff --git a/meta/3rd/devsper/.luarc.json b/meta/3rd/devsper/.luarc.json new file mode 100644 index 000000000..d35dcda47 --- /dev/null +++ b/meta/3rd/devsper/.luarc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "runtime": { + "version": "Lua 5.4", + "extension": { ".devsper": "lua" } + }, + "workspace": { + "library": ["${3rd}/devsper/library"], + "checkThirdParty": false + }, + "diagnostics": { + "globals": ["devsper"] + } +} From 255d89e89032177ce55f2323e8c8e209604451b7 Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:43:20 +0530 Subject: [PATCH 05/10] =?UTF-8?q?fix(diag):=20harden=20schema=20diagnostic?= =?UTF-8?q?=20=E2=80=94=20nil=20spec,=20receiver=20name=20check,=20yield?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- script/core/diagnostics/devsper-schema.lua | 140 ++++++++++++--------- 1 file changed, 80 insertions(+), 60 deletions(-) diff --git a/script/core/diagnostics/devsper-schema.lua b/script/core/diagnostics/devsper-schema.lua index 7cdc9efa9..950feff5e 100644 --- a/script/core/diagnostics/devsper-schema.lua +++ b/script/core/diagnostics/devsper-schema.lua @@ -1,30 +1,28 @@ local files = require 'files' local guide = require 'parser.guide' +local await = require 'await' -- Returns the value node of a named field in a table AST node, or nil. local function tableGet(tblNode, key) + if not tblNode or tblNode.type ~= 'table' then return nil end for i = 1, #tblNode do local child = tblNode[i] - if child.type == 'tablefield' - and child.field - and child.field[1] == key then + if child and child.type == 'tablefield' and child.field and child.field[1] == key then return child.value end end return nil end --- Returns true if `node` is a getfield call like `.methodName(...)` -local function isLocalMethod(callNode, methodName) - local callee = callNode.node - if not callee then return false end - if callee.type ~= 'getfield' then return false end - if not callee.field then return false end - if callee.field[1] ~= methodName then return false end - -- receiver must be a local variable (getlocal) +-- Returns true if `node` is a getfield call like `.methodName(...)` +local function isLocalMethod(source, receiverName, methodName) + if not source or source.type ~= 'call' then return false end + local callee = source.node + if not callee or callee.type ~= 'getfield' then return false end + if not callee.field or callee.field[1] ~= methodName then return false end local receiver = callee.node - if not receiver then return false end - return receiver.type == 'getlocal' + if not receiver or receiver.type ~= 'getlocal' then return false end + return receiver[1] == receiverName end -- Returns true if `callNode` is `devsper.(...)` @@ -56,66 +54,88 @@ return function (uri, callback) if not state then return end guide.eachSourceType(state.ast, 'call', function (source) + await.delay() local args = source.args if not args then return end -- wf.task(id, spec) — spec must have a `prompt` key - if isLocalMethod(source, 'task') then - local spec = args[2] - if spec and spec.type == 'table' then - if not tableGet(spec, 'prompt') then - callback { - start = spec.start, - finish = spec.finish, - message = 'devsper: wf.task() spec is missing required field "prompt"', - } - end + if isLocalMethod(source, 'wf', 'task') then + local spec = source.args and source.args[2] + if not spec or spec.type == 'nil' then + callback { + start = source.start, + finish = source.finish, + message = "devsper: wf.task() requires a spec table as second argument", + } + return -- no further checks possible + end + if spec.type ~= 'table' then return end -- non-literal, skip + if not tableGet(spec, 'prompt') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: wf.task() spec is missing required field "prompt"', + } end -- wf.plugin(name, spec) — spec must have a `source` key; builtin: prefix validated - elseif isLocalMethod(source, 'plugin') then - local spec = args[2] - if spec and spec.type == 'table' then - local sourceVal = tableGet(spec, 'source') - if not sourceVal then - callback { - start = spec.start, - finish = spec.finish, - message = 'devsper: wf.plugin() spec is missing required field "source"', - } - elseif sourceVal.type == 'string' then - local strVal = sourceVal[1] - if strVal then - local builtinName = strVal:match('^builtin:(.+)$') - if builtinName and not validBuiltins[builtinName] then - callback { - start = sourceVal.start, - finish = sourceVal.finish, - message = ('devsper: unknown builtin plugin %q (valid: git, search, http, code, filesystem)'):format(builtinName), - } - end + elseif isLocalMethod(source, 'wf', 'plugin') then + local spec = source.args and source.args[2] + if not spec or spec.type == 'nil' then + callback { + start = source.start, + finish = source.finish, + message = "devsper: wf.plugin() requires a spec table as second argument", + } + return -- no further checks possible + end + if spec.type ~= 'table' then return end -- non-literal, skip + local sourceVal = tableGet(spec, 'source') + if not sourceVal then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: wf.plugin() spec is missing required field "source"', + } + elseif sourceVal.type == 'string' then + local strVal = sourceVal[1] + if strVal then + local builtinName = strVal:match('^builtin:(.+)$') + if builtinName and not validBuiltins[builtinName] then + callback { + start = sourceVal.start, + finish = sourceVal.finish, + message = ('devsper: unknown builtin plugin %q (valid: git, search, http, code, filesystem)'):format(builtinName), + } end end end -- devsper.tool(name, spec) — spec must have `description` and `run` keys elseif isDevsperGlobalMethod(source, 'tool') then - local spec = args[2] - if spec and spec.type == 'table' then - if not tableGet(spec, 'description') then - callback { - start = spec.start, - finish = spec.finish, - message = 'devsper: devsper.tool() spec is missing required field "description"', - } - end - if not tableGet(spec, 'run') then - callback { - start = spec.start, - finish = spec.finish, - message = 'devsper: devsper.tool() spec is missing required field "run"', - } - end + local spec = source.args and source.args[2] + if not spec or spec.type == 'nil' then + callback { + start = source.start, + finish = source.finish, + message = "devsper: devsper.tool() requires a spec table as second argument", + } + return -- no further checks possible + end + if spec.type ~= 'table' then return end -- non-literal, skip + if not tableGet(spec, 'description') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: devsper.tool() spec is missing required field "description"', + } + end + if not tableGet(spec, 'run') then + callback { + start = spec.start, + finish = spec.finish, + message = 'devsper: devsper.tool() spec is missing required field "run"', + } end end end) From 3b25304456c1cdf87503ed28067a647b453e9f97 Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:45:39 +0530 Subject: [PATCH 06/10] feat(diag): add devsper depends_on cross-reference diagnostic Co-Authored-By: Claude Sonnet 4.6 --- script/core/diagnostics/devsper-depends.lua | 88 +++++++++++++++++++++ script/proto/diagnostic.lua | 8 ++ 2 files changed, 96 insertions(+) create mode 100644 script/core/diagnostics/devsper-depends.lua diff --git a/script/core/diagnostics/devsper-depends.lua b/script/core/diagnostics/devsper-depends.lua new file mode 100644 index 000000000..e73c77aa7 --- /dev/null +++ b/script/core/diagnostics/devsper-depends.lua @@ -0,0 +1,88 @@ +local files = require 'files' +local guide = require 'parser.guide' +local await = require 'await' + +-- Returns the value node of a named field in a table AST node, or nil. +local function tableGet(tblNode, key) + if not tblNode or tblNode.type ~= 'table' then return nil end + for i = 1, #tblNode do + local child = tblNode[i] + if child and child.type == 'tablefield' and child.field and child.field[1] == key then + return child.value + end + end + return nil +end + +-- Returns true if `node` is a getfield call like `.methodName(...)` +local function isLocalMethod(source, receiverName, methodName) + if not source or source.type ~= 'call' then return false end + local callee = source.node + if not callee or callee.type ~= 'getfield' then return false end + if not callee.field or callee.field[1] ~= methodName then return false end + local receiver = callee.node + if not receiver or receiver.type ~= 'getlocal' then return false end + return receiver[1] == receiverName +end + +return function (uri, callback) + -- Only run on .devsper files + if not uri:match('%.devsper$') then return end + + local state = files.getState(uri) + if not state then return end + + -- Pass 1: collect all declared task IDs from wf.task(id, spec) calls + local declaredIds = {} + + guide.eachSourceType(state.ast, 'call', function (source) + await.delay() + if not isLocalMethod(source, 'wf', 'task') then return end + local args = source.args + if not args then return end + local idNode = args[1] + if not idNode or idNode.type ~= 'string' then return end + local id = idNode[1] + if id then + declaredIds[id] = { start = idNode.start, finish = idNode.finish } + end + end) + + -- Pass 2: check depends_on entries in each wf.task spec + guide.eachSourceType(state.ast, 'call', function (source) + await.delay() + if not isLocalMethod(source, 'wf', 'task') then return end + local args = source.args + if not args then return end + local spec = args[2] + if not spec or spec.type ~= 'table' then return end + + local dependsOn = tableGet(spec, 'depends_on') + if not dependsOn or dependsOn.type ~= 'table' then return end + + -- Build sorted list of declared IDs for error messages + local sortedIds = {} + for k in pairs(declaredIds) do + sortedIds[#sortedIds + 1] = k + end + table.sort(sortedIds) + + -- Iterate direct children of the depends_on array table + for i = 1, #dependsOn do + local child = dependsOn[i] + if child and child.type == 'string' then + local val = child[1] + if val and val ~= '*' and not declaredIds[val] then + callback { + start = child.start, + finish = child.finish, + message = ("devsper: unknown task id '%s' in depends_on — declared ids: %s"):format( + val, table.concat(sortedIds, ', ') + ), + } + end + end + -- Non-string children (variables, function calls) are silently skipped + end + end) +end diff --git a/script/proto/diagnostic.lua b/script/proto/diagnostic.lua index 4c226399b..c5db62362 100644 --- a/script/proto/diagnostic.lua +++ b/script/proto/diagnostic.lua @@ -235,6 +235,14 @@ m.register { status = 'Any', } +m.register { + 'devsper-depends', +} { + group = 'devsper', + severity = 'Warning', + status = 'Any', +} + ---@return table function m.getDefaultSeverity() local severity = {} From 018dafb7b19f326163dd00bac510e1edfe1286ed Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:46:03 +0530 Subject: [PATCH 07/10] feat(diag): add devsper model string hint diagnostic --- script/core/diagnostics/devsper-model.lua | 104 ++++++++++++++++++++++ script/proto/diagnostic.lua | 8 ++ 2 files changed, 112 insertions(+) create mode 100644 script/core/diagnostics/devsper-model.lua diff --git a/script/core/diagnostics/devsper-model.lua b/script/core/diagnostics/devsper-model.lua new file mode 100644 index 000000000..ef1b0c3bd --- /dev/null +++ b/script/core/diagnostics/devsper-model.lua @@ -0,0 +1,104 @@ +local files = require 'files' +local guide = require 'parser.guide' +local await = require 'await' + +-- Returns the value node of a named field in a table AST node, or nil. +local function tableGet(tblNode, key) + if not tblNode or tblNode.type ~= 'table' then return nil end + for i = 1, #tblNode do + local child = tblNode[i] + if child and child.type == 'tablefield' and child.field and child.field[1] == key then + return child.value + end + end + return nil +end + +-- Returns true if `node` is a getfield call like `.methodName(...)` +local function isLocalMethod(source, receiverName, methodName) + if not source or source.type ~= 'call' then return false end + local callee = source.node + if not callee or callee.type ~= 'getfield' then return false end + if not callee.field or callee.field[1] ~= methodName then return false end + local receiver = callee.node + if not receiver or receiver.type ~= 'getlocal' then return false end + return receiver[1] == receiverName +end + +-- Returns true if `callNode` is `devsper.(...)` +local function isDevsperGlobalMethod(callNode, methodName) + local callee = callNode.node + if not callee then return false end + if callee.type ~= 'getfield' then return false end + if not callee.field then return false end + if callee.field[1] ~= methodName then return false end + local receiver = callee.node + if not receiver then return false end + if receiver.type ~= 'getglobal' then return false end + return receiver[1] == 'devsper' +end + +local KNOWN_PREFIXES = { + '^claude%-', + '^gpt%-', + '^o%d', -- o1, o3, etc. + '^gemini%-', + '^llama', + '^mistral', + '^phi', + '^qwen', + '^deepseek', + '^gemma', + '^falcon', +} + +local KNOWN_EXACT = { auto = true } + +local function isKnownModel(modelStr) + if KNOWN_EXACT[modelStr] then return true end + for _, pattern in ipairs(KNOWN_PREFIXES) do + if modelStr:match(pattern) then return true end + end + return false +end + +local function checkModelValue(modelNode, callback) + if not modelNode then return end + if modelNode.type ~= 'string' then return end -- skip non-literal values + local modelStr = modelNode[1] + if not modelStr then return end + if not isKnownModel(modelStr) then + callback { + start = modelNode.start, + finish = modelNode.finish, + message = ("devsper: unrecognized model '%s' — verify this provider is configured"):format(modelStr), + } + end +end + +return function (uri, callback) + -- Only run on .devsper files + if not uri:match('%.devsper$') then return end + + local state = files.getState(uri) + if not state then return end + + guide.eachSourceType(state.ast, 'call', function (source) + await.delay() + + -- devsper.workflow(config) — check config.model + if isDevsperGlobalMethod(source, 'workflow') then + local config = source.args and source.args[1] + if config and config.type == 'table' then + checkModelValue(tableGet(config, 'model'), callback) + end + + -- wf.task(id, spec) — check spec.model + elseif isLocalMethod(source, 'wf', 'task') then + local spec = source.args and source.args[2] + if spec and spec.type == 'table' then + checkModelValue(tableGet(spec, 'model'), callback) + end + end + end) +end diff --git a/script/proto/diagnostic.lua b/script/proto/diagnostic.lua index c5db62362..b743992bb 100644 --- a/script/proto/diagnostic.lua +++ b/script/proto/diagnostic.lua @@ -243,6 +243,14 @@ m.register { status = 'Any', } +m.register { + 'devsper-model', +} { + group = 'devsper', + severity = 'Hint', + status = 'Any', +} + ---@return table function m.getDefaultSeverity() local severity = {} From bb725d922f20854e2fd4226bd6bf68955dbf79ea Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 10:46:24 +0530 Subject: [PATCH 08/10] feat(vscode): add devsper VS Code extension with snippets and grammar --- tools/vscode/language-configuration.json | 26 ++++++++ tools/vscode/package.json | 32 ++++++++++ tools/vscode/snippets/devsper.json | 61 +++++++++++++++++++ tools/vscode/syntaxes/devsper.tmLanguage.json | 30 +++++++++ 4 files changed, 149 insertions(+) create mode 100644 tools/vscode/language-configuration.json create mode 100644 tools/vscode/package.json create mode 100644 tools/vscode/snippets/devsper.json create mode 100644 tools/vscode/syntaxes/devsper.tmLanguage.json diff --git a/tools/vscode/language-configuration.json b/tools/vscode/language-configuration.json new file mode 100644 index 000000000..d720f1655 --- /dev/null +++ b/tools/vscode/language-configuration.json @@ -0,0 +1,26 @@ +{ + "comments": { + "lineComment": "--", + "blockComment": ["--[[", "]]"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"" }, + { "open": "'", "close": "'" }, + { "open": "--[[", "close": "]]" } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ] +} diff --git a/tools/vscode/package.json b/tools/vscode/package.json new file mode 100644 index 000000000..05fa77c8f --- /dev/null +++ b/tools/vscode/package.json @@ -0,0 +1,32 @@ +{ + "name": "devsper-lsp", + "displayName": "Devsper Language Support", + "description": "Autocomplete, hover docs, and diagnostics for .devsper workflow and tool files", + "version": "0.1.0", + "publisher": "devsper", + "engines": { "vscode": "^1.85.0" }, + "categories": ["Programming Languages", "Snippets"], + "contributes": { + "languages": [ + { + "id": "devsper", + "aliases": ["Devsper", "devsper"], + "extensions": [".devsper"], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "devsper", + "scopeName": "source.devsper", + "path": "./syntaxes/devsper.tmLanguage.json" + } + ], + "snippets": [ + { + "language": "devsper", + "path": "./snippets/devsper.json" + } + ] + } +} diff --git a/tools/vscode/snippets/devsper.json b/tools/vscode/snippets/devsper.json new file mode 100644 index 000000000..2fbc6eb25 --- /dev/null +++ b/tools/vscode/snippets/devsper.json @@ -0,0 +1,61 @@ +{ + "Workflow": { + "prefix": "workflow", + "body": [ + "local wf = devsper.workflow({", + "\tname = \"${1:my-workflow}\",", + "\tmodel = \"${2:auto}\",", + "\tworkers = ${3:4},", + "})", + "", + "wf.task(\"${4:task-1}\", {", + "\tprompt = \"${5:Describe what this task should do.}\",", + "})", + "", + "return wf" + ], + "description": "New devsper workflow" + }, + "Task": { + "prefix": "task", + "body": [ + "wf.task(\"${1:task-name}\", {", + "\tprompt = \"${2:Describe what this task should do.}\",", + "\tdepends_on = { ${3} },", + "})" + ], + "description": "Add a task to the workflow DAG" + }, + "Plugin": { + "prefix": "plugin", + "body": [ + "wf.plugin(\"${1:git}\", { source = \"builtin:${1:git}\" })" + ], + "description": "Load a builtin plugin (git, search, http, code, filesystem)" + }, + "Input": { + "prefix": "input", + "body": [ + "wf.input(\"${1:param_name}\", {", + "\ttype = \"${2|string,number,boolean,object|}\",", + "\trequired = ${3|true,false|},", + "})" + ], + "description": "Declare a typed workflow input" + }, + "Tool": { + "prefix": "tool", + "body": [ + "devsper.tool(\"${1:tool.name}\", {", + "\tdescription = \"${2:What this tool does.}\",", + "\tparams = {", + "\t\t${3:param} = { type = \"${4|string,number,boolean,object|}\", required = ${5|true,false|} },", + "\t},", + "\trun = function(ctx, args)", + "\t\t${6:-- implementation}", + "\tend,", + "})" + ], + "description": "Register a new devsper tool" + } +} diff --git a/tools/vscode/syntaxes/devsper.tmLanguage.json b/tools/vscode/syntaxes/devsper.tmLanguage.json new file mode 100644 index 000000000..ea95b1c1d --- /dev/null +++ b/tools/vscode/syntaxes/devsper.tmLanguage.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Devsper", + "scopeName": "source.devsper", + "patterns": [ + { "include": "#devsper-keywords" }, + { "include": "source.lua" } + ], + "repository": { + "devsper-keywords": { + "patterns": [ + { + "comment": "devsper global and wf builder", + "match": "\\b(devsper|wf)\\b(?=\\.)", + "name": "support.class.devsper" + }, + { + "comment": "builtin: plugin name prefix", + "match": "\"builtin:[a-zA-Z0-9_-]+\"", + "name": "support.constant.devsper" + }, + { + "comment": "devsper method names", + "match": "\\.(workflow|tool|exec|log|http|get|post|task|plugin|input)\\b", + "name": "support.function.devsper" + } + ] + } + } +} From 31541c28bebb60ecb728cdd4a0bdd6c579687ec8 Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 11:06:35 +0530 Subject: [PATCH 09/10] chore: update changelog for devsper language support --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 42d0d59c5..7bab579d6 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,11 @@ ## Unreleased +* `NEW` Add devsper language support for `.devsper` workflow and tool files: type library (`meta/3rd/devsper/`) with full `---@class` annotations for the `devsper` global, `WorkflowBuilder`, and all spec types — enabling autocomplete and hover docs in any editor using this LSP +* `NEW` Add `devsper-schema` diagnostic: warns on missing required fields (`prompt` in `wf.task`, `source` in `wf.plugin`, `description`/`run` in `devsper.tool`), unknown `builtin:` plugin names, and missing spec arguments +* `NEW` Add `devsper-depends` diagnostic: cross-references `depends_on` task IDs against declared tasks in the same file; `"*"` wildcard always valid +* `NEW` Add `devsper-model` diagnostic: hints on unrecognized literal model strings; skips variables and known provider prefixes (`claude-*`, `gpt-*`, `gemini-*`, `llama*`, etc.) +* `NEW` Add VS Code extension scaffold (`tools/vscode/`) with `.devsper` file association, TextMate grammar inheriting Lua base, and five snippets (`workflow`, `task`, `plugin`, `input`, `tool`) ## 3.18.2 * `CHG` `duplicate-set-field` diagnostic now supports linked suppression: when any occurrence of a duplicate field is suppressed with `---@diagnostic disable` or `---@diagnostic disable-next-line`, all warnings for that field name will be suppressed From 5e993c7956b08389582733a29eeb789dfe464d2e Mon Sep 17 00:00:00 2001 From: Rithul Kamesh Date: Tue, 21 Apr 2026 11:09:10 +0530 Subject: [PATCH 10/10] fix(diag): fix depends_on array node type (tableexp) and hoist sortedIds out of callback --- script/core/diagnostics/devsper-depends.lua | 44 +++++++++++---------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/script/core/diagnostics/devsper-depends.lua b/script/core/diagnostics/devsper-depends.lua index e73c77aa7..c603775a3 100644 --- a/script/core/diagnostics/devsper-depends.lua +++ b/script/core/diagnostics/devsper-depends.lua @@ -48,6 +48,14 @@ return function (uri, callback) end end) + -- Build sorted ID list once for error messages + local sortedIds = {} + for k in pairs(declaredIds) do + sortedIds[#sortedIds + 1] = k + end + table.sort(sortedIds) + local declaredIdsStr = table.concat(sortedIds, ', ') + -- Pass 2: check depends_on entries in each wf.task spec guide.eachSourceType(state.ast, 'call', function (source) await.delay() @@ -60,29 +68,25 @@ return function (uri, callback) local dependsOn = tableGet(spec, 'depends_on') if not dependsOn or dependsOn.type ~= 'table' then return end - -- Build sorted list of declared IDs for error messages - local sortedIds = {} - for k in pairs(declaredIds) do - sortedIds[#sortedIds + 1] = k - end - table.sort(sortedIds) - - -- Iterate direct children of the depends_on array table + -- Array table elements are wrapped in tableexp nodes in the luals AST for i = 1, #dependsOn do - local child = dependsOn[i] - if child and child.type == 'string' then - local val = child[1] - if val and val ~= '*' and not declaredIds[val] then - callback { - start = child.start, - finish = child.finish, - message = ("devsper: unknown task id '%s' in depends_on — declared ids: %s"):format( - val, table.concat(sortedIds, ', ') - ), - } + local exp = dependsOn[i] + if exp and exp.type == 'tableexp' then + local child = exp.value + if child and child.type == 'string' then + local val = child[1] + if val and val ~= '*' and not declaredIds[val] then + callback { + start = child.start, + finish = child.finish, + message = ("devsper: unknown task id '%s' in depends_on — declared ids: %s"):format( + val, declaredIdsStr + ), + } + end end end - -- Non-string children (variables, function calls) are silently skipped + -- Non-string/non-literal children silently skipped end end) end