All notable changes to ntask are documented in this file.
The format loosely follows Keep a Changelog; this project uses semantic versioning.
- TUI now has a live log pane docked below the DAG tree that tails the
currently-running task's
.logfile. When a task moves torunningthe pane clears and rebinds; on completion it freezes on the final content so you can still read it. Polls every ~150ms.
- Structured loggers (stdlib
loggingandstructlogconfigured with the stdlib handler) were writing directly to the originalsys.__stderr__they captured at app-import time, fighting Textual for the same stream and spilling JSON across the live DAG view. The executor now walks alllogging.StreamHandlerinstances at run-start and rebinds those whose.streamis the captured-original stdout/stderr to point at our_LogTee.FileHandlerinstances and handlers bound to custom streams are left alone. Originals are restored on run-end. _LogTee(thesys.stdout/sys.stderrproxy installed by the executor to capture per-task output) was writing to the underlying stream even under the TUI. Textual owns stdout but not stderr, so library code that usedloggingto stderr (very common — structured JSON loggers, etc.) bled raw lines straight onto the live DAG view. The tee now consults_current_silent_captureand suppresses the underlying write while the TUI is active. The per-task log file still receives every line, so nothing is dropped — only the terminal stream is quiet.
docs/design/pre-post-hooks.mdcaptures the deferred design for per-task before/after hooks: why composition (@with_reset) is the recommended pattern today, and the constraints we'd commit to if we ever do build module-level hooks.- README now includes a worked
--graph --graph-format mermaidexample embedded as a real Mermaid block (the project's own DAG) so users can see what to paste into their own design docs. SeeISSUES.mdfor the target-scoping bug found while verifying this. - New README section "When NOT to use
@cached" documenting the categories of task — side effects, mutating shared state, time- or randomness- dependent, world-probing — where caching is the wrong default.
- The line-based renderers (
LogRenderer,RichRenderer) now prefix eachrunning <fqn>line with[k/n done; next: <fqn>]so multi-task runs no longer feel positionally opaque. Silent for single-task runs. The TUI already provided this implicitly via the live tree. - The end-of-run summary now lists the failed tasks by name on a second
line — useful for
--keep-goingruns where there was no fail-fast and the user would otherwise have to scroll back through the log. - Shell completion:
ntask --completion bash|zsh|fishprints a completion script. The script calls back intontask --completion-tasksandntask --completion-flags <task>, so task and per-task flag suggestions stay in sync with the project's actualtasks.py. ntask initscaffolds a minimaltasks.pyin the current directory.--template plain|django|fastapichooses the boilerplate (django/fastapi templates ship the framework bootstrap block users had to hand-roll). Refuses to overwrite an existingtasks.pywithout--force.initis also reserved, so naming a taskinitnow emits the same shadowing warning asclean/watch.ntask --listnow renders each task's CLI signature next to its name —<required: type>,[--name=default],[--flag]for bool default-False,[--no-flag]for default-True. Long signatures are truncated at 40 chars so the table stays readable.@task(deps=...)now accepts a zero-arg callable returning an iterable of task refs — e.g.@task(deps=lambda: [a, b])— so dependent tasks can sit above their deps in the source file without the linter complaining. The resolver is invoked once at graph-build time and memoised.- Task failures now stream a traceback to stderr by default and append the
full traceback to the per-task log file. New
--tbglobal flag acceptsshort(default),long,line, ornone— the renderer's one-linex <fqn> FAILED: ...summary on stdout is unchanged. - Per-task log files are now written for every run, regardless of which
renderer is active. The on-disk layout is
.ntask/runs/<UTC-run-id>/<task-fqn>.logplus arun.jsonmanifest with counts, per-task durations, and log paths. Override the location with the global--log-dir DIRflag. - Raw
print()output from inside a task body is captured to the per-task log file via a context-awaresys.stdout/sys.stderrtee. Previously the TUI absorbed those writes and they could not be recovered after the run. - Retention: only the
--max-runsmost recent run directories are kept; default is 50.--max-runs 0disables retention.
- Registering a top-level task whose name collides with a built-in subcommand
(
clean,watch) now emits aUserWarningat registration time. Routing is unchanged — the built-in still wins — but the warning makes it obvious that the user task body will never execute.
shell()subprocess output in the default sequential line renderer is not yet pumped through the Python-level tee, so it appears on the terminal but is not appended to the per-task log file in that mode. The TUI silent-log path and the parallel-prefix path both already capture it correctly; closing the gap in the default path is tracked separately.
- The TUI no longer auto-closes when the run finishes. It stays mounted with
the final DAG state and a summary footer until you press
q,Esc, orCtrl+C. Non-interactive runs (CI, piped output) are unaffected — the TUI is still gated onsys.stdout.isatty(). - The TUI footer now includes the run summary (counts + log path) plus a
press q/esc to quithint while the app is showing it. This was previously rendered only by the line-based renderers.
Initial public release.