Nadle Specification

Version: 4.1.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)
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 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).
  • 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:

  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").

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):

  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.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 dependsOn declarations.
  • Implicit edges respect --exclude: if the upstream task is excluded, no edge is created.
  • Deduplication: if an explicit dependsOn already 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 implicitDependencies is false.

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.

  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.5.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.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

  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.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 + retries attempts (default retries is 0, i.e. a single attempt).
  • Timeout — if timeout is set, each attempt is bounded. An attempt that does not settle within timeout milliseconds 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:

  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.7Cancellation

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.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

  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, options, dependencyFingerprints}.
  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).
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:

  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, 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:

  1. List all run directories for the task.
  2. If the count is within the limit, do nothing.
  3. Sort runs by timestamp (newest first).
  4. 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:

  1. Look for a package.json marked with nadle.root: true. If found, that directory is the root (and is further inspected for a monorepo layout).
  2. Otherwise, detect a monorepo root via package manager tooling (lock files, workspace config).
  3. 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: 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 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:

  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.
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, implicitDependencies must be booleans.
  • cacheDir must be a non-empty string.
  • maxCacheEntries must be a positive integer.
  • logLevel must be one of the supported levels; reporter one of the supported reporters.
  • minWorkers / maxWorkers must be a positive integer or a percentage string (e.g. "50%").
  • alias must 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 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.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...>
Rules
Passthroughargumentsaredeliveredonlytotasksexplicitlyrequestedonthecommandline(includingtasksmatchedbyaglobpattern).Dependencytasksneverreceivethem.
Everyrequestedtaskreceivesthesamearguments.Whenmorethanonerequestedtaskwillreceivearguments,aninformationalnoticeislogged.
Taskrunnersaccesstheargumentsviatherunnercontext(passthroughArgs).Exec-basedbuiltintasksappendthemtotheirunderlyingcommand.
Passthroughargumentsparticipateinthecachekeyofrequestedtasks;aninvocationwithargumentsisneverservedfromacacheentryproducedwithoutthem.Dependencytaskcachekeysareunaffected.
Strictoptionparsingstillappliesbefore--:unknownNadleflagsremainanerror.
Dryrunannotatesrequestedtaskswiththeargumentstheywouldreceive.

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 --list does), 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 its name, type, description, optional default, optional choices, and aliases. 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 --list would show), each with its identifier, name, label, workspace, and optional group and description.
  • 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() — returns true if 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

  1. Normalize arguments (see Argument Normalization).
  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.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

  1. Normalize filter to an array and expand each value into a --filter <value> pair.
  2. Normalize args (see Argument Normalization) and append it after the filter flags.
  3. Spawn pnpm with the combined arguments.
  4. Set working directory to the task’s workingDir.
  5. Force color output (FORCE_COLOR=1).
  6. Stream combined output to the task logger.
  7. 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

  1. Normalize arguments (see Argument Normalization).
  2. Spawn node <script> <args>.
  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.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

  1. Normalize arguments (see Argument Normalization).
  2. Spawn npm 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.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

  1. Normalize arguments (see Argument Normalization).
  2. Spawn pnpm exec <command> <args>.
  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.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

  1. Normalize arguments (see Argument Normalization).
  2. Spawn npx <command> <args>.
  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.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? }, where include/exclude are glob patterns matched against files inside dir (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

  1. Resolve all from selections to files (see File Selections).
  2. Compute each file’s destination: its selection-relative path under into, flattened to the base name when flatten is set, then renamed when its base name appears in rename.
  3. If two source files map to the same destination, the task fails.
  4. Apply the overwrite policy per existing destination file: replace overwrites, skip logs and skips, error fails the task.
  5. 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 overwrite policy 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

  1. Resolve and copy as CopyTask (always replacing).
  2. Delete every file under into that does not correspond to a source and does not match a preserve pattern.
  3. 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 sha256 is 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

  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.16Common Properties

All built-in tasks share these characteristics:

  • They all respect the workingDir from the runner context.
  • ExecTask, NodeTask, NpmTask, NpxTask, PnpmTask, and PnpxTask force color output via FORCE_COLOR=1 environment 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

  • 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

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

  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. A NadleError is re-thrown as-is; any other error is wrapped in a TaskExecutionError (with the original as cause) before being 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.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.
Rules
Exactlyonestructuredrecordisemittedperfailure,ontheerrorstream,independentofanyhuman-readableoutputalreadyproduced.
Anon-NadleErrorfailurereportserrorCode1andanerrorTypereflectingthegenericerrorcategory.
Onlyatask-executionfailurecarriestask;everyothererroromitsit.
Thedefault(human)errorpathisunchanged:whenthemodeisinactive,nostructuredrecordisemitted.

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}

§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.2Supplying Configuration
    3. 5.3dependsOn Resolution
    4. 5.4Declarations DSL
      1. 5.4.1Pattern Resolution
    5. 5.5Environment Variables
    6. 5.6Working Directory
    7. 5.7Timeouts and Retries
  6. 6Scheduling
    1. 6.1DAG Construction
      1. 6.1.1Analysis Phase
    2. 6.2Cycle Detection
    3. 6.3Workspace Task Expansion
    4. 6.4Implicit Workspace Dependencies
      1. 6.4.1Resolution Rules
      2. 6.4.2Root Task Aggregation
      3. 6.4.3Opt-Out
    5. 6.5Execution Modes
      1. 6.5.1Parallel Mode (`—parallel`)
      2. 6.5.2Sequential Mode (default)
      3. 6.5.3Ready Task Computation (Kahn’s Algorithm)
    6. 6.6Exclusion
    7. 6.7Execution 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.5Timeouts and Retries
    6. 7.6Environment Injection
    7. 7.7Cancellation
    8. 7.8Cleanup
    9. 7.9Task 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
      1. 8.5.1Dependency Fingerprints
    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
    11. 8.11Cache Eviction
    12. 8.12Corruption Recovery
    13. 8.13File I/O Concurrency
  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
      1. 11.5.1Validation
    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
      1. 12.1.1Glob Task Selection
      2. 12.1.2Argument Passthrough
    2. 12.2Flags
      1. 12.2.1Execution Options
      2. 12.2.2General Options
      3. 12.2.3Miscellaneous
    3. 12.3Shell Completion
    4. 12.4JSON Output
    5. 12.5Handler Chain
      1. 12.5.1Doctor
      2. 12.5.2Capabilities
      3. 12.5.3Handler Interface
    6. 12.6Exit Codes
    7. 12.7Interactive Task Selection
  13. 13Built-in Task Types
    1. 13.1Argument Normalization
    2. 13.2ExecTask
      1. 13.2.1Options
      2. 13.2.2Behavior
    3. 13.3PnpmTask
      1. 13.3.1Options
      2. 13.3.2Behavior
    4. 13.4NodeTask
      1. 13.4.1Options
      2. 13.4.2Behavior
    5. 13.5NpmTask
      1. 13.5.1Options
      2. 13.5.2Behavior
    6. 13.6PnpxTask
      1. 13.6.1Options
      2. 13.6.2Behavior
    7. 13.7NpxTask
      1. 13.7.1Options
      2. 13.7.2Behavior
    8. 13.8File Selections
    9. 13.9CopyTask
      1. 13.9.1Options
      2. 13.9.2Behavior
    10. 13.10MoveTask
    11. 13.11SyncTask
      1. 13.11.1Additional option
      2. 13.11.2Behavior
    12. 13.12ZipTask
      1. 13.12.1Options
    13. 13.13UnzipTask
      1. 13.13.1Options
    14. 13.14DownloadTask
      1. 13.14.1Options
      2. 13.14.2Behavior
    15. 13.15DeleteTask
      1. 13.15.1Options
      2. 13.15.2Behavior
    16. 13.16Common Properties
    17. 13.17Custom 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.2NadleError Subclasses
    3. 15.3Error Propagation
      1. 15.3.1Step-by-step
    4. 15.4Exit Code Determination
    5. 15.5Known Error Types
    6. 15.6Structured Error Output
    7. 15.7Stacktrace Display
  16. 16Reporting
    1. 16.1Reporters
      1. 16.1.1Agent Reporter
    2. 16.2Footer
      1. 16.2.1Content
      2. 16.2.2Update Frequency
      3. 16.2.3Rendering
      4. 16.2.4When Disabled
    3. 16.3Task Status Messages
      1. 16.3.1Empty (Lifecycle-Only) Tasks
    4. 16.4Execution Result
      1. 16.4.1Successful Run
      2. 16.4.2Failed Run
    5. 16.5Summary (`—summary`)
    6. 16.6Welcome Banner
    7. 16.7Task Resolution Display
  17. §Index