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:

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 instance
  • context — 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).

Rules
Muststartwithaletter.
Maycontainletters,digits,andhyphens.
Mustnotendwithahyphen.
Mustnotbeempty.
Mustnotstartwithadigit.

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:

  1. No colon (e.g., "build") — resolved within the current workspace.
  2. 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.
  3. 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):

  1. Resolve dependsOn from the task’s configuration.
  2. Filter out excluded tasks.
  3. Record edges in the dependency and dependents graphs.
  4. Recursively analyze each dependency.
  5. 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.

  1. The first specified task becomes the main task.
  2. Only tasks that are the main task or within its transitive dependency tree are eligible for scheduling.
  3. Within the main task’s tree, all zero-indegree tasks run concurrently (dependencies within a chain step still parallelize).
  4. When the main task completes, the scheduler advances to the next specified task.
  5. If the next main task’s dependencies are already satisfied, it may start immediately.

6.4.3Ready Task Computation (Kahn’s Algorithm)

  1. Initial: all tasks with indegree zero in the eligible set are “ready.”
  2. 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.
  3. 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

  1. 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.
  2. Look up the task by ID in the registry.
  3. Resolve the task’s configuration and options.
  4. Resolve the working directory (relative to project root).
  5. Run cache validation (see 05-caching.md).
  6. 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.

7.5Environment Injection

Before executing the task function:

  1. The original process environment is merged with the task’s env field.
  2. 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:

  1. The pool is destroyed, which terminates all worker threads.
  2. A terminated worker throws a “Terminating worker thread” error.
  3. The pool detects this error and checks if the task’s status is Running.
  4. 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

  1. Check if task is cacheable (inputs AND outputs defined). If not, return not-cacheable.
  2. Check if caching is enabled (cache flag). If not, return cache-disabled.
  3. Compute input fingerprints from config files and declared input patterns.
  4. Compute cache key from {taskId, inputsFingerprints, env}.
  5. Check if a cache entry exists for this key.
  6. If no cache entry exists, return cache-miss with reasons.
  7. Read the latest run metadata.
  8. Compute current output fingerprints.
  9. If the latest run’s cache key matches AND output fingerprints match, return up-to-date. 10. Otherwise, return restore-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:

  1. Compute fingerprints for all output files.
  2. For each output file, copy it from the project to the cache’s outputs/ directory, preserving relative paths.
  3. Write run metadata.
  4. Update the task’s latest pointer.

8.9.2Restoring

On restore-from-cache:

  1. Read all files from the cached outputs/ directory.
  2. Copy each file back to its original location in the project, creating directories as needed.
  3. 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:

  1. Look for a nadle.config.{js,mjs,ts,mts} file.
  2. Detect a monorepo root via package manager tooling (lock files, workspace config).
  3. Check for nadle.root: true in package.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: workspaces field in root package.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

  1. Find the project root (config file or monorepo root).
  2. Detect the package manager.
  3. Discover all workspaces.
  4. Create the root workspace with its config file path.
  5. Create child workspaces with their package metadata.
  6. Resolve workspace dependencies from package.json.
  7. Apply alias configuration (if any).
  8. 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:

  • dependencies
  • devDependencies
  • peerDependencies
  • optionalDependencies

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:

  1. nadle.config.js
  2. nadle.config.ts
  3. nadle.config.mjs
  4. nadle.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.

  1. CLI parse: yargs parses command-line arguments.
  2. Config file resolution: find and load the root config file.
  3. Root config execution: the config file runs within the instance context, calling tasks.register() and optionally configure().
  4. Workspace config loading: for each workspace with a config file, set the workspace context and load the file (still within the same instance context).
  5. Project resolution: resolve project structure, workspaces, and dependencies.
  6. Task finalization: flush the task registry buffer into the final registry.
  7. 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 by availableParallelism and 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:

  1. "error" — errors only
  2. "log" — standard output (default)
  3. "info" — informational messages
  4. "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() — returns true if 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

  1. Parse arguments (string arguments are split into an array).
  2. Spawn the process with the command and arguments.
  3. Set working directory to the task’s workingDir.
  4. Force color output in the subprocess (FORCE_COLOR=1).
  5. Stream all subprocess output (stdout and stderr combined) to the task logger.
  6. 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

  1. Normalize arguments to an array.
  2. Spawn pnpm with the arguments.
  3. Set working directory to the task’s workingDir.
  4. Force color output (FORCE_COLOR=1).
  5. Stream combined output to the task logger.
  6. 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:
  1. Create the destination directory.
  2. Glob all files matching include patterns, ignoring exclude patterns.
  3. Copy each matched file, preserving relative directory structure.
When `from` is a file:
  1. Check if the file matches include/exclude patterns.
  2. If the destination is a directory (or ends with a path separator), copy into it.
  3. Otherwise, copy to the exact destination path.
  4. 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

  1. Expand glob patterns against the working directory.
  2. Log the matched paths.
  3. 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 workingDir from the runner context.
  • ExecTask and PnpmTask force color output via FORCE_COLOR=1 environment 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

  • onTaskStart is only emitted for tasks that actually execute. Tasks resolved as up-to-date or from-cache do not receive onTaskStart.
  • onExecutionFinish and onExecutionFailed are 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:

  1. Iterate through all listeners.
  2. For each listener that implements the event method, call it and await the result.
  3. 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

  1. A task function throws an error.
  2. The worker’s default export promise rejects.
  3. The pool’s pushTask method catches the rejection.
  4. If the error is a worker termination (cancellation), onTaskCanceled is emitted and the error is swallowed.
  5. Otherwise, onTaskFailed is emitted and the error is re-thrown.
  6. The re-thrown error propagates to the execute() method.
  7. onExecutionFailed is emitted with the error.
  8. 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}

§Index

  1. Rules
  1. 1Purpose
  2. 2Concept Dependency Map
  3. 3Glossary
  4. 4Task Model
    1. 4.1Registration
      1. 4.1.1Task Function Signature
      2. 4.1.2Runner Context
    2. 4.2Naming Rules
    3. 4.3Duplicate Detection
    4. 4.4Task Identity
    5. 4.5Status Lifecycle
      1. 4.5.1Transition Rules
    6. 4.6Reusable Task Types
  5. 5Task Configuration
    1. 5.1Configuration Fields
    2. 5.2Builder Pattern
    3. 5.3dependsOn Resolution
    4. 5.4Declarations DSL
      1. 5.4.1Pattern Resolution
    5. 5.5Environment Variables
    6. 5.6Working Directory
  6. 6Scheduling
    1. 6.1DAG Construction
      1. 6.1.1Analysis Phase
    2. 6.2Cycle Detection
    3. 6.3Workspace Task Expansion
    4. 6.4Execution Modes
      1. 6.4.1Parallel Mode (`—parallel`)
      2. 6.4.2Sequential Mode (default)
      3. 6.4.3Ready Task Computation (Kahn’s Algorithm)
    5. 6.5Exclusion
    6. 6.6Execution Plan
  7. 7Execution
    1. 7.1Worker Pool
    2. 7.2Worker Parameters
    3. 7.3Message Protocol
      1. 7.3.1Completion Detection
    4. 7.4Worker Execution Flow
    5. 7.5Environment Injection
    6. 7.6Cancellation
    7. 7.7Cleanup
    8. 7.8Task Chaining
  8. 8Caching
    1. 8.1Precondition
    2. 8.2Validation Outcomes
    3. 8.3Validation Flow
    4. 8.4Input Fingerprinting
      1. 8.4.1Implicit Inputs
      2. 8.4.2Declared Inputs
    5. 8.5Cache Key Computation
    6. 8.6Up-to-date vs Restore-from-cache
    7. 8.7Cache Miss Reasons
    8. 8.8Storage Layout
      1. 8.8.1Task ID Encoding
      2. 8.8.2Metadata Structures
    9. 8.9Output Snapshot
      1. 8.9.1Saving
      2. 8.9.2Restoring
    10. 8.10Cache Update Flow
  9. 9Project Model
    1. 9.1Project Structure
    2. 9.2Root Detection
    3. 9.3Package Manager Detection
    4. 9.4Workspace Discovery
    5. 9.5Project Resolution Flow
    6. 9.6Current Workspace
  10. 10Workspace Model
    1. 10.1Workspace Fields
    2. 10.2Identity
    3. 10.3Config Files
    4. 10.4Workspace Dependencies
    5. 10.5Aliases
      1. 10.5.1Alias Rules
    6. 10.6Task Scoping
  11. 11Configuration Loading
    1. 11.1Supported Formats
    2. 11.2Default Config File
    3. 11.3Runtime Transpilation
    4. 11.4Loading Flow
    5. 11.5The `configure()` Function
    6. 11.6Option Precedence
    7. 11.7Built-in Defaults
    8. 11.8Worker Count Resolution
    9. 11.9Supported Log Levels
  12. 12CLI Interface
    1. 12.1Command Structure
    2. 12.2Flags
      1. 12.2.1Execution Options
      2. 12.2.2General Options
      3. 12.2.3Miscellaneous
    3. 12.3Handler Chain
      1. 12.3.1Handler Interface
    4. 12.4Exit Codes
    5. 12.5Interactive Task Selection
  13. 13Built-in Task Types
    1. 13.1ExecTask
      1. 13.1.1Options
      2. 13.1.2Behavior
    2. 13.2PnpmTask
      1. 13.2.1Options
      2. 13.2.2Behavior
    3. 13.3CopyTask
      1. 13.3.1Options
      2. 13.3.2Behavior
    4. 13.4DeleteTask
      1. 13.4.1Options
      2. 13.4.2Behavior
    5. 13.5Common Properties
    6. 13.6Custom Task Types
  14. 14Event System
    1. 14.1Listener Interface
    2. 14.2Events
      1. 14.2.1Important Notes
    3. 14.3Emission Order
    4. 14.4Built-in Listeners
    5. 14.5ExecutionTracker Details
    6. 14.6Custom Listeners
  15. 15Error Handling
    1. 15.1NadleError
    2. 15.2Error Propagation
      1. 15.2.1Step-by-step
    3. 15.3Exit Code Determination
    4. 15.4Known Error Types
    5. 15.5Stacktrace Display
  16. 16Reporting
    1. 16.1Footer
      1. 16.1.1Content
      2. 16.1.2Update Frequency
      3. 16.1.3Rendering
      4. 16.1.4When Disabled
    2. 16.2Task Status Messages
    3. 16.3Execution Result
      1. 16.3.1Successful Run
      2. 16.3.2Failed Run
    4. 16.4Summary (`—summary`)
    5. 16.5Welcome Banner
    6. 16.6Task Resolution Display
  17. §Index