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
data/README.md CHANGED
@@ -3,6 +3,9 @@
3
3
  > [!INFO]
4
4
  > See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory](examples/) contains working `.loki` files demonstrating the full feature set.
5
5
 
6
+ > [!WARNING]
7
+ > Version 0.6.0 has breaking changes to scheduled tasks, watchers and `sh`. Read [Breaking Changes](https://madbomber.github.io/asgard/breaking-changes/) before you upgrade.
8
+
6
9
  <br>
7
10
  <table>
8
11
  <tr>
@@ -13,27 +16,30 @@
13
16
  <td width="60%" valign="top">
14
17
  <strong>Key Features</strong><br>
15
18
 
16
- - <strong>Thor-Powered CLI</strong> — every Thor DSL feature available inside <code>.loki</code> task files<br>
17
- - <strong>Task Dependencies</strong> — sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
18
- - <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
19
- - <strong>Subcommands</strong> — group related tasks under a named namespace<br>
20
- - <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
21
- - <strong>`helper` DSL</strong> — define a method once, available in both class-level DSL calls (<code>header</code>) and inside task instance methods<br>
22
- - <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
23
- - <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
24
- - <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
25
- - <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
26
- - <strong>Scheduled Tasks</strong> — run any task periodically under launchd (macOS) or systemd timers (Linux) with <code>schedule</code> and <code>asgard schedule install</code><br>
27
- - <strong>Watched Files</strong> — run any task when files change with <code>watch</code> and <code>asgard watcher start</code>; the task reads what changed through <code>changes</code><br>
28
- - <strong>Built-in Flags</strong> — <code>--debug</code>, <code>--verbose</code>, <code>--version</code>, and <code>--doctor</code> built-in class options; header/footer DSL for static help text<br>
19
+ - <strong>Thor-Powered CLI</strong>: every Thor DSL feature available inside <code>.loki</code> task files<br>
20
+ - <strong>Task Dependencies</strong>: sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
21
+ - <strong>Concurrent Execution</strong>: parallel task groups run in native Ruby threads<br>
22
+ - <strong>Subcommands</strong>: group related tasks under a named namespace<br>
23
+ - <strong>Variables</strong>: shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
24
+ - <strong>`helper` DSL</strong>: define a method once, available in both class-level DSL calls (<code>header</code>) and inside task instance methods<br>
25
+ - <strong>Shell Helpers</strong>: <code>sh</code> (alias <code>shebang</code>) runs a command or heredoc under the default shell, or under any registered interpreter when you name one<br>
26
+ - <strong>Dotenv Support</strong>: load <code>.env</code> files into the environment with <code>dotenv</code><br>
27
+ - <strong>Auto-Discovery</strong>: <code>.loki</code> root marker searched from CWD upward through parent directories<br>
28
+ - <strong>Multi-File Tasks</strong>: split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
29
+ - <strong>Scheduled Tasks</strong>: run any task periodically under launchd (macOS) or systemd timers (Linux) with <code>schedule</code> and <code>asgard schedule install</code><br>
30
+ - <strong>Watched Files</strong>: run any task when files change with <code>watch</code> and <code>asgard watcher start</code>; the task reads what changed through <code>changes</code><br>
31
+ - <strong>Background Jobs</strong>: <code>sh "mkdocs serve", background: true</code> starts a job that outlives asgard; <code>asgard jobs</code> lists, stops, restarts and cleans them<br>
32
+ - <strong>Built-in Flags</strong>: <code>--debug</code>, <code>--verbose</code>, <code>--version</code>, and <code>--doctor</code> built-in class options; header/footer DSL for static help text<br>
29
33
  </td>
30
34
  </tr>
31
35
  </table>
32
36
 
33
- <p>Asgard is a <a href="https://github.com/rails/thor">Thor</a>-based task runner for Ruby projects. Define tasks in <code>.loki</code> files, declare dependencies between them, and let Asgard handle ordering and concurrent execution. Anything Thor can do — subcommands, typed options, argument validation — is available inside a <code>.loki</code> file.</p>
37
+ <p>Asgard is a <a href="https://github.com/rails/thor">Thor</a>-based task runner for Ruby projects. Define tasks in <code>.loki</code> files, declare dependencies between them, and let Asgard handle ordering and concurrent execution. Anything Thor can do (subcommands, typed options, argument validation) is available inside a <code>.loki</code> file.</p>
34
38
 
35
39
  ## Installation
36
40
 
41
+ Asgard requires Ruby 4.0 or later. Scheduled tasks and the file watcher need macOS or Linux, background jobs need a POSIX system (`fork`, `setsid` and `/bin/sh`), and installing compiles one native extension, so a C++17 compiler must be available (Xcode's command-line tools or a distribution's `build-essential` package).
42
+
37
43
  ```bash
38
44
  gem install asgard
39
45
  ```
@@ -48,7 +54,7 @@ bundle add asgard
48
54
 
49
55
  ## Tasks
50
56
 
51
- Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` class is pre-defined by the gem — just reopen it and add methods.
57
+ Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` class is pre-defined by the gem; just reopen it and add methods.
52
58
 
53
59
  ### A task with no parameters
54
60
 
@@ -81,7 +87,7 @@ asgard hello Alice
81
87
 
82
88
  ### A task with a formal argument declaration
83
89
 
84
- Use `argument` for richer metadata — type checking, enums, and help text. **Warning: `argument` is a class-level declaration that applies to every task in the class**, not just the one below it. It is best suited for single-command CLIs or when every task genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
90
+ Use `argument` for richer metadata: type checking, enums, and help text. **Warning: `argument` is a class-level declaration that applies to every task in the class**, not just the one below it. It is best suited for single-command CLIs or when every task genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
85
91
 
86
92
  ```ruby
87
93
  class Tasks
@@ -142,7 +148,7 @@ end
142
148
 
143
149
  `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.
144
150
 
145
- `desc` and `depends_on` are independent — either can come first, both must appear before `def`.
151
+ `desc` and `depends_on` are independent: either can come first, but both must appear before `def`.
146
152
 
147
153
  ### Sequential dependencies
148
154
 
@@ -192,7 +198,7 @@ asgard test # lint ∥ typecheck → test
192
198
 
193
199
  ### Mixed sequential and parallel
194
200
 
195
- Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` call. Execution proceeds stage by stage — each stage must complete before the next begins:
201
+ Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` call. Execution proceeds stage by stage; each stage must complete before the next begins:
196
202
 
197
203
  ```ruby
198
204
  class Tasks
@@ -223,7 +229,7 @@ asgard ci executes:
223
229
  ci
224
230
  ```
225
231
 
226
- `depends_on` also accepts a `Proc`/lambda (or, equivalently, a block) instead of a fixed list, resolved once every `.loki` file has finished loading rather than immediately — useful when the list can't be known upfront, e.g. "every task whose name ends in `_check`," discovered across several files:
232
+ `depends_on` also accepts a `Proc`/lambda (or, equivalently, a block) instead of a fixed list, resolved once every `.loki` file has finished loading rather than immediately. This is useful when the list can't be known upfront, e.g. "every task whose name ends in `_check`," discovered across several files:
227
233
 
228
234
  ```ruby
229
235
  depends_on -> { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
@@ -232,7 +238,7 @@ def quality = puts "running every *_check task..."
232
238
  depends_on { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
233
239
  def quality2 = puts "same, as a block..."
234
240
 
235
- # do...end for a block spanning multiple statements — braces are for
241
+ # do...end for a block spanning multiple statements; braces are for
236
242
  # single-line blocks like the two above. The last expression is still what
237
243
  # gets returned and validated.
238
244
  depends_on do
@@ -244,7 +250,7 @@ end
244
250
  def quality3 = puts "same, minus the slow checks, as a multi-line block..."
245
251
  ```
246
252
 
247
- See [Dependencies](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies-proc-block-form) for the full explanation.
253
+ See [Dependencies](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies) for the full explanation.
248
254
 
249
255
  ---
250
256
 
@@ -266,13 +272,13 @@ class Tasks
266
272
  end
267
273
  ```
268
274
 
269
- 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.
275
+ 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.
270
276
 
271
277
  ---
272
278
 
273
279
  ## Helper methods
274
280
 
275
- Private methods are callable from any task in the same class but are never registered as commands — they won't appear in `--help` output and can't be invoked from the CLI.
281
+ Private methods are callable from any task in the same class but are never registered as commands: they won't appear in `--help` output and can't be invoked from the CLI.
276
282
 
277
283
  ```ruby
278
284
  class Tasks
@@ -297,6 +303,8 @@ class Tasks
297
303
  def package(ver)
298
304
  sh "tar czf pkg/myapp-#{ver}.tar.gz bin/"
299
305
  end
306
+
307
+ def version = File.read("VERSION").strip
300
308
  end
301
309
  ```
302
310
 
@@ -328,7 +336,7 @@ def self.version = File.read(...).match(...)[1].freeze
328
336
  no_commands { private def version = self.class.version }
329
337
  ```
330
338
 
331
- `helper` accepts positional arguments, keyword arguments, and blocks — any signature valid in a Ruby method definition:
339
+ `helper` accepts positional arguments, keyword arguments, and blocks: any signature valid in a Ruby method definition:
332
340
 
333
341
  ```ruby
334
342
  helper(:tag) { |name, ver, prefix: "v"| "#{prefix}#{name}-#{ver}" }
@@ -365,7 +373,7 @@ Add static text above and below the command list in `asgard help` output:
365
373
 
366
374
  ```ruby
367
375
  class Tasks
368
- header "my-project — build & release tasks"
376
+ header "my-project: build & release tasks"
369
377
  footer "See https://example.com/docs for details"
370
378
  end
371
379
  ```
@@ -389,7 +397,10 @@ end
389
397
  ```
390
398
 
391
399
  ```
400
+ asgard v0.6.0 The Mighty Thor and Loki working for you
401
+
392
402
  my-project
403
+
393
404
  deploy targets: staging, production
394
405
 
395
406
  Commands:
@@ -399,7 +410,12 @@ Options:
399
410
  ...
400
411
 
401
412
  See runbook at wiki/deploy
413
+
402
414
  Maintainer: you@example.com
415
+
416
+
417
+ Documentation ... https://madbomber.github.io/asgard
418
+ Github Repo ..... https://github.com/MadBomber/asgard
403
419
  ```
404
420
 
405
421
  Header and footer text is only shown for the general `asgard help` page, not for `asgard help <command>`.
@@ -429,7 +445,7 @@ end
429
445
 
430
446
  ## Shell helpers
431
447
 
432
- `sh` runs any shell command or multiline heredoc. `shebang` writes a script body to a tempfile and executes it with the given interpreter. Both exit with the command's status code on failure.
448
+ `sh` writes a script body to a tempfile and runs it with a registered interpreter; given a script alone it uses the `default_shell` (bash), for one line or a heredoc. `shebang` is the same method under a longer name, which many authors reserve for scripts in another language. Both return `true` and exit with the command's status code on failure.
433
449
 
434
450
  ```ruby
435
451
  class Tasks
@@ -462,7 +478,18 @@ class Tasks
462
478
  end
463
479
  ```
464
480
 
465
- Supported interpreters: `:python3`, `:python`, `:node`, `:ruby`, `:perl`, `:bash`, `:sh`. Any other symbol is passed directly to `system` with a `.tmp` extension.
481
+ Registered shells: `:bash`, `:sh`, `:python3`, `:python`, `:node`, `:ruby`, `:perl`. Any other symbol runs as itself from `PATH` with a `.tmp` extension, except zsh, fish, csh and tcsh, which get `.sh`, `.fish` and `.csh`. Register your own, with a pinned path or options in front of the script, with `shell`:
482
+
483
+ ```ruby
484
+ class Tasks
485
+ shell :bash, "/opt/homebrew/bin/bash" # what sh and shebang :bash run
486
+ shell :strict, "bash -eu -o pipefail"
487
+ shell :uv, "uv run --script", ext: ".py"
488
+ default_shell :strict # what sh and a nameless shebang run
489
+ end
490
+ ```
491
+
492
+ The name may be left out: `shell "/usr/local/bin/bash -c -w"` registers `:bash`, named after the executable, with the options as its `args`. `default_shell` registers and selects in one line the same way: `default_shell "/usr/local/bin/bash -c -w"`, or `default_shell :strict, "bash", args: "-eu -o pipefail"`. A bare `default_shell :bash` only selects and leaves a pinned entry alone.
466
493
 
467
494
  Pass `silent: true` to both `sh` and `shebang` to suppress the script echo:
468
495
 
@@ -478,7 +505,7 @@ PY
478
505
 
479
506
  ## Environment variables
480
507
 
481
- `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:
508
+ `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:
482
509
 
483
510
  ```ruby
484
511
  class Tasks
@@ -501,13 +528,13 @@ end
501
528
  |------|-----------|
502
529
  | `env(:port, "3000")` | Returns `"3000"` when `PORT` is unset |
503
530
  | `env(:api_key)` | Raises `KeyError` when `API_KEY` is missing |
504
- | `env("DATABASE_URL")` | String name works too — always upcased |
531
+ | `env("DATABASE_URL")` | String name works too; always upcased |
505
532
 
506
533
  ---
507
534
 
508
535
  ## Abbreviated command matching
509
536
 
510
- Every task is a Thor command, so you don't have to type the full name — Thor resolves any unambiguous prefix automatically, with no extra code:
537
+ Every task is a Thor command, so you don't have to type the full name: Thor resolves any unambiguous prefix automatically, with no extra code:
511
538
 
512
539
  ```ruby
513
540
  class Tasks
@@ -523,12 +550,12 @@ end
523
550
  ```
524
551
 
525
552
  ```bash
526
- asgard b # same as: asgard build — only task starting with "b"
553
+ asgard b # same as: asgard build (only task starting with "b")
527
554
  asgard depl # Ambiguous command depl matches [deploy, deploy_staging]
528
- asgard deploy # runs deploy — an exact match always wins, even over a shorter ambiguous prefix
555
+ asgard deploy # runs deploy; an exact match always wins, even over a shorter ambiguous prefix
529
556
  ```
530
557
 
531
- This is Thor's own dispatch behavior, not an Asgard feature — it applies to every task in every `.loki` file automatically. When a prefix matches more than one task, Thor lists the candidates instead of guessing; type enough of the name to disambiguate, or use `map` (below) to pin a short name that stays stable even if a later-added task would otherwise make it ambiguous.
558
+ This is Thor's own dispatch behavior, not an Asgard feature; it applies to every task in every `.loki` file automatically. When a prefix matches more than one task, Thor lists the candidates instead of guessing; type enough of the name to disambiguate, or use `map` (below) to pin a short name that stays stable even if a later-added task would otherwise make it ambiguous.
532
559
 
533
560
  ---
534
561
 
@@ -632,10 +659,11 @@ asgard schedule stop NAME # stop one entry (persists across reboots and ins
632
659
  asgard schedule start NAME # start a stopped entry, or install just this one
633
660
  asgard schedule trigger NAME # run an installed entry now
634
661
  asgard schedule log NAME [-f] # print (or follow) the entry's log
635
- asgard schedule remove # remove all of this project's entries
662
+ asgard schedule remove [NAME] # remove one entry, or all of this project's
663
+ asgard schedule prune [-n] # remove every project's entries whose directory is gone (-n: list only)
636
664
  ```
637
665
 
638
- `stop`, `start`, `trigger` and `log` also take an entry of another project
666
+ `stop`, `start`, `trigger`, `log` and `remove` also take an entry of another project
639
667
  (`list --all` shows them); write `project/name` when two projects share a name.
640
668
 
641
669
  Jobs run `asgard <task> [options]` from the directory holding `.loki`, with
@@ -667,6 +695,7 @@ Then manage the watcher with the built-in `watcher` command:
667
695
  ```bash
668
696
  asgard watcher start # watch this project in the background
669
697
  asgard watcher stop # stop it; a task already running finishes first
698
+ asgard watcher stop --force # stop it and end any running task
670
699
  asgard watcher list # watches, daemon state, each entry's last run (--all: every project's)
671
700
  asgard watcher preview # the command and environment each watch would run
672
701
  asgard watcher log [-f] # print (or follow) the watcher's log
@@ -679,6 +708,44 @@ overlaps itself. Saving a `.loki` file restarts the watcher with the new
679
708
  declarations. See [Watched Files](https://madbomber.github.io/asgard/watch/)
680
709
  for every keyword, the environment, and file locations.
681
710
 
711
+ ## Background jobs
712
+
713
+ A script runs in one of three modes. `sh "rake build"` is the foreground:
714
+ the task waits for it and a failure exits asgard. `sh "mkdocs serve",
715
+ background: true` starts a managed job: the task continues at once, the
716
+ job runs on in its own session with its output in a log, it is still
717
+ running when asgard exits, and `asgard jobs` manages it from then on. A
718
+ trailing `&` in the script is the third mode, detached: the shell exits,
719
+ asgard reports success and keeps no record, and the process is yours to
720
+ find and kill by hand.
721
+
722
+ ```ruby
723
+ class Tasks
724
+ desc "doc_server", "Serve the documentation"
725
+ def doc_server = sh "mkdocs serve", background: true # job doc_server-1
726
+
727
+ desc "servers", "Bring up the development stack"
728
+ def servers
729
+ sh "docker compose up postgres", background: true # servers-1
730
+ sh "bin/rails server", background: :api # api
731
+ end
732
+ end
733
+ ```
734
+
735
+ ```bash
736
+ asgard jobs list # running, exited with a status, or gone (--all: every project's)
737
+ asgard jobs stop servers # by job name, or by task and the tasks it depends on
738
+ asgard jobs log api -f # print (or follow) one job's log
739
+ asgard jobs restart api # stop it, then run its owning task again
740
+ asgard jobs clean # forget every job that is no longer running
741
+ ```
742
+
743
+ `stop` sends TERM to the job's whole process group and KILL after a
744
+ timeout. A job's pid is verified against its command line before anything
745
+ trusts it, so a pid reused after a reboot shows as `gone`, never as
746
+ running. See [Background Jobs](https://madbomber.github.io/asgard/jobs/)
747
+ for naming, the trampoline, and file locations.
748
+
682
749
  ## `method_option` types reference
683
750
 
684
751
  | Type | CLI example | Ruby value |
@@ -687,7 +754,7 @@ for every keyword, the environment, and file locations.
687
754
  | `:boolean` | `--force` / `--no-force` | `true` / `false` |
688
755
  | `:numeric` | `--count 3` | `3` |
689
756
  | `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
690
- | `:hash` | `--vars KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
757
+ | `:hash` | `--vars KEY:val FOO:bar` | `{"KEY" => "val", "FOO" => "bar"}` |
691
758
 
692
759
  Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `enum`, `banner`.
693
760
 
@@ -706,7 +773,7 @@ myproject/
706
773
 
707
774
  ### Multiple files
708
775
 
709
- Split tasks across files — each reopens `class Tasks`:
776
+ Split tasks across files; each reopens `class Tasks`:
710
777
 
711
778
  ```
712
779
  myproject/
@@ -749,7 +816,7 @@ class Tasks
749
816
  end
750
817
  ```
751
818
 
752
- 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.
819
+ `.loki` is the only file asgard loads on its own, so in a single-file project it holds the tasks. In a multi-file project it needs at least the `import` calls that load the others, and may hold nothing else.
753
820
 
754
821
  ### Explicit loading
755
822
 
@@ -767,14 +834,41 @@ end
767
834
 
768
835
  ---
769
836
 
770
- ## `Asgard` module API
837
+ ## Tips
838
+
839
+ - **Start from working files.** The task files at this repository's root (`.loki`, `quality.loki`, `gem_tasks.loki`, `git.loki`, `doc_tasks.loki`) are a real multi-file project: quality gates that return `:pass` or `:fail`, a background job, a subcommand group, a `helper` feeding the `header`, and a deliberate override for `--doctor`. Copy what fits.
840
+
841
+ - **More examples.** [MadBomber/dev](https://github.com/MadBomber/dev) holds shared `.loki` libraries used across many repos: gem, git, Rails quality, schedule, status report and iTerm tasks. The [examples directory](examples/) here has one small file per feature.
842
+
843
+ - **One setup for every project.** Put the tasks, shells and helpers you want everywhere in `~/.loki`, then start each project's `.loki` with `import "~/.loki"`. Outside any project the same file is found by the walk up the tree, so those tasks work in any directory.
844
+
845
+ - **Give busy tasks a short name with `map`.** `map "t" => "test"` stays stable when a later task would otherwise make the prefix `t` ambiguous. Thor's prefix matching is handy; an alias is dependable.
846
+
847
+ - **Make bare `asgard` do the common thing.** `default_task :test` (or `:quality`, as this repo does) saves typing on the task you run most.
848
+
849
+ - **Flags go after the task name.** `asgard build --verbose` runs the task; `asgard --verbose build` prints the help for `build`. Only `--version` and `--doctor` go first.
850
+
851
+ - **See the tasks by file with `asgard --doctor`.** It lists every `.loki` file that loaded and, under each, the tasks that file defines with their line numbers, plus which `.loki` won the walk up the tree, what every `import` loaded, tasks hidden because they lack a `desc`, and definitions silently replaced by a later one. It works even when the `.loki` will not load.
852
+
853
+ - **See the commands as a tree with `asgard tree`.** Every command with its description, each subcommand group expanded under its own heading, header and footer included. `help` shows what you can type; `tree` shows how it is organised.
854
+
855
+ - **Keep helpers out of the command list.** A public method without a `desc` is a hidden task: it runs from the command line and as a dependency. Make helpers `private`, wrap them in `no_commands`, or define them with `helper`.
856
+
857
+ - **Pass results, not instance variables.** Parallel dependencies each run on their own copy of the task object, so an `@ivar` set in one never reaches the task that depends on it. Return a value and read it with `dep_result(:name)` or `dep_results`; [`examples/bad.loki`](examples/bad.loki) shows the trap.
858
+
859
+ - **Let gates report, let guards stop.** A quality gate returns `:fail` so every gate runs and the summary shows all failures at once. A guard such as [`examples/confirm.loki`](examples/confirm.loki) raises `Asgard::Error` so nothing after it runs. A dependency's return value never stops its dependent; only an exit, `abort` or `raise` does.
860
+
861
+ - **Name shell configurations, not shells.** The name you give `shell` is yours: `shell :strict, "bash -eu -o pipefail"` names a bash configuration that stops at the first failing line, and `shell :uv, "uv run --script", ext: ".py"` names a way of running Python. `sh` and `shebang` take a name as an optional first argument (`sh :strict, "..."`); without one they run the `default_shell`, so `default_shell :strict` makes every plain `sh` fail fast.
862
+
863
+ - **An AI agent is just another shell.** Register a coding agent's CLI with `shell` and a heredoc becomes the prompt: [`examples/robots/.loki`](examples/robots/.loki) wires up `claude -p` in one line, and its `ask` task sends a prompt with a plain `sh :claude, <<~PROMPT`. Any agent that reads a prompt from stdin fits the same way, so a task can review a diff, draft release notes or explain a failing test alongside the tasks that run them.
864
+
865
+ - **Prefer `background: true` to a trailing `&`.** A managed job gets a name, a log and `asgard jobs stop`; a process detached with `&` leaves no record and must be found with `ps`.
866
+
867
+ - **Import conditionally.** `.loki` is plain Ruby: `import "quality_rails.loki" if ENV["RAILS_ROOT"]` keeps Rails-only gates out of non-Rails projects, and a block-form `depends_on` picks up whatever `*_check` tasks are loaded.
771
868
 
772
- | Method | Description |
773
- |---|---|
774
- | `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
775
- | `Asgard.find_task_file` | Returns a `Pathname` to `.loki` searching from CWD upward, or `nil` |
869
+ - **Trace loading when something is missing.** `RUBYOPT=-w asgard help` prints each file as `import` loads it; `ASGARD_TRACE=1` turns a one-line load error into the full backtrace with Ruby's "Did you mean?" hint.
776
870
 
777
- `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.
871
+ More tips live in the manual at [Tips](https://madbomber.github.io/asgard/tips/).
778
872
 
779
873
  ---
780
874
 
@@ -784,8 +878,9 @@ end
784
878
  git clone git@github.com:MadBomber/asgard.git
785
879
  cd asgard
786
880
  bundle install
787
- bundle exec rake test # run tests (95% coverage minimum enforced)
788
- bundle exec bin/asgard help # exercise the CLI against this gem's own .loki
881
+ bundle exec bin/asgard test_check # run tests (95% coverage minimum enforced)
882
+ bundle exec bin/asgard quality # every quality gate in parallel
883
+ bundle exec bin/asgard help # exercise the CLI against this gem's own .loki
789
884
  ```
790
885
 
791
886
  ## Contributing
data/bin/asgard CHANGED
@@ -1,5 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # --version needs nothing but the constant, so answer it before loading
5
+ # Thor and the rest of the gem. Only before the task name: after it,
6
+ # --version is the task's own argument.
7
+ require_relative "../lib/asgard/leading_flag"
8
+ if Asgard.leading_flag?(ARGV, "--version")
9
+ require_relative "../lib/asgard/version"
10
+ puts Asgard::VERSION
11
+ exit
12
+ end
13
+
4
14
  require_relative "../lib/asgard"
5
15
  Asgard.run!(ARGV)