# Talooner > Talooner is a deterministic PR guard for GitHub and GitLab. Your repository declares its policy as tln rules; rules decide every verdict, and a model is consulted only where a rule asks. Talooner is the first line of defense against AI slop: a deterministic guard that reads every pull request against the policy declared in the repository (`.talooner/rules.tln`, written in tln — https://tln-lang.org/) and approves, blocks, or routes it to the right person or team. Same commit, same rules, same verdict. It runs self-hosted on the agent orchestration platform you already use — OpenClaw, Hermes Agent, OpenTalon and others. Talooner doesn't store any of your data because it runs on infrastructure you self-host: there is no hosted tier, the only call home is a daily license check (key, nonce, version — never code or PR data), the CI runner is a container that keeps nothing, and facts, review history and LLM credentials live only on your own servers. ## Example rules ### Secrets & tokens — require Anything touching SSO config, access tokens or credentials is routed to security — automatically. ```tln define "touches_auth_secrets" { attr "pr.changed_files" contains "sso.rb" or attr "pr.changed_files" contains "access_token" or attr "pr.changed_files" contains "config/credentials" or attr "pr.changed_files" contains ".env" } rule "Auth secrets & tokens need security" { for records where type == "pr" and is "touches_auth_secrets" requires "review.security_team" do require "review.security_team" do assign "pr" "@acme/security" do comment "pr" "Touches SSO/token/secret config — security review required." priority HIGH } ``` On the PR: Touches SSO/token/secret config — security review required. ### Authorization — require An agent quietly dropping an authorize! call is exactly the slop a tired reviewer misses. ```tln define "touches_authz" { attr "pr.changed_files" contains "app/policies/" or attr "pr.changed_files" contains "app/services/permission/" } rule "Independent review when author owns sensitive code" { for records where type == "pr" and is "touches_authz" and attr "user.owner" == attr "pr.author" requires "review.senior_engineer" do require "review.senior_engineer" do comment "pr" "Author {attr.pr.author} owns this code — needs an independent reviewer." priority CRITICAL } ``` On the PR: Author @dev-agent owns this code — needs an independent reviewer. ### Supply chain — require Hallucinated or freshly added packages get a license and supply-chain pass before they land. ```tln rule "New dependencies get a security pass" { for records where type == "pr" and attr "pr.new_dependencies" > 0 requires "review.security_team" do require "review.security_team" do comment "pr" "{attr.pr.new_dependencies} new dependency(ies) — security review (license + supply chain)." priority HIGH } ``` On the PR: 2 new dependency(ies) — security review (license + supply chain). ### Doc drift (LLM) — block The one place a model enters — because a rule asked. Its typed answer re-enters as a fact; the rule decides. ```tln enrich "Check changed code against its guide" { for records where type == "code_unit" and attr "unit.important" == true stale_after 1 hour tool "llm" "review" { doc attr "unit.doc_content" diff attr "unit.diff" } update attr "unit.llm_result" from result.verdict } strict rule "Block documented mismatch" { for records where type == "code_unit" and attr "unit.llm_result" == "mismatch" block "merge" do block "pr.merge" do comment "pr" "{attr.unit.name} diverges from its guide: {attr.unit.llm_explanation}" priority CRITICAL } ``` On the PR: permission/alteration_service.rb diverges from its guide: grants admin without an entity scope check. ### Migrations — require Schema changes go to the DBA with a checklist, not to whoever happens to be online. ```tln rule "Migrations need a DBA" { for records where type == "pr" and attr "pr.changed_files" contains "db/migrate/" requires "review.dba" do require "review.dba" do assign "pr" "@acme/platform-db" do comment "pr" "DB migration — confirm reversible, no remove_column on large tables, backfills in a job." priority HIGH } ``` On the PR: DB migration — confirm reversible, no remove_column on large tables, backfills in a job. ### Auto-approve — approve The payoff: small, green, documented changes off every sensitive path merge with no human in the loop. ```tln rule "Auto-approve safe, tested, documented changes" { for records where type == "pr" and attr "pr.lines_changed" < 50 and attr "pr.has_description" == true and attr "pr.tests_passing" == true and attr "pr.lint_passing" == true and not is "touches_authz" and not is "touches_auth_secrets" and not attr "unit.llm_result" == "mismatch" allow "merge" do approve "pr" do comment "pr" "Small, green, documented, and off every sensitive path — no objections." } ``` On the PR: Small, green, documented, and off every sensitive path — no objections. ### Tested — block Policy is code, so it ships with tests. tln test fails CI the moment a rule drifts. ```tln test "a documented mismatch blocks the merge" { given { record 2 type "code_unit" attr 2 "unit.name" "app/services/permission/alteration_service.rb" attr 2 "unit.llm_result" "mismatch" attr 2 "unit.llm_explanation" "grants admin without an entity scope check" } when rule "Block documented mismatch" expect { flagged 2 did 2 block "pr.merge" did 2 comment "pr" contains "diverges from its guide" } } ``` On the PR: ✓ a documented mismatch blocks the merge (3 assertions) --- # As a pipeline step > One Docker image, one step in your pipeline. No server to host — the job starts Talooner inside its own container, reviews the merge request and exits. Talooner ships as **one image** — `talooner-runner` — that you add as a step to the pipeline you already have. The job starts OpenTalon and the Talooner plugin inside its own container, checks your license, reviews the merge request or pull request against your repository's `.talooner/rules.tln`, posts a check run and one comment, and exits. ``` ghcr.io/talooner/talooner-runner:latest ``` The image holds binaries only — no source, no compiler — on a minimal, non-root base. ## What you need 1. **A license key** — from your license page (or [get a free demo license](https://talooner.dev/#pilot)). 2. **A forge token** so Talooner can post its review: GitLab — a project or group access token, *Developer* role, `api` scope; GitHub Actions — the built-in `github.token`. 3. **Optional: any LLM** for AI review — see [Any LLM](#any-llm). Without one, Talooner reviews with rules only. 4. **Optional: a context decision model** (`CDM_URL` + `CDM_TOKEN`) — [Jev](https://typesafe.ai/) from TypeSafe or [SubQ Snap](https://subq.ai/introducing-subq-snap) from Subquadratic as a fast first pass: each changed unit gets a typed *match / mismatch / unclear* with a confidence score, and only what the model can't confirm goes to the full LLM review. ## Any LLM Talooner works with any model behind an OpenAI-compatible API — hosted or on your own servers. Set three variables: | | `TALOONER_MODEL_BASE_URL` | `TALOONER_MODEL` | |---|---|---| | Qwen (Alibaba Cloud) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3-coder-plus` | | DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` | | OpenRouter | `https://openrouter.ai/api/v1` | any model it lists | | Ollama / vLLM on your servers | `http://llm.internal:11434/v1` | e.g. `qwen3-coder` | …plus `TALOONER_MODEL_API_KEY` (any non-empty value for a local server). Anthropic and OpenAI have shortcuts: `ANTHROPIC_API_KEY`, or `OPENAI_API_KEY` with `TALOONER_MODEL`. The snippets below use the three variables; swap in a shortcut if you prefer. ## GitLab CI Works on gitlab.com and self-managed GitLab. Add the job to `.gitlab-ci.yml`, then add `TALOONER_LICENSE_KEY` and `GITLAB_TOKEN` (and optionally your [LLM](#any-llm): `TALOONER_MODEL_BASE_URL`, `TALOONER_MODEL`, `TALOONER_MODEL_API_KEY`) under **Settings → CI/CD → Variables**, masked. ```yaml talooner: image: name: ghcr.io/talooner/talooner-runner:latest entrypoint: [""] stage: test needs: [] rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: TALOONER_PROVIDER: gitlab GIT_STRATEGY: none script: - talooner-step ``` Keep `entrypoint: [""]` — GitLab runs the job's `script:` through a shell. ## GitHub Actions Works on github.com and GitHub Enterprise Server. Add `TALOONER_LICENSE_KEY` (and optionally your [LLM](#any-llm)'s `TALOONER_MODEL_API_KEY`) under **Settings → Secrets and variables → Actions**, and allow *GitHub Actions to create and approve pull requests* under **Settings → Actions → General**. ```yaml # .github/workflows/talooner.yml name: talooner on: pull_request: pull_request_review: issue_comment: types: [created] permissions: contents: read pull-requests: write checks: write jobs: review: runs-on: ubuntu-latest steps: - uses: docker://ghcr.io/talooner/talooner-runner:latest env: TALOONER_PROVIDER: github TALOONER_LICENSE_KEY: ${{ secrets.TALOONER_LICENSE_KEY }} TALOONER_MODEL_BASE_URL: https://dashscope-intl.aliyuncs.com/compatible-mode/v1 TALOONER_MODEL: qwen3-coder-plus TALOONER_MODEL_API_KEY: ${{ secrets.TALOONER_MODEL_API_KEY }} GITHUB_TOKEN: ${{ github.token }} ``` ## Any other CI Jenkins, CircleCI, Buildkite, Drone, Woodpecker, TeamCity — anything that can run a container against a GitHub or GitLab repository: ```sh docker run --rm \ -e TALOONER_PROVIDER=gitlab \ -e TALOONER_LICENSE_KEY \ -e TALOONER_MODEL_BASE_URL -e TALOONER_MODEL -e TALOONER_MODEL_API_KEY \ -e GITLAB_TOKEN -e GITLAB_API_URL \ -e CI_PROJECT_ID -e CI_MERGE_REQUEST_IID -e CI_PIPELINE_SOURCE \ ghcr.io/talooner/talooner-runner:latest ``` The image prints a ready config for your CI: `docker run --rm snippet gitlab` (or `github`, `generic`). Gitea/Forgejo, Bitbucket and Azure DevOps are coming soon. ## One variable instead of five Put everything in one YAML file and store it as a single masked variable, `TALOONER_CONFIG_B64`: ```yaml # talooner.yaml provider: gitlab license_key: TLN1.... model: base_url: https://dashscope-intl.aliyuncs.com/compatible-mode/v1 name: qwen3-coder-plus api_key: sk-... gitlab_token: glpat-... ``` ```sh docker run --rm -i ghcr.io/talooner/talooner-runner:latest \ encode-config - < talooner.yaml # → paste the output into TALOONER_CONFIG_B64 ``` A variable set on its own always overrides the same setting in the file. ## Settings | Variable | In `talooner.yaml` | | |---|---|---| | `TALOONER_LICENSE_KEY` | `license_key` | required | | `TALOONER_PROVIDER` | `provider` | `gitlab` or `github` — detected in GitLab CI and GitHub Actions | | `GITLAB_TOKEN` / `GITHUB_TOKEN` | `gitlab_token` / `github_token` | required for the provider | | `GITLAB_API_URL` | `gitlab_url` | self-managed GitLab outside GitLab CI | | `TALOONER_MODEL_BASE_URL` + `TALOONER_MODEL_API_KEY` + `TALOONER_MODEL` | `model.base_url` + `model.api_key` + `model.name` | AI review with [any LLM](#any-llm) behind an OpenAI-compatible API | | `ANTHROPIC_API_KEY` | `model.anthropic_api_key` | shortcut for Anthropic; model defaults to `claude-sonnet-5-5` | | `OPENAI_API_KEY` + `TALOONER_MODEL` | `model.openai_api_key` + `model.name` | shortcut for OpenAI | | `CDM_URL` + `CDM_TOKEN` | `cdm.url` + `cdm.token` | decision-model triage before the full review | | `CDM_BACKEND` | `cdm.backend` | `jev` or `subq`; taken from the URL when unset (`subq.ai` → `subq`, else `jev`) | | `JEV_URL` + `JEV_TOKEN` | `jev.url` + `jev.token` | older names, still read when `CDM_URL` and `CDM_TOKEN` are unset | | `TALOONER_CDM_MIN_CONFIDENCE` | `cdm.min_confidence` | triage threshold, default `0.9` | | `TALOONER_LOG_LEVEL` | `log_level` | `debug` prints OpenTalon's own log | | `TALOONER_METRICS_PUSH_URL` | `metrics.push_url` | push `talooner_*` metrics at the end of each job — see below | | `TALOONER_METRICS_PUSH_TOKEN` | `metrics.push_token` | the gateway's bearer token — store it masked / as a secret | | `TALOONER_METRICS_PUSH_JOB` | `metrics.job` | the `job` label, default `talooner` | | `TALOONER_METRICS_LABELS` | `metrics.labels` | optional extra labels to filter by in Grafana, e.g. `team=payments` when several teams share one Prometheus | | `TALOONER_MODEL_COST_INPUT` / `_OUTPUT` | `model.cost_input` / `model.cost_output` | your model's price per 1M input / output tokens — fills the [LLM cost](#llm-cost) metrics | | `TALOONER_METRICS_PR_LABEL` | `metrics.pr_label` | `true` adds the PR number and title, for [cost per pull request](#llm-cost) — off by default | ## Metrics with Prometheus and Grafana Every verdict, action, rule hit and AI review is a `talooner_*` Prometheus metric. A pipeline job lives for seconds, so Prometheus can't scrape it — instead, each job **pushes** its metrics once, when it finishes, to a gateway next to your Prometheus. ### Push from your pipeline The job pushes its metrics with the [Prometheus Pushgateway](https://github.com/prometheus/pushgateway) protocol to your gateway — [prom-aggregation-gateway](https://github.com/zapier/prom-aggregation-gateway), which adds up the counters of every job. **GitLab CI** — add two variables under **Settings → CI/CD → Variables** (or once on the group, for every project): | Variable | Value | | |---|---|---| | `TALOONER_METRICS_PUSH_URL` | `https://metrics.acme.dev:9091` | | | `TALOONER_METRICS_PUSH_TOKEN` | the gateway's token — only if it has one | **masked**, never in `.gitlab-ci.yml` | The URL can also go in the job; the token always stays a masked variable: ```yaml talooner: image: name: ghcr.io/talooner/talooner-runner:latest entrypoint: [""] stage: test needs: [] rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: TALOONER_PROVIDER: gitlab GIT_STRATEGY: none TALOONER_METRICS_PUSH_URL: https://metrics.acme.dev:9091 # TALOONER_METRICS_PUSH_TOKEN comes from the masked CI/CD variable script: - talooner-step ``` When the review is done, `talooner-step` pushes once and the job log shows `talooner-step: metrics: pushed 23 series to https://metrics.acme.dev:9091`. The token is sent as `Authorization: Bearer `. **GitHub Actions** — the same two settings in the step's `env`: the URL as a repository variable, the token as a secret: ```yaml - uses: docker://ghcr.io/talooner/talooner-runner:latest env: TALOONER_PROVIDER: github TALOONER_LICENSE_KEY: ${{ secrets.TALOONER_LICENSE_KEY }} GITHUB_TOKEN: ${{ github.token }} TALOONER_METRICS_PUSH_URL: ${{ vars.TALOONER_METRICS_PUSH_URL }} TALOONER_METRICS_PUSH_TOKEN: ${{ secrets.TALOONER_METRICS_PUSH_TOKEN }} ``` **The runner must reach the gateway.** The push goes out from the machine that runs the job: - **Your own runners** (self-hosted GitLab runner or GitHub runner) — use the internal address, e.g. `http://metrics.internal:9091`. Nothing needs to be public. - **gitlab.com shared runners or GitHub-hosted runners** run on the internet, so the gateway needs a public HTTPS address. Put it behind a reverse proxy that checks a bearer token, and store the token as a masked variable or secret, `TALOONER_METRICS_PUSH_TOKEN`. ### Collect it in Prometheus ```sh docker run -d -p 9091:80 ghcr.io/zapier/prom-aggregation-gateway:latest ``` ```yaml # prometheus.yml scrape_configs: - job_name: talooner honor_labels: true # keep the repo / provider labels Talooner sets static_configs: - targets: ["metrics.internal:9091"] ``` Import the dashboard into Grafana (*Dashboards → New → Import*) — [download talooner-dashboard.json](https://talooner.dev/grafana/talooner-dashboard.json), or print the one that matches your image: ```sh docker run --rm ghcr.io/talooner/talooner-runner:latest \ dashboard > talooner-dashboard.json ``` ### What you get | Metric | | |---|---| | `talooner_pr_evaluations_total{repo, verdict}` | reviews by verdict — `approve`, `block`, `none` | | `talooner_pr_actions_total{verb}` | what the rules did: approve, block, route, comment… | | `talooner_pr_rules_fired` | rules that fired per review (histogram) | | `talooner_pr_evaluation_duration_seconds` | how long a review took (histogram) | | `talooner_llm_reviews_total{result}` | AI reviews: `match`, `mismatch`, `unclear`, triage hits… | | `talooner_pr_warnings_total{code}` | ruleset health warnings | | `opentalon_plugin_llm_cost_usd_total{plugin, action, model}` | LLM spend per plugin action and model — e.g. `talooner` · `evaluate_pr` | | `opentalon_plugin_llm_tokens_total{plugin, action, model, direction}` | the tokens behind it, `input` / `output` | | `opentalon_llm_*_tokens_total`, `opentalon_llm_*_cost_usd_total{model}` | all LLM tokens and cost of the run, by model | Each series also carries `repo`, `provider` and `pipeline_source`, plus your own `TALOONER_METRICS_LABELS`. The dashboard shows the auto-approval and needs-a-human rates, the verdict mix over time, latency, top repositories, AI review outcomes, warnings and LLM cost — watch the auto-approval rate climb as you tune the rules: ```promql sum(rate(talooner_pr_evaluations_total{verdict="approve"}[1d])) / sum(rate(talooner_pr_evaluations_total[1d])) ``` A push that fails — gateway down, wrong URL — is logged as a warning and never fails the job. ### LLM cost Set your model's price and every review reports what its AI calls cost — per plugin action and model, with your own LLM key: ```yaml TALOONER_MODEL_COST_INPUT: "0.8" # price per 1M input tokens TALOONER_MODEL_COST_OUTPUT: "3.2" # price per 1M output tokens ``` The dashboard's **LLM cost** row shows the spend in the selected range, the cost per review, cost by plugin action and by model, and the most expensive repositories: ```promql # cost per review this week sum(increase(opentalon_plugin_llm_cost_usd_total[7d])) / sum(increase(talooner_pr_evaluations_total{mode="execute"}[7d])) ``` Without a price you still get the token counts; the cost series stay at 0. **Cost per pull request.** Set `TALOONER_METRICS_PR_LABEL=true` and the cost and review series also carry the PR number (`!412` on GitLab, `#412` on GitHub); the title and link travel in a separate `talooner_pr_info` series. The dashboard's **Top pull requests by LLM cost** table then shows each PR's total cost, tokens and review runs — click one to open it. It's off by default: every PR adds a few series to your Prometheus, and PR titles end up in your monitoring. ## What leaves your pipeline - Calls to your forge's API. - Calls to **your own** LLM provider — only where a rule asks for `llm_review`. - Metrics to **your own** gateway, if you set one — counts, verdicts, cost and repository names; PR numbers and titles only with `TALOONER_METRICS_PR_LABEL=true`. - One license check to us: the key, a nonce and the version. Never code, repositories or PR data to us. Each job verifies the license online before it reviews. If the license is revoked or expired, or the license server can't be reached, Talooner posts a *"license inactive"* check run and comment — and the job still succeeds, so it never blocks your merges. --- # Onboard a repository > Answer a few questions about your repository; Talooner writes them down as a spec, builds tested rules from it and opens the pull request — from your terminal or your coding agent. Talooner reviews every pull request against the policy in `.talooner/rules.tln`. **Onboarding writes that first policy with you, in four steps:** 1. **Ask.** It drafts the sensitive paths it finds (auth, payments, migrations, deploy, CI…) with owners from CODEOWNERS, and asks you to confirm them, who reviews each, which extra rules you want (descriptions, PR size, new dependencies, migrations) and — when your license includes AI review — whether to use it ([decision-model](https://talooner.dev/llm-review/) triage first where your cluster has a decider). 2. **Spec.** Your answers become `.talooner/onboarding.md`, shown to you before anything is built. It is read by default on every later run: edit it and run onboarding again to regenerate the rules. 3. **Rules + tests.** The rules are built from the spec and checked against their own test suite before you see them. Sensitive paths hold the merge until their reviewer approves. 4. **Pull request.** Rules, tests, spec and the CI step — which tests the rules before every review — on one branch, or written into your checkout. **Decision rules, automatically.** With `CDM_URL` + `CDM_TOKEN` passed in and AI review in your license, the rules use a [context decision model](https://talooner.dev/llm-review/) too: cheap triage before each code review, and typed decisions on the PR itself — `tool "cdm" "decide"` steps that rate the PR's risk or the quality of its description, with rules that act only on a confident answer (certainty ≥ 0.85) and tests for the confident and the unsure case. **Guides for the AI review, with your coding agent.** The AI review checks each changed part of the code against the doc linked to it in `.talooner/architecture.yaml`, so its findings are only as sharp as that doc. The [onboarding skill](https://talooner.dev/onboard/#agent) writes those guides with you: it asks per sensitive area which mistakes have hurt you, what must never happen and which tests must exist, then writes one checkable rule per section — with a "gotcha" showing the typical wrong code, and a list of required spec cases per file — and checks the guides against your current code before anything is committed. Your rules stay generic; what's specific to your codebase lives in docs your team can read too. **Need help?** The Talooner team can help with onboarding your project — [message us](https://talooner.dev/contact/?topic=onboarding). It uses the same image as the [pipeline step](https://talooner.dev/install/#pipeline) — nothing else to install. ## What you need 1. **Docker**. 2. **Your license key** in `TALOONER_LICENSE_KEY` — from your license page (or [get a free demo license](https://talooner.dev/#pilot)). 3. **Recommended: any LLM** behind an OpenAI-compatible API — Qwen, DeepSeek, a model on your own servers… (`TALOONER_MODEL_BASE_URL` + `TALOONER_MODEL` + `TALOONER_MODEL_API_KEY`, [examples](https://talooner.dev/install/#any-llm)); Anthropic and OpenAI also work with `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. With one, the rules are drafted for your repository and checked against their tests before you see them. Without one, you get the generic starter ruleset to edit by hand. A context decision model — [Jev](https://typesafe.ai/) or [SubQ Snap](https://subq.ai/introducing-subq-snap) (`CDM_URL` + `CDM_TOKEN`) — is supported too; it speeds up the reviews in your pipeline; drafting the rules still needs one of the LLMs above. ## A repository on your machine Run it from the repository's root folder: ```sh cd path/to/your-repo docker run --rm -it \ -v "$PWD:/repo" --user "$(id -u):$(id -g)" \ -e TALOONER_LICENSE_KEY \ -e TALOONER_MODEL_BASE_URL -e TALOONER_MODEL -e TALOONER_MODEL_API_KEY \ -e CDM_URL -e CDM_TOKEN \ ghcr.io/talooner/talooner-runner:latest onboard ``` `-it` lets it ask. It writes into the repository: ``` .talooner/onboarding.md the spec — your answers .talooner/rules.tln the rules .talooner/rules.tln.test a test for each rule .github/workflows/talooner.yml or the talooner jobs added to .gitlab-ci.yml ``` `--user` makes the files yours rather than the container's. If `.talooner/rules.tln` already exists and differs, onboarding stops — add `--force` to replace it. Without `-it` (in CI, for example) nothing is asked: an existing spec is used as it is, else the draft. ## A repository on GitHub or GitLab Point it at a remote repository instead — no clone needed. It needs a token that can read the repository, and prints the files instead of writing them (add `--out ` to write them to a folder): ```sh docker run --rm -it \ -e TALOONER_LICENSE_KEY -e GITLAB_TOKEN \ -e TALOONER_MODEL_BASE_URL -e TALOONER_MODEL -e TALOONER_MODEL_API_KEY \ ghcr.io/talooner/talooner-runner:latest \ onboard --repo gitlab.com/acme/shop ``` Use `--repo github.com/acme/shop` with `GITHUB_TOKEN` for GitHub. For self-managed GitLab or GitHub Enterprise, give the host — `--repo git.acme.dev/team/shop` — and `--provider gitlab` or `github`. ## As a pull request Add `--pr` and onboarding opens a pull request (GitHub) or merge request (GitLab) instead of writing files. It holds the spec, the rules, their tests **and the review step** — `.github/workflows/talooner.yml`, or the `talooner` jobs added to your `.gitlab-ci.yml` — on a new `talooner-onboarding` branch, with a checklist of the CI variables to set: ```sh docker run --rm -it \ -v "$PWD:/repo" \ -e TALOONER_LICENSE_KEY -e GITLAB_TOKEN \ -e TALOONER_MODEL_BASE_URL -e TALOONER_MODEL -e TALOONER_MODEL_API_KEY \ ghcr.io/talooner/talooner-runner:latest onboard --pr ``` From a local checkout it opens the PR on the repository's `origin`; with `--repo gitlab.com/acme/shop --pr` it needs no checkout at all. The token needs write access — GitLab: `api` scope, *Developer* role; GitHub: contents and pull requests. An existing `.talooner/rules.tln` that differs is never replaced without `--force`, and a CI file that already runs Talooner is left as it is. `--branch` and `--base` change the branch names. ## What it reads Only the shape of the repository: folder names three levels deep, the names of its CI workflows, the first lines of the README, and three files as they are — the spec, CODEOWNERS and the CI file the step is added to. **Never the source code.** Those names go to the cluster running inside the container and, if you set an LLM key, to **your own** LLM provider. Nothing reaches us except the license check. ## Then 1. **Read `.talooner/rules.tln`** and adjust it to how your team actually works — the draft is a starting point. The [rules guide](https://talooner.dev/rules/) shows what rules can match on. 2. **Run the tests** after every change: ```sh docker run --rm -v "$PWD:/repo" -w /repo --entrypoint tln \ ghcr.io/talooner/talooner-runner:latest test .talooner/ ``` 3. **Commit the spec, the rules, the tests and the CI change** in one pull request (or let `--pr` do it). The [install guide](https://talooner.dev/install/#pipeline) has every CI option. ## Let your coding agent do it Onboarding comes as an **agent skill** too: a `SKILL.md` that tells your coding agent — Codex, Cursor, Claude Code, Qwen Code or any other — how to interview you about the current repository, write the spec with you, run onboarding from it, tailor the rules to the code it can see, keep the tests passing and open the pull request — always asking you before it commits. [Download SKILL.md](https://talooner.dev/skills/talooner-onboard/SKILL.md) Put it where your agent picks up skills or instructions — its skills folder, or reference it from `AGENTS.md` — then ask your agent to *"onboard this repo to Talooner"*. --- # How it works > The end-to-end flow — a deterministic engine with a single, optional probabilistic hop. Talooner runs as **one step in your GitHub or GitLab CI**. The job starts the `talooner-runner` container: the bot extracts facts from your forge, the `tln` engine decides, the bot acts on the verdict, and the container exits. No server to host. Same commit, same rules, same verdict — every time. ## The flow, step by step 1. **Trigger.** A PR/MR event fires, or a maintainer comments `!talooner /review`. 2. **Extract facts.** The bot reads the forge API with its scoped token: `pr.*`, `user.*`, `review.*`, the diff, changed files, CI status. 3. **Evaluate.** Inside the same container, the `tln` engine loads the repo's ruleset and resolves the verdict. Most PRs never touch a model. 4. **Consult a model — only if a rule asks.** Where a rule says `do llm_review`, the executor calls a model with the code-unit's *state* plus a *typed question*. With a context decision model — [Jev](https://typesafe.ai/) or [SubQ Snap](https://subq.ai/introducing-subq-snap) — that answer comes back as a **typed decision with calibrated confidence** — a `Choice`, a `Score`, or a `Noul` (true/false, 0–1) — which re-enters the engine as an ordinary fact (`llm.risk`, `llm.risk_confidence`, …). Not prose. 5. **Act.** The engine returns a list of actions; the bot executes them on the forge — a check run / commit status, one sticky review comment, an advisory approve/block, assignees and review requests — then exits. ## Works with your agents Talooner reviews the **pull request**, not the tool that wrote it — so it fits whatever your team already uses to write code. Native integrations are on the way. **Runs on** - **OpenClaw** (coming soon): Talooner as the guard inside your OpenClaw agents — rules check the work before it leaves. - **Hermes Agent** (coming soon): Nous Research's open-source agent — Talooner rules check its work before it ships. - **Devin** (coming soon): Talooner rules applied to Devin's work before its pull requests reach your team. - **OpenTalon** (coming soon): Talooner rules and the tln engine running inside your self-hosted OpenTalon cluster. **Reviews PRs from** - **Claude Code** (coming soon): Agentic coding in the terminal and CI, opening PRs on your forge. - **OpenAI Codex** (coming soon): Cloud coding agent that hands its work back as pull requests. - **GitHub Copilot agent** (coming soon): Assign it an issue, it opens a PR — Talooner reviews it first. - **Cursor** (coming soon): Background agents that push branches and open PRs. - **Flow-Next** (coming soon): Plan-first Claude Code / Codex workflow — its overnight Ralph runs meet your rules before merge. - **Factory Droids** (coming soon): Factory's agents that take a ticket all the way to a pull request. - **OpenHands** (coming soon): Open-source coding agent that turns issues into pull requests. - **Jules** (coming soon): Google's asynchronous coding agent that works in a VM and opens PRs. - **Kiro** (coming soon): AWS's spec-driven agentic IDE — specs become code, code becomes a PR. - **Aider** (coming soon): AI pair programming in the terminal that commits straight to git. **Autonomous loops** coming soon — tools that code unattended overnight (Flow-Next's Ralph mode, agent task runners) produce exactly the "insurmountable wall of pull requests" no reviewer can read. Talooner can be their last gate: the loop asks `talooner rules plan --repo … --pr …` before it hands off, sees *"needs security review"* or *"blocked: too large"*, and fixes it before a human ever looks. Because the author is just a fact, a rule can treat agent-written PRs differently: ```talon rule "Agent-authored PRs get an owner review" { for records where type == "pr" and attr "pr.author" ends_with "[bot]" requires "review.owner" do assign "pr" attr "user.owner" do comment "pr" "Opened by {attr.pr.author} — the code owner signs off before merge." } ``` ## Why this shape - **Everything stays in your pipeline.** The job holds a scoped forge token and, if you set one, your own LLM key — both as CI secrets. Every token a rule spends is billed to whoever ran the rule. Nobody's review load lands on someone else's API limits. - **Typed output makes confidence a first-class gate.** A rule can *act* on a high-confidence verdict and *escalate to a human* on a low one — something you can't express cleanly against free-text LLM output. - **Determinism where it counts.** The engine is deterministic. The single probabilistic hop is isolated behind `do llm_review` and cached per code-unit, so its variance is contained rather than driving the verdict. --- # Automated QA > Rules that decide when a PR is worth deploying, then verify it with LLM-generated test scenarios — and fold the result back into the verdict as a fact. Static review tells you whether a diff *looks* right. It can't tell you whether the change actually *works*. Automated QA closes that gap without handing the decision to a model: **the rules decide when a PR earns a throwaway deployment, an LLM generates the test scenarios, the scenarios run, and the results come back as facts the engine gates on.** The model proposes scenarios. It never decides the outcome — pass/fail is measured, and the verdict stays with the rules. ## The idea Once a PR is deemed deploy-worthy by policy — green CI, off a critical path, small enough to be safe — Talooner can: 1. **Spin up an ephemeral preview deployment** of the PR's code. 2. **Ask a model to generate test scenarios** from the diff, the PR description, and the module's own docs — the behaviours a reviewer would want exercised. 3. **Run those scenarios** against the preview environment. 4. **Fold the results back into the engine as facts** — `qa.scenarios_total`, `qa.scenarios_passed`, `qa.failed_scenarios`, `qa.coverage_confidence`. 5. **Let the rules decide** what the outcome means: approve, comment the failures, request changes, escalate to a human, or tear the environment down. ## What it looks like in a ruleset ```talon // Only PRs that already pass the cheap gates earn a deployment. define "deploy_candidate" { attr "pr.tests_passing" == true attr "pr.lint_passing" == true attr "pr.draft" == false not is "critical_path" } // Rules ask for the deployment and the generated scenarios — they aren't automatic. rule "Run automated QA on deploy candidates" { for records where type == "pr" and is "deploy_candidate" do deploy_preview "pr" do qa_scenarios "pr" } // The engine gates on the measured result, not on the model's opinion. rule "Block on failed QA scenarios" { for records where type == "pr" and attr "qa.scenarios_passed" < attr "qa.scenarios_total" block "merge" do block "pr.merge" do comment "pr" "Automated QA failed {attr.qa.failed_scenarios} — see the run before merging" reason "generated scenarios failed" priority HIGH } // Low coverage confidence is a human's call, not an auto-approve. rule "Escalate thin QA coverage" { for records where type == "pr" and attr "qa.coverage_confidence" < 0.8 do comment "pr" "QA coverage looks thin (confidence {attr.qa.coverage_confidence}) — a human should confirm the risky paths are exercised" } ``` ## Why route it through rules instead of a model - **The decision to deploy is policy, not a guess.** A rule — not a model — decides which PRs are safe to stand up. Fork PRs, critical paths, and secret-touching changes simply never enter the loop. - **Scenarios are generated; outcomes are measured.** The LLM's job ends at proposing what to test. Whether those tests pass is a fact, and facts are what the engine gates on. A confident, wrong model can't approve a broken PR. - **Every run is auditable.** The generated scenarios, the environment, and the pass/fail results are all recorded as facts — you can `explain` exactly why a PR was blocked or approved. - **Confidence is explicit.** `qa.coverage_confidence` lets a rule distinguish "verified working" from "we couldn't meaningfully exercise this" and route the second case to a human. ## Generated scenarios are real tests The scenarios aren't a prose summary of "what to check" — they're executable tests the runner can actually run against the preview. A functional scenario, generated from the diff, the PR description, and the module's docs, comes out as a Jest test: ```js // talooner-generated · scenario: "a user can rename themselves, but nobody else" // derived from: diff app/controllers/users_controller.rb, PR body, docs/users.md import { test, expect } from '@jest/globals'; const DEPLOY = process.env.PREVIEW_URL; const asAlice = { 'content-type': 'application/json', authorization: `Bearer ${process.env.ALICE_TOKEN}` }; test('updating your own name is saved', async () => { const res = await fetch(`${DEPLOY}/api/users/alice`, { method: 'PATCH', headers: asAlice, body: JSON.stringify({ name: 'Alice Doe' }), }); expect(res.status).toBe(200); const user = await fetch(`${DEPLOY}/api/users/alice`, { headers: asAlice }).then((r) => r.json()); expect(user.name).toBe('Alice Doe'); }); test("updating someone else's name is forbidden", async () => { const res = await fetch(`${DEPLOY}/api/users/bob`, { method: 'PATCH', headers: asAlice, body: JSON.stringify({ name: 'Hacked' }), }); expect(res.status).toBe(403); }); ``` The model proposed the case; Jest measures the outcome. `qa.scenarios_passed < qa.scenarios_total` is a fact, and the rules gate on it — a confident, wrong model can't turn a red suite green. ## Does it also look right? Working isn't the same as matching the design. [Design checks](https://talooner.dev/design-checks/) compare the rendered preview with your design file — Figma first — and fold the result into `design.*` facts the same way. --- # Design checks > Does the PR look like the design? Talooner renders the changed UI, compares it with your design file, and turns the result into facts your rules gate on. **coming soon** Review and tests tell you whether a change *works*. Design checks tell you whether it *looks like the design* — on every PR that touches UI, without a designer opening the preview. ## Design tools | Tool | | |---|---| | **Figma** | first — components, nodes and design tokens through the Figma API | | **Penpot** | next — the open-source design tool, self-hostable next to Talooner | | **Sketch** | planned | | **Framer** | planned | Want another one first? [Tell us](https://talooner.dev/contact/?topic=design-tools). ## How it works When a PR touches UI, Talooner can go past "does it work" to "does it look like the design". The `design_check` executor reads the referenced Figma nodes (reference image + design tokens), renders the changed components in the preview, and compares them — **both directions**: - **Drift from Figma** — a component whose pixels or tokens (color, spacing, type) don't match its Figma source. - **Not in Figma** — components rendered on the page that have *no* matching Figma component. Undocumented UI is drift too, and easy to miss in review. It folds the result into `design.*` facts: | Fact | Meaning | |---|---| | `design.figma_match` | every changed component matched its Figma source | | `design.pixel_drift` | worst-case visual difference, 0–1 | | `design.token_mismatches` | design tokens that didn't match (`color/primary`, `spacing/md`, …) | | `design.undocumented_count` | rendered components with **no** Figma source | | `design.undocumented_components` | which ones | | `design.match_confidence` | calibrated confidence in the verdict | The generated design scenario is a Jest + Playwright visual test: ```js // talooner-generated · design scenario: "PriceCard vs Figma node 1284:57" import { test, expect } from '@jest/globals'; import { chromium } from 'playwright'; import { fetchFigmaNode, diffAgainstFigma, componentsOnPage } from '@talooner/figma-qa'; const DEPLOY = process.env.PREVIEW_URL; test('PriceCard renders within 1% of its Figma component', async () => { const ref = await fetchFigmaNode('1284:57'); // reference image + tokens const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto(`${DEPLOY}/components/price-card`); const shot = await page.locator('[data-component="price-card"]').screenshot(); const { pixelDrift, tokenMismatches } = diffAgainstFigma(shot, ref); expect(pixelDrift).toBeLessThan(0.01); expect(tokenMismatches).toEqual([]); // colors/spacing/type match tokens await browser.close(); }); test('every rendered component maps to a Figma component', async () => { const rendered = await componentsOnPage(`${DEPLOY}/pricing`); // [data-component] nodes const documented = await fetchFigmaNode('page:pricing').then(n => n.componentNames); const orphans = rendered.filter(c => !documented.includes(c)); expect(orphans).toEqual([]); // UI with no Figma source is a finding, not a pass }); ``` And the rule that catches the not-in-Figma case: ```talon rule "Flag UI that isn't in Figma" { for records where type == "pr" and is "pr.touches_design" and attr "design.undocumented_count" > 0 do require "review.design" do comment "pr" "These rendered components have no Figma source: {attr.design.undocumented_components}. Add them to the design file, or remove them" reason "undocumented UI" priority MEDIUM } ``` Pair it with the drift and confidence rules on the [Rules](https://talooner.dev/rules/) page and a UI change is gated three ways: it must match Figma, contain nothing that *isn't* in Figma, and — when the check is unsure — go to a human. --- # Security checks > Before a PR merges, prove the app still enforces who may access what. Talooner checks the PR's preview against an access policy in your repo, and a rule blocks the merge on any violation. **coming soon** Broken access control tops the OWASP Top 10. A refactor that drops one ownership check looks fine in review, and it passes every test that signs in as the right user. Security checks catch it **before the merge**. CI starts the PR's build as a throwaway preview, and Talooner checks it against an access policy declared in the repository. One to three cooperating agents each act as a different role. Results come back as facts, and a rule **blocks the merge** on any violation. The model plans and proposes scenarios. It never decides the outcome: a finding counts only when the runner reproduces it deterministically. ## What it doesn't cover - Anything outside the pipeline's own preview environment. - Background jobs, webhooks, mobile clients and infrastructure. - Real user data or real credentials. ## What you provide in CI 1. **A preview deployment** of the PR, reachable from the job, passed in as `TALOONER_ACCESS_TARGET` (e.g. `https://pr-412.preview.acme.dev`). 2. **Seed data and test accounts**, created by your own CI step. Credentials reach the runner as one masked variable, `TALOONER_ACCESS_ACCOUNTS` (base64 YAML): ```yaml login: url: /api/session # how a role signs in; the runner keeps the session body: '{"email":"{{email}}","password":"{{password}}"}' roles: guest: {} # no session member_a: { email: a@test.acme.dev, password: … , tenant: team-a } member_b: { email: b@test.acme.dev, password: … , tenant: team-b } admin: { email: admin@test.acme.dev, password: … } ``` 3. **The policy**, in the repository: the `.talooner/access/` folder. ## The policy The policy is a folder of plain [tln](https://tln-lang.org) files, loaded together. Split it however your app is split: one file per area keeps each rule next to the team that owns it. ```text .talooner/access/ ├── guests.tln ├── tenants.tln └── admin.tln ``` Each route or resource the runner reaches is a record. The role the agent is acting as arrives as `context.actor` and `context.tenant`. Anything a rule blocks must stay out of reach for that role: ```talon // .talooner/access/guests.tln // Only sign-in and health checks are open without a session. strict rule "Guests stay out of the API" { for records where type == "endpoint" and attr "path" starts_with "/api/" and attr "path" not in ["/api/session", "/api/health"] and context.actor == "guest" block "access" reason "API routes need a session" } ``` ```talon // .talooner/access/tenants.tln // One tenant never sees another tenant's data. strict rule "Members stay in their own tenant" { for records where type == "project" and context.actor == "member" and attr "tenant" != context.tenant block "access" reason "cross-tenant access" } ``` ```talon // .talooner/access/admin.tln // Admin screens are for admins. strict rule "Admin area is admin-only" { for records where type == "endpoint" and attr "path" starts_with "/admin" and context.actor != "admin" block "access" reason "admin-only route" } ``` The rules are `strict`, so nothing else in the ruleset can override them. ## Gating the merge Every violation the runner reproduces becomes a fact on the PR. Your review ruleset gates on it like on any other fact: ```talon rule "Block on access violations" { for records where type == "pr" and attr "security.violations_count" > 0 block "merge" do block "pr.merge" do comment "pr" "Access policy violated: {attr.security.violations}. Each one reproduced against the preview" reason "access policy violated" priority CRITICAL } ``` ## Where this is headed: red-team agents The roles above are fixed probes. The direction we're exploring is a group of agents that **collaborate to discover access and authorization flaws** — a multi-agent red team, pointed only at software you've authorized and running in the preview's isolated sandbox. Each agent has a specialised role and shares findings through a private channel; a coordinator hands out the work and uses what comes back to decide what to look at next. ### The agents | Agent | Responsibility | |---|---| | **Scout** | Maps the app's surface and flags components worth a closer look | | **Analyst** | Reads the code and forms vulnerability hypotheses | | **Tester** | Runs controlled, benign tests against the isolated copy | | **Critic** | Independently checks findings and throws out false positives | The **coordinator** distributes the work and uses the agents' feedback to choose the next thing to investigate. ### Black box or white box How much the agents get to see is up to you, set per repo. - **Black box** — the agents see only the running preview, the way an outside user would. They map the surface and probe it without reading a line of source. Good for catching what's actually reachable, independent of what the code intends. - **White box** — the agents also read the diff and the source. The Analyst can reason straight from the implementation to a hypothesis, so a dropped ownership check is found by looking, not just by guessing which request to send. Because the PR gives Talooner both the preview and the repository, it can run either — or white box to form the hypothesis and black box to prove it against the live app. ### Limits The red team runs inside the CI job, so it stays bounded and predictable: - **Time** — a wall-clock budget per run; when it's up, the agents stop and report what they have so far. - **Retries** — a probe that fails to reproduce is retried a fixed number of times before it's dropped. A finding counts only once the runner reproduces it deterministically, so a flaky one never reaches the verdict. - **Budget** — a cap on model calls and requests per run, so cost stays flat whatever the agents decide to chase. - **Scope** — the agents touch only the authorized preview and its seed data; the sandbox and its audit log are the fence. Hit a limit and the run ends cleanly with a partial result — never a hang, and never a merge blocked just because the budget ran out. ### A collaboration cycle 1. **Scout** spots an API endpoint with unusual input handling. 2. **Analyst** reads the implementation behind it and proposes a possible authorization flaw. 3. **Tester** runs benign cases against a locally isolated copy of the app. 4. **Critic** checks whether the observed behaviour actually breaks the intended security policy. 5. **Coordinator** consolidates the evidence and recommends a fix or another test. Everything stays in bounds: the targets are ones you've authorized, and the whole run happens in an isolated sandbox with audit logs. Want security checks on your repos early? [Tell us](https://talooner.dev/contact/?topic=security-checks). --- # Rules > Policy as code — versioned, diffable, reviewable, and unit-testable before it ever gates a real PR. A full worked ruleset, with its test suite. Because the policy is a file in your repo, it is versioned, diffable, reviewable, and unit-testable with `.tln.test` before it ever gates a real PR. That is a claim no LLM-based reviewer can make. ## Anatomy of a ruleset Rules are written in [tln](https://tln-lang.org/), an open language for deterministic rules — the full language reference lives at [tln-lang.org](https://tln-lang.org/). `define` blocks are named predicates. `rule` blocks match records against facts and predicates, then declare a verdict (`allow` / `block` / `requires`) and the actions to take (`do …`). Every rule can carry a `reason` and a `priority`. ```talon define "small_change" { attr "pr.lines_changed" < 50 attr "pr.files_changed" < 5 } rule "Auto-approve safe changes" { for records where type == "pr" and is "small_change" and attr "pr.tests_passing" == true and attr "pr.has_description" == true and not is "critical_path" allow "merge" do approve "pr" priority MEDIUM } ``` --- ## A fuller policy Predicates first, then rules grouped by what they do. ### Predicates ```talon define "small_change" { attr "pr.lines_changed" < 80 and attr "pr.files_changed" < 6 } define "large_change" { attr "pr.lines_changed" >= 400 } define "huge_change" { attr "pr.lines_changed" >= 1500 } define "ready_for_review" { attr "pr.draft" == false attr "pr.has_description" == true } define "critical_path" { attr "pr.changed_files" contains "internal/auth/" or attr "pr.changed_files" contains "billing/" } define "pr.touches_migrations" { attr "pr.changed_files" contains "db/migrate/" } define "pr.touches_design" { attr "pr.changed_files" contains "app/components/" or attr "pr.changed_files" ends_with ".scss" or attr "pr.changed_files" ends_with ".css" or attr "pr.changed_files" contains "design-tokens" } ``` ### Gates — block incomplete work ```talon rule "Block PRs with no description" { for records where type == "pr" and attr "pr.draft" == false and attr "pr.has_description" == false block "merge" do block "pr.merge" do comment "pr" "No description. Say what changed and why before requesting review" reason "no description" priority HIGH } rule "Block PRs too large to review" { for records where type == "pr" and is "huge_change" block "merge" do block "pr.merge" do comment "pr" "{attr.pr.lines_changed} lines across {attr.pr.files_changed} files — split this before review" reason "unreviewably large" priority HIGH } ``` ### Auto-approve — narrow, green, low-risk ```talon rule "Auto-approve safe changes" { for records where type == "pr" and is "small_change" and is "ready_for_review" and attr "pr.tests_passing" == true and attr "pr.lint_passing" == true and not is "critical_path" and not is "pr.touches_design" allow "merge" do approve "pr" do comment "pr" "Small, tested, lint-clean, off critical paths — no objections" priority MEDIUM } ``` ### Ownership — escalate to the people who own the code Ownership is a *fact* (`user.owner` resolves CODEOWNERS, else the last toucher), so "the author owns the code they changed — get an independent reviewer" is trivially expressible. ```talon rule "Require human review for critical paths" { for records where type == "pr" and is "critical_path" requires "review.senior_engineer" do require "review.senior_engineer" do assign "pr" attr "user.owner" do comment "pr" "Touches critical code owned by {attr.user.owner} — human approval required" reason "touches a critical path" priority HIGH } rule "Escalate self-review of owned critical code" { for records where type == "pr" and is "critical_path" and attr "user.owner" == attr "pr.author" requires "review.senior_engineer" do require "review.senior_engineer" do comment "pr" "Author {attr.pr.author} owns this code — needs an independent reviewer" reason "author owns the code they changed" priority CRITICAL } rule "DBA review on migrations" { for records where type == "pr" and is "pr.touches_migrations" requires "review.dba" do require "review.dba" do comment "pr" "Schema change — DBA sign-off required. Confirm it's reversible and index-safe" reason "schema migration" priority HIGH } ``` ### Design changes — require a design review, and check against Figma UI changes get their own path: a design review request **and** an automated Figma conformance check (see [Automated QA](https://talooner.dev/automated-qa/)). The check produces `design.*` facts the rules gate on. ```talon rule "Design review + Figma check on UI changes" { for records where type == "pr" and is "pr.touches_design" do design_check "pr" // fetch Figma, render the preview, compare do require "review.design" do assign "pr" attr "user.owner" do comment "pr" "UI change — running Figma conformance and requesting a design review" reason "user-facing UI change" priority MEDIUM } rule "Block UI that drifts from Figma" { for records where type == "pr" and is "pr.touches_design" and attr "design.figma_match" == false and attr "design.match_confidence" >= 0.9 block "merge" do block "pr.merge" do comment "pr" "Rendered UI drifts from Figma: {attr.design.token_mismatches}. Align with the design tokens, or update the component in Figma first" reason "design drift from Figma" priority HIGH } rule "Escalate uncertain design checks to a human" { for records where type == "pr" and is "pr.touches_design" and attr "design.match_confidence" < 0.9 do comment "pr" "Figma conformance was inconclusive (confidence {attr.design.match_confidence}) — a designer should eyeball this" } ``` ### Dependencies ```talon rule "Security review for new dependencies" { for records where type == "pr" and attr "pr.new_dependencies" > 0 requires "review.security" do require "review.security" do assign "pr" "@org/security" do comment "pr" "{attr.pr.new_dependencies} new dependencies — security review required" reason "adds new dependencies" priority HIGH } ``` --- ## Tested before it gates anything A ruleset ships with a `.tln.test` file: you assert the verdict a set of facts should produce, and CI fails if the policy drifts. Each `test` block declares records with `given`, names the rule under test with `when`, and asserts with `expect` — the same `describe / it / expect` discipline you'd bring to a Jest suite, but over facts. ```talon test "Small, tested, non-design PR is approved; a critical one is not" { given { record 1 type "pr" attr 1 "pr.number" 101 attr 1 "pr.lines_changed" 30 attr 1 "pr.files_changed" 2 attr 1 "pr.draft" false attr 1 "pr.has_description" true attr 1 "pr.tests_passing" true attr 1 "pr.lint_passing" true attr 1 "pr.changed_files" ["README.md", "docs/usage.md"] record 2 type "pr" attr 2 "pr.number" 102 attr 2 "pr.lines_changed" 30 attr 2 "pr.files_changed" 2 attr 2 "pr.draft" false attr 2 "pr.has_description" true attr 2 "pr.tests_passing" true attr 2 "pr.lint_passing" true attr 2 "pr.changed_files" ["internal/auth/session.go"] } when rule "Auto-approve safe changes" expect { flagged 1 not flagged 2 did 1 approve "pr" did_not 2 approve "pr" } } test "CI still running leaves tests_passing unset, so nothing is approved" { given { // pr.tests_passing is deliberately absent — checks are queued. A positive // condition on an unset fact fails closed. record 1 type "pr" attr 1 "pr.number" 103 attr 1 "pr.lines_changed" 10 attr 1 "pr.files_changed" 1 attr 1 "pr.draft" false attr 1 "pr.has_description" true attr 1 "pr.lint_passing" true attr 1 "pr.changed_files" ["README.md"] } when rule "Auto-approve safe changes" expect { not flagged 1 did_not 1 approve "pr" } } test "A UI change that drifts from Figma is blocked; a matching one is not" { given { record 1 type "pr" attr 1 "pr.number" 210 attr 1 "pr.changed_files" ["app/components/price_card.tsx", "app/assets/price_card.scss"] attr 1 "design.figma_match" false attr 1 "design.match_confidence" 0.96 attr 1 "design.token_mismatches" ["color/primary", "spacing/md"] record 2 type "pr" attr 2 "pr.number" 211 attr 2 "pr.changed_files" ["app/components/price_card.tsx"] attr 2 "design.figma_match" true attr 2 "design.match_confidence" 0.98 } when rule "Block UI that drifts from Figma" expect { flagged 1 not flagged 2 did 1 block "pr.merge" did_not 2 block "pr.merge" } } test "Author owning critical code they changed is escalated" { given { record 1 type "pr" attr 1 "pr.number" 305 attr 1 "pr.author" "@alice" attr 1 "user.owner" "@alice" attr 1 "pr.changed_files" ["internal/auth/token.go"] } when rule "Escalate self-review of owned critical code" expect { flagged 1 did 1 require "review.senior_engineer" } } ``` Run the suite before it ever touches a real PR: ```bash talooner rules validate .github/talooner/ talooner rules test .github/talooner/ ``` ## Facts a rule can match on - **`pr.*`** — `lines_changed`, `files_changed`, `changed_files`, `tests_passing`, `lint_passing`, `has_description`, `draft`, `is_fork`, `labels`, `diff`, and more. - **`user.*`** — `owner`, `owners`, `last_toucher`. - **`review.*`** — `human.approved`, `changes_requested`, `.approved`, `.stale`. - **`module.*`** — the primary touched module, its owner and docs. - **`llm.*` / `qa.*` / `design.*`** — verdicts a rule asked for, re-entering the engine as facts. See [How it works](https://talooner.dev/how-it-works/) and [Automated QA](https://talooner.dev/automated-qa/). Unset facts fail closed: a rule gated on a fact that wasn't asserted simply doesn't fire — the safe direction. --- # Vibe coding > In 2026 anyone can describe an app and get one. Talooner looks after what comes with it — the rules, the testing and the security a non-technical builder shouldn't have to think about. In 2026, vibe coding became an ordinary part of life: people with no technical background describe what they need, get a working tool, and automate their routine work. We believe human history has seen three great leaps in how people get knowledge: the printing press, the internet, and — for now the latest — large language models. Any knowledge can be used for good or for harm, and so can automation. That is why oversight of vibe-coded solutions is Talooner's priority. Maintaining code, deploying it and above all keeping it secure are technical concerns that non-technical builders often — understandably — don't think much about. Talooner closes that gap in a vibe-coded product: - [Onboarding](https://talooner.dev/onboard/) writes the rules for the repository. - [Agent testing](https://talooner.dev/automated-qa/) (coming soon): agents use the app the way people will and catch what breaks. - [Security checks](https://talooner.dev/security-checks/) (coming soon): agents act as different roles and probe access; any leak blocks the release. --- # The future of LLM review > A rule needs one label and a confidence; a chat model bills a page of thinking and prose to get there. Point the rules' questions at a context decision model such as Jev or SubQ Snap. When a rule asks a model a question (`tool "llm" "review"`), it needs one label and a confidence. A chat model answers with thinking and prose — billed as output tokens — and the rule reads one word of it. A context decision model (CDM) — given the context and a fixed list of answers, it returns one answer and a calibrated confidence — bills no output. Two of them: Jev from TypeSafe (https://typesafe.ai/) and SubQ Snap from Subquadratic (https://subq.ai/introducing-subq-snap). ## The math Cost per question = (input tokens × input price + output tokens × output price) / 1M. - Claude Fable 5.1: $10 input / $50 output per 1M tokens - Claude Opus 5.5: $4 input / $20 output per 1M tokens - Claude Sonnet 5.5: $2 input / $10 output per 1M tokens - Jev (TypeSafe): $0.042 input per 1M tokens, output free - SubQ Snap (Subquadratic): $0.05 input per 1M tokens, output free Example: 2,000 PRs a month × 3 questions, 20,000 input and 1,500 output tokens per answer: $1,650 a month on Claude Fable 5.1, $5.04 on Jev (TypeSafe), $6.00 on SubQ Snap (Subquadratic) (at least 275× cheaper). TypeSafe's benchmark for Jev: 0.114s per decision against 8.566s. ## Why typed answers suit rules - The confidence is a gate: act on a sure answer, escalate an unsure one to a human. - Nothing to parse: the answer arrives as the type the rule asked for. - A chat model still fits where you want an explanation — ask it only on the PRs the decision model flagged. An interactive calculator is on the page. --- Free demo or open-source license: fill in the form at https://talooner.dev/#pilot — a 10-day key at once; open-source keys are extended after review. Questions: https://talooner.dev/contact/.