asgard 0.2.0 → 0.3.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.
data/README.md CHANGED
@@ -17,12 +17,12 @@
17
17
  - <strong>Task Dependencies</strong> — sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
18
18
  - <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
19
19
  - <strong>Subcommands</strong> — group related tasks under a named namespace<br>
20
- - <strong>Variables</strong> — static values and lazy-evaluated lambdas via <code>var</code><br>
20
+ - <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
21
21
  - <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
22
22
  - <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
23
23
  - <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
24
- - <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded on demand with <code>--auto-load</code><br>
25
- - <strong>Built-in Flags</strong> — <code>--version</code>, <code>--debug</code>, and <code>--verbose</code> available on every task<br>
24
+ - <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
25
+ - <strong>Built-in Flags</strong> — <code>--debug</code> and <code>--verbose</code> available on every task; <code>--version</code> at the top level<br>
26
26
  </td>
27
27
  </tr>
28
28
  </table>
@@ -51,8 +51,8 @@ Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` cl
51
51
 
52
52
  ```ruby
53
53
  class Tasks
54
- desc "hello", "Say hello"
55
- def hello = sh 'echo "Hello, World!"'
54
+ desc "Say hello"
55
+ def hello = puts "Hello, World!"
56
56
  end
57
57
  ```
58
58
 
@@ -67,7 +67,7 @@ Declare positional parameters directly in the method signature. Document them in
67
67
  ```ruby
68
68
  class Tasks
69
69
  desc "hello NAME", "Say hello to NAME"
70
- def hello(name = "World") = sh "echo 'Hello, #{name}!'"
70
+ def hello(name = "World") = puts "Hello, #{name}!"
71
71
  end
72
72
  ```
73
73
 
@@ -88,7 +88,7 @@ class Tasks
88
88
  desc: "Name to greet"
89
89
 
90
90
  desc "hello NAME", "Say hello to NAME"
91
- def hello = sh "echo 'Hello, #{name}!'"
91
+ def hello = puts "Hello, #{name}!"
92
92
  end
93
93
  ```
94
94
 
@@ -103,7 +103,7 @@ class Tasks
103
103
  method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
104
104
  def hello(name = "World")
105
105
  message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
106
- options[:count].times { sh "echo '#{message}'" }
106
+ options[:count].times { puts message }
107
107
  end
108
108
  end
109
109
  ```
@@ -128,7 +128,7 @@ class Tasks
128
128
  method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
129
129
  def hello(name = "World")
130
130
  message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
131
- options[:count].times { sh "echo '#{message}'" }
131
+ options[:count].times { puts message }
132
132
  end
133
133
  end
134
134
  ```
@@ -139,7 +139,7 @@ end
139
139
 
140
140
  `depends_on` declares what must run before a task. Each dependency runs at most once per `asgard` invocation regardless of how many tasks declare it. Circular dependencies are caught at startup.
141
141
 
142
- `desc` and `depends_on` are independent — either can come first, both must appear before `def`. `var` declarations between `depends_on` and `def` are safe and do not consume the pending dependency.
142
+ `desc` and `depends_on` are independent — either can come first, both must appear before `def`.
143
143
 
144
144
  ### Sequential dependencies
145
145
 
@@ -147,15 +147,15 @@ Bare symbols run one after another in the order declared:
147
147
 
148
148
  ```ruby
149
149
  class Tasks
150
- desc "build", "Compile the project"
150
+ desc "Compile the project"
151
151
  def build = sh "rake build"
152
152
 
153
153
  depends_on :build
154
- desc "test", "Run the test suite"
154
+ desc "Run the test suite"
155
155
  def test = sh "rake test"
156
156
 
157
157
  depends_on :test
158
- desc "release", "Publish the gem"
158
+ desc "Publish the gem"
159
159
  def release = sh "bundle exec rake release"
160
160
  end
161
161
  ```
@@ -170,15 +170,15 @@ Wrap symbols in an array to declare they can run concurrently. Asgard waits for
170
170
 
171
171
  ```ruby
172
172
  class Tasks
173
- desc "lint", "Check code style"
173
+ desc "Check code style"
174
174
  def lint = sh "bundle exec rubocop"
175
175
 
176
- desc "typecheck", "Run type checks"
176
+ desc "Run type checks"
177
177
  def typecheck = sh "bundle exec srb tc"
178
178
 
179
179
  # lint and typecheck run in parallel, test waits for both
180
180
  depends_on [:lint, :typecheck]
181
- desc "test", "Run the test suite"
181
+ desc "Run the test suite"
182
182
  def test = sh "bundle exec rake test"
183
183
  end
184
184
  ```
@@ -193,16 +193,16 @@ Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` cal
193
193
 
194
194
  ```ruby
195
195
  class Tasks
196
- desc "setup", "Install dependencies"; def setup = sh "bundle install"
197
- desc "lint", "Check code style"; def lint = sh "bundle exec rubocop"
198
- desc "build", "Compile assets"; def build = sh "rake assets:precompile"
199
- desc "test", "Run tests"; def test = sh "bundle exec rake test"
200
- desc "notify", "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
196
+ desc "Install dependencies"; def setup = sh "bundle install"
197
+ desc "Check code style"; def lint = sh "bundle exec rubocop"
198
+ desc "Compile assets"; def build = sh "rake assets:precompile"
199
+ desc "Run tests"; def test = sh "bundle exec rake test"
200
+ desc "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
201
201
 
202
202
  # setup first, then lint+build in parallel, then test, then notify
203
203
  depends_on :setup, [:lint, :build], :test, :notify
204
- desc "ci", "Full CI pipeline"
205
- def ci = sh "echo 'CI complete'"
204
+ desc "Full CI pipeline"
205
+ def ci = puts "CI complete"
206
206
  end
207
207
  ```
208
208
 
@@ -224,18 +224,24 @@ asgard ci executes:
224
224
 
225
225
  ## Variables
226
226
 
227
- `var` declares a named value available to all tasks as a method. Pass a lambda for lazy evaluation it is called once on first use:
227
+ Shared configuration values are declared as Ruby class variables (`@@name`) at the top of the class body. Use `||=` so the first declaration wins when multiple `.loki` files reopen `Tasks`, and `.freeze` to prevent mutation:
228
228
 
229
229
  ```ruby
230
230
  class Tasks
231
- var :app, "myapp"
232
- var :version, -> { `git describe --tags`.strip }
231
+ @@app ||= "myapp".freeze
232
+ @@max_jobs ||= 4
233
233
 
234
- desc "tag", "Create a release tag"
235
- def tag = sh "git tag #{app}-#{version}"
234
+ desc "Create a release tag"
235
+ def tag = sh "git tag #{@@app}-#{version}"
236
+
237
+ private
238
+
239
+ def version = `git describe --tags`.strip
236
240
  end
237
241
  ```
238
242
 
243
+ Class variables are visible in all task instance methods and in subcommand subclasses — unlike class instance variables (`@name`), which are not accessible inside instance methods.
244
+
239
245
  ---
240
246
 
241
247
  ## Helper methods
@@ -244,13 +250,13 @@ Private methods are callable from any task in the same class but are never regis
244
250
 
245
251
  ```ruby
246
252
  class Tasks
247
- desc "build", "Compile and package"
253
+ desc "Compile and package"
248
254
  def build
249
255
  compile("src")
250
256
  package(version)
251
257
  end
252
258
 
253
- desc "release", "Build and publish"
259
+ desc "Build and publish"
254
260
  def release
255
261
  build
256
262
  sh "gem push pkg/myapp-#{version}.gem"
@@ -286,7 +292,7 @@ require_relative "shared/helpers"
286
292
  class Tasks
287
293
  include BuildHelpers
288
294
 
289
- desc "build", "Compile the project"
295
+ desc "Compile the project"
290
296
  def build = compile("src")
291
297
  end
292
298
  ```
@@ -320,7 +326,7 @@ end
320
326
 
321
327
  ```ruby
322
328
  class Tasks
323
- desc "setup", "Bootstrap the development environment"
329
+ desc "Bootstrap the development environment"
324
330
  def setup
325
331
  sh <<~SHELL
326
332
  brew install redis postgresql
@@ -330,7 +336,7 @@ class Tasks
330
336
  SHELL
331
337
  end
332
338
 
333
- desc "analyze", "Run Python data analysis"
339
+ desc "Run Python data analysis"
334
340
  def analyze
335
341
  shebang :python3, <<~PYTHON
336
342
  import json
@@ -339,7 +345,7 @@ class Tasks
339
345
  PYTHON
340
346
  end
341
347
 
342
- desc "bundle_assets", "Build frontend assets with esbuild"
348
+ desc "Build frontend assets with esbuild"
343
349
  def bundle_assets
344
350
  shebang :node, <<~JS
345
351
  const esbuild = require("esbuild")
@@ -365,18 +371,31 @@ PY
365
371
 
366
372
  ## Environment variables
367
373
 
368
- `dotenv` loads a `.env` file into the environment before tasks run:
374
+ `dotenv` loads a `.env` file into the environment before tasks run. Use the `env` Kernel method to read environment variables inside task bodies — it accepts a symbol or string and upcases the key automatically:
369
375
 
370
376
  ```ruby
371
377
  class Tasks
372
378
  dotenv # loads .env
373
379
  dotenv ".env.local" # or a specific file
374
380
 
375
- desc "check", "Print the app name from .env"
376
- def check = sh "echo $APP_NAME"
381
+ desc "Start the server"
382
+ def start
383
+ sh "puma -p #{env(:port, '3000')} -e #{env(:rack_env, 'development')}"
384
+ end
385
+
386
+ desc "Deploy the app"
387
+ def deploy
388
+ sh "cap #{env(:deploy_target)} deploy" # raises KeyError if DEPLOY_TARGET is unset
389
+ end
377
390
  end
378
391
  ```
379
392
 
393
+ | Call | Behaviour |
394
+ |------|-----------|
395
+ | `env(:port, "3000")` | Returns `"3000"` when `PORT` is unset |
396
+ | `env(:api_key)` | Raises `KeyError` when `API_KEY` is missing |
397
+ | `env("DATABASE_URL")` | String name works too — always upcased |
398
+
380
399
  ---
381
400
 
382
401
  ## Command aliases
@@ -389,7 +408,7 @@ class Tasks
389
408
  map "--v" => "version"
390
409
  map "t" => "test"
391
410
 
392
- desc "version", "Print the version"
411
+ desc "Print the version"
393
412
  def version = puts Asgard::VERSION
394
413
  end
395
414
  ```
@@ -402,10 +421,10 @@ Group related tasks under a common name using Thor's `subcommand` method. Define
402
421
 
403
422
  ```ruby
404
423
  class DeployCommands < Tasks
405
- desc "staging", "Deploy to staging"
424
+ desc "Deploy to staging"
406
425
  def staging = sh "cap staging deploy"
407
426
 
408
- desc "production", "Deploy to production"
427
+ desc "Deploy to production"
409
428
  def production = sh "cap production deploy"
410
429
  end
411
430
 
@@ -421,20 +440,20 @@ asgard deploy staging
421
440
  asgard deploy production
422
441
  ```
423
442
 
424
- Subcommand tasks have all the same access to helper methods like `sh`, `shebang`, `depends_on`, `var`, and the built-in `--debug`/`--verbose` class options as normal tasks.
443
+ Subcommand tasks have all the same access to `sh`, `shebang`, `depends_on`, and the built-in `--debug`/`--verbose` class options as normal tasks. `@@` class variables declared on `Tasks` are also visible in subcommand subclasses.
425
444
 
426
445
  `depends_on` only works within a subcommand group exactly as it does at the top level:
427
446
 
428
447
  ```ruby
429
448
  class DBCommands < Tasks
430
- desc "migrate", "Run pending migrations"
449
+ desc "Run pending migrations"
431
450
  def migrate = sh "rails db:migrate"
432
451
 
433
- desc "seed", "Load seed data"
452
+ desc "Load seed data"
434
453
  def seed = sh "rails db:seed"
435
454
 
436
455
  depends_on :migrate, :seed
437
- desc "reset", "Migrate then seed"
456
+ desc "Migrate then seed"
438
457
  def reset = puts "Done."
439
458
  end
440
459
 
@@ -470,7 +489,7 @@ Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `
470
489
 
471
490
  ## Task files
472
491
 
473
- Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. `*.loki` files in the same directory are loaded only when `asgard` is invoked with `--auto-load`.
492
+ Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. Additional `*.loki` files are loaded only when your `.loki` file explicitly calls `import`.
474
493
 
475
494
  ### Single file
476
495
 
@@ -485,16 +504,23 @@ Split tasks across files — each reopens `class Tasks`:
485
504
 
486
505
  ```
487
506
  myproject/
488
- .loki ← entry point, can be empty
507
+ .loki ← entry point; calls import to load task files
489
508
  build.loki
490
509
  deploy.loki
491
510
  test.loki
492
511
  ```
493
512
 
513
+ The `.loki` entry file must call `import` to load the other task files. It also marks the project root for auto-discovery:
514
+
515
+ ```ruby
516
+ # .loki
517
+ import "*.loki"
518
+ ```
519
+
494
520
  ```ruby
495
521
  # build.loki
496
522
  class Tasks
497
- desc "build", "Compile the project"
523
+ desc "Compile the project"
498
524
  def build = sh "rake build"
499
525
  end
500
526
  ```
@@ -503,7 +529,7 @@ end
503
529
  # test.loki
504
530
  class Tasks
505
531
  depends_on :build
506
- desc "test", "Run the test suite"
532
+ desc "Run the test suite"
507
533
  def test = sh "bundle exec rake test"
508
534
  end
509
535
  ```
@@ -512,21 +538,21 @@ end
512
538
  # deploy.loki
513
539
  class Tasks
514
540
  depends_on :test
515
- desc "deploy", "Deploy to production"
541
+ desc "Deploy to production"
516
542
  def deploy = sh "cap production deploy"
517
543
  end
518
544
  ```
519
545
 
520
- The `.loki` entry point can be completely empty — it only needs to exist to mark the project root.
546
+ In a single-file project, `.loki` can be completely empty — its presence alone marks the project root. In a multi-file project, add at least an `import` call.
521
547
 
522
548
  ### Explicit loading
523
549
 
524
- Load any Ruby or `.loki` file manually from `.loki`:
550
+ Load any Ruby or `.loki` file manually from `.loki`. Use `require_relative` for plain Ruby; use `import` for `.loki` files (it enforces the extension and is idempotent):
525
551
 
526
552
  ```ruby
527
553
  # .loki
528
554
  require_relative "shared/helpers"
529
- require_relative "ci.loki"
555
+ import "ci.loki"
530
556
 
531
557
  class Tasks
532
558
  # additional tasks
@@ -541,7 +567,6 @@ end
541
567
  |---|---|
542
568
  | `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
543
569
  | `Asgard.find_task_file` | Returns path to `.loki` searching from CWD upward, or nil |
544
- | `Asgard.load_loki(dir)` | Loads all `*.loki` files in dir alphabetically — called by `run!` only when `--auto-load` is passed |
545
570
 
546
571
  `run!` handles its own errors — a missing `.loki`, a circular dependency, or a `depends_on` that names a task that doesn't exist all produce a clean one-line message and exit 1.
547
572
 
data/Rakefile CHANGED
@@ -3,7 +3,7 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
5
 
6
- SIMPLECOV_PRELUDE = <<~RUBY.freeze
6
+ SIMPLECOV_PRELUDE = <<~RUBY
7
7
  require "simplecov"
8
8
  SimpleCov.start do
9
9
  add_filter "/test/"
@@ -15,8 +15,87 @@ Minitest::TestTask.create do |t|
15
15
  t.test_prelude = SIMPLECOV_PRELUDE
16
16
  end
17
17
 
18
- task quality: :test do
19
- sh "flog lib/"
18
+ task default: :test
19
+
20
+ RUBOCOP_ENV = { "RUBOCOP_CACHE_ROOT" => "tmp/rubocop_cache" }.freeze
21
+
22
+ desc "Check code style with RuboCop"
23
+ task :rubocop do
24
+ sh RUBOCOP_ENV, "bundle exec rubocop"
20
25
  end
21
26
 
22
- task default: :test
27
+ desc "Auto-correct RuboCop offenses"
28
+ task :rubocop_fix do
29
+ sh RUBOCOP_ENV, "bundle exec rubocop -a"
30
+ end
31
+
32
+ desc "Check code complexity with Flog (warn >=20, fail >=50)"
33
+ task :flog_check do
34
+ require "flog"
35
+
36
+ # Target to work toward; methods above this are warned but don't fail the gate.
37
+ METHOD_WARN = 20.0
38
+ # Current baseline floor — established from first run. Reduce incrementally.
39
+ METHOD_FAIL = 50.0
40
+
41
+ flogger = Flog.new(all: true)
42
+ flogger.flog(*Dir.glob("lib/**/*.rb"))
43
+
44
+ warnings = []
45
+ failures = []
46
+
47
+ flogger.each_by_score do |method, score|
48
+ next if method.end_with?("#none")
49
+ if score > METHOD_FAIL
50
+ failures << "#{"%.1f" % score}: #{method}"
51
+ elsif score > METHOD_WARN
52
+ warnings << "#{"%.1f" % score}: #{method}"
53
+ end
54
+ end
55
+
56
+ unless warnings.empty?
57
+ puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
58
+ warnings.each { |v| puts " #{v}" }
59
+ end
60
+
61
+ if failures.empty?
62
+ puts "\nFlog: no methods exceed the failure threshold (>=#{METHOD_FAIL})"
63
+ else
64
+ puts "\nFlog failures (>=#{METHOD_FAIL}) — must be refactored:"
65
+ failures.each { |v| puts " #{v}" }
66
+ $stdout.flush
67
+ abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
68
+ end
69
+ end
70
+
71
+ desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
72
+ task :quality do
73
+ results = {}
74
+
75
+ puts "\n#{"=" * 60}"
76
+ puts "Quality Gate: Tests + Coverage"
77
+ puts "=" * 60
78
+ results[:tests] = system("bundle exec rake test") ? :pass : :fail
79
+
80
+ puts "\n#{"=" * 60}"
81
+ puts "Quality Gate: RuboCop"
82
+ puts "=" * 60
83
+ results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
84
+
85
+ puts "\n#{"=" * 60}"
86
+ puts "Quality Gate: Flog Complexity"
87
+ puts "=" * 60
88
+ results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
89
+
90
+ puts "\n#{"=" * 60}"
91
+ puts "Quality Summary"
92
+ puts "=" * 60
93
+ results.each do |gate, status|
94
+ icon = status == :pass ? "PASS" : "FAIL"
95
+ puts " [#{icon}] #{gate}"
96
+ end
97
+ puts "=" * 60
98
+
99
+ abort "\nQuality gate failed" if results.values.any?(:fail)
100
+ puts "\nAll quality gates passed."
101
+ end
data/docs/api.md CHANGED
@@ -12,7 +12,6 @@ These class methods are defined on the `Asgard` module itself.
12
12
  |---|---|---|
13
13
  | `run!` | `Asgard.run!(argv)` | Main entry point. Finds `.loki`, loads all task files, validates the dependency graph, and dispatches via Thor. Handles its own errors: missing `.loki` and circular dependencies both produce a clean one-line message and `exit 1`. |
14
14
  | `find_task_file` | `Asgard.find_task_file → String, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns the absolute path string of the first match, or `nil` if none is found. |
15
- | `load_loki` | `Asgard.load_loki(dir)` | Loads all `*.loki` files in `dir` alphabetically, excluding `.loki` itself. Called by `run!` only when `--auto-load` is present in `argv`. |
16
15
 
17
16
  ### `run!` Details
18
17
 
@@ -30,6 +29,82 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
30
29
 
31
30
  ---
32
31
 
32
+ ## Kernel Methods
33
+
34
+ These methods are defined as `module_function` on `Kernel` and are therefore available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. No `require` or `include` is needed; they are loaded when `asgard` starts.
35
+
36
+ | Method | Signature | Returns | Description |
37
+ |---|---|---|---|
38
+ | `loki_up` | `loki_up(name = ".loki") → String, nil` | Absolute path or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns the first match's absolute path, or `nil` if not found. Exact filenames only — does not expand globs. |
39
+ | `import` | `import(path) → true, false` | `true` if any file newly loaded | Loads one `.loki` file or a glob of `.loki` files. Relative paths resolve relative to the caller's file (like `require_relative`). Idempotent via `$LOADED_FEATURES`. Raises `ArgumentError` if `path` does not end with `.loki`. Raises `LoadError` if a non-glob path does not exist. |
40
+ | `import_up` | `import_up(name = ".loki") → true, false` | `true` if any file newly loaded | Combines `loki_up` and `import`. Walks ancestors to find the file or glob match, then loads it. Returns `false` if nothing is found. |
41
+ | `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
42
+ | `verbose?` | `verbose? → true, false` | `$VERBOSE` | Returns the current value of `$VERBOSE`. Set to `true` by `--verbose` on the CLI or directly via `$VERBOSE = true`. |
43
+ | `env` | `env(name, default = nil) → String, nil` | `ENV` value or default | Fetches an environment variable by symbol or string name. The name is upcased automatically. Raises `KeyError` when the variable is missing and no default is given. |
44
+
45
+ ### `loki_up` Details
46
+
47
+ Despite the name, `loki_up` is not limited to `.loki` files — it locates any file by walking up the directory tree:
48
+
49
+ ```ruby
50
+ loki_up # find .loki (the project root marker)
51
+ loki_up("gem_tasks.loki") # find gem_tasks.loki in CWD or any ancestor
52
+ loki_up(".env") # find the nearest .env file up the tree
53
+ loki_up("VERSION") # find a VERSION file in CWD or any ancestor
54
+ ```
55
+
56
+ Returns an absolute path string or `nil`. Does not load the file.
57
+
58
+ ```ruby
59
+ if (path = loki_up("gem_tasks.loki"))
60
+ import path
61
+ end
62
+
63
+ # Pass the located .env to dotenv — works from any subdirectory
64
+ dotenv loki_up(".env") || ".env"
65
+ ```
66
+
67
+ ### `import` Details
68
+
69
+ ```ruby
70
+ import "build.loki" # relative — resolved from the calling file's directory
71
+ import "/home/shared/gem_tasks.loki" # absolute
72
+ import "*.loki" # all *.loki in the same directory as the caller
73
+ import "../shared/*.loki" # all *.loki one level up
74
+ import "**/*.loki" # all *.loki recursively
75
+ import Pathname.new("tasks.loki") # Pathname accepted
76
+ ```
77
+
78
+ **Extension requirement:** the path (or glob pattern) must end with `.loki`. Passing any other extension raises `ArgumentError`.
79
+
80
+ **Glob behaviour:** `Dir.glob` is used for pattern expansion. `*.loki` does not match `.loki` (the dotfile) — Ruby's glob excludes dotfiles from `*` by default. Files are loaded in the order `Dir.glob` returns them (sorted on Ruby ≥ 2.7).
81
+
82
+ **Idempotency:** each resolved absolute path is checked against `$LOADED_FEATURES` before loading. A file already in `$LOADED_FEATURES` is silently skipped and contributes `false` to the return value.
83
+
84
+ **Return value:** `true` if at least one file was newly loaded; `false` if all matched files were already loaded or no glob pattern produced any matches.
85
+
86
+ **Verbose/debug output** (to stderr):
87
+ - `verbose?` true — prints each file path as it is loaded
88
+ - `debug?` true — also prints a skip message for each already-loaded file
89
+
90
+ ### `import_up` Details
91
+
92
+ ```ruby
93
+ import_up # find and load .loki
94
+ import_up "gem_tasks.loki" # find and load gem_tasks.loki up the tree
95
+ import_up "*.loki" # find the nearest ancestor with *.loki files and load them all
96
+ ```
97
+
98
+ **Exact name:** delegates to `loki_up` to find the file, then calls `import` with the absolute path. Returns `false` without raising if the file is not found.
99
+
100
+ **Glob name:** walks ancestor directories manually using `Dir.glob`. Stops at the **first** ancestor that has any matches and loads all of them — it does not continue walking after finding a match. Returns `false` if no ancestor contains matching files.
101
+
102
+ **Verbose/debug output** (to stderr):
103
+ - `verbose?` true — prints `name → /full/path` when a file or directory is found
104
+ - `debug?` true — also prints `name not found` when the search comes up empty
105
+
106
+ ---
107
+
33
108
  ## `Asgard::Base` DSL Class Methods
34
109
 
35
110
  `Asgard::Base` is a `Thor` subclass that provides the task DSL. It is the superclass of `Tasks`. All DSL methods are class methods (called in the class body).
@@ -37,8 +112,6 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
37
112
  | Method | Signature | Description |
38
113
  |---|---|---|
39
114
  | `depends_on` | `depends_on(*tasks)` | Declare prerequisites for the next `def`. Bare symbols run sequentially; arrays within the splat run as a parallel group. |
40
- | `var` | `var(name, value = nil, &block)` | Declare a named variable. If `value` responds to `call` (lambda/proc) or a block is given, the value is computed lazily on first access. Accessible in task bodies as a method. |
41
- | `import` | `import(mod)` | Include a module into the current class (thin alias for `include`). |
42
115
  | `dotenv` | `dotenv(path = ".env")` | Load the specified `.env` file into `ENV` using the dotenv gem. Silently skipped if the file does not exist. Called at class-load time. |
43
116
  | `sh` | `sh(script, silent: false)` | Instance method. Run a shell command or multiline heredoc. Single-line → `system(script)`; multiline → `system("bash", "-c", script)`. Exits with the command's status on failure. |
44
117
  | `shebang` | `shebang(interpreter, script, silent: false)` | Instance method. Write `script` to a tempfile and execute it with `interpreter`. See the [Shell Helpers](shell.md) page for the full interpreter table. |
@@ -65,9 +138,8 @@ depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently
65
138
  | `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
66
139
  | `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
67
140
  | `_version` | private task method | Implements `--version`. Prints `Asgard::VERSION` and exits. Registered via `map "--version" => :_version`. Uses `_` prefix convention. |
68
- | `debug?` | private instance method | Returns `$DEBUG`. Available in all task bodies and subcommand classes that inherit from `Tasks`. |
69
- | `verbose?` | private instance method | Returns `$VERBOSE`. Available in all task bodies and subcommand classes that inherit from `Tasks`. |
70
- | `--auto-load` | CLI flag (consumed by `run!`) | Triggers loading of all `*.loki` files before the main `.loki` and the requested task. Consumed by `run!` before Thor dispatch. |
141
+ | `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
142
+ | `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
71
143
 
72
144
  ---
73
145
 
@@ -78,9 +150,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
78
150
  | Method | Description |
79
151
  |---|---|
80
152
  | `_deps` | Hash mapping task name symbols to their stage arrays. Set by `depends_on` + `method_added`. |
81
- | `_vars` | Hash mapping var name symbols to their static values or callables. |
82
- | `_ran_tasks` | `Set` of task name symbols that have already run in the current invocation. |
83
- | `_ran_mutex` | `Mutex` protecting `_ran_tasks` for thread-safe deduplication. |
153
+ | `_done` | `Set` of task name symbols that have completed in the current invocation. |
154
+ | `_running` | `Set` of task name symbols currently executing (started but not yet finished). |
155
+ | `_cond` | Hash of `ConditionVariable` objects keyed by task name; threads wait here when a dep is in-flight. |
156
+ | `_ran_mutex` | `Mutex` protecting `_done`, `_running`, and `_cond` for thread-safe access. |
84
157
  | `_build_dep_graph(stages)` | Translates the stage array (from `_deps`) into a Dagwood-compatible hash. |
85
158
 
86
159
  ---
@@ -90,10 +163,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
90
163
  `Asgard::Base` overrides Thor's `invoke_command` to implement dependency resolution and deduplication:
91
164
 
92
165
  1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
93
- 2. Checks `_ran_tasks` skips if this task has already run.
94
- 3. Marks the task as ran.
95
- 4. Resolves dependency stages from `_deps`, builds the Dagwood graph, and executes groups (parallel groups in threads, sequential groups one at a time).
96
- 5. Calls `command.run(self, *args)` to execute the task itself.
166
+ 2. Tries to acquire a run token (`acquire_run_token`): if the task is already in `_done`, returns immediately (skip); if it is in `_running`, waits on the `_cond` ConditionVariable until it finishes, then returns (skip); otherwise adds the task to `_running` and continues.
167
+ 3. Resolves dependency stages from `_deps`, builds the Dagwood graph, and executes groups (parallel groups in threads, sequential groups one at a time).
168
+ 4. Calls `command.run(self, *args)` to execute the task itself.
169
+ 5. In an `ensure` block, adds the task to `_done` and broadcasts on its `_cond` to wake any waiting threads.
97
170
 
98
171
  ---
99
172
 
data/docs/changelog.md CHANGED
@@ -8,6 +8,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asg
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Removed
12
+
13
+ - **`var` DSL method** — replaced by 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. See [Variables](variables.md).
14
+
11
15
  ## [0.2.0] — 2026-05-29
12
16
 
13
17
  ### Changed