Repository navigation
v0.6.4
[0.6.4] — 2026-09-25
Added — Astro (.astro) frontmatter is indexed on the TypeScript grammar (#320, #67)
An .astro file's --- frontmatter is now parsed with the vendored TypeScript grammar, restricted to that
block by one included range, so a frontmatter call resolves into the .ts service it imports. The template
half is not read at all, and an .astro file reports lang="ts"; both are disclosed blind spots in
docs/ARCHITECTURE.md#astro-extraction. kParserVer 121 → 122; kCacheVersion is unchanged.
Documented — README: release notes moved to a section near the end
The four release blurbs (0.6.3, 0.6.2, 0.6.1, 0.6.0, with their "Thanks to" lines) sat near the top of
README.md, right after the Languages line — a lot for a first-time reader to get through before
Quickstart. They now live in a new ## Release notes section near the end, just before
## Documentation, text and thanks lines unchanged. The top of the README keeps one short line
naming the current version and pointing at that section and at CHANGELOG.md. The old ## What's new
section (a stale, differently-worded duplicate of the 0.6.0 blurb) is removed.
Fixed — next= is never dropped silently, and the nudge hook reads rev-parse's answer
next=is no longer silently dropped past its old 120-byte ceiling.--for's page/widen
follow-up (forPageInvocation/forWidenNext),--flip's cut-listing follow-up, and the
churn-decay map's--in=DIRscoped/stub follow-ups (including the task-router's--for-shaped
<choice>widening hint) used to build the full invocation and then throw it away when it
exceededkNextAttrMaxBytes, leavingnext=absent — indistinguishable from a root with nothing
to suggest, so a cut answer with a long follow-up (a deep path, a long symbol name) silently lost
its only route to the rest.nextAttrXml(the one place every next= producer funnels through)
now carries no length ceiling at all and always emits the full, runnable invocation; an answer
whose next= was already ≤120 B is unchanged.hooks/ripwire-nudge.sh's SessionStart primer readgit rev-parse --is-inside-work-treeby exit
status only. A bare repository, or a cwd inside a work tree's own.gitdirectory, printsfalse
with status 0 there, so the primer could still run in a population it was never meant to reach (the
same shape CodeRabbit flagged and 1cd00d4 fixed in the two route hooks). It now reads the answer.- CONTRIBUTING.md's Windows-matrix note named a stale gate count (647); the live count is 649.
Fixed — --test-gate derives node --test from a bare node:test import, with no package.json at all
#60's core defect (a TS/JS test file's call-graph reach and --affected/--test-gate listing) was fixed
in 0.6.2. What was left: the reporter's own repro (src/bounded.ts + test/behavior.test.ts, import test from "node:test", no package.json anywhere in the repo) still read run_unknown="1", because
jsrunner.h's runner derivation (#323) reads only the nearest package.json's own evidence, and a repo
with none has no evidence for that walk to find. Thanks to @YogevKr for the report and the clean two-arm
repro, and to @alex-michaud for the non-test --callers arm that helped confirm the core fix.
- New evidence source: the test file's own import/require. When
package.jsonevidence decides
nothing for a TS/JS test file — no manifest anywhere in the crawl boundary, or the nearest one is a true
marker (noscripts.test, novitest/jestdependency) — the file's own bytes are read for anode:test
import:import test from "node:test",import { test, describe } from "node:test", or
require("node:test"), single or double quoted. This is a real parse (the same grammar the file's own
extension selects), not a substring scan: a"node:test"mention inside a comment or an unrelated string
literal is not evidence and does not derive a runner. - Precedence is unchanged. An explicit
package.jsonscripts.test(or avitest/jestdependency)
still wins exactly as #323/#331 already decided — including an authoritative-but-unrecognized script
(mocha, say), which is a decided "no" and is never overridden by the weaker, file-local import evidence.
The import fallback applies ONLY to today'srun_unknown="1"case. .ts/.mts/.ctsget a Node-version-aware command, never a command proven to fail. Node's own
--testrunner needs--experimental-strip-typesto strip TypeScript types at all from Node 22.6
onward; below that the flag itself is a fatal "bad option" and there is no way to run a.tsfile with
plainnode. Stripping is ON BY DEFAULT (the flag becomes a harmless no-op) from two separate floors —
Node 22.18 and Node 23.6 — because 23.6 turned it on first and the 22.x line got it later, by backport, so
a bare 23.0–23.5 does not have it. This tool cannot see which
Node will run the emitted command, so it readsengines.nodefrom the nearest manifest
(if any): the bare form when that range proves every satisfying Node has stripping on by default; the
flagged form when it proves >= 22.6 but not provably default-on, or when there is no manifest at all (an
honest, stated assumption of Node >= 22.6, never a guess at an unseen runtime); andrun_unknown="1"
when the range admits ANY Node below 22.6 — a plain>=18/^20, or a compound range such as
>=24 || ^20(the LOWEST admitted alternative decides it, not the highest) — or cannot be read with
confidence at all. This applies to BOTH the new import-evidence path and the existing
scripts.test: "node --test"path (previously spellednode --test <file>unconditionally for a.ts
file, with no version awareness at all)..js/.mjs/.cjsnever need the flag and always get the bare
form, unlessengines.nodeadmits a Node below 18 (node:testitself does not exist there)..tsxand.jsxare never derived, on purpose. Node's type stripping does not cover.tsxat all
(ERR_UNKNOWN_FILE_EXTENSION), and plainnodecannot load a.jsxfile either, on any Node version, with
or without any flag — both stayrun_unknown="1", the same as before this evidence source existed.- A
.ts/.mts/.ctscommand is derived only when the test file's own relative imports can actually
load. Node's module resolver, under type stripping, never probes an extension and never maps a.js
specifier onto a.tssource — both are exactly how tsc-, tsx- and bundler-run TS code imports its own
siblings. So the command is derived only when every relative (.//../) staticimport/export … from
specifier orrequire(...)argument in the test file's own bytes names a file that exists on disk at
EXACTLY that path; an extensionless specifier, or one whose exact spelling does not exist, stays the
honestrun_unknown="1"instead of a command the file's own bytes already prove would fail. - New fixtures,
test/testgatenodetestimportfix/throughtest/testgatenodetestimportprecedencefix/
(test/testgatecheck.sharms x1-x6): the exact repro, a.jsvariant,require("node:test"), a negative
control ("node:test"only in a comment/string — parsed, not matched), and a precedence control
(scripts.test: "vitest run"still wins over anode:testimport in the same file). Five more,
test/testgatenodetesttsxfix/throughtest/testgatenodetestenginescompoundfix/(arms y1-y6, fix round):
red-first controls for the three refusal shapes above —.tsx,.jsx, an extensionless relative import,
and twoengines.nodeshapes (>=18, and the compound>=24 || ^20) that admit a pre-22.6 Node.
Documented — TS/JS test runners this still cannot derive (run_unknown="1" stays honest, not a bug)
Narrowed from the 0.6.3 list: the fourth gap below (node's test runner against a .ts file on an unknown
Node version) is now derived, honestly bounded — see the fix above, including the cases it deliberately
still refuses (.tsx/.jsx, an unresolvable relative import, a declared floor below Node 22.6). Still not
derived, and correctly run_unknown="1": node's own test runner invoked through tsx (a common way to run
it against .ts files, when neither package.json nor the test file's own bytes name node:test
directly) and bun's test runner. See #323's own discussion for a user-declared runner template, which
would be the way to name either of these explicitly once implemented.
Fixed — --deps no longer reports a TS/JS graph with missing alias edges as complete (#220, part 1)
A TypeScript or JavaScript import written through a tsconfig/jsconfig paths alias (@app/b), a
baseUrl-relative path, or a workspace package name (@acme/lib) draws no file-graph edge, so a cycle
spelled through one was missing and the absent <cycles> element read as "acyclic". Those imports are still
not resolved (that is part 2); they are now counted. A tree without one is byte-identical.
--depsand--archcarryimports_unresolved="N" graph_partial="1"on the root: every value is
measured over the resolved edges only. Not every one is a lower bound: a missing edge can merge two
reported cycles into one, andinstab=/I=/D=can move either way.afferent=,transitive=,
ccd/acd/nccdandviolations=can only rise.--archalso says on stderr thatviolations=can
only rise; its exit code is unchanged.--reportreads## Dependency cycles (showing 1 of 1; measured over resolved edges: 3 imports unresolved),
and an empty list reads "none found over the resolved edges" instead of "none (acyclic)".--impact(XML,--json,--format=columnar, and the MCPimpacttool) carriesimports_unresolved=
besideimporters=when a TS/JS import could land on one of the symbol's files.- Only a specifier the project's own config places in the tree counts: a
pathskey with a literal prefix
and an in-tree target, a catch-all key orbaseUrlpath only when it names an indexed file, or a
workspace member's name frompackage.jsonworkspacesorpnpm-workspace.yaml. A bare package such
asreactnever counts. Relativeextendschains are followed; a package-formextendsis not read. - Measured with
--deps --no-cache, before and after, on four TS/JS trees from the r4 corpus. Chainlit:
409 imports now disclosed (baseUrl 247, workspace 106, paths 57). Streamlit: 972 (workspace). mlflow: 204
(paths). Zulip and sktime report none and are byte-identical. An independent re-derivation from the
--depsrows and the config files agrees: mlflow exactly, Chainlit within 1, and Streamlit within the
48 rows--depsdoes not print (it lists at most 40 per file).
Fixed — Windows findings from the 0.6.3 preview test (#334, reported by @elsRobin)
skills/install.shno longer reports empty directories as installed skills. On Windows without symlink
privilege, Git Bash'sln -sfnexits 0 and leaves an empty directory; the installer printedinstalled
for each one, wrote all of them to the manifest and announced them as active. It now checks each link by
its result (a symlink whoseSKILL.mdreads back). When the link did not take, it copies the skill and
printscopied. When the copy fails too, it printsFAILED, leaves the skill out of the count and the
manifest, and exits 1. The prune step recognises its own copies (a marker file, an empty leftover
directory, or a name its last manifest listed) and leaves any otherripwire-*directory alone; before,
a user's ownripwire-*directory made the installer stop withrm: … is a directory.- A cache blob written by a different ripwire build is now refused with both numbers on the line:
format-version — not used; … rewrites it (blob format 24, this binary 25: another ripwire build wrote it; …). The
per-tree cache path does not depend on the build, so two builds that alternate on one tree (0.6.2 and
0.6.3 in the report) each refuse and rewrite the other's cache and re-parse on every run. One build run
twice in a row reuses its own cache, on Windows as elsewhere. The Windows CI job now checks that
directly: the second run'sRIPWIRE_CACHE_STATSline must show every file reused and none re-parsed. --doctor'sbinary-pathrow, when a differentripwirecomes first on PATH, now names that
binary's build:which_version=is the line it prints for--version. ItsSTALE:hint goes by the
release numbers the two binaries state, where it used to go by mtime. A 0.6.2 copied onto PATH after
0.6.3 was installed had the newer mtime, so the hint called the running 0.6.3 stale and said to run the
0.6.2. On Windows, Git Bash'swhichprints the name without.exe; the row now also tries the.exe
name before it reportson_path="0", so it compares the two files there too. It still marks
degraded="1"on Windows. When noripwireis on PATH, its hint on Windows is PowerShell's
$env:Path = '<dir>;' + $env:Pathwith native separators, where it used to print a POSIX
export PATH=line that does nothing in PowerShell (reported by @lennix1337 and @antoniojosedev).
On every platform the hint now single-quotes the directory (export PATH='<dir>':"$PATH"
elsewhere), so a$, a backtick or$(…)in the directory's name cannot expand or run when the
line is pasted; the PowerShell form also doubles'and the typographic quotes ‘ ’ ‚ ‛, which
PowerShell reads as quotes too.- The determinism check in AGENTS.md, CLAUDE.md, CONTRIBUTING, README,
--help, the skills and the docs
now writes its two outputs outside the crawled tree:
t=$(mktemp -d); ripwire . >"$t/a"; ripwire . >"$t/b"; diff -q "$t/a" "$t/b". Written inside it, the
second run crawled the first run's output as a new unindexed text file, and the map header's top-6
unindexed=list could change between the two runs. The engine was deterministic; the recipe was not.
The top-6 cut itself is well-defined (count descending, then extension name) and is unchanged. - The Windows cache location with
TMPDIR,TEMPandTMPall unset is now documented and kept as it
is. Windows' own temp-directory rule then falls back to the profile folder, so the cache is
%USERPROFILE%\ripwire-<uid>. That is per-user, and--doctor'scache-dirrow names it. - README's Windows section adds three notes. The hash check passes because
-eqignores case; use
.Hash.ToLower() -ceqfor a case-sensitive compare.Expand-Archivedoes not pass Mark-of-the-Web on
to the files it extracts, so no SmartScreen prompt is not a verdict on the exe. In Git Bash,fcis a
shell builtin, so compare outputs withcmp, or withMSYS_NO_PATHCONV=1 fc.exe /b.
What's Changed
- Train 20 (0.6.4): Astro frontmatter (#320),
node --testfor node:test files (#60), honest--depsover unresolved TS/JS imports (#220 part 1), the Windows preview testers' findings (#334),next=never dropped silently by @joyful-ii-V-I in #336 - Release 0.6.4: version bump, changelog, README release notes, deck by @joyful-ii-V-I in #337
Full Changelog: v0.6.3...v0.6.4