wellposed
September 18, 2026 · View on GitHub
Lint your jev requests before they come back confidently wrong.
question "route_team"
warn Choice "route_team" has no "other"/"none of the above" option. If an input fits none of
[billing, technical, account], jev must still pick one — measured at confidence 1.00 on a
wrong answer, so confidence gating will not catch it.
fix: Add e.g. {"other": "A case that fits none of the above"}
question "agent_name"
error Question "agent_name" references `ticket.assigned_agent.name`, but state has nothing at
"ticket.assigned_agent".
What jev is
TypeSafe's jev is a model that doesn't write text. You hand it some data and a question, and it hands back a typed answer your code can use directly — a true/false probability, a pick from a list, or a rating. Three question types:
- Noul — a yes/no question. Returns "how likely is this yes," a number from 0 to 1.
- Choice — pick one option from a list you provide.
- Score — rate something on levels you define, like
can wait→needs attention today.
You send it state (the stuff to look at) and questions (what to judge). It sends back answers.
No prompt engineering, no parsing JSON out of prose.
The problem
jev is strict about how you write a request and completely relaxed about what you ask.
Send it malformed JSON and it rejects you immediately with a precise error — wrong field type, missing key, it tells you exactly where. That part is handled.
But ask it a badly designed question and it just... answers. Confidently. No warning.
We measured this. We generated 40 jev requests the way a normal developer would, then checked them:
- 0 out of 40 had syntax errors — the API's own validation already covers that completely
- 16 out of 40 (40%) asked something that didn't make sense
- 0 out of 11 list-questions included a "none of the above" option
Why that last one is the dangerous one
Say you write a Choice question: "What is this email asking for?" with four options — meeting request, pricing question, support issue, introduction.
Then an email arrives saying "Please remove me from this mailing list. I never signed up."
That's none of the four. But you didn't give jev a way to say "none of these," so it has to pick one. Here is what actually happened when we ran it:
without a "none of the above" option -> "support issue" confidence 1.00
with a "none of the above" option -> "other" confidence 0.93
Confidence 1.00. Maximum. The model is as certain as it can possibly be, and it is wrong.
This matters because the standard advice for handling AI uncertainty is "check the confidence score, and send the low-confidence ones to a human." That advice cannot catch this bug. The confidence is perfect. Your monitoring sees a healthy, decisive answer. The wrong label flows into your database.
That is the whole reason wellposed exists: some bad questions produce answers that look perfect. You can't catch those by inspecting the answer. You have to catch them by inspecting the question, before you send it.
It's a paper form with no "Other" box. If a survey asks which of four departments your complaint is about and yours is about none of them, you still tick a box. The form gets filed, looks complete, and is wrong. Nothing downstream can tell.
The contrast that shapes the whole design: when we gave a Choice two overlapping options (angry and
furious), confidence collapsed to 0.19. That failure is loud — ordinary confidence checks catch
it fine. So wellposed doesn't spend effort there. It focuses on the failures that stay silent.
What wellposed does about it
Four parts, cheapest first.
1. It teaches your coding agent to write good requests
A SKILL.md that loads into Claude Code or Codex. When you say "add jev
to classify these tickets," the agent already knows:
- Structure your data with names. Not one blob of text —
{"ticket": {...}, "order": {...}, "policy": "..."}. Then point at the pieces from your question using backticks:Does `ticket.messages[0].text` request a refund?(This is TypeSafe's own convention; their playground uses it.) - Pick the right question type. "How urgent is this?" is a rating, so it needs a Score. Ask it as
a Noul and you get back
0.73, which means "73% likely the answer is yes" to a question that has no yes. - One question, one judgment. "Is this urgent AND about billing?" returns a single number that can't tell you which half was true. Split it — it costs nothing, because jev answers all your questions in one round trip.
- Don't ask jev to do math. It can't count reliably, can't compare dates, can't add. Extract the values with it, then do the arithmetic in your own code.
- Batch everything. jev reads your data once and answers all questions at the same time. One request with ten questions is documented as ~12x cheaper and ~10x faster than ten requests.
2. A linter that reads your request and finds problems — free, instantly, offline
No API key, no network, no waiting. It reads the JSON and checks 35 rules.
Things that are definitely broken (these fail the check):
- no instructions on a question
- wrong data type where text was expected
- a Noul's criteria given as a list instead of an object
- a Choice with duplicate options, or more than the 255 allowed
- a Score with fewer than 2 levels
- your data is too big — over 64,000 tokens total, or 32,000 for the data plus your longest question
- a broken reference — your question mentions
`ticket.assigned_agent.name`but there is nothing at that path in your data. This one is provably wrong: no opinion required, just walking the JSON.
Things that are probably wrong (warnings):
- a Choice with no "none of the above" option ← the big one
- a rating question asked as a yes/no question
- asking jev to count, do arithmetic, or compare dates
- two judgments crammed into one question
- double negatives, which measurably confuse it
Suggestions (just advice):
- Score levels that are single vague words like "weak / okay / strong" instead of real situations
- only one question in the request — you're leaving the batching discount on the table
- data you're sending that no question ever mentions — dead weight, and extra irrelevant data measurably makes jev less accurate
3. jev checking jev — for the things code can't decide
Some questions genuinely require judgment. Code can see that your Choice has no "none of the above" option, but it can't know whether "none of these" is even possible for your data. That's a question about meaning.
So for exactly those cases, wellposed asks jev about your question. Seven checks, each a simple yes/no:
- Is this really just one judgment, or several stuffed together?
- Can this be answered from the data provided, or does it need outside knowledge?
- Could two of these options both be true at once?
- Is there a realistic input none of these options would cover?
- Is this a rating question disguised as a yes/no?
- Are these rating levels actually in order?
- Do the criteria contradict the instructions?
This costs one API call per question and only runs when you ask for it (--semantic).
The two layers are deliberately different. The free one is aggressive — it flags every Choice missing an escape hatch, including ones that are fine, because it can't know. The paid one is careful — it only gets asked the questions that genuinely need thought. Cheap-and-noisy filters first; expensive-and-accurate resolves the leftovers.
4. A test set that proves the linter works
This is the part most tools skip. wellposed ships the 76 questions, hand-labelled, plus the grading rubric written before anything was generated, so the goalposts couldn't move afterward.
Run wellposed eval and it scores itself:
hand label n caught
----------------------------------------------
no-escape-hatch 9 9/9 fully covered
degree-as-noul 6 6/6 fully covered
jev-date-comparison 2 2/2 fully covered
jev-counting / arithmetic 2 2/2 fully covered
jev-double-negative 1 1/1 fully covered
bundled-judgments 3 2/3 partial
overlapping-choice-options 1 0/1 deferred to the semantic layer
unanswerable-from-state 2 0/2 deferred to the semantic layer
recall 22/26 = 85% precision 22/24 = 92%
In plain terms: it catches 85% of the known problems, and 92% of what it flags is genuinely a problem. Those numbers are computed live, not typed into this README. If a future change makes the linter worse, the number drops and you see it.
Plus 30 unit tests. Three exist specifically because we sent those exact broken requests to the real API and recorded what it said.
Install
Claude Code
claude plugin marketplace add suraj-phanindra/wellposed
claude plugin install wellposed@wellposed
Then /wellposed:wellposed, or just describe what you're building — the skill self-triggers on jev work.
Codex, or any agent
npx skills add suraj-phanindra/wellposed --skill wellposed -a claude-code -a codex
Copies the skill to ~/.agents/skills/wellposed and links each agent's directory at it. In Codex,
invoke with $wellposed.
By hand
Everything lives in one directory. Copy skills/wellposed/ anywhere your agent
reads skills from, or paste
SKILL.md
into the conversation.
Just the CLI, no agent
npx wellposed lint request.json # nothing to install
npm i -g wellposed # or put it on PATH
Node ≥ 18, zero dependencies. No API key needed for the structural checks.
Use
npx wellposed lint request.json # structural: free, offline, no API key
npx wellposed lint request.json --semantic # + jev-on-jev checks (needs TYPESAFE_API_KEY)
npx wellposed lint - < request.json # reads stdin
npx wellposed rules # every rule and where it comes from
npx wellposed eval # score the linter against the labelled corpus
Already installed as a skill and want to run it offline? The CLI ships inside the skill directory -
find ~/.claude/plugins ~/.agents/skills ~/.codex/skills -name wellposed.mjs -path '*wellposed/scripts/*' | head -1
and call node <that path> instead.
Exit code is 1 when errors are found, so it drops into CI unchanged. --json for machine output,
--max-warnings <n> to fail on warnings too.
Disagree with a rule? Turn it off. If your option list really is exhaustive, that warning is noise and you should silence it:
{ "rules": { "choice/no-escape-hatch": "off" } }
npx wellposed lint request.json --config wellposed.config.json
A working example ships at
examples/wellposed.config.json.
Where everything lives
| Layer | File | Cost |
|---|---|---|
| 1. Adapter | SKILL.md | — |
| 2. Structural | structural.mjs | free, offline |
| 3. Semantic | semantic.mjs | 1 call/question |
| 4. Eval | corpus.json + agreement.mjs | free, offline |
Honest limits
- The corpus was generated and labelled by one model. Self-grading biases the defect rate downward, so 40% is a floor, not a point estimate. The live behavioural probes exist precisely because they don't depend on that judgment.
- The first version of these rules was much worse than its own metric said. An adversarial audit
on 2026-09-18 found six rules firing on ordinary business English — "did not receive the invoice"
tripped the double-negative rule on the letters in "invoice", and a backticked option name was
reported as a broken state path at
errorseverity, failing requests the API answers at confidence 1.00. None of those phrasings were in the 40-item corpus, so the reported precision was unchanged before and after the fix: the metric could not see them. The corpus now carries 36 adversarial items (source: adversarial-2026-09-18incorpus.json) specifically so it can. - The semantic layer ranks; it does not cleanly separate. Clear defects scored 0.94–0.96, contested ones 0.60–0.74. Trust the high end, review the middle. Thresholds are tunable for a reason.
choice/no-escape-hatchover-flags by design. Whether "none of these" is reachable is a question about meaning. If your option set really is exhaustive, turn the rule off.- One label was corrected after the fact, and the correction is recorded in
corpus.jsonundercorrectionsrather than quietly applied. - Rules cite their source.
verifiedmeans reproduced against the live API; everything else cites a docs page.wellposed rulesshows which is which. - Codex behaviour was verified against Codex as of 2026-09-17. Older builds may need
codex --enable skills.
Not affiliated with TypeSafe
This is an independent tool. TypeSafe, System One and jev are theirs; the rules here are drawn from their public documentation, which is the authority whenever it and this tool disagree — in particular jev-1.13 jaggedness, the most useful page in the docs and the one most often skipped.
Development
npm test # 30 unit tests, zero dependencies
npm run eval # score the linter against the corpus
npm run lint:example # lint the bundled example
MIT.