rhylthyme-cli-runner 0.2.2a0__tar.gz → 0.2.3a0__tar.gz

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 (83) hide show
  1. {rhylthyme_cli_runner-0.2.2a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.3a0}/PKG-INFO +114 -2
  2. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/README.md +113 -1
  3. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/setup.py +1 -1
  4. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
  5. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/remote/cli.py +58 -8
  6. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +114 -2
  7. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_remote.py +55 -0
  8. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_skill.py +11 -0
  9. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/LICENSE +0 -0
  10. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/pyproject.toml +0 -0
  11. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/setup.cfg +0 -0
  12. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/cli.py +0 -0
  13. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
  14. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
  15. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
  16. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
  17. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
  18. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
  19. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
  20. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
  21. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
  22. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
  23. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
  24. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
  25. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
  26. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
  27. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
  28. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
  29. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
  30. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
  31. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
  32. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
  33. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
  34. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
  35. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
  36. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
  37. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
  38. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
  39. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
  40. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
  41. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
  42. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/render.py +0 -0
  43. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
  44. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
  45. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
  46. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
  47. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
  48. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
  49. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
  50. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
  51. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
  52. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
  53. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
  54. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
  55. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
  56. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
  57. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +0 -0
  58. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
  59. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
  60. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
  61. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
  62. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_calibrate.py +0 -0
  63. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_cli.py +0 -0
  64. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_eval_baseline.py +0 -0
  65. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_eval_harness.py +0 -0
  66. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_eval_providers.py +0 -0
  67. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_eval_scorer.py +0 -0
  68. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_evaluate.py +0 -0
  69. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_example_validation.py +0 -0
  70. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_examples_integration.py +0 -0
  71. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_expand_replicates.py +0 -0
  72. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_hash_parity.py +0 -0
  73. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_history_report.py +0 -0
  74. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_mcp_checks.py +0 -0
  75. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_planner_time_strings.py +0 -0
  76. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_predict.py +0 -0
  77. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_program_runner.py +0 -0
  78. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_replay.py +0 -0
  79. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_run_recorder.py +0 -0
  80. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_synth.py +0 -0
  81. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_usable.py +0 -0
  82. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_validate_examples_ci.py +0 -0
  83. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.3a0}/tests/test_validate_program.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhylthyme-cli-runner
3
- Version: 0.2.2a0
3
+ Version: 0.2.3a0
4
4
  Summary: CLI runner for Rhylthyme real-time program schedules
5
5
  Home-page: https://github.com/rhylthyme/rhylthyme-cli-runner
6
6
  Author: Rhylthyme Team
@@ -69,6 +69,106 @@ rhylthyme login
69
69
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
70
70
  ```
71
71
 
72
+ ## A timeline in five commands: a birthday party
73
+
74
+ Parents collect at 4:00. Working backwards from that: when does the cake go in
75
+ the oven, when do the balloons go up, and do the pizzas and the cake ever want
76
+ the oven at the same time?
77
+
78
+ ```bash
79
+ pip install rhylthyme-cli-runner
80
+
81
+ # 1. a program: the party, the kitchen and the setup crew, 13 steps
82
+ curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
83
+
84
+ # 2. is it well formed?
85
+ rhylthyme validate birthday-party.json --strict
86
+
87
+ # 3. any clashes, and what time does everything start if we finish at 4?
88
+ rhylthyme analyze birthday-party.json --finish-at 16:00
89
+ ```
90
+
91
+ ```
92
+ **Makespan:** 3h 10m across 3 tracks, 13 steps.
93
+ **Critical path:** Guests arrive → Party games → Pizza → Candles and cake → Piñata, until it breaks → Party bags and goodbyes
94
+ **Binding constraints:** step dependencies only — no resource or in-flight limit gates the critical path.
95
+ **Peak concurrency:** 2 steps at 10:00.
96
+ **Resource conflicts:** none.
97
+
98
+ **Tracks that finish early:** Kitchen (1h 10m early), Setup crew (50m early)
99
+
100
+ Start Sat 12:50, finish Sat 16:00 (local time)
101
+ Sat 12:50 Bake the cake
102
+ Sat 13:00 Balloons and banner
103
+ Sat 13:25 Cake cools
104
+ Sat 13:30 Hang the piñata
105
+ Sat 13:50 Guests arrive
106
+ Sat 14:05 Frost and decorate
107
+ Sat 14:20 Party games
108
+ Sat 14:35 Pizzas in the oven
109
+ Sat 14:50 Fill party bags
110
+ Sat 14:50 Pizza
111
+ Sat 15:15 Candles and cake
112
+ Sat 15:30 Piñata, until it breaks
113
+ Sat 15:45 Party bags and goodbyes
114
+ ```
115
+
116
+ ```bash
117
+ # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
118
+ rhylthyme publish birthday-party.json --open --image party-preview.png
119
+
120
+ # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
121
+ npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
122
+ --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
123
+ ```
124
+
125
+ ![Eighth birthday party: the party, the kitchen and the setup crew on a wall-clock axis](https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.png)
126
+
127
+ The pizzas go in 15 minutes after the games *start*, so they come out as the
128
+ games end; the party bags get filled while everyone is eating. Try
129
+ `--finish-at 17:30`, or turn the cake into a two-hour fruitcake (`"seconds": "2h"`
130
+ on `bake`) and `analyze` reports the clash: `oven needs 2 but max is 1 from
131
+ 1:45:00 to 2:00:00 (bake, heat-pizza)`. `rhylthyme run birthday-party.json --time-scale 60` rehearses
132
+ the whole afternoon in the terminal at a minute per second.
133
+
134
+ ## This repository and rhylthyme-mcp
135
+
136
+ Two repositories do related things and are easy to confuse. This one is the
137
+ **command a person types**. [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp)
138
+ is the **server an AI assistant talks to**.
139
+
140
+ | | [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp) | [rhylthyme-cli-runner](https://github.com/rhylthyme/rhylthyme-cli-runner) |
141
+ |---|---|---|
142
+ | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
143
+ | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
144
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
145
+ | Language | JavaScript (Node 20+) | Python 3.12+ |
146
+ | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
147
+ | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
148
+ | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
149
+ | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
150
+ | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
151
+ | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
152
+ | Catalog search, imports, account library | yes | no |
153
+ | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
154
+
155
+ How they fit together: this tool is one of the MCP server's clients.
156
+ `rhylthyme analyze`, `publish`, `generate` and `mcp-test` are MCP calls to
157
+ `mcp.rhylthyme.com`; `rhylthyme validate`, `run`, `runs` and `calibrate` never
158
+ touch the network. Each has its own validator for the same program schema
159
+ (Python here, JavaScript there), so a program is checked again when it is
160
+ published.
161
+
162
+ Use this repository if you have a program file and a terminal, want timers in
163
+ the terminal, or keep run records. Use rhylthyme-mcp to connect Claude, ChatGPT
164
+ or another assistant, or to read or self-host the server.
165
+
166
+ Two names to keep apart: `rhylthyme-cli-runner` on PyPI is this tool (command
167
+ `rhylthyme`); `rhylthyme-mcp` on PyPI is the server's stdio bridge (command
168
+ `rhylthyme-mcp`). The program format itself is defined in
169
+ [rhylthyme-spec](https://github.com/rhylthyme/rhylthyme-spec), with examples in
170
+ [rhylthyme-examples](https://github.com/rhylthyme/rhylthyme-examples).
171
+
72
172
  ## Installation
73
173
 
74
174
  ### From Source
@@ -257,6 +357,16 @@ it for conflicts, and hand back a live timeline. It follows the layout of
257
357
  [K-Dense scientific skills](https://github.com/K-Dense-AI/claude-scientific-skills):
258
358
  a `SKILL.md` plus `references/`.
259
359
 
360
+ The easiest install is the Claude Code plugin, which also connects the hosted
361
+ MCP server:
362
+
363
+ ```
364
+ /plugin marketplace add rhylthyme/rhylthyme-mcp
365
+ /plugin install rhylthyme@rhylthyme
366
+ ```
367
+
368
+ Or copy the skill by hand:
369
+
260
370
  ```bash
261
371
  # Claude Code, for one user
262
372
  mkdir -p ~/.claude/skills
@@ -421,13 +531,15 @@ and tracks that finish early. No sign-in; nothing is published.
421
531
 
422
532
  ### `rhylthyme publish`
423
533
 
424
- Publishes a program file as a live timeline and prints its URL. No sign-in.
534
+ Publishes a program (a file, an http(s) URL, or `-` for standard input) as a
535
+ live timeline and prints its URL. No sign-in.
425
536
  A published timeline is reachable by anyone who has the link.
426
537
 
427
538
  **Options:**
428
539
  - `-e, --env`: `generic`, `kitchen`, `lab`, `events`, `gym` (default: from `environmentType`)
429
540
  - `-q, --quiet`: print only the URL
430
541
  - `--json`: `url`, `shareId`, `imageUrl`, `makespanSeconds`, `warnings`
542
+ - `--image PATH`: also save a PNG of the timeline
431
543
  - `--open`: open it in a browser
432
544
 
433
545
  ### `rhylthyme plan`
@@ -9,6 +9,106 @@ rhylthyme login
9
9
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
10
10
  ```
11
11
 
12
+ ## A timeline in five commands: a birthday party
13
+
14
+ Parents collect at 4:00. Working backwards from that: when does the cake go in
15
+ the oven, when do the balloons go up, and do the pizzas and the cake ever want
16
+ the oven at the same time?
17
+
18
+ ```bash
19
+ pip install rhylthyme-cli-runner
20
+
21
+ # 1. a program: the party, the kitchen and the setup crew, 13 steps
22
+ curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
23
+
24
+ # 2. is it well formed?
25
+ rhylthyme validate birthday-party.json --strict
26
+
27
+ # 3. any clashes, and what time does everything start if we finish at 4?
28
+ rhylthyme analyze birthday-party.json --finish-at 16:00
29
+ ```
30
+
31
+ ```
32
+ **Makespan:** 3h 10m across 3 tracks, 13 steps.
33
+ **Critical path:** Guests arrive → Party games → Pizza → Candles and cake → Piñata, until it breaks → Party bags and goodbyes
34
+ **Binding constraints:** step dependencies only — no resource or in-flight limit gates the critical path.
35
+ **Peak concurrency:** 2 steps at 10:00.
36
+ **Resource conflicts:** none.
37
+
38
+ **Tracks that finish early:** Kitchen (1h 10m early), Setup crew (50m early)
39
+
40
+ Start Sat 12:50, finish Sat 16:00 (local time)
41
+ Sat 12:50 Bake the cake
42
+ Sat 13:00 Balloons and banner
43
+ Sat 13:25 Cake cools
44
+ Sat 13:30 Hang the piñata
45
+ Sat 13:50 Guests arrive
46
+ Sat 14:05 Frost and decorate
47
+ Sat 14:20 Party games
48
+ Sat 14:35 Pizzas in the oven
49
+ Sat 14:50 Fill party bags
50
+ Sat 14:50 Pizza
51
+ Sat 15:15 Candles and cake
52
+ Sat 15:30 Piñata, until it breaks
53
+ Sat 15:45 Party bags and goodbyes
54
+ ```
55
+
56
+ ```bash
57
+ # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
58
+ rhylthyme publish birthday-party.json --open --image party-preview.png
59
+
60
+ # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
61
+ npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
62
+ --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
63
+ ```
64
+
65
+ ![Eighth birthday party: the party, the kitchen and the setup crew on a wall-clock axis](https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.png)
66
+
67
+ The pizzas go in 15 minutes after the games *start*, so they come out as the
68
+ games end; the party bags get filled while everyone is eating. Try
69
+ `--finish-at 17:30`, or turn the cake into a two-hour fruitcake (`"seconds": "2h"`
70
+ on `bake`) and `analyze` reports the clash: `oven needs 2 but max is 1 from
71
+ 1:45:00 to 2:00:00 (bake, heat-pizza)`. `rhylthyme run birthday-party.json --time-scale 60` rehearses
72
+ the whole afternoon in the terminal at a minute per second.
73
+
74
+ ## This repository and rhylthyme-mcp
75
+
76
+ Two repositories do related things and are easy to confuse. This one is the
77
+ **command a person types**. [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp)
78
+ is the **server an AI assistant talks to**.
79
+
80
+ | | [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp) | [rhylthyme-cli-runner](https://github.com/rhylthyme/rhylthyme-cli-runner) |
81
+ |---|---|---|
82
+ | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
83
+ | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
84
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
85
+ | Language | JavaScript (Node 20+) | Python 3.12+ |
86
+ | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
87
+ | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
88
+ | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
89
+ | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
90
+ | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
91
+ | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
92
+ | Catalog search, imports, account library | yes | no |
93
+ | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
94
+
95
+ How they fit together: this tool is one of the MCP server's clients.
96
+ `rhylthyme analyze`, `publish`, `generate` and `mcp-test` are MCP calls to
97
+ `mcp.rhylthyme.com`; `rhylthyme validate`, `run`, `runs` and `calibrate` never
98
+ touch the network. Each has its own validator for the same program schema
99
+ (Python here, JavaScript there), so a program is checked again when it is
100
+ published.
101
+
102
+ Use this repository if you have a program file and a terminal, want timers in
103
+ the terminal, or keep run records. Use rhylthyme-mcp to connect Claude, ChatGPT
104
+ or another assistant, or to read or self-host the server.
105
+
106
+ Two names to keep apart: `rhylthyme-cli-runner` on PyPI is this tool (command
107
+ `rhylthyme`); `rhylthyme-mcp` on PyPI is the server's stdio bridge (command
108
+ `rhylthyme-mcp`). The program format itself is defined in
109
+ [rhylthyme-spec](https://github.com/rhylthyme/rhylthyme-spec), with examples in
110
+ [rhylthyme-examples](https://github.com/rhylthyme/rhylthyme-examples).
111
+
12
112
  ## Installation
13
113
 
14
114
  ### From Source
@@ -197,6 +297,16 @@ it for conflicts, and hand back a live timeline. It follows the layout of
197
297
  [K-Dense scientific skills](https://github.com/K-Dense-AI/claude-scientific-skills):
198
298
  a `SKILL.md` plus `references/`.
199
299
 
300
+ The easiest install is the Claude Code plugin, which also connects the hosted
301
+ MCP server:
302
+
303
+ ```
304
+ /plugin marketplace add rhylthyme/rhylthyme-mcp
305
+ /plugin install rhylthyme@rhylthyme
306
+ ```
307
+
308
+ Or copy the skill by hand:
309
+
200
310
  ```bash
201
311
  # Claude Code, for one user
202
312
  mkdir -p ~/.claude/skills
@@ -361,13 +471,15 @@ and tracks that finish early. No sign-in; nothing is published.
361
471
 
362
472
  ### `rhylthyme publish`
363
473
 
364
- Publishes a program file as a live timeline and prints its URL. No sign-in.
474
+ Publishes a program (a file, an http(s) URL, or `-` for standard input) as a
475
+ live timeline and prints its URL. No sign-in.
365
476
  A published timeline is reachable by anyone who has the link.
366
477
 
367
478
  **Options:**
368
479
  - `-e, --env`: `generic`, `kitchen`, `lab`, `events`, `gym` (default: from `environmentType`)
369
480
  - `-q, --quiet`: print only the URL
370
481
  - `--json`: `url`, `shareId`, `imageUrl`, `makespanSeconds`, `warnings`
482
+ - `--image PATH`: also save a PNG of the timeline
371
483
  - `--open`: open it in a browser
372
484
 
373
485
  ### `rhylthyme plan`
@@ -19,7 +19,7 @@ def read_readme():
19
19
 
20
20
  setup(
21
21
  name="rhylthyme-cli-runner",
22
- version="0.2.2a0",
22
+ version="0.2.3a0",
23
23
  description="CLI runner for Rhylthyme real-time program schedules",
24
24
  long_description=read_readme(),
25
25
  long_description_content_type="text/markdown",
@@ -6,7 +6,7 @@ This package provides the command-line interface for running and validating
6
6
  Rhylthyme real-time program schedules.
7
7
  """
8
8
 
9
- __version__ = "0.2.2a0"
9
+ __version__ = "0.2.3a0"
10
10
  __author__ = "Rhylthyme Team"
11
11
  __description__ = "CLI runner for Rhylthyme real-time program schedules"
12
12
 
@@ -386,16 +386,47 @@ def mcp_test(
386
386
  ctx.exit(1)
387
387
 
388
388
 
389
+ MAX_PROGRAM_BYTES = 2 * 1024 * 1024
390
+
391
+
392
+ def _fetch_bytes(url: str, limit: int = 8 * 1024 * 1024) -> bytes:
393
+ import urllib.request
394
+
395
+ if not url.startswith(("http://", "https://")):
396
+ raise ValueError("not an http(s) URL")
397
+ request = urllib.request.Request(url, headers={"User-Agent": "rhylthyme-cli"})
398
+ with urllib.request.urlopen(
399
+ request, timeout=60
400
+ ) as response: # nosec B310 - scheme checked above
401
+ data = response.read(limit + 1)
402
+ if len(data) > limit:
403
+ raise ValueError(f"larger than {limit // (1024 * 1024)} MB")
404
+ return data
405
+
406
+
407
+ def _read_source(source: str) -> str:
408
+ """The text of a program given as a file path, an http(s) URL, or `-`
409
+ for standard input."""
410
+ if source == "-":
411
+ return sys.stdin.read()
412
+ if source.startswith(("http://", "https://")):
413
+ return _fetch_bytes(source, MAX_PROGRAM_BYTES).decode("utf-8")
414
+ path = Path(source)
415
+ if not path.is_file():
416
+ raise FileNotFoundError("no such file")
417
+ return path.read_text()
418
+
419
+
389
420
  def _load_program(program_file: str) -> dict:
390
- path = Path(program_file)
391
421
  program = None
392
422
  try:
393
- if path.suffix.lower() in (".yaml", ".yml"):
423
+ text = _read_source(program_file)
424
+ if program_file.lower().split("?")[0].endswith((".yaml", ".yml")):
394
425
  import yaml
395
426
 
396
- program = yaml.safe_load(path.read_text())
427
+ program = yaml.safe_load(text)
397
428
  else:
398
- program = json.loads(path.read_text())
429
+ program = json.loads(text)
399
430
  except Exception as e: # noqa: BLE001
400
431
  _fail(f"Could not read {program_file}: {e}")
401
432
  if not isinstance(program, dict) or not isinstance(program.get("tracks"), list):
@@ -439,7 +470,7 @@ def _local(iso: Optional[str]) -> str:
439
470
 
440
471
 
441
472
  @click.command("analyze")
442
- @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
473
+ @click.argument("program_file", metavar="PROGRAM")
443
474
  @click.option(
444
475
  "--finish-at",
445
476
  "finish_at",
@@ -461,6 +492,8 @@ def _local(iso: Optional[str]) -> str:
461
492
  def analyze(program_file, finish_at, start_at, json_output, strict):
462
493
  """Total length, critical path, resource conflicts and clock times. No sign-in needed.
463
494
 
495
+ PROGRAM is a JSON or YAML file, an http(s) URL, or - for standard input.
496
+
464
497
  \b
465
498
  Computed by the hosted MCP server (analyze_schedule); nothing is published.
466
499
  rhylthyme analyze dinner.json --finish-at 19:00
@@ -520,7 +553,7 @@ ENV_BY_TYPE = {
520
553
 
521
554
 
522
555
  @click.command("publish")
523
- @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
556
+ @click.argument("program_file", metavar="PROGRAM")
524
557
  @click.option(
525
558
  "-e",
526
559
  "--env",
@@ -539,14 +572,24 @@ ENV_BY_TYPE = {
539
572
  help="Print {url, imageUrl, makespanSeconds, warnings} as JSON.",
540
573
  )
541
574
  @click.option("-q", "--quiet", is_flag=True, help="Print only the live-timeline URL.")
542
- def publish(program_file, environment, open_url, json_output, quiet):
543
- """Publish a program file as a live, shareable timeline. No sign-in needed.
575
+ @click.option(
576
+ "--image",
577
+ "image_path",
578
+ type=click.Path(dir_okay=False),
579
+ default=None,
580
+ help="Also save a PNG of the timeline here.",
581
+ )
582
+ def publish(program_file, environment, open_url, json_output, quiet, image_path):
583
+ """Publish a program as a live, shareable timeline. No sign-in needed.
584
+
585
+ PROGRAM is a JSON or YAML file, an http(s) URL, or - for standard input.
544
586
 
545
587
  \b
546
588
  The hosted MCP server validates the program first and refuses an invalid
547
589
  one, so run `rhylthyme validate` locally and fix what it reports.
548
590
  rhylthyme publish dinner.json
549
591
  rhylthyme publish blot.json -e lab -q
592
+ rhylthyme publish https://example.org/party.json --image party.png --open
550
593
  """
551
594
  program = _load_program(program_file)
552
595
  env = environment or ENV_BY_TYPE.get(
@@ -585,6 +628,13 @@ def publish(program_file, environment, open_url, json_output, quiet):
585
628
  click.echo(published.text.strip())
586
629
  if url:
587
630
  click.echo(f"\nLive timeline: {url}")
631
+ if image_path and info.get("imageUrl"):
632
+ try:
633
+ Path(image_path).write_bytes(_fetch_bytes(info["imageUrl"]))
634
+ if not json_output and not quiet:
635
+ click.echo(f"Picture: {image_path}")
636
+ except Exception as e: # noqa: BLE001
637
+ click.echo(f"Could not save the picture: {e}", err=True)
588
638
  if open_url and url:
589
639
  webbrowser.open(url)
590
640
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhylthyme-cli-runner
3
- Version: 0.2.2a0
3
+ Version: 0.2.3a0
4
4
  Summary: CLI runner for Rhylthyme real-time program schedules
5
5
  Home-page: https://github.com/rhylthyme/rhylthyme-cli-runner
6
6
  Author: Rhylthyme Team
@@ -69,6 +69,106 @@ rhylthyme login
69
69
  rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
70
70
  ```
71
71
 
72
+ ## A timeline in five commands: a birthday party
73
+
74
+ Parents collect at 4:00. Working backwards from that: when does the cake go in
75
+ the oven, when do the balloons go up, and do the pizzas and the cake ever want
76
+ the oven at the same time?
77
+
78
+ ```bash
79
+ pip install rhylthyme-cli-runner
80
+
81
+ # 1. a program: the party, the kitchen and the setup crew, 13 steps
82
+ curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
83
+
84
+ # 2. is it well formed?
85
+ rhylthyme validate birthday-party.json --strict
86
+
87
+ # 3. any clashes, and what time does everything start if we finish at 4?
88
+ rhylthyme analyze birthday-party.json --finish-at 16:00
89
+ ```
90
+
91
+ ```
92
+ **Makespan:** 3h 10m across 3 tracks, 13 steps.
93
+ **Critical path:** Guests arrive → Party games → Pizza → Candles and cake → Piñata, until it breaks → Party bags and goodbyes
94
+ **Binding constraints:** step dependencies only — no resource or in-flight limit gates the critical path.
95
+ **Peak concurrency:** 2 steps at 10:00.
96
+ **Resource conflicts:** none.
97
+
98
+ **Tracks that finish early:** Kitchen (1h 10m early), Setup crew (50m early)
99
+
100
+ Start Sat 12:50, finish Sat 16:00 (local time)
101
+ Sat 12:50 Bake the cake
102
+ Sat 13:00 Balloons and banner
103
+ Sat 13:25 Cake cools
104
+ Sat 13:30 Hang the piñata
105
+ Sat 13:50 Guests arrive
106
+ Sat 14:05 Frost and decorate
107
+ Sat 14:20 Party games
108
+ Sat 14:35 Pizzas in the oven
109
+ Sat 14:50 Fill party bags
110
+ Sat 14:50 Pizza
111
+ Sat 15:15 Candles and cake
112
+ Sat 15:30 Piñata, until it breaks
113
+ Sat 15:45 Party bags and goodbyes
114
+ ```
115
+
116
+ ```bash
117
+ # 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
118
+ rhylthyme publish birthday-party.json --open --image party-preview.png
119
+
120
+ # 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
121
+ npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
122
+ --style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
123
+ ```
124
+
125
+ ![Eighth birthday party: the party, the kitchen and the setup crew on a wall-clock axis](https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.png)
126
+
127
+ The pizzas go in 15 minutes after the games *start*, so they come out as the
128
+ games end; the party bags get filled while everyone is eating. Try
129
+ `--finish-at 17:30`, or turn the cake into a two-hour fruitcake (`"seconds": "2h"`
130
+ on `bake`) and `analyze` reports the clash: `oven needs 2 but max is 1 from
131
+ 1:45:00 to 2:00:00 (bake, heat-pizza)`. `rhylthyme run birthday-party.json --time-scale 60` rehearses
132
+ the whole afternoon in the terminal at a minute per second.
133
+
134
+ ## This repository and rhylthyme-mcp
135
+
136
+ Two repositories do related things and are easy to confuse. This one is the
137
+ **command a person types**. [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp)
138
+ is the **server an AI assistant talks to**.
139
+
140
+ | | [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp) | [rhylthyme-cli-runner](https://github.com/rhylthyme/rhylthyme-cli-runner) |
141
+ |---|---|---|
142
+ | What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
143
+ | Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
144
+ | Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
145
+ | Language | JavaScript (Node 20+) | Python 3.12+ |
146
+ | Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
147
+ | Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
148
+ | Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
149
+ | Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
150
+ | Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
151
+ | Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
152
+ | Catalog search, imports, account library | yes | no |
153
+ | Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
154
+
155
+ How they fit together: this tool is one of the MCP server's clients.
156
+ `rhylthyme analyze`, `publish`, `generate` and `mcp-test` are MCP calls to
157
+ `mcp.rhylthyme.com`; `rhylthyme validate`, `run`, `runs` and `calibrate` never
158
+ touch the network. Each has its own validator for the same program schema
159
+ (Python here, JavaScript there), so a program is checked again when it is
160
+ published.
161
+
162
+ Use this repository if you have a program file and a terminal, want timers in
163
+ the terminal, or keep run records. Use rhylthyme-mcp to connect Claude, ChatGPT
164
+ or another assistant, or to read or self-host the server.
165
+
166
+ Two names to keep apart: `rhylthyme-cli-runner` on PyPI is this tool (command
167
+ `rhylthyme`); `rhylthyme-mcp` on PyPI is the server's stdio bridge (command
168
+ `rhylthyme-mcp`). The program format itself is defined in
169
+ [rhylthyme-spec](https://github.com/rhylthyme/rhylthyme-spec), with examples in
170
+ [rhylthyme-examples](https://github.com/rhylthyme/rhylthyme-examples).
171
+
72
172
  ## Installation
73
173
 
74
174
  ### From Source
@@ -257,6 +357,16 @@ it for conflicts, and hand back a live timeline. It follows the layout of
257
357
  [K-Dense scientific skills](https://github.com/K-Dense-AI/claude-scientific-skills):
258
358
  a `SKILL.md` plus `references/`.
259
359
 
360
+ The easiest install is the Claude Code plugin, which also connects the hosted
361
+ MCP server:
362
+
363
+ ```
364
+ /plugin marketplace add rhylthyme/rhylthyme-mcp
365
+ /plugin install rhylthyme@rhylthyme
366
+ ```
367
+
368
+ Or copy the skill by hand:
369
+
260
370
  ```bash
261
371
  # Claude Code, for one user
262
372
  mkdir -p ~/.claude/skills
@@ -421,13 +531,15 @@ and tracks that finish early. No sign-in; nothing is published.
421
531
 
422
532
  ### `rhylthyme publish`
423
533
 
424
- Publishes a program file as a live timeline and prints its URL. No sign-in.
534
+ Publishes a program (a file, an http(s) URL, or `-` for standard input) as a
535
+ live timeline and prints its URL. No sign-in.
425
536
  A published timeline is reachable by anyone who has the link.
426
537
 
427
538
  **Options:**
428
539
  - `-e, --env`: `generic`, `kitchen`, `lab`, `events`, `gym` (default: from `environmentType`)
429
540
  - `-q, --quiet`: print only the URL
430
541
  - `--json`: `url`, `shareId`, `imageUrl`, `makespanSeconds`, `warnings`
542
+ - `--image PATH`: also save a PNG of the timeline
431
543
  - `--open`: open it in a browser
432
544
 
433
545
  ### `rhylthyme plan`
@@ -511,3 +511,58 @@ def test_clock_to_iso():
511
511
  assert _clock_to_iso(iso, now) == iso
512
512
  assert _clock_to_iso("25:00", now) == "25:00", "left for the server to reject"
513
513
  assert _clock_to_iso("7", now) == "7", "a bare number is not a time"
514
+
515
+
516
+ def test_publish_and_analyze_read_a_url_or_stdin(
517
+ fake_mcp, config_home, tmp_path, monkeypatch
518
+ ):
519
+ import threading
520
+ from http.server import BaseHTTPRequestHandler, HTTPServer
521
+
522
+ class Files(BaseHTTPRequestHandler):
523
+ def log_message(self, *a):
524
+ pass
525
+
526
+ def do_GET(self):
527
+ if self.path == "/dinner.json":
528
+ body, status = json.dumps(PROGRAM).encode(), 200
529
+ elif self.path == "/timeline.png":
530
+ body, status = b"\x89PNG fake", 200
531
+ else:
532
+ body, status = b"nope", 404
533
+ self.send_response(status)
534
+ self.send_header("Content-Length", str(len(body)))
535
+ self.end_headers()
536
+ self.wfile.write(body)
537
+
538
+ server = HTTPServer(("127.0.0.1", 0), Files)
539
+ threading.Thread(target=server.serve_forever, daemon=True).start()
540
+ base = f"http://127.0.0.1:{server.server_address[1]}"
541
+ try:
542
+ from_url = CliRunner().invoke(cli, ["publish", f"{base}/dinner.json", "-q"])
543
+ assert from_url.exit_code == 0, from_url.output
544
+ assert from_url.output.strip() == "https://kitchen.rhylthyme.com?share=abc"
545
+ assert fake_mcp.tool_calls()[-1][2]["program"]["programId"] == "dinner"
546
+
547
+ piped = CliRunner().invoke(cli, ["analyze", "-"], input=json.dumps(PROGRAM))
548
+ assert piped.exit_code == 0, piped.output
549
+ assert fake_mcp.tool_calls()[-1][1] == "analyze_schedule"
550
+
551
+ missing = CliRunner().invoke(cli, ["publish", f"{base}/nope.json"])
552
+ assert missing.exit_code != 0 and "404" in missing.output
553
+ assert (
554
+ CliRunner()
555
+ .invoke(cli, ["publish", str(tmp_path / "absent.json")])
556
+ .exit_code
557
+ != 0
558
+ )
559
+ finally:
560
+ server.shutdown()
561
+ server.server_close()
562
+
563
+
564
+ def test_clock_and_sources_reject_other_schemes():
565
+ from rhylthyme_cli_runner.remote.cli import _fetch_bytes
566
+
567
+ with pytest.raises(ValueError):
568
+ _fetch_bytes("file:///etc/passwd")
@@ -69,3 +69,14 @@ def test_every_command_the_skill_names_exists():
69
69
  assert {"validate", "analyze", "publish", "run", "calibrate", "generate"} <= named
70
70
  missing = named - set(cli.commands)
71
71
  assert not missing, missing
72
+
73
+
74
+ def test_the_readme_recipe_program_is_valid_and_is_what_the_readme_shows():
75
+ root = SKILL.parent.parent
76
+ recipe = root / "examples" / "recipe" / "birthday-party.json"
77
+ result = CliRunner().invoke(cli, ["validate", str(recipe), "--strict"])
78
+ assert result.exit_code == 0, result.output
79
+ readme = (root / "README.md").read_text()
80
+ assert "examples/recipe/birthday-party.json" in readme
81
+ for step in json.loads(recipe.read_text())["tracks"][0]["steps"]:
82
+ assert step["name"] in readme, f"README itinerary is missing {step['name']!r}"