run
re-shell run <task> runs a script across every workspace package in
dependency order. It builds an execution DAG of (package, task) nodes from
your workspace dependency graph plus an optional tasks config, detects cycles
before anything runs, then executes with bounded parallelism. Packages that
don’t define the script are skipped — their dependents still run.
re-shell run buildre-shell run test --affectedre-shell run lint --filter web --filter apire-shell run build --concurrency 1 --jsonUsage: re-shell run [options] <task>
Arguments: task Task/script name to run, e.g. build or test
Options: --affected Only run for packages affected by current changes --concurrency <n> Max parallel tasks (default: CPU count) --filter <pkg...> Restrict to specific package name(s) --json Output the run summary as a JSON envelope --continue Continue scheduling unaffected branches after a failure --no-cache Disable the content-addressed build cache --cache-dir <dir> Override the cache directory (default: <root>/.re-shell/cache)How ordering works
Section titled “How ordering works”For each requested (package, task), the runner expands the full set of nodes
that must run and wires two kinds of edges:
- Intra-package (
"build"): a sibling task in the same package must finish first. By defaulttestdepends onbuild, soweb:testwaits forweb:build. - Upstream (
"^build"): the same task on each upstream workspace dependency. By defaultbuilddepends on^build, so ifwebdepends onui, thenweb:buildwaits forui:build.
A package’s upstream dependencies are derived from its package.json
dependencies/devDependencies that resolve to other packages in the same
workspace (registry deps are never edges). Discovery scans the conventional
roots apps/, packages/, libs/, and tools/.
Example
Section titled “Example”Given web → ui → tokens (each depends on the next):
re-shell run build# tokens:build → ui:build → web:build
re-shell run test# tokens:build → ui:build → web:build, then each package's :test# (every test waits for its own build and all upstream builds)The tasks config
Section titled “The tasks config”Defaults work with zero config:
| Task | Depends on | Meaning |
|---|---|---|
build | ^build | build all upstream deps first |
test | build | build this package before testing it |
To customise the task graph, add a tasks section to
re-shell.workspaces.yaml at the workspace root:
tasks: build: dependsOn: ["^build"] test: dependsOn: ["build", "lint"] # test waits for this package's build AND lint lint: dependsOn: [] # leaf task, no prerequisites typecheck: dependsOn: ["^build"] # typecheck after all upstream buildsEach task name maps to { dependsOn?: string[] }. An entry is either:
- a sibling task name (
"build","lint") — same package, or - a
^-prefixed task name ("^build") — that task on every upstream workspace dependency.
A task present in your config fully replaces the default for that name (no
deep-merge of dependsOn), so overrides are predictable. Task names match
^\^?[a-zA-Z0-9][a-zA-Z0-9:_-]*$.
Cycle errors
Section titled “Cycle errors”If the resulting graph contains a cycle (e.g. build → test → build), the run
fails before executing anything:
re-shell run build# ✗ Task dependency cycle detected: a#build -> a#test -> a#buildThe exit code is non-zero and no script is spawned. In --json mode this is a
RUN_ERROR envelope (see below). Cycles are detected across both intra-package
and upstream edges.
Caching and outputs
Section titled “Caching and outputs”run is backed by a content-addressed build cache that is on by default.
When a task’s inputs are unchanged, its result is replayed from the cache —
declared outputs are restored to disk and logs are replayed — and the node is
reported as cached with no spawn:
re-shell run build # 1st run spawns + caches each taskre-shell run build # 2nd run: unchanged tasks are `cached`Declare each task’s inputs/outputs globs in the tasks config so the cache
knows what to hash and what to restore:
tasks: build: dependsOn: ["^build"] inputs: ["src/**", "package.json"] outputs: ["dist/**"]Cache keys fold in the script body, input file hashes, the dependency closure’s
keys, an offline toolchain fingerprint, and an allow-listed env subset, so any
real change busts the cache. Only successful (exit-0) runs are cached, every
artifact is HMAC-signed and re-verified before it is trusted, and a tampered
entry degrades to a normal run. Use --no-cache to bypass it and --cache-dir
to relocate it. See cache for keys, local vs remote
(hub) backends, CI hydration, and cache stats / cache clean.
--affected
Section titled “--affected”run <task> --affected scopes the target packages to those impacted by your
current working-tree changes:
re-shell run test --affectedIt reads git changes (git diff --name-only HEAD plus untracked files), maps
each file to its owning package, and expands that set with its transitive
dependents (a change to an upstream package affects everything downstream of
it). The analysis is fully offline and deterministic; if git is unavailable it
degrades to “nothing affected” rather than failing.
Upstream builds still run when a downstream package needs them. If only web
changed and web depends on ui, then run test --affected runs ui:build,
web:build, and web:test — but not ui:test.
--concurrency and --filter
Section titled “--concurrency and --filter”--concurrency <n>caps how many tasks run in parallel. The default is your CPU count.--concurrency 1serialises the whole plan while still honouring dependency order.--filter <pkg...>restricts the root target packages by name (repeatable or comma-separated). Upstream dependencies the targets need are still pulled in automatically.
re-shell run build --filter web,apire-shell run test --concurrency 4Scripts run via the detected package manager (pnpm / yarn / npm, chosen by
the nearest lockfile) as an argv array with shell: false — package and
task names are never interpreted by a shell.
JSON output
Section titled “JSON output”--json emits a single-line typed envelope conforming to the
JSON contract:
{ "ok": true, "data": { "task": "build", "concurrency": 8, "results": [ { "package": "ui", "task": "build", "status": "success", "exitCode": 0, "durationMs": 412 }, { "package": "web", "task": "build", "status": "success", "exitCode": 0, "durationMs": 880 } ], "affected": ["web"] }}resultsis ordered for stable display. Each entry’sstatusis one of"success","cached","failed", or"skipped". A"cached"node was replayed from the build cache instead of being spawned (its outputs were restored and its logs replayed).exitCodeisnullfor skipped nodes (the package had no such script, or an upstream dependency failed).affectedis present only when--affectedwas used.- A failing task still produces
ok: true(the run completed) with the failure recorded inresults; the process exit code is non-zero so CI fails. - A dependency cycle produces
ok: falsewith error codeRUN_ERRORand an empty plan — nothing ran:
{ "ok": false, "error": { "code": "RUN_ERROR", "message": "Task dependency cycle detected: a#build -> a#test -> a#build", "details": { "task": "build", "cycle": ["a#build", "a#test", "a#build"] } } }Polyglot by design
Section titled “Polyglot by design”run is task-name-agnostic and language-agnostic: it orchestrates whatever
scripts each package.json declares. The same command sequences a TypeScript
frontend, a Python service wrapper, and a Go tool in one dependency-ordered
pass, because edges come from the workspace graph and the tasks config — not
from any single toolchain. A package that doesn’t define the requested script is
simply skipped, so heterogeneous workspaces compose without special-casing.
Failure behaviour
Section titled “Failure behaviour”- A task that exits non-zero is recorded as
failed; its dependents are cascaded toskipped(never spawned) so the run still terminates. - The overall process exits non-zero if any task failed or a cycle was detected.
--continuekeeps scheduling independent branches after a failure rather than winding down.