Week 17 of 2026

Development log of Tad Better Behavior

7 items
  1. Bring tbb.nu library from Devlog Excavator
  2. Start writing the fixtures spec
  3. Write some more thoughts about fixtures
  4. Edit the fixtures spec
  5. Add the word "delegations" to the "fixtures" suite
  6. Start the work on a delegations sample
  7. Make the TBB contol process accept delegations

Bring tbb.nu library from Devlog Excavator

On by Tad Lispy

I've been working on the Devlog Excavator project, which is implemented in Nushell. It's fun once I got a grasp of it (and learned to avoid a few sharp edges). To test it I wrote an interpreter - also in Nu.

I think it's smart to distribute the helper libraries for implementing interpreters together with TBB itself. That way users can jump straight to implementing steps, instead of re-inventing the wheel around I/O and JSON parsing. So in the Nushell interpreter of the excavator I separated the shared logic from project specific assertions code. Now I'm bringing this library here, to be distributed along with the Python helper.

This however necessitated a cascade of changes. Since the libraries are to be used in outside projects, they are no longer "samples". I moved both of them to the new /lib/ directory.

This directory is now included in the Nix package, along with the bin/ directory containing the program itself.

If the libraries are distributed to the public, their behavior needs to be specified, and the relevant specs evaluated. So there is a sample suite in the samples/steteful.md document and an interpreter to run it in samples/basic.nu. They are used in a new spec suite spec/nushell-library.md.

To run the sample Nushell interpreter, we of course need the Nushell itself (just like Python is required for the self-check.py and various samples). I've added it to the development environment.

Becasue tbb.nu uses some recently added features, I also had to updated the Nix dependencies to get the latest version of Nushell. It was time to update anyway.

There is another one implemented in Babashka in Enshu's Form to Mail project, that we hope to also bring here too. But that's for another day.

The Python library also moved to lib, so all the interpreters needed to be updated too.

index 790b7aa..f594eed 100644
--- a/flake.lock
+++ b/flake.lock
@@ -19,11 +19,11 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1760971495,
-        "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=",
+        "lastModified": 1774017633,
+        "narHash": "sha256-CWhnwL2M83/ItapPVeJqCevRoQttesYxJ1h0Mo6ZCXs=",
=        "owner": "cachix",
=        "repo": "cachix",
-        "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2",
+        "rev": "e8be573b417f3daa3dd4cb9052178f848e0c9d1d",
=        "type": "github"
=      },
=      "original": {
@@ -33,23 +33,143 @@
=        "type": "github"
=      }
=    },
-    "devenv": {
+    "cachix_2": {
=      "inputs": {
-        "cachix": "cachix",
+        "devenv": [
+          "devenv",
+          "crate2nix"
+        ],
+        "flake-compat": [
+          "devenv",
+          "crate2nix"
+        ],
+        "git-hooks": "git-hooks",
+        "nixpkgs": "nixpkgs"
+      },
+      "locked": {
+        "lastModified": 1767714506,
+        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "cachix_3": {
+      "inputs": {
+        "devenv": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "git-hooks": "git-hooks_2",
+        "nixpkgs": "nixpkgs_2"
+      },
+      "locked": {
+        "lastModified": 1767714506,
+        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "crate2nix": {
+      "inputs": {
+        "cachix": "cachix_2",
+        "crate2nix_stable": "crate2nix_stable",
+        "devshell": "devshell_2",
+        "flake-compat": "flake-compat_2",
+        "flake-parts": "flake-parts_2",
+        "nix-test-runner": "nix-test-runner_2",
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ],
+        "pre-commit-hooks": "pre-commit-hooks_2"
+      },
+      "locked": {
+        "lastModified": 1772186516,
+        "narHash": "sha256-8s28pzmQ6TOIUzznwFibtW1CMieMUl1rYJIxoQYor58=",
+        "owner": "rossng",
+        "repo": "crate2nix",
+        "rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "rossng",
+        "repo": "crate2nix",
+        "rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e",
+        "type": "github"
+      }
+    },
+    "crate2nix_stable": {
+      "inputs": {
+        "cachix": "cachix_3",
+        "crate2nix_stable": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "devshell": "devshell",
=        "flake-compat": "flake-compat",
=        "flake-parts": "flake-parts",
-        "git-hooks": "git-hooks",
+        "nix-test-runner": "nix-test-runner",
+        "nixpkgs": "nixpkgs_3",
+        "pre-commit-hooks": "pre-commit-hooks"
+      },
+      "locked": {
+        "lastModified": 1769627083,
+        "narHash": "sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=",
+        "owner": "nix-community",
+        "repo": "crate2nix",
+        "rev": "7c33e664668faecf7655fa53861d7a80c9e464a2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "ref": "0.15.0",
+        "repo": "crate2nix",
+        "type": "github"
+      }
+    },
+    "devenv": {
+      "inputs": {
+        "cachix": "cachix",
+        "crate2nix": "crate2nix",
+        "flake-compat": "flake-compat_3",
+        "flake-parts": "flake-parts_3",
+        "git-hooks": "git-hooks_3",
=        "nix": "nix",
+        "nixd": "nixd",
=        "nixpkgs": [
=          "nixpkgs"
-        ]
+        ],
+        "rust-overlay": "rust-overlay"
=      },
=      "locked": {
-        "lastModified": 1762889687,
-        "narHash": "sha256-oKvHfeYDZ0LfuHSaFLA0w/dfZ9R6C5W8pCGUjUWawGI=",
+        "lastModified": 1777040077,
+        "narHash": "sha256-21hK/8gia6FbcE2+R49WNCD24yd6Qf0VPpiYi1qEI3I=",
=        "owner": "cachix",
=        "repo": "devenv",
-        "rev": "3b4fb549962342c928aae1bbea3a13f0eeed2703",
+        "rev": "0b0246b7fed27e788d639cb82364c452b405970c",
=        "type": "github"
=      },
=      "original": {
@@ -58,14 +178,87 @@
=        "type": "github"
=      }
=    },
+    "devshell": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768818222,
+        "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
+    "devshell_2": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768818222,
+        "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
=    "flake-compat": {
+      "locked": {
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+        "revCount": 69,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
+      }
+    },
+    "flake-compat_2": {
+      "locked": {
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+        "revCount": 69,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
+      }
+    },
+    "flake-compat_3": {
=      "flake": false,
=      "locked": {
-        "lastModified": 1761588595,
-        "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+        "lastModified": 1767039857,
+        "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
=        "owner": "edolstra",
=        "repo": "flake-compat",
-        "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+        "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
=        "type": "github"
=      },
=      "original": {
@@ -78,15 +271,60 @@
=      "inputs": {
=        "nixpkgs-lib": [
=          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768135262,
+        "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "flake-parts_2": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "crate2nix",
=          "nixpkgs"
=        ]
=      },
=      "locked": {
-        "lastModified": 1760948891,
-        "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
+        "lastModified": 1768135262,
+        "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
=        "owner": "hercules-ci",
=        "repo": "flake-parts",
-        "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
+        "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "flake-parts_3": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1775087534,
+        "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
=        "type": "github"
=      },
=      "original": {
@@ -99,20 +337,82 @@
=      "inputs": {
=        "flake-compat": [
=          "devenv",
+          "crate2nix",
+          "cachix",
=          "flake-compat"
=        ],
=        "gitignore": "gitignore",
=        "nixpkgs": [
=          "devenv",
+          "crate2nix",
+          "cachix",
=          "nixpkgs"
=        ]
=      },
=      "locked": {
-        "lastModified": 1760663237,
-        "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=",
+        "lastModified": 1765404074,
+        "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
=        "owner": "cachix",
=        "repo": "git-hooks.nix",
-        "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
+        "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "git-hooks_2": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_2",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1765404074,
+        "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "git-hooks_3": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_5",
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1775585728,
+        "narHash": "sha256-8Psjt+TWvE4thRKktJsXfR6PA/fWWsZ04DVaY6PUhr4=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "580633fa3fe5fc0379905986543fd7495481913d",
=        "type": "github"
=      },
=      "original": {
@@ -122,6 +422,102 @@
=      }
=    },
=    "gitignore": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "cachix",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_2": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_3": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "pre-commit-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_4": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "pre-commit-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_5": {
=      "inputs": {
=        "nixpkgs": [
=          "devenv",
@@ -169,27 +565,163 @@
=        ]
=      },
=      "locked": {
-        "lastModified": 1761648602,
-        "narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=",
+        "lastModified": 1776511668,
+        "narHash": "sha256-g2KEBuHpc3a56c+jPcg0+w6LSuIj6f+zzdztLCOyIhc=",
=        "owner": "cachix",
=        "repo": "nix",
-        "rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6",
+        "rev": "42d4b7de21c15f28c568410f4383fa06a8458a40",
=        "type": "github"
=      },
=      "original": {
=        "owner": "cachix",
-        "ref": "devenv-2.30.6",
+        "ref": "devenv-2.34",
=        "repo": "nix",
=        "type": "github"
=      }
=    },
+    "nix-test-runner": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1588761593,
+        "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "type": "github"
+      }
+    },
+    "nix-test-runner_2": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1588761593,
+        "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "type": "github"
+      }
+    },
+    "nixd": {
+      "inputs": {
+        "flake-parts": [
+          "devenv",
+          "flake-parts"
+        ],
+        "nixpkgs": "nixpkgs_4",
+        "treefmt-nix": "treefmt-nix"
+      },
+      "locked": {
+        "lastModified": 1776341634,
+        "narHash": "sha256-L//ltP2o5+BnuK+KEulbi2gGeDpyyex6SkXLZcGQ/Ac=",
+        "owner": "nix-community",
+        "repo": "nixd",
+        "rev": "951e98e2025c47614f5249556ecf509b0ea35b51",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "nixd",
+        "type": "github"
+      }
+    },
=    "nixpkgs": {
=      "locked": {
-        "lastModified": 1761313199,
-        "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=",
+        "lastModified": 1765186076,
+        "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs-src": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1776329215,
+        "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b86751bc4085f48661017fa226dee99fab6c651b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1765186076,
+        "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_3": {
+      "locked": {
+        "lastModified": 1769433173,
+        "narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "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": {
+      "inputs": {
+        "nixpkgs-src": "nixpkgs-src"
+      },
+      "locked": {
+        "lastModified": 1776852779,
+        "narHash": "sha256-WwO/ITisCXwyiRgtktZgv3iGhAGO+IB5Av4kKCwezR0=",
=        "owner": "cachix",
=        "repo": "devenv-nixpkgs",
-        "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff",
+        "rev": "ec3063523dcd911aeadb50faa589f237cdab5853",
=        "type": "github"
=      },
=      "original": {
@@ -199,13 +731,92 @@
=        "type": "github"
=      }
=    },
+    "pre-commit-hooks": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_3",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1769069492,
+        "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "type": "github"
+      }
+    },
+    "pre-commit-hooks_2": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_4",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1769069492,
+        "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "type": "github"
+      }
+    },
=    "root": {
=      "inputs": {
=        "devenv": "devenv",
-        "nixpkgs": "nixpkgs",
+        "nixpkgs": "nixpkgs_5",
=        "systems": "systems"
=      }
=    },
+    "rust-overlay": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1776741231,
+        "narHash": "sha256-k9G98qzn+7npROUaks8VqCFm7cFtEG8ulQLBBo5lItg=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "02061303f7c4c964f7b4584dabd9e985b4cd442b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
=    "systems": {
=      "locked": {
=        "lastModified": 1681028828,
@@ -220,6 +831,28 @@
=        "repo": "default",
=        "type": "github"
=      }
+    },
+    "treefmt-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "nixd",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1775636079,
+        "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "type": "github"
+      }
=    }
=  },
=  "root": "root",
index 3807d0f..e90623f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -35,6 +35,7 @@
=                    pkgs.python3Packages.python-lsp-server
=                    pkgs.python3Packages.python-lsp-ruff
=                    pkgs.python3Packages.pylsp-rope
+                    pkgs.nushell
=                  ];
=                }
=              ];
@@ -55,6 +56,9 @@
=              cargoLock = {
=                lockFile = ./Cargo.lock;
=              };
+              postInstall = ''
+                cp --recursive lib $out/lib
+              '';
=            };
=          });
=    };
new file mode 100644
index 0000000..0bf7a1d
--- /dev/null
+++ b/lib/tbb.nu
@@ -0,0 +1,146 @@
+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
+    }
+}
similarity index 100%
rename from spec/tbb.py
rename to lib/tbb.py
index 185958d..8990f32 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -78,7 +78,7 @@ We use this document mostly to keep track of our progress and a scratchpad for i
=      - [ ] Run unit tests autmatically
=    - [ ] for Clojure
=    - [ ] For POSIX shells
-    - [ ] For NuShell
+    - [x] For NuShell
=- [ ] Built-in interpreter (`tbb automation`)
=  - [ ] HTTP client
=  - [ ] Web Driver client
new file mode 100644
index 0000000..65360b7
--- /dev/null
+++ b/samples/basic.nu
@@ -0,0 +1,34 @@
+use ../lib/tbb.nu
+
+def main [] {
+    tbb run --initial-state { twists: 0 } {
+        "Do something {0} and then {1} for {2} hours": { |how, what, how_long, data|
+            $"OK boss. I will do something ($how) for ($how_long) hours and then I'll ($what)"
+            | tbb observe text --transform { str upcase }
+            $data.code_blocks
+            | first
+            | tbb observe snippet --caption "Received code block"
+            | ignore # Do not send any state update.
+        },
+        "Do the twist {0} times": { |how_many, data|
+            # All arguments (except data) are passed as strings
+            # TODO: Can we cast them based on closure parameter types?
+            $how_many | into int | let how_many
+
+            # This step demonstrates use of state. We count the total number of twists.
+            for n in 1..$how_many {
+                let done_twists = $n + $data.state.twists
+
+                $"Doing the twist ($done_twists) 🕺"
+                | tbb observe text 
+
+                if $done_twists > 5 {
+                    error make "That's too many! My head is spinning 🤣"
+                }
+            }
+
+            # Update the state for other steps. This record will be merged into the previous state.
+            { twists: ($data.state.twists + $how_many) }
+        }
+    }
+}
index eee3d46..2635d23 100644
--- a/samples/basic.py
+++ b/samples/basic.py
@@ -3,8 +3,8 @@
=import json
=import unittest
=
-import spec.tbb as tbb
-from spec.tbb import step, log, send_snippet, send_text
+import lib.tbb as tbb
+from lib.tbb import step, log, send_snippet, send_text
=
=
=# Nice assertions with helpful error messages
new file mode 100644
index 0000000..50d9872
--- /dev/null
+++ b/samples/stateful.md
@@ -0,0 +1,29 @@
+---
+interpreter: nu --stdin samples/basic.nu
+---
+
+# Stateful
+
+This sample is designed to check if the interpreter can maintain state between steps. It's intentionally broken - the last step will make the interpreter err.
+
+``` yaml tbb
+tags: [ not-implemented ] # It's intentionally broken
+```
+
+
+## Count the Twists
+
+  * Do the twist `2` times
+
+    That should be easy enough.
+    
+    
+  * Do something `nice` and then `sleep` for `2` hours
+  
+    ``` text
+    Zzzzz.....
+    ```
+
+  * Do the twist `4` times
+
+    The interpreter is hard-wired to fail after a total of 5 twists. We already did 2 in the first step, so this step should produce an error.
index 4787ef4..a5e5eec 100644
--- a/spec/basic-usage.md
+++ b/spec/basic-usage.md
@@ -201,7 +201,7 @@ When a directory is given as the last argument, load all documents inside (recur
=
=    This step also succeeded, producing a Link observation. These observations can be useful, because sometimes a root cause of a failure is in a proceeding steps, not the one that actually failed.
=
-  * The output will contain `an expanded successful step with a various observations` block
+  * The output will contain `an expanded successful step with various observations` block
=  
=    ``` text
=        ⊞ The following tables map words to their lengths
new file mode 100644
index 0000000..4b2deed
--- /dev/null
+++ b/spec/nushell-library.md
@@ -0,0 +1,144 @@
+---
+interpreter: "python -m spec.self-check"
+---
+
+# Nushell Library
+
+There is a library for implementing interpreters in [Nushell](https://www.nushell.sh/). This spec demonstrates how to use it. Notice that this suite itself is implemented using Python. For an example of Nushell interpreter, see `samples/basic.nu`.
+
+
+## Evaluating a spec from a single document
+
+  * Run the program with `evaluate samples/stateful.md` command line arguments
+
+     The `stateful.md` suit is designed to demonstrate managing state across steps, but also intentionally wrong. After doing many twists it will fail. It should be reflected in the status code.
+
+  * The output will contain `the suite header` block
+  
+    ```text
+    Stateful
+    tagged: not-implemented
+    interpreter: nu --stdin samples/basic.nu
+    source: samples/stateful.md
+    ```
+  
+  * The output will contain `the twists scenario` block
+  
+    ```text
+      x Count the Twists
+        source: samples/stateful.md:14-30
+    ```
+
+  * The output will contain `an expanded successful step with a snippet observation` block
+
+    In this funny scenario we are counting the twists done so far. This requires the interpreter to keep some state across the steps. The interpreter reports the changing state to TBB, and because the scenario ultimately fails, the final report shows those observations. We are leveraging Nushell's ability to render nice ASCII tables to present the state in a very readable format. Also notice the caption in front of the code block.
+
+    ``` text
+        ⊞ Do the twist 2 times
+          arguments: ["2"]
+          source: samples/stateful.md:16-20
+    
+          State before the step:
+    
+          \``` table
+          ╭────────┬───╮
+          │ twists │ 0 │
+          ╰────────┴───╯
+          \```
+    ```
+
+  * The output will contain `repeated text observations` block
+  
+    ``` text
+          > Doing the twist 1 🕺
+
+
+          > Doing the twist 2 🕺
+    ```
+
+
+  * The output will contain `an expanded successful step with a more snippets` block
+
+    The second step doesn't increase the number of twists.
+    
+    ``` text
+        ⊞ Do something nice and then sleep for 2 hours
+          arguments: ["nice", "sleep", "2"], code blocks: 1
+          source: samples/stateful.md:21-26
+
+          State update to be merged:
+
+          \``` table
+          ╭────────┬───╮
+          │ twists │ 2 │
+          ╰────────┴───╯
+          \```
+
+
+          State before the step:
+
+          \``` table
+          ╭────────┬───╮
+          │ twists │ 2 │
+          ╰────────┴───╯
+          \```
+    ```
+
+    This step also succeeded, producing a two observations about the state: an update from the previous step and the resulting state. These observations can be useful, because sometimes a root cause of a failure is in a proceeding steps, not the one that actually failed.
+
+  * The output will contain `a failed step with no update` block
+  
+      The previous step didn't produce any update, but that's not the problem.
+      
+    ``` text
+        ⊠ Do the twist 4 times
+          arguments: ["4"]
+          source: samples/stateful.md:27-29
+
+          The value returned by the step is not a record; Not updating:
+
+          \``` table
+          nothing
+          \```
+
+
+          State before the step:
+
+          \``` table
+          ╭────────┬───╮
+          │ twists │ 2 │
+          ╰────────┴───╯
+          \```
+    ```
+    
+  * The output will contain `explanation of the failure` block
+
+    ``` text
+          > Doing the twist 3 🕺
+
+
+          > Doing the twist 4 🕺
+
+
+          > Doing the twist 5 🕺
+
+
+          > Doing the twist 6 🕺
+
+
+          That's too many! My head is spinning 🤣
+
+          Error: nu::shell::error
+
+            x That's too many! My head is spinning 🤣
+              ,-[/home/tad/Projects/tad-better-behavior/samples/basic.nu:26:21]
+           25 |                 if $done_twists > 5 {
+           26 |                     error make "That's too many! My head is spinning 🤣"
+              :                     ^^^^^^^^^^
+           27 |                 }
+              `----
+    ```
+    
+    Aha! Too much fun will make your head spin.
+    
+  * The exit code should be `1`
index 94e628f..8a417a4 100644
--- a/spec/self-check.py
+++ b/spec/self-check.py
@@ -7,8 +7,8 @@ import subprocess
=import unittest
=from textwrap import dedent, indent
=
-import spec.tbb as tbb
-from spec.tbb import step, log, indent_tail, get_at
+import lib.tbb as tbb
+from lib.tbb import step, log, indent_tail, get_at
=
=
=base_command = "tbb"

Start writing the fixtures spec

On by Tad Lispy

Last week, while working on Devlog Excavator, I came up with an idea for fixtures - a mechanism to re-use TBB scenarios with different parameters. This would be very useful for testing different aspects of SUT in isolation. Current model requires either repeating the same steps in multiple scenarios, or testing everything in the same scenario to re-use the "given-when" steps.

Now I started to write a spec for the fixtures. There are some fundamental design questions yet to be answered, but I think the broad idea is sound.

index 8990f32..c075332 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -83,7 +83,6 @@ We use this document mostly to keep track of our progress and a scratchpad for i
=  - [ ] HTTP client
=  - [ ] Web Driver client
=  - [ ] E-Mail client
-  - [ ] Recursive calls (call `tbb evaluate` and such)
=- [ ] Capture more data in reports
=  - [ ] Attachments (screenshots, videos, datasets, etc.)
=  - [ ] Performance data (interpreters' startup times, steps' durations)
@@ -91,4 +90,6 @@ We use this document mostly to keep track of our progress and a scratchpad for i
=- [ ] Better reporters
=    - [ ] TUI
=    - [ ] Web
-
+- [ ] Fixtures
+  - [ ] Recursive calls
+  - [ ] Parameters
new file mode 100644
index 0000000..4f35260
--- /dev/null
+++ b/spec/fixtures.md
@@ -0,0 +1,56 @@
+---
+interpreter: "python -m spec.self-check"
+---
+
+# Fixtures
+
+In response to the "execute step" command, interpreters can request to run a fixture. Any scenario can be treated as a fixture, but scenarios with `fixture: true` property are not run, unless required by an interpreter.
+
+Scenarios can have parameters, defined as follows:
+
+``` markdown
+    \``` yaml tbb
+    parameters:
+        name: Placeholder
+    \```
+```
+
+When an interpreter requests a to run another scenario as a fixture, it can pass values to substitute the parameters with.
+
+``` json
+{ 
+    "parameters": {
+        "name": "Actual value"
+    } 
+}
+```
+
+When running the requested scenario, TBB will substitute every argument with the value `"Placeholder"` with the `"Actual value"`. Notice that the substitution is done by value, not by name. The names are given to values to create a substitution map. This way we avoid any special syntax for fixtures. Reusable scenarios are written with default (or placeholder) values, and re-used with substitutions. We maintain the natural, example driven manner of writing specs.
+
+A step that would request another scenario would look something like this:
+
+``` markdown
+  * Run the `setup a new project` scenario with the following parameters
+  
+    | parameter    | value        |
+    |--------------|--------------|
+    | project name | Project beta |
+    | path         | beta/        |
+```
+
+In the report, fixture steps should be immediately followed by the step that requested them to run, as if they were integral part of the requesting scenario.
+
+> Before or after? It's a bit weird to say "Run the ... scenario" after it's already completed. On the other hand, the success or failure of this step depends on the requested scenario, so probably after.
+
+> Is the requesting step notified of the result of running the fixture? If so, how? It would make sense to have some feedback mechanism. Maybe leave it up to the interpreters? But then what to do if the requesting step immediately reports a success? Maybe the requesting step should be excluded from the report, it's result ignored? Replaced by the fixture. Then it would be simpler to make the request another variant of step status, next to `Ok`, `Failed`, and `NotEvaluated`. We could call it `RequestedFixtures` with a vector of requests. This would meke the report very transparent. In that case the requesting step should be reported before the fixtures.
+
+The fixtures are executed in a separate process, just like any other scenario. This allows to use specialized interpreters to run fixtures, other than the requesting interpreter. Another effect of this is that they don't automatically share any state. If interpreters need to pass any data other than parameters, they need to implement an appropriate mechanism (like pre-arranged file, a database, etc.).
+
+> But if the interpreter is different, then the report should include some traces of this. Can observations be used for that?
+
+
+A step can request multiple fixtures to run. In that case, they should run consecutively, in order of requests.
+
+A fixture can request another fixture to run. 
+
+> Should there be limits to the recursion?

Write some more thoughts about fixtures

On by Tad Lispy

I'm beginning to think that the word is inadequate. It's more of a delegation than a fixture. It can be used to replicate fixtures from other systems, but it can have broader applications.

index 4f35260..2a839e3 100644
--- a/spec/fixtures.md
+++ b/spec/fixtures.md
@@ -48,9 +48,14 @@ The fixtures are executed in a separate process, just like any other scenario. T
=
=> But if the interpreter is different, then the report should include some traces of this. Can observations be used for that?
=
+While awaiting the fixtures to complete, the interpreter process is kept alive, but no messages should be passed. Once the fixtures are successfully completed, the control process will send the next step. If they fail, the interpreter will be terminated, as if one of it's own steps failed.
=
=A step can request multiple fixtures to run. In that case, they should run consecutively, in order of requests.
=
-A fixture can request another fixture to run. 
+A fixture can request another fixture to run. This way the process can be recursive. Failure will result in termination of a whole stack of interpreters. The report should explain the situation, including the stack of requests.
=
=> Should there be limits to the recursion?
+
+> What if a requested scenario is missing? Or there is a typo?
+
+> What if the parameters don't match?

Edit the fixtures spec

On by Tad Lispy

After a good night sleep and a breakfast I have a bit more clarity on the "fixtures" system. Indeed it should be called "delegations".

index 2a839e3..90b13ec 100644
--- a/spec/fixtures.md
+++ b/spec/fixtures.md
@@ -2,9 +2,9 @@
=interpreter: "python -m spec.self-check"
=---
=
-# Fixtures
+# Delegation and fixtures
=
-In response to the "execute step" command, interpreters can request to run a fixture. Any scenario can be treated as a fixture, but scenarios with `fixture: true` property are not run, unless required by an interpreter.
+In response to the "execute step" command, interpreters can request to run one or more scenarios. This is a feature sometimes known as "fixtures", but it has broader application. Any scenario can be re-used this way, but scenarios with `fixture: true` property are not run, unless required by an interpreter.
=
=Scenarios can have parameters, defined as follows:
=
@@ -15,7 +15,7 @@ Scenarios can have parameters, defined as follows:
=    \```
=```
=
-When an interpreter requests a to run another scenario as a fixture, it can pass values to substitute the parameters with.
+When an interpreter requests to run another scenario as a fixture, it can pass values to substitute the parameters with.
=
=``` json
={ 
@@ -38,21 +38,16 @@ A step that would request another scenario would look something like this:
=    | path         | beta/        |
=```
=
-In the report, fixture steps should be immediately followed by the step that requested them to run, as if they were integral part of the requesting scenario.
=
-> Before or after? It's a bit weird to say "Run the ... scenario" after it's already completed. On the other hand, the success or failure of this step depends on the requested scenario, so probably after.
+The delegation request is a terminating message of the step, just like `Ok` and `Failure`. While awaiting a delegation to complete, the delegating interpreter process is kept alive, but no messages should be passed until the next step. Once the delegation is successfully completed, the control process will send the next step and the interpreter will resume. If a delegation fail, the interpreter will be terminated, as if one of it's own steps failed.
=
-> Is the requesting step notified of the result of running the fixture? If so, how? It would make sense to have some feedback mechanism. Maybe leave it up to the interpreters? But then what to do if the requesting step immediately reports a success? Maybe the requesting step should be excluded from the report, it's result ignored? Replaced by the fixture. Then it would be simpler to make the request another variant of step status, next to `Ok`, `Failed`, and `NotEvaluated`. We could call it `RequestedFixtures` with a vector of requests. This would meke the report very transparent. In that case the requesting step should be reported before the fixtures.
+In the report, the delegating step should be immediately followed by the requested steps, as if they were integral part of a single scenario. The delegating step is marked with the telephone sigil (☎). The block should list all the requested scenarios, with their result sigils, source position and interpreters (so combining parts of the suite block and parts of the scenario block). The delegation block should be appropriately indented, not to be confused with a top-level scenario. But the delegate steps should not be indented to avoid rightward drift. Conceptually they are part of the delegating scenario.
=
-The fixtures are executed in a separate process, just like any other scenario. This allows to use specialized interpreters to run fixtures, other than the requesting interpreter. Another effect of this is that they don't automatically share any state. If interpreters need to pass any data other than parameters, they need to implement an appropriate mechanism (like pre-arranged file, a database, etc.).
+The delegate scenarios are executed in a separate process, using the interpreter specified in their suite, just like any other scenarios. This allows to use specialized interpreters to run fixtures, other than the delegating interpreter. Another effect of this is that they don't automatically share any state. If interpreters need to pass any data other than parameters, they need to implement an appropriate mechanism (like pre-arranged file, named pipe, a database, etc.).
=
-> But if the interpreter is different, then the report should include some traces of this. Can observations be used for that?
+A step can delegate work to multiple scenarios. In that case, they should run consecutively, in order of requests.
=
-While awaiting the fixtures to complete, the interpreter process is kept alive, but no messages should be passed. Once the fixtures are successfully completed, the control process will send the next step. If they fail, the interpreter will be terminated, as if one of it's own steps failed.
-
-A step can request multiple fixtures to run. In that case, they should run consecutively, in order of requests.
-
-A fixture can request another fixture to run. This way the process can be recursive. Failure will result in termination of a whole stack of interpreters. The report should explain the situation, including the stack of requests.
+A delegate can request another delegation. This way the process can be recursive. Failure will result in termination of a whole stack of interpreters. The report should explain the situation, including the stack of requests.
=
=> Should there be limits to the recursion?
=

Add the word "delegations" to the "fixtures" suite

On by Tad Lispy

To align the filename with the new title and content.

similarity index 100%
rename from spec/fixtures.md
rename to spec/delegations-and-fixtures.md

Start the work on a delegations sample

On by Tad Lispy

Now that I have a basic picture of how delegations should work, I need to code some proof-of-concept. Let's do it front to back - pretend that the API is there and then slowly build the required elements.

new file mode 100644
index 0000000..a3ccc17
--- /dev/null
+++ b/samples/delegating.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+import unittest
+
+import lib.tbb as tbb
+from lib.tbb import step, log, send_snippet, send_text
+
+
+# Nice assertions with helpful error messages
+tester = unittest.TestCase()
+
+@step("Run the {0} scenario from the {1} suite")
+def step_implementation_01(scenario: str, suite: str, **kwargs):
+    tbb.delegate({
+        "suite": suite,
+        "scenario": scenario
+    })
+
+tbb.ready()
new file mode 100644
index 0000000..27b881a
--- /dev/null
+++ b/samples/delegations.md
@@ -0,0 +1,11 @@
+---
+interpreter: "python -m samples.delegating"
+---
+
+# Let's delegate some work
+
+This sample and related interpreter demonstrates how to use delegation feature of TBB.
+
+## Simple delegation
+
+  * Run the `Arithmetic` scenario from the `samples/basic.md` suite

Make the TBB contol process accept delegations

On by Tad Lispy

And print them in the report, but do nothing else yet - i.e. do not actually run the delegated scenarios.

For now I'm just trying to figure out the right API. I diverged from the spec I wrote in the morning in that delegation is detached from the ultimate result of the step. It works more like observation, in that an interpreter can request delegations multiple times, and then report the status (Ok or Failure). TBB will run delegated scenarios after the delegating step is successfully complete.

index 8e8f0db..4e77e0f 100644
--- a/lib/tbb.py
+++ b/lib/tbb.py
@@ -185,6 +185,11 @@ def ready():
=            break
=
=
+def delegate(suite: str, scenario: str, **kwargs):
+    send(
+        {"type": "Delegate", "suite": suite, "scenario": scenario, "parameters": kwargs}
+    )
+
=def send_text(content):
=    """ Send a fragment of text to be displayed in a TBB report
=    """
index a3ccc17..fb31490 100644
--- a/samples/delegating.py
+++ b/samples/delegating.py
@@ -5,15 +5,13 @@ import unittest
=import lib.tbb as tbb
=from lib.tbb import step, log, send_snippet, send_text
=
-
=# Nice assertions with helpful error messages
=tester = unittest.TestCase()
=
=@step("Run the {0} scenario from the {1} suite")
=def step_implementation_01(scenario: str, suite: str, **kwargs):
-    tbb.delegate({
-        "suite": suite,
-        "scenario": scenario
-    })
+    tbb.delegate(suite, scenario)
+    tbb.delegate(suite, "Hardcoded scenario title")
+
=
=tbb.ready()
index 6bc03b3..c65ba7f 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -4,10 +4,11 @@ use anyhow::{Context, anyhow};
=use colored::Colorize;
=use indoc::formatdoc;
=use serde::{Deserialize, Serialize, Serializer};
-use std::collections::BTreeSet;
+use std::collections::{BTreeSet, HashMap};
=use std::fmt::Display;
=use std::io::{BufRead, BufReader, LineWriter, Write};
=use std::ops::Not;
+use std::path::PathBuf;
=use std::process::{Command, Stdio};
=
=#[derive(Debug, Default)]
@@ -157,6 +158,7 @@ impl<'a> ScenarioReport<'a> {
=        'steps_loop: for &mut StepReport {
=            step,
=            ref mut observations,
+            ref mut delegations,
=            ref mut status,
=        } in self.steps.iter_mut()
=        {
@@ -189,6 +191,16 @@ impl<'a> ScenarioReport<'a> {
=                    InterpreterMessage::Link { url, label } => {
=                        observations.push(Observation::Link { url, label });
=                    }
+                    InterpreterMessage::Delegate {
+                        suite,
+                        scenario,
+                        parameters,
+                    } => delegations.push(Delegation {
+                        suite: PathBuf::from(suite),
+                        scenario,
+                        parameters: parameters,
+                        status: ScenarioStatus::Pending,
+                    }),
=                    InterpreterMessage::Success => {
=                        log::debug!("Step executed successfully: {step:#?}");
=                        *status = StepStatus::Ok;
@@ -283,6 +295,7 @@ pub struct StepReport<'a> {
=    step: &'a Step,
=    status: StepStatus,
=    observations: Vec<Observation>,
+    delegations: Vec<Delegation>,
=}
=
=#[derive(Debug, Serialize)]
@@ -337,6 +350,21 @@ impl Display for Observation {
=    }
=}
=
+#[derive(Debug, Serialize)]
+struct Delegation {
+    suite: PathBuf,
+    scenario: String,
+    parameters: HashMap<String, String>,
+    status: ScenarioStatus,
+}
+
+impl Display for Delegation {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let sigil = "☎";
+        writeln!(f, "{sigil} {}", self.scenario)
+    }
+}
+
=#[derive(Debug, Eq, PartialEq, Serialize)]
=pub enum StepStatus {
=    Ok,
@@ -430,6 +458,7 @@ impl Display for EvaluationReport<'_> {
=                            step,
=                            status,
=                            observations,
+                            delegations,
=                        },
=                    ) in steps.iter().enumerate()
=                    {
@@ -460,6 +489,15 @@ impl Display for EvaluationReport<'_> {
=                            indentation = "".indent(4),
=                        )?;
=
+                        if delegations.len() != 0 {
+                            let heading = format!("\n\nDelegations:\n").indent(6).bold().dimmed();
+                            writeln!(f, "{heading}")?;
+                        }
+
+                        for delegation in delegations {
+                            writeln!(f, "{}", delegation.to_string().indent(6))?;
+                        }
+
=                        for observation in observations {
=                            writeln!(f, "\n{}", observation.to_string().indent(6))?;
=                        }
@@ -498,6 +536,7 @@ impl<'a> From<&'a Step> for StepReport<'a> {
=            step,
=            status: StepStatus::NotEvaluated,
=            observations: Vec::default(),
+            delegations: Vec::default(),
=        }
=    }
=}
@@ -522,6 +561,11 @@ pub enum InterpreterMessage {
=        url: String,
=        label: Option<String>,
=    },
+    Delegate {
+        suite: String,
+        scenario: String,
+        parameters: HashMap<String, String>,
+    },
=    Success,
=    Failure {
=        reason: String,