<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pramod Basavaraj Menasi</title>
    <description>The latest articles on DEV Community by Pramod Basavaraj Menasi (@pm_22).</description>
    <link>https://dev.to/pm_22</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4197547%2F32a6b482-6732-4b17-896e-89e139e2d974.png</url>
      <title>DEV Community: Pramod Basavaraj Menasi</title>
      <link>https://dev.to/pm_22</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pm_22"/>
    <language>en</language>
    <item>
      <title>siftwise v0.3: riskiest tests first, time budgets, Jest and Java selection, and live dogfooding numbers</title>
      <dc:creator>Pramod Basavaraj Menasi</dc:creator>
      <pubDate>Wed, 11 Nov 2026 04:00:30 +0000</pubDate>
      <link>https://dev.to/pm_22/siftwise-v03-riskiest-tests-first-time-budgets-jest-and-java-selection-and-live-dogfooding-2b0c</link>
      <guid>https://dev.to/pm_22/siftwise-v03-riskiest-tests-first-time-budgets-jest-and-java-selection-and-live-dogfooding-2b0c</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;siftwise&lt;/a&gt;&lt;/strong&gt; is an open-source, self-hosted tool that runs &lt;strong&gt;only the tests your change can affect&lt;/strong&gt;, and tells you whether a failing test is &lt;strong&gt;your fault, already broken on main, or just flaky&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;New here? &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;Post 1&lt;/a&gt; explains the idea and sets up the server and the GitHub Action. &lt;a href="https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f"&gt;Post 2&lt;/a&gt; adds GitLab CI and five coding agents over MCP.&lt;/p&gt;

&lt;p&gt;Today &lt;strong&gt;v0.3.0&lt;/strong&gt; is out. The theme: &lt;strong&gt;smarter selection&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip install --upgrade siftwise   &lt;span class="c"&gt;# 0.3.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's new in v0.3
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;strong&gt;Risk order:&lt;/strong&gt; the tests most likely to fail run first, so a failure shows up early.&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Time budget:&lt;/strong&gt; "the riskiest tests that fit in 5 minutes", for fast feedback.&lt;/li&gt;
&lt;li&gt;🟨 &lt;strong&gt;Jest selection:&lt;/strong&gt; a JS/TS import graph, with no Node.js needed.&lt;/li&gt;
&lt;li&gt;☕ &lt;strong&gt;Java selection&lt;/strong&gt; for Maven and Gradle, including multi-module builds.&lt;/li&gt;
&lt;li&gt;🐶 &lt;strong&gt;Dogfooding:&lt;/strong&gt; siftwise now runs on its own CI on every PR and push, with live numbers.&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Follow-ups and fixes&lt;/strong&gt; from testing v0.2 on real repos, three of which made siftwise silently miss tests.&lt;/li&gt;
&lt;li&gt;🙌 &lt;strong&gt;First outside contributors:&lt;/strong&gt; two of them, the same morning I opened my first &lt;code&gt;good first issue&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like the earlier posts, this one is the whole guide, with the real numbers, including the ones that didn't go my way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Planned vs shipped
&lt;/h2&gt;

&lt;p&gt;At the end of &lt;a href="https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f"&gt;post 2&lt;/a&gt; I promised five things for v0.3. Here's what happened:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Promised in v0.2&lt;/th&gt;
&lt;th&gt;Shipped in v0.3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run the tests most likely to fail first&lt;/td&gt;
&lt;td&gt;✅ Risk order (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/42" rel="noopener noreferrer"&gt;#42&lt;/a&gt;), plus import depth on top (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/48" rel="noopener noreferrer"&gt;#48&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-budget mode&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;--budget 5m&lt;/code&gt; everywhere: CLI, Action, GitLab, MCP (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/43" rel="noopener noreferrer"&gt;#43&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A real JS/TS import graph for Jest&lt;/td&gt;
&lt;td&gt;✅ (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/44" rel="noopener noreferrer"&gt;#44&lt;/a&gt;), plus computed imports like &lt;code&gt;import.meta.glob&lt;/code&gt; (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/51" rel="noopener noreferrer"&gt;#51&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test selection for Java&lt;/td&gt;
&lt;td&gt;✅ Maven and Gradle (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/45" rel="noopener noreferrer"&gt;#45&lt;/a&gt;), plus module build-file scoping, one command per build and a first Gradle replay (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/54" rel="noopener noreferrer"&gt;#54&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live recall numbers from dogfooding&lt;/td&gt;
&lt;td&gt;⚠️ Live &lt;strong&gt;time-saved&lt;/strong&gt; numbers, yes. Live &lt;strong&gt;recall&lt;/strong&gt;, not yet: none of siftwise's own runs has failed so far, so there's nothing to catch or miss. More in Part 5.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Not planned, but shipped too:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the v0.2 follow-ups (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/46" rel="noopener noreferrer"&gt;#46&lt;/a&gt;): Jest without &lt;code&gt;addFileAttribute&lt;/code&gt;, JUnit 5 &lt;code&gt;@DisplayName&lt;/code&gt; re-runs, Go workspaces and nested modules, and the GitLab Tests tab;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go list&lt;/code&gt; no longer edits go.mod (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/52" rel="noopener noreferrer"&gt;#52&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;six bug fixes, three of which made siftwise silently miss tests (Part 6);&lt;/li&gt;
&lt;li&gt;new evals: time to first failure, recall within a budget, and a Gradle replay.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Upgrading from 0.2
&lt;/h2&gt;

&lt;p&gt;v0.3 has &lt;strong&gt;two database migrations&lt;/strong&gt; (0011 and 0012; both only add columns). The server runs them on start, so with Docker it's the usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; siftwise &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull                       &lt;span class="c"&gt;# the compose file now names the 0.3.0 image&lt;/span&gt;
docker compose up -d --wait                   &lt;span class="c"&gt;# pulls 0.3.0, migrates, restarts&lt;/span&gt;
uv tool upgrade siftwise                      &lt;span class="c"&gt;# the CLI; or: pip install --upgrade siftwise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then move your Action pins to &lt;code&gt;@v0.3.0&lt;/code&gt;, and the GitLab template to &lt;code&gt;v0.3.0&lt;/code&gt; with its new &lt;code&gt;integrity&lt;/code&gt; hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update the server and the clients together.&lt;/strong&gt; A new CLI talking to a 0.2 server still works: it sends the request again without the new fields and gets 0.2's selection. The exception is a &lt;strong&gt;time budget&lt;/strong&gt;, which can't be dropped silently: the Action, GitLab and MCP fall back to the full suite with a warning, and &lt;code&gt;siftwise select --budget&lt;/code&gt; exits with an error. An old CLI works with a new server unchanged.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: risk order, riskiest tests first
&lt;/h2&gt;

&lt;p&gt;In v0.2, siftwise picked the right tests but ran them in path order. If the one that fails is last, you still wait for everything before it.&lt;/p&gt;

&lt;p&gt;Now every selected test gets a &lt;strong&gt;score&lt;/strong&gt;, a &lt;strong&gt;rank&lt;/strong&gt; and a &lt;strong&gt;reason&lt;/strong&gt;, and the command runs the riskiest first. The score is a fixed, documented formula, with no machine learning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = link + 3 × recency + 2 × failure rate   (recency and rate discounted if the test is flaky)
        + 2 if the test is new

link     = 6 if the change reaches the test directly, 3 through other files, 0 by history only
recency  = halves every 7 days since the test last failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a test whose file imports the code you changed, and that failed last week, goes to the front. Ties go to the faster test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing to configure:&lt;/strong&gt; it's on by default. &lt;code&gt;siftwise select --json&lt;/code&gt; shows each test's &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;rank&lt;/code&gt; and &lt;code&gt;rank_reason&lt;/code&gt;; the PR comment and GitLab note name the three riskiest tests. To keep the old order: &lt;code&gt;--order default&lt;/code&gt;, the Action's &lt;code&gt;order: default&lt;/code&gt;, &lt;code&gt;SIFTWISE_ORDER: default&lt;/code&gt; on GitLab, or &lt;code&gt;order: "default"&lt;/code&gt; over MCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The order never changes which tests run,&lt;/strong&gt; only when. How much it's respected depends on the runner:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runner&lt;/th&gt;
&lt;th&gt;What the order guarantees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pytest&lt;/td&gt;
&lt;td&gt;arguments run in the order given (unless a plugin like pytest-xdist or pytest-randomly reorders them)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;packages run one after another, riskiest first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest&lt;/td&gt;
&lt;td&gt;a hint: Jest runs files in parallel and may reorder them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maven&lt;/td&gt;
&lt;td&gt;modules run in Maven's reactor (dependency) order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Does it help? The numbers
&lt;/h3&gt;

&lt;p&gt;I replayed real CI history, evaluating each run using only the history from before it. The number is &lt;strong&gt;how much of a selective run passes before the first failure shows&lt;/strong&gt;, as a median (lower is better):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Default order&lt;/th&gt;
&lt;th&gt;Risk order&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RDFLib/rdflib (Python), 17 runs&lt;/td&gt;
&lt;td&gt;37.3% of the time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-substrate/substrate (Go), 19 runs&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On rdflib, the failure typically shows in the first 1.4% of the run instead of after a third of it. Recall didn't change: the same tests ran, in a different order.&lt;/p&gt;

&lt;p&gt;I also tried a "cost-aware" order (risk per second of estimated test time). It wasn't clearly better on both repos and was worse by test count, so it didn't ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  The feature that made the numbers worse
&lt;/h3&gt;

&lt;p&gt;v0.3 also adds &lt;strong&gt;import depth&lt;/strong&gt;: a test file that imports the changed code &lt;em&gt;itself&lt;/em&gt; counts as a direct link (6), above one that reaches it through other files (3).&lt;/p&gt;

&lt;p&gt;My first version did this for every language, and &lt;strong&gt;substrate got worse&lt;/strong&gt;: the median went from 23.3% to 34.4% (by test count), with the first failure coming later in 6 runs and earlier in only 1.&lt;/p&gt;

&lt;p&gt;Going failure by failure showed why. Go's graph is per &lt;strong&gt;package&lt;/strong&gt;, and in Go most dependents import the changed package directly: 83% of the import-linked packages were at depth 1. So "direct" lifted almost every package above the e2e suites that a &lt;code&gt;[[depends]]&lt;/code&gt; rule selects, and those were the ones failing.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;Go packages aren't promoted by depth&lt;/strong&gt;; Python, Jest and Java test files are. substrate is back to exactly its earlier numbers. I chose that rule because a test file's imports say much more precisely what a test uses than a package's do, not because it scored best on two repos. Honestly, import depth didn't move rdflib's medians either (only the mean improved a little), so I'm not claiming a speed-up from it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: time budget
&lt;/h2&gt;

&lt;p&gt;Sometimes even the selection is too big for the feedback you want right now. Set a budget, and siftwise runs the &lt;strong&gt;riskiest tests whose estimated time fits&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;siftwise &lt;span class="k"&gt;select&lt;/span&gt; --repo acme/shop --base origin/main --budget 5m   &lt;span class="c"&gt;# 90s, 5m, 1h30m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Action&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;budget: 5m&lt;/code&gt; (outputs &lt;code&gt;budget-limited&lt;/code&gt;, &lt;code&gt;budget-left-out&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_BUDGET: 5m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP &lt;code&gt;select_tests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;budget&lt;/code&gt; argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;budget_seconds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's off by default. Two things matter before you use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A budget is an estimate, not a cap.&lt;/strong&gt; siftwise uses each test's mean duration from history, so a run can take longer. Keep a real CI timeout if you need a hard limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A budget-limited run is not a complete check,&lt;/strong&gt; and siftwise says so plainly: the PR comment shows &lt;em&gt;"budget-limited (estimated 4m 50s of a 5m target): N tests left out"&lt;/em&gt;. Run the full selection before merging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In shadow mode, a failure the budget left out is counted as a miss with the cause &lt;code&gt;budget&lt;/code&gt;, so you can measure what a budget costs you on your own repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much a budget catches,&lt;/strong&gt; on the same replays: runs whose failure is still caught at a budget of 10%, 25% and 50% of each run's time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;10%&lt;/th&gt;
&lt;th&gt;25%&lt;/th&gt;
&lt;th&gt;50%&lt;/th&gt;
&lt;th&gt;No budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rdflib&lt;/td&gt;
&lt;td&gt;22 of 31&lt;/td&gt;
&lt;td&gt;24 of 31&lt;/td&gt;
&lt;td&gt;28 of 31&lt;/td&gt;
&lt;td&gt;30 of 31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substrate&lt;/td&gt;
&lt;td&gt;34 of 72&lt;/td&gt;
&lt;td&gt;43 of 72&lt;/td&gt;
&lt;td&gt;54 of 72&lt;/td&gt;
&lt;td&gt;71 of 72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On rdflib, a 10% budget still catches most failures. On substrate, it catches under half. So a budget is great for a quick first signal, and not a replacement for the full selection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Jest selection
&lt;/h2&gt;

&lt;p&gt;Until now, siftwise matched Jest tests only by file name (&lt;code&gt;foo.ts&lt;/code&gt; → &lt;code&gt;foo.test.ts&lt;/code&gt;), so a change most tests reach through imports ran the whole suite. Now siftwise reads your JS/TS imports and also runs every test file that imports the changed file, at any depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it understands, without running Node:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;relative imports, &lt;code&gt;index&lt;/code&gt; files and TypeScript's &lt;code&gt;.js&lt;/code&gt; → &lt;code&gt;.ts&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;tsconfig/jsconfig &lt;code&gt;paths&lt;/code&gt; and &lt;code&gt;baseUrl&lt;/code&gt; (with &lt;code&gt;extends&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;npm, yarn, pnpm and lerna workspace packages;&lt;/li&gt;
&lt;li&gt;a JSON Jest &lt;code&gt;moduleNameMapper&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;JSON and other files a test imports;&lt;/li&gt;
&lt;li&gt;computed imports with a fixed folder: &lt;code&gt;import(`./locales/${lang}.json`)&lt;/code&gt;, &lt;code&gt;require('./plugins/' + name)&lt;/code&gt;, Vite's &lt;code&gt;import.meta.glob&lt;/code&gt; and webpack's &lt;code&gt;require.context&lt;/code&gt; count as imports of every file they could load (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/51" rel="noopener noreferrer"&gt;#51&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When it can't see, it plays safe:&lt;/strong&gt; a test that reaches an import it can't bound (like &lt;code&gt;require(somePath)&lt;/code&gt;) runs for every JS change, and changes to tsconfig, Jest, Babel or SWC configs, root manifests or lockfiles run the full suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New test files run first,&lt;/strong&gt; by path, once siftwise can tell where Jest expects them. On jestjs/jest (2,272 JS/TS files), the graph builds in about 2 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3.1: let siftwise know each test's file
&lt;/h3&gt;

&lt;p&gt;siftwise learns which file each test lives in from the JUnit report. Add &lt;code&gt;jest-junit&lt;/code&gt;, ideally with &lt;code&gt;addFileAttribute&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm install --save-dev jest-junit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"jest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"reporters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"jest-junit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"addFileAttribute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New in v0.3:&lt;/strong&gt; without &lt;code&gt;addFileAttribute&lt;/code&gt;, &lt;code&gt;record&lt;/code&gt; searches the checkout for the one test file whose string literals match the suite's test names, and adds it to the report. That works for most files, but titles built from variables can't be matched, so setting the attribute is still the reliable option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3.2: the workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;siftwise&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/select@v0.3.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.mode }}&lt;/span&gt;
          &lt;span class="na"&gt;COMMAND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.command }}&lt;/span&gt;
          &lt;span class="na"&gt;JEST_JUNIT_OUTPUT_DIR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.junit-dir }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;export PATH="$PWD/node_modules/.bin:$PATH"&lt;/span&gt;   &lt;span class="c1"&gt;# the command starts with `jest`
&lt;/span&gt;          &lt;span class="s"&gt;if [ "$MODE" = selective ]; then&lt;/span&gt;
            &lt;span class="s"&gt;if [ -n "$COMMAND" ]; then eval "$COMMAND"; else echo "No tests affected."; fi&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;jest&lt;/span&gt;   &lt;span class="c1"&gt;# your normal full suite
&lt;/span&gt;          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/record@v0.3.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JEST_JUNIT_OUTPUT_DIR&lt;/code&gt; puts the report where &lt;code&gt;record&lt;/code&gt; looks for it. As always, pass the command through &lt;code&gt;env:&lt;/code&gt;, never as &lt;code&gt;${{ }}&lt;/code&gt; inside &lt;code&gt;run:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An honest gap:&lt;/strong&gt; there's no recall number for Jest yet. Replays need old CI runs that uploaded JUnit with file paths, and no public Jest repo I found does that. I validated the graph by hand on jestjs/jest. If you run siftwise in shadow mode on a JS repo, I'd love your numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Java selection, Maven and Gradle
&lt;/h2&gt;

&lt;p&gt;In v0.2, Java tests were recorded and re-run but always ran in full. Now siftwise reads your build &lt;strong&gt;without running Maven, Gradle or a JVM&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the modules from &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;settings.gradle[.kts]&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a static graph of which test classes use which classes (imports, same-package names, fully-qualified names, &lt;code&gt;Class.forName&lt;/code&gt; strings), across modules;&lt;/li&gt;
&lt;li&gt;name matches: changing &lt;code&gt;Foo.java&lt;/code&gt; selects &lt;code&gt;FooTest&lt;/code&gt;, &lt;code&gt;FooTests&lt;/code&gt;, &lt;code&gt;TestFoo&lt;/code&gt; and &lt;code&gt;FooIT&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a module's &lt;code&gt;src/test/resources&lt;/code&gt; runs that module's tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One command per build&lt;/strong&gt; where that's safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./mvnw -B -pl core,api -am &lt;span class="nb"&gt;test&lt;/span&gt; -Dtest=CartTest,MoneyTest            &lt;span class="c"&gt;# Maven&lt;/span&gt;
./gradlew :core:test --tests CartTest :api:test --tests MoneyTest   &lt;span class="c"&gt;# Gradle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build files are scoped too&lt;/strong&gt; (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/54" rel="noopener noreferrer"&gt;#54&lt;/a&gt;): a change to a module's own &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;build.gradle[.kts]&lt;/code&gt; runs the tests of that module and of every module that depends on it. The root build file, &lt;code&gt;settings.gradle&lt;/code&gt;, version catalogs, wrappers, parent and BOM POMs still run the full suite, because they can affect everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it can't see, the full suite runs:&lt;/strong&gt; Kotlin, Groovy or Scala sources, custom source folders, code-generating annotation processors, or a build it can't read. Reflection (Spring scanning, &lt;code&gt;ServiceLoader&lt;/code&gt;) needs a &lt;code&gt;[[depends]]&lt;/code&gt; rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4.1: the workflow
&lt;/h3&gt;

&lt;p&gt;The only difference from Python is where the reports live, so point &lt;code&gt;record&lt;/code&gt; at them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temurin&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;siftwise&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/select@v0.3.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.mode }}&lt;/span&gt;
          &lt;span class="na"&gt;COMMAND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.command }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$MODE" = selective ]; then&lt;/span&gt;
            &lt;span class="s"&gt;if [ -n "$COMMAND" ]; then eval "$COMMAND"; else echo "No tests affected."; fi&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;./mvnw -B test&lt;/span&gt;   &lt;span class="c1"&gt;# or: ./gradlew test
&lt;/span&gt;          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/record@v0.3.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"**/target/surefire-reports/TEST-*.xml **/build/test-results/test/*.xml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The numbers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dropwizard/dropwizard&lt;/strong&gt; (Maven, 42 modules), 148 PR runs: &lt;strong&gt;74 of 74 failures caught&lt;/strong&gt;. In the 22 selective runs, siftwise skipped a median &lt;strong&gt;90.6%&lt;/strong&gt; of the estimated test time (26% to 93%).&lt;/li&gt;
&lt;li&gt;The catch: most of dropwizard's PRs are dependency bumps to its BOM, which correctly run everything. So over &lt;strong&gt;all&lt;/strong&gt; 148 runs, the time saved is much smaller: 11.9%. Module build-file scoping moved 5 more runs from full to selective. A repo with more code changes and fewer bumps would save more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;naver/fixture-monkey&lt;/strong&gt; (Gradle): 3 of 3 failures caught. That's a small history, so treat it as a first check, not proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dropwizard replay also found a real bug: Surefire writes a class-level result with an empty test name for a JUnit 5 &lt;code&gt;@Nested&lt;/code&gt; class, and siftwise rejected the &lt;strong&gt;whole upload&lt;/strong&gt; for it. Every Windows leg of dropwizard's CI history (781 uploads) was rejected before the fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: dogfooding, siftwise on its own CI
&lt;/h2&gt;

&lt;p&gt;This was the last promise from post 2, and the one I care most about. Replays of other projects' history are useful, but they're replays. Dogfooding is siftwise running &lt;strong&gt;live on real changes, as they happen&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Every siftwise PR and push to &lt;code&gt;main&lt;/code&gt; runs a &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/.github/workflows/dogfood.yml" rel="noopener noreferrer"&gt;Dogfood workflow&lt;/a&gt; in &lt;strong&gt;shadow mode&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A siftwise server starts inside the CI job,&lt;/strong&gt; on the runner's own Postgres, with a random per-job token. There's no hosted server and nothing to pay for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History comes from the GitHub Actions cache.&lt;/strong&gt; The job restores the database from main's latest cache entry, and saves a new one at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;siftwise stores what it &lt;em&gt;would&lt;/em&gt; have selected,&lt;/strong&gt; then the &lt;strong&gt;full suite runs anyway&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It compares:&lt;/strong&gt; for every failure, would the selection have caught it? How much time would it have saved?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each run uploads a small summary file.&lt;/strong&gt; &lt;code&gt;scripts/dogfood_report.py&lt;/code&gt; adds them all up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It gates nothing: no &lt;code&gt;fail-on&lt;/code&gt;, no PR comment, and every siftwise step fails open. So it can never block my own CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safety part:&lt;/strong&gt; a PR's cache entry is scoped to that PR, so a PR (including one from a fork) can read main's history but can never write into it. Only main's own runs build the history. For the numbers, I also leave out the fork PRs' runs, because code from the fork wrote their summaries; each of those changes still counts once, as its push to main.&lt;/p&gt;

&lt;h3&gt;
  
  
  The numbers (26 Oct to 9 Nov 2026)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflow runs&lt;/td&gt;
&lt;td&gt;36 (17 PR runs, 19 pushes to main)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow runs counted&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;32&lt;/strong&gt; (the first 2 had no history yet; the 2 fork runs are left out)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Would have been selective&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 of 32&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests selected in those 9&lt;/td&gt;
&lt;td&gt;a median 39% of the known tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time saved&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;14.7%&lt;/strong&gt; of 23.0 minutes over the 32 runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failures&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;none&lt;/strong&gt;: every run's suite passed, so recall can't be measured yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly time saved&lt;/td&gt;
&lt;td&gt;21%, 7% and 34%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Read these as early numbers.&lt;/strong&gt; A merged PR usually counts twice (once as its PR run, once as its push to main), so the 9 selective runs are really about &lt;strong&gt;6 distinct changes&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What dogfooding taught me
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why 23 of 32 ran in full:&lt;/strong&gt; 20 changed a build or config file (the &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; in 6 runs each, workflows, and my &lt;strong&gt;test fixtures' own&lt;/strong&gt; &lt;code&gt;build.gradle.kts&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt; and &lt;code&gt;jest.config.ts&lt;/code&gt;), and 3 changed a file no test reaches (like &lt;code&gt;glama.json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fixture finding is the most useful one.&lt;/strong&gt; siftwise's tests include tiny sample Gradle and TypeScript projects. Their build files are test data, but siftwise treats them like real build files and runs everything. Scoping them safely is the first item for v0.4. I'd never have found this from replays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each run now records the risk rank of every failure,&lt;/strong&gt; so once a dogfood run does fail, the report shows not just "caught or missed" but how early risk order would have surfaced it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You can do the same on your repo:&lt;/strong&gt; the Action's &lt;code&gt;shadow&lt;/code&gt; input (&lt;code&gt;always&lt;/code&gt;, or a sample like &lt;code&gt;10%&lt;/code&gt;) runs the full suite and reports what siftwise would have caught or missed, before you trust it with anything. Details: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise#shadow-mode" rel="noopener noreferrer"&gt;Shadow mode&lt;/a&gt; in the README.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: v0.2 follow-ups and fixes
&lt;/h2&gt;

&lt;p&gt;Things I found while testing v0.2 on real projects (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/46" rel="noopener noreferrer"&gt;#46&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jest without &lt;code&gt;addFileAttribute&lt;/code&gt;:&lt;/strong&gt; selection and re-runs now work without it (Part 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JUnit 5 &lt;code&gt;@DisplayName&lt;/code&gt;&lt;/strong&gt; and parameterized test names map back to their methods, so Gradle and phrased-Maven re-runs work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go workspaces (&lt;code&gt;go.work&lt;/code&gt;) and nested modules&lt;/strong&gt; work for selection, commands and re-runs.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;GitLab template&lt;/strong&gt; now declares its JUnit reports, which the pipeline's Tests tab reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fixes&lt;/strong&gt;, three of which (🔴) made siftwise silently miss tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 &lt;strong&gt;Nested Go modules:&lt;/strong&gt; a nested module's dependents were missed, and its tests ran from the root, where Go can't build them. Now they run as &lt;code&gt;go test -C &amp;lt;module&amp;gt; ./pkg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Missed JS edges:&lt;/strong&gt; &lt;code&gt;import('./a/' + x)&lt;/code&gt;, &lt;code&gt;import.meta.glob&lt;/code&gt; and &lt;code&gt;require.context&lt;/code&gt; created no edges, so changing a file they load didn't select the tests that load it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;go list&lt;/code&gt; could edit go.mod&lt;/strong&gt; when GOFLAGS had &lt;code&gt;-mod=mod&lt;/code&gt;. It now runs read-only (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/52" rel="noopener noreferrer"&gt;#52&lt;/a&gt;). On Go 1.15 or older, set &lt;code&gt;GOFLAGS=-mod=readonly&lt;/code&gt; yourself; making siftwise do it is an open good first issue (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues/67" rel="noopener noreferrer"&gt;#67&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java uploads rejected&lt;/strong&gt; for a &lt;code&gt;@Nested&lt;/code&gt; class's result (the dropwizard bug above).&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Same-named Java test classes&lt;/strong&gt; in two modules: a change to one module's test resources ran only the other module's copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deleted Jest test file&lt;/strong&gt; was still named in the &lt;code&gt;jest&lt;/code&gt; command, which then failed with "No tests found".&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What building v0.3 taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the features you're sure about.&lt;/strong&gt; Import depth was the "obviously better" change, and it made one repo worse. Without the replay, I'd have shipped it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your dependencies change under you.&lt;/strong&gt; On release day, a new Typer version started requiring pydantic, which broke the check that keeps the light CLI install small. siftwise's code didn't change; the light install grew from 9 MB to 18 MB, and the check now verifies that siftwise itself never imports server code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;good first issue&lt;/code&gt; works.&lt;/strong&gt; I labelled a one-line bug, and by the same morning two people had sent fixes. One fixed it; the other added the test that stops it from coming back. Both got merged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Thanks to &lt;strong&gt;&lt;a href="https://github.com/bfs2021" rel="noopener noreferrer"&gt;@bfs2021&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/pull/62" rel="noopener noreferrer"&gt;#62&lt;/a&gt;) and &lt;strong&gt;&lt;a href="https://github.com/mursalhayat1" rel="noopener noreferrer"&gt;@mursalhayat1&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/pull/63" rel="noopener noreferrer"&gt;#63&lt;/a&gt;), siftwise's first outside contributors.&lt;/li&gt;
&lt;li&gt;siftwise's MCP server is listed in the &lt;strong&gt;&lt;a href="https://github.com/hashgraph-online/hol-guard" rel="noopener noreferrer"&gt;HOL Guard&lt;/a&gt;&lt;/strong&gt; MCP server catalog (opt-in, after a review of its code and disclosures) and on &lt;strong&gt;&lt;a href="https://glama.ai/mcp/servers" rel="noopener noreferrer"&gt;Glama&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jest tests always run in full&lt;/td&gt;
&lt;td&gt;add &lt;code&gt;addFileAttribute: "true"&lt;/code&gt; to jest-junit (Step 3.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jest: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;put &lt;code&gt;node_modules/.bin&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt;, as in Step 3.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java tests never selected&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;record&lt;/code&gt; didn't find the reports: set its &lt;code&gt;junit&lt;/code&gt; input (Step 4.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every Java PR runs the full suite&lt;/td&gt;
&lt;td&gt;it changed a root or parent build file, which runs everything by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--budget&lt;/code&gt; fails with a server error&lt;/td&gt;
&lt;td&gt;the server is still on 0.2: upgrade it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The order looks wrong in Jest&lt;/td&gt;
&lt;td&gt;Jest reorders files across its workers; the order is a hint there&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Known limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jest:&lt;/strong&gt; no recall number yet; &lt;code&gt;jest.config.js&lt;/code&gt; can't be read without Node (tsconfig and relative paths still resolve); a custom &lt;code&gt;testMatch&lt;/code&gt; isn't read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java:&lt;/strong&gt; Kotlin, Groovy and Scala tests fall back to the full suite; Maven runs modules in reactor order, not risk order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget:&lt;/strong&gt; an estimate, not a cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dogfooding:&lt;/strong&gt; 32 runs and no failures yet, so no live recall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP&lt;/strong&gt; is tested on Windows only; &lt;strong&gt;GitLab&lt;/strong&gt; on gitlab.com Free only.&lt;/li&gt;
&lt;li&gt;Everything else: the &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: v0.4
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Safe scoping for build files inside test fixtures (the dogfooding finding).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-mod=readonly&lt;/code&gt; for older Go versions.&lt;/li&gt;
&lt;li&gt;More recall numbers: a Jest replay, more Gradle history, and the first live recall from dogfooding.&lt;/li&gt;
&lt;li&gt;More &lt;code&gt;good first issue&lt;/code&gt;s. If you'd like to contribute, &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues" rel="noopener noreferrer"&gt;start here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 PyPI: &lt;a href="https://pypi.org/project/siftwise/" rel="noopener noreferrer"&gt;https://pypi.org/project/siftwise/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 Release notes: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/releases/tag/v0.3.0" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise/releases/tag/v0.3.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📊 All eval numbers and method: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/docs/eval-results.md" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise/blob/main/docs/eval-results.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 Post 1 (server and GitHub Action): &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 Post 2 (GitLab CI and coding agents): &lt;a href="https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f"&gt;https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, break it, tell me
&lt;/h2&gt;

&lt;p&gt;If you have a Jest or Java repo, try siftwise in shadow mode and tell me what it would have caught or missed: that's exactly the data v0.3 is missing. Misses make the best &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues" rel="noopener noreferrer"&gt;bug reports&lt;/a&gt;. And a ⭐ helps a small project get seen. 👇&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>testing</category>
      <category>devops</category>
      <category>java</category>
    </item>
    <item>
      <title>siftwise v0.2: run it on GitLab CI and with 5 coding agents (full setup guide)</title>
      <dc:creator>Pramod Basavaraj Menasi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:27:40 +0000</pubDate>
      <link>https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f</link>
      <guid>https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; v0.3 is out: riskiest tests first, time budgets, Jest and Java (Maven, Gradle) selection, and live dogfooding numbers. &lt;a href="https://dev.to/pm_22/siftwise-v03-riskiest-tests-first-time-budgets-jest-and-java-selection-and-live-dogfooding-2b0c"&gt;Read the v0.3 guide →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A week ago I launched &lt;strong&gt;&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;siftwise&lt;/a&gt;&lt;/strong&gt;: an open-source, self-hosted tool that runs &lt;strong&gt;only the tests your change can affect&lt;/strong&gt;, and tells you whether a failing test is &lt;strong&gt;your fault, already broken on main, or just flaky&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you missed it, &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;the first post&lt;/a&gt; explains the idea and how to set up the server and the GitHub Action.&lt;/p&gt;

&lt;p&gt;Today &lt;strong&gt;v0.2.0&lt;/strong&gt; is out. The theme: &lt;strong&gt;use it in more places&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip install --upgrade siftwise   &lt;span class="c"&gt;# 0.2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's new in v0.2
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🦊 &lt;strong&gt;GitLab CI:&lt;/strong&gt; the same test selection, failure verdicts and sticky comment as on GitHub, now on merge requests.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;5 coding agents tested:&lt;/strong&gt; Claude Code, GitHub Copilot, Cursor, Codex CLI and Gemini CLI can ask siftwise which tests to run and why a test failed.&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Flaky-test re-runs for Go, Jest, Maven and Gradle,&lt;/strong&gt; not just pytest.&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Safer PR comments:&lt;/strong&gt; a file name in a PR can no longer sneak a link or an @mention into siftwise's comment.&lt;/li&gt;
&lt;li&gt;🐶 &lt;strong&gt;Dogfooding:&lt;/strong&gt; siftwise now runs on its own CI, collecting live numbers for v0.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like the first post, this one is the whole guide: by the end you can use every new feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You need a &lt;strong&gt;siftwise server&lt;/strong&gt;. If you don't have one yet, Step 1 of the &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;first post&lt;/a&gt; gets one running locally in 5 minutes with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/PramodBasavarajMenasi/siftwise.git
&lt;span class="nb"&gt;cd&lt;/span&gt; siftwise
&lt;span class="o"&gt;[&lt;/span&gt; -f .env &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SIFTWISE_API_TOKEN=$(openssl rand -hex 32)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up -d --wait
curl -fsS http://localhost:8000/healthz   &lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Already on 0.1.0?&lt;/strong&gt; Upgrading is a drop-in, with no database migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; siftwise &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull                       &lt;span class="c"&gt;# the compose file now names the 0.2.0 image&lt;/span&gt;
docker compose up -d --wait                   &lt;span class="c"&gt;# pulls 0.2.0 and restarts the server&lt;/span&gt;
uv tool upgrade siftwise                      &lt;span class="c"&gt;# the CLI; or: pip install --upgrade siftwise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All commands are bash; on Windows, use Git Bash or WSL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: connect your coding agent (MCP)
&lt;/h2&gt;

&lt;p&gt;siftwise ships an MCP server with three tools your agent can call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;The agent calls it&lt;/th&gt;
&lt;th&gt;It gets back&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select_tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;before running tests&lt;/td&gt;
&lt;td&gt;the test command, with a reason for each test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;triage_failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when tests fail, before fixing&lt;/td&gt;
&lt;td&gt;a verdict per failure: new regression, pre-existing, flaky or unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when something looks odd&lt;/td&gt;
&lt;td&gt;one test's history and why it was (not) selected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it looks like in practice:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You: &lt;em&gt;"Fix the bulk discount bug."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The agent edits &lt;code&gt;cart.py&lt;/code&gt;, calls &lt;code&gt;select_tests&lt;/code&gt;, and runs &lt;code&gt;pytest tests/test_cart.py&lt;/code&gt; instead of the whole suite.&lt;/li&gt;
&lt;li&gt;A test fails. It calls &lt;code&gt;triage_failure&lt;/code&gt; and gets &lt;strong&gt;new_regression&lt;/strong&gt;, linked to the file it just changed, so it fixes its own bug.&lt;/li&gt;
&lt;li&gt;A failure that's &lt;strong&gt;pre_existing&lt;/strong&gt; (already broken on main) gets left alone instead of "fixed".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In v0.1 I had only tested this with Claude Code. For v0.2 I tested it by hand &lt;strong&gt;on Windows&lt;/strong&gt; in five clients, and all three tools worked in each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Config file&lt;/th&gt;
&lt;th&gt;How siftwise finds your repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude mcp add&lt;/code&gt; (or &lt;code&gt;.mcp.json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;the project folder Claude Code tells it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;GitHub Copilot&lt;/strong&gt; (VS Code, agent mode)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.vscode/mcp.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the open workspace folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; or &lt;code&gt;.cursor/mcp.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;asks Cursor which folder is open, nothing to configure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex/config.toml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;cwd&lt;/code&gt; you set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.gemini/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;cwd&lt;/code&gt; you set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 1.1: install the MCP extra
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool install &lt;span class="s1"&gt;'siftwise[mcp]'&lt;/span&gt;   &lt;span class="c"&gt;# or: pip install 'siftwise[mcp]'&lt;/span&gt;
uv tool update-shell              &lt;span class="c"&gt;# once, if `siftwise` isn't found; then restart the agent&lt;/span&gt;
uv tool dir --bin                 &lt;span class="c"&gt;# prints the folder, if you need the full path below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need two values for every client: your server address (&lt;code&gt;http://localhost:8000&lt;/code&gt; locally) and the API token from the server's &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Windows tip:&lt;/strong&gt; agents don't always see your shell's PATH. If a client can't start &lt;code&gt;siftwise&lt;/code&gt;, use the full path from &lt;code&gt;uv tool dir --bin&lt;/code&gt;, e.g. &lt;code&gt;"C:\\Users\\you\\.local\\bin\\siftwise.exe"&lt;/code&gt; in JSON, or &lt;code&gt;'C:\Users\you\.local\bin\siftwise.exe'&lt;/code&gt; (single quotes) in TOML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 1.2: add it to your client
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (one command, run inside your repo):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add --env &lt;span class="nv"&gt;SIFTWISE_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 \
    --env &lt;span class="nv"&gt;SIFTWISE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-token --transport stdio siftwise -- siftwise mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check with &lt;code&gt;/mcp&lt;/code&gt; in a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Copilot in VS Code:&lt;/strong&gt; create &lt;code&gt;.vscode/mcp.json&lt;/code&gt; in your repo. VS Code asks for the token on first start and stores it securely, so this file is safe to commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"promptString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise API token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_SERVER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"${input:siftwise-token}"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Start&lt;/strong&gt; above &lt;code&gt;"siftwise"&lt;/code&gt; in the file, then use Copilot Chat in &lt;strong&gt;Agent&lt;/strong&gt; mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor:&lt;/strong&gt; &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; (all projects). No repo path needed: siftwise asks Cursor which folder you have open.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_SERVER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"your-token"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check in Cursor's settings (search "MCP"): siftwise should show a green dot and 3 tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex CLI:&lt;/strong&gt; &lt;code&gt;~/.codex/config.toml&lt;/code&gt;. Set &lt;code&gt;cwd&lt;/code&gt; to your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[mcp_servers.siftwise]&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;
&lt;span class="na"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/your/repo"&lt;/span&gt;   &lt;span class="c1"&gt;# Windows: 'C:\Users\you\code\repo'&lt;/span&gt;

&lt;span class="k"&gt;[mcp_servers.siftwise.env]&lt;/span&gt;
&lt;span class="na"&gt;SIFTWISE_SERVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;
&lt;span class="na"&gt;SIFTWISE_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check with &lt;code&gt;/mcp&lt;/code&gt;. If Codex says the model "isn't supported with a ChatGPT account", pick another one with &lt;code&gt;/model&lt;/code&gt;; that's Codex's account check, not siftwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini CLI:&lt;/strong&gt; &lt;code&gt;~/.gemini/settings.json&lt;/code&gt;. The token &lt;strong&gt;must&lt;/strong&gt; be in &lt;code&gt;env&lt;/code&gt;, because Gemini CLI strips inherited variables with &lt;code&gt;TOKEN&lt;/code&gt; in their name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/your/repo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_SERVER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"your-token"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check with &lt;code&gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep tokens out of git.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Windsurf and Cline&lt;/strong&gt; configs are in &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/docs/mcp-clients.md" rel="noopener noreferrer"&gt;docs/mcp-clients.md&lt;/a&gt;, but I haven't tested those yet. I've also only tested on Windows so far; reports from macOS and Linux are very welcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1.3: tell your agent when to use it
&lt;/h3&gt;

&lt;p&gt;Add this to your &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt; or the client's rules file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Before running tests, call the siftwise &lt;span class="sb"&gt;`select_tests`&lt;/span&gt; tool and run the command it returns.
When tests fail, call &lt;span class="sb"&gt;`triage_failure`&lt;/span&gt; before fixing anything: fix &lt;span class="sb"&gt;`new_regression`&lt;/span&gt; failures,
re-run &lt;span class="sb"&gt;`flaky`&lt;/span&gt; ones, and leave &lt;span class="sb"&gt;`pre_existing`&lt;/span&gt; ones (already failing on main).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1.4: try it
&lt;/h3&gt;

&lt;p&gt;Make a change in your repo and ask the agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ask&lt;/th&gt;
&lt;th&gt;Expect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"Which tests should I run for my change? Use siftwise."&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;a test command with a reason per test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"The tests failed. Ask siftwise before fixing anything."&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;a verdict per failing test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"Why does siftwise select &lt;code&gt;&amp;lt;a test&amp;gt;&lt;/code&gt;?"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;the test's history and the link to your change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your repo needs some recorded history first (a recorded run on &lt;code&gt;main&lt;/code&gt;; see Step 2 of the first post), otherwise siftwise safely says "run the full suite".&lt;/p&gt;

&lt;h3&gt;
  
  
  How siftwise finds your repo
&lt;/h3&gt;

&lt;p&gt;Clients start MCP servers in different folders, so siftwise uses the first of these that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a &lt;code&gt;repo_path&lt;/code&gt; the agent passes;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SIFTWISE_REPO_PATH&lt;/code&gt; in the config;&lt;/li&gt;
&lt;li&gt;the project folder Claude Code sets;&lt;/li&gt;
&lt;li&gt;the folders the client has open (MCP roots);&lt;/li&gt;
&lt;li&gt;the folder the server started in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none is a git repo, every tool says so and how to fix it, and &lt;code&gt;select_tests&lt;/code&gt; falls back to "run the full suite".&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: GitLab CI
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7i4o9llka94dwe48wi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7i4o9llka94dwe48wi4.png" alt="siftwise note on a GitLab merge request: selective, 2 of 4 tests, test_bulk_discount as a new regression with high confidence" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selective:&lt;/strong&gt; 2 of 4 tests ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verdict:&lt;/strong&gt; the failing test is a &lt;strong&gt;new regression, high confidence&lt;/strong&gt;, linked to the changed file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One sticky note,&lt;/strong&gt; updated on every push, not a new comment each time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gate:&lt;/strong&gt; &lt;code&gt;fail-on&lt;/code&gt; failed the pipeline, as configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2.1: make your server reachable from GitLab
&lt;/h3&gt;

&lt;p&gt;GitLab's runners can't see your &lt;code&gt;localhost&lt;/code&gt;. Run the server somewhere they can reach, behind HTTPS. The &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;first post's Step 4.1&lt;/a&gt; shows a VM with Docker and Caddy. The token travels in a request header, so use &lt;code&gt;https://&lt;/code&gt;; siftwise warns if you don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.2: create a GitLab token for the comment
&lt;/h3&gt;

&lt;p&gt;siftwise posts its note through GitLab's API, so it needs a token. Give it the least that works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitLab.com Free:&lt;/strong&gt; a &lt;strong&gt;fine-grained personal access token&lt;/strong&gt;, limited to this one project, with &lt;strong&gt;Work Item&lt;/strong&gt; create, read and update (MR comments are part of it), &lt;strong&gt;Merge Request&lt;/strong&gt; read, &lt;strong&gt;User&lt;/strong&gt; read (so siftwise only edits its own note), and an expiry date. Ideally it belongs to a dedicated bot user with the Reporter role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium, Ultimate or Self-Managed:&lt;/strong&gt; a &lt;strong&gt;project access token&lt;/strong&gt; with the &lt;strong&gt;Reporter&lt;/strong&gt; role and the &lt;strong&gt;&lt;code&gt;api&lt;/code&gt;&lt;/strong&gt; scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitLab's built-in &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; can't write MR comments, which is why a separate token is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.3: add three CI/CD variables
&lt;/h3&gt;

&lt;p&gt;In your project: &lt;strong&gt;Settings → CI/CD → Variables → Add variable&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Visibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_SERVER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://siftwise.your-domain.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;visible is fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your server's &lt;code&gt;SIFTWISE_API_TOKEN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Masked and hidden&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_GITLAB_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the token from Step 2.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Masked and hidden&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Protected or not?&lt;/strong&gt; 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 &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise#protected-or-unprotected-variables" rel="noopener noreferrer"&gt;protected or unprotected&lt;/a&gt; section has the details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.4: add the template to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Include the template &lt;strong&gt;by its release tag and a hash&lt;/strong&gt;, so the code that runs with your tokens can't change under you. Get the hash once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -sL https://raw.githubusercontent.com/PramodBasavarajMenasi/siftwise/v0.2.0/gitlab/siftwise.gitlab-ci.yml \
  &lt;span class="o"&gt;|&lt;/span&gt; openssl dgst -sha256 -binary &lt;span class="o"&gt;|&lt;/span&gt; openssl base64 -A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/PramodBasavarajMenasi/siftwise/v0.2.0/gitlab/siftwise.gitlab-ci.yml&lt;/span&gt;
    &lt;span class="na"&gt;integrity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sha256-PASTE-THE-HASH&lt;/span&gt;

&lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# merge request pipelines for open MRs, branch pipelines otherwise
&lt;/span&gt;  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_SOURCE == "merge_request_event"&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH &amp;amp;&amp;amp; $CI_OPEN_MERGE_REQUESTS&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;

&lt;span class="na"&gt;siftwise-select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.siftwise-select&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.siftwise-record&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.12&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;if [ "$SIFTWISE_MODE" = selective ]; then&lt;/span&gt;
        &lt;span class="s"&gt;if [ -s "$SIFTWISE_COMMAND_FILE" ]; then sh "$SIFTWISE_COMMAND_FILE"; else echo "No tests affected."; fi&lt;/span&gt;
      &lt;span class="s"&gt;else&lt;/span&gt;
        &lt;span class="s"&gt;pytest --junitxml=.siftwise-ci/junit/full.xml&lt;/span&gt;   &lt;span class="c1"&gt;# your full suite
&lt;/span&gt;      &lt;span class="s"&gt;fi&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.siftwise-ci/junit/*.xml&lt;/span&gt;

&lt;span class="na"&gt;siftwise-rollup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.siftwise-rollup&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SIFTWISE_FAIL_ON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;new_regression,unknown&lt;/span&gt;   &lt;span class="c1"&gt;# optional: fail the pipeline on real regressions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each job does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;siftwise-select&lt;/code&gt;&lt;/strong&gt; picks the tests and passes the result to your test job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your test job&lt;/strong&gt; runs them, and its &lt;code&gt;after_script&lt;/code&gt; uploads the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;siftwise-rollup&lt;/code&gt;&lt;/strong&gt; gives each failure a verdict, updates the MR note and applies &lt;code&gt;fail-on&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;GIT_DEPTH: "0"&lt;/code&gt; (full history) is already set by the template, since GitLab clones only 20 commits by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.5: open a merge request
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Push &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; to your default branch first. That pipeline records &lt;code&gt;main&lt;/code&gt;'s history.&lt;/li&gt;
&lt;li&gt;Make a change on a branch and open an MR.&lt;/li&gt;
&lt;li&gt;The MR shows the siftwise note, and &lt;code&gt;.siftwise-ci/&lt;/code&gt; in the job artifacts has the full reports.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using &lt;code&gt;parallel: matrix:&lt;/code&gt;? Give each leg a &lt;code&gt;SIFTWISE_VARIANT&lt;/code&gt;; the README's &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise#gitlab-ci" rel="noopener noreferrer"&gt;GitLab section&lt;/a&gt; shows it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just want to try it first?&lt;/strong&gt; &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/tree/main/examples/gitlab" rel="noopener noreferrer"&gt;examples/gitlab&lt;/a&gt; is a tiny project that runs its own siftwise server &lt;strong&gt;inside&lt;/strong&gt; the pipeline, so you need no server at all. Push it to a new private gitlab.com project, add &lt;code&gt;SIFTWISE_GITLAB_TOKEN&lt;/code&gt;, and open an MR that breaks &lt;code&gt;shop/cart.py&lt;/code&gt;. That's exactly the test behind the screenshot above.&lt;/p&gt;

&lt;h3&gt;
  
  
  What keeps it safe
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens only come from environment variables,&lt;/strong&gt; never command arguments. They're redacted from every log line, file and note, and tests check that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;siftwise only edits a note its own token's user wrote,&lt;/strong&gt; so nobody can hijack it with a fake one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test names come from the MR's code,&lt;/strong&gt; so they're shown as code: they can't add links, @mentions or GitLab quick actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork MRs get no tokens:&lt;/strong&gt; they run the full suite, with no note. Never run untrusted forks' pipelines in your project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MR can't mark its own failures flaky:&lt;/strong&gt; &lt;code&gt;known_flaky&lt;/code&gt; is read from the MR's base commit.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 3: flaky-test re-runs for Go, Jest, Maven and Gradle
&lt;/h2&gt;

&lt;p&gt;When a failure's verdict is &lt;code&gt;unknown&lt;/code&gt;, siftwise can &lt;strong&gt;re-run just that test&lt;/strong&gt;. If it passes, the failure is marked &lt;strong&gt;flaky&lt;/strong&gt; (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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;What siftwise runs&lt;/th&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pytest &amp;lt;node IDs&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go test -count=1 -run&lt;/code&gt; with the failing test names, per package&lt;/td&gt;
&lt;td&gt;Go; &lt;code&gt;gotestsum&lt;/code&gt; optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jest --runTestsByPath &amp;lt;file&amp;gt; -t&lt;/code&gt; with the full test name&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jest-junit&lt;/code&gt; with &lt;code&gt;addFileAttribute: "true"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Java (Maven)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mvn -Dtest=Class#method test&lt;/code&gt; (&lt;code&gt;./mvnw&lt;/code&gt; if present)&lt;/td&gt;
&lt;td&gt;nothing extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Java (Gradle)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gradle test --tests Class.method&lt;/code&gt; (&lt;code&gt;./gradlew&lt;/code&gt; if present)&lt;/td&gt;
&lt;td&gt;nothing extra&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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 &lt;code&gt;mvn&lt;/code&gt;/&lt;code&gt;gradle&lt;/code&gt; (or &lt;code&gt;./mvnw&lt;/code&gt;/&lt;code&gt;./gradlew&lt;/code&gt;), so other versions should work too.&lt;/p&gt;

&lt;p&gt;Turn it on in the GitHub Action's &lt;code&gt;record&lt;/code&gt; step (or &lt;code&gt;SIFTWISE_RERUN_UNKNOWN&lt;/code&gt; on GitLab):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/record@v0.2.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;rerun-unknown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;      &lt;span class="c1"&gt;# re-run unknown failures up to twice
&lt;/span&gt;    &lt;span class="na"&gt;rerun-prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;     &lt;span class="c1"&gt;# how Jest (or pytest, e.g. "uv run") runs in your job
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rerun-prefix&lt;/code&gt; only applies to pytest and Jest. Go, Maven and Gradle pick their own tool, so a mixed-language repo doesn't break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits:&lt;/strong&gt; 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 &lt;code&gt;@DisplayName&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: safer PR comments
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;real link&lt;/strong&gt; in siftwise's comment, and a path like &lt;code&gt;pkgs/@acme/...&lt;/code&gt; would &lt;strong&gt;@mention&lt;/strong&gt; someone.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;@v0.2.0&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: dogfooding, siftwise on its own CI
&lt;/h2&gt;

&lt;p&gt;siftwise now runs on &lt;strong&gt;its own&lt;/strong&gt; CI in shadow mode: the full suite still runs every time, and siftwise records what it &lt;strong&gt;would&lt;/strong&gt; have selected and whether that would have caught each failure.&lt;/p&gt;

&lt;p&gt;It needs no hosted server: a siftwise server starts inside each CI job, and the history lives in the GitHub Actions cache between runs.&lt;/p&gt;

&lt;p&gt;My earlier numbers (2,049 of 2,050 failures caught on rdflib, for example) came from &lt;strong&gt;replaying&lt;/strong&gt; other projects' old CI history. These will be &lt;strong&gt;live&lt;/strong&gt; numbers from real changes, and I'll publish them with v0.3.&lt;/p&gt;




&lt;h2&gt;
  
  
  What testing on the real thing taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mocks aren't enough.&lt;/strong&gt; Every GitLab API call was mocked in my tests, and they all passed. On real gitlab.com, the pipeline's own job token got &lt;code&gt;404 Project Not Found&lt;/code&gt; downloading main's artifacts, so the example no longer depends on that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-running a test can delete other reports.&lt;/strong&gt; A Gradle &lt;code&gt;--tests&lt;/code&gt; re-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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every MCP client is a little different:&lt;/strong&gt; where it starts the server, which environment variables it passes, how it reads JSON schemas. Codex turned optional parameters written as &lt;code&gt;anyOf [type, null]&lt;/code&gt; into plain strings, so every schema got simpler. Testing in the real clients still found three bugs the automated tests had missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The agent can't start &lt;code&gt;siftwise&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;use the full path from &lt;code&gt;uv tool dir --bin&lt;/code&gt; in the config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tool says "not inside a git repository"&lt;/td&gt;
&lt;td&gt;open the repo folder itself in the client, or set &lt;code&gt;SIFTWISE_REPO_PATH&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;select_tests&lt;/code&gt; always says "full suite"&lt;/td&gt;
&lt;td&gt;no history yet: record a run on &lt;code&gt;main&lt;/code&gt; first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex: "model not supported"&lt;/td&gt;
&lt;td&gt;pick another model with &lt;code&gt;/model&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini: siftwise says the token is missing&lt;/td&gt;
&lt;td&gt;put &lt;code&gt;SIFTWISE_API_TOKEN&lt;/code&gt; in the server's &lt;code&gt;env&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab: no MR note, &lt;code&gt;HTTP 403&lt;/code&gt; in the log&lt;/td&gt;
&lt;td&gt;the token lacks the permissions in Step 2.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab: full suite on every MR&lt;/td&gt;
&lt;td&gt;protected variables don't reach MR branches (see Step 2.3), or the clone is shallow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Known limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MCP is tested on Windows only; Windsurf and Cline are untested.&lt;/li&gt;
&lt;li&gt;GitLab is tested on gitlab.com Free; Self-Managed and Premium are untested.&lt;/li&gt;
&lt;li&gt;Java and Jest re-runs need matching test names (no JUnit 5 &lt;code&gt;@DisplayName&lt;/code&gt; yet).&lt;/li&gt;
&lt;li&gt;Full details are in the &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: v0.3, smarter selection
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test prioritization:&lt;/strong&gt; run the tests most likely to fail first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-budget mode:&lt;/strong&gt; "the best tests that fit in 5 minutes".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real JS/TS import graph&lt;/strong&gt; for Jest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test selection for Java&lt;/strong&gt;, not just re-runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live recall numbers&lt;/strong&gt; from dogfooding.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 PyPI: &lt;a href="https://pypi.org/project/siftwise/" rel="noopener noreferrer"&gt;https://pypi.org/project/siftwise/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 Release notes: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/releases/tag/v0.2.0" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise/releases/tag/v0.2.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 First post (server and GitHub Action setup): &lt;a href="https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf"&gt;https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, break it, tell me
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;: misses are the most useful bug reports. And a ⭐ really helps a solo project get seen. 👇&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>testing</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>I built siftwise: run only the tests your change needs (open source, self-hosted)</title>
      <dc:creator>Pramod Basavaraj Menasi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:25:08 +0000</pubDate>
      <link>https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf</link>
      <guid>https://dev.to/pm_22/i-built-siftwise-run-only-the-tests-your-change-needs-open-source-self-hosted-51jf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; v0.2 is out: GitLab CI, 5 coding agents tested, and flaky-test re-runs for Go, Jest and Java. &lt;a href="https://dev.to/pm_22/siftwise-v02-run-it-on-gitlab-ci-and-with-5-coding-agents-full-setup-guide-o2f"&gt;Read the v0.2 guide →&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Update:&lt;/strong&gt; v0.3 is out: riskiest tests first, time budgets, and Jest and Java selection. &lt;a href="https://dev.to/pm_22/siftwise-v03-riskiest-tests-first-time-budgets-jest-and-java-selection-and-live-dogfooding-2b0c"&gt;Read the v0.3 guide →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your coding agent just changed one file. CI is now running &lt;strong&gt;all 18,000 tests&lt;/strong&gt;. Again.&lt;/p&gt;

&lt;p&gt;Coding agents now write code faster than CI can test it. Every pull request, whether a person or an agent wrote it, still runs the entire test suite. Anthropic reported a 25x increase in CI jobs over six months as agents sped up writing code (&lt;a href="https://claude.com/blog/agentic-coding-is-straining-ci-heres-how-we-scaled-test-impact-analysis-at-anthropic" rel="noopener noreferrer"&gt;Agentic coding is straining CI&lt;/a&gt;), and Linear wrote that &lt;a href="https://linear.app/now/ci-bottleneck-reworked" rel="noopener noreferrer"&gt;AI coding has made CI a bottleneck&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That got me thinking: &lt;strong&gt;what if CI only ran the tests a change could actually break?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So over the last six weeks I built &lt;strong&gt;&lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;siftwise&lt;/a&gt;&lt;/strong&gt;, an open-source, self-hosted tool that does exactly that. v0.1.0 is out on PyPI and GHCR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip install siftwise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post is the whole guide: what it does, how to run it on your laptop, how to connect your own repo, your coding agent, and your CI. By the end you'll have everything you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one sentence
&lt;/h2&gt;

&lt;p&gt;siftwise looks at your diff, works out which tests could be affected, runs only those, and &lt;strong&gt;falls back to the full suite whenever it isn't sure&lt;/strong&gt;, because missing a real failure is worse than running a few extra tests.&lt;/p&gt;

&lt;p&gt;Test impact analysis isn't a new idea; there are good commercial tools for it. What I wanted was something &lt;strong&gt;open source, self-hosted, and built for agent-driven CI&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why you might care
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Faster feedback&lt;/strong&gt;: a one-file change can run just the tests that touch that file, not the whole suite.&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;Safe by default&lt;/strong&gt;: unsure → full suite. Server down → full suite. It never blocks CI (unless you opt into &lt;code&gt;fail-on&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;Every test has a reason&lt;/strong&gt;: no black box. You see &lt;em&gt;why&lt;/em&gt; each test was picked.&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;"Is this my fault?"&lt;/strong&gt;: every failure gets a verdict: new regression, already broken on main, flaky, or unknown.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Agent-ready&lt;/strong&gt;: an MCP server, so a coding agent can pick tests and triage failures itself.&lt;/li&gt;
&lt;li&gt;🏠 &lt;strong&gt;Your data stays yours&lt;/strong&gt;: runs on your own machine or server, on your own Postgres. No SaaS, no account, no test instrumentation.&lt;/li&gt;
&lt;li&gt;📏 &lt;strong&gt;Proves itself&lt;/strong&gt;: shadow mode runs the full suite on a sample of PRs and tells you what selection &lt;em&gt;would&lt;/em&gt; have missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;select&lt;/strong&gt;: diffs your branch, builds a Python or Go import graph locally, and asks the server which tests the change can affect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;run&lt;/strong&gt;: you run only those tests (or the full suite if siftwise isn't confident).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;record&lt;/strong&gt;: JUnit results are uploaded to the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rollup&lt;/strong&gt;: per-test history is updated (failures, durations, flakiness, broken-on-main).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;triage&lt;/strong&gt;: every failure gets a verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadow compare&lt;/strong&gt; (optional): on sampled runs, the full suite runs anyway and siftwise checks what it would have missed.&lt;/li&gt;
&lt;li&gt;In CI, a &lt;strong&gt;sticky PR comment&lt;/strong&gt; shows it all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Selection signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Path mapping&lt;/strong&gt; (&lt;code&gt;test_cart.py&lt;/code&gt; ↔ &lt;code&gt;cart.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import graphs&lt;/strong&gt; for Python and Go (&lt;code&gt;go list -deps -test&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure history&lt;/strong&gt;: tests that failed when these files changed before&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declared rules&lt;/strong&gt; in &lt;code&gt;.siftwise.toml&lt;/code&gt;, for dependencies no import graph can see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every selected test comes with a reason, like &lt;code&gt;imports changed module rdflib/term.py&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported languages
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;How tests are selected&lt;/th&gt;
&lt;th&gt;Command siftwise gives you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Python&lt;/strong&gt; (pytest)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;foo.py&lt;/code&gt; → &lt;code&gt;test_foo.py&lt;/code&gt;; test files importing a changed module (incl. parent packages, conftests); changed test files&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pytest&lt;/code&gt; node IDs or test files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the changed package and every package whose tests import it&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go test ./pkg&lt;/code&gt;, or &lt;code&gt;go test -run&lt;/code&gt; with the selected test names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;JS/TS&lt;/strong&gt; (Jest)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;foo.ts&lt;/code&gt; → &lt;code&gt;foo.test.*&lt;/code&gt;, &lt;code&gt;foo.spec.*&lt;/code&gt;, &lt;code&gt;__tests__/foo.*&lt;/code&gt; (needs file paths in the JUnit)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jest &amp;lt;files&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Java&lt;/strong&gt; (Maven/Gradle)&lt;/td&gt;
&lt;td&gt;recorded with full history (for triage); selection always runs the full suite (for now)&lt;/td&gt;
&lt;td&gt;none: you run your usual &lt;code&gt;mvn test&lt;/code&gt; or &lt;code&gt;./gradlew test&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Safe fallbacks
&lt;/h3&gt;

&lt;p&gt;Unknown diff, a changed build/CI/dependency file, no history yet, or a changed file that no signal covers: &lt;strong&gt;full suite&lt;/strong&gt;. If the server is down, CI just runs everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure triage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new_regression  high   tests.test_cart::test_total
    tests/test_cart.py tests changed src/cart.py
pre_existing    high   tests.test_db::test_query
    failing on main on ubuntu-py3.12 since 4f2a9c1 (3 main runs)
flaky           medium tests.test_net::test_fetch
    flaky 1 time in 90 days (1 unrelated fail-then-pass)
siftwise: 3 failing test(s): 1 new regression, 1 pre existing, 1 flaky, 0 unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody wastes an hour debugging a test that was already broken on main.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; (with Docker Compose) for the server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;git&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;&lt;/strong&gt; (or pip / pipx) for the CLI; Python 3.11+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go users:&lt;/strong&gt; &lt;code&gt;go&lt;/code&gt; on your PATH, plus &lt;a href="https://github.com/gotestyourself/gotestsum" rel="noopener noreferrer"&gt;&lt;code&gt;gotestsum&lt;/code&gt;&lt;/a&gt; for JUnit output (&lt;code&gt;go install gotest.tools/gotestsum@latest&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All commands are bash. &lt;strong&gt;On Windows, use Git Bash or WSL.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Try it in 5 minutes (quickstart)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start the server&lt;/strong&gt; (Postgres included):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/PramodBasavarajMenasi/siftwise.git
&lt;span class="nb"&gt;cd&lt;/span&gt; siftwise
&lt;span class="o"&gt;[&lt;/span&gt; -f .env &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SIFTWISE_API_TOKEN=$(openssl rand -hex 32)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up -d --wait             &lt;span class="c"&gt;# pulls the image, waits until healthy&lt;/span&gt;
curl -fsS http://localhost:8000/healthz &lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token in &lt;code&gt;.env&lt;/code&gt; is a shared secret you choose; the server and every client use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the CLI and give it the token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool install siftwise                &lt;span class="c"&gt;# or: pip install siftwise&lt;/span&gt;
&lt;span class="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;SIFTWISE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;sed -n &lt;span class="s1"&gt;'s/^SIFTWISE_API_TOKEN=//p'&lt;/span&gt; .env&lt;span class="o"&gt;)&lt;/span&gt;
siftwise --version                      &lt;span class="c"&gt;# 0.1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;siftwise: command not found&lt;/code&gt;? Run &lt;code&gt;uv tool update-shell&lt;/code&gt; once and open a new terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Record a run, change a file, and select:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;demo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$(mktemp -d)/shop"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cp -r examples/quickstart &lt;span class="s2"&gt;"$demo"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"$demo"&lt;/span&gt;
git init -q &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add -A &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit -qm &lt;span class="s2"&gt;"A shop"&lt;/span&gt;
uv run pytest -q --junitxml=junit.xml
siftwise record junit.xml --repo demo/shop --branch main --main

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# reviewed the bulk discount"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; shop/cart.py &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit -qam &lt;span class="s2"&gt;"Touch the cart"&lt;/span&gt;
siftwise &lt;span class="k"&gt;select&lt;/span&gt; --repo demo/shop --base HEAD~1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; selected.sh
cat selected.sh                         &lt;span class="c"&gt;# pytest tests/test_cart.py&lt;/span&gt;
uv run sh selected.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;siftwise: python imports: 1 test file(s) import changed modules
siftwise: selective (2 of 4 known tests): 2 of 4 known tests selected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You changed &lt;code&gt;cart.py&lt;/code&gt;, and siftwise ran only the tests that import it. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Use it on your own project (locally)
&lt;/h2&gt;

&lt;p&gt;Keep the server running. Now point siftwise at &lt;strong&gt;your&lt;/strong&gt; repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1 Connect the CLI to the server.&lt;/strong&gt; Two environment variables are the whole connection. First print your token (in the &lt;code&gt;siftwise&lt;/code&gt; folder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sed -n &lt;span class="s1"&gt;'s/^SIFTWISE_API_TOKEN=//p'&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/code/my-app
&lt;span class="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;SIFTWISE_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000     &lt;span class="c"&gt;# the default; change it for a remote server&lt;/span&gt;
&lt;span class="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;SIFTWISE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&amp;lt;&lt;/span&gt;paste the token&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Put both lines in your &lt;code&gt;~/.bashrc&lt;/code&gt; (or &lt;code&gt;~/.zshrc&lt;/code&gt;) so you don't have to repeat them. Never commit the token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2.2 Record a first full run on &lt;code&gt;main&lt;/code&gt;.&lt;/strong&gt; This gives siftwise history to work with. Pick a repo name (&lt;code&gt;owner/name&lt;/code&gt;) and &lt;strong&gt;use the same one in every command&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main

&lt;span class="c"&gt;# Python&lt;/span&gt;
pytest --junitxml=junit.xml

&lt;span class="c"&gt;# Go&lt;/span&gt;
gotestsum --junitfile junit.xml ./...

&lt;span class="c"&gt;# Jest: use a JUnit reporter that writes file paths (e.g. jest-junit with addFileAttribute="true")&lt;/span&gt;

siftwise record junit.xml --repo me/my-app --branch main --main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Want history from the start? If your GitHub Actions runs already upload JUnit XML as artifacts, &lt;code&gt;scripts/backfill.py&lt;/code&gt; in the siftwise repo can import past runs (see the README's "Seed history from past CI runs").&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2.3 Make a change and select.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout -b fix-discount
&lt;span class="c"&gt;# ...edit src/my_app/pricing.py...&lt;/span&gt;
git commit -am &lt;span class="s2"&gt;"Fix discount rounding"&lt;/span&gt;

siftwise &lt;span class="k"&gt;select&lt;/span&gt; --repo me/my-app --base main &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; selected.sh
cat selected.sh            &lt;span class="c"&gt;# only the tests your change can affect&lt;/span&gt;
sh selected.sh             &lt;span class="c"&gt;# in a uv project: uv run sh selected.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--json&lt;/code&gt; prints every selected test with the reason it was picked.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;empty&lt;/strong&gt; &lt;code&gt;selected.sh&lt;/code&gt; means no tests are affected.&lt;/li&gt;
&lt;li&gt;If you touched something siftwise can't reason about (say &lt;code&gt;pyproject.toml&lt;/code&gt; or a CI file), &lt;code&gt;selected.sh&lt;/code&gt; runs the full suite. That's by design.&lt;/li&gt;
&lt;li&gt;Exit code &lt;strong&gt;3&lt;/strong&gt; means "full suite needed, but I don't know your command": just run your normal test command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.4 A test failed? Ask whether it's your fault.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the failing tests with a JUnit report, then triage it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/test_pricing.py --junitxml=junit.xml    &lt;span class="c"&gt;# the tests from selected.sh&lt;/span&gt;
siftwise triage junit.xml --repo me/my-app --base main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--json&lt;/code&gt; for the full evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.5 Keep feeding it.&lt;/strong&gt; Record your branch runs too (with &lt;code&gt;--base&lt;/code&gt;, so triage can link later failures to this change):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;siftwise record junit.xml --repo me/my-app --branch fix-discount --base main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more runs it has, the better its history: flakiness, "already broken on main", and co-failures all come from recorded runs. In practice, CI does this for you (Step 4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.6 Add rules for what imports can't see.&lt;/strong&gt; Drop a &lt;code&gt;.siftwise.toml&lt;/code&gt; in your repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="na"&gt;always_run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tests/smoke/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"tests.test_health::*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# test file paths or test IDs&lt;/span&gt;
&lt;span class="na"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"build/reports/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# files your test runs write (never "changed code")&lt;/span&gt;
&lt;span class="na"&gt;known_flaky&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tests/test_net.py"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# triage: unlinked failures are flaky&lt;/span&gt;

&lt;span class="k"&gt;[[depends]]&lt;/span&gt;                               &lt;span class="c1"&gt;# dependencies no import graph can see&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test/cli/**"&lt;/span&gt;                     &lt;span class="c1"&gt;# test file paths, or &amp;lt;Go package dir&amp;gt;/&amp;lt;TestName&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"**/*.go"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"!**/*_test.go"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="c1"&gt;# ! excludes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Globs: &lt;code&gt;**&lt;/code&gt; spans directories; &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; stay within one folder. Go test IDs contain the import path, so match them with &lt;code&gt;**::TestName&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.7 Stop the server&lt;/strong&gt; when you're done: &lt;code&gt;docker compose down&lt;/code&gt; in the siftwise folder (&lt;code&gt;down -v&lt;/code&gt; also deletes its data).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect your coding agent (MCP)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;siftwise mcp&lt;/code&gt; is a standard stdio MCP server with three tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;The agent calls it&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select_tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;before running tests after a change&lt;/td&gt;
&lt;td&gt;the mode, the command to run, a reason per test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;triage_failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when tests fail, before fixing them&lt;/td&gt;
&lt;td&gt;a verdict per failure, with evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when a selection or verdict surprises it&lt;/td&gt;
&lt;td&gt;one test's recent history and why it would (not) be selected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It includes &lt;strong&gt;uncommitted work&lt;/strong&gt; (the diff runs to your working tree, untracked files too), and it fails open: if the server is down, it tells the agent to run the full suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.1 Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool install &lt;span class="s1"&gt;'siftwise[mcp]'&lt;/span&gt;
uv tool update-shell        &lt;span class="c"&gt;# once, if ~/.local/bin isn't on your PATH; then restart the agent&lt;/span&gt;
uv tool dir --bin           &lt;span class="c"&gt;# prints the folder, if you need the full path below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3.2 Add it to your agent.&lt;/strong&gt; Open the agent &lt;strong&gt;inside your repository&lt;/strong&gt;: siftwise reads the repo name from your &lt;code&gt;origin&lt;/code&gt; remote and the base branch from &lt;code&gt;origin/HEAD&lt;/code&gt; (else &lt;code&gt;main&lt;/code&gt;). Override with &lt;code&gt;SIFTWISE_REPO&lt;/code&gt; / &lt;code&gt;SIFTWISE_BASE_REF&lt;/code&gt; if they don't match the name you recorded with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (tested ✅):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add siftwise --env &lt;span class="nv"&gt;SIFTWISE_SERVER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 \
    --env &lt;span class="nv"&gt;SIFTWISE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-token -- siftwise mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or a project &lt;code&gt;.mcp.json&lt;/code&gt;, which reads the token from your environment so it never lands in git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_SERVER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;"SIFTWISE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"${SIFTWISE_API_TOKEN}"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Codex&lt;/strong&gt; (&lt;code&gt;~/.codex/config.toml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[mcp_servers.siftwise]&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"siftwise"&lt;/span&gt;
&lt;span class="na"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;SIFTWISE_SERVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SIFTWISE_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-token"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; for every project, or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in one): the same JSON as above, with the token value written in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Honest note:&lt;/strong&gt; siftwise is a standard stdio MCP server, so any MCP client should be able to run it. So far I've tested it with &lt;strong&gt;Claude Code&lt;/strong&gt;. The Codex and Cursor configs follow each tool's MCP format but haven't been tested yet. If you try them, please tell me (or open an issue)!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Agent can't find &lt;code&gt;siftwise&lt;/code&gt;?&lt;/strong&gt; (common on Windows: the agent doesn't see your shell's PATH) Use the full path from &lt;code&gt;uv tool dir --bin&lt;/code&gt; as the command. In JSON, double the backslashes: &lt;code&gt;"C:\\Users\\you\\.local\\bin\\siftwise.exe"&lt;/code&gt;. In TOML, use a single-quoted string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.3 Tell the agent when to use it&lt;/strong&gt;, in &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Before running tests, call the siftwise &lt;span class="sb"&gt;`select_tests`&lt;/span&gt; tool and run the command it returns.
When tests fail, call &lt;span class="sb"&gt;`triage_failure`&lt;/span&gt; before fixing anything: fix &lt;span class="sb"&gt;`new_regression`&lt;/span&gt; failures,
re-run &lt;span class="sb"&gt;`flaky`&lt;/span&gt; ones, and leave &lt;span class="sb"&gt;`pre_existing`&lt;/span&gt; ones (already failing on main).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What that looks like in practice&lt;/strong&gt; (an example flow):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You: &lt;em&gt;"Fix the discount rounding bug."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The agent edits &lt;code&gt;pricing.py&lt;/code&gt;, calls &lt;code&gt;select_tests&lt;/code&gt; → gets &lt;code&gt;pytest tests/test_pricing.py tests/test_cart.py&lt;/code&gt; instead of the whole suite.&lt;/li&gt;
&lt;li&gt;A test fails → it calls &lt;code&gt;triage_failure&lt;/code&gt; → &lt;code&gt;new_regression&lt;/code&gt;, linked to the file it just changed → it fixes its own bug.&lt;/li&gt;
&lt;li&gt;A different failure is &lt;code&gt;pre_existing&lt;/code&gt; (already broken on main) → the agent leaves it alone instead of "fixing" something unrelated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Less waiting, fewer wrong fixes, and a reason for every decision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The MCP server only reads. History comes from runs you (or CI) record, so it gets smarter once Step 4 is in place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Connect it to your CI (GitHub Actions)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;4.1 Host the server where CI can reach it.&lt;/strong&gt; GitHub's runners can't see your &lt;code&gt;localhost&lt;/code&gt;. The quickstart compose file deliberately binds the API to &lt;code&gt;127.0.0.1&lt;/code&gt;, so on a server you put a &lt;strong&gt;TLS reverse proxy&lt;/strong&gt; in front (the token travels in a request header).&lt;/p&gt;

&lt;p&gt;On any Linux VM with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/PramodBasavarajMenasi/siftwise.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; siftwise
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SIFTWISE_API_TOKEN=$(openssl rand -hex 32)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
docker compose up -d --wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point a DNS record (e.g. &lt;code&gt;siftwise.your-domain.com&lt;/code&gt;) at the VM, open ports 80 and 443, install &lt;a href="https://caddyserver.com/docs/install" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;, and use this &lt;code&gt;Caddyfile&lt;/code&gt; (Caddy gets the HTTPS certificate for you):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;siftwise.your-domain.com {
    reverse_proxy localhost:8000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo systemctl reload caddy
curl -fsS https://siftwise.your-domain.com/healthz   &lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Already have Postgres? Run just the image: &lt;code&gt;docker run -d -p 8000:8000 -e SIFTWISE_API_TOKEN=... -e SIFTWISE_DATABASE_URL=postgresql+psycopg://user:password@db-host:5432/siftwise ghcr.io/pramodbasavarajmenasi/siftwise:0.1.0&lt;/code&gt;. It applies database migrations on start.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4.2 Add two repo secrets&lt;/strong&gt; (Settings → Secrets and variables → Actions):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://siftwise.your-domain.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIFTWISE_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the server's &lt;code&gt;SIFTWISE_API_TOKEN&lt;/code&gt; (from its &lt;code&gt;.env&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;4.3 Add the workflow.&lt;/strong&gt; The &lt;code&gt;push: main&lt;/code&gt; trigger matters: main-branch runs are the history siftwise learns from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# records main history
&lt;/span&gt;  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="c1"&gt;# NEVER pull_request_target: it exposes secrets to fork code
&lt;/span&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;           &lt;span class="c1"&gt;# the run's start time
&lt;/span&gt;  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;    &lt;span class="c1"&gt;# the PR comment
&lt;/span&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# full history, so the diff works
&lt;/span&gt;      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"3.12"&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;   &lt;span class="c1"&gt;# your install step
&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;siftwise&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/select@v0.1.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.mode }}&lt;/span&gt;
          &lt;span class="na"&gt;COMMAND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.command }}&lt;/span&gt;
          &lt;span class="na"&gt;JUNIT_DIR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.siftwise.outputs.junit-dir }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$MODE" = selective ]; then&lt;/span&gt;
            &lt;span class="c1"&gt;# each pytest command writes its own report to JUNIT_DIR
&lt;/span&gt;            &lt;span class="s"&gt;if [ -n "$COMMAND" ]; then eval "$COMMAND"; else echo "No tests affected."; fi&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;pytest --junitxml="$JUNIT_DIR/full.xml"&lt;/span&gt;   &lt;span class="c1"&gt;# your normal full suite
&lt;/span&gt;          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PramodBasavarajMenasi/siftwise/actions/record@v0.1.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;server-url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIFTWISE_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules that save you debugging time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass the command through &lt;code&gt;env:&lt;/code&gt;&lt;/strong&gt; as above, never as &lt;code&gt;${{ }}&lt;/code&gt; inside &lt;code&gt;run:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;record&lt;/code&gt; uploads `&lt;/strong&gt;/junit*.xml&lt;code&gt;** plus everything in &lt;/code&gt;junit-dir&lt;code&gt;. Name your report files &lt;/code&gt;junit*.xml&lt;code&gt; or set its &lt;/code&gt;junit` input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go:&lt;/strong&gt; write JUnit with &lt;code&gt;gotestsum --junitfile&lt;/code&gt;. &lt;strong&gt;Jest:&lt;/strong&gt; use a JUnit reporter with file paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java:&lt;/strong&gt; siftwise gives no command, so run &lt;code&gt;mvn test&lt;/code&gt; or &lt;code&gt;./gradlew test&lt;/code&gt; in the full-suite branch. Their reports are named &lt;code&gt;TEST-*.xml&lt;/code&gt;, which the default glob misses, so set &lt;code&gt;junit: "**/target/surefire-reports/TEST-*.xml"&lt;/code&gt; (Maven) or &lt;code&gt;junit: "**/build/test-results/test/TEST-*.xml"&lt;/code&gt; (Gradle) on &lt;code&gt;record&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin &lt;code&gt;@v0.1.0&lt;/code&gt;&lt;/strong&gt; (or a commit SHA) for reproducible CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using a test matrix?&lt;/strong&gt; Give each leg's &lt;code&gt;record&lt;/code&gt; a &lt;code&gt;variant&lt;/code&gt;, let only one leg comment (&lt;code&gt;comment: ${{ strategy.job-index == 0 }}&lt;/code&gt;), and add one &lt;code&gt;actions/rollup&lt;/code&gt; job after all legs. Full example in the &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise#matrix" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4.4 Open a PR.&lt;/strong&gt; You'll get a sticky comment: what ran, what was skipped and why, and a verdict for every failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.5 Optional extras:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gate on real regressions:&lt;/strong&gt; set &lt;code&gt;fail-on: new_regression,unknown&lt;/code&gt; on &lt;code&gt;record&lt;/code&gt; (single job) or &lt;code&gt;rollup&lt;/code&gt; (matrix), give the test step &lt;code&gt;continue-on-error: true&lt;/code&gt;, and make that job a required check. With &lt;code&gt;fail-on&lt;/code&gt; set, siftwise fails closed: if verdicts are unavailable, the step fails too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run unknown failures&lt;/strong&gt; to catch flakes (pytest): &lt;code&gt;rerun-unknown: 2&lt;/code&gt; and &lt;code&gt;rerun-prefix: uv run&lt;/code&gt; on &lt;code&gt;record&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow mode&lt;/strong&gt; (recommended at first): add &lt;code&gt;shadow: 10%&lt;/code&gt; to &lt;code&gt;select&lt;/code&gt;. On 10% of runs the full suite runs anyway, and the PR comment says what selection would have caught or missed. Check your recall any time:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;siftwise report --repo me/my-app --days 30 --by week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  My suggested rollout
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run the server and record &lt;code&gt;main&lt;/code&gt; for a few days (just the &lt;code&gt;push: main&lt;/code&gt; trigger).&lt;/li&gt;
&lt;li&gt;Turn on PRs with &lt;code&gt;shadow: always&lt;/code&gt; or &lt;code&gt;shadow: 10%&lt;/code&gt;, and watch the recall.&lt;/li&gt;
&lt;li&gt;Happy with the numbers? Let selection run, and add &lt;code&gt;fail-on&lt;/code&gt; if you want gating.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Security and limits (read this before you host it)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One token, full access.&lt;/strong&gt; &lt;code&gt;SIFTWISE_API_TOKEN&lt;/code&gt; can read and write every repo on that server. No per-repo or read-only tokens yet. Use a separate server for repos that shouldn't see each other's history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork PRs get no secrets&lt;/strong&gt;, so they run the full suite with no comment. That's expected. Keep another required check for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect &lt;code&gt;.github/workflows/&lt;/code&gt;&lt;/strong&gt; with CODEOWNERS or a ruleset: a PR that edits workflows can change any gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's stored:&lt;/strong&gt; test IDs, outcomes, durations, failure messages (truncated to 4 KB), changed paths, branch names, SHAs. Treat the database like your CI logs. PR comments never show raw failure messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always full suite:&lt;/strong&gt; merge-queue, scheduled and manual runs (no diff base). The Action is tested on Linux runners only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;siftwise: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uv tool update-shell&lt;/code&gt;, then open a new terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every endpoint returns &lt;strong&gt;503&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;the server has no &lt;code&gt;SIFTWISE_API_TOKEN&lt;/code&gt;; check its &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;select&lt;/code&gt; fails in CI about a shallow clone&lt;/td&gt;
&lt;td&gt;add &lt;code&gt;fetch-depth: 0&lt;/code&gt; to &lt;code&gt;actions/checkout&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No PR comment&lt;/td&gt;
&lt;td&gt;check &lt;code&gt;pull-requests: write&lt;/code&gt; permission and the two secrets; fork PRs never get one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Always the full suite&lt;/td&gt;
&lt;td&gt;no history yet (record &lt;code&gt;main&lt;/code&gt; first), or the diff touched a build/CI file; run &lt;code&gt;select --json&lt;/code&gt; to see the reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server restarts with &lt;code&gt;Can't locate revision ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you went back to an older image; use the newer one (with &lt;code&gt;--build&lt;/code&gt; from a checkout: rebuild after pulling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent can't find &lt;code&gt;siftwise&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;use the full path from &lt;code&gt;uv tool dir --bin&lt;/code&gt; in its MCP config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Does it actually work?
&lt;/h2&gt;

&lt;p&gt;I didn't want to just claim it works, so I replayed real CI history from open-source projects, with no peeking at the future:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Failures caught&lt;/th&gt;
&lt;th&gt;Test time skipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;rdflib&lt;/strong&gt; (Python, ~18,000 tests)&lt;/td&gt;
&lt;td&gt;2,049 of 2,050; 140 of 141 in selective runs (the one miss: a flaky test)&lt;/td&gt;
&lt;td&gt;10.4% on the 10 most recent PRs (from lint-only PRs); 0.6% over 19 main commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;substrate&lt;/strong&gt; (Go, ~9,000 tests)&lt;/td&gt;
&lt;td&gt;2,043 of 2,044; 38 of 39 in selective runs, with two declared rules (26 of 38 without)&lt;/td&gt;
&lt;td&gt;5.1% on PRs, 13.9% over 40 main commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cirq&lt;/strong&gt; (Python monorepo, 9 packages)&lt;/td&gt;
&lt;td&gt;not measurable (main never failed)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;37.9%&lt;/strong&gt; over 132 merged PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;kubo&lt;/strong&gt; (Go, ~4,100 tests, older week-2 code)&lt;/td&gt;
&lt;td&gt;19 of 19 (those runs fell back to the full suite)&lt;/td&gt;
&lt;td&gt;30.7% over 20 main commits; 0% on recent PRs (all dependency/CI bumps)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Triage:&lt;/strong&gt; on substrate, &lt;code&gt;new_regression&lt;/code&gt; verdicts held up on &lt;strong&gt;319 of 319&lt;/strong&gt; labelled failures, and only 5 of 406 labelled regressions were wrongly called flaky (4 likely label noise). On rdflib, 0 regressions were called flaky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed:&lt;/strong&gt; &lt;code&gt;/select&lt;/code&gt; takes about 0.4 s on 18,000 tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt; savings follow package boundaries. In codebases where everything imports everything (like rdflib's core), safe selection still runs ~99% of the suite. siftwise just runs it, rather than guess. These are replays of history, not live production numbers; shadow mode is how you measure it on your own repo.&lt;/p&gt;

&lt;p&gt;All the numbers and methodology: &lt;a href="https://pramodbasavarajmenasi.github.io/siftwise-blog/posts/introducing-siftwise.html" rel="noopener noreferrer"&gt;full write-up&lt;/a&gt; · &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/docs/eval-results.md" rel="noopener noreferrer"&gt;eval results&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v0.2: prove it and widen its reach&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live dogfooding:&lt;/strong&gt; run siftwise on its own CI and publish live shadow-mode recall numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More runners for &lt;code&gt;rerun-unknown&lt;/code&gt;:&lt;/strong&gt; Go, Jest and Java (currently pytest only).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other MCP clients:&lt;/strong&gt; test and document Cursor and Codex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI support&lt;/strong&gt;, beyond GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise" rel="noopener noreferrer"&gt;https://github.com/PramodBasavarajMenasi/siftwise&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 PyPI: &lt;a href="https://pypi.org/project/siftwise/" rel="noopener noreferrer"&gt;https://pypi.org/project/siftwise/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐳 Docker: &lt;code&gt;ghcr.io/pramodbasavarajmenasi/siftwise:0.1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;📝 Blog: &lt;a href="https://pramodbasavarajmenasi.github.io/siftwise-blog/" rel="noopener noreferrer"&gt;https://pramodbasavarajmenasi.github.io/siftwise-blog/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, break it, tell me
&lt;/h2&gt;

&lt;p&gt;This is v0.1.0 (alpha), built by one person in six weeks, and fully open source (Apache-2.0). The fastest way to make it better is you running it on a real repo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;Star it&lt;/strong&gt; if the idea clicks: it genuinely helps a solo project get seen.&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Open an issue&lt;/strong&gt; if it picks the wrong tests. Misses are the most valuable bug reports.&lt;/li&gt;
&lt;li&gt;🛠️ &lt;strong&gt;Contribute&lt;/strong&gt;: anything on the v0.2 list (re-run runners, MCP client testing, GitLab CI) is a great place to start. See &lt;a href="https://github.com/PramodBasavarajMenasi/siftwise/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point it at your repo and drop your numbers in the comments. I want to know how much CI time it saves you. 👇&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>testing</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
