asgard 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -99
  3. data/README.md +141 -46
  4. data/bin/asgard +10 -0
  5. data/lib/asgard/base/dispatch.rb +257 -31
  6. data/lib/asgard/base/registry.rb +7 -3
  7. data/lib/asgard/base.rb +140 -9
  8. data/lib/asgard/declarations.rb +21 -0
  9. data/lib/asgard/doctor/task_sections.rb +11 -3
  10. data/lib/asgard/doctor.rb +12 -8
  11. data/lib/asgard/jobs/commands.rb +152 -0
  12. data/lib/asgard/jobs/declaration.rb +149 -0
  13. data/lib/asgard/jobs/launcher.rb +109 -0
  14. data/lib/asgard/jobs/state.rb +190 -0
  15. data/lib/asgard/jobs/stopper.rb +141 -0
  16. data/lib/asgard/jobs.rb +25 -0
  17. data/lib/asgard/kernel_methods.rb +19 -14
  18. data/lib/asgard/leading_flag.rb +10 -0
  19. data/lib/asgard/pid.rb +130 -0
  20. data/lib/asgard/schedule/commands.rb +39 -11
  21. data/lib/asgard/schedule/declaration.rb +139 -12
  22. data/lib/asgard/schedule/job_files.rb +24 -0
  23. data/lib/asgard/schedule/launchd.rb +55 -18
  24. data/lib/asgard/schedule/systemd.rb +69 -27
  25. data/lib/asgard/schedule/table.rb +34 -16
  26. data/lib/asgard/schedule.rb +49 -9
  27. data/lib/asgard/shell.rb +372 -44
  28. data/lib/asgard/state_dir.rb +84 -0
  29. data/lib/asgard/tasks.rb +8 -0
  30. data/lib/asgard/version.rb +1 -1
  31. data/lib/asgard/watch/changes.rb +26 -1
  32. data/lib/asgard/watch/commands.rb +52 -12
  33. data/lib/asgard/watch/daemon.rb +334 -43
  34. data/lib/asgard/watch/declaration.rb +135 -40
  35. data/lib/asgard/watch/dispatcher.rb +61 -10
  36. data/lib/asgard/watch/mailbox.rb +50 -0
  37. data/lib/asgard/watch/reporter.rb +50 -34
  38. data/lib/asgard/watch/spawn.rb +41 -16
  39. data/lib/asgard/watch/state.rb +93 -27
  40. data/lib/asgard/watch/worker.rb +44 -9
  41. data/lib/asgard/watch.rb +4 -11
  42. data/lib/asgard.rb +80 -14
  43. metadata +167 -186
  44. data/.envrc +0 -6
  45. data/.github/workflows/deploy-github-pages.yml +0 -52
  46. data/.loki +0 -39
  47. data/.reek.yml +0 -166
  48. data/.rubocop.yml +0 -163
  49. data/Archspec.rb +0 -24
  50. data/CLAUDE.md +0 -139
  51. data/COMMITS.md +0 -196
  52. data/bin/console +0 -11
  53. data/bin/setup +0 -8
  54. data/doc_tasks.loki +0 -21
  55. data/docs/api.md +0 -368
  56. data/docs/assets/css/custom.css +0 -93
  57. data/docs/assets/images/asgard.jpg +0 -0
  58. data/docs/changelog.md +0 -1
  59. data/docs/dependencies.md +0 -313
  60. data/docs/environment.md +0 -129
  61. data/docs/examples.md +0 -194
  62. data/docs/getting-started.md +0 -181
  63. data/docs/helpers.md +0 -262
  64. data/docs/index.md +0 -87
  65. data/docs/options.md +0 -224
  66. data/docs/schedule.md +0 -106
  67. data/docs/shell.md +0 -237
  68. data/docs/subcommands.md +0 -181
  69. data/docs/task-files.md +0 -407
  70. data/docs/tasks.md +0 -326
  71. data/docs/variables.md +0 -338
  72. data/docs/watch.md +0 -123
  73. data/examples/.env +0 -4
  74. data/examples/.envrc +0 -3
  75. data/examples/.loki +0 -24
  76. data/examples/bad.loki +0 -63
  77. data/examples/concurrent.loki +0 -58
  78. data/examples/confirm.loki +0 -48
  79. data/examples/db_subcommands.loki +0 -74
  80. data/examples/depends_on_block/bad/.loki +0 -51
  81. data/examples/depends_on_block/good/.loki +0 -83
  82. data/examples/do.loki +0 -45
  83. data/examples/env_usage.loki +0 -27
  84. data/examples/kitchen_sink.loki +0 -210
  85. data/examples/server_subcommands.loki +0 -65
  86. data/examples/subdir/.loki +0 -12
  87. data/examples/subdir/import_demo.loki +0 -14
  88. data/examples/subdir/import_up_demo.loki +0 -18
  89. data/examples/watcher_examples/.envrc +0 -3
  90. data/examples/watcher_examples/.loki +0 -54
  91. data/examples/watcher_examples/README.md +0 -112
  92. data/examples/watcher_examples/bin/hello +0 -3
  93. data/examples/watcher_examples/data/records.json +0 -1
  94. data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
  95. data/examples/watcher_examples/docs/index.md +0 -4
  96. data/examples/watcher_examples/lib/greeter.rb +0 -7
  97. data/gem_tasks.loki +0 -51
  98. data/git.loki +0 -13
  99. data/mkdocs.yml +0 -166
  100. data/quality.loki +0 -266
  101. data/quality_rails.loki +0 -47
  102. data/xyzzy.loki +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 299e2b451fc4acf80bf87970012a5fea712bf7f2efc4103465355d091d2b1282
4
- data.tar.gz: e9dc3f90d5abaaac9b735e2b92a35296b5956bc0e261526244aff5d1ba500a8e
3
+ metadata.gz: 0ce55713997e41bcc3531ce3b34765e872184ab435aaa310eecc45625240c840
4
+ data.tar.gz: 0913ae51128056486619cf7655479dd28c6298300699ee307e6ec88a0c5e6d6d
5
5
  SHA512:
6
- metadata.gz: 5279427ba559b0067881a8e828642222c3189b002c01dbeb409f69bfe39a018e7288a088125ea29b56d71f8a16370965ed4bea03a9328be9ef6c29b7a4c5d3af
7
- data.tar.gz: cf33a3581b2fceeec5baf59d8fe42505c3d29e9d7f0717e5b81110208c1f9aa339883e06a1c5e9bc29487eba13e4f63ab37d509e48d03285a0ef90a20a64fcb1
6
+ metadata.gz: 416c0a47d231bc39c1b14351d48edaa75a00538f3ee5a9f6e0b2aeb78ca63badc72e77f1d6010f4e50e7235d22f436e93ae5b6c7a9719851ca82bfe43a8338bb
7
+ data.tar.gz: 7426f8fdd9fc2f56655f2584685b91634890cc8ccb84ef0c0a01ec25d8e7085770f2e0ce5fbedfe6da89ac01c552b938b5dc87fa0efc9ec5242acbac1f963348
data/CHANGELOG.md CHANGED
@@ -2,9 +2,142 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-10-31
9
+
10
+ Upgrading from 0.5.x? Read
11
+ [Breaking Changes](https://madbomber.github.io/asgard/breaking-changes/)
12
+ first: schedules and watchers must be removed before you upgrade and
13
+ reinstalled after.
14
+
15
+ ### Added
16
+
17
+ - **Background jobs.** `sh "mkdocs serve", background: true` starts a job
18
+ that outlives asgard and logs its output; `asgard jobs list`, `stop`,
19
+ `log`, `restart` and `clean` manage them.
20
+ - **Shells registry.** `shell` registers an interpreter
21
+ (`shell :strict, "bash -eu -o pipefail"`), `default_shell` picks the one
22
+ `sh` uses, and `shebang :name, script` runs any of them. Each executable
23
+ is checked as the `.loki` loads.
24
+ - **`asgard schedule remove [NAME]` and `asgard schedule prune
25
+ [--dry-run]`.**
26
+ - **Watcher**: triggers that arrive while a task is busy merge into one
27
+ run, and `watcher.log` rotates at 10 MB (`ASGARD_LOG_MAX`).
28
+ `asgard watcher stop --force` ends running tasks instead of waiting for
29
+ them, as a second Ctrl-C does in `watcher run`. Globs accept `[ab]`,
30
+ `[!ab]` and nested braces.
31
+ - **`asgard help jobs`, `help schedule` and `help watcher`** list their
32
+ subcommands.
33
+ - **CI** on Linux and macOS, Ruby 4.0.9 and 4.1.0-preview1.
34
+ - **Runtime dependencies are bounded** to the versions asgard is built
35
+ against: `thor ~> 1.5`, `listen ~> 3.10`, `ractor_queue ~> 0.2`,
36
+ `dotenv ~> 3.2`, `tty-table ~> 0.12` and `tsort ~> 0.2`.
37
+
38
+ ### Changed
39
+
40
+ - **BREAKING: Ruby 4.0 or later.** On Ruby 3.4 the file watcher hangs at
41
+ shutdown.
42
+ - **BREAKING: project slugs carry a hash of the path** (`api-9f3a1c2b`),
43
+ so two projects in directories of the same name no longer share
44
+ schedules or a watcher. Nothing migrates: run `asgard schedule remove`
45
+ and `asgard watcher stop` in each project before upgrading, then
46
+ reinstall and restart. `list --all` marks a project whose directory is
47
+ gone with `[root missing]`.
48
+ - **`sh` runs every script under the default shell.** A script given no
49
+ shell name, one line or many, runs under the shell set with
50
+ `default_shell`, or `bash` when none is set. A one-liner used to go
51
+ through `/bin/sh` while a heredoc ran under bash. Fixes #1.
52
+ - **`sh` is `shebang`**: one method under two names, so
53
+ `sh :python3, "..."` works.
54
+ - **A public method without a `desc` is a hidden task.** It runs from the
55
+ command line and as a dependency, and `help` leaves it out. Make helper
56
+ methods `private`, put them in `no_commands`, or use `helper`.
57
+ - **Parallel dependencies each run on their own instance**, so they no
58
+ longer race on instance variables; pass values with `dep_result`.
59
+ - **A `.loki` that fails to load says so in one line**, with file and
60
+ line; set `ASGARD_TRACE=1` for the backtrace.
61
+ - **`--version` and `--doctor` are asgard's only before the task name**,
62
+ and `--debug` and `--verbose` show one spelling in help.
63
+ - **State is private**: state directories are created 0700 and schedule
64
+ job files 0600, since logs, scripts and `env:` values can hold secrets.
65
+ - **asgard's helpers are reserved names.** A task or helper named `sh`,
66
+ `shebang`, `env`, `import`, `import_up`, `loki_up`, `changes`,
67
+ `dep_result`, `dep_results` or `current_task` is refused at load with
68
+ file and line; it used to replace the helper silently.
69
+ - **Schedule entry names from `options:` can carry a hash.** When an
70
+ option is not plain lowercase words, or the name passes 64 characters,
71
+ an 8-character hash is added, so `--period=week` and `--period week` no
72
+ longer collide. Two different commands whose names still slug alike
73
+ (`--period week` and `period week`) both take a hashed name.
74
+ `asgard schedule install` moves such entries to their
75
+ new names. `every:` together with `on:` is now an error instead of
76
+ ignoring `on:`.
77
+ - **Watcher ignores and patterns.** `.git`, `.hg`, `.svn` and
78
+ `node_modules` are ignored at any depth; `coverage`, `pkg`, `tmp`,
79
+ `log`, `.bundle`, `bundle`, `vendor/bundle` and `vendor/ruby` only at
80
+ the project root, so `app/log/` is watched. A path pattern applies only
81
+ to its own directory, and a bad `env:` is an error when the `.loki`
82
+ loads.
83
+
84
+ ### Removed
85
+
86
+ - **BREAKING: `exec:` removed from `sh` and `shebang`.** Passing it raises
87
+ `ArgumentError (unknown keyword: :exec)`; delete the keyword.
88
+
89
+ ### Fixed
90
+
91
+ - **Dependencies**: a failing parallel dependency waits for its siblings
92
+ and every failure is named; a task waiting on a shared dependency that
93
+ failed no longer runs; a raising dependency no longer prints Ruby's
94
+ thread report. A dependency's own option defaults apply when it runs
95
+ for another task; a task run inside a caller's `rescue` is no longer
96
+ recorded as failed; a runtime `invoke` cycle is reported as a circular
97
+ dependency instead of crashing; methods every object has (`to_s`,
98
+ `inspect`) are not tasks.
99
+ - **`sh`**: a command ended by a signal exits 128 plus the signal number
100
+ instead of crashing; a missing interpreter is named and exits 127. INT
101
+ or TERM sent to asgard alone (`timeout`, a cancelled CI job) reaches the
102
+ running script and everything it started, instead of leaving them
103
+ running.
104
+ - **Loading**: `import_up` from an imported file no longer recurses until
105
+ the stack overflows, and neither do two files that import each other;
106
+ `import` works in a directory whose name contains `[`, `{`, `?` or `*`;
107
+ task and helper names Thor relies on (`args`, `shell`, `options`, ...)
108
+ are rejected with file and line; a `.loki` directory is not a task file.
109
+ - **Subcommand groups** list only their own tasks in help, and
110
+ `asgard tree` shows the name the group was registered under.
111
+ - **`--doctor`** credits a subcommand group to the `.loki` line that
112
+ registered it instead of a file inside Thor, and lists only `.loki`
113
+ files as imports, not every library a task file requires.
114
+ - **Watcher**: it reloads on a `.loki` change instead of silently watching
115
+ nothing; a pidfile is trusted only while its pid is still the watcher; a
116
+ failed spawn no longer kills a worker; an idle daemon no longer spins;
117
+ two starts at once cannot leave two daemons. Tasks no longer inherit the
118
+ watcher's lock, so a task that leaves a process behind cannot block the
119
+ next `start`; a `.loki` edit that drops a watched task keeps the running
120
+ watcher instead of killing it; changes saved during a reload are not
121
+ lost; the watcher's own log lines survive rotation and `log -f` follows
122
+ across it; ignored directories are not scanned; plus smaller path,
123
+ glob, environment and shutdown fixes.
124
+ - **Background jobs**: concurrent starts of one name start one job;
125
+ `jobs stop` can never signal asgard's own process group; a reused pid
126
+ shows as `gone`. A malformed record is skipped instead of crashing every
127
+ listing; processes left in a job's group after its script exits are
128
+ shown and stopped; a job killed by `stop` shows `exited 137`; `clean`
129
+ cannot remove a job that a concurrent start just recorded; a `ps` that
130
+ cannot list processes (BusyBox) is a clear error.
131
+ - **Schedules**: a `$` in a systemd `env:` value stays one `$`; empty
132
+ `on:`/`at:`, control characters and invalid env names are rejected; an
133
+ unchanged install leaves a running job alone; `status` reports running
134
+ jobs and signal exits. `remove`, `prune` and `install` stop with an
135
+ error instead of reporting success when the scheduler will not unload a
136
+ job; a long `options:` string no longer makes a file name too long to
137
+ write; on Linux, background jobs a scheduled task starts survive it.
138
+ - **Packaging**: the gem ships only `lib/`, `bin/asgard`, `sig/`, and the
139
+ README, LICENSE and CHANGELOG.
140
+
8
141
  ## [0.5.1] - 2026-10-19
9
142
 
10
143
  ### Added
@@ -156,7 +289,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
156
289
  def show_version = puts version # instance context
157
290
  end
158
291
  ```
159
-
160
292
  - **`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.
161
293
  ```ruby
162
294
  class Tasks
@@ -169,18 +301,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
169
301
  class_option :version, type: :boolean, default: false, desc: "Show version and exit"
170
302
  no_negate :version
171
303
  ```
172
-
173
- ### Changed
174
-
175
- - **`--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.
176
- - **`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.
177
-
178
- ### Fixed
179
-
180
- - **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)`.
181
-
182
- ### Added (continued)
183
-
184
304
  - **`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`.
185
305
  - **`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`.
186
306
  - **`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.
@@ -201,111 +321,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
201
321
  - **`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.
202
322
  - **`status` task in `kitchen_sink.loki`** — demonstrates `debug?` and `verbose?` predicates with conditional output; shows `--debug` printing `$DEBUG`, `$VERBOSE`, and the full options hash.
203
323
  - **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 ||=`.
204
-
205
- ### Removed
206
-
207
- - **`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`.
208
- - **`import` DSL method** — `import(mod)` was a one-line alias for Ruby's built-in `include`. Callers can use `include` directly.
209
- - **`--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`.
210
- - **`Asgard.load_loki(dir)`** — replaced by `import` with glob support. Callers can use `import(File.join(dir, "*.loki"))` directly.
211
- - **`debug?` / `verbose?` private methods on `Tasks`** — removed as redundant. The identical `module_function` versions on `Kernel` are available everywhere, including inside task bodies.
212
-
213
- ### Refactored
214
-
215
- - **`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.
216
- - **`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.
217
-
218
- ### Fixed
219
-
220
- - **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.
221
- - **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`.
222
- - **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.
223
- - **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.
224
- - **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.
225
- - **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.
226
- - **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.
227
- - **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.
228
- - **`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.
229
- - **`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`.
230
-
231
- ### Changed
232
-
233
- - **`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.
234
- - **`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.
235
- - **`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.
236
- - **`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`.
237
-
238
- ### Added (continued)
239
-
240
324
  - **`--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](https://madbomber.github.io/asgard/api/#asgarddoctor).
241
325
  - **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.
242
326
  - **`test_verbose` task** — runs the test suite with Minitest's verbose (`-v`) output.
243
327
  - **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.
244
328
  - **`console` task** — opens an IRB console with the gem loaded (`bin/console` if present, otherwise `bundle exec irb`).
245
329
  - **`git.loki`** — per-repo git tasks (`push`, `pull`, `fetch`), imported from `.loki`.
246
-
247
- ### Changed (continued)
248
-
249
- - **`release` task** now prompts for confirmation (`Release asgard vX.Y.Z to RubyGems? [y/N]`) unless `-y`/`--yes` is passed, before tagging and pushing.
250
-
251
- ### Fixed (continued)
252
-
253
- - **`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.
254
-
255
- ### Added (continued 2)
256
-
257
330
  - **`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.
258
331
  - **`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.
259
332
  - **`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.
260
333
  - **`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.
261
334
  - 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.
262
-
263
- ### Changed (continued 2)
264
-
265
- - **`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.
266
- - **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.
267
-
268
- ### Added (continued 3)
269
-
270
335
  - **`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](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies-proc-block-form).
271
336
  - **`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.
272
337
  - **`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.
338
+ - **`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.
339
+ - **`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](https://madbomber.github.io/asgard/shell/#handing-off-with-exec).
340
+ - **`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.
341
+ - **`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](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies-proc-block-form).
342
+ - **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`.
343
+ - **`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.
273
344
 
274
- ### Changed (continued 3)
345
+ ### Changed
275
346
 
347
+ - **`--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.
348
+ - **`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.
349
+ - **`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.
350
+ - **`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.
351
+ - **`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.
352
+ - **`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.
353
+ - **`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.
354
+ - **`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`.
355
+ - **`release` task** now prompts for confirmation (`Release asgard vX.Y.Z to RubyGems? [y/N]`) unless `-y`/`--yes` is passed, before tagging and pushing.
356
+ - **`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.
357
+ - **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.
276
358
  - **`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.
359
+ - **`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.
277
360
 
278
- ### Removed (continued)
361
+ ### Removed
279
362
 
363
+ - **`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`.
364
+ - **`import` DSL method** — `import(mod)` was a one-line alias for Ruby's built-in `include`. Callers can use `include` directly.
365
+ - **`--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`.
366
+ - **`Asgard.load_loki(dir)`** — replaced by `import` with glob support. Callers can use `import(File.join(dir, "*.loki"))` directly.
367
+ - **`debug?` / `verbose?` private methods on `Tasks`** — removed as redundant. The identical `module_function` versions on `Kernel` are available everywhere, including inside task bodies.
280
368
  - **`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.
281
369
 
282
- ### Changed (continued 4)
283
-
284
- - **`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.
285
-
286
- ### Fixed (continued 2)
370
+ ### Fixed
287
371
 
372
+ - **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)`.
373
+ - **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.
374
+ - **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`.
375
+ - **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.
376
+ - **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.
377
+ - **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.
378
+ - **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.
379
+ - **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.
380
+ - **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.
381
+ - **`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.
382
+ - **`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`.
383
+ - **`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.
288
384
  - **`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`.
289
-
290
- ### Added (continued 4)
291
-
292
- - **`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.
293
-
294
- ### Added (continued 5)
295
-
296
- - **`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](https://madbomber.github.io/asgard/shell/#handing-off-with-exec).
297
- - **`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.
298
-
299
- ### Fixed (continued 3)
300
-
301
385
  - **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.
302
386
 
303
- ### Added (continued 6)
304
-
305
- - **`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](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies-proc-block-form).
306
- - **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`.
307
- - **`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.
308
-
309
387
  ## [0.2.0] - 2026-05-29
310
388
 
311
389
  ### Changed
@@ -328,17 +406,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
328
406
  - `db_subcommands.loki` — subcommand group for database management with `depends_on` chaining
329
407
  - README sections: Helper methods, Subcommands, Thor wrapper callout
330
408
 
331
- ### Fixed
332
-
333
- - 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
334
-
335
409
  ### Changed
336
410
 
337
411
  - `--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
338
412
  - Removed all references to `just` task runner and `recipe` terminology; Asgard uses "task" throughout
339
413
  - `depends_on` parameter renamed from `*recipes` to `*tasks` for consistency
340
414
 
415
+ ### Fixed
416
+
417
+ - 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
418
+
341
419
  ## [0.1.1] - 2026-05-28
420
+
342
421
  ### Added
343
422
 
344
423
  - Parallel dependency execution — wrap deps in an array to run them concurrently:
@@ -383,7 +462,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
383
462
  - 100% test coverage enforced via SimpleCov (95% minimum threshold)
384
463
  - Quality task in `.loki` runs flog after tests
385
464
 
386
- [0.5.0]: https://github.com/MadBomber/asgard/compare/v0.4.1...v0.5.0
465
+ [0.6.0]: https://github.com/MadBomber/asgard/compare/v0.5.1...v0.6.0
466
+ [0.5.1]: https://github.com/MadBomber/asgard/compare/v0.4.1...v0.5.1
387
467
  [0.4.1]: https://github.com/MadBomber/asgard/compare/v0.4.0...v0.4.1
388
468
  [0.4.0]: https://github.com/MadBomber/asgard/compare/v0.3.3...v0.4.0
389
469
  [0.3.3]: https://github.com/MadBomber/asgard/compare/v0.2.0...v0.3.3