Skip to content

feat(run): include nested vp run plans in --dry-run --json - #967

Merged
wan9chi merged 1 commit into
claude/dry-run-08-run-plan-jsonfrom
claude/dry-run-09-nested-plans
Nov 9, 2026
Merged

wan9chi merged 1 commit into
claude/dry-run-08-run-plan-jsonfrom
claude/dry-run-09-nested-plans

Conversation

@wan9chi

@wan9chi wan9chi commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Motivation

Since #966, vp run --dry-run --json fails for a task whose command runs vp run …. The dry-run RFC shows such a step as expandedFrom with its own plan, so the run plan is a tree, and each nested plan has its own dependencies.

Changes

// "nested": "vp run build"
{ "expandedFrom": "vp run build",
  "plan": { "build": { "package": ".", "name": "build", "steps": [ { "program": "…", "cache": { "status": "hit", … } } ] } } }
  • The layout recurses into nested graphs. Each plan's dependsOn is scoped to its own tasks, so the same task ID can appear at several levels.
  • expandedFrom is the item's command as written, with any args given after the task name.
  • A leaf's step is found by its path through the nested plans.
  • The error for tasks that run vp run goes away.

A nested vp run --dry-run is still a spawned step, which a dry run doesn't start. The e2e fixture run_plan_json_nested covers plans at several levels, args forwarded to a nested run, and a nested dry run.

🤖 Generated with Claude Code


Note

Medium Risk
Touches dry-run JSON output shape and reporter layout for nested graphs; behavior is covered by new e2e snapshots but consumers parsing the old flat step model need to handle NestedStep.

Overview
vp run --dry-run --json no longer errors when a task runs another vp run. Those steps are emitted as nested objects with expandedFrom (the command as written, including args after the task name) and an embedded plan in the same shape as the root document.

The JSON reporter recursively lays out nested execution graphs before cache checks. Each nested plan keeps dependsOn scoped to its own tasks, so the same task ID can appear at multiple levels. Cache status is applied by walking a path through nested plans rather than only top-level task/step indices.

The run-plan JSON Schema splits steps into CommandStep vs NestedStep, and the changelog documents the new behavior. E2E fixtures replace the former “not supported yet” error case with snapshots for multi-level nested plans, forwarded args, and nested vp run --dry-run (still shown as a spawn step, not expanded).

Reviewed by Cursor Bugbot for commit bc48a17. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

fspy benchmark

linux

dynamic/launch             change  +0.80%  [ -7.57% ..  +8.53%]  overhead   +61.18%
dynamic/access             change  +0.29%  [ -5.94% .. +19.74%]  overhead   +20.76%
dynamic/access-relative    change  +0.26%  [ -8.61% ..  +8.10%]  overhead   +73.19%
dynamic/access-contended   change  +4.77%  [ -9.21% .. +21.41%]  overhead    +4.58%
static/launch              change  +0.24%  [ -6.58% ..  +7.94%]  overhead  +196.57%
static/access              change  +0.35%  [ -4.34% ..  +3.38%]  overhead +1481.58%
static/access-relative     change  +0.37%  [ -2.09% ..  +1.89%]  overhead +1977.80%
static/access-contended    change  +0.35%  [ -1.18% ..  +2.43%]  overhead +2065.44%

macos

dynamic/launch             change  -0.27%  [ -4.75% ..  +5.72%]  overhead  +233.84%
dynamic/access             change  +0.36%  [ -2.43% ..  +4.48%]  overhead    +5.03%
dynamic/access-relative    change  -0.43%  [ -5.67% ..  +9.34%]  overhead  +253.12%
dynamic/access-contended   change  +1.67%  [ -8.19% .. +12.15%]  overhead    +2.67%

windows

dynamic/launch             change  -0.08%  [ -3.70% ..  +2.88%]  overhead   +32.40%
dynamic/access             change  +0.00%  [ -1.02% ..  +0.83%]  overhead    +1.47%
dynamic/access-relative    change  +0.00%  [ -0.61% ..  +0.82%]  overhead    +2.05%
dynamic/access-contended   change  -0.20%  [ -2.41% ..  +0.39%]  overhead    +1.76%

@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 7f725dd to c78783a Compare November 7, 2026 11:50
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from c78783a to 480cb60 Compare November 7, 2026 12:00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 480cb60. Configure here.

@wan9chi
wan9chi removed this pull request from stack #970 November 7, 2026 14:54
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 480cb60 to 7209175 Compare November 7, 2026 14:54
@wan9chi
wan9chi added this pull request to stack #973 November 7, 2026 14:55
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 7209175 to e96fb3d Compare November 7, 2026 15:07
@wan9chi
wan9chi removed this pull request from stack #973 November 7, 2026 17:18
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from e96fb3d to 968b86f Compare November 7, 2026 17:19
@wan9chi
wan9chi added this pull request to stack #975 November 7, 2026 17:19
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 968b86f to e76f876 Compare November 7, 2026 18:04
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from e76f876 to 1d46e32 Compare November 7, 2026 18:38
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 1d46e32 to 1e22ef3 Compare November 9, 2026 10:11
Since `vp run --dry-run --json` was added, it failed for a task whose
command runs `vp run …`. The RFC shows such a step as `expandedFrom`
with its own `plan`, so the run plan is a tree, and each nested plan has
its own dependencies.

- The layout recurses into nested graphs. Each plan's `dependsOn` is
  scoped to its own tasks, so the same task ID can appear at several
  levels.
- `expandedFrom` is the item's command as written, with the args given
  after the task name.
- A leaf's step is found by its path through the nested plans.
- The error for tasks that run `vp run` goes away.

A nested `vp run --dry-run` is still a spawned step, which a dry run
doesn't start. The e2e fixture `run_plan_json_nested` covers plans at
several levels, args forwarded to a nested run, and a nested dry run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wan9chi
wan9chi force-pushed the claude/dry-run-09-nested-plans branch from 1e22ef3 to bc48a17 Compare November 9, 2026 10:44
@wan9chi
wan9chi merged commit 38ada25 into main Nov 9, 2026
18 checks passed
@wan9chi
wan9chi deleted the claude/dry-run-09-nested-plans branch November 9, 2026 11:29
wan9chi added a commit that referenced this pull request Nov 9, 2026
## Motivation

Before starting a long run, there's no way to see which tasks would come
from the cache and which would run, or why one would miss, without
running them (#791, voidzero-dev/vite-plus#2848). [The dry-run
RFC](https://github.com/voidzero-dev/vite-task/blob/main/docs/rfcs/dry-run.md)
specifies `vp run --dry-run` for this. This is the first PR of the stack
that implements it: the text output. The PRs above it add every change
of a miss (#961), `-v` (#963), `--json` (#966, #967), `unknown` (#968)
and `-vv` (#969).

This supersedes #792 by @lifeiscontent, whose write-free lookup and test
scenario it builds on, so it credits them as co-author.

Closes #791.

## Changes

`vp run --dry-run` plans the run and checks the cache as a real run
does, the remote cache included, then prints each command's line and the
summary. It runs nothing and records nothing. The cache is opened as in
a real run, but no entry is written, a hit's outputs aren't downloaded
or restored, and `last-summary.json` isn't written, so `--last-details`
still shows the last real run.

```
$ vp run --dry-run test
$ vtt print-file src/a.txt ◉ cache hit
$ vtt print tested

---
vp run: 1/2 cache hit (50%), nothing was run.
```

- A dry run goes through the real run's lookup and spawn path, which
skips each step that would write or run: recording a local hit,
downloading a remote hit, replaying a hit, and spawning the command.
- Each line is a real run's, without `, executing` or `, replaying`, and
with no blank line between commands. `--log` is ignored, since it only
shapes task output: `--dry-run` and `--log` move from `RunFlags`, which
selects tasks, to `RunCommand`, and resolve to a `RunMode` of `Real {
log }` or `Dry`, so a dry run can't carry a log mode.
- The summary follows a real run's rules, with "nothing was run" in
place of the successful count and the time saved, and without the
`--last-details` hint. The detailed summary (`-v`) has no ✓ or exit
codes.

  Real run, unchanged:

  ```
vp run: 1/2 cache hit (50%), 2/2 successful. (Run `vp run
--last-details` for full details)
          │ │            │     │ │
          │ │            │     │ └─ tasks
          │ │            │     └─ tasks that succeeded
          │ │            └─ cache hit rate
          │ └─ tasks
          └─ cache hits
  ```

  Dry run:

  ```
  vp run: 1/2 cache hit (50%), nothing was run.
          │ │            │
          │ │            └─ cache hit rate
          │ └─ tasks
          └─ cache hits
  ```

- A cache lookup error is printed under its command and stops the dry
run, as it stops a real run, and both exit 1. Real runs stopped but
exited 0 before, since the summary only counted tasks that started; a
task that fails before it starts now fails the run. Otherwise a dry run
exits 0 once planning succeeds.
- A nested `vp run --dry-run` isn't inlined. It's started as a process
that prints its own dry run, so an enclosing dry run doesn't start it.
- Without a task, `--dry-run` is an error, like `vp run -v`. After the
task name, it's passed to the task, like any other flag.
- RFC: the summary and cache-error rules are written down, and the
sentence about the first changed file being the first in path order is
removed. Without `-v`, the file check stops at whichever changed file it
finds first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches cache lookup, remote fetch, spawn, and exit-code paths across
the runner; mistakes could skip real execution side effects or
mis-report failures, though behavior is gated on `RunKind::Dry`.
> 
> **Overview**
> Adds **`vp run --dry-run`**, which plans the task graph and performs
full cache lookups (including remote) but **does not** spawn processes,
replay outputs, download remote archives, write cache entries, or
persist `last-summary.json`.
> 
> The CLI introduces **`RunMode`/`RunKind`**: `--dry-run` and `--log`
move onto `RunCommand` (dry runs always use interleaved output).
Execution and cache layers thread `RunKind` through lookup, spawn, and
reporters so dry runs short-circuit after reporting cache status
(`WouldSpawn` / `DryRun` not-updated reasons).
> 
> **Output and summary** match a real run’s command lines and cache
indicators, but omit `, executing` / `, replaying`, skip extra blank
lines, use **“nothing was run”** instead of success/saved time, and omit
the `--last-details` hint. **`-v`** detailed summaries drop exit icons
and saved-time wording for hits.
> 
> **Bugfix:** runs now **exit 1** when a task’s cache cannot be checked
(e.g. DB unreadable), including failures before `start()`; nested `vp
run --dry-run` is not inlined. Changelog and dry-run RFC/e2e snapshots
document the behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3010605. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Aaron Reisman <aaron@lifeiscontent.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi added a commit that referenced this pull request Nov 9, 2026
…966)

## Motivation

Coding agents and CI need a dry run's answer as data, without parsing
text: which tasks would run, in what order, and why each would hit or
miss (#791, voidzero-dev/vite-plus#2848). [The dry-run
RFC](https://github.com/voidzero-dev/vite-task/blob/main/docs/rfcs/dry-run.md)
specifies the run plan that `vp run --dry-run --json` prints, and this
PR implements it through a new reporter. For now, a task whose command
runs `vp run` fails and says so; #967 adds nested plans.

## Changes

```jsonc
// vp run --dry-run --json build, after a.txt changed (indented in a terminal, one line when captured)
{
  "$schema": "file:///tmp/vp_run-1000/run-plan_<hash>.json",
  "version": 1,
  "workspaceRoot": "/…",
  "plan": {
    "build": {
      "package": ".",
      "name": "build",
      "steps": [{
        "program": "node_modules/.bin/vtt",
        "args": ["print-file", "a.txt", "b.txt"],
        "cache": {
          "status": "changed",
          "baseline": { "durationMs": 12 },
          "changes": [{ "kind": "file", "path": "a.txt", "change": "changed" }],
          "changesPartial": true
        }
      }]
    }
  }
}
```

- **CLI.** `--json` is no longer `exclusive`. It conflicts with an
explicit `--log`, and `RunCommand::resolve` checks the rest once, with
clap-style errors. `--json` alone resolves to the task graph, and with
`--dry-run` to a run whose `RunMode` is `DryJson`. With a task but no
`--dry-run`, the error says `--json` is only supported with `--dry-run`
for now. With other flags and no task, it's the same conflict error as
before. Without a task, `--dry-run --json` fails when it's planned, as
`--dry-run` does. The session matches on that, so nothing re-checks
`--json` later, and `plan_from_cli_run` resolves its command the same
way. It's never interactive: a task that isn't found is an error on
stderr, not the task list on stdout. An empty plan, such as when
`--filter` matches nothing, prints `"plan": {}`.
- **Cache data.** `Comparison` gains the baseline's duration and
`changes_partial`. `changes_partial` is set when the file check stopped
at the first changed file, or couldn't check some files. After a
command, config or env change, only the previous run's key is compared,
so the step is `changed` with its key changes, `changesPartial`, and no
`baseline`; the RFC's `baseline` is optional for that case. Program,
args and cwd changes keep what they changed from and to. vt_plan gets a
program accessor on the spawn fingerprint, and name and args accessors
on built-ins.
- **Reporter** (`session/reporter/json/`).
- When it's created, it lays out from the graph each task's ID, its
sorted `dependsOn`, and its steps (`program`, `args`, `cwd`, `hook`).
- Each leaf's `start` fills in its `cache`: `hit`, `changed`,
`noBaseline`, `error` or `disabled`, or absent for a built-in.
  - At the end, it serializes the whole document and writes it once.
- A run cancelled with Ctrl-C prints nothing. A failed cache lookup
stops the run, as it stops a real run. As with any error that stops the
document, the error goes to stderr and nothing is printed.
- **Document.** Rust types with doc comments, deriving schemars under
`cfg(test)`. A test regenerates the committed `schemas/run-plan.json`.
`artifact!` embeds it, and each run copies it to the user's temp folder
for `$schema`, as for `vp run --json`. Both documents are written by one
`json::write_document`, which serializes into memory first, so an error
partway prints nothing.
- **Tests.**
- `run_plan_json` covers the plan's layout: `-r` edges and `--parallel`,
hooks, programs inside and outside the workspace, `--no-cache` and
`--cache`, forwarded args, the shell, `cd` and the `cwd` setting,
built-ins, and encoded task IDs.
- Its `selection` case covers which tasks are planned: a task that some
or all selected packages lack, an unknown package, and a filter that
matches nothing, with and without `--fail-if-no-match`.
- `dry_run_json` covers the cache statuses and flags that combine with
`--dry-run --json`, or that are passed to the task when they come after
its name.
- Its `errors` case covers `--json` with a task but no `--dry-run`, with
`--log`, with `--last-details`, or with flags but no task; `--dry-run
--json` without a task; and a task that isn't found.
- In the plan snapshots, only the expected `--json build` error message
changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches CLI parsing, session dispatch, and cache comparison shapes
used by dry runs and reporters; behavior is well snapshot-tested but
flag combinations and cache edge cases warrant review.
> 
> **Overview**
> Adds **`vp run --dry-run --json`**, which emits a single **run-plan**
JSON document (tasks, `dependsOn`, per-step program/args/cwd/hook, and
projected cache status) for CI and agents, with a committed
**`schemas/run-plan.json`** and `$schema` materialized like the task
graph.
> 
> **CLI:** `--json` is no longer exclusive-only; with a task it
**requires `--dry-run`** (otherwise a clap-style error). Bare `vp run
--json` still dumps the task graph. Resolution flows through
`RunCommand::resolve` / `RunMode::DryJson`, and **`--json` conflicts
with `--log`**.
> 
> **Cache / JSON payload:** `Comparison` and `Change` now carry
**baseline duration**, **`changesPartial`**, and **from/to** details for
program, args, and cwd so misses serialize faithfully; validation
returns `Checked` with partial file lists.
> 
> **Execution path:** A new **JSON reporter** lays out the graph up
front, fills cache on each dry leaf `start`, writes once at the end (or
stderr-only on lookup failure / cancel / nested `vp run`, which is
rejected). Shared **`json::write_document`** and schema snapshot tests
cover both JSON outputs.
> 
> **Tests / docs:** E2E fixtures for plan layout, cache statuses, flags,
and errors; dry-run RFC examples updated for optional baseline and
`changesPartial`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
220dbb0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant