A week ago I launched siftwise: an open-source, self-hosted tool that runs only the tests your change can affect, and tells you whether a failing test is your fault, already broken on main, or just flaky.
If you missed it, the first post explains the idea and how to set up the server and the GitHub Action.
Today v0.2.0 is out. The theme: use it in more places.
pip install --upgrade siftwise # 0.2.0
What's new in v0.2
- 🦊 GitLab CI: the same test selection, failure verdicts and sticky comment as on GitHub, now on merge requests.
- 🤖 5 coding agents tested: Claude Code, GitHub Copilot, Cursor, Codex CLI and Gemini CLI can ask siftwise which tests to run and why a test failed.
- 🔁 Flaky-test re-runs for Go, Jest, Maven and Gradle, not just pytest.
- 🔒 Safer PR comments: a file name in a PR can no longer sneak a link or an @mention into siftwise's comment.
- 🐶 Dogfooding: siftwise now runs on its own CI, collecting live numbers for v0.3.
Like the first post, this one is the whole guide: by the end you can use every new feature.
Before you start
You need a siftwise server. If you don't have one yet, Step 1 of the first post gets one running locally in 5 minutes with Docker:
git clone https://github.com/PramodBasavarajMenasi/siftwise.git
cd siftwise
[ -f .env ] || echo "SIFTWISE_API_TOKEN=$(openssl rand -hex 32)" > .env
docker compose up -d --wait
curl -fsS http://localhost:8000/healthz # {"status":"ok"}
Already on 0.1.0? Upgrading is a drop-in, with no database migration:
cd siftwise && git pull # the compose file now names the 0.2.0 image
docker compose up -d --wait # pulls 0.2.0 and restarts the server
uv tool upgrade siftwise # the CLI; or: pip install --upgrade siftwise
All commands are bash; on Windows, use Git Bash or WSL.
Part 1: connect your coding agent (MCP)
siftwise ships an MCP server with three tools your agent can call:
| Tool | The agent calls it | It gets back |
|---|---|---|
select_tests |
before running tests | the test command, with a reason for each test |
triage_failure |
when tests fail, before fixing | a verdict per failure: new regression, pre-existing, flaky or unknown |
explain |
when something looks odd | one test's history and why it was (not) selected |
What it looks like in practice:
- You: "Fix the bulk discount bug."
- The agent edits
cart.py, callsselect_tests, and runspytest tests/test_cart.pyinstead of the whole suite. - A test fails. It calls
triage_failureand gets new_regression, linked to the file it just changed, so it fixes its own bug. - A failure that's pre_existing (already broken on main) gets left alone instead of "fixed".
In v0.1 I had only tested this with Claude Code. For v0.2 I tested it by hand on Windows in five clients, and all three tools worked in each:
| Client | Config file | How siftwise finds your repo |
|---|---|---|
| Claude Code |
claude mcp add (or .mcp.json) |
the project folder Claude Code tells it |
| GitHub Copilot (VS Code, agent mode) | .vscode/mcp.json |
the open workspace folder |
| Cursor |
~/.cursor/mcp.json or .cursor/mcp.json
|
asks Cursor which folder is open, nothing to configure |
| Codex CLI | ~/.codex/config.toml |
the cwd you set |
| Gemini CLI | ~/.gemini/settings.json |
the cwd you set |
Step 1.1: install the MCP extra
uv tool install 'siftwise[mcp]' # or: pip install 'siftwise[mcp]'
uv tool update-shell # once, if `siftwise` isn't found; then restart the agent
uv tool dir --bin # prints the folder, if you need the full path below
You need two values for every client: your server address (http://localhost:8000 locally) and the API token from the server's .env.
Windows tip: agents don't always see your shell's PATH. If a client can't start
siftwise, use the full path fromuv tool dir --bin, e.g."C:\\Users\\you\\.local\\bin\\siftwise.exe"in JSON, or'C:\Users\you\.local\bin\siftwise.exe'(single quotes) in TOML.
Step 1.2: add it to your client
Claude Code (one command, run inside your repo):
claude mcp add --env SIFTWISE_SERVER=http://localhost:8000 \
--env SIFTWISE_API_TOKEN=your-token --transport stdio siftwise -- siftwise mcp
Check with /mcp in a session.
GitHub Copilot in VS Code: create .vscode/mcp.json in your repo. VS Code asks for the token on first start and stores it securely, so this file is safe to commit:
{
"inputs": [
{ "type": "promptString", "id": "siftwise-token", "description": "siftwise API token", "password": true }
],
"servers": {
"siftwise": {
"type": "stdio",
"command": "siftwise",
"args": ["mcp"],
"env": {
"SIFTWISE_SERVER": "http://localhost:8000",
"SIFTWISE_API_TOKEN": "${input:siftwise-token}"
}
}
}
}
Click Start above "siftwise" in the file, then use Copilot Chat in Agent mode.
Cursor: ~/.cursor/mcp.json (all projects). No repo path needed: siftwise asks Cursor which folder you have open.
{
"mcpServers": {
"siftwise": {
"type": "stdio",
"command": "siftwise",
"args": ["mcp"],
"env": {
"SIFTWISE_SERVER": "http://localhost:8000",
"SIFTWISE_API_TOKEN": "your-token"
}
}
}
}
Check in Cursor's settings (search "MCP"): siftwise should show a green dot and 3 tools.
Codex CLI: ~/.codex/config.toml. Set cwd to your repo:
[mcp_servers.siftwise]
command = "siftwise"
args = ["mcp"]
cwd = "/path/to/your/repo" # Windows: 'C:\Users\you\code\repo'
[mcp_servers.siftwise.env]
SIFTWISE_SERVER = "http://localhost:8000"
SIFTWISE_API_TOKEN = "your-token"
Check with /mcp. If Codex says the model "isn't supported with a ChatGPT account", pick another one with /model; that's Codex's account check, not siftwise.
Gemini CLI: ~/.gemini/settings.json. The token must be in env, because Gemini CLI strips inherited variables with TOKEN in their name:
{
"mcpServers": {
"siftwise": {
"command": "siftwise",
"args": ["mcp"],
"cwd": "/path/to/your/repo",
"env": {
"SIFTWISE_SERVER": "http://localhost:8000",
"SIFTWISE_API_TOKEN": "your-token"
}
}
}
}
Check with /mcp.
Keep tokens out of git. The Cursor, Codex and Gemini files above live in your home folder, outside any repo. Don't put a real token into a config file inside your project.
Windsurf and Cline configs are in docs/mcp-clients.md, but I haven't tested those yet. I've also only tested on Windows so far; reports from macOS and Linux are very welcome.
Step 1.3: tell your agent when to use it
Add this to your CLAUDE.md, AGENTS.md or the client's rules file:
Before running tests, call the siftwise `select_tests` tool and run the command it returns.
When tests fail, call `triage_failure` before fixing anything: fix `new_regression` failures,
re-run `flaky` ones, and leave `pre_existing` ones (already failing on main).
Step 1.4: try it
Make a change in your repo and ask the agent:
| Ask | Expect |
|---|---|
| "Which tests should I run for my change? Use siftwise." | a test command with a reason per test |
| "The tests failed. Ask siftwise before fixing anything." | a verdict per failing test |
"Why does siftwise select <a test>?" |
the test's history and the link to your change |
Your repo needs some recorded history first (a recorded run on main; see Step 2 of the first post), otherwise siftwise safely says "run the full suite".
How siftwise finds your repo
Clients start MCP servers in different folders, so siftwise uses the first of these that works:
- a
repo_paththe agent passes; -
SIFTWISE_REPO_PATHin the config; - the project folder Claude Code sets;
- the folders the client has open (MCP roots);
- the folder the server started in.
If none is a git repo, every tool says so and how to fix it, and select_tests falls back to "run the full suite".
Part 2: GitLab CI
GitLab merge requests now get what GitHub PRs get. Here's the note from a real test MR on gitlab.com, where I broke a discount calculation on purpose:
- Selective: 2 of 4 tests ran.
- The verdict: the failing test is a new regression, high confidence, linked to the changed file.
- One sticky note, updated on every push, not a new comment each time.
-
The gate:
fail-onfailed the pipeline, as configured.
Step 2.1: make your server reachable from GitLab
GitLab's runners can't see your localhost. Run the server somewhere they can reach, behind HTTPS. The first post's Step 4.1 shows a VM with Docker and Caddy. The token travels in a request header, so use https://; siftwise warns if you don't.
Step 2.2: create a GitLab token for the comment
siftwise posts its note through GitLab's API, so it needs a token. Give it the least that works:
- GitLab.com Free: a fine-grained personal access token, limited to this one project, with Work Item create, read and update (MR comments are part of it), Merge Request read, User read (so siftwise only edits its own note), and an expiry date. Ideally it belongs to a dedicated bot user with the Reporter role.
-
Premium, Ultimate or Self-Managed: a project access token with the Reporter role and the
apiscope.
GitLab's built-in CI_JOB_TOKEN can't write MR comments, which is why a separate token is needed.
Step 2.3: add three CI/CD variables
In your project: Settings → CI/CD → Variables → Add variable:
| Key | Value | Visibility |
|---|---|---|
SIFTWISE_SERVER |
https://siftwise.your-domain.com |
visible is fine |
SIFTWISE_TOKEN |
your server's SIFTWISE_API_TOKEN
|
Masked and hidden |
SIFTWISE_GITLAB_TOKEN |
the token from Step 2.2 | Masked and hidden |
Protected or not? Protected variables only reach protected branches, so most MR pipelines wouldn't get the tokens (siftwise then runs the full suite and posts nothing). Unprotected (masked and hidden) works for every MR, but anyone who can push a branch could read them in their pipeline. If everyone with Developer access may use siftwise, go unprotected, and run a separate siftwise server per team or trust boundary. The README's protected or unprotected section has the details.
Step 2.4: add the template to .gitlab-ci.yml
Include the template by its release tag and a hash, so the code that runs with your tokens can't change under you. Get the hash once:
curl -sL https://raw.githubusercontent.com/PramodBasavarajMenasi/siftwise/v0.2.0/gitlab/siftwise.gitlab-ci.yml \
| openssl dgst -sha256 -binary | openssl base64 -A
Then:
include:
- remote: https://raw.githubusercontent.com/PramodBasavarajMenasi/siftwise/v0.2.0/gitlab/siftwise.gitlab-ci.yml
integrity: sha256-PASTE-THE-HASH
workflow: # merge request pipelines for open MRs, branch pipelines otherwise
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
siftwise-select:
extends: .siftwise-select
test:
extends: .siftwise-record
image: python:3.12
script:
- pip install -r requirements.txt
- |
if [ "$SIFTWISE_MODE" = selective ]; then
if [ -s "$SIFTWISE_COMMAND_FILE" ]; then sh "$SIFTWISE_COMMAND_FILE"; else echo "No tests affected."; fi
else
pytest --junitxml=.siftwise-ci/junit/full.xml # your full suite
fi
artifacts:
reports:
junit: .siftwise-ci/junit/*.xml
siftwise-rollup:
extends: .siftwise-rollup
variables:
SIFTWISE_FAIL_ON: new_regression,unknown # optional: fail the pipeline on real regressions
What each job does:
-
siftwise-selectpicks the tests and passes the result to your test job. -
Your test job runs them, and its
after_scriptuploads the results. -
siftwise-rollupgives each failure a verdict, updates the MR note and appliesfail-on.
GIT_DEPTH: "0" (full history) is already set by the template, since GitLab clones only 20 commits by default.
Step 2.5: open a merge request
- Push
.gitlab-ci.ymlto your default branch first. That pipeline recordsmain's history. - Make a change on a branch and open an MR.
- The MR shows the siftwise note, and
.siftwise-ci/in the job artifacts has the full reports.
Using parallel: matrix:? Give each leg a SIFTWISE_VARIANT; the README's GitLab section shows it.
Just want to try it first? examples/gitlab is a tiny project that runs its own siftwise server inside the pipeline, so you need no server at all. Push it to a new private gitlab.com project, add SIFTWISE_GITLAB_TOKEN, and open an MR that breaks shop/cart.py. That's exactly the test behind the screenshot above.
What keeps it safe
- Tokens only come from environment variables, never command arguments. They're redacted from every log line, file and note, and tests check that.
- siftwise only edits a note its own token's user wrote, so nobody can hijack it with a fake one.
- Test names come from the MR's code, so they're shown as code: they can't add links, @mentions or GitLab quick actions.
- Fork MRs get no tokens: they run the full suite, with no note. Never run untrusted forks' pipelines in your project.
-
An MR can't mark its own failures flaky:
known_flakyis read from the MR's base commit.
Part 3: flaky-test re-runs for Go, Jest, Maven and Gradle
When a failure's verdict is unknown, siftwise can re-run just that test. If it passes, the failure is marked flaky (high confidence), so nobody wastes an hour chasing it. In v0.1 this only worked for pytest. Now each failing test gets its own runner:
| Language | What siftwise runs | You need |
|---|---|---|
| Python | pytest <node IDs> |
nothing extra |
| Go |
go test -count=1 -run with the failing test names, per package |
Go; gotestsum optional |
| Jest |
jest --runTestsByPath <file> -t with the full test name |
jest-junit with addFileAttribute: "true"
|
| Java (Maven) |
mvn -Dtest=Class#method test (./mvnw if present) |
nothing extra |
| Java (Gradle) |
gradle test --tests Class.method (./gradlew if present) |
nothing extra |
Tested with JDK 17 and 21, Maven 3.9 and 3.10, and Gradle 8.10 and 9.8; siftwise uses your project's own mvn/gradle (or ./mvnw/./gradlew), so other versions should work too.
Turn it on in the GitHub Action's record step (or SIFTWISE_RERUN_UNKNOWN on GitLab):
- if: always()
uses: PramodBasavarajMenasi/siftwise/actions/record@v0.2.0
with:
server-url: ${{ secrets.SIFTWISE_URL }}
token: ${{ secrets.SIFTWISE_TOKEN }}
rerun-unknown: 2 # re-run unknown failures up to twice
rerun-prefix: npx # how Jest (or pytest, e.g. "uv run") runs in your job
rerun-prefix only applies to pytest and Jest. Go, Maven and Gradle pick their own tool, so a mixed-language repo doesn't break.
Limits: at most 20 tests within 10 minutes; re-runs never turn a red job green; and for Java and Jest the re-run must name tests the way your CI run did (JUnit 5 @DisplayName doesn't match yet). Maven and Gradle re-runs put your CI run's original reports back afterwards, so nothing downstream gets the wrong files.
Part 4: safer PR comments
While building the GitLab note, I noticed the GitHub PR comment didn't escape everything that comes from the PR. A file named like a Markdown link would show up as a real link in siftwise's comment, and a path like pkgs/@acme/... would @mention someone.
Now everything from the PR (test IDs, file paths, variant names) is shown as code, on both GitHub and GitLab. Nothing to configure: upgrade the Action pin to @v0.2.0.
Part 5: dogfooding, siftwise on its own CI
siftwise now runs on its own CI in shadow mode: the full suite still runs every time, and siftwise records what it would have selected and whether that would have caught each failure.
It needs no hosted server: a siftwise server starts inside each CI job, and the history lives in the GitHub Actions cache between runs.
My earlier numbers (2,049 of 2,050 failures caught on rdflib, for example) came from replaying other projects' old CI history. These will be live numbers from real changes, and I'll publish them with v0.3.
What testing on the real thing taught me
-
Mocks aren't enough. Every GitLab API call was mocked in my tests, and they all passed. On real gitlab.com, the pipeline's own job token got
404 Project Not Founddownloading main's artifacts, so the example no longer depends on that. -
Re-running a test can delete other reports. A Gradle
--testsre-run removed the results of every class it didn't re-run. siftwise now backs up and restores them around every Maven and Gradle re-run. -
Every MCP client is a little different: where it starts the server, which environment variables it passes, how it reads JSON schemas. Codex turned optional parameters written as
anyOf [type, null]into plain strings, so every schema got simpler. Testing in the real clients still found three bugs the automated tests had missed.
Troubleshooting
| You see | Fix |
|---|---|
The agent can't start siftwise
|
use the full path from uv tool dir --bin in the config |
| A tool says "not inside a git repository" | open the repo folder itself in the client, or set SIFTWISE_REPO_PATH
|
select_tests always says "full suite" |
no history yet: record a run on main first |
| Codex: "model not supported" | pick another model with /model
|
| Gemini: siftwise says the token is missing | put SIFTWISE_API_TOKEN in the server's env block |
GitLab: no MR note, HTTP 403 in the log |
the token lacks the permissions in Step 2.2 |
| GitLab: full suite on every MR | protected variables don't reach MR branches (see Step 2.3), or the clone is shallow |
Known limits
- MCP is tested on Windows only; Windsurf and Cline are untested.
- GitLab is tested on gitlab.com Free; Self-Managed and Premium are untested.
- Java and Jest re-runs need matching test names (no JUnit 5
@DisplayNameyet). - Full details are in the CHANGELOG.
What's next: v0.3, smarter selection
- Test prioritization: run the tests most likely to fail first.
- Time-budget mode: "the best tests that fit in 5 minutes".
- A real JS/TS import graph for Jest.
- Test selection for Java, not just re-runs.
- Live recall numbers from dogfooding.
Links
- 💻 GitHub: https://github.com/PramodBasavarajMenasi/siftwise
- 📦 PyPI: https://pypi.org/project/siftwise/
- 📝 Release notes: https://github.com/PramodBasavarajMenasi/siftwise/releases/tag/v0.2.0
- 📖 First post (server and GitHub Action setup): https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf
Try it, break it, tell me
This is still one person building in the open. If you try siftwise with GitLab or with your coding agent, tell me how it went in the comments, especially on macOS or Linux, or with Windsurf or Cline. If it picks the wrong tests, please open an issue: misses are the most useful bug reports. And a ⭐ really helps a solo project get seen. 👇

Top comments (0)