Week 18 of 2026

Development log of Devlog Excavator

6 items
  1. Spec: update TBB; use the new comment syntax
  2. Silence noisy logging around substitutions
  3. Replace local tbb.nu with one coming with TBB
  4. Separate project development spec to a fixture
  5. Write a second project fixture
  6. Tag projects' suites as fixtures

Spec: update TBB; use the new comment syntax

On by Tad Lispy

To avoid interpreting semantic lists as steps definitions. In the latest version, numbered (ordered) lists are not considered for steps. So now the spec works again.

index 745410f..4e54f71 100644
--- a/flake.lock
+++ b/flake.lock
@@ -19,11 +19,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1767714506,
-        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "lastModified": 1774017633,
+        "narHash": "sha256-CWhnwL2M83/ItapPVeJqCevRoQttesYxJ1h0Mo6ZCXs=",
=        "owner": "cachix",
=        "repo": "cachix",
-        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "rev": "e8be573b417f3daa3dd4cb9052178f848e0c9d1d",
=        "type": "github"
=      },
=      "original": {
@@ -156,6 +156,7 @@
=        "crate2nix": "crate2nix",
=        "flake-compat": "flake-compat_3",
=        "flake-parts": "flake-parts_3",
+        "ghostty": "ghostty",
=        "git-hooks": "git-hooks_3",
=        "nix": "nix",
=        "nixd": "nixd",
@@ -165,11 +166,11 @@
=        "rust-overlay": "rust-overlay"
=      },
=      "locked": {
-        "lastModified": 1776080969,
-        "narHash": "sha256-2uZxF6q0KOIH8tq3r65ZCz7dM+XYP3avtRP8IBq5zYY=",
+        "lastModified": 1777372895,
+        "narHash": "sha256-PGesjpeDbEPigEP7tdRw8Sm0mmOmonUdOOthBY+nhJA=",
=        "owner": "cachix",
=        "repo": "devenv",
-        "rev": "8d558a84fa38242a7f13781670fee1a6a8902b48",
+        "rev": "cb344e4a5ab9241ae49739352c24268d5f8be13b",
=        "type": "github"
=      },
=      "original": {
@@ -267,6 +268,22 @@
=        "type": "github"
=      }
=    },
+    "flake-compat_4": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1761588595,
+        "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
=    "flake-parts": {
=      "inputs": {
=        "nixpkgs-lib": [
@@ -320,11 +337,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1772408722,
-        "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
+        "lastModified": 1775087534,
+        "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
=        "owner": "hercules-ci",
=        "repo": "flake-parts",
-        "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
+        "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
=        "type": "github"
=      },
=      "original": {
@@ -333,6 +350,30 @@
=        "type": "github"
=      }
=    },
+    "ghostty": {
+      "inputs": {
+        "flake-compat": "flake-compat_4",
+        "home-manager": "home-manager",
+        "nixpkgs": "nixpkgs_4",
+        "systems": "systems",
+        "zig": "zig",
+        "zon2nix": "zon2nix"
+      },
+      "locked": {
+        "lastModified": 1776365871,
+        "narHash": "sha256-lAFTUeJy7AT4V+t8/HlMM7O5z6W+G4eUhzRoh3ZdZu8=",
+        "owner": "cachix",
+        "repo": "ghostty",
+        "rev": "d882f9106d15c213239b8916083835263d4fb9bc",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "cachix-upstream",
+        "repo": "ghostty",
+        "type": "github"
+      }
+    },
=    "git-hooks": {
=      "inputs": {
=        "flake-compat": [
@@ -408,11 +449,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1772893680,
-        "narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=",
+        "lastModified": 1775585728,
+        "narHash": "sha256-8Psjt+TWvE4thRKktJsXfR6PA/fWWsZ04DVaY6PUhr4=",
=        "owner": "cachix",
=        "repo": "git-hooks.nix",
-        "rev": "8baab586afc9c9b57645a734c820e4ac0a604af9",
+        "rev": "580633fa3fe5fc0379905986543fd7495481913d",
=        "type": "github"
=      },
=      "original": {
@@ -539,6 +580,28 @@
=        "type": "github"
=      }
=    },
+    "home-manager": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "ghostty",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1770586272,
+        "narHash": "sha256-Ucci8mu8QfxwzyfER2DQDbvW9t1BnTUJhBmY7ybralo=",
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "rev": "b1f916ba052341edc1f80d4b2399f1092a4873ca",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "type": "github"
+      }
+    },
=    "nix": {
=      "inputs": {
=        "flake-compat": [
@@ -565,11 +628,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1775984952,
-        "narHash": "sha256-FciKF0weMXVirN+ZBSniR4wpKx168cBa9IXhuaLOkkU=",
+        "lastModified": 1776511668,
+        "narHash": "sha256-g2KEBuHpc3a56c+jPcg0+w6LSuIj6f+zzdztLCOyIhc=",
=        "owner": "cachix",
=        "repo": "nix",
-        "rev": "e671135fc5b783798c444e4ece101f6b15ff0c46",
+        "rev": "42d4b7de21c15f28c568410f4383fa06a8458a40",
=        "type": "github"
=      },
=      "original": {
@@ -617,18 +680,15 @@
=          "devenv",
=          "flake-parts"
=        ],
-        "nixpkgs": [
-          "devenv",
-          "nixpkgs"
-        ],
+        "nixpkgs": "nixpkgs_5",
=        "treefmt-nix": "treefmt-nix"
=      },
=      "locked": {
-        "lastModified": 1773634079,
-        "narHash": "sha256-49qb4QNMv77VOeEux+sMd0uBhPvvHgVc0r938Bulvbo=",
+        "lastModified": 1776341634,
+        "narHash": "sha256-L//ltP2o5+BnuK+KEulbi2gGeDpyyex6SkXLZcGQ/Ac=",
=        "owner": "nix-community",
=        "repo": "nixd",
-        "rev": "8ecf93d4d93745e05ea53534e8b94f5e9506e6bd",
+        "rev": "951e98e2025c47614f5249556ecf509b0ea35b51",
=        "type": "github"
=      },
=      "original": {
@@ -656,11 +716,11 @@
=    "nixpkgs-src": {
=      "flake": false,
=      "locked": {
-        "lastModified": 1775888245,
-        "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
+        "lastModified": 1776329215,
+        "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=",
=        "owner": "NixOS",
=        "repo": "nixpkgs",
-        "rev": "13043924aaa7375ce482ebe2494338e058282925",
+        "rev": "b86751bc4085f48661017fa226dee99fab6c651b",
=        "type": "github"
=      },
=      "original": {
@@ -703,15 +763,41 @@
=      }
=    },
=    "nixpkgs_4": {
+      "locked": {
+        "lastModified": 1772963539,
+        "narHash": "sha256-G4+9cEu8XSqEWYUB6iRgDfrg53av6yyRwAKhSeKbUVw=",
+        "rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
+        "type": "tarball",
+        "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre960399.9dcb002ca169/nixexprs.tar.xz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
+      }
+    },
+    "nixpkgs_5": {
+      "locked": {
+        "lastModified": 1772963539,
+        "narHash": "sha256-G4+9cEu8XSqEWYUB6iRgDfrg53av6yyRwAKhSeKbUVw=",
+        "rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
+        "type": "tarball",
+        "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre960399.9dcb002ca169/nixexprs.tar.xz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
+      }
+    },
+    "nixpkgs_6": {
=      "inputs": {
=        "nixpkgs-src": "nixpkgs-src"
=      },
=      "locked": {
-        "lastModified": 1776097194,
-        "narHash": "sha256-XD4DsgNcfXC5nlCxlAcCP5hSjTYlgLXEIoTj7fKkQg4=",
+        "lastModified": 1776852779,
+        "narHash": "sha256-WwO/ITisCXwyiRgtktZgv3iGhAGO+IB5Av4kKCwezR0=",
=        "owner": "cachix",
=        "repo": "devenv-nixpkgs",
-        "rev": "6e8a07b02f6f8557ffab71274feac9827bcc2532",
+        "rev": "ec3063523dcd911aeadb50faa589f237cdab5853",
=        "type": "github"
=      },
=      "original": {
@@ -782,8 +868,8 @@
=    "root": {
=      "inputs": {
=        "devenv": "devenv",
-        "nixpkgs": "nixpkgs_4",
-        "systems": "systems",
+        "nixpkgs": "nixpkgs_6",
+        "systems": "systems_2",
=        "tad-better-behavior": "tad-better-behavior"
=      }
=    },
@@ -795,11 +881,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1773630837,
-        "narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=",
+        "lastModified": 1776741231,
+        "narHash": "sha256-k9G98qzn+7npROUaks8VqCFm7cFtEG8ulQLBBo5lItg=",
=        "owner": "oxalica",
=        "repo": "rust-overlay",
-        "rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316",
+        "rev": "02061303f7c4c964f7b4584dabd9e985b4cd442b",
=        "type": "github"
=      },
=      "original": {
@@ -809,6 +895,7 @@
=      }
=    },
=    "systems": {
+      "flake": false,
=      "locked": {
=        "lastModified": 1681028828,
=        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -838,6 +925,21 @@
=        "type": "github"
=      }
=    },
+    "systems_3": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
=    "tad-better-behavior": {
=      "inputs": {
=        "devenv": [
@@ -846,14 +948,14 @@
=        "nixpkgs": [
=          "nixpkgs"
=        ],
-        "systems": "systems_2"
+        "systems": "systems_3"
=      },
=      "locked": {
-        "lastModified": 1776280929,
-        "narHash": "sha256-Z6wXuMFjoRzeCgMSEhb1H93w5SoqtfC539ZRdemaDEU=",
+        "lastModified": 1777445619,
+        "narHash": "sha256-HjHKZsgkNXvhPC22js+lEvhVBjP5delbuZ4jfsFw1sU=",
=        "ref": "refs/heads/main",
-        "rev": "b70531b7ce4b77625cd8824842d5b0cdb62a2b88",
-        "revCount": 163,
+        "rev": "a5dc03a114f6284aa4e8e79a27d3846461d251ce",
+        "revCount": 186,
=        "type": "git",
=        "url": "http://codeberg.org/tad-lispy/tad-better-behavior"
=      },
@@ -871,11 +973,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1772660329,
-        "narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=",
+        "lastModified": 1775636079,
+        "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
=        "owner": "numtide",
=        "repo": "treefmt-nix",
-        "rev": "3710e0e1218041bbad640352a0440114b1e10428",
+        "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
=        "type": "github"
=      },
=      "original": {
@@ -883,6 +985,85 @@
=        "repo": "treefmt-nix",
=        "type": "github"
=      }
+    },
+    "zig": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "ghostty",
+          "flake-compat"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "ghostty",
+          "nixpkgs"
+        ],
+        "systems": [
+          "devenv",
+          "ghostty",
+          "systems"
+        ]
+      },
+      "locked": {
+        "lastModified": 1773145353,
+        "narHash": "sha256-dE8zx8WA54TRmFFQBvA48x/sXGDTP7YaDmY6nNKMAYw=",
+        "owner": "mitchellh",
+        "repo": "zig-overlay",
+        "rev": "8666155d83bf792956a7c40915508e6d4b2b8716",
+        "type": "github"
+      },
+      "original": {
+        "owner": "mitchellh",
+        "repo": "zig-overlay",
+        "type": "github"
+      }
+    },
+    "zig_2": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "ghostty",
+          "zon2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1776208985,
+        "narHash": "sha256-IOuRFpbeQ9jSk54OURX5yvjoC759ujgSNjkMKpChdDA=",
+        "ref": "refs/heads/main",
+        "rev": "e8ee348125247e7bd74932cc42ac92df90961d5b",
+        "revCount": 1666,
+        "type": "git",
+        "url": "https://codeberg.org/jcollie/zig-overlay.git"
+      },
+      "original": {
+        "type": "git",
+        "url": "https://codeberg.org/jcollie/zig-overlay.git"
+      }
+    },
+    "zon2nix": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "ghostty",
+          "nixpkgs"
+        ],
+        "zig": "zig_2"
+      },
+      "locked": {
+        "lastModified": 1776269939,
+        "narHash": "sha256-tOGsI1d1Xk1PYapQJ/ByG0utbWXJasIna/fUib+/b5A=",
+        "owner": "jcollie",
+        "repo": "zon2nix",
+        "rev": "cc467a77c2ebcd9aab84024196abfc37eaf1007d",
+        "type": "github"
+      },
+      "original": {
+        "owner": "jcollie",
+        "ref": "main",
+        "repo": "zon2nix",
+        "type": "github"
+      }
=    }
=  },
=  "root": "root",
index e2c4b25..002bfcd 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -229,8 +229,8 @@ By default `./devlog.toml` shall be used, but it can be changed with `--config-f
=
=In addition it can specify:
=
-  - `drafts_dir`: where devlog entries are written (default: `./drafts/devlog`)
-  - `projects_stubs_dir`: where projects' are written by `excavate stub projects`(defautl: `./drafts/projects`)
+  - // `drafts_dir`: where devlog entries are written (default: `./drafts/devlog`)
+  - // `projects_stubs_dir`: where projects' are written by `excavate stub projects`(defautl: `./drafts/projects`)
=
=It can also include the `ignore` section. Any path under `ignore.paths` will be ignored, i.e. no entries will be excavated from it and there will be no warning.
=
@@ -246,7 +246,7 @@ In the future there might be more things to ignore, like authors, files in patch
=
=A configuration file can also contain `frontmatter` section with configuration related to the entries front-matter.
=
-  - `projects_cell_path`: where to write the name of a project that the entry belongs to
+  - // `projects_cell_path`: where to write the name of a project that the entry belongs to
=
=See the `devlog-sample.toml` file.
=

Silence noisy logging around substitutions

On by Tad Lispy

index c59d1b0..82e781f 100755
--- a/excavate
+++ b/excavate
@@ -248,7 +248,6 @@ def "format entry" [
=    let substitute = { |entry|
=        $config.substitute
=        | reduce --fold  $entry { |substitute, memo|
-            log info $"Substituting ($substitute.what) with ($substitute.with)"
=            $memo | str replace --all $substitute.what $substitute.with
=        }
=    }

Replace local tbb.nu with one coming with TBB

On by Tad Lispy

This was the first project in which I started using TBB with a nushell interpreter. So naturally I developed the tbb.nu helper library here. Recently I felt like it's mature enough to be included with TBB itself.

By setting the NU_LIB_DIRS environment variable in a development shell I make nu look for libraries in nix store, where TBB is installed. It also isolates the development environment from any paths a developer can have set in their own environment.

index 5b67689..561ccf7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -37,6 +37,9 @@
=                    pkgs.nushell
=                    tbb.default
=                  ];
+                  env = {
+                    NU_LIB_DIRS = "${tbb.default}/lib";
+                  };
=                }
=              ];
=            };
deleted file mode 100644
index 0bf7a1d..0000000
--- a/spec/interpreters/tbb.nu
+++ /dev/null
@@ -1,146 +0,0 @@
-use std/assert
-
-export def run [
-    steps: record
-    --initial-state: record = {}
-]: string -> nothing {
-    tee { # This preserves the input stream for the lines command
-        print --stderr "Interpreter listening..."
-        { type: InterpreterState, ready: true }
-        | to json --raw
-        | print
-    }
-    | lines
-    | reduce --fold $initial_state { |line, state|
-        $state
-        | observe snippet --caption "State before the step" 
-        
-        $line
-        | handle incoming $state $steps
-        | let state_update
-
-        if ($state_update | describe --detailed | $in.type == "record") {
-            $state_update
-            | observe snippet --caption "State update to be merged"
-            $state | merge $state_update
-        } else {
-            $state_update
-            | observe snippet --caption "The value returned by the step is not a record; Not updating"
-            $state
-        }
-    }
-
-    print --stderr "Interpreter done."
-}
-
-def "handle incoming" [
-    state: record,
-    steps: record,
-] {
-    $in
-    | observe snippet --raw "json" --caption  "Received input"
-    | from json
-    | observe snippet --caption  "Decoded control message"
-    | let incoming
-
-    match $incoming.type {
-        Execute => {
-            try {
-                $incoming.step
-                | insert state $state
-                | let step
-
-                let closure = $steps | get $step.variant
-                let state_update = do $closure ...$step.arguments $step
-                { type: Success } | to json --raw | print
-                $state_update
-            } catch { |err|
-                {
-                    type: Failure,
-                    reason: $err.msg
-                    hint: $err.rendered
-                } | to json --raw | print
-            }
-        },
-        _ => {
-            error make "Unsupported message type: ($incoming.type)" 
-        }
-    }
-}
-
-export def "observe snippet" [
-    --raw: string,
-    --caption: string,
-    --meta: string = "",
-    --transform: closure
-]: any -> any {
-    # By default transform is identity
-    $transform
-    | default { {||} }
-    | let transform
-
-    $in
-    | do $transform
-    | let transformed
-    | if ($raw | is-empty) { table --expand } else { $in }
-    | let formatted
-
-    {
-        type: Snippet
-        caption: $caption,
-        language: ($raw | default "table"),
-        meta: $meta,
-        content: ($formatted | default "nothing")
-    }
-    | to json --raw
-    | print
-
-    $in # Pass the value on
-}
-
-export def "observe text" [
-    --transform: closure
-] {
-    # By default transform is identity
-    $transform
-    | default { {||} }
-    | let transform
-
-    $in
-    | do $transform
-    | let formatted
-
-
-    {
-        type: Text
-        content: $formatted
-    }
-    | to json --raw
-    | print 
-
-    $in # Pass the value on
-}
-
-export def "shell" [command: string]: string -> record {
-    $in
-    | sh -c $command
-    | complete
-    | observe snippet --caption $"Results of running `($command)`"
-    | if $in.exit_code != 0 {
-        error make {
-            msg: $"Command exited with code ($in.exit_code)",
-            labels: [{ text: $"Command:\n($command)", span: (metadata $command).span }]
-        }
-    } else {
-        $in.stdout
-    }
-}
-
-export def "assert match" [
-    pattern: string
-]: string -> string {
-    assert ($in | find --regex $pattern | is-not-empty) --error-label {
-        text: "Pattern not found in the input",
-        span: (metadata $pattern).span
-    }
-}

Separate project development spec to a fixture

On by Tad Lispy

Using the new delegations API from TBB we can separate the steps needed to create a fictional Project Alpha into their own scenario (called Develop Project Alpha).

Then compose it with the existing scenario, removing a lot of boilerplate and cognitive load from its definition.

The delegation API is not yet implemented in the Nushell helper library distributed with TBB, so I had to extend it. Fortunately Nushell makes it really easy.

The delegation step demonstrates use of parameters and state. It passes a temporary path created at runtime to the delegated scenario as a "project path" parameter.

While working on this, I accidentally overwritten the devlog.toml file in this project directory. It's in .gitignore, so it was painful to restore it. Fortunately I had a Nushell table representation of it in my terminal's history. Reconstructing TOML from this ASCII art was a fun exercise in Emacs fu, but I wouldn't recommend it. To safeguard from this kind of thing in the future, now there are two step variants to write to files:

I'm also using the freshly minted step comment syntax to show planned, but not yet implemented step "Develop project Beta". It will work the same way Project Alpha goes - a step will delegate to a fixture scenario. I just need to write that scenario, and I feel like this commit is already too large.

index 002bfcd..78ecc14 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -15,113 +15,21 @@ tags: [ focus ]
=
=Before you excavate, it might be smart to list the commits. You can use `--dry-run` flag for that. Consider the following scenario.
=
-  * Write `devlog.toml`
-    
+  * Work in a new temporary directory
+  * Develop project Alpha
+  * // Develop project Beta
+  * Create `devlog.toml`
+
=    ``` toml
=    [[projects]]
=    name = "Project Alpha"
=    path = "./project-alpha"
-    ```
-  
-  * Initialize a git repository at `project-alpha`
-
-  * Write `README.md`
-  
-    ``` markdown
-    # This is Project Alpha
-    
-    The best project ever. Like seriosuly.
-    ```
-    
-  * On `2026-03-27 13:22` commit as `Alice <alice@example.com>`
-  
-    ``` text
-    Write a readme
-    
-    It's very exciting to start a new project.
-    ```
-    
-  * Write `src/main.rs`
-     
-    ``` rust
-    fn main() {
-        println ("Hello, World!")
-    }
-    ```
-
-  * On `2026-03-27 14:06` commit as `Bob <bob@example.com>`
-  
-    ``` text
-    Implement an MVP
-    
-    I need to keep my phone charged. Investors are going to call any minute now.
-    ```
-  
-  * Write `src/main.rs`
-     
-    ``` rust
-    fn main() {
-        println!("Hello, World!")
-    }
-    ```
=
-  * Write `CHANGELOG.md`
-     
-    ``` markdown
-    # v1.0.0
-    
-    * Now it's working
+    # [[projects]]
+    # name = "Project Beta"
+    # path = "./project-beta"
=    ```
=
-  * On `2026-03-28 03:06` commit as `Alice <alice@example.com>`
-  
-    Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
-      
-    ``` text
-    Make Project Alpha work
-    
-    Coding is hard for Bob. He should have used Tad Better Behavior to test his stuff before pushing it to prod!
-    ```
-
-  * Write `LICENSE`
-  
-    ``` text
-    All rights reserved! You monkeys better not even look at my pro codes.
-    ```
-  
-  
-  * On `2026-04-01 09:03` commit as `Bob <bob@example.com>`
-  
-    Two days later
-      
-    ``` text
-    I've lawyered up
-    
-    With a perl like this project, I have to protect my IP.
-    ```
-    
-  * Write `src/main.rs`
-     
-    ``` rust
-    fn main() {
-        println ("Eat 💩, Bob!")
-    }
-    ```
-
-  * On `2026-04-04 14:18` commit as `Alice <alice@example.com>`
-      
-    ``` text
-    I quit
-    
-    Bob is an asshole.
-    ```
-
-TODO: Setup a second project.
-
-  * Change directory to `..`
-  
-    Leave the repository and go back to where the config file is.
-
=  * Run the `excavate --projects-dir . --dry-run` command
=  
=  * Expect the output to contain `the header row`
@@ -279,3 +187,106 @@ with = "\\%}"
=## Stubbing projects 
=
=Use the `excavate stub projects` command to write a markdown file for each project listed in the configuration file. The output path is controlled by the `projects_stubs_dir` key.
+
+
+## Develop project Alpha
+
+``` yaml tbb
+fixture: true
+tags: [ focus, fixture ] # Temporary solution. Run `tbb evaluate --exclude scanario:fixture`
+parameters:
+  project path: ./project-alpha
+```
+
+  * Initialize a git repository at `./project-alpha`
+
+  * Create `README.md`
+
+    ``` markdown
+    # This is Project Alpha
+
+    The best project ever. Like seriosuly.
+    ```
+
+  * On `2026-03-27 13:22` commit as `Alice <alice@example.com>`
+
+    ``` text
+    Write a readme
+
+    It's very exciting to start a new project.
+    ```
+
+  * Create `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println ("Hello, World!")
+    }
+    ```
+
+  * On `2026-03-27 14:06` commit as `Bob <bob@example.com>`
+
+    ``` text
+    Implement an MVP
+
+    I need to keep my phone charged. Investors are going to call any minute now.
+    ```
+
+  * Write to `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println!("Hello, World!")
+    }
+    ```
+
+  * Create `CHANGELOG.md`
+
+    ``` markdown
+    # v1.0.0
+
+    * Now it's working
+    ```
+
+  * On `2026-03-28 03:06` commit as `Alice <alice@example.com>`
+
+    Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
+
+    ``` text
+    Make Project Alpha work
+
+    Coding is hard for Bob. He should have used Tad Better Behavior to test his stuff before pushing it to prod!
+    ```
+
+  * Create `LICENSE`
+
+    ``` text
+    All rights reserved! You monkeys better not even look at my pro codes.
+    ```
+
+
+  * On `2026-04-01 09:03` commit as `Bob <bob@example.com>`
+
+    Two days later
+
+    ``` text
+    I've lawyered up
+
+    With a perl like this project, I have to protect my IP.
+    ```
+
+  * Write to `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println ("Eat 💩, Bob!")
+    }
+    ```
+
+  * On `2026-04-04 14:18` commit as `Alice <alice@example.com>`
+
+    ``` text
+    I quit
+
+    Bob is an asshole.
+    ```
index 69c1644..33ebb9d 100644
--- a/spec/interpreters/basic.nu
+++ b/spec/interpreters/basic.nu
@@ -1,21 +1,78 @@
=use tbb.nu
=
+def write [
+  --force # Overwite existing files?
+  path
+]: string -> string {
+   let content = $in
+
+   $path
+   | tbb observe text --transform { $"Writing content to ($in)" }
+   | path dirname
+   | tbb observe text --transform  { $"Parent directory ($in)" }
+   | mkdir $in
+
+   if $force {
+        $content | save --force $path
+    } else {
+        $content | save $path
+    }
+}
+
+
+def "tbb delegate" [
+    suite: path         # Path to the Markdown file containing the scenario
+    scenario: string    # The title of the scenario to run
+]: record -> record {
+   {
+        type: Delegate,
+        suite: $suite,
+        scenario: $scenario,
+        parameters: $in
+    }
+    | to json --raw
+    | print
+
+    $in # Pass the value on
+}
+
=def main [] {
-    tbb run --initial-state { pwd: (mktemp --directory ) } {
-        "Write {0}": { |filename, data|
+    tbb run --initial-state { pwd: . } {
+        "Work in a new temporary directory": {
+            mktemp --directory
+            | tbb observe text --transform { $"Setting PWD to ($in)" }
+            | { pwd: $in }
+        },
+        "Develop project Alpha": { |data|
=            $data.state.pwd
-            | path join $filename
-            | tbb observe text --transform { $"Writing content to ($in)" }
+            | path join "project-alpha"
+            | tbb observe text --transform  { $"Developing project Alpha at ($in)" }
=            | let path
-            | path dirname
-            | tbb observe text --transform  { $"Parent directory ($in)" }
-            | mkdir $in
=            
+            { "project path": $path }
+            | tbb delegate spec/BASIC.md "Develop project Alpha"
+        },
+        "Create {0}": { |filename, data|
+            $data.state.pwd
+            | path join $filename
+            | let path
+
+            $data.code_blocks
+            | first
+            | get value
+            | write $path
=            
+        },
+        "Write to {0}": { |filename, data|
+            $data.state.pwd
+            | path join $filename
+            | tbb observe text --transform { $"Writing content to ($in)" }
+            | let path
+
=            $data.code_blocks
=            | first
=            | get value
-            | save --force $path
+            | write --force $path
=        },
=
=        "Initialize a git repository at {0}": {|dirname, data|

Write a second project fixture

On by Tad Lispy

There are now two fictional projects in the spec: Alpha and Beta.

To avoid accidentally writing to the project directory (like the last time), I reverted to initializing the interpreter with a new temporary directory as PWD.

I also reduced some flakiness, by disabling gpg signatures from the fake commits. It would be nice to isolate all git operations from the site environment.

The assertions are flaky. Specifically they depend on the width of a terminal in which they are evaluated. This is because of Nushell's smart table formatting. Also, the assertions using regular expressions are not very readable or maintainable. I think the solution is to import excavate as a module inside the interpreter and evaluate assertions over the structured data instead of the terminal output.

index 78ecc14..b40d244 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -13,17 +13,16 @@ This program excavates a devlog (i.e. a bunch of .md files) from commit messages
=tags: [ focus ]
=```
=
-Before you excavate, it might be smart to list the commits. You can use `--dry-run` flag for that. Consider the following scenario.
+Before you excavate, it might be smart to see what the result would be. You can use `--dry-run` flag for that. Consider the following scenario.
=
-  * Work in a new temporary directory
-  * Develop project Alpha
-  * // Develop project Beta
+  * Develop project `Alpha`
+  * Develop project `Beta`
=  * Create `devlog.toml`
=
=    ``` toml
=    [[projects]]
=    name = "Project Alpha"
-    path = "./project-alpha"
+    path = "./alpha"
=
=    # [[projects]]
=    # name = "Project Beta"
@@ -43,7 +42,7 @@ Before you excavate, it might be smart to list the commits. You can use `--dry-r
=  * Expect the output to contain `the first commit from Alice`
=
=    ``` regex
-    │ +0 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Write a readme +│ +Fri, 27 Mar 2026 13:22:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/project-alpha +│
+    │ +0 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Write a readme +│ +Fri, 27 Mar 2026 13:22:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
=    ```
=
=      > NOTE: To make the spec portable, the timezone, sha and part of the path has to be expressed as wildcard.
@@ -53,13 +52,13 @@ Before you excavate, it might be smart to list the commits. You can use `--dry-r
=    Notice that the logical day (in the second column) is one before the actual day (in the seventh column). That's because work done at night is still counted toward the previous day. This can be controlled with `start-of-day` configuration parameter.
=    
=    ``` regex
-    │ +2 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Make Project Alpha work +│ +Sat, 28 Mar 2026 03:06:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/project-alpha +│
+    │ +2 +│ +2026-03-27 +│ +Project Alpha +│ +Alice +│ +alice@example.com +│ +Make Project Alpha work +│ +Sat, 28 Mar 2026 03:06:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
=    ```
=
=  * Expect the output to contain `the silly commit from Bob`
=  
=    ``` regex
-    │ +3 +│ +2026-04-01 +│ +Project Alpha +│ +Bob +│ +bob@example.com +│ +I've lawyered up +│ +Wed, 1 Apr 2026 09:03:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/project-alpha +│
+    │ +3 +│ +2026-04-01 +│ +Project Alpha +│ +Bob +│ +bob@example.com +│ +I've lawyered up +│ +Wed, 1 Apr 2026 09:03:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/alpha +│
=    ```
=
=## The report
@@ -189,104 +188,3 @@ with = "\\%}"
=Use the `excavate stub projects` command to write a markdown file for each project listed in the configuration file. The output path is controlled by the `projects_stubs_dir` key.
=
=
-## Develop project Alpha
-
-``` yaml tbb
-fixture: true
-tags: [ focus, fixture ] # Temporary solution. Run `tbb evaluate --exclude scanario:fixture`
-parameters:
-  project path: ./project-alpha
-```
-
-  * Initialize a git repository at `./project-alpha`
-
-  * Create `README.md`
-
-    ``` markdown
-    # This is Project Alpha
-
-    The best project ever. Like seriosuly.
-    ```
-
-  * On `2026-03-27 13:22` commit as `Alice <alice@example.com>`
-
-    ``` text
-    Write a readme
-
-    It's very exciting to start a new project.
-    ```
-
-  * Create `src/main.rs`
-
-    ``` rust
-    fn main() {
-        println ("Hello, World!")
-    }
-    ```
-
-  * On `2026-03-27 14:06` commit as `Bob <bob@example.com>`
-
-    ``` text
-    Implement an MVP
-
-    I need to keep my phone charged. Investors are going to call any minute now.
-    ```
-
-  * Write to `src/main.rs`
-
-    ``` rust
-    fn main() {
-        println!("Hello, World!")
-    }
-    ```
-
-  * Create `CHANGELOG.md`
-
-    ``` markdown
-    # v1.0.0
-
-    * Now it's working
-    ```
-
-  * On `2026-03-28 03:06` commit as `Alice <alice@example.com>`
-
-    Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
-
-    ``` text
-    Make Project Alpha work
-
-    Coding is hard for Bob. He should have used Tad Better Behavior to test his stuff before pushing it to prod!
-    ```
-
-  * Create `LICENSE`
-
-    ``` text
-    All rights reserved! You monkeys better not even look at my pro codes.
-    ```
-
-
-  * On `2026-04-01 09:03` commit as `Bob <bob@example.com>`
-
-    Two days later
-
-    ``` text
-    I've lawyered up
-
-    With a perl like this project, I have to protect my IP.
-    ```
-
-  * Write to `src/main.rs`
-
-    ``` rust
-    fn main() {
-        println ("Eat 💩, Bob!")
-    }
-    ```
-
-  * On `2026-04-04 14:18` commit as `Alice <alice@example.com>`
-
-    ``` text
-    I quit
-
-    Bob is an asshole.
-    ```
index 33ebb9d..49cb70b 100644
--- a/spec/interpreters/basic.nu
+++ b/spec/interpreters/basic.nu
@@ -37,20 +37,29 @@ def "tbb delegate" [
=}
=
=def main [] {
-    tbb run --initial-state { pwd: . } {
-        "Work in a new temporary directory": {
-            mktemp --directory
-            | tbb observe text --transform { $"Setting PWD to ($in)" }
-            | { pwd: $in }
-        },
-        "Develop project Alpha": { |data|
+    tbb run --initial-state { pwd: (mktemp --directory) } {
+
+        # Use one of the fixtures in the ./spec/projects/ directory
+        "Develop project {0}": { |project_name, data|
+            $project_name
+            | str kebab-case
+            | let project_slug
+
+            {
+                parent: "./spec/projects/",
+                stem: $project_slug,
+                extension: "md"
+            }
+            | path join
+            | let suite_path
+
=            $data.state.pwd
-            | path join "project-alpha"
-            | tbb observe text --transform  { $"Developing project Alpha at ($in)" }
-            | let path
+            | path join $project_slug
+            | tbb observe text --transform  { $"Developing project ($project_name) at ($in)" }
+            | let project_path
=            
-            { "project path": $path }
-            | tbb delegate spec/BASIC.md "Develop project Alpha"
+            { "project path": $project_path }
+            | tbb delegate $suite_path "Develop the project"
=        },
=        "Create {0}": { |filename, data|
=            $data.state.pwd
@@ -96,7 +105,7 @@ def main [] {
=            $data.code_blocks
=            | first
=            | get value
-            | git commit --date $time --author $author --message $in
+            | git commit --date $time --author $author --message $in --no-gpg-sign
=            | tbb observe text
=            | ignore
=        },
new file mode 100644
index 0000000..983c3c0
--- /dev/null
+++ b/spec/projects/alpha.md
@@ -0,0 +1,113 @@
+---
+interpreter: nu --stdin spec/interpreters/basic.nu
+---
+
+# Project Alpha
+
+This suite contains instructions for TBB to fake a development of a fictional project. It doesn't really test anything about Devlog Excavator and is meant to be used as a fixture for other suites and scenarios in the spec. To use the fixture, add a step like this to a scenario:
+
+  * Develop project `Alpha`
+
+## Develop the project
+
+``` yaml tbb
+fixture: true
+tags: [ fixture ] # Temporary solution. Run `tbb evaluate --exclude scanario:fixture`
+parameters:
+  project path: ./project-alpha
+```
+
+  * Initialize a git repository at `./project-alpha`
+
+  * Create `README.md`
+
+    ``` markdown
+    # This is Project Alpha
+
+    The best project ever. Like seriosuly.
+    ```
+
+  * On `2026-03-27 13:22` commit as `Alice <alice@example.com>`
+
+    ``` text
+    Write a readme
+
+    It's very exciting to start a new project.
+    ```
+
+  * Create `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println ("Hello, World!")
+    }
+    ```
+
+  * On `2026-03-27 14:06` commit as `Bob <bob@example.com>`
+
+    ``` text
+    Implement an MVP
+
+    I need to keep my phone charged. Investors are going to call any minute now.
+    ```
+
+  * Write to `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println!("Hello, World!")
+    }
+    ```
+
+  * Create `CHANGELOG.md`
+
+    ``` markdown
+    # v1.0.0
+
+    * Now it's working
+    ```
+
+  * On `2026-03-28 03:06` commit as `Alice <alice@example.com>`
+
+    Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
+
+    ``` text
+    Make Project Alpha work
+
+    Coding is hard for Bob. He should have used Tad Better Behavior to test his stuff before pushing it to prod!
+    ```
+
+  * Create `LICENSE`
+
+    ``` text
+    All rights reserved! You monkeys better not even look at my pro codes.
+    ```
+
+
+  * On `2026-04-01 09:03` commit as `Bob <bob@example.com>`
+
+    Two days later
+
+    ``` text
+    I've lawyered up
+
+    With a perl like this project, I have to protect my IP.
+    ```
+
+  * Write to `src/main.rs`
+
+    ``` rust
+    fn main() {
+        println ("Eat 💩, Bob!")
+    }
+    ```
+
+  * On `2026-04-04 14:18` commit as `Alice <alice@example.com>`
+
+    ``` text
+    I quit
+
+    Bob is an asshole.
+    ```
+
+
new file mode 100644
index 0000000..462bd76
--- /dev/null
+++ b/spec/projects/beta.md
@@ -0,0 +1,113 @@
+---
+interpreter: nu --stdin spec/interpreters/basic.nu
+---
+
+# Project Beta
+
+This suite contains instructions for TBB to fake a development of a fictional project
+
+## Develop the project
+
+``` yaml tbb
+fixture: true
+tags: [ fixture ] # Temporary solution. Run `tbb evaluate --exclude scanario:fixture`
+parameters:
+  project path: ./project-beta
+```
+
+  * Initialize a git repository at `./project-beta`
+
+  * Create `README.md`
+
+    ``` markdown
+    # This is Project Beta
+
+    The second best project ever. Not quite Alpha, but close.
+    ```
+
+  * On `2026-03-27 15:30` commit as `Charlie <charlie@example.com>`
+
+    ``` text
+    Set up initial project structure
+
+    Starting fresh with Beta. Let's see if we can learn from Alpha's mistakes.
+    ```
+
+  * Create `src/lib.rs`
+
+    ``` rust
+    pub fn greet(name: &str) -> String {
+        format!("Hello, {}!", name)
+    }
+    ```
+
+  * On `2026-03-27 16:45` commit as `Alice <alice@example.com>`
+
+    ``` text
+    Add greeting function
+
+    Alice thinks this is going to be a game changer.
+    ```
+
+  * Write to `src/lib.rs`
+
+    ``` rust
+    pub fn greet(name: &str) -> String {
+        format!("Hello, {}! Welcome aboard.", name)
+    }
+    ```
+
+  * Create `tests/greeting_test.rs`
+
+    ``` rust
+    #[test]
+    fn test_greeting() {
+        assert_eq!(crate::greet("World"), "Hello, World! Welcome aboard.");
+    }
+    ```
+
+  * On `2026-03-28 02:15` commit as `Bob <bob@example.com>`
+
+    Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
+
+    ``` text
+    Add tests for the greeting function
+
+    Charlie says tests are important. Bob says tests are boring but necessary.
+    ```
+
+  * Create `Cargo.toml`
+
+    ``` toml
+    [package]
+    name = "project-beta"
+    version = "0.1.0"
+    edition = "2021"
+    ```
+
+  * On `2026-04-01 10:22` commit as `Charlie <charlie@example.com>`
+
+    Two days later
+
+    ``` text
+    Configure Cargo properly
+
+    Finally got the build system right. Took longer than expected.
+    ```
+
+  * Write to `src/lib.rs`
+
+    ``` rust
+    pub fn greet(name: &str) -> String {
+        format!("Hello, {}! Welcome aboard. 🚀", name)
+    }
+    ```
+
+  * On `2026-04-04 11:30` commit as `Alice <alice@example.com>`
+
+    ``` text
+    Merge Beta into Alpha
+
+    Time to combine forces. Bob is thrilled, Charlie is skeptical.
+    ```
+

Tag projects' suites as fixtures

On by Tad Lispy

Now I can do tbb evaluate --exclude fixtures to skip entire suites. But since they now run in a random temporary directory, I can also safely run them. TBH I'm just playing with TBB to get a feeling of it.

I've also improved on project Beta commit messages a bit. They didn't always made sense.

index b40d244..888f0b5 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -15,6 +15,7 @@ tags: [ focus ]
=
=Before you excavate, it might be smart to see what the result would be. You can use `--dry-run` flag for that. Consider the following scenario.
=
+
=  * Develop project `Alpha`
=  * Develop project `Beta`
=  * Create `devlog.toml`
@@ -30,6 +31,8 @@ Before you excavate, it might be smart to see what the result would be. You can
=    ```
=
=  * Run the `excavate --projects-dir . --dry-run` command
+
+    TODO: Use structured data for assertions. Currently the tests are very flaky wrt terminal width.
=  
=  * Expect the output to contain `the header row`
=
index 983c3c0..de87fe6 100644
--- a/spec/projects/alpha.md
+++ b/spec/projects/alpha.md
@@ -1,5 +1,6 @@
=---
=interpreter: nu --stdin spec/interpreters/basic.nu
+tags: [ fixture ]
=---
=
=# Project Alpha
index 462bd76..2970245 100644
--- a/spec/projects/beta.md
+++ b/spec/projects/beta.md
@@ -1,5 +1,6 @@
=---
=interpreter: nu --stdin spec/interpreters/basic.nu
+tags: [ fixture ]
=---
=
=# Project Beta
@@ -46,7 +47,7 @@ parameters:
=    ``` text
=    Add greeting function
=
-    Alice thinks this is going to be a game changer.
+    Bob thinks this is going to be a game changer.
=    ```
=
=  * Write to `src/lib.rs`
@@ -73,7 +74,7 @@ parameters:
=    ``` text
=    Add tests for the greeting function
=
-    Charlie says tests are important. Bob says tests are boring but necessary.
+    Charlie says tests are important. I say tests are boring but necessary.
=    ```
=
=  * Create `Cargo.toml`
@@ -106,8 +107,8 @@ parameters:
=  * On `2026-04-04 11:30` commit as `Alice <alice@example.com>`
=
=    ``` text
-    Merge Beta into Alpha
+    Use a rocket emoji
=
-    Time to combine forces. Bob is thrilled, Charlie is skeptical.
+    To express our enthusiasm for the project. Bob is thrilled, Charlie is skeptical.
=    ```
=