<?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: VX</title>
    <description>The latest articles on DEV Community by VX (@vzn-vx).</description>
    <link>https://dev.to/vzn-vx</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%2F4172151%2F5cf8fc26-89c1-4591-8d21-8dd3f2f0fa46.png</url>
      <title>DEV Community: VX</title>
      <link>https://dev.to/vzn-vx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vzn-vx"/>
    <language>en</language>
    <item>
      <title>Flags you already know</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:43:39 +0000</pubDate>
      <link>https://dev.to/vzn-vx/flags-you-already-know-aaj</link>
      <guid>https://dev.to/vzn-vx/flags-you-already-know-aaj</guid>
      <description>&lt;p&gt;Years of &lt;code&gt;turbo run&lt;/code&gt; and &lt;code&gt;nx run-many&lt;/code&gt; live in your fingers. vx does not&lt;br&gt;
ask you to forget them on day one. Every flag either works, is rewritten,&lt;br&gt;
or is answered with the command that does it here.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgRlsiYSBUdXJibyBvciBOeCBmbGFnIl0gLS0-IFNBTUVbInNhbWU6IHdvcmtzIGFzIGlzIl0KICBGIC0tPiBBTElBU1siYWxpYXM6IHJld3JpdHRlbiBiZWZvcmUgdGhlIHBhcnNlIl0KICBGIC0tPiBSRUZVU0VbInJlZnVzZTogbmFtZXMgdGhlIHZ4IHdheSJdCiAgc3R5bGUgQUxJQVMgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgRlsiYSBUdXJibyBvciBOeCBmbGFnIl0gLS0-IFNBTUVbInNhbWU6IHdvcmtzIGFzIGlzIl0KICBGIC0tPiBBTElBU1siYWxpYXM6IHJld3JpdHRlbiBiZWZvcmUgdGhlIHBhcnNlIl0KICBGIC0tPiBSRUZVU0VbInJlZnVzZTogbmFtZXMgdGhlIHZ4IHdheSJdCiAgc3R5bGUgQUxJQVMgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" alt="diagram" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Works as is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-t&lt;/code&gt;, &lt;code&gt;-p&lt;/code&gt;, &lt;code&gt;--exclude&lt;/code&gt; and Nx's &lt;code&gt;--skip-nx-cache&lt;/code&gt; mean what you expect:&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;$ &lt;/span&gt;vx run build &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--skip-nx-cache&lt;/span&gt; &lt;span class="nt"&gt;--dry&lt;/span&gt;
would run:
  ▶  @demo/api#build   cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;32b3a0bf  ~326ms
  ▶  @demo/docs#build  cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;1f858edb  ~311ms
  ▶  @demo/ui#build    cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;46046abb  ~313ms
  ▶  @demo/web#build   cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;b25e5bf4  ~310ms

4 task&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; planned, 4 would run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Answered with the vx way
&lt;/h2&gt;

&lt;p&gt;A flag vx has no use for is never ignored. It is refused with the&lt;br&gt;
spelling that does the job:&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;$ &lt;/span&gt;vx run build &lt;span class="nt"&gt;--parallel&lt;/span&gt;
vx run: &lt;span class="nt"&gt;--parallel&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;turbo&lt;span class="o"&gt;)&lt;/span&gt;: vx always honours &lt;span class="sb"&gt;`&lt;/span&gt;dependsOn&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--concurrency&lt;/span&gt; &amp;lt;n&amp;gt;&lt;span class="sb"&gt;`&lt;/span&gt; sets how many run at once &lt;span class="o"&gt;(&lt;/span&gt;see &lt;span class="sb"&gt;`&lt;/span&gt;vx run &lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nx's verbs and &lt;code&gt;project:target&lt;/code&gt; get the same treatment:&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;$ &lt;/span&gt;vx run web:build
vx run: &lt;span class="sb"&gt;`&lt;/span&gt;web:build&lt;span class="sb"&gt;`&lt;/span&gt; is Nx&lt;span class="s1"&gt;'s project:target: vx run @demo/web#build

$ vx run-many -t build
`nx run-many` is `vx run &amp;lt;task&amp;gt; --all` here; -t, -p, --exclude and --parallel work as they are

$ vx affected -t test
`nx affected` is `vx run &amp;lt;task&amp;gt; --affected` here; -t, --base and --exclude work as they are

$ vx graph
`nx graph` is `vx run &amp;lt;task&amp;gt; --graph[=&amp;lt;file&amp;gt;.dot]` here: the task graph as Graphviz DOT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One table, held by a test
&lt;/h2&gt;

&lt;p&gt;Every row lives in one table, rendered into the CLI reference, and a&lt;br&gt;
test drives each row, so the docs and the binary cannot disagree.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/cli/#turbo-and-nx-flags" rel="noopener noreferrer"&gt;Turbo and Nx flags&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/features/turbo-nx-flags/" rel="noopener noreferrer"&gt;Flags you already know&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/flags-you-already-know/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your run, on the pull request</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:39:42 +0000</pubDate>
      <link>https://dev.to/vzn-vx/your-run-on-the-pull-request-3h08</link>
      <guid>https://dev.to/vzn-vx/your-run-on-the-pull-request-3h08</guid>
      <description>&lt;p&gt;A red CI job should not mean scrolling a log. &lt;code&gt;@vzn/vx-ci&lt;/code&gt; writes each&lt;br&gt;
&lt;code&gt;vx run&lt;/code&gt; where you already look on GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineWorkspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx-ci&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;github&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;That is the whole setup. Off GitHub Actions the plugin declines and&lt;br&gt;
costs nothing, so the line stays on for laptops too.&lt;/p&gt;
&lt;h2&gt;
  
  
  The job summary
&lt;/h2&gt;

&lt;p&gt;Every run appends a block to the workflow's summary page: the verdict,&lt;br&gt;
the counts, the failures first, then every task. This is the markdown&lt;br&gt;
the plugin wrote for a run where one test failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## ❌ vx run&lt;/span&gt;

&lt;span class="gs"&gt;**8**&lt;/span&gt; tasks · &lt;span class="gs"&gt;**6**&lt;/span&gt; executed · &lt;span class="gs"&gt;**2**&lt;/span&gt; cache hits (2 up-to-date, 0 restored) · &lt;span class="gs"&gt;**1**&lt;/span&gt; failed · 901ms

&lt;span class="gu"&gt;### Failures&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**@demo/ui#test**&lt;/span&gt; — exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below that sits a row per task with its status and time, and a footer&lt;br&gt;
naming the vx version and the command.&lt;/p&gt;

&lt;p&gt;A task killed by a signal reads &lt;code&gt;exit 137 (128 + SIGKILL)&lt;/code&gt;, a timeout&lt;br&gt;
reads &lt;code&gt;timed out, exit 143&lt;/code&gt;, and each failure names the tasks it&lt;br&gt;
blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  A check on the commit
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in the job, the plugin also posts a check run&lt;br&gt;
(named &lt;code&gt;vx&lt;/code&gt; unless you pass &lt;code&gt;checkName&lt;/code&gt;) whose output is that same&lt;br&gt;
summary, so the result sits next to the commit and the pull request.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgUlVOWyJ2eCBydW4iXSAtLT4gU0lOS1siZ2l0aHViKCkgc2luayJdCiAgU0lOSyAtLT4gU1VNWyJqb2Igc3VtbWFyeSJdCiAgU0lOSyAtLT4gQ0hFQ0tbImNoZWNrIHJ1biBvbiB0aGUgY29tbWl0Il0KICBDRkdbImNvbmZpZyBzdGFnZSJdIC0tPiBTQ09QRVsiY2FjaGUgc2NvcGUgZnJvbSB0aGUgcmVmIl0KICBzdHlsZSBDSEVDSyBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4Cg%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgUlVOWyJ2eCBydW4iXSAtLT4gU0lOS1siZ2l0aHViKCkgc2luayJdCiAgU0lOSyAtLT4gU1VNWyJqb2Igc3VtbWFyeSJdCiAgU0lOSyAtLT4gQ0hFQ0tbImNoZWNrIHJ1biBvbiB0aGUgY29tbWl0Il0KICBDRkdbImNvbmZpZyBzdGFnZSJdIC0tPiBTQ09QRVsiY2FjaGUgc2NvcGUgZnJvbSB0aGUgcmVmIl0KICBzdHlsZSBDSEVDSyBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4Cg%3FbgColor%3Dwhite" alt="diagram" width="733" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A cache a pull request cannot poison
&lt;/h2&gt;

&lt;p&gt;On Actions the plugin sets the cache scope from the ref. A push to the&lt;br&gt;
default branch reads and writes the trusted keys. A pull request becomes&lt;br&gt;
&lt;code&gt;pr-&amp;lt;n&amp;gt;&lt;/code&gt;: it reads its own keys, then main's, and writes only its own. So&lt;br&gt;
a branch never writes what main reads. Set &lt;code&gt;cacheScope: false&lt;/code&gt; to opt&lt;br&gt;
out, or set &lt;code&gt;cacheScope&lt;/code&gt; in &lt;code&gt;vx.workspace.ts&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  It never breaks the build
&lt;/h2&gt;

&lt;p&gt;The summary is written by a telemetry sink. vx isolates sinks and bounds&lt;br&gt;
their flush, so a slow or failing GitHub write can never fail or stall&lt;br&gt;
the run.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/guides/ci/" rel="noopener noreferrer"&gt;CI and remote&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/features/github-ci/" rel="noopener noreferrer"&gt;Results on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/results-on-github/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A run you can read</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:34:09 +0000</pubDate>
      <link>https://dev.to/vzn-vx/a-run-you-can-read-4jei</link>
      <guid>https://dev.to/vzn-vx/a-run-you-can-read-4jei</guid>
      <description>&lt;p&gt;Eight tasks in four projects, run in parallel, used to mean eight logs&lt;br&gt;
cut into each other. vx prints a run in three parts, and each part has&lt;br&gt;
one job.&lt;/p&gt;
&lt;h2&gt;
  
  
  A line per task
&lt;/h2&gt;

&lt;p&gt;While the run works, each task gets one line: its glyph, its time, its&lt;br&gt;
outcome and whether the cache had it.&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;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
 ⏺︎   310ms success miss     @demo/ui#build
 ◼︎     3ms failed  miss     @demo/ui#test
 ⏺︎   308ms success miss     @demo/docs#build
 ⏺︎   310ms success miss     @demo/web#build
 ⏺︎   207ms success miss     @demo/docs#test
 ⏺︎   207ms success miss     @demo/web#test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A frame for what failed
&lt;/h2&gt;

&lt;p&gt;A failed task's log comes back whole, in its own frame, with the command&lt;br&gt;
that ran. No other task's output is woven into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;┌─ @demo/ui#test &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; failed &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"expected 2, got 3"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

├─ OUTPUT ──────────────────────────────────────────────────

expected 2, got 3

└─ @demo/ui#test ── &lt;span class="o"&gt;(&lt;/span&gt;3ms&lt;span class="o"&gt;)&lt;/span&gt; failed &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for every log with &lt;code&gt;--output-logs full&lt;/code&gt;: each task still gets its&lt;br&gt;
own frame, in the order tasks finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  One summary, last
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;─ vx 0.0.0 ───────────────────────────────────────────────────
  projects  ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
            4 &lt;span class="k"&gt;in &lt;/span&gt;run · 4 total
  tasks     ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
            8 success · 8 total
  cache     ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
            8 up-to-date

  info      4 workers · &lt;span class="nb"&gt;local &lt;/span&gt;cache
  &lt;span class="nb"&gt;time      &lt;/span&gt;23ms
  result    8 tasks · all cached · 2.12s saved · 23ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the same eight tasks a second time, all cached: 23 ms, and the&lt;br&gt;
summary says how much work the cache saved.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgTFsiYSBsaW5lIHBlciB0YXNrIl0gLS0-IEZbImEgZnJhbWUgcGVyIGZhaWx1cmUiXQogIEYgLS0-IFNbIm9uZSBzdW1tYXJ5IGJsb2NrIl0KICBTIC0tPiBOWyJub3RoaW5nIGJlbG93IGl0Il0KICBzdHlsZSBTIHN0cm9rZTojYzZmODRlLHN0cm9rZS13aWR0aDoycHgK%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgTFsiYSBsaW5lIHBlciB0YXNrIl0gLS0-IEZbImEgZnJhbWUgcGVyIGZhaWx1cmUiXQogIEYgLS0-IFNbIm9uZSBzdW1tYXJ5IGJsb2NrIl0KICBTIC0tPiBOWyJub3RoaW5nIGJlbG93IGl0Il0KICBzdHlsZSBTIHN0cm9rZTojYzZmODRlLHN0cm9rZS13aWR0aDoycHgK%3FbgColor%3Dwhite" alt="diagram" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The summary is the last thing on screen, so the answer to "did it pass?"&lt;br&gt;
is always where your eyes land. On CI the frames fold into log groups,&lt;br&gt;
and &lt;code&gt;vx last&lt;/code&gt; replays the summary later.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/features/framed-output/" rel="noopener noreferrer"&gt;Framed output&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://vznjs.github.io/vx/features/run-summary/" rel="noopener noreferrer"&gt;Run summary&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/a-run-you-can-read/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The last run, on request</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:28:37 +0000</pubDate>
      <link>https://dev.to/vzn-vx/the-last-run-on-request-jhc</link>
      <guid>https://dev.to/vzn-vx/the-last-run-on-request-jhc</guid>
      <description>&lt;p&gt;The terminal scrolled away, CI printed ten thousand lines, or an agent&lt;br&gt;
ran the build an hour ago. vx keeps a record of every run, so the&lt;br&gt;
question "what happened?" has an answer after the fact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx last
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The last run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;vx last
run 01a11e14-87de-749d-a6ba-eca7bb441469 — ok
  &lt;span class="nv"&gt;$ &lt;/span&gt;vx run build &lt;span class="nt"&gt;--all&lt;/span&gt;
  2026-10-09T00:33:44.415Z · 62ms · master @ 64a144f9
  4 tasks · 4 hits &lt;span class="o"&gt;(&lt;/span&gt;1 up-to-date, 3 restored: 3 &lt;span class="nb"&gt;local&lt;/span&gt;, 0 remote&lt;span class="o"&gt;)&lt;/span&gt;

  restored-local    @demo/ui#build        31ms  46046abb75e9c084
  restored-local    @demo/web#build       31ms  b25e5bf41cb7a54d
  restored-local    @demo/docs#build      31ms  1f858edb724ca91b
  up-to-date        @demo/api#build        9ms  32b3a0bfa973a7a8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command, the branch and commit, the time, and every task with its&lt;br&gt;
outcome and cache key.&lt;/p&gt;
&lt;h2&gt;
  
  
  The run that broke
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--failed&lt;/code&gt; skips to the newest run that failed, and ends with the&lt;br&gt;
command that runs only what failed:&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;$ &lt;/span&gt;vx last &lt;span class="nt"&gt;--failed&lt;/span&gt;
run 01a11dab-5561-75fe-aa33-fd0fa220ad42 — FAILED
  &lt;span class="nv"&gt;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
  2026-10-08T22:38:50.209Z · 606ms · feat @ 5f0b0da6+dirty
  8 tasks · 2 hits &lt;span class="o"&gt;(&lt;/span&gt;2 up-to-date, 0 restored&lt;span class="o"&gt;)&lt;/span&gt; · 1 failed

  failed &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;   @demo/ui#test          4ms  fb349347b8a40aeb  0.5× cpu
  success           @demo/web#test       206ms  9b78e7e1a95c53db  0.0× cpu
  success           @demo/docs#test      205ms  36a0120a08d6c9d9  0.0× cpu
  ...

  re-run what failed: vx run @demo/ui#test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recent runs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;vx last &lt;span class="nt"&gt;--list&lt;/span&gt;
ok     2026-10-09T00:33:44.415Z  01a11e14-87de  4 tasks · 4 hits &lt;span class="o"&gt;(&lt;/span&gt;1 up-to-date, 3 restored: 3 &lt;span class="nb"&gt;local&lt;/span&gt;, 0 remote&lt;span class="o"&gt;)&lt;/span&gt;   62ms  &lt;span class="nv"&gt;$ &lt;/span&gt;vx run build &lt;span class="nt"&gt;--all&lt;/span&gt;
FAILED 2026-10-08T22:38:50.209Z  01a11dab-5561  8 tasks · 2 hits &lt;span class="o"&gt;(&lt;/span&gt;2 up-to-date, 0 restored&lt;span class="o"&gt;)&lt;/span&gt; · 1 failed          606ms  &lt;span class="nv"&gt;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
ok     2026-10-08T22:38:41.669Z  01a11dab-3405  8 tasks · 2 hits &lt;span class="o"&gt;(&lt;/span&gt;2 up-to-date, 0 restored&lt;span class="o"&gt;)&lt;/span&gt;                     912ms  &lt;span class="nv"&gt;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One task's output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;vx last &lt;span class="nt"&gt;--log&lt;/span&gt; app#test
FAIL src/sum.test.ts &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; adds
  expected 4, received 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--log&lt;/code&gt; prints what one task printed in the last run that ran it. A&lt;br&gt;
failure's output is kept, and a cached task's log is its cache entry,&lt;br&gt;
so a task that passed reads too. Agents get the same through&lt;br&gt;
&lt;code&gt;vx mcp&lt;/code&gt;'s &lt;code&gt;getTaskLog&lt;/code&gt;.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgUlVOWyJ2eCBydW4iXSAtLT4gREJbKCJjYWNoZS5kYjogcnVucyIpXQogIERCIC0tPiBMQVNUWyJ2eCBsYXN0Il0KICBEQiAtLT4gRkFJTEVEWyJ2eCBsYXN0IC0tZmFpbGVkIl0KICBEQiAtLT4gTElTVFsidnggbGFzdCAtLWxpc3QiXQogIERCIC0tPiBMT0dbInZ4IGxhc3QgLS1sb2ciXQogIHN0eWxlIEZBSUxFRCBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4Cg%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgUlVOWyJ2eCBydW4iXSAtLT4gREJbKCJjYWNoZS5kYjogcnVucyIpXQogIERCIC0tPiBMQVNUWyJ2eCBsYXN0Il0KICBEQiAtLT4gRkFJTEVEWyJ2eCBsYXN0IC0tZmFpbGVkIl0KICBEQiAtLT4gTElTVFsidnggbGFzdCAtLWxpc3QiXQogIERCIC0tPiBMT0dbInZ4IGxhc3QgLS1sb2ciXQogIHN0eWxlIEZBSUxFRCBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4Cg%3FbgColor%3Dwhite" alt="diagram" width="498" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The record lives in the workspace's cache database, so it costs the run&lt;br&gt;
nothing it was not already writing. &lt;code&gt;--format json&lt;/code&gt; gives agents the&lt;br&gt;
same answer as data.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/cli/" rel="noopener noreferrer"&gt;the CLI reference&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/features/vx-last/" rel="noopener noreferrer"&gt;vx last&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/vx-last/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>See what a task really is</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:23:05 +0000</pubDate>
      <link>https://dev.to/vzn-vx/see-what-a-task-really-is-16lb</link>
      <guid>https://dev.to/vzn-vx/see-what-a-task-really-is-16lb</guid>
      <description>&lt;p&gt;A &lt;code&gt;vx.config.ts&lt;/code&gt; is TypeScript. Presets compose, plugins add tasks, a&lt;br&gt;
function can compute a command. So the file is not always the answer to&lt;br&gt;
"what will this task do?". &lt;code&gt;vx show&lt;/code&gt; is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx show @demo/web#build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Every project, one line each
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;vx show
@demo/api   packages/api   2 tasks
@demo/docs  packages/docs  2 tasks
@demo/ui    packages/ui    2 tasks
@demo/web   packages/web   2 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One task, resolved
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;vx show @demo/web#build
@demo/web — packages/web

build
  &lt;span class="nb"&gt;command&lt;/span&gt;:       &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; dist &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp &lt;/span&gt;src/index.ts dist/index.js
  dependsOn:     ^build
  inputs.files:  src/&lt;span class="k"&gt;**&lt;/span&gt;
  outputs.files: dist/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the config the cache key sees, not the source text. When a&lt;br&gt;
preset sets &lt;code&gt;outputs&lt;/code&gt; or a plugin rewrites a command, it shows here.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgU1JDWyJ2eC5jb25maWcudHMiXSAtLT4gRVZBTFsiZXZhbHVhdGUiXQogIFBSRVsicHJlc2V0cyJdIC0tPiBFVkFMCiAgUExVR1sicGx1Z2lucyJdIC0tPiBFVkFMCiAgRVZBTCAtLT4gU0hPV1sidnggc2hvdyJdCiAgRVZBTCAtLT4gS0VZWyJjYWNoZSBrZXkiXQogIHN0eWxlIFNIT1cgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgU1JDWyJ2eC5jb25maWcudHMiXSAtLT4gRVZBTFsiZXZhbHVhdGUiXQogIFBSRVsicHJlc2V0cyJdIC0tPiBFVkFMCiAgUExVR1sicGx1Z2lucyJdIC0tPiBFVkFMCiAgRVZBTCAtLT4gU0hPV1sidnggc2hvdyJdCiAgRVZBTCAtLT4gS0VZWyJjYWNoZSBrZXkiXQogIHN0eWxlIFNIT1cgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" alt="diagram" width="509" height="278"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The same answer, for agents
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--format json&lt;/code&gt; prints the object itself, so a script or an agent reads&lt;br&gt;
the task without parsing text:&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;$ &lt;/span&gt;vx show @demo/web#build &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"@demo/web"&lt;/span&gt;,
  &lt;span class="s2"&gt;"dir"&lt;/span&gt;: &lt;span class="s2"&gt;"packages/web"&lt;/span&gt;,
  &lt;span class="s2"&gt;"task"&lt;/span&gt;: &lt;span class="s2"&gt;"build"&lt;/span&gt;,
  &lt;span class="s2"&gt;"config"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"exec"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"command"&lt;/span&gt;: &lt;span class="s2"&gt;"mkdir -p dist &amp;amp;&amp;amp; sleep 0.3 &amp;amp;&amp;amp; cp src/index.ts dist/index.js"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"dependsOn"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"cache"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"inputs"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"files"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="s2"&gt;"outputs"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"files"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vx show --affected&lt;/code&gt; lists the projects a change reaches, without&lt;br&gt;
running anything.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/cli/" rel="noopener noreferrer"&gt;the CLI reference&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/features/vx-show/" rel="noopener noreferrer"&gt;vx show&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/vx-show/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>See the plan before you run it</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:17:32 +0000</pubDate>
      <link>https://dev.to/vzn-vx/see-the-plan-before-you-run-it-13fb</link>
      <guid>https://dev.to/vzn-vx/see-the-plan-before-you-run-it-13fb</guid>
      <description>&lt;p&gt;Before a long run, you want to know what it will do. &lt;code&gt;--dry&lt;/code&gt; answers&lt;br&gt;
without running anything: every task in the graph, whether the cache&lt;br&gt;
would answer it, its key, and how long it took last time.&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;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--dry&lt;/span&gt;
would run:
  ▶  @demo/api#test    cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;62269624  ~213ms
  ▶  @demo/api#build   cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;10361de3  ~326ms
  ◉  @demo/docs#test   cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         067839a7
  ◉  @demo/docs#build  cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         b5134b6e
  ◉  @demo/ui#build    cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         6a36fb4c
  ◉  @demo/ui#test     cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         d5097182
  ◉  @demo/web#test    cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         a5787dc7
  ◉  @demo/web#build   cache hit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;         731116aa

8 task&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; planned, 6 cache hits &lt;span class="o"&gt;(&lt;/span&gt;6 &lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;, 2 would run.
predicted: ~539ms wall · ~539ms total execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One edit to &lt;code&gt;api&lt;/code&gt;: two tasks would run, six come from the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real keys, real predictions
&lt;/h2&gt;

&lt;p&gt;A dry run derives the same keys a real run would and asks the same&lt;br&gt;
cache. A "hit" in the plan is a hit in the run. The time next to each&lt;br&gt;
task is what it took the last time it ran. The wall prediction lays&lt;br&gt;
those times on the graph: &lt;code&gt;api#test&lt;/code&gt; waits for &lt;code&gt;api#build&lt;/code&gt;, so the two&lt;br&gt;
run one after the other, ~539 ms.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgQ1siY29uZmlncyJdIC0tPiBHWyJ0YXNrIGdyYXBoIl0gLS0-IEtbImtleXMiXSAtLT4gUXsiY2FjaGU_In0KICBRIC0tPnxoaXR8IEhbIndvdWxkIHJlc3RvcmUiXQogIFEgLS0-fG1pc3N8IE1bIndvdWxkIGV4ZWMiXQogIHN0eWxlIFEgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgQ1siY29uZmlncyJdIC0tPiBHWyJ0YXNrIGdyYXBoIl0gLS0-IEtbImtleXMiXSAtLT4gUXsiY2FjaGU_In0KICBRIC0tPnxoaXR8IEhbIndvdWxkIHJlc3RvcmUiXQogIFEgLS0-fG1pc3N8IE1bIndvdWxkIGV4ZWMiXQogIHN0eWxlIFEgc3Ryb2tlOiNjNmY4NGUsc3Ryb2tlLXdpZHRoOjJweAo%3FbgColor%3Dwhite" alt="diagram" width="849" height="174"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  For scripts and agents
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--dry=json&lt;/code&gt; prints the same plan as JSON: tasks, keys, hit or miss,&lt;br&gt;
predicted times. An agent can decide whether a run is worth starting,&lt;br&gt;
or show a reviewer what a change will rebuild.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx run build &lt;span class="nt"&gt;--affected&lt;/span&gt; &lt;span class="nt"&gt;--dry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The graph itself
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--graph&lt;/code&gt; writes the task graph as Graphviz DOT, to stdout or a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx run build &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;graph.dot
dot &lt;span class="nt"&gt;-Tsvg&lt;/span&gt; graph.dot &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; graph.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/features/dry-run/" rel="noopener noreferrer"&gt;Plan before you run&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/cli/" rel="noopener noreferrer"&gt;the CLI reference&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/dry-run/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Run only what a change reaches</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:11:59 +0000</pubDate>
      <link>https://dev.to/vzn-vx/run-only-what-a-change-reaches-3c9p</link>
      <guid>https://dev.to/vzn-vx/run-only-what-a-change-reaches-3c9p</guid>
      <description>&lt;p&gt;A pull request that touches one library should not test forty apps.&lt;br&gt;
&lt;code&gt;--affected&lt;/code&gt; runs a task only in the projects a change reaches: the ones&lt;br&gt;
whose files changed, and every project that depends on them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--affected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it picks
&lt;/h2&gt;

&lt;p&gt;Four packages. The branch edits &lt;code&gt;ui&lt;/code&gt;. &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;docs&lt;/code&gt; depend on &lt;code&gt;ui&lt;/code&gt;;&lt;br&gt;
&lt;code&gt;api&lt;/code&gt; depends on nothing.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgRVsiZWRpdCBwYWNrYWdlcy91aS9zcmMvaW5kZXgudHMiXSAtLT4gVUlbIkBkZW1vL3VpIl0KICBVSSAtLT4gV0VCWyJAZGVtby93ZWIiXQogIFVJIC0tPiBET0NTWyJAZGVtby9kb2NzIl0KICBBUElbIkBkZW1vL2FwaSJdCiAgc3R5bGUgRSBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4CiAgc3R5bGUgQVBJIHN0cm9rZS1kYXNoYXJyYXk6NCA0LG9wYWNpdHk6MC41Cg%3FbgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgRVsiZWRpdCBwYWNrYWdlcy91aS9zcmMvaW5kZXgudHMiXSAtLT4gVUlbIkBkZW1vL3VpIl0KICBVSSAtLT4gV0VCWyJAZGVtby93ZWIiXQogIFVJIC0tPiBET0NTWyJAZGVtby9kb2NzIl0KICBBUElbIkBkZW1vL2FwaSJdCiAgc3R5bGUgRSBzdHJva2U6I2M2Zjg0ZSxzdHJva2Utd2lkdGg6MnB4CiAgc3R5bGUgQVBJIHN0cm9rZS1kYXNoYXJyYXk6NCA0LG9wYWNpdHk6MC41Cg%3FbgColor%3Dwhite" alt="diagram" width="664" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask for the plan first. &lt;code&gt;--dry&lt;/code&gt; prints it and runs nothing:&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;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--affected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;master &lt;span class="nt"&gt;--dry&lt;/span&gt;
would run:
  ▶  @demo/docs#test   cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;067839a7  ~216ms
  ▶  @demo/docs#build  cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;b5134b6e  ~313ms
  ▶  @demo/ui#build    cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;6a36fb4c  ~320ms
  ▶  @demo/ui#test     cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;d5097182  ~209ms
  ▶  @demo/web#test    cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;a5787dc7  ~216ms
  ▶  @demo/web#build   cache miss — would &lt;span class="nb"&gt;exec   &lt;/span&gt;731116aa  ~311ms

6 task&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; planned, 6 would run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;api&lt;/code&gt; is not in it. The run shows the same set, three of four projects:&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;$ &lt;/span&gt;vx run &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--affected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;master
 ⏺︎   313ms success miss     @demo/ui#build
 ⏺︎   205ms success miss     @demo/ui#test
 ⏺︎   309ms success miss     @demo/web#build
 ⏺︎   311ms success miss     @demo/docs#build
 ⏺︎   204ms success miss     @demo/web#test
 ⏺︎   205ms success miss     @demo/docs#test

─ vx 0.0.0 ───────────────────────────────────────────
  projects  3 &lt;span class="k"&gt;in &lt;/span&gt;run · 4 total
  tasks     6 success · 6 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which base
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--affected&lt;/code&gt; compares against a git base. Name one (&lt;code&gt;--affected=main&lt;/code&gt;),&lt;br&gt;
or let vx pick: the workspace's &lt;code&gt;affectedBase&lt;/code&gt;, else &lt;code&gt;origin/HEAD&lt;/code&gt;, else&lt;br&gt;
&lt;code&gt;main&lt;/code&gt; or &lt;code&gt;master&lt;/code&gt;, else &lt;code&gt;HEAD~1&lt;/code&gt;. On a pull request, set&lt;br&gt;
&lt;code&gt;affectedBase&lt;/code&gt; once in &lt;code&gt;vx.workspace.ts&lt;/code&gt; and every run agrees.&lt;/p&gt;

&lt;p&gt;It is sugar for a filter, &lt;code&gt;--filter "...[&amp;lt;base&amp;gt;]"&lt;/code&gt;: the projects changed&lt;br&gt;
since the base, and their dependents. So it combines with any other&lt;br&gt;
filter, and &lt;code&gt;vx show --affected&lt;/code&gt; lists the projects without running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the cache still matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--affected&lt;/code&gt; decides which projects are in the run. The cache key still&lt;br&gt;
decides which tasks execute. A file outside every task's inputs, a&lt;br&gt;
README say, puts its project in the set, and every task there is a hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edges are task edges
&lt;/h2&gt;

&lt;p&gt;vx follows the task graph, not only &lt;code&gt;package.json&lt;/code&gt;. A task that reads&lt;br&gt;
another project's output through &lt;code&gt;dependsOn&lt;/code&gt; is downstream of it, even&lt;br&gt;
without a package dependency. So the set is what your tasks really&lt;br&gt;
depend on.&lt;/p&gt;

&lt;p&gt;Learn more: &lt;a href="https://vznjs.github.io/vx/cli/#--affectedbase" rel="noopener noreferrer"&gt;the CLI reference&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/features/affected/" rel="noopener noreferrer"&gt;Filters&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/affected/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Observability that cannot break a run</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:00:51 +0000</pubDate>
      <link>https://dev.to/vzn-vx/observability-that-cannot-break-a-run-4pd2</link>
      <guid>https://dev.to/vzn-vx/observability-that-cannot-break-a-run-4pd2</guid>
      <description>&lt;p&gt;Every build tool eventually grows an integration point for "tell&lt;br&gt;
someone what happened": Sentry, Slack, a metrics endpoint, an&lt;br&gt;
OpenTelemetry collector. Every one of those integrations is also a way&lt;br&gt;
for a network hiccup to fail a build, and the usual defence is a&lt;br&gt;
policy: please catch your errors, please do not block.&lt;/p&gt;

&lt;p&gt;vx's &lt;code&gt;telemetry&lt;/code&gt; capability makes it a structure instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TelemetrySink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;wants&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.log&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.sample&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run.end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;onRecord&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TelemetryRecord&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;          &lt;span class="c1"&gt;// must return promptly; buffer here&lt;/span&gt;
  &lt;span class="nx"&gt;onRunSummary&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RunSummaryRecord&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;    &lt;span class="c1"&gt;// one per run, at the end&lt;/span&gt;
  &lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;        &lt;span class="c1"&gt;// awaited at end of run, time-bounded;&lt;/span&gt;
                                                    &lt;span class="c1"&gt;// `signal` aborts at the deadline&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sink is handed plain-data records and a read-only context: the&lt;br&gt;
workspace root, the cache directory, a &lt;code&gt;warn&lt;/code&gt; function. No event bus,&lt;br&gt;
no cache handle, no run request. There is no method on anything it&lt;br&gt;
receives that reaches scheduling, caching or execution. The guarantee&lt;br&gt;
that a sink cannot change a build is not a rule sinks follow; it is the&lt;br&gt;
absence of a path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash isolation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If a sink throws, from &lt;code&gt;onRecord&lt;/code&gt;, &lt;code&gt;onRunSummary&lt;/code&gt; or &lt;code&gt;flush&lt;/code&gt;, it is
&lt;strong&gt;disabled for the rest of the run&lt;/strong&gt; and a warning is printed. Other
sinks keep receiving records. The run's outcome is unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onRecord&lt;/code&gt; must return promptly, so the contract says buffer and do
not await. &lt;code&gt;flush()&lt;/code&gt; is the one awaited drain point, at the end of
the run. All sinks flush at once under one shared bound, three
seconds by default (&lt;code&gt;VX_TEARDOWN_TIMEOUT_MS&lt;/code&gt; overrides it), so a
wedged collector cannot hold the process's exit hostage.&lt;/li&gt;
&lt;li&gt;A sink that lists what it &lt;code&gt;wants&lt;/code&gt; costs the source nothing for the
kinds it skips; the large &lt;code&gt;task.log&lt;/code&gt; stream is off unless asked for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other direction is isolated too. A plugin whose &lt;code&gt;setup()&lt;/code&gt; throws&lt;br&gt;
aborts the run before any work starts, naming the plugin, because a&lt;br&gt;
broken plugin should fail loudly and early rather than silently&lt;br&gt;
degrade. An &lt;code&gt;executor&lt;/code&gt; or &lt;code&gt;cache&lt;/code&gt; factory that throws aborts the same&lt;br&gt;
way; those are load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero cost when absent
&lt;/h2&gt;

&lt;p&gt;The design has a second property that matters as much as safety: &lt;strong&gt;no&lt;br&gt;
telemetry plugin means no telemetry cost&lt;/strong&gt;. No bus subscriber is&lt;br&gt;
registered, no run summary is assembled, no &lt;code&gt;git&lt;/code&gt; is spawned for&lt;br&gt;
provenance. A workspace with no sinks pays nothing for the capability&lt;br&gt;
existing. That is the general rule for every seam in vx: a stage nobody&lt;br&gt;
fills is not a no-op call, it is no call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is built on it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vzn/vx-otel&lt;/code&gt;&lt;/strong&gt; maps each run to OTLP traces, metrics and logs over
HTTP/JSON with no OpenTelemetry SDK dependency. The wire format is
small and the SDK is not. Each task's span is sent as the task ends,
so a dashboard follows a CI run while it runs; no send ever holds a
task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@vzn/vx-ci&lt;/code&gt;&lt;/strong&gt; writes every run as a GitHub Actions job summary
and, given a token, a completed check run on the built commit, so a
red run explains itself in the pull request's checks list.&lt;/li&gt;
&lt;li&gt;Anything else is a few dozen lines: buffer records in &lt;code&gt;onRecord&lt;/code&gt;,
post them in &lt;code&gt;flush&lt;/code&gt;. The guide has a runnable sink against the
exported types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same rule for remote caches
&lt;/h2&gt;

&lt;p&gt;The never-fail discipline is not only for telemetry. A remote cache&lt;br&gt;
layer that errors, times out or is unreachable degrades to a miss on&lt;br&gt;
that layer, the lookup continues down the chain to the local cache,&lt;br&gt;
and the run completes. Every remote path (get, put, ingest, prefetch)&lt;br&gt;
is wrapped the same way; a remote outage is a slower run, never a&lt;br&gt;
broken one, and a plugin that never fails still warns so you know it&lt;br&gt;
happened.&lt;/p&gt;

&lt;p&gt;The guide is &lt;a href="https://vznjs.github.io/vx/guides/plugins/" rel="noopener noreferrer"&gt;Writing a vx plugin&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/telemetry-never-breaks-a-run/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Remote execution without moving the scheduler</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:55:19 +0000</pubDate>
      <link>https://dev.to/vzn-vx/remote-execution-without-moving-the-scheduler-5b6i</link>
      <guid>https://dev.to/vzn-vx/remote-execution-without-moving-the-scheduler-5b6i</guid>
      <description>&lt;p&gt;Distributed builds usually arrive as a platform: a service that owns&lt;br&gt;
the graph, agents that run it, a dashboard that shows it. vx's version&lt;br&gt;
is a plugin that fills two seams, &lt;code&gt;executor&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt;, against a&lt;br&gt;
wire that already exists: Bazel's Remote Execution API, spoken by&lt;br&gt;
NativeLink, BuildBuddy, Buildbarn and bazel-remote.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vx.workspace.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineWorkspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reapi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx-reapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;reapi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grpcs://cache.example.com:443&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;execute&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="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;OSFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Linux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;container-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docker://node:22&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&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;Execution is off unless &lt;code&gt;execute: true&lt;/code&gt; is set, even with the plugin&lt;br&gt;
declared for caching. Changing where a build runs is not something a&lt;br&gt;
plugin should do by being present.&lt;/p&gt;
&lt;h2&gt;
  
  
  The scheduler never leaves
&lt;/h2&gt;

&lt;p&gt;vx owns the task graph and decides placement once per task, before&lt;br&gt;
scheduling. Telemetry, retries, timeouts, the cache and the logger&lt;br&gt;
behave exactly as they do locally, because none of them moved. The&lt;br&gt;
executor's job is one function: run this command with these inputs and&lt;br&gt;
give me the outputs. &lt;code&gt;capacity&lt;/code&gt; gives it its own scheduler pool, so a&lt;br&gt;
64-wide fleet is not throttled by a laptop's core count and remote&lt;br&gt;
tasks reserve none of the local CPU budget.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vx run --dry&lt;/code&gt; prints the decision per line: &lt;code&gt;@vx/reapi&lt;/code&gt;, &lt;code&gt;@local&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;@noop&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What goes remote
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only cacheable tasks.&lt;/strong&gt; A task with no &lt;code&gt;cache&lt;/code&gt; block has no
declared inputs, so a worker would run it against an empty tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not persistent tasks, or anything depending on one.&lt;/strong&gt; A worker
cannot reach a port on your machine; the placement stage knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not sandboxed tasks, or anything depending on one.&lt;/strong&gt; The sandbox
is local machinery a worker does not have, and a boundary verified
remotely would pass vacuously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not &lt;code&gt;exec.remote: false&lt;/code&gt;, or anything depending on it.&lt;/strong&gt; A task
that talks to Docker, a device or a local daemon is pinned by one
field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a task whose key folds a runtime probe&lt;/strong&gt; (&lt;code&gt;cache.inputs.runtime&lt;/code&gt;,
&lt;code&gt;workspaceRuntime&lt;/code&gt;): the key holds this machine's &lt;code&gt;node -v&lt;/code&gt;, which a
worker cannot prove it shares. Its dependants may still go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A task's inputs on the worker are exactly what its cache key declares:&lt;br&gt;
&lt;code&gt;cache.inputs.files&lt;/code&gt;, resolved env values, upstream outputs. Ambient&lt;br&gt;
state such as an undeclared &lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;.npmrc&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;node_modules&lt;/code&gt; is not in the key and therefore not on the worker. The&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/blog/the-sandbox/" rel="noopener noreferrer"&gt;sandbox&lt;/a&gt; is how you find the gap before you mark a&lt;br&gt;
task remote-eligible: a task that passes locally with the declared&lt;br&gt;
paths as its only reads will pass on a worker.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;node_modules&lt;/code&gt;: install as an action
&lt;/h2&gt;

&lt;p&gt;Workers are stateless and &lt;code&gt;node_modules&lt;/code&gt; is ambient. The answer is an&lt;br&gt;
explicit install task pinned to the pool with &lt;code&gt;exec.remote: 'only'&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pnpm install --frozen-lockfile&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;only&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pnpm-lock.yaml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/**&lt;/span&gt;&lt;span class="dl"&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;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;exec&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tsc -p .&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&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="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/**&lt;/span&gt;&lt;span class="dl"&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;Verified end to end against a live NativeLink pool: &lt;code&gt;install&lt;/code&gt; runs on a&lt;br&gt;
worker, once per lockfile change, and its outputs never touch your&lt;br&gt;
disk. A dependent's input tree references the install outputs in the&lt;br&gt;
remote content-addressed store, so the bytes flow worker to store to&lt;br&gt;
worker without transiting your machine. With no remote executor&lt;br&gt;
declared, &lt;code&gt;install&lt;/code&gt; is a local no-op and dependents use whatever your&lt;br&gt;
machine has, so a laptop run behaves as it did before the field&lt;br&gt;
existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  One artifact format, both directions
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;tar.zst&lt;/code&gt; bytes serve the local cache and the remote's&lt;br&gt;
action cache. Nothing is repacked at the boundary. A remote error, a&lt;br&gt;
timeout, an unreachable endpoint all degrade to a miss on that layer&lt;br&gt;
and the run continues locally; a cache-only server that advertises no&lt;br&gt;
execution capability declines the executor with a warning and keeps&lt;br&gt;
serving the cache.&lt;/p&gt;

&lt;p&gt;None of this is in core. Core has the two seams and the placement&lt;br&gt;
stage; &lt;code&gt;@vzn/vx-reapi&lt;/code&gt; is the proof they are wide enough. The guide,&lt;br&gt;
including worker image requirements and how output globs travel over a&lt;br&gt;
wire that has no globs, is &lt;a href="https://vznjs.github.io/vx/guides/ci/#remote-execution" rel="noopener noreferrer"&gt;Remote&lt;br&gt;
execution&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/remote-execution/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Flaky is a claim only declared inputs can back</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:49:46 +0000</pubDate>
      <link>https://dev.to/vzn-vx/flaky-is-a-claim-only-declared-inputs-can-back-1pal</link>
      <guid>https://dev.to/vzn-vx/flaky-is-a-claim-only-declared-inputs-can-back-1pal</guid>
      <description>&lt;p&gt;Flaky-test detection is one of the features that usually lives behind&lt;br&gt;
a cloud login. Nx sells it. It is also one of the easiest things to get&lt;br&gt;
wrong, because "flaky" is used to mean "failed and I do not want to&lt;br&gt;
look," and a tool that agrees with that use is teaching a team to&lt;br&gt;
ignore red.&lt;/p&gt;

&lt;p&gt;vx defines it narrowly, and the narrow definition is the feature.&lt;/p&gt;
&lt;h2&gt;
  
  
  The definition
&lt;/h2&gt;

&lt;p&gt;A task is &lt;strong&gt;flaky&lt;/strong&gt; when one of two things is on record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its exact cache key &lt;strong&gt;failed after it had passed&lt;/strong&gt;, this run
included (a cache hit counts as a pass; it replayed one), or&lt;/li&gt;
&lt;li&gt;it needed a &lt;strong&gt;retry&lt;/strong&gt; this run (&lt;code&gt;exec.retries&lt;/code&gt; or &lt;code&gt;--retry&lt;/code&gt;) and then
passed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failure on a key that never passed is a &lt;strong&gt;break&lt;/strong&gt;. The inputs&lt;br&gt;
changed and the result is red, which is what a red run usually means,&lt;br&gt;
and it is not listed. Nor is the first pass after only failures: that&lt;br&gt;
is a recovery (a missing tool installed), not a flake. Only tasks with a &lt;code&gt;cache&lt;/code&gt; block are judged at&lt;br&gt;
all: "same inputs, different outcome" is a claim only declared inputs&lt;br&gt;
can back. A task without them is keyed on its config alone, and two&lt;br&gt;
runs of it are not the same inputs in any meaningful sense.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you see
&lt;/h2&gt;

&lt;p&gt;A flaky task says so on its own row in the run's task list, in a dim&lt;br&gt;
note after its name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ◼︎   4.21s failed  miss     app#test flaky - passed 3× before
 ⏺︎  12.84s success miss     api#e2e flaky - failed 1× before · 2 attempts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A task that was not flaky carries no note. &lt;code&gt;vx info&lt;/code&gt; keeps the&lt;br&gt;
standing list across runs, &lt;code&gt;--summarize&lt;/code&gt; carries it as typed data&lt;br&gt;
(&lt;code&gt;flaky: { passes, failures, attempts }&lt;/code&gt; on the task, so a consumer can&lt;br&gt;
tell a break from a flake without parsing text), and the MCP server&lt;br&gt;
reports the same signal to an agent — &lt;code&gt;getRunHistory&lt;/code&gt; as each task's&lt;br&gt;
failure mode, &lt;code&gt;getWorkspaceInfo&lt;/code&gt; as the standing list — so an agent&lt;br&gt;
does not learn to shrug at a repeated failure on changing inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is free
&lt;/h2&gt;

&lt;p&gt;The information was already there. Every cache entry records its key,&lt;br&gt;
its outcome and its run. Asking "has this key ever had the other&lt;br&gt;
outcome" is one probe of the failed-row index on a green miss, and&lt;br&gt;
nothing at all on a run that executed nothing. There is no upload&lt;br&gt;
because there is nothing to upload to; the history is the local&lt;br&gt;
&lt;code&gt;cache.db&lt;/code&gt; that &lt;code&gt;vx why&lt;/code&gt; and &lt;code&gt;vx last&lt;/code&gt; already read.&lt;/p&gt;

&lt;p&gt;That is also why it works on the first day. A service-side detector&lt;br&gt;
needs a fleet of runs before it can say anything. vx's needs the second&lt;br&gt;
run of the same key on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;It is not a retry policy. &lt;code&gt;exec.retries&lt;/code&gt; is separate and explicit, and&lt;br&gt;
a task that passes on retry is &lt;em&gt;reported&lt;/em&gt; as flaky rather than quietly&lt;br&gt;
made green. It is not a quarantine: nothing is skipped, ever, because a&lt;br&gt;
skip is a silent pass. It is a label on a fact, kept where you can act&lt;br&gt;
on it, and the act is yours.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/flaky-tasks/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A lockfile bump should re-key two tasks, not sixty</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:44:14 +0000</pubDate>
      <link>https://dev.to/vzn-vx/a-lockfile-bump-should-re-key-two-tasks-not-sixty-2b5m</link>
      <guid>https://dev.to/vzn-vx/a-lockfile-bump-should-re-key-two-tasks-not-sixty-2b5m</guid>
      <description>&lt;p&gt;Out of the box, vx folds the whole lockfile into the workspace&lt;br&gt;
fingerprint (Turborepo keys each package on the lockfile changes that&lt;br&gt;
reach it), and the workspace fingerprint is in&lt;br&gt;
every task's key. That is coarse but correct. Any &lt;code&gt;pnpm install&lt;/code&gt; that&lt;br&gt;
changes &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; invalidates every cached task, and &lt;code&gt;--affected&lt;/code&gt;&lt;br&gt;
selects every project.&lt;/p&gt;

&lt;p&gt;It is also the single largest source of "why did everything rebuild"&lt;br&gt;
in a large workspace. A patch bump to a test utility used by one&lt;br&gt;
package re-runs the builds of all of them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Key each project on what it can reach
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@vzn/vx-lockfile&lt;/code&gt; ships one plugin per package manager: &lt;code&gt;pnpm()&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;bun()&lt;/code&gt;, &lt;code&gt;npm()&lt;/code&gt;, &lt;code&gt;yarn()&lt;/code&gt;. Each claims its lockfile out of the&lt;br&gt;
workspace fingerprint through the &lt;code&gt;fingerprint&lt;/code&gt; seam and instead&lt;br&gt;
contributes, per task, a digest of &lt;strong&gt;its project's resolved dependency&lt;br&gt;
closure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vx.workspace.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineWorkspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pnpm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vzn/vx-lockfile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;pnpm&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;Nothing else changes. &lt;code&gt;vx why&lt;/code&gt; names the material as&lt;br&gt;
&lt;code&gt;plugin @vzn/vx-lockfile/pnpm&lt;/code&gt;, and the workspace fingerprint line&lt;br&gt;
stops moving when the lockfile is edited.&lt;/p&gt;

&lt;p&gt;The closure is exactly what the project's &lt;code&gt;node_modules&lt;/code&gt; can resolve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt; and &lt;code&gt;optionalDependencies&lt;/code&gt;,
transitively through the lockfile's own resolution graph;&lt;/li&gt;
&lt;li&gt;each package by name, version &lt;strong&gt;and resolved peers&lt;/strong&gt;, because
&lt;code&gt;foo@1(react@18)&lt;/code&gt; and &lt;code&gt;foo@1(react@19)&lt;/code&gt; are different directories on
disk, plus its integrity and any patch applied to it;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;link:&lt;/code&gt; or &lt;code&gt;workspace:&lt;/code&gt; dependency folds the linked workspace
package's whole reach, since what A can import through B is B's
closure;&lt;/li&gt;
&lt;li&gt;install-wide material every project folds: the lockfile version and
fields such as pnpm's &lt;code&gt;settings&lt;/code&gt;. Under &lt;code&gt;pnpm()&lt;/code&gt; and &lt;code&gt;bun()&lt;/code&gt;,
overrides, catalogs and patches count only where they reach.&lt;/li&gt;
&lt;li&gt;the root package's own closure, folded into every project (added
2026-09-24): the root's tools run from the root &lt;code&gt;node_modules/.bin&lt;/code&gt;
on every task's PATH, so a root devDependency bump that re-keyed
nothing replayed the old tool's output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;bun()&lt;/code&gt; does the same through Bun's hoisted layout (a nested version&lt;br&gt;
counts for the package it is nested under and no other); &lt;code&gt;npm()&lt;/code&gt;&lt;br&gt;
through &lt;code&gt;package-lock.json&lt;/code&gt;'s &lt;code&gt;packages&lt;/code&gt; map; &lt;code&gt;yarn()&lt;/code&gt; per workspace&lt;br&gt;
through berry's descriptors. Yarn classic records no workspaces, so&lt;br&gt;
every project folds one root digest, which is coarse and honest about&lt;br&gt;
what that file contains.&lt;/p&gt;

&lt;p&gt;A phantom dependency, imported but never declared, is in no closure.&lt;br&gt;
Declare it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;--affected&lt;/code&gt; follows the same claim
&lt;/h2&gt;

&lt;p&gt;The plugin's &lt;code&gt;affected(change)&lt;/code&gt; hook answers the selection question&lt;br&gt;
too. &lt;code&gt;vx run test --affected&lt;/code&gt; with a lockfile diff selects the projects&lt;br&gt;
whose closure the diff reaches, not every project. The rule that keys a&lt;br&gt;
task and the rule that selects it are one rule, so they cannot drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured in the repository that ships it
&lt;/h2&gt;

&lt;p&gt;vx's own repository declares &lt;code&gt;bun()&lt;/code&gt;. Measured 2026-10-08 with&lt;br&gt;
&lt;code&gt;vx run ci --all --dry=json&lt;/code&gt; before and after a version bump in&lt;br&gt;
&lt;code&gt;bun.lock&lt;/code&gt;: bumping &lt;code&gt;astro&lt;/code&gt;, which only the docs site reaches, re-keys&lt;br&gt;
that site's 6 tasks of the gate's 57; without the plugin, all 57&lt;br&gt;
re-key.&lt;/p&gt;

&lt;p&gt;The root's closure is in every key, and this repository's root links&lt;br&gt;
seven of its packages as devDependencies, so bumping &lt;code&gt;protobufjs&lt;/code&gt;&lt;br&gt;
(reached through &lt;code&gt;@vzn/vx-reapi&lt;/code&gt;) re-keys every task either way. A&lt;br&gt;
dependency change costs what the dependency change touches, and the&lt;br&gt;
root touches every task.&lt;/p&gt;

&lt;p&gt;The plugin is separate from core on purpose. Core owns the &lt;code&gt;fingerprint&lt;/code&gt;&lt;br&gt;
seam, the memo and the per-run gate; the lockfile &lt;em&gt;parsers&lt;/em&gt; are&lt;br&gt;
package-manager knowledge, and package-manager knowledge changes on the&lt;br&gt;
package manager's schedule. The guide is&lt;br&gt;
&lt;a href="https://vznjs.github.io/vx/guides/configure/#lockfiles" rel="noopener noreferrer"&gt;Configure › Lockfiles&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/lockfile-aware-keys/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>vx lock: freezing what the key sees</title>
      <dc:creator>VX</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:38:41 +0000</pubDate>
      <link>https://dev.to/vzn-vx/vx-lock-freezing-what-the-key-sees-3n1o</link>
      <guid>https://dev.to/vzn-vx/vx-lock-freezing-what-the-key-sees-3n1o</guid>
      <description>&lt;p&gt;A TypeScript config buys composition and costs one guarantee: what it&lt;br&gt;
evaluates to can depend on where it is evaluated. &lt;code&gt;process.env.CI&lt;/code&gt;,&lt;br&gt;
a &lt;code&gt;Date&lt;/code&gt;, an import that resolved differently on another machine. On a&lt;br&gt;
laptop that is fine. In a pipeline that must run exactly the config a&lt;br&gt;
reviewer approved, it is a hole.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vx lock&lt;/code&gt; closes it the way package managers closed the same hole for&lt;br&gt;
dependencies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three verbs
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx lock              &lt;span class="c"&gt;# evaluate every vx.config.* now; write vx-lock.json&lt;/span&gt;
vx lock &lt;span class="nt"&gt;--check&lt;/span&gt;      &lt;span class="c"&gt;# audit: hashes + full re-evaluation against the lock; exit 1 on drift&lt;/span&gt;
vx run … &lt;span class="nt"&gt;--frozen&lt;/span&gt;    &lt;span class="c"&gt;# consume the lock; no evaluation at all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;vx lock&lt;/code&gt; evaluates each config in the current environment and stores&lt;br&gt;
the post-evaluation object plus a content hash of the config file.&lt;br&gt;
Plain runs &lt;strong&gt;always&lt;/strong&gt; evaluate live; the lock's existence changes&lt;br&gt;
nothing about &lt;code&gt;vx run&lt;/code&gt;. Only &lt;code&gt;--frozen&lt;/code&gt; consumes it, and under&lt;br&gt;
&lt;code&gt;--frozen&lt;/code&gt; there is no evaluation and no staleness check of its own: a&lt;br&gt;
config's env reads keep their lock-time values, a project absent from&lt;br&gt;
the lock is a hard error, a missing lock is a hard error.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check that a hash cannot do
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--check&lt;/code&gt; does two things, and the second is the reason the verb&lt;br&gt;
exists. It compares the stored content hash of each config file, which&lt;br&gt;
catches an edit. Then it re-evaluates every config in the current&lt;br&gt;
environment and compares the result to the frozen object, which&lt;br&gt;
catches what an edit-detector cannot: an env value read at evaluation&lt;br&gt;
time that differs from the lock's, an imported preset that changed, a&lt;br&gt;
computed command whose input moved. Every mismatched project is listed&lt;br&gt;
on stderr and the exit code is 1.&lt;/p&gt;

&lt;p&gt;The CI recipe is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vx lock &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; vx run ci &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line proves the lock still describes the configs as this&lt;br&gt;
machine would evaluate them. The second runs exactly the lock. A pull&lt;br&gt;
request that changes a config without re-running &lt;code&gt;vx lock&lt;/code&gt; fails the&lt;br&gt;
first line, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the lock is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a cache key input.&lt;/strong&gt; &lt;code&gt;vx-lock.json&lt;/code&gt; is excluded from every
task's input set and from the workspace fingerprint, so running &lt;code&gt;vx
lock&lt;/code&gt; does not re-key the workspace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a substitute for &lt;code&gt;cache.inputs.env&lt;/code&gt;.&lt;/strong&gt; The lock freezes what a
config &lt;em&gt;evaluated to&lt;/em&gt;. An env var a task reads at run time is still a
run-time input and belongs under &lt;code&gt;cache.inputs.env&lt;/code&gt;, where it joins
the key. The two mechanisms are parallel on purpose: config-time
values are locked, run-time values are keyed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a performance feature.&lt;/strong&gt; Evaluation is already cheap and cached
where it is provably pure. &lt;code&gt;--frozen&lt;/code&gt; is about determinism, and the
milliseconds it saves are incidental.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  For agents
&lt;/h2&gt;

&lt;p&gt;An agent asks the same question in JSON: &lt;code&gt;vx lock --check --format json&lt;/code&gt;&lt;br&gt;
prints &lt;code&gt;{"upToDate": false, "audited": 4, "notAudited": 0, "problems": [...]}&lt;/code&gt;&lt;br&gt;
with the same exit code, and &lt;code&gt;vx mcp&lt;/code&gt; answers it as &lt;code&gt;checkLock&lt;/code&gt;. Each&lt;br&gt;
problem is the line a person would read on stderr.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it came from
&lt;/h2&gt;

&lt;p&gt;The lock exists because the resolved-config hash made configs&lt;br&gt;
powerful, and power needs an audit. Package managers learned the same&lt;br&gt;
lesson with &lt;code&gt;--frozen-lockfile&lt;/code&gt;: let resolution be dynamic in&lt;br&gt;
development and pin it where reproducibility is the contract. The&lt;br&gt;
design note is in the repository under &lt;code&gt;docs/design/config-lock&lt;/code&gt;; the&lt;br&gt;
reference is &lt;a href="https://vznjs.github.io/vx/cli/#vx-lock" rel="noopener noreferrer"&gt;&lt;code&gt;vx lock&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://vznjs.github.io/vx/blog/lock-and-frozen/" rel="noopener noreferrer"&gt;vx blog&lt;/a&gt;. vx is an MIT task runner and build cache for JS monorepos: &lt;a href="https://github.com/vznjs/vx" rel="noopener noreferrer"&gt;github.com/vznjs/vx&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
