Nadle Specification
Version: 1.2.0
This directory contains the language-agnostic specification for Nadle, a type-safe, Gradle-inspired task runner for Node.js.
1Purpose
This specification is the single source of truth for Nadle’s behavior. It describes concepts, rules, and contracts in plain English without referencing any specific programming language. It is intended for:
- Implementors porting Nadle to another language or runtime
- Contributors verifying that behavior matches the spec
- Testers writing assertions grounded in documented rules
2Concept Dependency Map
Task (01) --> Configuration (02) --> Scheduling (03) --> Execution (04) --> Caching (05)
^
Project (06) --> Workspace (07) |
|
Configuration Loading (08) ---------------+
|
CLI (09) ---------------------------------+
Built-in Tasks (10)
Events (11)
Error Handling (12)
Reporting (13)
3Glossary
| Term | Definition |
|---|---|
| Task | A named unit of work with an optional function and optional typed options. |
| Workspace | A directory within the project that can register its own tasks. |
| Project | The top-level container: a root workspace, zero or more child workspaces, and a detected package manager. |
| Declaration | A file or directory pattern used to describe task inputs or outputs. |
| Fingerprint | A SHA-256 hash of a file’s contents, used for cache key computation. |
| Cache Key | A hash derived from task ID, input fingerprints, and task environment. |
| DAG | Directed Acyclic Graph representing task dependencies. |
| Listener | An object with optional methods for lifecycle events. |
| Handler | A command handler (List, DryRun, Execute, etc.) selected by the CLI. |
| Runner Context | The logger and working directory provided to every task function. |
---
4Task Model
A task is the fundamental unit of work in Nadle. Each task has a name, belongs to a workspace, and may carry a function to execute and typed options.
4.1Registration
Tasks are registered via the tasks API, which is available from the public API. During config file loading, calls to tasks.register() delegate to the active Nadle instance via an AsyncLocalStorage context. Each Nadle instance owns its own task registry, ensuring full isolation between instances. There are three registration forms:
| Form | Parameters | Description |
|---|---|---|
| No-op | name |
Registers a lifecycle-only task with no function body. Useful as an aggregation point for dependencies. |
| Function | name, taskFn |
Registers a task with a function that receives a runner context. |
| Typed task | name, taskObject, optionsResolver |
Registers a reusable task type with typed options and a resolver. |
All three forms return a configuration builder that exposes a .config() method (see 02-task-configuration.md).
4.1.1Task Function Signature
A task function receives an object with:
context— the runner context (see below)
A typed task’s run function receives:
options— the resolved options for this task instancecontext— the runner context
Both must return void (or a promise of void).
4.1.2Runner Context
Every task function receives a runner context containing:
| Field | Description |
|---|---|
logger |
A structured logger with methods: log, warn, info, error, debug, throw, getColumns. |
workingDir |
The resolved absolute working directory for this task. |
4.2Naming Rules
Task names must match the pattern: ^[a-z]([a-z0-9-]*[a-z0-9])?$ (case-insensitive).
If a task name is invalid, registration fails with an error.
4.3Duplicate Detection
Task names must be unique within a workspace. The same name may appear in different workspaces. If a duplicate name is registered in the same workspace, registration fails with an error.
4.4Task Identity
A task is uniquely identified by a task identifier string:
| Scope | Format | Example |
|---|---|---|
| Root workspace | {taskName} |
build |
| Child workspace | {workspaceId}:{taskName} |
packages:foo:build |
The separator is a colon (:). The last segment is always the task name; preceding segments form the workspace ID.
4.5Status Lifecycle
A task moves through the following statuses:
+-> Finished
|
Registered -> Scheduled -> Running -+-> Failed
| |
| +-> Canceled
|
+-> UpToDate
|
+-> FromCache
| Status | Value | Meaning |
|---|---|---|
| Registered | "registered" |
Task is registered but not yet scheduled. |
| Scheduled | "scheduled" |
Task is included in the execution plan. |
| Running | "running" |
Task function is currently executing in a worker. |
| Finished | "finished" |
Task function completed successfully. |
| UpToDate | "up-to-date" |
Cache validation determined outputs are current; task was skipped. |
| FromCache | "from-cache" |
Outputs were restored from cache; task was skipped. |
| Failed | "failed" |
Task function threw an error. |
| Canceled | "canceled" |
Worker was terminated before the task completed. |
4.5.1Transition Rules
- UpToDate and FromCache are entered directly from Scheduled, without passing through Running. These tasks never emit a “start” event.
- Only tasks in Running can transition to Finished, Failed, or Canceled.
- The Running counter is only decremented for Finished, Failed, and Canceled transitions (not for UpToDate or FromCache).
4.6Reusable Task Types
The defineTask() function creates a reusable task type with a typed options contract. It is an identity function that enables type inference for the run function’s options parameter.
A reusable task type is then registered with tasks.register(name, taskObject, resolver) where the resolver provides the concrete options for this instance.
---
5Task Configuration
Every registered task may be configured via a builder pattern. The .config() method accepts either a static configuration object or a callback that returns one.
5.1Configuration Fields
All fields are optional.
| Field | Type | Description |
|---|---|---|
dependsOn |
string or array of strings | Tasks that must complete before this task runs. |
env |
map of string to string/number/boolean | Environment variables injected into the worker. |
workingDir |
string | Working directory for the task, relative to the project root. |
inputs |
declaration or array of declarations | File patterns the task reads from. Used for cache fingerprinting. |
outputs |
declaration or array of declarations | File patterns the task produces. Used for caching and restoration. |
group |
string | Group label for display in --list output only. |
description |
string | Description for display in --list output only. |
5.2Builder Pattern
The configuration builder exposes a single method:
config(builderOrObject)— accepts a static object or a callback returning the object.
Calling .config() replaces the entire configuration (it does not merge). The last call wins.
5.3dependsOn Resolution
Dependency strings are resolved as follows:
- No colon (e.g.,
"build") — resolved within the current workspace. - With colon (e.g.,
"packages:foo:build") — the last segment is the task name; preceding segments form the workspace ID. Resolved by workspace ID or label. - Root workspace — use
"root:taskName"(root workspace ID is always"root").
If a dependency is not found in the target workspace, Nadle falls back to the root workspace. If still not found, an error is raised with suggestions.
Excluded tasks (via --exclude) are filtered out of the resolved dependency set.
5.4Declarations DSL
Declarations describe file patterns for inputs and outputs. There are two types:
| Type | Factory | Pattern Behavior |
|---|---|---|
| File | Inputs.files(...patterns) or Outputs.files(...patterns) |
Each pattern is a file glob resolved against the working directory. |
| Directory | Inputs.dirs(...patterns) or Outputs.dirs(...patterns) |
Each pattern matches directories; all files within matched directories are included recursively. |
Outputs.files and Outputs.dirs are aliases for Inputs.files and Inputs.dirs respectively — there is no functional difference.
5.4.1Pattern Resolution
- Static paths are resolved relative to the working directory.
- Glob patterns are expanded using a glob library with
onlyFiles: true. - Directory declarations expand to
{pattern}/**/*to capture all nested files.
5.5Environment Variables
The env field accepts a map of key-value pairs where values may be strings, numbers, or booleans. Non-string values are converted to strings (via String(val)) before being applied to the worker process environment.
Environment variables are applied before the task function runs and restored to their original values afterward.
5.6Working Directory
The workingDir field is resolved relative to the project root workspace’s absolute path. If omitted, it defaults to the project root. The resolved absolute path is provided to the task function via the runner context.
---
6Scheduling
Nadle schedules tasks by constructing a directed acyclic graph (DAG) from declared dependencies, then processes the graph using a topological-sort-based algorithm.
6.1DAG Construction
The scheduler maintains three internal graphs:
| Graph | Key → Value | Purpose |
|---|---|---|
| Dependency graph | taskId → set of dependency taskIds | Direct dependencies of each task. |
| Transitive dependency graph | taskId → set of all transitive dependency taskIds | Full closure used for sequential mode filtering. |
| Dependents graph (reverse) | taskId → set of dependent taskIds | Reverse edges for indegree updates. |
Additionally, an indegree map tracks the number of unresolved dependencies for each task.
6.1.1Analysis Phase
For each requested task (and its transitive dependencies):
- Resolve
dependsOnfrom the task’s configuration. - Filter out excluded tasks.
- Record edges in the dependency and dependents graphs.
- Recursively analyze each dependency.
- Build transitive closure in the transitive dependency graph.
6.2Cycle Detection
After analysis, cycles are detected using depth-first traversal. For each task, the scheduler walks its dependency chain. If a task is encountered that already exists in the current path, a cycle is detected.
- If a cycle is found, Nadle raises an error that includes the full cycle path (e.g.,
a -> b -> c -> a). - Cycle detection runs before any task execution begins.
6.3Workspace Task Expansion
When a task is specified at the root workspace level and child workspaces have tasks with the same name, Nadle automatically expands the request to include the matching child workspace tasks. This only applies to tasks registered in the root workspace.
6.4Execution Modes
6.4.1Parallel Mode (`—parallel`)
All requested tasks and their dependencies are considered together. Any task whose indegree reaches zero is immediately eligible for execution.
- The scheduler does not restrict which zero-indegree tasks can run.
- All ready tasks from all requested task trees run concurrently.
6.4.2Sequential Mode (default)
Tasks are processed one “main task” at a time, in the order they were specified on the command line.
- The first specified task becomes the main task.
- Only tasks that are the main task or within its transitive dependency tree are eligible for scheduling.
- Within the main task’s tree, all zero-indegree tasks run concurrently (dependencies within a chain step still parallelize).
- When the main task completes, the scheduler advances to the next specified task.
- If the next main task’s dependencies are already satisfied, it may start immediately.
6.4.3Ready Task Computation (Kahn’s Algorithm)
- Initial: all tasks with indegree zero in the eligible set are “ready.”
- On completion: for each dependent of the completed task, decrement its indegree. If the dependent’s indegree reaches zero and it belongs to the current eligible set, it becomes ready.
- Main task completion (sequential mode only): advance to the next main task and recompute the initial ready set.
6.5Exclusion
Tasks specified via --exclude are removed from consideration during analysis. They are filtered out of dependency sets, so they and their exclusive subtrees are not scheduled.
6.6Execution Plan
The execution plan is the ordered list of tasks produced by simulating Kahn’s algorithm to completion. This plan is used by dry-run mode to display the intended execution order.
---
7Execution
Tasks are executed in isolated worker threads managed by a thread pool.
7.1Worker Pool
The pool is configured with:
| Setting | Default | Description |
|---|---|---|
minThreads |
availableParallelism - 1 |
Minimum number of worker threads. |
maxThreads |
availableParallelism - 1 |
Maximum number of worker threads. |
concurrentTasksPerWorker |
1 |
Always one task per worker at a time. |
Worker count values are clamped to [1, availableParallelism]. Percentage strings (e.g., "50%") are multiplied by availableParallelism and rounded.
7.2Worker Parameters
Each task dispatch sends these parameters to the worker:
| Parameter | Description |
|---|---|
taskId |
The task identifier string. |
port |
A MessagePort for sending messages back to the pool. |
env |
The original process environment at dispatch time. |
options |
The fully resolved Nadle options (with footer forced to false). |
7.3Message Protocol
Workers communicate back to the pool via MessagePort. There are exactly three message types:
| Type | Fields | Meaning |
|---|---|---|
"start" |
threadId |
The task function is about to execute. Sent after cache validation determines the task must run. |
"up-to-date" |
threadId |
Cache validation determined outputs are current. No execution needed. |
"from-cache" |
threadId |
Outputs were restored from cache. No execution needed. |
7.3.1Completion Detection
There is no explicit “done” message. Completion is inferred:
- Success: the worker’s promise resolves. The pool then checks the message type received to determine the outcome (execute, up-to-date, or from-cache).
- Failure: the worker’s promise rejects with an error.
7.4Worker Execution Flow
- Initialize Nadle in the worker thread on the first task dispatch using a lightweight path: the worker receives the fully resolved options (including the project structure) from the main thread, loads config files to populate task function closures and the task registry, but skips project resolution, option merging, and task input resolution. The instance is cached at module scope and reused for subsequent dispatches within the same thread, so config files are loaded at most once per worker thread lifetime.
- Look up the task by ID in the registry.
- Resolve the task’s configuration and options.
- Resolve the working directory (relative to project root).
- Run cache validation (see 05-caching.md).
- Based on validation result:
- not-cacheable or cache-disabled: send
"start", apply env, execute, restore env. - up-to-date: send
"up-to-date", return. - restore-from-cache: restore outputs, update cache pointer, send
"from-cache". - cache-miss: log reasons, send
"start", apply env, execute, restore env, save outputs and metadata.
- not-cacheable or cache-disabled: send
7.5Environment Injection
Before executing the task function:
- The original process environment is merged with the task’s
envfield. - After execution, injected keys are removed and original values restored.
Non-string env values are converted to strings before application.
7.6Cancellation
If a task fails and other tasks are still running:
- The pool is destroyed, which terminates all worker threads.
- A terminated worker throws a “Terminating worker thread” error.
- The pool detects this error and checks if the task’s status is Running.
- If Running, the task is marked as Canceled (not Failed).
7.7Cleanup
The pool is always destroyed in a finally block after execution, whether it succeeds or fails. This ensures all worker threads are terminated.
7.8Task Chaining
After a task completes successfully, the pool queries the scheduler for newly ready tasks (those whose indegree has reached zero). Each ready task is dispatched to the pool, enabling concurrent execution of independent tasks.
---
8Caching
Nadle caches task outputs to avoid redundant work. Caching is based on input fingerprinting and output snapshots.
8.1Precondition
A task is cacheable only if both inputs and outputs are declared in its configuration. If either is missing, the task is always executed.
8.2Validation Outcomes
Cache validation produces exactly one of five results:
| Result | Condition | Action |
|---|---|---|
not-cacheable |
Task has no inputs or no outputs declared. | Execute the task. |
cache-disabled |
The --no-cache flag is set. |
Execute the task. |
up-to-date |
Cache key matches the latest run AND output fingerprints are unchanged on disk. | Skip execution entirely. |
restore-from-cache |
Cache key found in run history, but outputs need restoration. | Copy cached outputs to project, skip execution. |
cache-miss |
No cache entry exists for the current cache key. | Execute the task, then save outputs. |
8.3Validation Flow
- Check if task is cacheable (inputs AND outputs defined). If not, return
not-cacheable. - Check if caching is enabled (
cacheflag). If not, returncache-disabled. - Compute input fingerprints from config files and declared input patterns.
- Compute cache key from
{taskId, inputsFingerprints, env}. - Check if a cache entry exists for this key.
- If no cache entry exists, return
cache-misswith reasons. - Read the latest run metadata.
- Compute current output fingerprints.
- If the latest run’s cache key matches AND output fingerprints match, return
up-to-date. 10. Otherwise, returnrestore-from-cache.
8.4Input Fingerprinting
Each input file is hashed with SHA-256 to produce a hex-encoded fingerprint. The result is a map from absolute file path to fingerprint string.
8.4.1Implicit Inputs
Config files are always included as implicit inputs:
- The root workspace config file (always present).
- The current workspace config file (if it exists and differs from the root).
This ensures cache invalidation when configuration changes.
8.4.2Declared Inputs
File declarations are resolved via glob against the working directory. Directory declarations are expanded to include all nested files recursively.
8.5Cache Key Computation
The cache key is computed by hashing an object containing:
| Field | Description |
|---|---|
taskId |
The task identifier string. |
inputsFingerprints |
Map of file path to SHA-256 hash. |
env |
The task’s environment variables (if any). |
The hash is SHA-256 with unordered object and array comparison, producing a 64-character hex string.
8.6Up-to-date vs Restore-from-cache
| | Up-to-date | Restore-from-cache | | ---------------------------- | ------------------------- | ----------------------------------------------- | | Cache key matches latest run | Yes | Not necessarily (may match a non-latest run) | | Output files exist on disk | Yes, with correct content | May be missing or modified | | Action | Skip entirely | Copy cached outputs back, update latest pointer |
8.7Cache Miss Reasons
When a cache miss occurs, reasons are computed by comparing the previous run’s input fingerprints with the current ones:
| Reason | Condition |
|---|---|
no-previous-cache |
No previous run metadata exists at all. |
input-changed |
A file exists in both old and new, but its fingerprint differs. |
input-removed |
A file existed in the old fingerprints but not in the new. |
input-added |
A file exists in the new fingerprints but not in the old. |
Multiple reasons may be reported for a single cache miss.
8.8Storage Layout
Cache data is stored under the cache directory (default: .nadle/):
{cacheDir}/
tasks/
{encodedTaskId}/
metadata.json # Latest run pointer
runs/
{cacheKey}/ # 64-char hex hash
metadata.json # Run metadata
outputs/ # Snapshot of output files
{relative-paths}...
8.8.1Task ID Encoding
Task identifiers containing colons are encoded by replacing colons with underscores for filesystem compatibility. For example, packages:foo:build becomes packages_foo_build.
8.8.2Metadata Structures
Task metadata (tasks/{id}/metadata.json):
| Field | Description |
|---|---|
latest |
Cache key of the most recent run. |
Run metadata (tasks/{id}/runs/{key}/metadata.json):
| Field | Description |
|---|---|
version |
Schema version (currently 1). |
taskId |
Task identifier string. |
cacheKey |
Cache key for this run. |
timestamp |
ISO 8601 timestamp of when the run was cached. |
inputsFingerprints |
Map of file path to SHA-256 hash. |
outputsFingerprint |
SHA-256 hash of all output fingerprints combined. |
8.9Output Snapshot
8.9.1Saving
After a successful execution on cache miss:
- Compute fingerprints for all output files.
- For each output file, copy it from the project to the cache’s
outputs/directory, preserving relative paths. - Write run metadata.
- Update the task’s latest pointer.
8.9.2Restoring
On restore-from-cache:
- Read all files from the cached
outputs/directory. - Copy each file back to its original location in the project, creating directories as needed.
- Update the task’s latest pointer to the restored cache key.
8.10Cache Update Flow
After validation, the cache is updated based on the result:
| Result | Update Action |
|---|---|
not-cacheable |
No action. |
up-to-date |
No action. |
restore-from-cache |
Update latest run pointer. |
cache-miss |
Save outputs, write run metadata, update latest pointer. |
cache-disabled |
No action. |
---
9Project Model
A project is the top-level container in Nadle. It consists of a root workspace, zero or more child workspaces, and a detected package manager.
9.1Project Structure
| Field | Description |
|---|---|
rootWorkspace |
The root workspace (always present, always has a config file). |
workspaces |
Sorted list of child workspaces. |
packageManager |
Detected package manager name ("pnpm", "npm", or "yarn"). |
currentWorkspaceId |
ID of the workspace where Nadle was invoked (defaults to root). |
9.2Root Detection
The project root is found by searching upward from the current directory:
- Look for a
nadle.config.{js,mjs,ts,mts}file. - Detect a monorepo root via package manager tooling (lock files, workspace config).
- Check for
nadle.root: trueinpackage.json.
The root workspace must have a config file. If no config file is found, Nadle raises an error with guidance to use --config.
9.3Package Manager Detection
The package manager is detected automatically from lock files and workspace configuration — it is not manually configured. Detection uses the @manypkg/tools library.
| Lock File | Package Manager |
|---|---|
pnpm-lock.yaml |
pnpm |
package-lock.json |
npm |
yarn.lock |
yarn |
9.4Workspace Discovery
Child workspaces are discovered via the package manager’s workspace configuration:
- pnpm:
pnpm-workspace.yaml - npm/yarn:
workspacesfield in rootpackage.json
Each discovered package directory becomes a workspace (see 07-workspace.md).
Workspaces are sorted by their relative path for deterministic ordering.
9.5Project Resolution Flow
- Find the project root (config file or monorepo root).
- Detect the package manager.
- Discover all workspaces.
- Create the root workspace with its config file path.
- Create child workspaces with their package metadata.
- Resolve workspace dependencies from
package.json. - Apply alias configuration (if any).
- Validate workspace labels for uniqueness.
9.6Current Workspace
The current workspace is determined by the directory where Nadle is invoked. It defaults to the root workspace. The current workspace ID affects which workspace receives task registrations when loading config files.
---
10Workspace Model
A workspace is a directory within the project that can register its own tasks.
10.1Workspace Fields
| Field | Description |
|---|---|
id |
Unique identifier derived from the relative path. |
label |
Human-readable display label (defaults to the ID). |
relativePath |
Path relative to the project root. |
absolutePath |
Absolute filesystem path. |
dependencies |
List of workspace IDs this workspace depends on (from package.json). |
packageJson |
Parsed package.json contents. |
configFilePath |
Path to this workspace’s config file, or null if none exists. |
10.2Identity
Workspace IDs are derived from the relative path by replacing path separators with colons:
| Relative Path | Workspace ID |
|---|---|
packages/foo |
packages:foo |
shared/api |
shared:api |
apps/web/client |
apps:web:client |
. (root) |
root |
The root workspace always has the ID "root" and the relative path ".".
Backslashes (Windows paths) are normalized to forward slashes before conversion.
10.3Config Files
Each workspace may have its own nadle.config.{js,mjs,ts,mts} file:
- The root workspace’s config file is required.
- Child workspace config files are optional.
- Workspace config files are loaded after the root config file.
- Config files register tasks scoped to their workspace.
10.4Workspace Dependencies
Workspace dependencies are populated from the package.json dependency fields:
dependenciesdevDependenciespeerDependenciesoptionalDependencies
If a dependency references another workspace in the project (e.g., via workspace:* protocol), it is recorded as a workspace dependency. These dependencies are informational and used for workspace ordering — they do not automatically create task dependencies.
10.5Aliases
Aliases provide human-readable labels for workspaces. They are configured via the configure() function in the root config file:
- Object map:
{ "shared/api": "api" }— maps workspace paths to labels. - Function:
(workspacePath) => label | undefined— returns a label or undefined.
10.5.1Alias Rules
- Aliases affect display labels only — not task identifiers or resolution logic.
- An alias must not be empty for non-root workspaces.
- An alias must not duplicate another workspace’s label.
- An alias must not duplicate another workspace’s ID.
- The root workspace label defaults to empty string (so its tasks display without a prefix).
10.6Task Scoping
- Tasks are scoped to the workspace whose config file registered them.
- The same task name may exist in different workspaces.
- When resolving a task reference without a workspace prefix, Nadle looks in the current workspace first.
- If the task is not found in the current workspace, Nadle falls back to the root workspace.
---
11Configuration Loading
Nadle configuration is loaded from config files, merged with CLI options, and resolved to a final set of options.
11.1Supported Formats
Config files may use any of these extensions:
| Extension | Module Format |
|---|---|
.js |
CommonJS or ESM (detected from package.json type field) |
.mjs |
ESM |
.ts |
TypeScript (transpiled at runtime) |
.mts |
TypeScript ESM (transpiled at runtime) |
11.2Default Config File
The default config file name is nadle.config.ts. Nadle searches for config files in this precedence order:
nadle.config.jsnadle.config.tsnadle.config.mjsnadle.config.mts
If multiple exist, the first match wins (JS before TS, TS before MTS).
The --config flag overrides this search and specifies an explicit path.
11.3Runtime Transpilation
Config files are loaded using jiti, which provides:
- ESM support regardless of the project’s module format.
- TypeScript transpilation without a separate build step.
- Interop for default exports.
11.4Loading Flow
Config files are loaded within an AsyncLocalStorage context bound to the active Nadle instance. This enables tasks.register() and configure() to route registrations to the correct instance without requiring explicit parameters.
- CLI parse: yargs parses command-line arguments.
- Config file resolution: find and load the root config file.
- Root config execution: the config file runs within the instance context, calling
tasks.register()and optionallyconfigure(). - Workspace config loading: for each workspace with a config file, set the workspace context and load the file (still within the same instance context).
- Project resolution: resolve project structure, workspaces, and dependencies.
- Task finalization: flush the task registry buffer into the final registry.
- Options merge: combine defaults, file options, and CLI options.
11.5The `configure()` Function
The configure() function may be called from the root config file only. It sets file-level options that are merged between defaults and CLI options.
Accepted options:
| Option | Type | Description |
|---|---|---|
alias |
object or function | Workspace alias configuration (see 07-workspace.md). |
cache |
boolean | Enable or disable caching. |
cacheDir |
string | Custom cache directory path. |
footer |
boolean | Enable or disable the live footer. |
logLevel |
string | Log level ("error", "log", "info", "debug"). |
parallel |
boolean | Enable parallel execution mode. |
minWorkers |
number or string | Minimum worker thread count. |
maxWorkers |
number or string | Maximum worker thread count. |
If configure() is called from a non-root workspace config file, it raises an error.
11.6Option Precedence
Options are merged in this order (later wins):
Built-in defaults < File options (configure()) < CLI flags
11.7Built-in Defaults
| Option | Default |
|---|---|
cache |
true |
footer |
true (but false in CI environments) |
parallel |
false |
logLevel |
"log" |
summary |
false |
cleanCache |
false |
minWorkers |
availableParallelism - 1 |
maxWorkers |
availableParallelism - 1 |
11.8Worker Count Resolution
Worker count values can be:
- An integer: used directly.
- A percentage string (e.g.,
"50%"): multiplied byavailableParallelismand rounded.
The result is always clamped to [1, availableParallelism]. The minWorkers value is additionally capped at maxWorkers.
11.9Supported Log Levels
The following log levels are supported, in increasing verbosity:
"error"— errors only"log"— standard output (default)"info"— informational messages"debug"— debug-level output
---
12CLI Interface
Nadle is invoked from the command line as nadle [tasks...] [options].
12.1Command Structure
nadle [tasks...] [options]
tasks— zero or more task names or task identifiers to execute.- If no tasks are specified and stdin is a TTY, Nadle enters interactive task selection.
12.2Flags
12.2.1Execution Options
| Flag | Alias | Type | Default | Description | | ------------------- | ----- | -------- | ------- | ------------------------------------------------------------------- | | --parallel | | boolean | false | Run all specified tasks in parallel while respecting dependencies. | | --exclude | -x | string[] | | Tasks to exclude from execution. Supports comma-separated values. | | --no-cache | | boolean | false | Disable task caching. All tasks execute and results are not stored. | | --clean-cache | | boolean | false | Delete all files in the cache directory. | | --list | -l | boolean | false | List all available tasks. | | --list-workspaces | | boolean | false | List all available workspaces. | | --dry-run | -m | boolean | false | Show execution plan without running tasks. | | --show-config | | boolean | false | Print the resolved configuration. | | --config-key | | string | | Path to a specific config value (dot/bracket notation). | | --stacktrace | | boolean | false | Print full stacktrace on error. |
12.2.2General Options
| Flag | Alias | Type | Default | Description | | --------------- | ----- | ------- | ------------------------------ | -------------------------------------------------------- | | --config | -c | string | nadle.config.{js,mjs,ts,mts} | Path to config file. | | --cache-dir | | string | <projectDir>/.nadle | Directory to store cache results. | | --log-level | | string | "log" | Logging level. Choices: error, log, info, debug. | | --min-workers | | string | availableParallelism - 1 | Minimum workers (integer or percentage). | | --max-workers | | string | availableParallelism - 1 | Maximum workers (integer or percentage). | | --footer | | boolean | !isCI | Enable the live progress footer during execution. | | --summary | | boolean | false | Print a task execution summary at the end of the run. |
12.2.3Miscellaneous
| Flag | Alias | Description |
|---|---|---|
--help |
-h |
Show help. |
--version |
-v |
Show version number. |
12.3Handler Chain
After options are resolved, Nadle selects a handler using a first-match-wins chain:
| Priority | Handler | Condition |
|---|---|---|
| 1 | List | --list is true |
| 2 | ListWorkspaces | --list-workspaces is true |
| 3 | CleanCache | --clean-cache is true |
| 4 | DryRun | --dry-run is true |
| 5 | ShowConfig | --show-config is true |
| 6 | Execute | Always matches (default handler) |
Each handler is instantiated and its canHandle() method is checked. The first handler that returns true has its handle() method invoked. Only one handler runs per invocation.
12.3.1Handler Interface
All handlers extend a base class with:
name— handler display name for debug logging.description— human-readable description.canHandle()— returnstrueif this handler should run.handle()— performs the handler’s action.
12.4Exit Codes
| Code | Meaning |
|---|---|
0 |
Success (implicit — Nadle does not explicitly exit on success). |
1 |
Unknown error or default NadleError code. |
N |
NadleError with a specific errorCode. |
When an error is caught during execution:
- If the error is a NadleError, exit with its
errorCode. - Otherwise, exit with code
1.
12.5Interactive Task Selection
When no tasks are specified on the command line and stdin is a TTY, Nadle enters an interactive mode where the user can select tasks from a list. This state is tracked internally and affects footer rendering.
---
13Built-in Task Types
Nadle provides four built-in reusable task types, all created via defineTask().
13.1ExecTask
Executes an arbitrary external command.
13.1.1Options
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | The command to execute. |
args |
string or array of strings | Yes | Arguments for the command. If a string, it is parsed into arguments by splitting on spaces. |
13.1.2Behavior
- Parse arguments (string arguments are split into an array).
- Spawn the process with the command and arguments.
- Set working directory to the task’s
workingDir. - Force color output in the subprocess (
FORCE_COLOR=1). - Stream all subprocess output (stdout and stderr combined) to the task logger.
- Await subprocess completion.
13.2PnpmTask
Executes a pnpm command. Specialized variant of ExecTask with pnpm as the command.
13.2.1Options
| Field | Type | Required | Description |
|---|---|---|---|
args |
string or array of strings | Yes | Arguments to pass to pnpm. |
13.2.2Behavior
- Normalize arguments to an array.
- Spawn
pnpmwith the arguments. - Set working directory to the task’s
workingDir. - Force color output (
FORCE_COLOR=1). - Stream combined output to the task logger.
- Await subprocess completion.
13.3CopyTask
Copies files and directories using glob patterns.
13.3.1Options
| Field | Type | Required | Description |
|---|---|---|---|
from |
string | Yes | Source path (relative to working directory). |
to |
string | Yes | Destination path (relative to working directory). |
include |
string or array of strings | No | Glob patterns to include. Default: **/*. |
exclude |
string or array of strings | No | Glob patterns to exclude. Default: none. |
13.3.2Behavior
When `from` is a directory:
- Create the destination directory.
- Glob all files matching
includepatterns, ignoringexcludepatterns. - Copy each matched file, preserving relative directory structure.
When `from` is a file:
- Check if the file matches include/exclude patterns.
- If the destination is a directory (or ends with a path separator), copy into it.
- Otherwise, copy to the exact destination path.
- Create parent directories as needed.
If the source path does not exist, a warning is logged and no error is raised.
13.4DeleteTask
Deletes files and directories using glob patterns.
13.4.1Options
| Field | Type | Required | Description | | -------------- | -------------------------- | -------- | ------------------------------------------------------- | | paths | string or array of strings | Yes | Glob patterns for files/directories to delete. | | (additional) | | No | All options supported by the underlying rimraf library. |
13.4.2Behavior
- Expand glob patterns against the working directory.
- Log the matched paths.
- Delete all matched paths using rimraf (recursive, handles non-empty directories).
13.5Common Properties
All built-in tasks share these characteristics:
- They all respect the
workingDirfrom the runner context. - ExecTask and PnpmTask force color output via
FORCE_COLOR=1environment variable. - All tasks stream output through the task logger.
13.6Custom Task Types
Users create custom reusable task types using defineTask():
defineTask({
run: ({ options, context }) => { ... }
})
The run function receives typed options and a runner context. The returned task object is then registered with tasks.register(name, taskObject, optionsResolver).
---
14Event System
Nadle emits lifecycle events via a listener-based event system.
14.1Listener Interface
A listener is an object with optional methods for each lifecycle event. All event methods are optional — a listener may implement any subset.
14.2Events
Events are listed in typical emission order:
| Event | Parameters | When Emitted |
|---|---|---|
onInitialize |
_(none)_ | After Nadle is initialized, before execution starts. |
onExecutionStart |
_(none)_ | Immediately before the handler chain runs. |
onTasksScheduled |
tasks (list of registered tasks) |
After the scheduler produces the execution plan. |
onTaskStart |
task, threadId |
When a worker begins executing a task function (after “start” message). |
onTaskFinish |
task |
When a task function completes successfully. |
onTaskFailed |
task |
When a task function throws an error. |
onTaskCanceled |
task |
When a worker is terminated while a task is running. |
onTaskUpToDate |
task |
When cache validation determines outputs are current. |
onTaskRestoreFromCache |
task |
When outputs are restored from cache. |
onExecutionFinish |
_(none)_ | After all tasks complete successfully. |
onExecutionFailed |
error |
When any task fails or an unhandled error occurs. |
14.2.1Important Notes
onTaskStartis only emitted for tasks that actually execute. Tasks resolved as up-to-date or from-cache do not receiveonTaskStart.onExecutionFinishandonExecutionFailedare mutually exclusive — exactly one is emitted per run.
14.3Emission Order
Events are emitted sequentially through all registered listeners, in registration order. For each event:
- Iterate through all listeners.
- For each listener that implements the event method, call it and await the result.
- Move to the next listener.
This means listeners are called in order and each listener’s handler completes before the next is invoked.
14.4Built-in Listeners
Nadle registers two built-in listeners in this order:
| Order | Listener | Purpose |
|---|---|---|
| 1 | ExecutionTracker | Aggregates task statistics: counts by status, duration tracking, per-task state. |
| 2 | DefaultReporter | Renders UI output: task start/finish messages, footer, summary. |
The ExecutionTracker runs first so that statistics are up-to-date when the DefaultReporter renders output.
14.5ExecutionTracker Details
The execution tracker maintains:
- Task stats: count of tasks in each status (Scheduled, Running, Finished, UpToDate, FromCache, Failed, Canceled).
- Duration: total execution time, updated every 100ms via an interval timer.
- Per-task state: status, duration, start time, and thread ID for each task.
The duration timer is unreferenced so it does not prevent the process from exiting.
14.6Custom Listeners
Custom listeners are not directly supported through the public API in the current implementation. The event emitter is initialized with a fixed set of listeners (ExecutionTracker and DefaultReporter).
---
15Error Handling
15.1NadleError
NadleError is a specialized error class with a numeric exit code.
| Property | Type | Default | Description |
|---|---|---|---|
message |
string | _(required)_ | Human-readable error message. |
errorCode |
number | 1 |
Process exit code used when this error reaches the top level. |
name |
string | "NadleError" |
Error name for stack traces. |
15.2Error Propagation
Errors flow through the system in this chain:
Task function throws
-> Worker promise rejects
-> Pool catches the error
-> onTaskFailed event emitted
-> Error re-thrown to handler
-> onExecutionFailed event emitted
-> Process exits with error code
15.2.1Step-by-step
- A task function throws an error.
- The worker’s default export promise rejects.
- The pool’s
pushTaskmethod catches the rejection. - If the error is a worker termination (cancellation),
onTaskCanceledis emitted and the error is swallowed. - Otherwise,
onTaskFailedis emitted and the error is re-thrown. - The re-thrown error propagates to the
execute()method. onExecutionFailedis emitted with the error.- The process exits with the appropriate code.
15.3Exit Code Determination
if error is NadleError:
exit with error.errorCode
else:
exit with 1
15.4Known Error Types
| Error | Message Pattern | When Raised |
|---|---|---|
| Cycle detected | "Cycle detected in task {path}. Please resolve the cycle before executing tasks." |
During scheduling, before execution. |
| Duplicate task name | "Task {name} already registered in workspace {id}" |
During task registration. |
| Invalid task name | "Invalid task name: {name}. Task names must contain only letters, numbers, and dashes; start with a letter, and not end with a dash." |
During task registration. |
| Config file not found | "No nadle.config.{...} found in {path} directory or parent directories." |
During config resolution. |
| Task not found | "Task {name} not found in {workspace} workspace." |
During task resolution. |
| Task not found (with suggestions) | "Task {name} not found in {workspace} nor {fallback} workspace. {suggestions}" |
During task resolution with fallback. |
| Invalid worker config | "Invalid value for --{min/max}-workers. Expect to be an integer or a percentage." |
During CLI option parsing. |
| Invalid configure usage | "configure function can only be called from the root workspace." |
When configure() called from non-root workspace. |
| Workspace not found | "Workspace {input} not found. Available workspaces: {list}." |
During workspace resolution. |
| Empty workspace label | "Workspace {id} alias can not be empty." |
During alias validation. |
| Duplicate workspace label | "Workspace {id} has a duplicated label {label} with workspace {other}." |
During alias validation. |
15.5Stacktrace Display
By default, only the error message is shown. When --stacktrace is passed:
- The full error stack trace is printed.
- Without
--stacktrace, a hint is shown suggesting the user re-run with the flag.
---
16Reporting
Nadle provides real-time execution feedback through a footer renderer and an optional end-of-run summary.
16.2Task Status Messages
During execution, each task event produces a status message:
| Event | Output |
|---|---|
| Task started | > Task {label} STARTED |
| Task finished | ✓ Task {label} DONE {duration} |
| Task up-to-date | - Task {label} UP-TO-DATE |
| Task from cache | ↩ Task {label} FROM-CACHE |
| Task failed | ✗ Task {label} FAILED {duration} |
| Task canceled | ✗ Task {label} CANCELED |
16.3Execution Result
16.3.1Successful Run
On successful completion:
RUN SUCCESSFUL in {duration}
{N} tasks executed[, {N} tasks up-to-date][, {N} tasks restored from cache]
Up-to-date and from-cache counts are only shown if greater than zero.
16.3.2Failed Run
On failure:
RUN FAILED in {duration} ({N} tasks executed, {N} tasks failed)
If --stacktrace is not set, a hint is shown:
For more details, re-run the command with the --stacktrace option...
16.4Summary (`—summary`)
When --summary is passed, an end-of-run profiling table is printed showing each finished task with its execution duration. This is rendered after the success message and before the final status line.
Only tasks with status Finished are included in the summary (not up-to-date or from-cache tasks).
16.5Welcome Banner
At execution start (unless --show-config is active), Nadle prints:
🛠️ Welcome to Nadle v{version}!
Using Nadle from {path}
Loaded configuration from {configFile}[ and {N} other(s) files]
16.6Task Resolution Display
If any task names were auto-corrected during resolution (e.g., fuzzy matching), the corrected mappings are displayed:
Resolved tasks:
{original} → {corrected}