How what's true? works
what's true? keeps a product's public docs true, and keeps agents from making promises. It has no built-in AI. People and their own agents write the docs. what's true? decides what needs writing, checks it, and records who approved it.
It checks docs for drift and risky specifics. It is not legal review.
What it does
- Each doc says what it covers: code paths, features (pull request labels), routes and facts.
- A merged pull request that touches what a doc covers marks the doc stale. Someone then updates the doc, or says that nothing needs to change.
- The guard checks public text before it ships: personal names, internal terms, retired products, vendors you have not decided to name, and on legal pages the specifics that become promises.
- A required GitHub status holds the merge until a person approves that exact commit. Agents and tokens never can.
Your docs stay in your repository, and your own site publishes them. what's true? reads them. It does not host a help centre, crawl your site or give legal advice, and the alpha has no billing.
Set up a product
- Sign in with KRNL, and make a workspace. Making one means accepting the terms and the privacy policy, and passing a short check that you are a person. When public sign-up is closed, it also needs an invitation.
- Add a product. No two products in what's true? share an address.
- In the product's settings, install the what's true? GitHub App on the repository. The App reads the repository's contents, pull requests and metadata, and writes commit statuses.
- Choose how the docs are read: Markdown files in the repository, or a manifest built in CI.
- Require the
what's true?status on the default branch, with a ruleset. Classic branch protection works too. The product shows Enforced once GitHub requires it, and Can't check if GitHub does not show it the branch's rules: switch to a ruleset. If GitHub does not let you require a status on that repository, the status still shows on each pull request, but it does not hold the merge.
The product's first report lists what is wrong in its published docs today: guard findings, coverage that matches nothing or everything, and docs with no review date.
How a repo describes its docs
There are two ways. A product uses one of them, set in its settings.
Markdown with front matter
what's true? reads Markdown files straight from the repository, through the GitHub App, at each commit it needs. There is no build step. whatstrue/config.yaml says where the docs are and what the guard should know:
product: your-product # the product's address in what's true?
docs: ["docs/**/*.md"] # up to 20 globs
facts: whatstrue/facts.yaml
guard:
people: [] # extra names; workspace members are added on the server
internal: [YourInternalName]
publicVendors: [GitHub]
review: { support: P90D, legal: P365D }
skipLabels: [not-user-facing]Without the file, the globs come from the product's settings, docs/**/*.md by default. Each doc starts with front matter:
---
id: help/editing
title: Editing
tier: support # legal, support, changelog or internal
status: published # draft, published or retired
url: https://example.com/help/editing
covers:
paths: ["app/editor/**"]
features: [editor]
routes: [/editor]
facts: [plans.pro.name]
reviewed: 2026-10-01
---
# Editing
Open a document and type.iddefaults to the file's path withoutdocs/and its extension, andtitleto the first heading.- A doc with no
tieris read by its path: one that looks legal is read as legal, and the rest mostly as support. Set it. statusispublishedunless it says otherwise. Only published docs become stale or appear in the product's report, and a change to a draft that stays a draft needs no approval.covers.pathsare globs of files in the repository.covers.featuresare pull request labels.covers.routesare recorded with the doc and shown with it.reviewedandeffectiveare dates,YYYY-MM-DD.
A manifest built in CI
A repo whose docs live anywhere else, or are built, writes whatstrue/manifest.json from its own build and uploads it from CI for each commit, with whatstrue push or the Action. It follows the v1 schema at /schema/manifest-v1.json:
{
"version": 1,
"commit": "the full commit SHA it was built from",
"docs": [{ "id": "help/editing", "title": "Editing", "tier": "support", "text": "The text exactly as published.",
"url": "https://example.com/help/editing", "covers": { "paths": ["app/editor/**"] } }],
"facts": [{ "key": "plans.pro.name", "value": "Pro" }]
}Each doc needs id, title, tier and text. The other fields are the same as in front matter. whatstrue build writes a manifest from Markdown docs, so a repo can move to manifest mode without rewriting them. A product in manifest mode reads only manifests: a pull request's review waits for the manifest of its head commit, and the status says so.
Tiers and facts
| Tier | Who approves a change | Guard preset |
|---|---|---|
| legal | A person with the approve legal permission | strict |
| support | A person with the approve support permission | standard |
| changelog | Nobody here: what changed? approves release notes | standard, without hard-coded facts |
| internal | Nobody | exposure only |
A pull request that changes docs needs approval for each tier it touches. A doc that moves between tiers needs both. A pull request that changes no legal or support doc passes.
Members have a level: viewer, editor, admin or owner. Approve permissions are separate, and are given to editors and above, never to tokens. Whoever makes a workspace starts with both.
Facts are the values docs depend on, such as a plan's name or a support address. In Markdown mode they live in whatstrue/facts.yaml:
plans.pro.name: Pro
plans.team.name: { value: Team, status: retired, retiredOn: 2026-10-01 }
support.address: { value: help@example.com, visibility: public }A fact has a status, active or retired, and a visibility: public, checkout (shown only where people buy) or internal. A doc uses a fact by writing {{fact:<key>}} in its text, with the fact's key in place of <key>, or by listing the key under facts. When a fact a doc uses changes, the doc is stale.
Stale records
A doc becomes stale for one of four reasons:
- a merged pull request changed a file its
covers.pathsmatch; - a merged pull request had a label its
covers.featuresname; - a fact it uses changed, or was retired or removed;
- it passed its review date:
reviewedplus the period for its tier,P90Dfor support andP365Dfor legal unless the product says otherwise.
A doc has one open record at a time, and new reasons are added to it. Pull requests with a skip label, not-user-facing by default, are recorded but mark nothing stale, and so are pull requests from bots unless the product's settings say otherwise. Edits to a doc's own file never make it stale. Changelog docs are never stale here.
A stale support doc is fixed by a pull request that changes it and is approved here, which clears the record when it merges. A person with approve support can also mark it "No change needed", or dismiss it. A stale legal doc asks a question: does the change affect anything you promise? Only a person with approve legal answers it. "No" clears the record. "Yes" keeps it open until the page is changed and approved.
Legal versions
Every change to a published legal doc that merges is kept as a version, with its effective date and who approved it. Approving a legal change can mark it material, which opens a "Tell your users" task. what's true? sends nothing to your users: the task records that you did. You can also record the day counsel reviewed a version.
The guard
The guard is one function. The service, the CLI, the Action and what changed? all run it, so a doc gets the same findings wherever it is checked. A block finding holds the merge. A warning is for a person to look at.
| Rule | What it finds | Legal | Support | Changelog | Internal |
|---|---|---|---|---|---|
people | Names of people: workspace members, and the names the product and the repo list | block | block | block | |
internal | Internal terms the product and the repo list, and an agent presented as staff. Words that sound like an internal system are warnings. | block | block | block | |
retired | The value of a retired fact | block | block | block | |
internal-facts | The value of an internal fact | block | block | block | |
vendors | A known service that is not on the product's public vendor list | block | warning | warning | |
legal-specifics | Time periods, amounts of money, rates, limits, promises of speed or with no limit, named cookies, plan names and checkout facts | block | |||
promises | Words that promise an outcome, and a time something will happen in | warning | warning | ||
hardcoded-facts | A fact's value written out, in a support doc that does not use the fact | warning | |||
exposure | An internal doc with a public URL | block |
Facts' values are checked as they are published: a fact reference is read as its value, and HTML comments are left out.
Exceptions. When a doc must say something a rule finds, write the exception in the doc, with the reason. In front matter:
allow:
- rule: legal-specifics
match: "30 days"
reason: Required by the consumer law where we sell.The same can be written as an HTML comment in the text that starts with whatstrue-allow, then the rule, the text in quotes and reason:. A new exception is listed in the pull request's review, with its reason, so the person who approves the change sees it.
Approval
Each pull request to the default branch gets the what's true? status. Its review shows the docs the pull request adds, changes or removes, with their diffs and findings.
- Pending while the docs it changes need approval.
- Failure while a block finding has no exception, while a file it touches cannot be read as a doc, or when a person requests changes. A pull request with block findings cannot be approved until they are fixed.
- Success once a person with the right permission approves, or when no doc in it needs approval.
An approval is for one commit. A new push is a new review, and it needs approving again: approval never carries over. Only a person signed in to what's true? in their browser, in the last 12 hours, with the approve permission for each tier involved, can approve. A token, an agent, MCP and KRNL OS never can.
The CLI
The CLI is the npm package whatstrue, for Node 20 or later. It is not on npm yet.
whatstrue check # every doc the config's globs find
whatstrue check docs/legal/terms.md # named files
echo "Open a document and type." | whatstrue check --stdin --tier support
whatstrue build # writes whatstrue/manifest.json from Markdown docs
WHATSTRUE_TOKEN=wt_... whatstrue push # uploads the manifest for this commit| Command | Options |
|---|---|
check [files...] | --tier <tier> treats the files, or standard input, as that tier. --stdin checks text from standard input and needs --tier. --manifest [path] checks the docs in a built manifest. --json prints the result as data. |
build | --out <path>, whatstrue/manifest.json by default. --commit <sha>, by default HEAD, or a pull request's head commit in GitHub Actions. |
push | Needs WHATSTRUE_TOKEN, a product token with the write scope. --manifest <path>, --commit <sha>, --ref <branch>, and --origin <url> or WHATSTRUE_ORIGIN, https://true.krnl.studio by default. The token is only sent over https. |
Every command takes --config <path> and --cwd <dir>. check exits 0 when nothing blocks, 1 when something does, and 2 when the command, the config or the docs cannot be read. A push that fails exits 1. Files outside the config's globs are skipped, so an agent's hook can run whatstrue check on every file it edits and see findings before it finishes.
The GitHub Action
The Action runs the CLI's steps in a workflow, on pull requests and on the default branch. It is not on GitHub Marketplace yet. It builds the manifest from the Markdown docs if your build did not write one, pushes it, and runs the guard, with each finding on its line in the pull request and a summary on the run. The job fails on block findings.
Check out the pull request's own commit, not the merge commit GitHub makes: give actions/checkout the ref ${{ github.event.pull_request.head.sha || github.sha }}. In Markdown mode, what's true? reads the docs from the repository itself, so leave out token and the Action only checks.
| Input | Default | |
|---|---|---|
token | A product token with the write scope. Without one, nothing is pushed. | |
config | whatstrue/config.yaml | |
manifest | whatstrue/manifest.json | Your build's manifest. If it is not there, it is built from the Markdown docs before pushing. |
check | true | Run the guard, and fail on block findings. |
push | auto | Push when a token is set. |
origin | https://true.krnl.studio | |
working-directory | . | Where the repository is checked out. |
Its outputs are blocking, warnings and commit.
Tokens and their scopes
A product token lets CI or an agent act for one product. An admin makes it in the product's settings. It looks like wt_ and 64 hexadecimal characters, and it is shown once: what's true? keeps only its SHA-256 hash.
| Scope | What it may do |
|---|---|
read | Every token has it. It acts as a viewer: the workspace's own entry for its product, docs, facts, reports, stale records, reviews, legal versions, the audit log, and running the guard. |
write | It also acts as an editor: uploading manifests, and adding information to support-tier stale records. |
A token reaches only its own product. It never runs a command that is for members only or for a signed-in person, whatever its scopes. A product holds 25 live tokens. Revoking one in the product's settings stops it at once.
The API and MCP
Every caller uses one set of commands: the browser, the CLI, MCP and KRNL OS. Send {command, args} to /api/command:
curl https://true.krnl.studio/api/command \
-H "Authorization: Bearer $WHATSTRUE_TOKEN" -H "Content-Type: application/json" \
-d '{"command":"docs.check","args":{"text":"Open a document and type.","tier":"support"}}'The answer is {"ok":true,"data":...}, or {"ok":false,"error":{"code":"...","message":"..."}} with a code such as unauthorized, forbidden, not_found, invalid_arguments, limit or rate_limited. A token's commands are for its own product. A person names the workspaceId and productId in args. What you may do comes from who you are, never from the arguments.
| Command | Least level | Who may run it |
|---|---|---|
workspace.get | viewer | Members, or a token with read |
workspace.create | viewer | A signed-in person only |
workspace.update | admin | Members, no token |
workspace.delete | owner | A signed-in person only |
workspace.export | admin | Members, no token |
member.invite | admin | Members, no token |
member.update | admin | Members, no token |
member.remove | admin | Members, no token |
product.create | admin | Members, no token |
product.update | admin | Members, no token |
product.delete | admin | A signed-in person only |
github.connect | admin | Members, no token |
github.status | viewer | Members, or a token with read |
token.create | admin | Members, no token |
token.revoke | admin | Members, no token |
manifest.push | editor | Members, or a token with write |
docs.list | viewer | Members, or a token with read |
docs.get | viewer | Members, or a token with read |
docs.check | viewer | Members, or a token with read |
docs.report | viewer | Members, or a token with read |
facts.list | viewer | Members, or a token with read |
facts.usage | viewer | Members, or a token with read |
stale.list | viewer | Members, or a token with read |
stale.get | viewer | Members, or a token with read |
stale.note | editor | Members, or a token with write (support-tier records only) |
stale.answer | editor | A signed-in person only |
stale.dismiss | editor | A signed-in person only |
review.list | viewer | Members, or a token with read |
review.get | viewer | Members, or a token with read |
review.decide | editor | A signed-in person only |
legal.versions | viewer | Members, or a token with read |
legal.counsel | editor | A signed-in person only |
legal.notice | editor | A signed-in person only |
audit.list | viewer | Members, or a token with read |
Only a signed-in person
Eight commands need a person signed in to what's true? in their browser. No token, MCP tool or KRNL OS call can run them, whatever its scopes:
workspace.createandworkspace.delete,product.delete;- the approvals:
review.decide,stale.answer,stale.dismiss,legal.counselandlegal.notice.
The approvals also need a sign-in in the last 12 hours, and the approve permission for the tier: approve legal for legal docs and legal versions, approve support for support docs.
MCP
/api/mcp speaks MCP over HTTP (JSON-RPC 2.0), with a product token as Authorization: Bearer wt_.... It answers initialize, tools/list and tools/call. Its tools are the commands a token may run, named with underscores: workspace_get, github_status, manifest_push, docs_list, docs_get, docs_check, docs_report, facts_list, facts_usage, stale_list, stale_get, stale_note, review_list, review_get, legal_versions and audit_list. No tool approves, answers or dismisses. An agent runs docs_check on text before it writes it.
Public endpoints
GET /api/public/<product>/legal/<doc>/versionslists a published legal doc's versions, for your own site's "Last updated" line and its past versions. Anyone may read it, and it says nothing about who approved what.GET /schema/manifest-v1.jsonis the manifest's JSON Schema.
KRNL OS and what changed?
KRNL OS. You sign in to what's true? with your KRNL account. In a KRNL OS project, Enable docs makes a product in a workspace you own, or links one you have, and gives KRNL OS a product token for it with the read and write scopes. It acts as you, so your level applies, and it can never approve. Someone who has no workspace yet is sent to make one here first. Unlink in KRNL OS revokes that token.
For a linked product, each stale record becomes a KRNL OS issue in that project:
- A support doc's issue says what changed and how to fix it, and an agent can claim it. Its pull request has the issue's key in its title, and a person with approve support approves it here. When it merges, the record clears and the issue closes.
- A legal doc's issue is a question. It goes to the people with approve legal, never to an agent, and they answer it here.
Clearing or dismissing a record closes its issue. Approvals happen only in what's true?.
what changed? A what changed? product can be linked to a what's true? product with a read token, in what changed?'s settings. Before a release is approved there, what changed? runs the guard on the release text at the changelog tier, with the exceptions recorded on the release, and lists the docs still stale for the pull requests in it. What changed? holds approval while anything blocks, or while what's true? cannot be reached. Changelog docs are approved in what changed?, not here.
Limits
The alpha has these limits. A refusal says which limit it hit and what to do.
| Limit | |
|---|---|
| Workspaces each account makes | 3 |
| Products in a workspace | 10 |
| Members and invitations in a workspace | 20 |
| Docs in a product | 2,000 |
| Manifest size | 5 MB |
| Facts in a manifest | 200 |
| Manifest pushes per product | 60 an hour |
| API requests per token | 600 a minute |
| Live tokens per product | 25 |
| Doc globs in the config | 20 |
| Notes on a stale record | 50 |
Reviewed 2026-10-30.