If you let AI coding agents loose on a frontend repo, they run npm run build and npm run dev constantly — and often from several sessions at once. Nothing is broken, exactly. You just have four builds racing in one checkout, two dev servers fighting over a port, no way to tell them apart, and no record of what any of them did.
tman fixes the everyday version of that problem: it gives every run a name, a slot, and a record. The same suite never runs twice at once, excess runs queue instead of stampeding your cores, and every run leaves a JSON record you can query afterwards. Underneath sits a backstop that kills the rare run that genuinely hangs or leaks.
It's a single ~3.8 MB NativeAOT binary with zero runtime dependencies (Linux, macOS, Windows). This post walks through adopting it in a stock Vite project.
Install
npm install -g @standardbeagle/tman
Or the shell one-liner if you don't want it in your global node modules:
curl -fsSL https://raw.githubusercontent.com/standardbeagle/tman/main/install.sh | sh
A stock Vite project
Nothing special about the target project — this is the plain scaffold:
npm create vite@latest vite-demo -- --template react-ts
cd vite-demo
npm install
Which gives you the usual scripts in package.json:
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
}
Step 1 — adopt tman
One command:
tman init --shims --gitignore
tman init reads package.json, detects the scripts worth supervising, and writes three things:
-
.tman.kdl— the per-project config with an alias per detected script -
shims — tiny
./buildand./lintwrappers at the repo root, so existing muscle memory (and agents that just run./build) go through tman transparently -
.gitignoreentries for the shims
Note what it did not scaffold: there's no test script in a fresh Vite app, so there's no ./test shim pretending otherwise. Aliases tman can't detect are left commented out — a missing suite fails loudly instead of faking a pass.
Step 2 — look at .tman.kdl
The defaults block is deliberately conservative:
-
stall "30m"— a hang backstop, not a runtime budget. It only fires when a run is silent and idle for 30 minutes. A coldtsc -bcan legitimately think for a long while without printing; a stall sized like an expected runtime would kill healthy work. -
max-parallel 2— at most two runs per bucket at once; the rest queue instead of racing. -
retain "24h"— how long finished run records stick around. - The memory/CPU/wall-clock ceilings are commented out because builds legitimately saturate cores and eat RAM. You opt in when a suite has actually misbehaved.
Each alias block maps a name to a command, so tman build (or the ./build shim) means npm run build — supervised.
Step 3 — run a supervised build
./build
Identical output to npm run build, because it is npm run build — plus a name, a dedup lock, a slot in the queue, and a run record. The supervision is invisible until the day you need it.
Step 4 — the dev server problem
Here's the case that sold me. Two agent sessions both decide the dev server should be running:
tman run --name dev -- npm run dev &
tman run --name dev -- npm run dev
The second invocation is refused immediately:
tman: run 'dev' already active (pid 2244513, id f281b5c2ad30); use --replace to kill it
No second Vite instance, no port fallback to 5174, no zombie server owning the port after the session that started it dies. If you want takeover semantics — a fresh server after a config change — that's --replace, which kills the old run and waits for it to hand the name back.
Locks bucket by name and directory, so a dev run in this repo never blocks a dev run in another checkout.
One tuning note for Vite specifically: never give a dev server --max-time. It's supposed to run forever. The wall-clock cap is for suites and builds, not servers.
Step 5 — what ran, and how did it go?
tman list --all
tman status
Every run leaves one JSON record: command, cwd, exit code, peak memory, start and finish, and the caps it ran under. tman status --json emits the same thing machine-readable, which is exactly what you want when an agent asks "did the build I started actually finish?" — or when you ask what your agents ran overnight.
Orphan reaping rides along for free: every tman command kills children whose runner died and prunes expired records. A crashed agent session doesn't leave a Vite server squatting on the port.
Step 6 — catch the runs that bypass the shims
Shims only catch commands that go through a shell lookup. An agent calling its Bash tool with a bare npm run build walks straight past them. For Claude Code, one hook applies the same policy a level up:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "tman hook pretooluse" }] }
]
}
}
It rewrites bare test/build commands through tman run, leaves everything else untouched, and never blocks: every failure path (tman missing, malformed request, unreadable project) leaves the command exactly as written. There are equivalent guides for Codex CLI, Gemini CLI, Cursor, opencode, and others.
Wrap-up
Total setup cost for a Vite project: one npm install -g, one tman init. From then on:
- duplicate builds and dev servers are refused, not raced
- excess runs queue for a slot instead of saturating cores
- every run leaves a record you (or your agents) can query
- hangs and leaks get killed by a backstop that a healthy week never triggers
Docs: dev.standardbeagle.com/tman · Source: github.com/standardbeagle/tman · npm: @standardbeagle/tman






Top comments (0)