DecisionLint: Jev/Luna Lint

Registre officiel Outils de développement

Lint Jev and Luna decision-model questions, or ask the model to check them with your key.

Que pouvez-vous faire avec DecisionLint: Jev/Luna Lint ?

  • Lints as you type in JSON, JavaScript, TypeScript, Python, Rust and Go.
  • Fixes on the lightbulb for the mechanical mistakes, and a way to silence
  • The same rules in CI, from a command line, and for AI agents, from an
  • Every platform, one set of rules. TypeSafe's /v1/systemone shape and
  • Two optional commands that use your own key: one asks the model

Installation

Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).

NPM package decisionlint-mcp
Claude Code
claude mcp add decisionlint-jev-luna-lint -- npx -y decisionlint-mcp@1.1.0
Configuration JSON (Claude Desktop, Cursor, VS Code)
{
    "mcpServers": {
        "decisionlint-jev-luna-lint": {
            "command": "npx",
            "args": [
                "-y",
                "decisionlint-mcp@1.1.0"
            ]
        }
    }
}

Documentation

Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.

Sommaire
  1. DecisionLint: Jev/Luna Lint
  2. What it does
  3. Install
  4. Use it from an AI agent
  5. What it catches
  6. Where it looks
  7. OpenAI's Decisions API
  8. Every platform
  9. Where it has been tested
  10. Ask a model about a file
  11. Probe a question
  12. Project settings
  13. What it will not tell you
  14. Suppressing a finding
  15. Fixing
  16. The CLI
  17. Commands
  18. Settings
  19. Privacy & security
  20. Documentation
  21. Development
  22. More from the LE family
  23. Also by nolindnaidoo
  24. License

DecisionLint: Jev/Luna Lint

Lint Jev and Luna decision-model questions before you send them

JSON, JavaScript, TypeScript, Python, Rust and Go, for TypeSafe, OpenAI and every platform that serves either


Useful? A star or rating is how other developers find it — ★ GitHub · ★ Open VSX · ★ Marketplace

What it does

Lint Jev and Luna questions before you send them. DecisionLint checks the questions you write for a decision model: TypeSafe's Jev, OpenAI's Decisions API (gpt-6-luna), and every platform that serves either shape, from OpenRouter and Vercel AI Gateway to a llama.cpp on your own machine. DecisionLint reads them out of your JSON and your code, and reports the ones written in a way that is measured to produce bad answers. Linting makes no API calls and needs no key. One optional command asks the model itself to check a file, using your own key, at whichever platform you send it to.

  • Lints as you type in JSON, JavaScript, TypeScript, Python, Rust and Go.
  • Fixes on the lightbulb for the mechanical mistakes, and a way to silence a finding you disagree with.
  • The same rules in CI, from a command line, and for AI agents, from an MCP server. One settings file covers all three.
  • Every platform, one set of rules. TypeSafe's /v1/systemone shape and OpenAI's /v1/decisions shape, predicate, choices and levels, are read into the same rules, and the AI SDK's decide() call for either. The limits a request is held to are the platform's own, read from the model id's spelling, a URL in the file or a target setting: Vercel's 255 choices, LiteLLM's 128 questions, none where a platform publishes none.
  • Two optional commands that use your own key: one asks the model whether your questions have problems a text pattern cannot see, and one re-sends a question with its layout changed to see whether the answer holds. Both go to the platform the model id names, or to an endpoint you set, a local llama.cpp included.

A request like this one looks fine and has three problems:

{
  "model": "jev-latest",
  "state": "My parcel came three days late and the lid is cracked.",
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": { "billing": "Charges and refunds", "delivery": "Late or damaged parcels" }
    },
    "late": { "type": "noul", "instructions": "Did more than two parcels arrive late?" }
  }
}
FindingWhy it matters
DEC001 on jev-latestThe alias moves with each release, so answers can change with no change on your side
DEC004 on teamA message about neither team is still forced into one of them
DEC102 on lateJev does not count reliably. Ask about each parcel and count in code

Install

WhereWhat you getInstall
VS CodeLinting as you type, fixes on the lightbulb, and the Ask and Probe commandsMarketplace · ext install nolindnaidoo.decisionlint
Cursor, VSCodium, WindsurfThe same extensionOpen VSX
A terminal or a CI stepThe same rules over a whole tree, with exit codes, SARIF and JUnitnpx decisionlint · npm
Any MCP agent, via NodeEight tools over stdionpx -y decisionlint-mcp · npm

Use it from an AI agent

The same program is an MCP server, so an agent that writes Jev or OpenAI Decisions questions can lint what it wrote and fix it before you see it. The tool descriptions and the server's instructions name both vendors and all three request shapes, so an agent knows to call it for any of them.

EditorHow
VS Code 1.101+Nothing to install. The extension offers the server to the editor, which starts it when an agent calls a tool
Claude Codeclaude mcp add decisionlint -- npx -y decisionlint-mcp
Cursor, Windsurf, anything elsePoint it at npx -y decisionlint-mcp

It offers eight tools. lint_text lints a request or source code passed as text and lint_paths lints files on disk, with each finding carrying the edit that would mend it. fix_text returns the text with the safe fixes applied and the report of what is left, and writes nothing. list_rules lists every rule with what it means, and explain_rule returns a rule's own page, with an example that is flagged and one that is not. Those send nothing and need no key.

The other three are the editor's Ask commands and Probe for an agent. ask_model asks Jev, or Luna with model: gpt-6-luna, to check the questions in text or in files, and returns the lint report with the model's findings added. plan_ask says what that would send, and sends nothing. probe_question sends one question with its layout varied and reports whether the answer held. The two that send use your key and cost money, and their descriptions tell an agent to call them only when you ask. The key is read from TYPESAFE_API_KEY or OPENAI_API_KEY in the environment the client started the server with, or from .env.local or .env in the directory it started it in, and never from a tool argument, so no key lands in an agent's transcript. Every tool answers as structured content beside the text, and none writes a file.

It is listed on the MCP registry as io.github.nolindnaidoo/decisionlint, for a client that installs servers from there.

Configuring it by hand: any host with an MCP config file

Most hosts read a JSON config. Add one entry:

{
  "mcpServers": {
    "decisionlint": { "command": "npx", "args": ["-y", "decisionlint-mcp"] }
  }
}

-y skips the install prompt on first run. To skip npx on every launch, install it once with npm install -g decisionlint-mcp and set "command": "decisionlint-mcp" instead.

A project that already pins decisionlint for CI has the same server behind npx decisionlint --mcp. The package with no flag is the one to give a client, because decisionlint started bare is a linter that exits, not a server.

What it catches

Jev and Luna guarantee the type of the answer, not the answer. A badly formed question still comes back with a confident-looking number. These rules catch the mistakes that can be read from the text.

CodeNameDefaultWhat it means
DEC000unreadablehintPart of the question is built at runtime, so the rules that need it did not run
DEC001unpinned-modelwarningAn alias such as jev-latest moves with each release, so answers can change with no change on your side
DEC002choice-option-limiterrorA Choice has more than 255 options. The API rejects it
DEC003score-level-limiterrorA Score has more than 10 levels. The API rejects it
DEC004no-fallback-optioninfoA Choice has no other or none of the above, so an input that fits no option is forced into one. The largest cost measured anywhere, and the hardest to tell from text
DEC005duplicateerrorA question id, option or level appears twice. In an object the later one silently replaces the earlier
DEC006criteria-shapeerrorA Choice needs a map of options, a Score needs an array of levels, and a Noul takes true and false
DEC007invalid-questionerrorThe type is missing or unknown, or the instructions are an empty string
DEC008numeric-levelswarningScore levels are bare numbers. Jev matches the state against each description and never sees its position
DEC009too-few-optionsinfoA Choice with one option or a Score with one level gives every input the same answer
DEC010unused-disablewarningA decisionlint-disable comment silences nothing, so it can only hide a finding added later
DEC011description-repeats-nameinfoAn option's description only repeats its name, or a Noul's criteria say yes and no. Jev matches the state against the description, and this one says nothing
DEC012terse-instructionsinfoThe instructions are one or two words, so the question leans on its id. Jev is sent the instructions and never the id
DEC013newer-modelinfomodel names a Jev version newer than the one these rules were measured on, so a finding may cost nothing there and a weakness new to it is not looked for
DEC014question-limiterrorA request has more questions than the platform it goes to takes in one call. The platform rejects it
DEC101double-negativeinfoA question negates twice in one clause, or a negation sits directly on another
DEC102arithmeticwarningThe question asks Jev to count or compare numbers
DEC103date-comparisoninfoThe question asks Jev to order two times or measure the gap between them
DEC104compoundoffA Noul joins two judgments with 'and'
DEC105generationwarningThe question asks for a value or for text to be written
DEC107negated-nouloffA Noul with no criteria is phrased so that yes means something is absent
DEC108inverted-criteriaoffA Noul's 'true' criterion describes the negative case
DEC110degree-levelsoffScore levels are degree words, bare labels, or one word turned up and down. Nothing for Jev to match the state against
DEC111numeric-encodingoffThe question refers to a value by hex or RGB encoding
DEC112undefined-boundaryoffA Noul with no criteria turns on a word such as large, often or enough
DEC301model-countingwarningJev reads the question as needing counting or arithmetic
DEC302model-undefined-boundaryinfoJev reads a Noul as turning on a matter of degree with no stated line
DEC303model-overlapping-optionsinfoJev reads two options of a Choice as covering the same cases
DEC304model-overlapping-levelswarningJev reads two levels of a Score as the same situation
DEC305model-label-mismatchwarningJev reads an option as named for one thing and described as another
DEC306model-criteria-off-topicinfoJev reads the criteria as deciding something the instructions do not ask
DEC307model-ordered-optionsinfoJev reads a Choice's options as steps on one scale, which a Score would place between
DEC308model-unordered-levelsinfoJev reads a Score's levels as unordered categories, which a Choice would pick from
DEC309model-depends-on-siblinginfoJev reads a question as needing another question's answer from the same request
DEC310model-overlapping-questionsinfoJev reads two questions in one request as asking for the same judgment
DEC311model-answer-not-in-stateinfoJev reads the state written in the file as not holding what the question asks about
DEC312model-orders-in-stateinfoJev reads part of the state written in the file as giving orders to its reader

Each finding links to its own page, with an example that is flagged, one that is not, how to fix it and how to silence it. Each page links on to the TypeSafe documentation the rule comes from.

Where it looks

  • JSON and JSONC request bodies, in TypeSafe's shape or OpenAI's.
  • Object literals in JavaScript and TypeScript, including requests passed inline to client.systemOne(...) and client.decisions.create(...), and the Vercel AI SDK's decide({ questions }) for either vendor, with the model id inside typeSafeAi.decisionModel('...') read as the model.
  • noul(), choice() and score() calls in files that use @typesafe-ai/sdk.
  • Python: dicts, Noul(...), Choice(...) and Score(...) from typesafe_sdk, from LangChain's langchain_typesafe with its NoulCriteria(...), or from a library that wraps either, and requests passed to system_one(...) or client.decisions.create(...).
  • Rust: the JSON inside json!, structs and enum variants named for a question type, and ::noul(...) style constructors.
  • Go: maps with string keys, structs with a Type field, structs named for a question type, and OpenAI's Go SDK, openai.DecisionNewParams{...} with its OfPredicate, OfChoice and OfScore questions.
  • A request pasted as JSON into a string, in any of these languages.
  • Any other language: have your program write the request it sends to a .jev.json file and open that. Every check runs on it, and nothing in it is built at runtime, so nothing is skipped.
  • jev/ask rules in an oxlint-plugin-jev config.

OpenAI's Decisions API

OpenAI's Decisions API, model gpt-6-luna, asks the same kind of question as Jev in a different shape. DecisionLint reads it from 0.5.0. What that covers, exactly:

  • Read. A /v1/decisions request body, client.decisions.create(...) in TypeScript and Python, and the Vercel AI SDK's decide(). A predicate is read as a Noul, choices: [{ value, description }] as a Choice's options and levels: [{ label, description }] as a Score's levels, into the same rules.
  • Checked. Every rule with evidence behind it: the missing fallback option, a description that only repeats its name, terse instructions, degree levels, duplicates, counting, an undefined boundary and the rest. Findings name Luna. OpenAI's own guide asks for the same things.
  • Held back. DEC002 and DEC003 check a platform's published limits, and OpenAI publishes none, so a request bound for OpenAI gets neither. One bound for a gateway that does, such as Vercel's, gets both. DEC001 is about Jev's moving aliases, and OpenAI has one model id.
  • Shape mistakes are reported in OpenAI's own field names: choices written as a map or as bare names, a Choice with no choices, a Score with no levels. A mistyped predicate is fixed on the lightbulb. The reshape fix is Jev-only.
  • Asking Luna. Run Ask Luna About This File, or pass --ask-platform openai, with an OpenAI key. The next section says how.
  • Calibrated on Jev, with cutoffs of its own on Luna. Every default in the wording rules was set by measuring what the defect costs jev-1.13.0. The model-backed checks were run on Luna over the whole calibration set on 2026-10-24 and each got a cutoff fit on Luna's own answers, where one could be: seven of the twelve. The other five are not asked of Luna, because no cutoff told the examples with the defect from the examples without, and docs/cutoffs.md names the example in the way of each. What the wording rules cost Luna is not measured yet.

Every platform

Jev and Luna are two of the models, and TypeSafe and OpenAI two of the places a request can go. Microsoft, Liquid AI and Cloudflare serve their own models in TypeSafe's shape. OpenRouter, Vercel AI Gateway, LiteLLM and LLM Gateway forward to any of them, in either shape. llama.cpp, vLLM and Ollaya run open models on your own machine. DecisionLint reads the two shapes, so every one of them is covered by the same rules. What differs between them is held in one table, src/lint/targets.ts, with the page each fact was read from:

PlatformReached asKey variableLimits it publishes
TypeSafejev-1.13.0, jev-latestTYPESAFE_API_KEY255 options, 10 levels
OpenAIgpt-6-lunaOPENAI_API_KEYnone
OpenRouter~typesafe/jev-latest, typesafe/jev-1.13OPENROUTER_API_KEYnone
Vercel AI Gatewaytypesafe-ai/jev, openai/gpt-6-luna-decisionsAI_GATEWAY_API_KEY255 choices, 10 levels
LLM Gatewaygpt-6-luna-decisions, microsoft-decision-1LLM_GATEWAY_API_KEYnone
Liquid AId1, d1:freeLIQUID_API_KEYnone
LiteLLMan endpoint you setthe shape's128 questions per request
Eden AIits own pathits own255 options
llama.cppan endpoint you setnone10 levels

Three things follow from the table:

  • The limits a request is held to are its platform's. The platform is read from the model id where its spelling says, from the one platform a URL in the file names, or from target in decisionlint.json or decisionlint.target for a project that sends everything to one place. A platform that publishes no limit gets no finding for it. Nothing says where a request goes: a TypeSafe-shaped question keeps TypeSafe's limits, and any other shape keeps none.
  • A floating alias is caught in every spelling, and pinned in the spelling the platform documents: jev-latest to jev-1.13.0, ~typesafe/jev-latest to typesafe/jev-1.13.
  • A check with the model goes to the platform the id names, with the key in that platform's variable. A spelling that names no host, such as openai/gpt-6-luna-decisions, which every gateway writes, is refused rather than guessed at, until you set the endpoint. The next two sections say how.

"Open Jev" is not one of these. It is a name used by at least nine unrelated projects that reimplement /v1/systemone; the one llama.cpp serves is ggml-org/OpenJev-GGUF. Each is reached as an endpoint you set.

Where it has been tested

Every platform below was sent real requests through its live API, and the model-backed checks were run over the whole calibration set on each:

  • TypeSafe, jev-1.13.0, where Jev's cutoffs were set.
  • OpenRouter, typesafe/jev-1.13. It answers as TypeSafe does, so its findings are treated as calibrated.
  • LLM Gateway, jev-1.13.0, and Vercel AI Gateway, typesafe-ai/jev. Both are sent OpenAI's shape, which moves Jev's answers a little, so their findings say the cutoff was set at TypeSafe.
  • OpenAI, gpt-6-luna, where Luna's cutoffs were fit, and Vercel AI Gateway, openai/gpt-6-luna-decisions, which answers as OpenAI does.
  • Liquid AI, d1, where d1's cutoffs were fit.

Luna and d1 have cutoffs of their own since 1.1.0, each fit on that model's run over the whole calibration set: a cutoff flags none of the good examples and catches at least six in ten of the bad ones. Where no cutoff can, the check is not asked of that model: five checks on Luna and two on d1. Any other model, or one of these under another id or through another route, is judged by its family's cutoffs, or Jev's, and every finding from it says so. docs/cutoffs.md has each cutoff, how many known defects it caught and how many good questions it flagged on each model, why each check that is not asked is not, and how far each platform's answers sit from the same model elsewhere. It is written from the saved runs, so its numbers are the runs' own. LiteLLM, Eden AI and llama.cpp have not been run yet.

Ask a model about a file

Rules DEC301 to DEC312 are not part of linting. They run only when you run one of the Ask commands, or pass --ask on the command line. Each sends every question in the file to a decision model and asks it about problems a text pattern cannot see: options that overlap, an option whose name contradicts its description, criteria about the wrong thing, a Choice that should be a Score, two questions that ask the same thing, a question that needs another question's answer.

There is one command per platform, and each has its own key:

CommandSends toModel by defaultKey
Ask Jev About This FileTypeSafejev-1.13.0Set TypeSafe API Key, or TYPESAFE_API_KEY
Ask Luna About This FileOpenAIgpt-6-lunaSet OpenAI API Key, or OPENAI_API_KEY
Ask via OpenRouter About This FileOpenRoutertypesafe/jev-1.13Set OpenRouter API Key, or OPENROUTER_API_KEY
Ask via Vercel AI Gateway About This FileVercel AI Gatewaytypesafe-ai/jevSet Vercel AI Gateway API Key, or AI_GATEWAY_API_KEY
Ask via LLM Gateway About This FileLLM Gatewayjev-1.13.0Set LLM Gateway API Key, or LLM_GATEWAY_API_KEY
Ask Liquid d1 About This FileLiquid AId1Set Liquid AI API Key, or LIQUID_API_KEY
Ask the Model at My Endpoint About This Filedecisionlint.ask.endpointdecisionlint.ask.endpointModelSet Endpoint API Key, or none
  1. Give it the platform's key, in any of three ways. Run its Set ... API Key command and paste the key, which stores it in your operating system keychain. Or type it into Settings under decisionlint.ask.keys, by the platform's id, which can only be set in your user settings and is left out of Settings Sync. Or put it in the environment variable the platform names, in the environment VS Code runs in.
  2. Open a file with questions and run the platform's Ask command.

The model each command sends is decisionlint.ask.models, by platform. The TypeSafe default is the version the checks' cutoffs were set on, and a finding from any other model, Luna included, says so. OpenRouter's typesafe/jev-1.13 was measured to answer as that version does, so its findings do not. The probe asks Jev at TypeSafe, with the TypeSafe key and model.

For a platform you host, a llama.cpp, vLLM, LiteLLM or Ollaya, set decisionlint.ask.endpoint to the full URL it listens at, decisionlint.ask.endpointModel to the model id it serves, and decisionlint.ask.endpointShape to typesafe or openai when the path does not end in /systemone or /decisions. Then run Ask the Model at My Endpoint. It sends the endpoint key if you set one, else the key in TYPESAFE_API_KEY or OPENAI_API_KEY, whichever the shape's SDK reads, or no key when there is none, which is how a local llama.cpp is reached. The endpoint can only be set in your user settings, so a repository cannot point your key at a host of its choosing.

What to know before you run it:

  • It uses your key and your credits. One request per question and one more per request body, roughly 800 input tokens each on Jev and twice that on Luna. At TypeSafe's published price that is a few thousandths of a cent per question, and at OpenAI's one or two hundredths. The count shown before sending is an estimate, rounded up, and what the platform bills can differ.
  • Only questions are sent: type, instructions, criteria and ids. Your state is not, unless you turn on decisionlint.ask.sendState. With that on, a state written out in the file is sent too, and two more checks run: does the state hold what each question asks about, and does it carry text that gives orders to its reader.
  • A question with a part built at runtime is not sent. The summary says how many were held back.
  • A run sends at most decisionlint.ask.maxCalls requests, 25 by default.
  • It is disabled in an untrusted workspace. Run Workspaces: Manage Workspace Trust and trust the folder to turn it on.
  • Findings disappear when you edit the file, because they were about the old text. Editing or closing the file during a check stops the check.
  • Each finding shows the probability Jev gave it. Treat it as an argument with a number attached, not a verdict.

Turn on decisionlint.ask.confirm and it first tells you how many requests it will make, roughly how many tokens and what that costs, and waits for a yes. It is off by default: running the command is the decision to send.

Probe a question

Put the cursor in a question and run DecisionLint: Probe the Jev Question at the Cursor. It sends that question against the state written beside it, three times as written and then with the layout changed: options reversed, option names hidden, levels reversed, criteria removed. A report opens showing whether the answer moved.

If the answer changes when only the order of the options changes, the question is not deciding it. If three identical requests disagree, the input is too close to call.

It needs the state written out in the file, it sends that state, and it costs five or six requests.

Nothing else in this extension uses the network. On the command line, only --ask does.

Project settings

Put a decisionlint.json in your project and the editor and the command line both read it, so what you see while writing is what CI reports.

{
  "rules": { "DEC004": "error", "DEC110": "warning" },
  "fallbackOptions": ["other", "none", "unsure"],
  "ignore": ["DEC004:department"],
  "exclude": ["fixtures", "**/*.generated.ts"]
}

exclude lists files and folders not to lint, relative to the settings file. ** crosses folders, * stays inside one, and a name with no slash matches at any depth. The editor and the command line leave out the same files.

The nearest file wins, looking from the linted file upward, so a folder can have its own. Where one applies, it replaces the rules, fallbackOptions and ignore settings in the editor. If it cannot be read, nothing is linted and the status bar says why. A key it does not know is an error, so a typo cannot quietly leave a rule on.

To silence one finding, use the lightbulb: Disable DEC004 for this line or for this file writes the comment for you. Strict JSON has no comments, so there a finding is silenced with ignore.

What it will not tell you

  • Whether a well-formed question is right on your data. That needs labeled examples and calls to Jev.
  • Anything about a value it cannot see. A question built from variables is reported as unreadable and counted in the status bar. A file is never shown as clean while part of it went unread.
  • Everything about wording. DEC101 to DEC112 are heuristics over English text. A rule is on only where a badly written question cost answers or confidence against Jev, in our runs or in someone else's, and it stays on only while it is right on at least 9 of 10 of its findings on public code it was not tuned against. Four that describe failure modes in TypeSafe's docs, DEC104, DEC107, DEC108 and DEC111, did not fail on jev-1.13.0 and are off. DEC110 and DEC112 cost answers but fell under 9 of 10 on the second public sample, at 8 of 9 and 2 of 3, and are off since 1.1.0. Switch any of them on in decisionlint.rules. Two more were removed in 0.4.0 for being wrong every time they fired.
  • Anything about a question that is not in English.
  • What a lone object lacks. A { type, instructions } outside a questions map may be a template or half of a builder, so a missing criteria is only reported inside a request.

DEC004 is informational because the advice behind it is conditional: add a fallback when the options might not cover every input. It fires on 9 of the 11 Choice examples in TypeSafe's own docs. If your options are exhaustive, turn it off for that question.

Suppressing a finding

In code:

// decisionlint-disable-next-line DEC004
const side = { type: 'choice', instructions: 'Heads or tails?', criteria: { heads: null, tails: null } };

decisionlint-disable-line covers the same line and decisionlint-disable the whole file. Leave the code off to silence every rule.

A comment that silences nothing is reported as DEC010, so one left behind after its finding was fixed cannot hide a new finding later.

What was written for JevLint-LE still works: a jevlint-le-disable comment, and the JEV codes in a comment, in rules or in ignore, are read for ever. Every finding is written with the DEC code.

Fixing

Some findings can be mended without a decision from you: a Noul criteria key written yes where the API takes true, a mistyped question type, criteria in the wrong shape. npx decisionlint --fix writes those, and the summary of any run says how many it would mend.

In the editor, fix on save does the same:

{ "editor.codeActionsOnSave": { "source.fixAll.decisionlint": "explicit" } }

Adding a fallback option and pinning a model change what a working request does, so they are never written for you. They stay on the lightbulb.

JSON has no comments, so use the setting:

{ "decisionlint.ignore": ["DEC004:side"] }

The CLI

The same linting runs outside the editor, for CI and for any editor that is not VS Code.

One version for the editor and CI

Install it in the project, as you would any linter:

npm install --save-dev decisionlint

The editor then lints with that copy, not the one the extension carries, so what you see while typing is what npx decisionlint reports in CI. The version changes when package.json does, and for everyone at once. The status bar shows project 0.5.2 while a project's copy is in use.

With nothing installed the extension lints with its own copy, with no setup.

It also uses its own copy, and the status bar says project has 0.2.0 with the reason in its tooltip, when the project's copy cannot be used:

  • the workspace is not trusted, because loading the copy runs code from it
  • the copy is older than 0.3.0, the first version an editor can load
  • the copy fails to load, or does not read that kind of file

When the copy's package.json cannot be read there is no version to show, so the status bar says project copy unreadable, again with the reason in its tooltip.

The Ask commands and the probe always run the extension's own code, whatever the project installs.

npx decisionlint src/                 # a directory, searched for files it reads
npx decisionlint request.jev.json     # one file
npx decisionlint --format github .    # annotations on a pull request

It exits 0 when the run passes, 1 when a finding fails it, and 2 when it could not do what was asked: an unknown option, a missing path, or nothing to read. Errors always fail a run. Warnings fail it only past --max-warnings.

OptionWhat it does
`--format <stylish\compact\json\github\sarif\junit>`How findings are printed. stylish groups them by file and is the default. compact is one per line as path:line:column. sarif is for code scanning and junit for test reporters. Also -f
--rule <CODE=level>Set one rule to off, hint, info, warning or error. A rule's name works in place of its code, and warn means warning
--config <file>Use this settings file, and no decisionlint.json found near the files. Also -c
--max-warnings <n>Fail when more than n warnings are reported
--stdin-filename <path>Lint standard input as if it were that file
--fixWrite the fixes that only mend what the API would refuse
--quietPrint errors only
--askAlso ask the model about each question. Sends them to the platform the model id names: TypeSafe with TYPESAFE_API_KEY by default, OpenAI with OPENAI_API_KEY under --ask-model gpt-6-luna, OpenRouter, Vercel AI Gateway, LLM Gateway or Liquid AI under their spellings, or wherever --ask-endpoint says
--ask-planSay what --ask would send, and send nothing
--ask-platform <id>Where --ask sends, named outright: typesafe, openai, openrouter, vercel, llmgateway or liquid, each with its own key variable and a default model in its own spelling. Without it a model id goes where its spelling says
--ask-model <id>The model --ask asks: a Jev version, gpt-6-luna for OpenAI's Decisions API, or a model id in another platform's spelling. Default jev-1.13.0
--ask-endpoint <url>The full URL --ask posts to, for a platform you host or a gateway spelling of the model that names no host. Also read from DECISIONLINT_ASK_ENDPOINT. A host the tool does not know is sent the key of the shape, or none
--ask-shape <shape>The shape sent to --ask-endpoint where its path does not end in /systemone or /decisions: typesafe or openai. Also read from DECISIONLINT_ASK_SHAPE
--ask-max-calls <n>The most requests --ask may send in the run. Default 25
--ask-send-stateWith --ask, also send state written out in a file
--color, --no-colorColour the default format, or do not. Without either it is coloured in a terminal, unless NO_COLOR is set
--no-error-on-unmatched-patternPass when the paths hold no file to lint
--mcpRun as an MCP server

The summary line always says how many questions could not be read in full and names any file skipped for its size and any folder or file it could not read. It does not follow linked folders. It does not run the checks that ask Jev, and it never uses the network, unless you pass --ask.

In CI and before a commit

On GitHub, the action annotates a pull request:

- uses: nolindnaidoo/decisionlint@v1.1.0
  with:
    paths: src

For GitHub code scanning, write SARIF and upload it:

- run: npx decisionlint --format sarif src > decisionlint.sarif
  continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: decisionlint.sarif

--format junit writes the XML that Jenkins, GitLab and most test reporters read. Only an error is a failed case there, since only an error fails a run.

With pre-commit:

- repo: https://github.com/nolindnaidoo/decisionlint
  rev: v1.1.0
  hooks:
    - id: decisionlint
Asking a model

--ask runs the checks that ask a decision model, DEC301 to DEC312, after the linting. It is the only option that uses the network, and nothing in a settings file can turn it on.

npx decisionlint --ask-plan src/                    # what it would send. No key, nothing sent
TYPESAFE_API_KEY=... npx decisionlint --ask src/    # send it
  • --ask-platform names where it goes: typesafe, openai, openrouter, vercel, llmgateway or liquid, each with a default model in its own spelling and its own key variable, TYPESAFE_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, LLM_GATEWAY_API_KEY or LIQUID_API_KEY. Without it, --ask-model with a platform's spelling, ~typesafe/jev-latest for OpenRouter, sends there, and a bare id goes to its vendor. The key is read from that variable and from nowhere else, and is never printed.
  • --ask-endpoint, or DECISIONLINT_ASK_ENDPOINT, sends to a platform you host, with --ask-shape where the path does not say which shape it takes. A spelling that names no host, such as openai/gpt-6-luna-decisions, is refused without it, with nothing sent.
  • A question with a part built at runtime is never sent. The summary counts the ones held back.
  • State is not sent unless you add --ask-send-state.
  • --ask-max-calls is a limit for the whole run. A run that needs more exits 2, so a job cannot pass after checking part of a project.
  • A rejected key, a failed request or a run you stop also exits 2, and says how many requests were answered of how many were planned.
  • These findings are warnings or less by default, so they fail a run only when --rule raises one or --max-warnings is passed.

Commands

Every command can be given a shortcut. Open Keyboard Shortcuts, search for DecisionLint, and press the keys you want on any command in the list. None is bound out of the box, so nothing here can collide with a shortcut you already use.

Or write them into keybindings.json. The keys here are only an example:

[
  { "key": "ctrl+alt+j", "command": "decisionlint.lintFile", "when": "editorTextFocus" },
  { "key": "ctrl+alt+k", "command": "decisionlint.askJev", "when": "editorTextFocus" }
]
CommandId
Lint the Questions in This Filedecisionlint.lintFile
Lint the Questions in the Workspacedecisionlint.lintWorkspace
Open Settingsdecisionlint.openSettings
Ask Jev About This Filedecisionlint.askJev
Ask Luna About This Filedecisionlint.askLuna
Ask via OpenRouter About This Filedecisionlint.askOpenRouter
Ask via Vercel AI Gateway About This Filedecisionlint.askVercel
Ask via LLM Gateway About This Filedecisionlint.askLlmGateway
Ask Liquid d1 About This Filedecisionlint.askLiquid
Ask the Model at My Endpoint About This Filedecisionlint.askEndpoint
Set TypeSafe API Keydecisionlint.setTypeSafeApiKey
Set OpenAI API Keydecisionlint.setOpenAIApiKey
Set OpenRouter API Keydecisionlint.setOpenRouterApiKey
Set Vercel AI Gateway API Keydecisionlint.setVercelApiKey
Set LLM Gateway API Keydecisionlint.setLlmGatewayApiKey
Set Liquid AI API Keydecisionlint.setLiquidApiKey
Set Endpoint API Keydecisionlint.setEndpointApiKey
Clear API Keysdecisionlint.clearApiKeys
Probe the Question at the Cursordecisionlint.probeQuestion

The Ask commands and the probe send requests with your key, so pick keys for them you will not press by accident.

Settings

SettingDefaultMeaning
decisionlint.rules{}Severity per rule, by code or name: off, hint, info, warning, error. An entry that names no rule or no level stops linting and says why, as it would in a decisionlint.json
decisionlint.fallbackOptionsother, none, unknown, unclear, uncertain, unsure, ambiguous, undetermined, neither, not stated, not applicable, insufficient evidence, cannot tell, n/a, and "other" in five other languagesWords that make an option a fallback for DEC004. An option counts when one of them is a word in its name, so none_implied and other_or_unclear count
decisionlint.ignore[]CODE:questionId entries to drop
decisionlint.target""The platform every request goes to, for its limits. Empty reads it from each model id
decisionlint.ask.modelsjev-1.13.0 for typesafe, each platform's own default for the restThe model id each Ask command sends, by platform. The TypeSafe default is the version the checks were calibrated on
decisionlint.ask.keysemptyAn API key per platform, and endpoint for a host you name, if you would rather type them here than use the keychain. User settings only, not synced
decisionlint.ask.endpoint""The URL Ask the Model at My Endpoint posts to, for a platform you host. User settings only
decisionlint.ask.endpointShape""The shape sent to that endpoint, typesafe or openai, where its path does not say
decisionlint.ask.endpointModel""The model id sent to that endpoint
decisionlint.ask.maxCalls25The most requests one Ask command may send
decisionlint.ask.sendStatefalseAlso send a state written in the file, so the model can check it
decisionlint.ask.confirmfalseBefore a paid command sends anything, show what it will send and cost, and wait for a yes
decisionlint.includeJSON, JavaScript, TypeScript, Python, Rust and Go filesFiles the workspace command lints
decisionlint.excludenode_modules and build outputFiles the workspace command skips
decisionlint.maxFileSizeBytes1000000Larger files are reported as skipped
decisionlint.notificationsLevelimportantHow much is said in notifications. all adds a summary after each command, and silent shows a message only when a command could not run. Findings always show in the editor

Privacy & security

  • Linting never uses the network. The extension, the command line and the MCP server lint with no key and send nothing. No telemetry is collected.
  • Only the Ask commands, the probe, --ask and the two MCP tools that send reach a platform, with your key, and only when you run them. They send questions, not your state, unless you turn on decisionlint.ask.sendState or --ask-send-state. The probe sends the state written beside the question.
  • Keys stay out of the repository. The Set ... API Key commands store a key in your operating system keychain. decisionlint.ask.keys and decisionlint.ask.endpoint can only be set in your user settings, which are left out of Settings Sync, so a repository cannot point your key at a host of its choosing. The command line reads a key from its environment variable only and never prints it.
  • Untrusted workspaces cannot send. The Ask commands are disabled there, and a project's own copy of the linter is not loaded, because loading it runs code from the workspace.
  • The MCP server writes nothing, and reads a key from the environment the client started it with, or .env.local or .env in its directory, never from a tool argument, so no key lands in an agent's transcript.

Documentation

WhatWhere
What the product is, and what is plannedSPEC.md
How it is built and held together: architecture, invariants, releaseAGENTS.md
Every rule, with an example that is flagged and one that is notdocs/rules
Each cutoff, and what it caught on each model and platformdocs/cutoffs.md
What changedCHANGELOG.md
The tool's page, and the rest of the familyletools.dev/tools/decisionlint

Development

bun install
bun run typecheck && bun run lint && bun run test
bun run build
bun run test:integration   # real VS Code against samples/
bun run package            # release/decisionlint-<version>.vsix
bun run test:e2e-vsix      # the packaged VSIX in a clean profile

AGENTS.md is the engineering standard. SPEC.md is what the product is and what is planned. samples/ is a workspace with planted mistakes, used by bun run test:integration, which drives a real VS Code.

More from the LE family

Sixteen single-purpose tools for the work in front of every model. Each ships a Rust CLI and an MCP server. One page: letools.dev

Get it out

  • String-LE — Extract every string in a codebase, so a person can read them
  • Numbers-LE — Extract every hardcoded number in a codebase, so a person can check them
  • Units-LE — Extract every quantity with its unit, normalized, and refuse the ambiguous ones by name
  • Dates-LE — Extract every date and timestamp, and the exact instant each one resolves to
  • IDs-LE — Extract every UUID, ULID, NanoID, ObjectId and Snowflake, and decode the time inside
  • IPs-LE — Extract every IP address, CIDR block and MAC, normalized and classified by scope
  • URLs-LE — Extract every URL in a codebase, with its protocol and exact position
  • Paths-LE — Extract every file path in a codebase, and say whether it still points at anything
  • Colors-LE — Extract every color in a codebase, and say which ones are not in your palette

Check it

  • Regex-LE — Find every regex in a codebase, and report which can be driven into catastrophic backtracking
  • Versions-LE — Find where one dependency is constrained differently across a repository's manifests
  • i18n-LE — Identify the i18n library a project uses, then audit its catalogs by that library's rules
  • Scrape-LE — Check whether a page is scrapeable before the scraper is written, and say when it cannot tell

Guard it

  • Secrets-LE — Find hardcoded credentials in a codebase, and never print one into the report
  • EnvSync-LE — Compare the dotenv files in a tree, and say which keys are missing from which
  • Unicode-LE — Find the Unicode that hides meaning — bidi controls, invisibles, homoglyphs, mixed scripts

Each stands on its own: no shared crate, no published core. Where two of them agree, it is because the same answer was right twice.

Contact — nolindnaidoo.com · GitHub · LinkedIn

Also by nolindnaidoo

Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. Their own tools, their own voice — not part of the LE family.

License

MIT © nolindnaidoo

Questions fréquentes

Qu'est-ce que DecisionLint: Jev/Luna Lint ?

Lint Jev and Luna decision-model questions, or ask the model to check them with your key.

Comment installer DecisionLint: Jev/Luna Lint ?

Il s'exécute sur votre machine. Dans Claude Code : claude mcp add decisionlint-jev-luna-lint -- npx -y decisionlint-mcp@1.1.0. Pour les autres clients, collez la configuration JSON de cette page dans leurs réglages MCP.

Faut-il une clé API pour DecisionLint: Jev/Luna Lint ?

Son entrée au registre n'indique aucune clé ni secret obligatoire. Certaines fonctions peuvent tout de même demander un compte auprès du service auquel il se connecte.

DecisionLint: Jev/Luna Lint est-il open source ?

Oui. Son code est sur GitHub sous licence MIT.

Qui maintient DecisionLint: Jev/Luna Lint ?

Il est publié par nolindnaidoo. AI.tn le référence sans le maintenir ni le recommander.

Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.