Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Added

  • The gem links its docs site and issue tracker: the gemspec sets documentation_uri and bug_tracker_uri, so the RubyGems page links to the docs site and the GitHub issues.
  • The docs site serves llms.txt: /llms.txt lists every docs page with a one-line description, in the llms.txt format.

Changed

  • Schedule log directories are private: asgard schedule install creates the log directory 0700 (and tightens an existing one), as the watch and jobs state directories already are.

Fixed

  • asgard --doctor exits 1 when it reports problems, including overridden task definitions, so scripts and CI can use it as a check. Warnings alone still exit 0.
  • A background job started right after it ended starts again, instead of being reported as already running while its old shell exits.
  • asgard --doctor reports a .loki that calls exit or abort as a load failure instead of exiting with no report.

0.6.0 - 2026-10-31

Upgrading from 0.5.x? Read Breaking Changes first: schedules and watchers must be removed before you upgrade and reinstalled after.

Added

  • Background jobs. sh "mkdocs serve", background: true starts a job that outlives asgard and logs its output; asgard jobs list, stop, log, restart and clean manage them.
  • Shells registry. shell registers an interpreter (shell :strict, "bash -eu -o pipefail"), default_shell picks the one sh uses, and shebang :name, script runs any of them. Each executable is checked as the .loki loads.
  • asgard schedule remove [NAME] and asgard schedule prune [--dry-run].
  • Watcher: triggers that arrive while a task is busy merge into one run, and watcher.log rotates at 10 MB (ASGARD_LOG_MAX). asgard watcher stop --force ends running tasks instead of waiting for them, as a second Ctrl-C does in watcher run. Globs accept [ab], [!ab] and nested braces.
  • asgard help jobs, help schedule and help watcher list their subcommands.
  • CI on Linux and macOS, Ruby 4.0.9 and 4.1.0-preview1.
  • Runtime dependencies are bounded to the versions asgard is built against: thor ~> 1.5, listen ~> 3.10, ractor_queue ~> 0.2, dotenv ~> 3.2, tty-table ~> 0.12 and tsort ~> 0.2.

Changed

  • BREAKING: Ruby 4.0 or later. On Ruby 3.4 the file watcher hangs at shutdown.
  • BREAKING: project slugs carry a hash of the path (api-9f3a1c2b), so two projects in directories of the same name no longer share schedules or a watcher. Nothing migrates: run asgard schedule remove and asgard watcher stop in each project before upgrading, then reinstall and restart. list --all marks a project whose directory is gone with [root missing].
  • sh runs every script under the default shell. A script given no shell name, one line or many, runs under the shell set with default_shell, or bash when none is set. A one-liner used to go through /bin/sh while a heredoc ran under bash. Fixes #1.
  • sh is shebang: one method under two names, so sh :python3, "..." works.
  • A public method without a desc is a hidden task. It runs from the command line and as a dependency, and help leaves it out. Make helper methods private, put them in no_commands, or use helper.
  • Parallel dependencies each run on their own instance, so they no longer race on instance variables; pass values with dep_result.
  • A .loki that fails to load says so in one line, with file and line; set ASGARD_TRACE=1 for the backtrace.
  • --version and --doctor are asgard's only before the task name, and --debug and --verbose show one spelling in help.
  • State is private: state directories are created 0700 and schedule job files 0600, since logs, scripts and env: values can hold secrets.
  • asgard's helpers are reserved names. A task or helper named sh, shebang, env, import, import_up, loki_up, changes, dep_result, dep_results or current_task is refused at load with file and line; it used to replace the helper silently.
  • Schedule entry names from options: can carry a hash. When an option is not plain lowercase words, or the name passes 64 characters, an 8-character hash is added, so --period=week and --period week no longer collide. Two different commands whose names still slug alike (--period week and period week) both take a hashed name. asgard schedule install moves such entries to their new names. every: together with on: is now an error instead of ignoring on:.
  • Watcher ignores and patterns. .git, .hg, .svn and node_modules are ignored at any depth; coverage, pkg, tmp, log, .bundle, bundle, vendor/bundle and vendor/ruby only at the project root, so app/log/ is watched. A path pattern applies only to its own directory, and a bad env: is an error when the .loki loads.

Removed

  • BREAKING: exec: removed from sh and shebang. Passing it raises ArgumentError (unknown keyword: :exec); delete the keyword.

Fixed

  • Dependencies: a failing parallel dependency waits for its siblings and every failure is named; a task waiting on a shared dependency that failed no longer runs; a raising dependency no longer prints Ruby's thread report. A dependency's own option defaults apply when it runs for another task; a task run inside a caller's rescue is no longer recorded as failed; a runtime invoke cycle is reported as a circular dependency instead of crashing; methods every object has (to_s, inspect) are not tasks.
  • sh: a command ended by a signal exits 128 plus the signal number instead of crashing; a missing interpreter is named and exits 127. INT or TERM sent to asgard alone (timeout, a cancelled CI job) reaches the running script and everything it started, instead of leaving them running.
  • Loading: import_up from an imported file no longer recurses until the stack overflows, and neither do two files that import each other; import works in a directory whose name contains [, {, ? or *; task and helper names Thor relies on (args, shell, options, ...) are rejected with file and line; a .loki directory is not a task file.
  • Subcommand groups list only their own tasks in help, and asgard tree shows the name the group was registered under.
  • --doctor credits a subcommand group to the .loki line that registered it instead of a file inside Thor, and lists only .loki files as imports, not every library a task file requires.
  • Watcher: it reloads on a .loki change instead of silently watching nothing; a pidfile is trusted only while its pid is still the watcher; a failed spawn no longer kills a worker; an idle daemon no longer spins; two starts at once cannot leave two daemons. Tasks no longer inherit the watcher's lock, so a task that leaves a process behind cannot block the next start; a .loki edit that drops a watched task keeps the running watcher instead of killing it; changes saved during a reload are not lost; the watcher's own log lines survive rotation and log -f follows across it; ignored directories are not scanned; plus smaller path, glob, environment and shutdown fixes.
  • Background jobs: concurrent starts of one name start one job; jobs stop can never signal asgard's own process group; a reused pid shows as gone. A malformed record is skipped instead of crashing every listing; processes left in a job's group after its script exits are shown and stopped; a job killed by stop shows exited 137; clean cannot remove a job that a concurrent start just recorded; a ps that cannot list processes (BusyBox) is a clear error.
  • Schedules: a $ in a systemd env: value stays one $; empty on:/at:, control characters and invalid env names are rejected; an unchanged install leaves a running job alone; status reports running jobs and signal exits. remove, prune and install stop with an error instead of reporting success when the scheduler will not unload a job; a long options: string no longer makes a file name too long to write; on Linux, background jobs a scheduled task starts survive it.
  • Packaging: the gem ships only lib/, bin/asgard, sig/, and the README, LICENSE and CHANGELOG.

0.5.1 - 2026-10-19

Added

  • Watched files, built in — watch :task, paths: "lib/**/*.rb" at class level in a .loki declares that a task runs when matching files change, with only:, ignore:, events:, debounce:, env: and as: keywords. asgard watcher start|stop|list|preview|log|run manages the watcher: one background process per project that runs each trigger as asgard <task> from the project root, with a fixed environment (a whitelist of the daemon's variables, the entry's env:, .envrc through direnv exec) plus the changed paths in ASGARD_MODIFIED, ASGARD_ADDED and ASGARD_REMOVED. Different tasks run concurrently; the same task never overlaps itself, and a trigger that arrives while its task runs is queued behind it. Saving a .loki file restarts the watcher with the new declarations. stop lets a running task finish. list shows each entry's last run; --all covers every project. State and logs live under ~/.local/state/asgard/watch/. The command is registered as the gem-owned _watcher and mapped to watcher. The daemon and every trigger run the same asgard executable that started the watcher, by absolute path, even under direnv exec. See Watched Files.
  • changes Kernel method — inside a task, the Asgard::Changes the watcher triggered it with (modified, added, removed, all, empty?); every list is empty when the task is run by hand.
  • Asgard::Watch::Error (subclass of Asgard::Error) — raised when a watch names a task the project does not define.
  • examples/do.loki — asgard do lint test build runs tasks in sequence, with comma-separated steps for arguments and options (asgard do hello Alice, farewell Bob --formal). A plain user task, not a built-in; it stops at the first failure.
  • examples/confirm.loki — a hidden confirm task to list first in a dangerous task's depends_on; answering anything but y/yes aborts with exit 1. Refuses without a terminal unless ASGARD_YES is set.

Changed

  • New runtime dependencies: listen (file events) and ractor_queue (the queue between the daemon and its worker Ractors). ractor_queue is a native extension, so gem install asgard now compiles and needs a C++17 compiler; Asgard was pure Ruby before.
  • Asgard::Schedule::Table.draw takes headers: so watcher list can reuse it; a running state renders green.
  • Asgard.machine_wide? is true for watcher list as well as schedule list, so both work from a directory with no .loki above it.

Fixed

  • Usage errors exit 1 — a missing argument, an unknown command, or a bad subcommand (asgard greet with no NAME, asgard nope) printed Thor's error but exited 0, so CI and scripts saw success. Asgard::Base now defines exit_on_failure? as true.

0.4.1 - 2026-10-02

Added

  • asgard schedule list --all (-a) — lists every project's scheduled entries, one table per project under a Project: name (/full/path/to/project) header. Other projects' .loki files are not loaded: their COMMAND and SCHEDULE columns, and the project path, are read back from the installed launchd plist or systemd unit files. Outside any .loki tree, asgard schedule list shows every project automatically instead of failing with "no .loki file found".
  • stop, start, trigger and log accept entries of other projects, so they work from any directory. A bare name matches this project's entry first, then any other project's; when two other projects share a name, write it as project/name.
  • A project-scoped list with nothing installed now ends with "Use asgard schedule list --all to see all currently scheduled tasks."
  • Backend API: installed_entries (every project's entries), installed_columns(name) and installed_directory(name) (read from the job files) on both the launchd and systemd backends.
  • Runtime dependency: tty-table (~> 0.12), which brings pastel and tty-screen.

Changed

  • asgard schedule list prints a table — NAME, COMMAND, SCHEDULE, STATE and LAST EXIT, with long cells wrapped to the terminal width, replacing one text line per entry. A run of hourly times collapses (10:00-17:00 hourly weekdays). Color is used on a terminal and turned off by NO_COLOR or when piped.
  • The not-installed messages from stop, start, trigger and log point at asgard schedule list --all.

Fixed

  • Scheduled entries were invisible outside the directory tree of the project that installed them: list and every per-entry command were scoped to the project found from the current directory, so launchctl list showed jobs that asgard schedule list did not.

0.4.0 - 2026-09-26

Added

  • Scheduled tasks, built in — schedule :task, at: "17:30", on: :weekdays (or every: 3600) at class level in a .loki declares a periodic run, and asgard schedule preview|install|list|start|stop|trigger|log|remove manages the entries under launchd (macOS) or systemd user timers (Linux). Missed calendar runs fire on wake; a project .envrc is loaded at run time through direnv exec. Promoted from the standalone dev/schedule.loki; job labels and paths are unchanged, so entries it installed are still recognized. ActiveSupport is no longer required: every: takes seconds or any object with in_seconds. The command is registered as the gem-owned _schedule and mapped to schedule. See Scheduled Tasks.
  • Asgard::Schedule::Error (subclass of Asgard::Error) — raised when launchctl/systemctl fails; Asgard.run! reports it as a one-line asgard: ... message.

Changed

  • Upgrading from dev/schedule.loki: remove import_up "dev/schedule.loki" from your .loki. Its Tasks.schedule overrides the built-in one and records declarations where asgard schedule never looks.
  • Asgard::Error and Asgard::CircularDependencyError now live in lib/asgard/errors.rb, loaded first. No change to the classes themselves.
  • The gem's own test run (test_check, test_verbose) loads every test/test_*.rb in one process, so coverage is measured across the whole suite.

Fixed

  • asgard <task> now exits 1 when the task returns :fail — quality-gate tasks (*_check) signal failure by returning :fail; previously that result was discarded and asgard always exited 0, so callers relying on the exit status (CI, cross-repo runners like ws_quality.loki) saw every individual gate as passing. The aggregate quality task was unaffected (it aborts itself). Only a top-level result of exactly :fail maps to exit 1; :warn and :skip remain non-blocking.

0.3.3 - 2026-08-27

Added

  • helper(name, &block) DSL method on Asgard::Base — defines a method available in both class context (e.g. inside header or footer) and as a private instance method inside task bodies, with a single declaration. Eliminates the manual def self.name + no_commands { private def name = self.class.name } boilerplate. Supports positional arguments, keyword arguments, default values, and block arguments.
    class Tasks
      @@project ||= "myapp".freeze
    
      helper(:version) {
        File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
      }
    
      header "#{@@project} v#{version}"   # class context
    
      desc "Show the current version"
      def show_version = puts version     # instance context
    end
    
  • header(text) and footer(text) DSL methods on Asgard::Base — attach static text to the general help output. header lines are printed above the commands list; footer lines are printed below the options block. Multiple calls accumulate: each header call appends a line, each footer call prepends a line, so content from later-loaded files naturally wraps around content from earlier files. Neither appears when asgard help <command> is called for per-command detail.
    class Tasks
      header "my-project — build & release tasks"
      footer "See https://example.com/docs for details"
    end
    
  • no_negate(*names) DSL method on Asgard::Base — suppresses the [--no-name] and [--skip-name] negation variants from help output for boolean class options where negation is meaningless. Call it after the class_option declaration:
    class_option :version, type: :boolean, default: false, desc: "Show version and exit"
    no_negate :version
    
  • loki_up(name = ".loki") Kernel method — searches Dir.pwd and each ancestor directory for a file with the given name; returns the absolute path of the first match or nil. Available everywhere in Ruby (task bodies, .loki files, top-level code) as a module_function on Kernel.
  • import(path) Kernel method — loads a .loki file (or a glob of .loki files) with require-like idempotency via $LOADED_FEATURES. Accepts a String or Pathname. Relative paths are resolved relative to the caller's file (like require_relative). Glob patterns (*.loki, **/*.loki) expand via Dir.glob and load all matches. Returns true if any file was newly loaded, false if all were already loaded or no glob matches were found. Raises ArgumentError if the path does not end with .loki.
  • import_up(name = ".loki") Kernel method — combines loki_up and import. For exact names, finds the first ancestor directory containing that file and loads it. For glob names, finds the first ancestor directory containing any matching files and loads them all — stopping at that level rather than aggregating across multiple ancestors. Returns false if nothing is found.
  • debug? and verbose? Kernel module functions — thin wrappers around $DEBUG and $VERBOSE, available everywhere in Ruby as module_function on Kernel. Set via --debug / --verbose CLI flags or directly via $DEBUG / $VERBOSE.
  • env(name, default = nil) Kernel method — fetches a system environment variable by symbol or string name, upcasing the key automatically. env(:port, "3000") returns "3000" when PORT is unset; env(:api_key) raises KeyError when API_KEY is missing and no default is provided. Accepts both env(:port) and env("PORT") forms. Cleaner than ENV['PORT'] in task bodies.
  • Verbose/debug feedback for import and import_up — when verbose? is true, each file loaded is printed to stderr. When debug? is true, already-loaded files are also reported (with an "already loaded" suffix), and import_up reports when a file is not found.
  • RuboCop lint gate — RuboCop is now a first-class quality gate alongside tests and Flog. Added rubocop to the Gemfile, a .rubocop.yml tuned for this codebase (Ruby 3.2 target, relaxed Metrics thresholds consistent with Flog as the primary complexity gate, examples/ excluded), and rake rubocop / rake rubocop_fix tasks backed by a tmp/rubocop_cache directory for fast re-runs.
  • Expanded rake quality task — quality now runs three independent gates (tests + coverage, RuboCop, Flog) in parallel using depends_on [:test, :rubocop, :flog_check]. Each gate captures its pass/fail result in an instance variable; output is suppressed on pass and filtered to failures only on fail, preventing interleaved output from concurrent subprocesses. A formatted pass/fail summary table is printed after all gates complete, so every failure is visible in a single run.
  • rake flog_check task — replaces the bare flog lib/ call with a structured task that enforces per-method thresholds (warn ≥20, fail ≥50), lists warnings and failures in separate sections, and exits non-zero only when the failure threshold is breached.
  • Single-argument desc shorthand — desc now accepts one string (the description) with the usage string omitted. The usage defaults to the method name, eliminating the redundant first argument for the common case:
    desc "Run the test suite"   # usage defaults to "test"
    def test = sh "bundle exec rake test"
    
    The two-argument form (desc "usage", "description") still works unchanged and is still required when the usage string differs from the method name (e.g. desc "build NAME", "Build an artifact").
  • default_task override warning — Asgard::Base now overrides Thor's default_task to warn to stderr when a second call would silently replace the first. The warning includes the task name, file, and line number for both the original declaration and the override, making accidental cross-file clobbering visible immediately.
  • examples/env_usage.loki and examples/.env — demonstrate the env() Kernel helper, including the default-fallback form (env(:log_level, "info")) for variables absent from the environment. Uses loki_up(".env") so dotenv locates the .env file correctly regardless of which directory asgard is invoked from.
  • examples/subdir/ — three-file demo showing import and import_up across directory boundaries: subdir/.loki imports import_up_demo.loki by name; import_up_demo.loki calls import_up "env_usage.loki" to locate and load a file from an ancestor directory without a hardcoded path.
  • status task in kitchen_sink.loki — demonstrates debug? and verbose? predicates with conditional output; shows --debug printing $DEBUG, $VERBOSE, and the full options hash.
  • Computed value methods in kitchen_sink.loki — three private methods (version, sha, branch) demonstrating the idiomatic Ruby replacement for the removed var DSL, including memoization via @ivar ||=.
  • --doctor built-in CLI flag — diagnoses .loki resolution, import chains, and task definitions for the current directory, then exits. Handled directly in Asgard.run! before the .loki file is loaded (same pattern as --version), so it keeps working in exactly the situations that would otherwise abort the whole process: a broken .loki file, a circular or undefined dependency, or a task silently redefined by a later def. Backed by the new Asgard::Doctor class. The report includes a "Tasks by file" listing: every command grouped by the file it's defined in, printed as relative/path:line so an editor can jump straight to it. A task name defined at more than one location gets every definition annotated inline — the earlier one(s) as OVERRIDDEN by <file>:<line> — never callable, the winning (last) one as active — redefines <file>:<line> — replacing the old flat "Tasks#x redefined" summary line with an annotation right where the problem is. See API Reference.
  • Flay and Reek quality gates — flay_check checks for structural code duplication (mass ≥ 150); reek checks code smells. Both run as part of quality alongside test, rubocop, and flog_check. A .reek.yml tunes several detectors to this codebase's conventions (no doc-comment requirement, short variable names, disabled TooManyStatements, etc), plus per-method exclude: entries (generated with reek --todo and hand-curated) that grandfather specific reviewed smells at specific methods — precise enough that a genuinely new smell still fails the gate even at an already-reviewed method, unlike a per-file count.
  • test_verbose task — runs the test suite with Minitest's verbose (-v) output.
  • Colorized quality gate summary — quality's final report now prints a green/red PASS/FAIL badge per gate plus a passed/failed tally, via a shared print_quality_summary helper.
  • console task — opens an IRB console with the gem loaded (bin/console if present, otherwise bundle exec irb).
  • git.loki — per-repo git tasks (push, pull, fetch), imported from .loki.
  • SKIP and WARN quality-gate statuses — alongside PASS/FAIL. Both are non-blocking (quality only aborts on FAIL); SKIP is for a required external tool that isn't installed, WARN is for a check that ran successfully but has non-blocking suggestions. print_quality_summary renders all four with distinct colored badges and a combined tally.
  • typos_check / typos_fix tasks — spell-checking via the external typos CLI (brew install typos-cli, not a gem dependency). typos_check writes full findings to typos_output.txt and participates in quality. If typos isn't installed, the gate prints a one-line notice recommending brew install typos-cli and reports SKIP rather than failing.
  • fasterer_check task — runs the fasterer gem (new dev dependency) against lib/, reporting performance-idiom suggestions as WARN (non-blocking) — these are suggestions on working code, not correctness problems.
  • asgard tree now shows the project header/footer — Base#tree wraps Thor's built-in command tree the same way Base#help already wraps the command list, so both example outputs are consistent.
  • Every quality gate (test_check, rubocop_check, flog_check, flay_check, reek_check, typos_check, fasterer_check, bundler_audit_check) now writes its full detailed output to a <gate>_output.txt file at the repo root (gitignored via *_output.txt) and prints only a one-line summary to stdout — full detail stays on disk without cluttering the terminal.
  • depends_on accepts a Proc/lambda in addition to a fixed list — a sole callable defers resolution to validate_deps! (once, right after every .loki file has loaded) instead of resolving immediately when depends_on itself is evaluated. This solves the "load order matters" problem for a dependency list that can't be known upfront — e.g. "every task whose name ends in _check," discovered across several files including ones imported conditionally (import "quality_rails.loki" if defined?(Rails)). The Proc must return the same shape the splat form would receive (an array of stages, each a Symbol or Array); it's written directly in the class body, so it lexically captures that class as self and can call all_commands bare. A Proc that raises is re-raised as Asgard::Error naming the task it was declared for; a Proc that resolves to an undefined task or a cycle is still caught by the existing startup validation, since the resolved result is checked exactly like a plain array. See Dynamic Dependencies.
  • bundler_audit_check task — runs bundle-audit check --update against Gemfile.lock (new dev dependency bundler-audit); reports FAIL on any known vulnerability, since this is a security gate, not a suggestion.
  • quality_rails.loki — imported by .loki only when Rails is defined; currently ships brakeman_check, a Rails security-scan example. Needs no wiring into quality's dependency list — quality's depends_on Proc discovers it automatically once it's loaded.
  • examples/bad.loki — a worked demonstration of the race the fix above addresses: 4 parallel workers read-modify-write a shared @hits counter directly (the anti-pattern quality.loki used to have), reliably losing updates. Kept as a contrast example for what dep_result/dep_results is for.
  • sh(script, exec: true) — hands the command the asgard process itself via Kernel.exec instead of forking. For a task's final, long-running command (a dev server, a REPL) this replaces the ruby process outright, so nothing sits resident in memory behind it and Ctrl-C is handled directly by the command instead of unwinding back through asgard. doc_tasks.loki's doc_server task (sh "mkdocs serve", exec: true) is the motivating example. See Shell Helpers.
  • bootstrap and env_info tasks in kitchen_sink.loki — demonstrate sh with a multi-line heredoc (routed through bash -c) and a single-line command, respectively.
  • depends_on accepts a block in addition to a Proc/lambda — depends_on { ... } (or depends_on do ... end for a block spanning multiple statements) defers resolution to validate_deps! exactly like the existing sole-Proc/lambda form; the two are interchangeable. depends_on still accepts task arguments or a block, never both — combining them raises Asgard::Error. See Dynamic Dependencies.
  • The resolved Proc/lambda/block result is now shape-validated — once validate_deps! calls it, the return value must be an Array of stages, each a Symbol/String (sequential) or an Array of Symbol/String (parallel group), nested no deeper than that. A bad shape (wrong type, an invalid stage, a non-Symbol/String leaf, or nesting more than one level deep) now raises Asgard::Error naming the task and the offending value, instead of failing later with an opaque NoMethodError.
  • examples/depends_on_block/good/ and examples/depends_on_block/bad/ — two self-contained example projects (each its own .loki root, isolated from the main examples/ tree) demonstrating the block form: good/ covers single-line { ... }, do...end, and a mixed sequential+parallel shape; bad/ demonstrates the double-wrapped-array mistake that the new shape validation catches, with the exact Asgard::Error message it produces.

Changed

  • --version reimplemented as a class_option — the flag now appears in the "Options" section of asgard help alongside --debug and --verbose, rather than as a listed command. The _version method and its map "--version" => :_version registration have been removed. The actual early-exit behaviour still lives in Asgard.run! (before the .loki file is required), so --version works even when no .loki file exists. no_negate :version suppresses the spurious [--no-version] / [--skip-version] variants.
  • loki_up returns Pathname instead of String — the return value is now a Pathname instance (or nil when not found). Pathname is accepted everywhere loki_up's result is used: import, dotenv, load, and standard Ruby file methods all accept Pathname via to_path / to_s. Code that passes the result directly to those methods is unaffected; code that performs string operations on the path should call .to_s first.
  • validate_deps! decomposed into focused private helpers — the method was performing four unrelated validations in one body (orphaned depends_on check, undefined dep name check, dep arity check, cycle detection). Each concern is now a dedicated _-prefixed private method (_check_orphaned_deps!, _check_undefined_deps!, _check_dep_arities!, _build_and_sort_graph). validate_deps! is now a sequencer of ~8 lines. Flog score dropped from 87.3 to 24.4.
  • invoke_command dispatch helpers made private with descriptive names — the dispatch hook was decomposed into focused helpers. Those helpers (acquire_run_token, run_deps_for, run_dep_group, signal_done, run_dep) are now declared private on Asgard::Base rather than wrapped in no_commands. The _ prefix was dropped — the underscore convention is reserved for gem-owned methods on Tasks; private is sufficient to exclude instance methods from Thor's command registry. invoke_command itself stays in no_commands so Thor does not warn about an undescribed public method.
  • validate_deps! detects undefined dependency names — depends_on :nonexistent previously passed validation silently and produced no error at runtime. validate_deps! now raises Asgard::Error listing every dep name that does not correspond to a defined task.
  • default_task behaviour documented — docs/tasks.md now notes that running asgard with no arguments displays the help message when default_task is not set.
  • loki_up scope clarified in docs — docs/task-files.md and docs/api.md now make explicit that loki_up locates any file by name, not just .loki files, with examples for .env and VERSION. The dotenv loki_up(".env") || ".env" pattern is shown as the canonical way to load a .env file from any subdirectory.
  • examples/.loki — updated to use explicit import "*.loki" (sibling files) and import "subdir/import_demo.loki" (subdirectory file), with comments explaining import, import_up, and loki_up.
  • release task now prompts for confirmation (Release asgard vX.Y.Z to RubyGems? [y/N]) unless -y/--yes is passed, before tagging and pushing.
  • Asgard::Base and Asgard::Doctor split into focused mixins — Asgard::Base (31 methods) is now Registry, DependencyGraph, TaskDSL, and Dispatch (lib/asgard/base/*.rb), with method_added/header/footer/help/tree remaining directly on the class as the orchestrator. Asgard::Doctor (19 methods) is now TaskSections and Report (lib/asgard/doctor/*.rb), with the diagnostic flow (run, report_markers, load_chain, etc.) staying on the class itself. Purely a file-organization change — behavior, asgard help/asgard --doctor output, and the public API are unchanged. Drops Reek's TooManyMethods/TooManyInstanceVariables warnings on both classes to zero.
  • Reek grandfathering made precise — replaced the per-file smell-count baseline (.quality/reek_baseline.txt, the reek_baseline task) with per-method, per-detector exclude: entries in .reek.yml itself, generated via reek --todo and hand-curated. Unlike a count, this still catches a genuinely new smell at an already-reviewed method, without relying on the file's total count staying the same. The reek_baseline and ensure_quality_dir tasks and the .quality/ directory have been removed as no longer needed.
  • test, rubocop, reek renamed to test_check, rubocop_check, reek_check — for consistency with the other gates, all of which already ended in _check. This convention is what makes automatic discovery possible: quality's depends_on Proc finds every task whose name matches _check\z rather than naming them one by one.
  • validate_deps! cycle detection now uses stdlib TSort instead of Dagwood::DependencyGraph#order — a private Graph Struct (edges member, include TSort) owns the task→dependency Hash and the traversal, raising TSort::Cyclic on a cycle exactly as before (converted to Asgard::CircularDependencyError). run_deps_for no longer round-trips through a rebuilt DAG on every dispatch — it iterates _deps[target]'s stage groups directly, which is already the parallel-execution plan depends_on built.

Removed

  • var DSL method — removed in favour of native Ruby class variables. Use @@name ||= "value".freeze in the class body. Class variables are visible in all task instance methods and in subcommand subclasses, making them the correct tool for shared configuration in a Thor-based task runner. Breaking change for projects using var.
  • import DSL method — import(mod) was a one-line alias for Ruby's built-in include. Callers can use include directly.
  • --auto-load CLI flag — sibling *.loki loading is now entirely user-controlled: place import "*.loki" (or any glob or explicit path) at the top of your .loki file to load additional task files. Breaking change for projects that relied on --auto-load.
  • Asgard.load_loki(dir) — replaced by import with glob support. Callers can use import(File.join(dir, "*.loki")) directly.
  • debug? / verbose? private methods on Tasks — removed as redundant. The identical module_function versions on Kernel are available everywhere, including inside task bodies.
  • dagwood runtime dependency — replaced by stdlib TSort for the one thing it was still doing (cycle detection); the parallel-execution plan itself was already derived directly from depends_on's stage list, not from a rebuilt DAG. _build_dep_graph (dead code — its return value was already unused) is deleted along with the gemspec entry.

Fixed

  • Help output showed tasks prefix before every command — asgard help displayed asgard tasks build instead of asgard build. The cause was a keyword-vs-positional mismatch in the Asgard::Base#help override: declaring subcommand: false as a keyword argument caused Ruby's super to forward it as the hash {subcommand: false}. Thor's banner method received this hash as the positional subcommand argument, treated it as truthy, and prepended the class namespace (tasks) to every command name. Fixed by changing the override signature to match Thor's positional signature: def help(command = nil, subcommand = false).
  • Multiple parallel dep failures now all surfaced — when two or more parallel deps raised, only the first exception was re-raised; the rest were silently discarded. All errors are now printed to stderr via warn before a general Asgard::Error is raised. When only one dep fails, its exception is re-raised directly as before.
  • Subcommand deps not validated at startup — run! only called Tasks.validate_deps!, so circular dependencies and undefined dep names in subcommand groups were silently ignored. run! now snapshots Asgard::Base.subclasses before loading task files and validates every newly defined subclass alongside Tasks.
  • Parallel dep thread orphaned on exception — when a parallel dep group contained one fast-failing task and one slow task, the join loop re-raised the first thread's exception and abandoned the remaining threads. The join loop now collects all thread exceptions before re-raising, ensuring every thread completes before execution exits the group.
  • Dep with required arguments gave a cryptic runtime error — depends_on :build where build(name) has required parameters caused Thor::InvocationError at task invocation time with no indication of where the problem was. validate_deps! now checks arity via instance_method.parameters and raises Asgard::Error at startup with the task name and argument count.
  • Orphaned depends_on silently discarded — a depends_on declaration at the end of a class body with no following desc/def left @_pending_deps non-empty but was ignored by validate_deps! due to an early return if _deps.empty? guard. validate_deps! now checks @_pending_deps before that guard and raises Asgard::Error naming the orphaned dependencies.
  • Single-arg desc options silently dropped — when desc "description", hide: true was used, Ruby routed the options hash to the description positional parameter. The override now detects a Hash in the description position and treats it as options.
  • Single-arg desc stolen by no_commands blocks — if a no_commands block appeared between a single-arg desc and its method, method_added consumed @_pending_single_desc for the interstitial helper and leaked a stale @usage onto the next real command. The fix: @_pending_single_desc is only consumed when no_commands? is false.
  • Parallel dep race condition — when two parallel tasks shared a common dependency, the second thread could start before the shared dep finished. _ran_tasks (a single Set) has been replaced with _running / _done Sets and a per-task ConditionVariable. Threads that arrive at an already-running dep now wait for its completion.
  • depends_on silently dropped before no_commands blocks — the method_added guard now checks @usage instead of the no_commands? counter, so no_commands helpers placed between depends_on and def no longer silently steal the dependency.
  • shebang ignored its silent: keyword argument — the parameter was accepted but never referenced; the script body is now echoed to stdout unless silent: true is passed, matching the behavior of sh.
  • bin/asgard could silently run the wrong asgard version — the executable did require "asgard", which (without bundle exec) is resolved by RubyGems independently of where the script itself lives, so it could load a separately-installed gem version instead of this repo's own lib/. Changed to require_relative "../lib/asgard" so the executable always loads the library that ships alongside it, regardless of what else is installed.
  • quality.loki's parallel *_check tasks raced on shared instance state — each *_check task wrote its pass/fail status to an ivar on self (@test_result, @rubocop_result, ...) from inside a Thread.new spawned by the same parallel-dependency group — an unsynchronized write across threads that MRI's GVL happens to hide today but would not on JRuby/TruffleRuby. Fixed at the framework level: Dispatch#run_dep_group now collects each task's own return value into a Hash (via Thread#value), run_deps_for merges per-stage results, and a new dep_result/dep_results instance API (backed by Thread.current, not self) hands them to the task body that depends on them. quality.loki and quality_rails.loki's *_check tasks now return their status as a plain value instead of writing to an ivar; quality reads dep_result(name) instead of instance_variable_get.
  • Ctrl-C during a running sh command printed a raw Interrupt backtrace — SIGINT hits the whole foreground process group, so asgard's own ruby process raised Interrupt independently of whatever the shelled-out command did with the signal, and it went uncaught, unwinding through Thor and printing a stack trace before exiting. Asgard.run! now rescues Interrupt and exits with the conventional 130 status.

0.2.0 - 2026-05-29

Changed

  • *.loki files are no longer auto-loaded by default. Pass --auto-load to asgard to load all *.loki files from the project root alphabetically before .loki. This is a breaking change for projects using the multi-file layout.
  • Added --auto-load as a built-in CLI flag in Tasks, visible in asgard help

0.1.2 - 2026-05-29

Added

  • --version built-in CLI flag — prints Asgard::VERSION and exits; implemented as a _-prefixed method in Tasks per the gem-owned naming convention
  • --debug and --verbose built-in class_option declarations on Tasks — set $DEBUG/$VERBOSE before any task runs via the invoke_command hook in Asgard::Base
  • debug? and verbose? private predicate helpers on Tasks — thin wrappers around $DEBUG and $VERBOSE for use inside task bodies
  • _ prefix convention for gem-owned methods in Tasks — built-in methods use _ prefix to distinguish them from user-defined tasks
  • run! guards against direct invocation of _-prefixed commands with a clean error message and exit 1
  • examples/ directory with working .loki files:
  • kitchen_sink.loki — demonstrates the full Thor DSL (all option types, long_desc, class_option, default_task, map, depends_on, var, no_commands, private)
  • server_subcommands.loki — subcommand group for server management
  • db_subcommands.loki — subcommand group for database management with depends_on chaining
  • README sections: Helper methods, Subcommands, Thor wrapper callout

Changed

  • --debug and --verbose promoted from mapped tasks to class_option — they now work as modifiers alongside other commands (e.g. asgard build --debug) rather than as standalone commands
  • Removed all references to just task runner and recipe terminology; Asgard uses "task" throughout
  • depends_on parameter renamed from *recipes to *tasks for consistency

Fixed

  • Replaced warn/exit 1 with abort throughout run! — Kernel#warn is silenced when $VERBOSE = nil, which is the default in Ruby 4.0; abort writes to $stderr regardless

0.1.1 - 2026-05-28

Added

  • Parallel dependency execution — wrap deps in an array to run them concurrently: depends_on [:build, :lint] or depends_on :setup, [:build, :lint], :deploy
  • Asgard.run!(argv) — single entry point encapsulating find, load, validate, and start
  • Asgard.load_loki(dir) — auto-loads all *.loki files in a directory alphabetically
  • Tasks class pre-defined by the gem (class Tasks < Asgard::Base) — task files reopen it without restating the superclass
  • lib/asgard/tasks.rb — ships the pre-defined Tasks class

Changed

  • Replaced SimpleFlow dependency with Dagwood — purpose-built DAG library with no extra dependencies and no Ruby 4 compatibility issues
  • bin/asgard simplified to two lines: require "asgard" + Asgard.run!(ARGV)
  • Task file convention: .loki is the project root marker and entry point; *.loki files each reopen class Tasks and are auto-loaded before .loki
  • Asgard.find_task_files renamed to Asgard.find_task_file (singular — only .loki is the entry point)
  • depends_on now accepts mixed sequential/parallel stages; bare symbols run sequentially, arrays within the splat run in parallel
  • run! handles its own errors — missing .loki and circular dependencies produce a clean one-line message and exit 1 rather than a backtrace
  • Thread-safe dep deduplication via class-level _ran_tasks Set + Mutex replaces Thor's @_invocations
  • Removed import macro — task files use Ruby class reopening instead of modules

Removed

  • SimpleFlow dependency (replaced by Dagwood)
  • logger gem workaround (was only needed for SimpleFlow on Ruby 4)
  • *.loki glob fallback in find_task_file — only .loki is the auto-discovered entry point

0.1.0 - 2026-05-28

Added

  • Asgard::Base — Thor subclass providing the task DSL
  • depends_on — declare task dependencies; dependencies run at most once per invocation
  • var — declare static or lazy-evaluated variables available to all tasks
  • import — flat-merge a task module into the current class
  • dotenv — load a .env file into the environment
  • sh — run a shell command or multiline heredoc script; exits with the command's status on failure
  • shebang — write a script body to a tempfile and execute it with a given interpreter (:python3, :node, :ruby, :perl, :bash, :sh, or any custom interpreter)
  • Asgard.find_task_files — search current directory and ancestors for task files
  • Task file resolution: .loki takes priority; falls back to all *.loki files sorted alphabetically
  • asgard executable — finds task files, validates dependency graph, dispatches via Thor
  • Circular dependency detection via SimpleFlow::DependencyGraph at startup
  • 100% test coverage enforced via SimpleCov (95% minimum threshold)
  • Quality task in .loki runs flog after tests