This directory holds the decision record, the spike that validated it, the review that reshaped the plans, and one implementation plan per pull request, in the order the PRs should land.
New to this work? Read HANDOFF.md first; its "Start here" section gives the reading order.
This README is the execution index.
00-assessment.md: the assessment and decision record (situation, options, impact, phased plan, spike results, module-by-module refactoring plan), with an errata section dated 2026-10-15. Read sections 10 and 11 before any plan.REVIEW-2026-10-15.md: the principal review of the first draft of the plans (structural findings S1–S13, per-plan defects, the landing strategy, the decisions taken in its section 6). The plans below are the reworked version.REVIEW-2026-10-16.md: the Codex review of the reworked plans (R01–R21 and eleven open decisions).TRIAGE-2026-10-16.mdverifies each finding and splits the follow-up into decisions (A, taken), a reconciliation pass (B, landed on 2026-10-16; the commit per item is listed in the triage) and local plan corrections (C, applied 2026-10-16 and 2026-10-17, one commit per row listed in the triage; its step-4 re-read is complete). Its A9–A11 are still open; see "Decisions and conventions" below. The organisation review of 2026-10-17 (/mnt/project-files/revue-organisation/, report in French) added the "Roles", "Release calendar" and "Open decisions with an owner" subsections, ground rule 8 and step 0 of "How to execute a plan".SPIKES.md: the two time-boxed spikes that sit inside Phase 2 (Spike #2 on Deno's Node compatibility and the dual-runtime window; Spike V on the test runner). Their outcomes set constants in PRs 28, 29, 31, 33 and 37.spike/: the throwaway tooling that ran the unmodified CLI on Node 22. It is the reference for PR 26._TEMPLATE.md: plan structure and the normative shared vocabulary forsrc/deno_ral/; every plan uses these names and signatures.NN-*.md: one plan per PR. Each is self-contained: spec, files, tasks with checkbox steps, verification commands, review checklist, risks, and a suggested executor (Opus or Sonnet) with a size estimate.HANDOFF.md: start here; state of the work, the reading order and how the work is tracked.
- Every PR lands on
mainone at a time and ships on Deno until PR 33. The full existing test suite is the gate; from PR 26 a real-Node job runs the layer's unit tests per PR with a known-failures list that may only shrink; PR 32 makes the bucketed Node matrix required. - Application code never calls
Deno.*and never imports the standard library directly, except the runtime-neutral import-map seams the std boundary rule allow-lists (async,crypto,datetime,streams,io/*,logother thanlog/file-handler,fmt/printf,dotenv/stringify; the rule is in02-runtime-boundary-check.md, the module inventory behind it in22-std-import-seams.md); everything else importssrc/deno_ral/. PR 02 adds a CI ratchet with five scopes (src,src-js,resources,package/src,tests) whose counts can only go down. PR runs count only the PR's own increase (PR 02c); main keeps the absolute baseline. - Runtime-shaped APIs (process, HTTP, TCP, watch, key-value, errors) get Quarto's own interface.
Plain filesystem and environment calls keep Deno's names so the codemods are mechanical and reversible (
--reverse). - No synchronous network calls in any interface.
- Each
deno_ralmodule ships with a contract test undertests/unit/ral/; those tests are the compatibility contract for the Node implementations and run on real Node from PR 26 on. - The flip (PR 33) switches the CLI to Node but keeps Deno downloaded as a tool for
quarto runandbuild-ts-extensionuntil PRs 35 and 36 move them; PR 37 stops downloading it. Every Phase 3 PR is green and revertible on its own. If Spike #2 passes, one prerelease cycle ships both runtimes behindQUARTO_RUNTIME=node|deno. - Plans are executed with
superpowers:subagent-driven-development(orsuperpowers:executing-plans), in a worktree, one PR per branch, reviewed on their own. Codemod PRs are generated from freshmainand merged the same day; a conflict is resolved by resetting the file and re-running the codemod, never by hand. Branching model (decided 2026-10-21): no stacked PRs, no group merges. Parallel work runs as concurrent worktrees inside one dependency tier (03–06 after 00b; 10–13 after 22), never as a stack: every parent merge would restack the children and re-run the full CI matrix per layer, and each plan's step 0 re-greps itspath:linecitations against mergedmain, which a long-lived stack would rot. Group merges are out because each plan's acceptance criteria and baseline steps assume it lands individually. The chain 00a → 00b → 03–06 → 07 is the Phase 0 critical path and never waits for a worktree slot; 00c, 01, 02b, 08 and 09 can start in parallel right after 02 (08 and 09 unblock all of Phase 1). - Shared files are never merged by hand (organisation review of 2026-10-17,
/mnt/project-files/revue-organisation/). After a rebase:tools/runtime-boundary-baseline.jsonis regenerated withquarto run --dev tools/check-runtime-boundary.ts --update-baseline(--accept-increasewhen the plan says so) and the plan's expected delta is re-read against the newmain;deno.lockis regenerated withpackage/scripts/vendoring/update-lock.shafter takingmain's copy;tools/node-bundle/deno-shim.mjsandknown-failures.jsonare re-checked withnode tools/node-bundle/build.mjs --check-shimandrun-unit-on-node.mjs; a table row added to.claude/rules/typescript/deno-ral.mdor a changelog line is re-applied on top ofmain's version. Suggested merge order where the graph allows parallel work: 03 → 04 → 05 → 06 (simplest first); 20 before 23 and 24 (PR 20 editspackage/srcandtests/files those codemods rewrite; a codemod PR opened after 20 merged picks up its edits); 34 before 35 and 36 (34 createsdev-docs/runtime-migration/docs-updates.md; 35 and 36 create it if it is absent); 28, 29 and 30 may run in parallel and each re-runs the two shim checks after rebasing on the others.
Decided on 2026-10-15 (REVIEW-2026-10-15.md section 6); later plans assume them.
Change them in the plan that decides them, not downstream.
- Releases: 1.11 ships the boundary work on Deno and the
quarto rundeprecation notice (PR 00c); Phase 1 codemods land before thev1.11branch is cut; 1.12 ships on Node; PR 37 targets 1.13. Backports tov1.10and older use the codemod's--reversemode (step added todev-docs/checklist-backport-a-pr.mdby PR 09, rewritten by PR 37 after the flip). - Pre-flip Deno protection (triage A3): from PR 32 a pull request runs
deno-sanityplus the nightlydeno-nightly; PR 33 merges only with one greendeno-nightlyandparity: OKon its branch (PR 32, "Deno in CI after PR 32"). Rollout and rollback after the flip (triage A4): one full prerelease soak, revert by default for a P1 in the layer, the runtime switches for user-side script/extension breakage only, 1.11 stable as the way back (PR 33, "Risks and rollback"). - Still open (triage A9–A11; they name owners and thresholds, not plan text, and no plan assumes an answer): who opens the upstream issue with PumasAI on
QuartoAPI.system.connectTcp()/spawnDetached()and whether the process/network interfaces freeze before the answer (A9; read before PR 08 lands, PR 08 "Decisions needed"); an owner each for the packaging readiness gate, the HTTP publishing contract, distribution compliance and the offline promise (A10; the names go in this bullet once decided; read before PRs 16, 29, 33 and 34); the performance baseline and thresholds (A11; set in PR 34, "Decisions needed" 4). The--reversebackport demo (triage cross-cutting row 2) is owed after PR 10 merges (PR 10, "Follow-up after merge"). - Pins: Deno
v2.9.x(PR 00a; newest 2.9 at execution), Nodev26.12.0or newer 26.x, CI on Node 26,@cliffy/*1.3.1 (PR 06),@b-fuze/deno-dom0.1.56 on WASM with aparseHtml-time gate of at most 2× the native plugin (PRs 05, 20),ws8.22 (PR 29),web-tree-sitter0.20.2 (PR 20b). Fact-checked 2026-10-16 against nodejs.org/dist, nodejs/Releaseschedule.json,BUILDING.mdat v26.12.0, the v26.x API docs, Deno'sReleases.md, jsr and the npm registry (triage "cross-cutting" row 1). - Lockfile:
deno.lockis committed and used with--frozenby configure, CI, the dev launchers and the bundle (PR 00b); every import-map edit is followed bypackage/scripts/vendoring/update-lock.shand committed with the lock. The bundle gate (coldDENO_DIR,--cached-only, external-specifier grep, five renders, three OSes) runs from PR 00b. - Posit Connect keeps a cookie jar (tough-cookie, PR 04).
Puppeteer launch helpers and the
chromiumtool are deleted, not ported (PR 07). tinitexis rewritten in Rust as a separate project; PR 39 only removes thedeno compilebuild.- Julia engine: ported upstream (milestone J); the subtree is pulled before PR 32; no in-tree edit.
The Node port exists: Gordon Woodhull's fork, branch
q2/node-portofPumasAI/quarto-julia-engine— use that fork until the subtree pull. The upstream PR waits for quarto-cli #14936 to ship in a stable Quarto 1 release (PR #15133 comment, 2026-10-21); #14936 merged tomain2026-09-30, is not in thev1.10branch, so that means the v1.11 stable release unless it is backported. - Test runner:
node:testor Vitest, decided by Spike V before PR 31 executes. - Node bundle and tooling:
tools/node-bundle/dist/quarto.mjs; all Node tooling and the shim are.mjs; the rootpackage.jsonhas no"type": "module"; dev bundles arepackage/dist/bin/quarto-dev.mjsandquarto-bld.mjs; dist keepsbin/quarto.jsplus abin/package.jsonwith"type": "module". - Node flags set by the launchers (PR 33):
--enable-experimental-regexp-engine --max-old-space-size=8192 --stack-trace-limit=100 --use-system-ca --use-env-proxy --disable-warning=MODULE_TYPELESS_PACKAGE_JSON. - Environment contract (PR 33/34):
QUARTO_NODE,QUARTO_NODE_OPTIONS,QUARTO_NODE_VERSION;NODE_OPTIONS/NODE_PATHare saved intoQUARTO_SAVED_*for Quarto's own process and restored for children;QUARTO_DENO*are ignored or translated with a one-time warning andQUARTO_DENOis never aliased to the Node binary;QUARTO_RUN_RUNTIME=denoor the project keyscript-runtimeselects the Deno tool for.tsscripts (PR 35);QUARTO_RUN_DENO_NOTICE=0silences the notice (PR 00c, 35). - Store names: project cache
index, Sass cachesass-index; the backend picks the file (<name>.kvfor PR 18'sDeno.openKvbackend, which Quarto 1.11 ships;<name>.jsonfor PR 30's JSON backend, the only one on both runtimes from PR 30 on) so Quarto versions on different backends never share a file (PRs 18, 30). Until PR 30, Deno KV stores also stay per V8 release branch, in av8-<v8Branch>directory (src/deno_ral/platform.ts, #15180), because a Deno with a newer V8 writes values an older one cannot read. src/deno_ral/keeps its name.src/core/puppeteer.tskeeps its name.- Ratchet exclusions for the
srcscope (kSrcExclusions, PR 02):src/deno_ral/,src/vendor/,src/resources/extension-subtrees/,src/resources/scripts/,src/resources/create/,src/webui/quarto-preview/build.ts(allow-listed until PR 33). PR 25 adds an explicit allow-list with reasons for the standalone scripts. - Landing order inside Phase 1: 02 → 02b → 08 → 22 → 10–13 → 14, with 09 before 10.
PR 22 depends on PR 04 (Phase 0, behind 00b and 00a), so 10–13 and 14 wait for that part of Phase 0 too.
tests/asserts.tslands in PR 02; tests written by PRs 09 and later use the layer, never rawDeno.*. - Migration page for script authors:
https://quarto.org/docs/projects/scripts-deno-to-node.html(PRs 00c, 35; created in quarto-web). tests/run-tests.shaccepts files only, never directories: plans write$(find unit/ral -name '*.test.ts').
The migration is executed by a solo maintainer (Chris) plus LLM-driven agents. The table below maps each role the plans rely on to how it is actually discharged. The plans refer here rather than to named people.
| Role | Who | What the plans expect |
|---|---|---|
| PR reviewer | Chris | reviews every rt/* PR on its own (rule 7); available the same day for the codemod PRs 10–13, 23, 24 and the baseline re-generation they need |
Maintainer (push, Actions and admin rights on quarto-dev/quarto-cli) |
Chris | runs every gh pr create, git push on quarto-dev/quarto-cli, gh workflow run, gh label create (PR 26 Task 7), branch-protection change (PRs 02b, 25, 32, 37, 39), and the nightly CI cost validation of PR 32. An executor without those rights stops at the step, says so in the PR description, and hands the branch over |
| Release manager | Chris | confirms the v1.11 and v1.12 cut dates below, declares the 1.12 soak over and the promotion to stable, decides a revert for a P1 in the layer, writes the "back to 1.11" release-notes line (PR 33 "Rollout and rollback after the flip"), owns the 1.13 target of PR 37 |
| Spike decider | Chris | ends each spike at its time box, takes the go/no-go from the Results section, reviews the spike-closing PR before it edits plans 27–33 (SPIKES.md) |
| Plan coordinator | a fresh LLM session per plan | removes the conditional lines PR 39 names from plans not yet executed (PR 39 "When to land"), keeps this README's status column current, refreshes a plan before it is executed (step 0 of "How to execute a plan": re-grep every cited path:line, check dependencies, check the open-decisions table) |
| Plan executor | an LLM agent (sub-agent driven) | works a plan's checkboxes top to bottom, commits at each commit step, runs the tests the plan names, opens the PR (the gh pr create itself is run by Chris). Driven per plan via superpowers:subagent-driven-development or superpowers:executing-plans as the plan specifies |
| quarto-web author | Chris | creates docs/projects/scripts-deno-to-node.qmd before 1.11 ships (PR 00c prints its URL), opens the one quarto-web PR after PRs 33–36 (PR 34 Task 7) |
| External contacts | Chris | the Posit Connect question of PR 04 Decision 1 (answer consumed by PR 04 or a follow-up), the conda-forge feedstock issue of PR 34, the c4spar/deno-cliffy issue if Spike #2 (e) fails, the link to the Rust tinitex repository for PR 39; PumasAI is A9 |
| Manual verification on a machine the executor lacks | Chris per OS | Windows (PRs 00b, 05, 06, 08, 14b, 28), macOS 12 and Apple Silicon (PR 34), a Posit Connect server (PR 04), a browser (PR 16), Shiny with a second host (PR 15), a corporate CA (PR 34). The result is pasted in the PR description; CI on that OS is the evidence when nobody can run it by hand |
| Milestone | Exit check | Target (00-assessment section 7) | Cut date |
|---|---|---|---|
| Phase 0 done | PR 07 merged (Phase 0 exit check) | end of 2026 | – |
| Phase 1 done, boundary at zero | PR 25 merged, "Phase 1 complete on " written below | March 2027 | before the v1.11 branch is cut: date to confirm |
| Phase 2 done, Node matrix required | PR 32 merged and its branch protection switched | June 2027 | – |
| Flip shipped in a prerelease | PR 33 merged, first 1.12 prerelease on Node | September 2027 | v1.12 branch cut: date to confirm |
| Deno removed | PR 37 merged | 1.13 (decided; PR 37 Decision 1 is closed) | after the 1.12 soak |
PR 00c prints "Starting with Quarto 1.12" and the 1.12 changelog is named by PRs 33–36: if 1.12 slips, the coordinator updates PR 00c's text, test and changelog line in the same PR that moves the date.
Each plan's "Decisions needed" section already carries a recommendation that the plan applies. The rows below are the ones that still need a named person or an external answer; "pick one in the PR description" does not apply to them.
| Where | Decision | Decider | Trigger |
|---|---|---|---|
| PR 20 Task 1 step 4 | go/no-go of the parseHtml WASM gate when it fails on an OS |
maintainer (same as the A10 packaging gate once named) | the three-OS timing run |
| PR 04 Decision 1 | whether Posit Connect ever needs a session cookie | external contact above | before PR 04 merges, or a follow-up PR named in PR 04's description |
| PR 33 Decisions 4 and 5 | deno fmt of generated types and deno check of OJS imports dropped (recommendation (b) in both) |
maintainer | before PR 33 starts |
| PR 34 Decision 3 | removal of the QUARTO_DENO_V8_OPTIONS translation in 1.13 |
release manager | PR 37 Task 3 carries it when 1.13 is cut |
| PR 35 Decisions 1 and 2 | shim removal date, stdlib/* beyond 1.13 |
release manager | the 1.13 branch cut, from issue reports during the 1.12 soak |
| PRs 06 and 07 Decision 1 | the optional renames (@cliffy/command import style, src/core/puppeteer.ts) |
coordinator | after PR 26 and after Phase 1 respectively; dropped if nobody asks |
| PR 33 and PR 35 size | whether PR 33 lands as one PR or as 33a (Tasks 1, 11, 12, 16) + 33b (the flip), and PR 35 as one PR or 35a (Task 1 builder) + 35b (scripts) | maintainer | before Phase 3 starts |
| User breakage after the flip | issue label, weekly triage owner during the soak, criterion to extend the dual-runtime window or the shim | release manager | before the first 1.12 prerelease |
| PR 18, all tasks (from #15180) | the Deno backend keeps the v8-<v8Branch> store directory; reconcile it with the store names index/sass-index and the <identifierHash>-<inputHash>.css naming. Task 2 Step 2 shrinks to the API change: this.index.list({ prefix: [] }) becomes this.index.list([]), and the clear() body #15180 shipped stays as is (storeDir paths, safeRemoveSync, immediate entries skipped; test projectCache - clear() removes disk and immediate entries of a populated cache). Drop the Context sentence saying join throws in clear(), and the Risks lines about that throwing path; one cache.ts Deno.removeSync is now safeRemoveSync. Refresh every Verified usage line number |
coordinator | PR 18 step 0 |
PR 18 Tasks 1–3 (from the #15119 fix, once src/deno_ral/kv.ts is on main) |
kv.ts already exists (openKv, getKv, kvOp, KvDamagedError) and both caches go through it, so Task 1 extends the file instead of creating it and its "Module not found" RED step no longer holds. openKeyValueStore keeps the damage mapping: a damaged value reads as a miss with one warning, a damaged file fails with KvDamagedError naming the folder to delete, any other error is wrapped with the path. The src baseline drops by 5, not 7 (the two Deno.openKv calls are already in the layer). The Deno.openKv grep acceptance check covers src and package only, since three cache tests build damaged fixtures with Deno.openKv on purpose. The kv.ts row in .claude/rules/typescript/deno-ral.md exists: update it |
coordinator | PR 18 step 0 |
| PR 19 Task 1, PR 27b | both replace the whole body of platform.ts: keep the v8Branch export (_TEMPLATE.md, "platform.ts"); PR 27b computes it from process.versions.v8 |
coordinator | step 0 of each |
PR 02c (from #15202, which turned main red) |
Decided 2026-10-27: PR runs fail on any increase that leaves a count above their own tools/runtime-boundary-baseline.json (exactly what the push check on main will compare), instead of only warning; an increase that still fits because main sits below its baseline is a notice; 02c's "everything else is a warning for the maintainer" and its "exit 1 if any error, else 0" no longer hold. Per-line findings stay warnings saying the contributor has nothing to change, plus one ::error saying a maintainer pushes the baseline update before merging; the remedy is unchanged (update the branch with main, --update-baseline --accept-increase, push to the PR). Rejected: an acceptance label (the merged tree still carries the old baseline, so main fails anyway) and warn-only with a merge checklist (the human step is what failed). A plan whose expected delta raises a count runs --accept-increase in its PR (PRs 01 and 05 already do) |
maintainer | step 0 of any plan that raises a count |
| PR 30 | tagged v8-<branch>/ directories are left behind like the untagged .kv files; remove v8Branch and its contract-test line together with its last consumer. What "damaged" means for the JSON index (unparsable or truncated file): keep the miss-with-warning versus fail-naming-the-folder split or not. The Deno KV damage tests (tests/unit/kv-damage.test.ts and the damaged-store cases in sass-cache.test.ts and project-cache.test.ts, from the #15119 fix) build real SQLite damage: they are replaced or deleted here, because PR 32 runs the full unit suite on Node, where neither Deno.openKv nor its damage exists, and "sass-cache.test.ts stays green" is restated accordingly |
maintainer (damage semantics), coordinator (the rest) | PR 30 step 0 |
Each rule or table below is written once, in the plan that owns it, and the other plans refer to it by file and section (reconciliation pass of 2026-10-16, bucket B of TRIAGE-2026-10-16.md).
Change it in its owner.
- The std boundary rule and
kStdAllowListed: PR 02, Context (PR 22's inventory table is the source; PR 25, this README and_TEMPLATE.mdrefer to it). - The runtime residue table (every
Deno.*or Deno CLI use kept after Phase 1, with its runtime, owner PR, removal PR and gate): PR 25, Context (PRs 24, 33, 35, 36, 37 and 39 refer to it). - The
deno_ralvocabulary (env key validation,exitdraining, the KV file rule,execPath()):_TEMPLATE.md, "Shared vocabulary". - The builder contract (
bundle()inputs, CLI and fixture modes, the interop prelude): PR 26, Interfaces (PRs 20b, 31, 32 and 33 refer to it). - The resolver scope rule (the import map applies to Quarto's own imports only; an import from inside
node_modulesresolves as Node would, relative to its importer; mapped targets from the root, unmapped bare names from the importer): PR 26, Interfaces, "Resolver scope" (PR 31's import-map hook and vitest plugin state the same rule; PR 33 keeps the plugin). - The open-flag rule (
openFlagBits(): numericnode:fsflags from Deno's independent options,O_CREATiffcreate,O_TRUNCifftruncate,O_APPENDiffappend, nothing implied;writeTextFileopens withtruncate: !append) and the one-descriptor-owner rule forFsFile: PR 27, Interfaces, "Open flags" and "One descriptor owner" (PR 35'sdeno-members.cjscopies the flag rule, PR 35 Interfaces "Open flags (PR 27's rule, copied)"; triage row 35). - The child-process rules of the Node layer (per-method stdio defaults,
spawninherits andexecpipes;.bat/.cmdthroughcmd.exe /e:ON /v:OFF /d /cwithbatchCommandLine(), Rust std's escaping;128 + nfor a signal exit; Web Streams on the child): PR 28, Interfaces (PR 35'sdeno-members.cjscopies them forDeno.Command, PR 35 Interfaces "Child processes (PR 28's rules, copied)"; triage row 35). - The server lifecycle rule (
shutdown()waits for a pending bind to settle; each server owns the WebSockets upgraded through it, closes them with 1001 on graceful shutdown and terminates them in the forced path;Request.signaland the response body are cancelled when the client is gone): PR 29, Interfaces, "Server lifecycle" (decision A7;_TEMPLATE.md'shttp.tsentry and PR 16's contract refer to it). - The lock ownership rule for lock directories (
<lock>/ownerwritten on acquire with pid, host and nonce; a waiter probes the owner withprocess.kill(pid, 0)and breaks a gone owner's lock at once, an unknown owner's by age, a live owner's never; release and the final rename only while the marker still matches; best-effort exclusion over a recomputable cache, said as such): PR 30, Interfaces, "Lock ownership" (decision A6; PR 33'sensure.mjsbuild lock copies it, PR 33 Interfaces "Lock ownership", withbundle()'s optionalbeforeCommitinput in PR 26's builder contract so the bundle is renamed into place only while held;_TEMPLATE.md'skv.tsentry refers to it). - The leak guarantees of the Node test harness (the per-test timeout is a watchdog for hangs;
runChecked()inregisterTestfails a test that leaves a child process, a listening server or a timer open or that callsexit(), withsanitizemapped onto it as on Deno; the forced exit after a file's last test bounds the run and detects nothing; open files, client sockets and pending fs operations are intentionally undetected on Node): PR 31, Interfaces, "Leak guarantees" (R13;_TEMPLATE.md'ssanitizeentry,tests/README.mdandSPIKES.md's Spike V refer to it). - The parity evidence rule (the nightly
parity-comparejob fails before comparing unless every test job it depends on succeeded and every bucket of both runtimes uploaded a readable report; a test is<file> :: <name>,junit.mjscaseIdin PR 26, the file read from the report'sfileattribute,classnameor file suite; a failure on either side is never parity): PR 32, Interfaces, "Parity evidence" (R14; PR 37 deletes the job and keepsjunit.mjsandjunit-finish.mjs). - The CI graph after Deno removal (events, OSes, bucket generation, required check names): PR 37, Context (PR 32 refers to it).
- Spike V scheduling and its inconclusive outcome:
SPIKES.md, Spike V (after PR 30; PRs 31 and 32 refer to it). - The Windows coverage table (which POSIX process and preview assertions have a Windows equivalent, through which mechanism, and which have none): PR 14b, Context, "Windows coverage" (PRs 28 and 34 refer to it).
- The supported migration surface matrix, the "not available on Node" table, the version rule for
npm:/jsr:specifiers and the governing-lockfile table: PR 35, Spec, "Supported migration surface" (PRs 00c, 34, 36 and 37 refer to it). - The dual-runtime combinations table (CLI runtime × script runtime until PR 37, with the executable each uses) and the rule that
src/spawns Node onarchitectureToolsPath("node")and Deno onrequireDenoToolBinary(), never onexecPath(): PR 33, "Dual-runtime combinations until PR 37" (PRs 28, 35, 36, 37 and_TEMPLATE.mdrefer to it). - The pre-flip Deno protection rule (what protects
mainon Deno between PR 32 and PR 33, and PR 33's merge condition): PR 32, "Deno in CI after PR 32" (PR 33 refers to it). - Rollout and rollback after the flip (soak, revert by default, what the switches are for, returning to 1.11): PR 33, "Risks and rollback" (PRs 34 and 37 refer to it).
Step 0, before the plan is executed (coordinator): the plans were written against main at b86f98d and cite line numbers.
Re-grep every path:line the plan cites, check each "assumes PR xx landed with ..."
against main, check the plan's **Depends on:** PRs are merged, read the "Open decisions with an owner" table above for a row that names this PR, and set the plan's row in "The sequence" to in progress.
The person who merges the PR sets it to merged.
git checkout main && git pull
git worktree add ../<branch-named-in-the-plan> -b <branch-named-in-the-plan> # rule 7: one worktree per PR
# open the plan, work the checkboxes top to bottom, commit at each commit step
cd tests && QUARTO_TESTS_NO_CONFIG=true ./run-tests.sh --agent <test files named in the plan>Each plan names the tests it adds and the existing tests it must keep green.
When a plan says "Decision needed", the options and a recommendation are in the plan; pick one in the PR description unless the "Open decisions with an owner" table names a decider.
A plan that says "decided by Spike #2" or "decided by Spike V" waits for the outcome recorded in SPIKES.md.
Actions reserved to the maintainer (see "Roles"): a step that runs gh pr create or git push on quarto-dev/quarto-cli, gh workflow run, gh label create, or changes branch protection is done by the maintainer (Chris, see "Roles"); an executor without those rights stops at the step, says so in the PR description, and hands the branch over.
A PR whose completion needs such an action (PRs 02b, 25, 32, 37, 39) is merged only once the action is done.
A test that fails on an OS the executor cannot run: CI on that OS is the evidence; the reviewer triages the failure (dev-docs/debugging-flaky-tests.md); a test is never disabled, skipped or quarantined to get green; a manual Windows or macOS step the executor cannot do is done by Chris (see "Roles") or, when the plan names a CI job that covers it, trusted to that job with a line in the PR description saying so.
Commits follow the plan's steps as written (a failing-test step followed by its implementation step may leave one red commit; no squash is required).
Changelog: a dependency bump gets a line under ## Dependencies of news/changelog-<version>.md (created by PR 00a); a plan with no user-visible change says Out: changelog in its Scope with the reason.
Status values: planned, in progress, merged.
Update this table as PRs land.
Phase 0: de-risk dependencies (PR 02 first, then 02c, then 00a → 00b, then 03–06 in parallel, then 07)
Every PR that changes a count lands after 02c; preparing it earlier is fine.
| PR | Plan | What | Depends on | Executor | Size | Status |
|---|---|---|---|---|---|---|
| 02 | 02-runtime-boundary-check.md | CI ratchet counting Deno.* and direct std imports in five scopes (src, src-js, resources, package/src, tests); tests/asserts.ts; lands first |
none | Sonnet | S | merged |
| 02c | 02c-ratchet-pr-delta-and-guidance.md | PR runs compare with the base branch; findings name a working replacement or ask the maintainer to accept; --accept-increase |
02 | Sonnet | S | merged |
| 00a | 00a-deno-bump.md | Deno 2.7.14 → newest 2.9.x everywhere the version appears | 02 | Sonnet | S | planned |
| 00b | 00b-lockfile-and-bundle-gate.md | Committed deno.lock with --frozen (six entrypoints); tools/check-test-imports.ts keeps the unlocked tests inside that graph; three-OS bundle gate (cold cache, external-specifier grep, five renders) |
02, 00a | Opus | M | planned |
| 00c | 00c-quarto-run-deprecation-notice.md | One-time notice when quarto run or a pre/post-render .ts script runs on Deno; QUARTO_RUN_DENO_NOTICE=0 |
02 | Sonnet | S | planned |
| 01 | 01-quick-fixes-from-spike.md | assert→with on two JSON imports; deterministic bslib Sass ordering |
02 | Sonnet | S | planned |
| 03 | 03-deps-skypack-to-npm.md | Replace every cdn.skypack.dev import-map entry with npm:; CommonJS bundling proof |
02, 00b | Sonnet | M | planned |
| 04 | 04-deps-deno-land-x-small.md | Replace semver, media_types, xmlp, xml, cache, events; cookie jar on tough-cookie | 02, 00b | Opus | M | planned |
| 05 | 05-deps-deno-dom-jsr.md | deno_dom from deno.land/x to jsr:@b-fuze/deno-dom 0.1.56; parseHtml timing baseline on the native plugin |
02, 00b | Opus | M | planned |
| 06 | 06-deps-cliffy-jsr.md | Cliffy 1.0.0-rc.3 to @cliffy/* 1.3.1 (62 files) with a table-driven argv characterisation |
02, 00b | Opus | L | planned |
| 07 | 07-remove-puppeteer-and-chromium-tool.md | Delete the unused puppeteer launch helpers and the deprecated chromium tool; std-redirect cleanup; Phase 0 exit check (rg 'deno\.land|skypack' empty outside the allow-list) |
02, 00b, 03–06 | Opus | M | planned |
| PR | Plan | What | Depends on | Executor | Size | Status |
|---|---|---|---|---|---|---|
| 02b | 02b-node-compatible-ts-syntax.md | verbatimModuleSyntax; import type codemod (2,067 specifiers in 504 files); enums, parameter properties and <T> casts rewritten; check-ts-syntax.mjs in CI |
02 | Opus | L | planned |
| 08 | 08-ral-process.md | deno_ral/process.ts: spawn/exec interface with detached, killProcess (group on POSIX, taskkill /T tree on Windows), flushing exit, execProcess on it, 16 direct Deno.Command sites, exit/cwd/args/execPath/signals/stdio |
02 | Opus | L | planned |
| 39 | 39-remove-quarto-latexmk-compile.md | Remove the tinitex deno compile build and the standalone entry (Rust rewrite replaces it); before PR 25, whose zero needs the files gone |
08 | Sonnet | S | planned |
| 09 | 09-ral-fs-and-temp-layer.md | deno_ral/fs.ts, temp.ts, errors.ts API (canonicalPathSync, retyped FileInfo), unit tests, and the idempotent ts-morph codemod with --reverse |
02 | Opus | M | planned |
| 22 | 22-std-import-seams.md | colors.ts, flags.ts, base64 seams; table of std modules with internal Deno.* and their owner (PR 20b) |
04, 08 | Sonnet | S | planned |
| 10 | 10-codemod-fs-core.md | Run the fs/temp codemod on src/core, src/quarto-core, src/config (56 files) |
09, 08, 22 | Sonnet | L | planned |
| 11 | 11-codemod-fs-command.md | Codemod on src/command (43 files) |
09, 08, 22 | Sonnet | L | planned |
| 12 | 12-codemod-fs-project-format.md | Codemod on src/project, src/format (51 files) |
09, 08, 22 | Sonnet | L | planned |
| 13 | 13-codemod-fs-rest.md | Codemod on src/execute, src/publish, src/tools, src/extension, src/preview, TS under src/resources (35 files) |
09, 08, 22 | Sonnet | M | planned |
| 14 | 14-ral-env.md | deno_ral/env.ts and codemod of all Deno.env uses in src/ (58 files) |
02, 10–13 | Sonnet | L | planned |
| 14b | 14b-coverage-first.md | Tests before the rewrites: preview reload end-to-end (document and website project, three OSes), Jupyter daemon reuse, inspect through a pipe, process-group kill (POSIX) and taskkill /T tree kill (Windows) |
02, 08, 09 | Opus | S | planned |
| 15 | 15-ral-net-async-ports.md | deno_ral/net.ts; async port probing; Jupyter kernel transport; Shiny control server on localhost |
08, 09, 14b | Opus | M | planned |
| 16 | 16-ral-http.md | deno_ral/http.ts with listening; preview server, dev server, axe server on it |
15, 08, 14b | Opus | M | planned |
| 17 | 17-ral-watch.md | deno_ral/watch.ts; polling watcher and serve watcher on it |
09, 14b | Sonnet | S | planned |
| 18 | 18-ral-kv.md | deno_ral/kv.ts with backend-named stores and cross-process contract; project cache and Sass cache on it |
09, 08 | Opus | S | planned |
| 19 | 19-ral-platform-main-permissions.md | platform.ts with real runtime() detection, main.ts, Deno.permissions → warning-only probe, version/consoleSize/inspect/uid |
08, 09 | Sonnet | S | planned |
| 20 | 20-remove-deno-dom-ffi.md | Drop the native deno_dom plugin from code, packaging, launchers, conda, docs; gated on tools/parsehtml-gate.sh (≤ 2× native, three OSes) |
05, 00b, 14, 02 | Opus | M | planned |
| 20b | 20b-hidden-deno-uses.md | tree-sitter-deno.js → web-tree-sitter; graphviz-wasm.js; deno-cri JS; onFileRead hook for metrics.ts; own FileHandler; dotenv parser |
02, 08, 09, 14 | Opus | M | planned |
| 23 | 23-codemod-tests.md | Codemod tests/; tests/harness.ts adapter around Deno.test; assertions through tests/asserts.ts |
02, 08, 09, 14, 19, 22 | Opus | L | planned |
| 24 | 24-codemod-package-src.md | Codemod package/src; Deno.run → exec; remaining Deno CLI calls listed for PR 33 |
02, 08, 09, 14, 19, 22 | Sonnet | M | planned |
| 25 | 25-boundary-zero-and-monkey-patch-removal.md | Delete core/deno/monkey-patch.ts and original-real-path.ts; types sweep (former PR 21); comment sweep; standalone-script allow-list; hidden-dependency inventory and the runtime residue table (owners and gates for every remaining Deno.* or Deno CLI use); ratchet to zero and required |
04, 07, 08–20, 20b, 22–24, 39 | Sonnet | M | planned |
| J | milestone, no plan | Julia engine ported upstream against the PR 08/09 engine API; subtree pulled; Julia entries leave the PR 32 allowlist (PR 32 does not wait for J; the allowlist is emptied when the subtree is pulled); PRs 08 and 09 have nothing to deliver for J beyond their public interfaces; the port exists on Gordon Woodhull's fork (gordonwoodhull/quarto-julia-engine branch q2/node-port, to use until the pull); the upstream PR to PumasAI waits for #14936 to ship in a stable Quarto 1 release — on main only as of 2026-10-21, so v1.11 stable unless backported to v1.10 (PR #15133 comment, 2026-10-21; partially answers triage A9 — Gordon owns the port, the connectTcp/spawnDetached API question of PR 36 Decisions needed 1 stays open) |
08, 09, upstream | Gordon Woodhull (port), maintainer (subtree pull) | – | in progress (fork) |
| PR | Plan | What | Depends on | Executor | Size | Status |
|---|---|---|---|---|---|---|
| 26 | 26-node-build-job.md | Node bundler (import map scoped to first-party importers) and .mjs shim from spike/, root package.json, check-shim.mjs over bundle and resources, real-Node run of tests/unit/ral per PR with known-failures.json, junit.mjs (cases by file and name), render job |
01, 03–07, 20b, 23 | Opus | M | planned |
| S2 | SPIKES.md | Spike #2 (one week): node: bundle on Deno 2.9, node:http+ws upgrade, fs.watch, process groups and .bat quoting, Cliffy prompts on Node, --use-env-proxy |
26 | Opus | – | planned |
| 27 | 27-ral-fs-on-node-apis.md | fs.ts/temp.ts/errors.ts on node:fs; canonicalPathSync on realpathSync.native; walk/expandGlob without @std/fs; the open-flag rule (openFlagBits) and the one-owner FsFile |
20b, 25, 26, S2 | Opus | L | planned |
| 27b | 27b-ral-platform-env-tar-log-on-node.md | platform.ts, env.ts, tar.ts, log.ts FileHandler on node: |
14, 19, 20b, 25, 26, 27 | Sonnet | S | planned |
| 28 | 28-ral-process-on-node-apis.md | process.ts on node:child_process; BatBadBut-safe batch quoting; draining exit; taskkill /T |
14b, 25, 26, 27b, S2 | Opus | M | planned |
| 29 | 29-ral-net-http-watch-on-node-apis.md | net.ts, http.ts (with ws; the server lifecycle rule: bind states, owned WebSockets, cancellation), watch.ts (recreated-root and polling fallback) on Node APIs |
14b, 25, 26, 27b, S2 | Opus | M | planned |
| 30 | 30-ral-kv-on-node.md | kv.ts on a locked JSON file (<name>.json), per-key merge, both runtimes; the lock ownership rule (owner marker, liveness probe, guarded release) |
18, 25, 26, 27b | Opus | S | planned |
| 27x–30x | fix PRs, no plan | Correctives for the "runtime gaps" Spike V rule 0 records against PRs 27, 27b, 28, 29, 30 (a test failing on both runners in a node: body of the layer); one PR per gap, branch rt/NN-fix-<gap>, its own node-unit run shows the fix; none if the spike records no gap |
SV | Opus | S | planned |
| SV | SPIKES.md | Spike V (two days): Vitest vs node:test on ten files after the layer is on node: APIs; decision rule with an inconclusive outcome |
02b, 27, 27b, 28, 29, 30 | Opus | – | planned |
| SV-rerun | SPIKES.md | One-day re-run of Spike V (files 1–9) after the 27x–30x fixes, only when the spike was inconclusive (rule 4); records the definitive runner | 27x–30x | Opus | – | planned |
| 31 | 31-test-harness-on-node.md | tests/harness.ts on the chosen runner; run-tests-node.sh; run-parallel-tests.mjs; testArgs(); leak guarantees (runChecked(), per-test timeout, forced exit) |
02b, 23, 26, 27–30, SV (SV-rerun before PR 32 Task 6 when inconclusive) | Opus | L | planned |
| 32 | 32-node-ci-required.md | Bucketed Node matrix (Linux, Windows per PR; macOS nightly) required; prerequisite-aware parity by file and name, computed only from complete evidence (green jobs, every bucket reported, failures counted); allowlist empty except Julia until J; Deno matrix to nightly plus deno-sanity; merged only after one nightly parity: OK on main and the maintainer's branch-protection switch |
14b, 27, 27b, 28–31 (J is not a hard prerequisite: PR 32 may merge with the Julia allowlist non-empty, PR 36 Context) | Opus | M | planned |
| PR | Plan | What | Depends on | Executor | Size | Status |
|---|---|---|---|---|---|---|
| 33 | 33-build-tooling-node.md | CLI launchers (shell, batch, Rust) run Node; Deno kept as a tool; quarto install deno; quarto-bld and prepare-dist on Node; tsc; environment contract; ensure.mjs dev bundles under an owned lock (PR 30's rule); Deno CLI dev utilities off deno info/fmt/check; QUARTO_RUNTIME if S2 passes, with the dual-runtime combinations table |
32, 00b, 20b | Opus | L | planned |
| 34 | 34-installers-and-check.md | Installers ship Node with Node's entitlements and OS minimums; quarto check Node row; conda; the 17-row runtime contract table with tests; docs |
33 | Opus | M | planned |
| 35 | 35-user-scripts-compat.md | .ts scripts run on Node by default (per-file esbuild compile, on-demand Deno shim whose writes and Deno.Command copy PR 27's and PR 28's rules, 34 stdlib/* bundles); QUARTO_RUN_RUNTIME / script-runtime selects the Deno tool; the supported migration surface matrix and the version rule; juice.ts and ast-diagram on Node APIs |
33, 31, 00c | Opus | L | planned |
| 36 | 36-build-ts-extension-node.md | build-ts-extension on esbuild with the default import map served on Node (path/log/fs as @quarto/std-* packages from PR 35's builder); package.json quarto.build governs, a package.json without it hides no legacy deno.json; warning for prebuilt engines that reference Deno. |
33, 35 | Opus | M | planned |
| 37 | 37-ci-switch-and-deno-removal.md | Stop downloading Deno; remove QUARTO_RUNTIME, the nightly Deno matrix, vendoring, deno.jsonc, deno_std, deno.lock; backport checklist step; dev-docs updates |
33–36, 39 | Sonnet | M | planned |
| 38 | 38-lint-format-typecheck-tooling.md | dprint (Deno-parity config) and oxlint (61 matching rules) on src/; drop @types/deno |
37 | Sonnet | M | planned |
02 ──┬─ 00a ─ 00b ─┬─ 03 ─┐
├─ 00c ├─ 04 ─┤
├─ 01 ├─ 05 ─┼─ 07 ──────────────────┐ (Phase 0)
├─ 02b └─ 06 ─┘ │
├─ 08 ─┬─ 22 ─┬─ 10 11 12 13 ─ 14 ─┐ │
│ │ │ │ │
├─ 09 ─┤ ├─ 23 24 │ │
│ ├─ 14b ─┬─ 15 ─ 16 │ │ (Phase 1)
│ ├─ 17 ├─ 18 19 │ │
│ ├─ 20b ─┘ 05 ─ 20 │ │
│ ├─ 39 (latexmk build) │ │
│ └─ J (Julia upstream) │ │
└──────────────── 25 <─────────────┴─────────┘
│
01 03–07 20b 23 ─ 26 ─ S2 ─── 27 ─ 27b ─── 28 29 30 ─┬─ SV ─ 31 ─┐
│ │ (Phase 2)
14b, (J) ─────┴─────────── 32 (27x–30x and SV-rerun sit between SV and 31 when Spike V records gaps)
│
00b 20b ─┤
│
33 ─ 34 35 ─ 36 │ (Phase 3)
└─ 37 ─ 38
Edges that cross phases: 05 → 20 (timing baseline); 20b → 26, 27, 33 (shim members, tree-sitter in the bundle); 14b → 28, 29, 32 (the end-to-end gates); 02b → 31, 35 (type-only imports); 00b → 33 (bundle gate); 00c → 35 (notice); S2 → 33 and 37 (QUARTO_RUNTIME); J → 32 (allowlist; soft, see row 32); 39 → 25 (zero needs the deno compile files gone) and 37.
Every remaining Deno.* or Deno CLI use after Phase 1 has an owner and a gate in PR 25's runtime residue table.
The PR 02 ratchet prints the current counts.
Baseline as measured by the reworked PR 02 script (2026-10-15, main at b86f98d):
| Scope | Deno.* sites |
Direct std imports (files) |
|---|---|---|
src (.ts outside kSrcExclusions) |
992 | 49 |
src-js (.js under src/, excluding src/resources) |
7 | 0 |
resources (runtime-loaded src/resources/**) |
10 | 1 |
package/src |
240 | 3 |
tests (excluding tests/docs) |
814 | 165 |
The std column counts files with a direct std import that the std boundary rule does not allow-list (PR 02); 13 src files and 1 package/src file import allow-listed seams only and are not counted.
Zero in src, src-js and package/src, and an explicit allow-list for resources, is the exit criterion of Phase 1 (PR 25).
tests reaches zero in PR 23.