rescale verb
- To alter the scale of a drawing or project; to change the physical proportions.
- To change the scope of a business or project to meet a change in demands.
- Establish on a new scale.
re-scale is an utility created to make it easier to port useful libraries to Scala using Claude Code via a project-agnostic Scala Native CLI.
It gives any codebase the same disciplined developer workflow without dragging
in project-specific behavior. One installable, tested, memory-bounded
binary that learns project specifics from .rescale/*.yaml config
files instead of hard-coding them.
re-scale is the third iteration of the same idea: a project-local dev CLI that wraps a fast Claude Code hook + a persistent migration state layer, written specifically to support large AI-assisted language-to-language porting work.
The lineage:
-
sge-devwas the original. It lived inside SGE — the Scala Game Engine, a cross-platform Scala Native game engine ported from libgdx + custom native components, targeting six desktop and three Android architectures simultaneously. sge-dev started as a handful of helper scripts and grew organically into ~2500 LOC of project-specific Scala. -
ssg-devwas bootstrapped FROM sge-dev when the same author started SSG — the Scala Static Site Generator — porting five upstream libraries to Scala Native cross-platform:flexmark-java(Markdown, Java),liqp(Liquid, Java),dart-sass(SCSS, Dart),jekyll-minifier(HTML/JS/CSS minification, Ruby), andterser(JavaScript compiler, JS). At the time of the re-scale split, ssg carried ~944 .scala files across 5 modules and ~80 KLoC. ssg-dev started as a copy of sge-dev with the obviously-game-engine-specific bits replaced; from there the two forks diverged file-by-file as each project grew its own opinions, until they were nearly-identical- but-not-quite duplicates. -
re-scaleis the rewrite that finally factors out the project-specific behavior into.rescale/*.yamlconfig files. No more forks. The tool itself is generic; the YAML in any given consuming repo carries the SGE-specific or SSG-specific or $YOUR-NEXT-PROJECT-specific opinions.
All three iterations share the same operating constraint: a single human guiding multiple Claude Code sessions through hundreds of files of language-to-language porting work. Neither sge-dev nor ssg-dev nor re-scale is a standard or widely-adopted approach — they were invented to make this specific workflow tractable.
Claude Code is excellent at code transformation, but the porting workflow exposed two recurring failure modes that off-the-shelf tooling didn't address:
By default, Claude Code can shell out to anything the OS allows. In a repo with hundreds of files and active in-flight work, that's a liability:
rm -rf <some-path>is usually correct, but the one time it isn't, it's catastrophic. A harddenysaves an hour of "did I just delete my work?" panic.- Reaching for
grep/find/cat/sedworks, but the dedicated Claude Code tools (Grep, Glob, Read, Edit) are much faster for the user (no shell startup, no terminal scrollback flooding) AND let Claude reason about results structurally instead of reparsing wall-of-text output. Redirecting these to the right tool with a one-linedenyreason teaches Claude to default to the better path. sbt --clienttalks to a persistent sbt server that keeps the JVM warm and avoids the multi-second startup penalty baresbtpays on every invocation. Forcing the client form means a 200 mscompileinstead of a 30 s one — and across a session of dozens of build invocations that compounds into hours of dead time. (sbt --clientitself can hang ifbuild.sbthas an error AND reload-on-change is enabled; the rule isn't "client is always hang-proof," it's "stop paying JVM startup over and over.")- A handful of patterns (downloading a
.jarto grep through, writing under/etc, reaching for.env) are so universally wrong they deserve a hard stop regardless of intent.
The sge-dev / ssg-dev hook layer was essentially a 288-LOC opinion
piece on "what should Claude Code never do without confirmation in
this project." It needed to be fast (every Bash invocation goes
through it) and always reachable (a stale binary or a missing
build is worse than no hook). re-scale's hook is a single ~1 ms
native binary that's pre-built and on $PATH — no per-call cold
start, no compile-on-first-use surprise.
Claude Code has built-in MEMORY.md files for cross-session memory,
but the porting workflows hit three limits:
- Memory is summarized, not authoritative. The model can mis-recall, conflate, or quietly drop entries. For "which Java file maps to which Scala file, what's its audit verdict, when was it last reviewed," the answer needs to be byte-exact, queryable, and never paraphrased.
- Memory isn't queryable. "Show me every minor-issues row in ssg-md, sorted by last-updated" isn't something you can ask the memory file. A real TSV can answer it in milliseconds.
- Memory isn't shared between humans and the agent. A teammate
reviewing migration progress wants to read the same data the agent
is reading. A TSV under
.rescale/data/is git-versioned, diff-able, and reviewable in a PR. AMEMORY.mdis per-user.
So both projects ended up with three TSV "tables" the dev CLI
managed, all stored under .rescale/data/:
.rescale/data/migration.tsv— every source file in the upstream library, its ported state (pending/in-progress/ported/skipped), and the corresponding Scala file path..rescale/data/audit.tsv— per-file audit verdict (pass/minor_issues/major_issues) with notes, used to gate "is this port actually faithful?" review..rescale/data/issues.tsv— open per-file issues with severity, category, and status — the porting equivalent of a bug tracker scoped to one repo.
All three are append-mostly, atomically locked, and read by Claude
Code via re-scale db <table> list/get/set. The agent never directly
reads or writes the TSV files (which would risk corruption); every
mutation goes through a single locked entry point. State is durable,
queryable, and survives every kind of session boundary — context
limit, network drop, machine reboot.
sge-dev started as a handful of scala-cli scripts and accreted features. ssg-dev was bootstrapped from sge-dev and then diverged file-by-file as ssg grew its own opinions. By the time anyone noticed, three nearly-identical-but-not-quite forks existed across three repos (sge-dev, ssg-dev main, and a long-lived ssg-dev worktree variant) with:
- Zero tests. Regressions silently accumulated for months.
- No memory ceiling. A scanner pass on a 944-file codebase consumed 48 GB of RAM and had to be killed manually before macOS paged out.
- Hand-rolled hook rules in 288 LOC of Scala that needed to be edited and rebuilt to add a single project-specific deny rule.
- Hard-coded module lists (
ssg-md,ssg-liquid, ...) that made the tool impossible to use in any other project without forking.
re-scale is the rewrite. The same developer-experience surface — hook gating, db CRUD, build/test/git wrappers, code-quality scanning — but generic, tested, memory-bounded, and configured per-project via YAML.
Drop a .rescale/ directory into any Scala project (or any project,
really — much of re-scale doesn't care about Scala). re-scale
auto-discovers source roots from */src/main/scala or
.rescale/scan-targets.txt, so the same binary works in
single-module, multi-module sbt, mill, or non-sbt projects.
re-scale hook is a PreToolUse validator that gates every Bash tool
call against a configurable rule set. Defaults catch the universal
footguns (rm -rf, git push --force, secret-file access, system-dir
writes, suboptimal-tool patterns), and any project can override or
extend them via .rescale/claude-hooks.yaml:
rules:
- when:
starts-with: [adb]
action: deny
reason: "Use 're-scale test android' instead of direct adb"
- when:
and:
- program-in: [ls]
- has-any: ["-la"]
action: allow
reason: "ls -la is fine in this project"The condition DSL supports starts-with, has-any, has-any-suffix,
has-any-contains, has-redirect-target-prefix, program-in, plus
and/or/not composition. Per-project rules merge in front of the
defaults (first-match-wins).
re-scale enforce is the anti-cheat workhorse. Five scanners share a
streaming FS2 file-I/O backend so a 1000-file scan stays under
150 MiB of RSS:
| Subcommand | What it catches |
|---|---|
enforce shortcuts |
TODO/HACK/???/UnsupportedOperationException patterns + 12 anti-cheat marker variants (null casts, comment-only stubs, "for now" hedges, scalastyle:ignore returns, ...) |
enforce stale-stubs |
Comments like // Foo.BAR is not yet ported where Foo.BAR is now actually defined elsewhere — the stub is stale and the file should be re-wired |
enforce verify |
Re-runs the Covenant header check (method-set parity + zero shortcuts) on a single file or every covenanted file |
enforce skip-policy |
TSV-backed allow list for legitimate exceptions (vendored code, generated files, etc.) |
enforce compare --strict |
Method-set + body-token-count + constructor-arity comparison between a Scala port and its source (.java/.dart) |
The headline acceptance gate (Ssg944FileMemoryBoundSpec) runs the
full scanner suite against the 944-file SSG codebase and asserts peak
RSS stays under 512 MiB. The legacy ssg-dev consumed 48 GB on the
same workload before the streaming rewrite.
re-scale doctor reads .rescale/doctor.yaml and walks every
declared check + optional install step. The engine is generic; the
sge project ships Rust + Android NDK + Zig steps, ssg ships sbt
checks, a Python project ships pip steps. re-scale never knows what
an NDK is.
steps:
- id: jdk
name: "JDK 22+ (Panama FFM)"
check:
command: java
args: [-version]
success-when:
stderr-matches: 'openjdk version "(2[2-9]|[3-9][0-9])\.'
install:
command: sdk
args: [install, java, 25.0.2-zulu]
interactive: true # skipped by --ci
hint: "Run: sdk install java 25.0.2-zulu"
- id: rust-targets
name: "Rust cross-compile targets"
check:
command: rustup
args: [target, list, --installed]
success-when:
stdout-contains: aarch64-apple-darwin
install:
command: rustup
args: [target, add, aarch64-apple-darwin, x86_64-apple-darwin]re-scale doctor --ci skips interactive installs but still reports
their status — great for actions/setup-* style CI bootstrapping.
re-scale runner reads .rescale/runners.yaml and dispatches
arbitrary test harnesses. The mode-file dance preserves the legacy
SassSpec correctness fix (write-before-exec, runner reads + deletes
on entry, never delete from the wrapper):
runners:
sass-spec:
description: "dart-sass spec compatibility test harness"
invoke:
command: sbt
args: [--client, "ssg-sass/testOnly ssg.sass.SassSpecRunner"]
mode-file:
path: ssg-sass/target/sass-spec-mode.tsv
format: kv
output:
success:
regex: 'sass-spec:\s+Total=(\d+)\s+Passing=(\d+)'
capture:
total: 1
passing: 2
failure:
keep-lines-matching: ['sass-spec:', 'FAIL', 'regressions']
max-lines: 10
modes:
regression: {}
strict: { strict: "1" }
snapshot: { snapshot: "1" }
subdir: { subdir: "$1" } # $1 = first positional CLI argre-scale runner sass-spec --mode subdir spec/css/unitsre-scale db operates on three append-mostly TSVs:
| Table | Rows |
|---|---|
migration |
per-source-file porting status |
issues |
open issues with id / severity / status |
audit |
per-file audit verdict (pass / minor / major) |
All writes go through Tsv.modify, which combines a per-path
in-process Mutex[IO] with a cross-process FileChannel.lock(). The
combination survives 20 concurrent writers without torn rows or ID
collisions — verified by IssuesDbConcurrencySpec. (Scala Native's
FileChannel.lock() is process-level, so the in-process mutex is
load-bearing for in-JVM parallelism.)
re-scale db merge --target a.tsv --source b.tsv reconciles two TSVs
across branches with ID-collision renumbering — exactly the bug we
hit during a multi-worktree gap-audit campaign where two agents
independently allocated ISS-002..025 and ISS-002..012.
re-scale proc list shows pid / %cpu / %mem / kind / cwd / command
for every sbt server, java process, and metals daemon on the machine.
--kind and --dir filters let you kill ONLY the sbt servers
running inside one specific worktree without nuking unrelated work
in other repos.
re-scale proc list --kind sbt
re-scale proc list --kind sbt --dir /Users/me/work/my-repo
re-scale proc kill --kind sbt --dir /Users/me/work/my-repo
re-scale proc kill --pid 12345--kind all requires --dir to prevent fat-finger disasters.
re-scale ships with a mandatory wrapper script that sets
SCALANATIVE_MAX_HEAP_SIZE=1G. Direct invocation of the binary is
unsupported. Combined with the streaming-first architecture
(FileOps.streamLines for every per-file walk; the StaleStubs
two-pass design that bounds memory by suspect-comment count instead
of identifier count; reusable java.util.regex.Matcher instances
that avoid per-match allocation), the headline gate is:
Scanning 944 Scala files (5 modules, ~80 KLoC of Scala) must peak under 512 MiB of RSS in under 30 s.
The legacy ssg-dev consumed 48 GB of RAM on the same workload before the rewrite. The current re-scale binary peaks at ~150 MiB.
re-scale is a Claude Code plugin marketplace. One command installs the CLI binary, all 11 skills, and the PreToolUse hook:
/plugin marketplace add https://github.com/kubuszok/re-scale
/plugin install re-scale@kubuszok-tools
On the first session after install, the plugin's SessionStart hook
automatically:
- Clones the re-scale repo to
~/.local/share/re-scale(or$RESCALE_HOMEif set) - Runs
sbt installto build the Scala Native binary (~30 s first time) - Adds
re-scale/bin/toPATHfor the session
The PreToolUse hook delegates to re-scale hook — no manual
.claude/hooks/pre-tool-use.sh setup needed.
To update after a re-scale release:
/plugin marketplace update kubuszok-tools
If you prefer manual control:
git clone https://github.com/kubuszok/re-scale.git
cd re-scale
sbt install # builds .build/re-scale-bin
export PATH="$PWD/bin:$PATH" # add to shell rcThe bin/re-scale wrapper auto-compiles on first run if the binary
is missing. No separate install script needed.
Both options require:
- JDK 22+ (sbt + Scala Native compilation)
- sbt (build tool)
- Clang / LLVM (Scala Native linker)
| Skill | Auto-loads when... |
|---|---|
guide-conversion |
converting a file from Java/Dart/Ruby to Scala |
guide-code-style |
formatting or style questions arise |
guide-nullable |
null-safety / Nullable[A] patterns needed |
guide-control-flow |
replacing return/break/continue |
guide-verification |
post-conversion verification |
audit-file |
auditing a file against its source |
verify-file |
verifying a ported file |
find-issues |
scanning for code quality issues |
check-progress |
checking migration/audit progress |
audit-status |
audit status per-package or overall |
gap-fix |
fixing enforcement failures in a file |
Skills reference re-scale commands, so the binary must be installed
first (see above).
Project-specific skills (like type-mappings for a specific library
or build architecture details) should stay in the project's own
.claude/skills/ directory — they don't belong in the marketplace.
Per-project, drop any subset of these into .rescale/:
| File | Purpose |
|---|---|
.rescale/claude-hooks.yaml |
Per-project hook rule overrides (merged in front of defaults) |
.rescale/doctor.yaml |
Dev-environment bootstrap steps for re-scale doctor |
.rescale/runners.yaml |
Test-runner adapters for re-scale runner <name> |
.rescale/modules.txt |
Module list (one per line) — overrides auto-discovery |
.rescale/scan-targets.txt |
Source roots for scanners — overrides auto-discovery |
Plus the auto-managed db tables, all under .rescale/data/:
| File | Purpose |
|---|---|
.rescale/data/migration.tsv |
Migration tracking |
.rescale/data/issues.tsv |
Issues database |
.rescale/data/audit.tsv |
Per-file audit verdict |
.rescale/data/skip-policy.tsv |
Enforcement allow list |
Never edit these files by hand — every mutation goes through the
locked re-scale db <table> set/add/resolve entry points so concurrent
agent sessions don't tear rows. The directory is git-versioned and
diff-friendly: a PR review can show exactly which rows changed.
Add the hook to your .claude/hooks/pre-tool-use.sh:
#!/bin/bash
set -euo pipefail
if command -v re-scale >/dev/null 2>&1; then
exec re-scale hook
fi
# (optional) fall back to a vendored binary if re-scale isn't installed yet
exec "$(dirname "$0")/../../scripts/bin/dev-tool" hookThat's it. re-scale hook reads the PreToolUse JSON event from stdin,
evaluates the merged rule set, and emits a wrapped
hookSpecificOutput decision.
src/main/scala/rescale/
Main.scala # IOApp dispatcher
common/ # Cli, Paths, Term, Tsv, FileOps, Proc
hook/ # BashParser, RuleEvaluator, DefaultRules,
# RuleConfig (YAML loader), HookCmd
db/ # MigrationDb, IssuesDb, AuditDb, Merge, DbCmd
enforce/ # Covenant, Shortcuts, Methods, StaleStubs,
# SkipPolicy, EnforceCmd
build/ # BuildCmd (sbt --client wrappers)
test/ # TestCmd
git/ # GitCmd (git + gh)
proc/ # ProcCmd (process discovery + filtering)
doctor/ # Doctor engine + DoctorCmd
runner/ # Runner engine + RunnerCmd
metals/ # MetalsCmd (LSP server lifecycle)
Built on:
- Scala Native 0.5 for fast-startup native binaries
- Cats Effect 3.7 for resource-safe IO
- FS2 3.13 for streaming file I/O (the linchpin of the memory invariant)
- kindlings-yaml-derivation for YAML config schemas
- kindlings-jsoniter-json for future structured CLI output
- cats-parse (legacy) for the bash grammar
- munit + munit-cats-effect for tests
| Suite | Tests | Notes |
|---|---|---|
rescale.common.* |
34 | Cli, FileOps, Paths, Tsv, PathsModules |
rescale.hook.* |
98 | BashParser, RuleEvaluator, DefaultRules, HookCmd, RuleConfig |
rescale.db.* |
23 | DbCmd, Merge, IssuesDbConcurrency |
rescale.enforce.* |
44 | Covenant, CovenantApply, Shortcuts, Methods, StaleStubs, SkipPolicy + Ssg944 gate |
rescale.proc.* |
8 | ProcCmd parsers |
rescale.doctor.* |
12 | DoctorConfig schema + Doctor engine |
rescale.runner.* |
10 | RunnersConfig schema + Runner engine + 100k-line streaming regression |
rescale (Version, Memory bound) |
5 | sanity + memory acceptance |
| Total | 274 | 0 failed |
Apache-2.0. See LICENSE.