livepilot 1.9.9 → 1.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +45 -0
  4. package/CODE_OF_CONDUCT.md +27 -0
  5. package/CONTRIBUTING.md +131 -0
  6. package/README.md +119 -395
  7. package/SECURITY.md +48 -0
  8. package/bin/livepilot.js +41 -1
  9. package/livepilot/.Codex-plugin/plugin.json +8 -0
  10. package/livepilot/.claude-plugin/plugin.json +1 -1
  11. package/livepilot/commands/beat.md +18 -6
  12. package/livepilot/commands/sounddesign.md +6 -5
  13. package/livepilot/skills/livepilot-core/SKILL.md +30 -7
  14. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  15. package/livepilot/skills/livepilot-release/SKILL.md +7 -4
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/LivePilot_Analyzer.maxpat +378 -4
  18. package/m4l_device/livepilot_bridge.js +413 -184
  19. package/mcp_server/__init__.py +1 -1
  20. package/mcp_server/connection.py +35 -0
  21. package/mcp_server/m4l_bridge.py +55 -5
  22. package/mcp_server/server.py +16 -29
  23. package/mcp_server/tools/_perception_engine.py +26 -3
  24. package/mcp_server/tools/_theory_engine.py +36 -5
  25. package/mcp_server/tools/analyzer.py +62 -17
  26. package/mcp_server/tools/automation.py +4 -1
  27. package/mcp_server/tools/devices.py +199 -10
  28. package/mcp_server/tools/generative.py +4 -1
  29. package/mcp_server/tools/harmony.py +16 -3
  30. package/mcp_server/tools/notes.py +4 -1
  31. package/mcp_server/tools/perception.py +27 -1
  32. package/mcp_server/tools/scenes.py +4 -1
  33. package/mcp_server/tools/theory.py +15 -7
  34. package/package.json +1 -1
  35. package/remote_script/LivePilot/__init__.py +1 -1
  36. package/remote_script/LivePilot/arrangement.py +13 -116
  37. package/remote_script/LivePilot/diagnostics.py +81 -5
  38. package/remote_script/LivePilot/router.py +22 -0
  39. package/remote_script/LivePilot/server.py +25 -13
package/SECURITY.md ADDED
@@ -0,0 +1,48 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 1.9.x | Yes |
8
+ | < 1.9 | No |
9
+
10
+ ## Architecture Security Notes
11
+
12
+ LivePilot operates through three local interfaces:
13
+
14
+ - **TCP 9878** — MCP server ↔ Ableton Remote Script (localhost only)
15
+ - **UDP 9880** — M4L Analyzer → MCP server (localhost only)
16
+ - **OSC 9881** — MCP server → M4L Analyzer (localhost only)
17
+
18
+ All communication is **local only** — no ports are exposed to the network.
19
+ The MCP server runs via stdio transport (stdin/stdout) and does not open HTTP endpoints.
20
+
21
+ ## Reporting a Vulnerability
22
+
23
+ If you discover a security vulnerability, please report it responsibly:
24
+
25
+ 1. **Do not** open a public issue
26
+ 2. Email **security@pilotstudio.dev** with:
27
+ - A description of the vulnerability
28
+ - Steps to reproduce
29
+ - Potential impact
30
+ 3. You will receive an acknowledgment within 48 hours
31
+ 4. We will work with you to understand and address the issue before any public disclosure
32
+
33
+ If you don't have access to the email, you can use [GitHub's private vulnerability reporting](https://github.com/dreamrec/LivePilot/security/advisories/new).
34
+
35
+ ## Scope
36
+
37
+ The following are in scope:
38
+
39
+ - Remote code execution through MCP tool inputs
40
+ - Path traversal in file-handling tools (MIDI I/O, sample loading)
41
+ - Unauthorized access to Ableton session data
42
+ - Denial of service against the MCP server or Remote Script
43
+
44
+ The following are out of scope:
45
+
46
+ - Issues requiring physical access to the machine
47
+ - Issues in Ableton Live itself (report to Ableton)
48
+ - Issues in MCP clients (report to the client maintainer)
package/bin/livepilot.js CHANGED
@@ -55,6 +55,31 @@ function venvPython() {
55
55
  return path.join(VENV_DIR, isWin ? "Scripts" : "bin", isWin ? "python.exe" : "python3");
56
56
  }
57
57
 
58
+ function findOtherLiveClient(host, port) {
59
+ try {
60
+ const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`], {
61
+ encoding: "utf-8",
62
+ timeout: 3000,
63
+ stdio: ["pipe", "pipe", "ignore"],
64
+ });
65
+ const target = `->${host}:${port}`;
66
+ const lines = out.trim().split("\n").slice(1);
67
+ for (const line of lines) {
68
+ if (!line.includes(target) || !line.includes("(ESTABLISHED)")) {
69
+ continue;
70
+ }
71
+ const parts = line.trim().split(/\s+/);
72
+ const pid = parseInt(parts[1], 10);
73
+ if (!Number.isNaN(pid) && pid !== process.pid) {
74
+ return `PID ${pid} (${parts[0]})`;
75
+ }
76
+ }
77
+ } catch {
78
+ // best-effort only
79
+ }
80
+ return null;
81
+ }
82
+
58
83
  // ---------------------------------------------------------------------------
59
84
  // Virtual environment bootstrap
60
85
  // ---------------------------------------------------------------------------
@@ -133,6 +158,13 @@ function checkStatus() {
133
158
  if (resp.ok === true && resp.result && resp.result.pong) {
134
159
  console.log(" Ableton Live: connected on %s:%d", HOST, PORT);
135
160
  ok = true;
161
+ } else if (resp.ok === false && resp.error && resp.error.code === "STATE_ERROR") {
162
+ console.log(
163
+ " Ableton Live: reachable, but another LivePilot client is already connected"
164
+ );
165
+ if (resp.error.message) {
166
+ console.log(" Detail: %s", resp.error.message);
167
+ }
136
168
  } else {
137
169
  console.log(" Ableton Live: unexpected response:", JSON.stringify(resp));
138
170
  }
@@ -145,7 +177,15 @@ function checkStatus() {
145
177
  });
146
178
 
147
179
  sock.on("timeout", () => {
148
- console.log(" Ableton Live: connection timed out");
180
+ const otherClient = findOtherLiveClient(HOST, PORT);
181
+ if (otherClient) {
182
+ console.log(
183
+ " Ableton Live: reachable, but another LivePilot client appears connected (%s)",
184
+ otherClient
185
+ );
186
+ } else {
187
+ console.log(" Ableton Live: connection timed out");
188
+ }
149
189
  sock.destroy();
150
190
  resolve(false);
151
191
  });
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "livepilot",
3
+ "version": "1.9.11",
4
+ "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
+ "author": {
6
+ "name": "Pilot Studio"
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.9",
3
+ "version": "1.9.11",
4
4
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -8,11 +8,23 @@ Guide the user through creating a beat from scratch. Follow these steps:
8
8
  1. **Ask about the vibe** — genre, tempo range, mood, reference tracks
9
9
  2. **Set up the session** — `set_tempo`, create tracks for drums/bass/harmony/melody with `create_midi_track`, name and color them
10
10
  3. **Load instruments** — use `find_and_load_device` for appropriate instruments per track
11
- 4. **Program drums first** — create a clip, add kick/snare/hat patterns with `add_notes`
12
- 5. **Add bass** create clip, program a bassline that locks with the kick
13
- 6. **Add harmony** chords or pads that set the mood
14
- 7. **Add melody** top-line or lead element
15
- 8. **Mix** balance levels with `set_track_volume` and `set_track_pan`
16
- 9. **Fire the scene** to listen, iterate based on feedback
11
+ 4. **Verify device health** — after loading, run `get_device_info` on each loaded device. A `parameter_count` of 0 or 1 on AU/VST plugins means the plugin failed to initialize (dead plugin). If detected:
12
+ - Delete the dead plugin with `delete_device`
13
+ - Replace with a native Ableton alternative (e.g., Saturator instead of tape plugins, Operator instead of failed FM synths)
14
+ - Run `get_session_diagnostics` for any other issues (armed tracks, missing instruments)
15
+ - Inform the user which plugin failed and what replacement was used
16
+ 5. **Program drums first** create a clip, add kick/snare/hat patterns with `add_notes`
17
+ 6. **Add bass** — create clip, program a bassline that locks with the kick
18
+ 7. **Add harmony** — chords or pads that set the mood
19
+ 8. **Add melody** — top-line or lead element
20
+ 9. **Mix** — balance levels with `set_track_volume` and `set_track_pan`
21
+ 10. **Pitch & tuning audit** — MANDATORY before firing. Run on every melodic track (skip drums):
22
+ - `identify_scale` on each track — verify all tracks agree on the same tonal center
23
+ - `analyze_harmony` on chordal tracks (pads, keys) — verify chord quality (no accidental augmented/diminished chords unless intentional)
24
+ - `detect_theory_issues` with `strict=true` on each track — check for out-of-key notes, parallel fifths, voice crossing
25
+ - **Interpret results against the intended scale**, not just C major. The analyzer only knows 7 standard modes — exotic scales (Hijaz, Hungarian minor, whole tone, etc.) will produce false "out of key" warnings. Cross-reference flagged notes against the intended scale manually.
26
+ - Report a clear tuning table to the user: which tracks are clean, which have issues, what the issues are
27
+ - Fix wrong notes with `modify_notes` before proceeding
28
+ 11. **Fire the scene** to listen, iterate based on feedback
17
29
 
18
30
  Use the livepilot-core skill for all tool calls. Verify after each step. Keep the user informed of what you're doing and why.
@@ -7,10 +7,11 @@ Guide the user through designing a sound. Follow these steps:
7
7
 
8
8
  1. **Ask about the target sound** — what character? (warm pad, aggressive bass, shimmering lead, atmospheric texture, etc.)
9
9
  2. **Choose an instrument** — pick the right synth for the job, load it with `find_and_load_device`
10
- 3. **Get parameters** — `get_device_parameters` to see what's available
11
- 4. **Shape the sound** — `set_device_parameter` or `batch_set_parameters` to dial in the character
12
- 5. **Add effects** — load effects (reverb, delay, chorus, distortion, etc.) and tweak their parameters
13
- 6. **Create a test pattern** — `create_clip` + `add_notes` with a simple pattern to audition
14
- 7. **Fire the clip** to listen, iterate based on feedback
10
+ 3. **Verify device loaded** — `get_device_info` to confirm plugin initialized (AU/VST with `parameter_count` <= 1 = dead plugin — delete and replace with native alternative)
11
+ 4. **Get parameters** — `get_device_parameters` to see what's available
12
+ 5. **Shape the sound** — `set_device_parameter` or `batch_set_parameters` to dial in the character
13
+ 6. **Add effects** — load effects (reverb, delay, chorus, distortion, etc.) and tweak their parameters
14
+ 7. **Create a test pattern** `create_clip` + `add_notes` with a simple pattern to audition
15
+ 8. **Fire the clip** to listen, iterate based on feedback
15
16
 
16
17
  Explain what each parameter does as you adjust it. Use `undo` liberally if something sounds wrong.
@@ -26,6 +26,9 @@ These layers sit on top of 178 deterministic tools across 17 domains: transport,
26
26
  9. **Tempo range 20-999 BPM** — validated before sending to Ableton
27
27
  10. **Always name your tracks and clips** — organization is part of the creative process
28
28
  11. **Respect tool speed tiers** — see below. Never call heavy tools without user consent.
29
+ 12. **ALWAYS report tool errors to the user** — if any tool call returns an error, immediately tell the user what failed, why, and what workaround you're using. Never silently swallow errors or switch strategies without explaining. Include: the tool name, the error message, and your fallback plan. This applies to all tool errors including missing M4L analyzer, dead plugins (`parameter_count` <= 1 on AU/VST), connection timeouts, and invalid parameter responses.
30
+ 13. **Verify plugin health after loading** — v1.9.11+ tools now return `health_flags`, `mcp_sound_design_ready`, and `plugin_host_status` on device load and info calls. Check `mcp_sound_design_ready` — if `false`, check `health_flags` for: `opaque_or_failed_plugin` (dead or untweakable AU/VST), `sample_dependent` (granular synth needing source audio). On failure: delete with `delete_device`, replace with native Ableton alternative, report to user.
31
+ 14. **Use `C hijaz` for Hijaz/Phrygian Dominant keys** — v1.9.11+ theory tools accept `hijaz` as a key alias. Use `key="C hijaz"` in `detect_theory_issues`, `analyze_harmony`, etc. to avoid false out-of-key warnings on Hijaz, manele, or Middle Eastern scales.
29
32
 
30
33
  ## Tool Speed Tiers
31
34
 
@@ -88,6 +91,13 @@ Never skip levels. The user's question determines the entry point, but always st
88
91
  2. **For Drum Racks: `get_rack_chains`** — confirm chains exist (an empty Drum Rack = silence). You need named chains like "Bass Drum", "Snare", etc.
89
92
  3. **For synths: `get_device_parameters`** — confirm `Volume`/`Gain` parameter is not 0. Check oscillators are on.
90
93
  4. **For effects: check `Dry/Wet` and `Drive`/key params** — a Saturator with Drive=0 or a Reverb with Dry/Wet=0 does nothing.
94
+ 5. **For AU/VST plugins: `get_device_info`** — if `parameter_count` <= 1 and `class_name` contains "PluginDevice", the plugin is dead. Delete and replace with native alternative. Report the failure.
95
+ 6. **CRITICAL — Sample-dependent instruments produce silence without source material.** These plugins load "successfully" (many parameters) but output nothing until a sample/audio source is provided. Since MCP tools CANNOT load samples into third-party plugin UIs, **NEVER use these as standalone instruments**:
96
+ - **Granular synths:** iDensity, Tardigrain, Koala Sampler, Burns Audio Granular
97
+ - **Samplers without presets:** bare Simpler, bare Sampler (always load a preset, never the empty shell)
98
+ - **Sample players:** AudioLayer, sEGments (need user to load samples via plugin GUI)
99
+ - **Instead use:** Wavetable, Operator, Drift, Analog, Meld, Collision, Tension — these are self-contained synthesizers that produce sound immediately from MIDI input alone.
100
+ - **If granular textures are needed:** Use Wavetable with aggressive wavetable position modulation, or Operator with FM feedback and short envelopes, or load a Simpler/Sampler **preset** (not the bare instrument) from the Sounds browser.
91
101
 
92
102
  ### After programming notes:
93
103
  1. **`fire_clip` or `fire_scene`** — always listen. If the track has notes but the instrument has no samples/chains, you're playing silence.
@@ -100,11 +110,15 @@ Never skip levels. The user's question determines the entry point, but always st
100
110
 
101
111
  ### Quick health check pattern:
102
112
  ```
103
- 1. get_track_info(track_index) has devices? has clips?
104
- 2. get_rack_chains (if Drum Rack) has chains with samples?
105
- 3. get_device_parameters → volume > 0? key params set?
106
- 4. Check mixer.volume > 0 → track is audible?
107
- 5. fire_clip / fire_scene sound comes out?
113
+ 1. get_device_info(track, device) class_name? parameter_count?
114
+ - PluginDevice with param_count<=1? DEAD PLUGIN. Delete + replace.
115
+ - Is it a sample-dependent plugin? SILENT. Replace with self-contained synth.
116
+ 2. get_track_info(track_index) → has devices? has clips?
117
+ 3. get_rack_chains (if Drum Rack) has chains with samples?
118
+ 4. get_device_parameters → volume > 0? key params set?
119
+ 5. Check mixer.volume > 0 → track is audible?
120
+ 6. fire_clip / fire_scene → sound comes out?
121
+ 7. REPORT any issues to the user → never silently work around failures.
108
122
  ```
109
123
 
110
124
  ### Red flags (things that produce silence):
@@ -116,6 +130,8 @@ Never skip levels. The user's question determines the entry point, but always st
116
130
  - Master volume at 0
117
131
  - MIDI track with no instrument loaded
118
132
  - Notes programmed but clip not fired
133
+ - **Dead AU/VST plugin** — `parameter_count` <= 1 on a PluginDevice (plugin shell loaded, DSP engine crashed)
134
+ - **Sample-dependent plugin with no sample** — granular synths, bare samplers, and sample players load "successfully" with many parameters but produce zero audio without source material. The sneakiest silent failure because `get_device_info` looks healthy.
119
135
 
120
136
  ## Tool Domains (178 total)
121
137
 
@@ -246,8 +262,15 @@ MIDI file import/export — works with standard .mid files on disk.
246
262
  6. `create_clip` — create clips on each track (4 beats = 1 bar at 4/4)
247
263
  7. `add_notes` — program MIDI patterns. Each note needs `pitch`, `start_time`, `duration`
248
264
  8. **HEALTH CHECK per track:** `get_track_info` → confirm device loaded, mixer volume > 0, not muted
249
- 9. `fire_scene` or `fire_clip` listen to your work
250
- 10. Iterate: `get_notes` `modify_notes` / `transpose_notes` listen again
265
+ 9. **PITCH & TUNING AUDIT (MANDATORY before firing):**
266
+ - `identify_scale` on every melodic track verify all tracks share the same tonal center
267
+ - `analyze_harmony` on chordal tracks — verify chord quality (no accidental augmented/diminished)
268
+ - `detect_theory_issues` with `strict=true` — check out-of-key notes, parallel fifths, voice crossing
269
+ - **Interpret against intended scale:** The analyzer only knows 7 standard modes. Exotic scales (Hijaz, Hungarian minor, whole tone, etc.) produce false "out of key" warnings. Cross-reference flagged pitches against the intended scale manually.
270
+ - Report a clear tuning table to the user before proceeding
271
+ - Fix wrong notes with `modify_notes` before firing
272
+ 10. `fire_scene` or `fire_clip` — listen to your work
273
+ 11. Iterate: `get_notes` → `modify_notes` / `transpose_notes` → listen again
251
274
 
252
275
  ## Workflow: Sound Design
253
276
 
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.9 — Architecture & Tool Reference
1
+ # LivePilot v1.9.11 — Architecture & Tool Reference
2
2
 
3
3
  Agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
4
4
 
@@ -12,7 +12,8 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
12
12
  - [ ] `package.json` → `"version"`
13
13
  - [ ] `package-lock.json` → `"version"` (run `npm install --package-lock-only` if stale)
14
14
  - [ ] `server.json` → `"version"` (TWO locations: top-level and package)
15
- - [ ] `livepilot/.claude-plugin/plugin.json` → `"version"`
15
+ - [ ] `livepilot/.Codex-plugin/plugin.json` → `"version"` (primary Codex manifest)
16
+ - [ ] `livepilot/.claude-plugin/plugin.json` → `"version"` (must match Codex plugin)
16
17
  - [ ] `.claude-plugin/marketplace.json` → `"version"` in plugins array
17
18
  - [ ] `mcp_server/__init__.py` → `__version__`
18
19
  - [ ] `remote_script/LivePilot/__init__.py` → `__version__` (log message auto-uses it)
@@ -23,7 +24,7 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
23
24
  - [ ] `docs/social-banner.html` → version display
24
25
  - [ ] `docs/M4L_BRIDGE.md` → ping response example
25
26
 
26
- **How to check:** `grep -rn "1\.[0-9]\.[0-9]" package.json server.json livepilot/.claude-plugin/plugin.json .claude-plugin/marketplace.json mcp_server/__init__.py remote_script/LivePilot/__init__.py m4l_device/livepilot_bridge.js CHANGELOG.md CLAUDE.md livepilot/skills/livepilot-core/references/overview.md docs/social-banner.html docs/M4L_BRIDGE.md`
27
+ **How to check:** `grep -rn "1\.[0-9]\.[0-9]" package.json server.json livepilot/.Codex-plugin/plugin.json livepilot/.claude-plugin/plugin.json .claude-plugin/marketplace.json mcp_server/__init__.py remote_script/LivePilot/__init__.py m4l_device/livepilot_bridge.js CHANGELOG.md CLAUDE.md livepilot/skills/livepilot-core/references/overview.md docs/social-banner.html docs/M4L_BRIDGE.md`
27
28
 
28
29
  ## 2. Tool Count (must ALL match)
29
30
 
@@ -36,7 +37,8 @@ Files that reference tool count:
36
37
  - [ ] `README.md` — header, PERCEPTION section ("139 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
37
38
  - [ ] `package.json` → `"description"` (178 tools, 17 domains)
38
39
  - [ ] `server.json` → `"description"`
39
- - [ ] `livepilot/.claude-plugin/plugin.json` → `"description"`
40
+ - [ ] `livepilot/.Codex-plugin/plugin.json` → `"description"` (primary Codex manifest)
41
+ - [ ] `livepilot/.claude-plugin/plugin.json` → `"description"` (must match Codex plugin)
40
42
  - [ ] `.claude-plugin/marketplace.json` → `"description"`
41
43
  - [ ] `CLAUDE.md` → "178 tools across 17 domains"
42
44
  - [ ] `livepilot/skills/livepilot-core/SKILL.md` — "178 tools across 17 domains", Analyzer (29), Perception (4)
@@ -75,6 +77,7 @@ Current: **17 domains**: transport, tracks, clips, notes, devices, scenes, mixin
75
77
 
76
78
  - [ ] `~/.claude/plugins/cache/dreamrec-LivePilot/livepilot/` has current version directory
77
79
  - [ ] Old version directories removed
80
+ - [ ] `~/.claude/plugins/installed_plugins.json` → `livepilot@dreamrec-LivePilot` entry: update `version`, `installPath`, `gitCommitSha`, `lastUpdated`
78
81
 
79
82
  ## 7. Social/Promotional Assets
80
83
 
@@ -105,5 +108,5 @@ Current: **17 domains**: transport, tracks, clips, notes, devices, scenes, mixin
105
108
  ## Quick Verify Command
106
109
 
107
110
  ```bash
108
- echo "=== Versions ===" && grep -h '"version"' package.json server.json livepilot/.claude-plugin/plugin.json .claude-plugin/marketplace.json | head -6 && grep __version__ mcp_server/__init__.py remote_script/LivePilot/__init__.py && echo "=== Tool count ===" && grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print "Total:", sum}' && echo "=== Tests ===" && python3 -m pytest tests/ -q 2>&1 | tail -1
111
+ echo "=== Versions ===" && grep -h '"version"' package.json server.json livepilot/.Codex-plugin/plugin.json livepilot/.claude-plugin/plugin.json .claude-plugin/marketplace.json | head -7 && grep __version__ mcp_server/__init__.py remote_script/LivePilot/__init__.py && echo "=== Tool count ===" && grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print "Total:", sum}' && echo "=== Tests ===" && python3 -m pytest tests/ -q 2>&1 | tail -1
109
112
  ```
Binary file