Nadle Specification
Version: 4.1.1
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)
Plugins (14)
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. |
| Plugin | A distributable unit applied with use() that contributes task types, lifecycle hooks, and/or custom reporters. |
| Handler | A command handler (List, DryRun, Execute, etc.) selected by the CLI. |
| Runner Context | The logger and working directory provided to every task function. |
| Kernel | Shared zero-dependency package (@nadle/kernel) providing workspace identity, task identifiers, and alias resolution. |
| Project Resolver | Package (@nadle/project-resolver) that discovers projects, scans workspaces, and resolves dependencies. |
---
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. A registration associates a name with an optional task body and a set of configuration fields. There are three registration forms:
| Form | Provides | Description |
|---|---|---|
| No-op | name only | Registers a lifecycle-only task with no function body. Useful as an aggregation point for dependencies. |
| Function | name + a function body | Registers a task with a function that receives a runner context. |
| Typed task | name + a typed task body + an options resolver | Registers a reusable task type with typed options. The resolver provides those options. |
For the typed-task form, the options resolver is optional when the options type has no required fields (an empty object satisfies it); in that case the options default to an empty object ({}). When the options type has at least one required field, both the body and the resolver are mandatory.
A task’s configuration (group, dependsOn, inputs, outputs, env, etc.) is provided as part of the registration alongside the body and options — it is not a separate, later step. See 02-task-configuration.md. Configuration may also be supplied lazily, deferring its resolution until the configuration is first needed (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).
- Empty (lifecycle-only) tasks still transition through Running and emit start/finish events, but the reporter suppresses the STARTED message — only DONE is printed. See 13-reporting.md for details.
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 by providing it as the task body together with an options resolver that supplies the concrete options for this instance.
---
5Task Configuration
Every registered task may carry configuration. Configuration is provided as part of the registration itself (see 01-task.md) — alongside the task body and options — rather than through a separate, later configuration step. The configuration may be supplied directly, or lazily so that its resolution is deferred until first needed.
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.2Supplying Configuration
Configuration is supplied as a set of fields at registration time. The configuration provided at registration is the task’s complete configuration; there is no separate merge step.
A task’s configuration may instead be supplied lazily — deferred rather than determined eagerly at registration. A lazily-supplied configuration is resolved at most once per task: it is not evaluated at registration, only when the configuration is first needed (scheduling, execution, or reporting), and the result is memoized so the deferred resolution never runs more than once for a task in a given invocation (configuration avoidance). A lazy configuration must therefore be pure with respect to that single evaluation; do not rely on a side effect running on every read.
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").
A dependency is resolved only within its target workspace (the current workspace for a colon-less name, or the explicit workspace for a qualified name) — there is no implicit fallback to the root workspace. If the task is not found there, an error is raised with suggestions. To depend on a root task, qualify it explicitly with "root:taskName".
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.
5.7Timeouts and Retries
timeout (milliseconds, positive integer) bounds each execution attempt of the task function; an attempt that does not settle in time fails with a timeout error. retries (non-negative integer, default 0) is the number of additional attempts after the first failure. Together a task runs up to 1 + retries attempts and fails only if all attempts fail. Both apply only to the task function, not to cache restore. See 04-execution.md.
---
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.4Implicit Workspace Dependencies
When implicitDependencies is enabled (the default), Nadle automatically creates task dependency edges based on workspace dependency relationships declared in package.json.
6.4.1Resolution Rules
For each non-root task being analyzed, Nadle examines the workspace’s dependency list (populated from package.json — see 07-workspace.md). For each upstream workspace, if it defines a task with the same name, an implicit dependency edge is added so the upstream task runs first.
- Implicit edges are additive: they combine with any explicit
dependsOndeclarations. - Implicit edges respect
--exclude: if the upstream task is excluded, no edge is created. - Deduplication: if an explicit
dependsOnalready targets the same upstream task, the implicit edge is a no-op (no duplicate edge). - Implicit dependencies are only resolved for tasks within child workspaces. Root workspace tasks are not subject to implicit dependency resolution (they have no upstream workspaces).
- If the upstream workspace has no task with the matching name, the edge is silently skipped.
6.4.2Root Task Aggregation
When workspace task expansion adds child workspace tasks for a root task (see above), and implicitDependencies is enabled, the root task automatically depends on all expanded child workspace tasks. This ensures the root task runs last, after all child workspace instances of the same task have completed.
- Aggregation edges respect
--exclude: excluded child tasks are not added as dependencies. - Aggregation combines with implicit workspace dependencies: child tasks respect their own inter-workspace ordering, and the root task waits for all of them.
- Aggregation is disabled when
implicitDependenciesisfalse.
6.4.3Opt-Out
Set implicitDependencies: false via configure() or CLI flag to disable all implicit dependency behavior, including both workspace dependency edges and root task aggregation.
6.5Execution Modes
6.5.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.5.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.5.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.6Exclusion
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.7Execution 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.5Timeouts and Retries
A task may declare a timeout (milliseconds) and/or a retries count (see 02-task-configuration.md). They apply only to the execution of the task function — never to cache restore, which is not retried or timed.
- Attempt — one invocation of the task function. A task runs up to
1 + retriesattempts (defaultretriesis0, i.e. a single attempt). - Timeout — if
timeoutis set, each attempt is bounded. An attempt that does not settle withintimeoutmilliseconds fails with a timeout error. The task function is not forcibly interrupted (its asynchronous work may continue); the attempt is treated as failed for scheduling and retry purposes. - Retry — when an attempt fails (including by timeout), the task is retried until it succeeds or the attempts are exhausted. The final failure (the last attempt’s error) is the task’s error. A succeeding attempt makes the task succeed regardless of earlier failures.
- Environment injection is applied and restored around the attempts, not around each individual attempt.
timeout must be a positive integer and retries a non-negative integer; otherwise a configuration error is raised.
7.6Environment 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.7Cancellation
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.8Cleanup
The pool is always destroyed in a finally block after execution, whether it succeeds or fails. This ensures all worker threads are terminated.
7.9Task 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, options, dependencyFingerprints}. - 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). |
options |
The resolved task options (if the task uses optionsResolver). |
dependencyFingerprints |
Map of dependency task ID to output fingerprint (if any). |
The hash is SHA-256 with unordered object and array comparison, producing a 64-character hex string.
8.5.1Dependency Fingerprints
When a task depends on other tasks (via dependsOn), the cache key includes the output fingerprints of its direct dependencies. This ensures that a downstream task is re-executed whenever an upstream task produces different outputs, even if the downstream task’s own inputs have not changed.
After a task completes, its output fingerprint is stored by the task pool. When dispatching a downstream task, the pool collects fingerprints from all direct dependencies and passes them as dependencyFingerprints in the worker parameters.
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: node_modules/.cache/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, evict old entries. |
cache-disabled |
No action. |
8.11Cache Eviction
Each task has a maximum number of cache entries (maxCacheEntries, default: 5). After a cache-miss save, entries beyond this limit are evicted:
- List all run directories for the task.
- If the count is within the limit, do nothing.
- Sort runs by timestamp (newest first).
- Delete the oldest runs that exceed the limit, never deleting the current latest.
The maxCacheEntries can be set globally via configure() or per-task in the task configuration. The per-task value takes precedence over the global value.
8.12Corruption Recovery
Cache metadata files are written atomically (write to .tmp, then rename) to minimize corruption risk. If corruption does occur:
- Corrupted JSON (SyntaxError during parse): Treated as missing cache. The task re-executes and overwrites the corrupted entry.
- Failed cache restore (missing or partial output files): Falls back to re-executing the task, then saves fresh outputs.
No explicit cleanup of corrupted entries is performed. The eviction mechanism naturally prunes old entries over time.
8.13File I/O Concurrency
Cache save and restore operations use a concurrency limiter (default: 64 concurrent file operations) to prevent “too many open files” errors when tasks have large output sets.
---
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
package.jsonmarked withnadle.root: true. If found, that directory is the root (and is further inspected for a monorepo layout). - Otherwise, detect a monorepo root via package manager tooling (lock files, workspace config).
- Otherwise, fall back to the closest ancestor directory that contains a
package.json, treated as a single-package project.
If no package.json is found in any ancestor directory, Nadle raises an error.
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), except the project root itself: a workspace pattern that matches the root directory (for example a pattern of .) does not create a second workspace, because the root is already represented by the root workspace. Such a match is ignored rather than treated as an error.
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:
dependenciesdevDependencies
peerDependencies and optionalDependencies are intentionally excluded: they rarely imply a build-ordering relationship and including them risks spurious edges (or cycles) in the task graph.
If a dependency references another workspace in the project (e.g., via workspace:* protocol), it is recorded as a workspace dependency.
When implicitDependencies is enabled (the default), these workspace dependencies are used to automatically create task dependency edges between workspaces. See 03-scheduling.md for details on implicit dependency resolution and root task aggregation.
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.
- When the alias is an object map, every key must match a known workspace path — the root path (
.) or a sub-workspace’s relative path. A key matching no workspace is an error (this catch does not apply to the function form, which is only ever called with known workspace paths). - 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. |
implicitDependencies |
boolean | Enable implicit workspace task dependencies and root aggregation. |
logLevel |
string | Log level ("error", "log", "info", "debug"). |
maxCacheEntries |
number | Maximum cache entries to keep per task (positive integer). |
reporter |
string | Output reporter ("default", "agent"). |
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.5.1Validation
configure() validates its options at config-load time and raises a configuration error (exit code 2) for any malformed value, rather than failing later or silently ignoring it:
cache,footer,parallel,implicitDependenciesmust be booleans.cacheDirmust be a non-empty string.maxCacheEntriesmust be a positive integer.logLevelmust be one of the supported levels;reporterone of the supported reporters.minWorkers/maxWorkersmust be a positive integer or a percentage string (e.g."50%").aliasmust be an object or a function.
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) |
implicitDependencies |
true |
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.1.1Glob Task Selection
A task name (the name segment, after any workspace qualifier) may be a glob pattern — any input containing *, ?, [, ], {, }, or !. Patterns are matched against the registered task names of the resolved workspace:
- An unqualified pattern (e.g.
build*) matches task names in the target workspace; if none match there, the root workspace is tried as a fallback. - A workspace-qualified pattern (e.g.
backend:build*) matches only within that workspace. - A pattern that matches no task is an error (exit code 3) — patterns never silently expand to nothing.
Glob patterns apply equally to the --exclude option. Because task names never contain glob characters, an input is treated as a glob if and only if it contains one.
12.1.2Argument Passthrough
Arguments after the first bare -- are not parsed as Nadle options; they are captured verbatim and passed through to tasks.
nadle <tasks...> [options] -- <args...>
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. | | --watch | -w | boolean | false | Re-run the requested tasks when their declared inputs change. | | --graph | | string | tree | Print the dependency graph instead of executing. tree/mermaid. | | --explain | | string | | Explain a single task (why it runs, dependents, inputs); no run. | | --since | | string | | Run only the requested tasks affected by changes since a git ref. | | --show-config | | boolean | false | Print the resolved configuration. | | --config-key | | string | | Path to a specific config value (dot/bracket notation). | | --json | | boolean | false | Emit machine-readable JSON from read commands instead of human text. | | --doctor | | boolean | false | Diagnose project, config, and cache health; no execution. | | --capabilities | | boolean | false | Emit a machine-readable JSON description of flags, tasks, and config. | | --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>/node_modules/.cache/nadle | Directory to store cache results. | | --log-level | | string | "log" | Logging level. Choices: error, log, info, debug. | | --reporter | | string | "default" | Output reporter: a built-in (default/agent) or a plugin-registered reporter name. | | --min-workers | | string | availableParallelism - 1 | Minimum workers (integer or percentage). | | --max-workers | | string | availableParallelism - 1 | Maximum workers (integer or percentage). | | --footer | | boolean | !isCI && isTTY | Enable the live progress footer during execution. | | --summary | | boolean | false | Print profiling insights at the end: slow-task table, critical path, cache-miss hotspots. | | --why | | boolean | false | Explain each task’s cache outcome (hit/miss and changes). |
12.2.3Miscellaneous
| Flag | Alias | Description |
|---|---|---|
--help |
-h |
Show help. |
--version |
-v |
Show version number. |
12.3Shell Completion
The completion command prints a shell completion script to standard output for the detected shell (bash, zsh, or fish). The user installs it by sourcing the output (e.g. nadle completion >> ~/.zshrc).
Once installed, pressing TAB completes:
- task names — the labels of all tasks registered by the live configuration (discovered by loading the config, exactly as
--listdoes), and - option flags — the known CLI flags.
When the active shell can render a description alongside each candidate (such shells accept a value:description pairing), task completions carry the task’s description so the menu shows the same context as --list. Tasks without a description, and shells that cannot display descriptions, complete to the bare task name. The description is the only annotation; no other metadata is attached.
Completion discovers task names dynamically from the current project, so it always reflects the tasks actually defined. The completion command and the completion callback produce no other output (no banner, footer, or logs).
12.4JSON Output
The --json flag switches the read-only inspection commands from human-oriented text to a single machine-readable JSON document on standard output. It is intended for tooling and automation that need to parse Nadle’s introspection output reliably.
When --json is set:
- The selected read command prints exactly one JSON document and nothing else: no banner, no progress footer, no colors, and no trailing run summary.
- The live progress footer is forced off regardless of its own default.
--json applies to these commands; any other command ignores it:
| Command | JSON document |
|---|---|
--list |
An array of task objects, each with name, label, group, description, dependsOn, inputs, outputs, and workspace. |
--list-workspaces |
An array of workspace objects, each with id, label, and parent (the id of the nearest enclosing workspace, or null for the root). |
--dry-run |
An object with the ordered execution plan; each entry has the task id, label, its implicit-dependency ids, and the passthrough arguments it would receive. |
--graph |
An object describing the dependency graph: the requested roots and a nodes array, each node with id, label, explicit dependencies, and implicitDependencies. The tree/mermaid format choice is ignored. |
--explain |
An object describing one task: its label, whether it was requestedDirectly, the pullPaths that transitively request it, its dependents, declared inputs, and whether caching is enabled. |
--show-config and --config-key already emit JSON and are unaffected by --json.
A task’s dependsOn, inputs, and outputs reflect its declared configuration. inputs and outputs are rendered as <type>: <pattern> entries (one per declared pattern).
12.5Handler 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 | Graph | --graph is set |
| 5 | Explain | --explain is set |
| 6 | DryRun | --dry-run is true |
| 7 | ShowConfig | --show-config is true |
| 8 | Doctor | --doctor is true |
| 9 | Capabilities | --capabilities is true |
| 10 | Watch | --watch is true |
| 11 | 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.
The Execute handler additionally honors --since <ref>: before scheduling, it filters the requested (expanded) task set to those affected by files changed since the git ref. A task is affected when a changed file lies within its workspace directory; the dependencies of an affected task are included so its inputs are produced. If no task is affected, Execute reports it and runs nothing. Cross-workspace dependent propagation is out of scope for this version.
12.5.1Doctor
The Doctor handler (--doctor) runs a set of read-only diagnostic checks and prints each as a status line, then a summary. It performs no execution and mutates nothing. Each check yields one of: ok, warning, or error. The process exits non-zero if any check is an error (zero if only warnings or all ok).
The checks are:
| Check | Warning / error condition |
|---|---|
| Project | Reports the detected package manager and workspace count (informational, always ok). |
| Cache directory | Warns if the cache directory exists but is not writable. |
| Partial cacheability | Warns for each task that declares inputs without outputs or vice versa (never cached). |
| Stale outputs | Warns for each cacheable task whose declared outputs are entirely missing on disk. |
The set of checks may grow over time; the contract is that Doctor is read-only and that an error-level finding makes the exit code non-zero.
12.5.2Capabilities
The Capabilities handler (--capabilities) prints a single machine-readable JSON document describing what this version of Nadle can do, then exits without executing anything. It is intended for tools and agents that need to discover Nadle’s surface programmatically instead of parsing help text or loading the configuration themselves.
The document is the only output (no banner, footer, or logs) and has the shape:
version— the Nadle version that produced the document.flags— the full list of recognized CLI flags, each with itsname,type,description, optionaldefault, optionalchoices, andaliases. This list is derived from the same definitions that drive option parsing, so it can never drift from the flags Nadle actually accepts. Internal/hidden flags are omitted.tasks— the tasks discovered from the live configuration (exactly the set--listwould show), each with its identifier, name, label, workspace, and optionalgroupanddescription.config— a JSON Schema describing the task configuration object accepted in a configuration file (the fields a task may declare, e.g.dependsOn,env,workingDir,inputs,outputs, and caching/retry controls).
Because task discovery loads the configuration, configuration errors surface here as they would for any other handler; when the configuration loads, the handler always succeeds.
12.5.3Handler 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.6Exit 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.
In a machine-readable error mode (see Error Handling — Structured Error Output), the same failure also emits a one-line structured error record to the error stream before exiting.
12.7Interactive 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 thirteen built-in reusable task types, all created via defineTask().
13.1Argument Normalization
All exec-based tasks (ExecTask, NodeTask, NpmTask, NpxTask, PnpmTask, PnpxTask) share one semantic for the args option: a string is split into arguments on spaces, with backslash-escaped spaces preserved (a\ b stays one argument); an array is taken as-is, each element one argument.
13.2ExecTask
Executes an arbitrary external command.
13.2.1Options
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | The command to execute. |
args |
string or array of strings | No | Arguments for the command (see Argument Normalization). |
13.2.2Behavior
- Normalize arguments (see Argument Normalization).
- 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.3PnpmTask
Executes a pnpm command. Specialized variant of ExecTask with pnpm as the command.
13.3.1Options
| Field | Type | Required | Description |
|---|---|---|---|
args |
string or array of strings | Yes | Arguments to pass to pnpm. |
filter |
string or array of strings | No | Workspace package(s) to scope the command to, as --filter flag(s). |
13.3.2Behavior
- Normalize
filterto an array and expand each value into a--filter <value>pair. - Normalize
args(see Argument Normalization) and append it after the filter flags. - Spawn
pnpmwith the combined 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.4NodeTask
Executes a Node.js script. Specialized variant of ExecTask with node as the command.
13.4.1Options
| Field | Type | Required | Description |
|---|---|---|---|
script |
string | Yes | The script to execute via node. |
args |
string or array of strings | No | Arguments for the script. |
13.4.2Behavior
- Normalize arguments (see Argument Normalization).
- Spawn
node <script> <args>. - 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.5NpmTask
Executes an npm command. Specialized variant of ExecTask with npm as the command.
13.5.1Options
| Field | Type | Required | Description |
|---|---|---|---|
args |
string or array of strings | Yes | Arguments to pass to npm. |
13.5.2Behavior
- Normalize arguments (see Argument Normalization).
- Spawn
npmwith 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.6PnpxTask
Executes a locally-installed package binary via pnpm exec. Specialized variant of ExecTask for running binaries from node_modules/.bin through pnpm.
13.6.1Options
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | The command to execute via pnpm exec. |
args |
string or array of strings | No | Arguments for the command. |
13.6.2Behavior
- Normalize arguments (see Argument Normalization).
- Spawn
pnpm exec <command> <args>. - 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.7NpxTask
Executes a locally-installed package binary via npx. Specialized variant of ExecTask for running binaries from node_modules/.bin through npx.
13.7.1Options
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | The command to execute via npx. |
args |
string or array of strings | No | Arguments for the command. |
13.7.2Behavior
- Normalize arguments (see Argument Normalization).
- Spawn
npx <command> <args>. - 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.8File Selections
File-operation tasks share one source vocabulary. A file selection is either:
- a string — a path (relative to the working directory) to a file or a directory, or
- a selector object —
{ dir, include?, exclude? }, whereinclude/excludeare glob patterns matched against files insidedir(include defaults to all files).
A string selection pointing to a file yields that file. One pointing to a directory selects the files inside it (applying the task-level default include/exclude patterns, if any). A missing source logs a warning and yields nothing — unless the task’s strict option is set, in which case it is an error.
13.9CopyTask
Copies files into a destination directory.
13.9.1Options
| Field | Type | Required | Description |
|---|---|---|---|
from |
file selection or array thereof | Yes | Source files, directories, or selectors. |
into |
string | Yes | Destination directory (relative to working directory). Created if missing. |
include |
string or array of strings | No | Default include patterns for directory selections without their own. |
exclude |
string or array of strings | No | Default exclude patterns for directory selections without their own. |
flatten |
boolean | No | Copy all files directly into into, dropping source directory structure. |
rename |
record of string to string | No | Renames by exact base name, e.g. { "config.dev.json": "config.json" }. |
overwrite |
replace \ |
skip \ |
error |
strict |
boolean | No | Fail when a source is missing or nothing matches. Default: false. |
13.9.2Behavior
- Resolve all
fromselections to files (see File Selections). - Compute each file’s destination: its selection-relative path under
into, flattened to the base name whenflattenis set, then renamed when its base name appears inrename. - If two source files map to the same destination, the task fails.
- Apply the
overwritepolicy per existing destination file:replaceoverwrites,skiplogs and skips,errorfails the task. - Create parent directories as needed and copy.
13.10MoveTask
Moves files into a destination directory. Identical options and destination-mapping behavior to CopyTask (including flatten, rename, overwrite, strict), with one difference: each source file is removed after it reaches its destination.
- A filesystem rename is used when possible; cross-device moves fall back to copy-then-delete.
- Files skipped by the
overwritepolicy keep their source. - Emptied source directories are not removed.
13.11SyncTask
Mirrors sources into a destination directory. Identical selection and destination-mapping behavior to CopyTask (including flatten, rename, strict), without an overwrite option — existing destination files are always replaced.
13.11.1Additional option
| Field | Type | Required | Description |
|---|---|---|---|
preserve |
string or array of strings | No | Glob patterns (relative to into) for files never deleted. |
13.11.2Behavior
- Resolve and copy as CopyTask (always replacing).
- Delete every file under
intothat does not correspond to a source and does not match apreservepattern. - Prune directories left empty.
The destination ends up containing exactly the selected files (plus preserved ones).
13.12ZipTask
Creates a zip archive from selected files.
13.12.1Options
| Field | Type | Required | Description |
|---|---|---|---|
from |
file selection or array thereof | Yes | Source files, directories, or selectors (see File Selections). |
archive |
string | Yes | Path of the archive to create (relative to working directory). |
prefix |
string | No | Entry-name prefix; files are stored as <prefix>/<relative path>. |
include |
string or array of strings | No | Default include patterns for directory selections without their own. |
exclude |
string or array of strings | No | Default exclude patterns for directory selections without their own. |
strict |
boolean | No | Fail when a source is missing or nothing matches. Default: false. |
Entry names are the selection-relative paths (always with forward slashes). Two sources mapping to the same entry name fail the task. Parent directories of the archive are created as needed.
13.13UnzipTask
Extracts a zip archive into a directory.
13.13.1Options
| Field | Type | Required | Description |
|---|---|---|---|
archive |
string | Yes | Path of the archive to extract (relative to working directory). |
into |
string | Yes | Destination directory. Created if missing. |
include |
string or array of strings | No | Glob patterns selecting which entries to extract. Default: all. |
A missing archive is an error. Entries whose names would escape the destination directory (path traversal) fail the task.
13.14DownloadTask
Downloads a file over HTTP(S).
13.14.1Options
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | The URL to download. |
into |
string | Yes | Destination directory. Created if missing. |
filename |
string | No | Destination file name. Default: last segment of the URL path. |
sha256 |
string | No | Expected SHA-256 hex digest; the task fails on mismatch. |
13.14.2Behavior
- A non-success HTTP status fails the task.
- When
sha256is given and the destination file already exists with a matching digest, the download is skipped. - A digest mismatch after download fails the task and removes the file.
13.15DeleteTask
Deletes files and directories using glob patterns.
13.15.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.15.2Behavior
- Expand glob patterns against the working directory.
- Log the matched paths.
- Delete all matched paths using rimraf (recursive, handles non-empty directories).
13.16Common Properties
All built-in tasks share these characteristics:
- They all respect the
workingDirfrom the runner context. - ExecTask, NodeTask, NpmTask, NpxTask, PnpmTask, and PnpxTask force color output via
FORCE_COLOR=1environment variable. - ExecTask, NodeTask, NpmTask, NpxTask, PnpmTask, and PnpxTask append the runner context’s passthrough arguments (CLI args after
--, see spec 09) after their configured arguments. CopyTask and DeleteTask ignore passthrough arguments. - All tasks stream output through the task logger.
13.17Custom 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 by providing it as the task body together with an options resolver (see 01-task.md).
---
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
The core registers a fixed set of listeners (ExecutionTracker and the active reporter). User-facing extension is through the plugin system (specified in full in 14-plugins.md): a plugin applied with use() contributes lifecycle hooks that the core dispatches on the main thread via an internal listener. The hooks map to events as follows:
| Plugin hook | Event(s) |
|---|---|
beforeAll |
onExecutionStart (may throw to abort the run) |
afterAll |
onExecutionFinish / onExecutionFailed (errors downgraded to a warning) |
beforeTask |
onTaskStart (fires only for tasks that actually execute, not cache hits) |
afterTask |
onTaskFinish / onTaskFailed / onTaskUpToDate / onTaskRestoreFromCache / onTaskCanceled (errors downgraded to a warning) |
Hooks run in plugin order, grouped by the optional enforce (pre → normal → post). Because beforeTask is skipped for cache hits while afterTask always fires, the two are not a guaranteed pair. Plugins may also contribute task types and reporters.
---
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.2NadleError Subclasses
NadleError has a hierarchy of subclasses so consumers can catch specific error categories programmatically. Each subclass fixes a distinct errorCode.
| Subclass | errorCode |
Raised when |
|---|---|---|
ConfigurationError |
2 |
Config is missing or invalid: config file not found, invalid options or task inputs, invalid task name, duplicate task name, invalid configure() usage, invalid worker config. |
TaskNotFoundError |
3 |
A requested task or workspace cannot be resolved. |
CyclicDependencyError |
4 |
The task graph contains a cycle. |
TaskExecutionError |
1 |
A task throws during execution. Wraps the original error as cause; keeps exit code 1 to preserve the baseline failure contract. |
Invariant violations (states that should be impossible — unset working directory, project not yet configured, exhaustiveness fallbacks) remain plain Error, not NadleError subclasses.
15.3Error 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.3.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. A NadleError is re-thrown as-is; any other error is wrapped in aTaskExecutionError(with the original ascause) before being re-thrown. - The re-thrown error propagates to the
execute()method. onExecutionFailedis emitted with the error.- The process exits with the appropriate code.
15.4Exit Code Determination
if error is NadleError:
exit with error.errorCode
else:
exit with 1
15.5Known 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 (no root 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. |
15.6Structured Error Output
By default, a failure prints a human-readable message (and an optional repro hint). In a machine-readable error mode — active whenever the selected reporter is the agent reporter, and intended to extend to any future explicit machine-output flag — a failure additionally emits a single structured error record to the error stream.
The record is a one-line, machine-parseable object with these fields:
| Field | Type | Presence | Description |
|---|---|---|---|
errorCode |
number | always | The numeric exit code for the failure (see Exit Code, above). |
errorType |
string | always | The error category name (e.g. the NadleError subclass name). |
message |
string | always | The human-readable error message. |
task |
string | only for task-execution errors | The label of the task that failed. Omitted for all other errors. |
15.7Stacktrace 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.1Reporters
The output style is selected by the reporter option (--reporter):
| Reporter | Audience | Behavior |
|---|---|---|
default |
Humans (default) | Welcome banner, colored task status messages, optional live footer, optional summary table. |
agent |
AI agents/scripts | Compact, plain (no color/banner/footer/spinner): one stable line per task plus a summary line. |
The reporter name space is open: in addition to the two built-ins, a plugin may contribute a named reporter (a listener factory), selected by the same --reporter <name> option. Exactly one reporter is active per run (a plugin reporter replaces the default). A plugin reporter may not shadow a built-in name, and an unknown reporter name is a configuration error listing the available reporters. See 14-plugins.md for how reporters are contributed.
The remaining sections of this document describe the default reporter. The agent reporter is specified below.
16.1.1Agent Reporter
Selected with --reporter=agent. Emits one plain line per task outcome and a single summary line. No colors, welcome banner, footer, STARTED lines, or profiling table.
| Event | Output |
|---|---|
| Task finished | DONE {label} {duration} |
| Task up-to-date | UP-TO-DATE {label} |
| Task from cache | FROM-CACHE {label} |
| Task failed | FAILED {label} {duration} |
| Task canceled | CANCELED {label} |
Summary line:
SUCCESS in {duration} (done {N}[ up-to-date {N}][ cached {N}][ failed {N}])
FAILED in {duration} (done {N}[ ... ] failed {N})
With --stacktrace, the full error stack is printed after a failed run. The process exit code is unchanged from the default reporter.
16.3Task 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.3.1Empty (Lifecycle-Only) Tasks
Tasks registered with no function body (lifecycle-only tasks) only produce the DONE message. The STARTED message is suppressed because these tasks perform no work — they exist solely as dependency aggregation points. This matches the single-message pattern used by UP-TO-DATE and FROM-CACHE tasks.
16.4Execution Result
16.4.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.4.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.5Summary (`—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.6Welcome 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.7Task Resolution Display
If any task names were auto-corrected during resolution (e.g., fuzzy matching), the corrected mappings are displayed:
Resolved tasks:
{original} → {corrected}