rhylthyme-cli-runner 0.2.5a0__tar.gz → 0.2.7a0__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 (87) hide show
  1. {rhylthyme_cli_runner-0.2.5a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.7a0}/PKG-INFO +12 -3
  2. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/README.md +11 -2
  3. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/setup.py +1 -1
  4. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
  5. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/import_cli.py +105 -3
  6. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/checks.py +1 -1
  7. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +12 -3
  8. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_import_cli.py +71 -0
  9. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/LICENSE +0 -0
  10. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/pyproject.toml +0 -0
  11. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/setup.cfg +0 -0
  12. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/cli.py +0 -0
  13. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
  14. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
  15. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
  16. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
  17. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
  18. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
  19. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
  20. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
  21. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
  22. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
  23. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
  24. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
  25. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
  26. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
  27. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
  28. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
  29. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
  30. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
  31. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
  32. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
  33. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
  34. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
  35. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
  36. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
  37. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
  38. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
  39. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
  40. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
  41. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
  42. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/render.py +0 -0
  43. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
  44. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
  45. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
  46. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
  47. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
  48. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
  49. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
  50. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
  51. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
  52. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
  53. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
  54. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/cli.py +0 -0
  55. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
  56. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/render_cli.py +0 -0
  57. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
  58. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +0 -0
  59. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
  60. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
  61. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
  62. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
  63. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_calibrate.py +0 -0
  64. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_cli.py +0 -0
  65. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_baseline.py +0 -0
  66. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_harness.py +0 -0
  67. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_providers.py +0 -0
  68. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_scorer.py +0 -0
  69. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_evaluate.py +0 -0
  70. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_example_validation.py +0 -0
  71. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_examples_integration.py +0 -0
  72. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_expand_replicates.py +0 -0
  73. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_hash_parity.py +0 -0
  74. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_history_report.py +0 -0
  75. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_mcp_checks.py +0 -0
  76. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_planner_time_strings.py +0 -0
  77. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_predict.py +0 -0
  78. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_program_runner.py +0 -0
  79. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_remote.py +0 -0
  80. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_render_cli.py +0 -0
  81. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_replay.py +0 -0
  82. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_run_recorder.py +0 -0
  83. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_skill.py +0 -0
  84. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_synth.py +0 -0
  85. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_usable.py +0 -0
  86. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_validate_examples_ci.py +0 -0
  87. {rhylthyme_cli_runner-0.2.5a0 → rhylthyme_cli_runner-0.2.7a0}/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.5a0
3
+ Version: 0.2.7a0
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
@@ -561,8 +561,17 @@ rhylthyme search "pad thai" -i spoonacular # then impo
561
561
  ```
562
562
 
563
563
  **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
564
- `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
565
- source from stdin (with `-i`).
564
+ `--stdout`, `--publish` (and `--open`), `--no-validate`, `--review`. `-` reads
565
+ pasted source from stdin (with `-i`).
566
+
567
+ `--review` sends the import and its source to a model on the server (GPT-5.6
568
+ Luna, the model that related steps best in the paper's evaluation) and prints
569
+ what looks wrong: a default duration on "simmer until thick", a step the
570
+ parser dropped, steps chained that the source runs in parallel, a total that
571
+ disagrees with the source. It needs `rhylthyme login`, spends one model call
572
+ (capped per day), and changes nothing: the findings are yours to apply. On
573
+ rhylthyme.com the same review runs automatically after every import when you
574
+ are signed in.
566
575
 
567
576
  ### `rhylthyme render`
568
577
 
@@ -501,8 +501,17 @@ rhylthyme search "pad thai" -i spoonacular # then impo
501
501
  ```
502
502
 
503
503
  **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
504
- `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
505
- source from stdin (with `-i`).
504
+ `--stdout`, `--publish` (and `--open`), `--no-validate`, `--review`. `-` reads
505
+ pasted source from stdin (with `-i`).
506
+
507
+ `--review` sends the import and its source to a model on the server (GPT-5.6
508
+ Luna, the model that related steps best in the paper's evaluation) and prints
509
+ what looks wrong: a default duration on "simmer until thick", a step the
510
+ parser dropped, steps chained that the source runs in parallel, a total that
511
+ disagrees with the source. It needs `rhylthyme login`, spends one model call
512
+ (capped per day), and changes nothing: the findings are yours to apply. On
513
+ rhylthyme.com the same review runs automatically after every import when you
514
+ are signed in.
506
515
 
507
516
  ### `rhylthyme render`
508
517
 
@@ -19,7 +19,7 @@ def read_readme():
19
19
 
20
20
  setup(
21
21
  name="rhylthyme-cli-runner",
22
- version="0.2.5a0",
22
+ version="0.2.7a0",
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.5a0"
9
+ __version__ = "0.2.7a0"
10
10
  __author__ = "Rhylthyme Team"
11
11
  __description__ = "CLI runner for Rhylthyme real-time program schedules"
12
12
 
@@ -74,6 +74,10 @@ def _import_one(importer, source: str, text: Optional[str]):
74
74
  if hasattr(importer, "allow_local_files"):
75
75
  importer.allow_local_files = True
76
76
  return importer.import_from_url(str(path))
77
+ if not source.startswith(("http://", "https://")) and (
78
+ path.suffix or "/" in source
79
+ ):
80
+ raise click.ClickException(f"No such file: {source}")
77
81
  return importer.import_from_url(source)
78
82
 
79
83
 
@@ -130,8 +134,21 @@ def _validation(program: dict):
130
134
  @click.option(
131
135
  "--no-validate", is_flag=True, help="Skip validation of the imported program."
132
136
  )
137
+ @click.option(
138
+ "--review",
139
+ "do_review",
140
+ is_flag=True,
141
+ help="Have a model read the import against its source and report what looks wrong (needs `rhylthyme login`).",
142
+ )
133
143
  def import_command(
134
- source, importer_name, output, to_stdout, do_publish, open_url, no_validate
144
+ source,
145
+ importer_name,
146
+ output,
147
+ to_stdout,
148
+ do_publish,
149
+ open_url,
150
+ no_validate,
151
+ do_review,
135
152
  ):
136
153
  """Import a recipe, protocol or deck as a program.
137
154
 
@@ -141,6 +158,7 @@ def import_command(
141
158
  .cook or .pptx file. `-` reads the source text from stdin (with -i).
142
159
  rhylthyme import https://www.seriouseats.com/the-best-chili-recipe
143
160
  rhylthyme import 52772 -i themealdb --publish
161
+ rhylthyme import recipe.cook -i cooklang --review # a second opinion on durations, order, gaps
144
162
  rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py
145
163
  rhylthyme importers # what is installed
146
164
  """
@@ -153,8 +171,22 @@ def import_command(
153
171
  )
154
172
  text = sys.stdin.read()
155
173
  importer = _pick(registry, source, importer_name)
174
+ token = None
175
+ if do_review:
176
+ # Before any work: the review needs an account, say so now.
177
+ from .remote import auth
178
+
179
+ try:
180
+ token = auth.access_token()
181
+ except auth.AuthError as e:
182
+ raise click.ClickException(
183
+ f"{e} (--review sends the import to a model on the server, which needs your account.)"
184
+ )
156
185
  click.echo(f"Importing with {importer.name}…", err=True)
157
186
  result = _import_one(importer, source, text)
187
+ source_text = (
188
+ text if text is not None else _source_text_for_review(importer, source)
189
+ )
158
190
  if not result.success:
159
191
  raise click.ClickException(result.error or "import failed")
160
192
  program = result.program
@@ -163,9 +195,10 @@ def import_command(
163
195
  v = _validation(program)
164
196
  errors = v.get("errors") or v.get("logic_errors") or []
165
197
  if not v.get("is_valid", v.get("valid", True)):
166
- for e in errors[:8]:
198
+ for finding in errors[:8]:
167
199
  click.echo(
168
- f" ✗ {e if isinstance(e, str) else e.get('message', e)}", err=True
200
+ f" ✗ {finding if isinstance(finding, str) else finding.get('message', finding)}",
201
+ err=True,
169
202
  )
170
203
  raise click.ClickException(
171
204
  "The imported program does not validate; not published. Re-run with --no-validate to keep it anyway."
@@ -187,6 +220,14 @@ def import_command(
187
220
  target.write_text(payload + "\n")
188
221
  click.echo(f"Saved {target}", err=True)
189
222
 
223
+ if do_review:
224
+ _review(
225
+ program,
226
+ token,
227
+ source_text=source_text,
228
+ source_url=source if source.startswith(("http://", "https://")) else None,
229
+ )
230
+
190
231
  if do_publish:
191
232
  from .remote.cli import publish as publish_command
192
233
 
@@ -208,6 +249,67 @@ def import_command(
208
249
  Path(tmp_path).unlink(missing_ok=True)
209
250
 
210
251
 
252
+ def _source_text_for_review(importer, source: str) -> Optional[str]:
253
+ """The text the import came from, when the CLI has it: a local file, or
254
+ a URL the CLI fetched for a text importer. Recipe-site imports do not
255
+ keep the page, so the reviewer gets the URL instead."""
256
+ path = Path(source)
257
+ if path.is_file() and path.suffix.lower() in (".cook", ".py", ".txt", ".md"):
258
+ try:
259
+ return path.read_text()
260
+ except OSError:
261
+ return None
262
+ if (
263
+ source.startswith(("http://", "https://"))
264
+ and hasattr(importer, "import_from_source")
265
+ and not getattr(importer, "supported_domains", None)
266
+ ):
267
+ try:
268
+ from .remote.cli import _fetch_bytes
269
+
270
+ return _fetch_bytes(source).decode("utf-8")
271
+ except Exception: # noqa: BLE001
272
+ return None
273
+ return None
274
+
275
+
276
+ def _review(
277
+ program: dict, token: str, *, source_text: Optional[str], source_url: Optional[str]
278
+ ) -> None:
279
+ from .remote import mcp_client as MC
280
+
281
+ click.echo("Reviewing…", err=True)
282
+ client = MC.McpClient(url=MC.endpoint_for(None))
283
+ args = {"program": program, "token": token}
284
+ if source_text:
285
+ args["source_text"] = source_text
286
+ elif source_url:
287
+ args["source_url"] = source_url
288
+ try:
289
+ result = client.call_tool("review_program", args, timeout=150)
290
+ except MC.ToolError as e:
291
+ raise click.ClickException(f"Review failed: {e.text}")
292
+ except MC.McpError as e:
293
+ raise click.ClickException(f"Review failed: {e}")
294
+ review = result.structured or {}
295
+ findings = review.get("findings") or []
296
+ mark = {"error": "✗", "warning": "!", "note": "·"}
297
+ click.echo("")
298
+ click.echo(
299
+ review.get("summary")
300
+ or ("Review: no problems found." if not findings else "Review:")
301
+ )
302
+ for f in findings:
303
+ where = f" [{f['stepId']}]" if f.get("stepId") else ""
304
+ click.echo(f" {mark.get(f.get('severity'), '·')}{where} {f.get('message')}")
305
+ if f.get("suggestion"):
306
+ click.echo(f" → {f['suggestion']}")
307
+ if findings:
308
+ click.echo(
309
+ "\nThe file is unchanged; edit it and `rhylthyme validate` again.", err=True
310
+ )
311
+
312
+
211
313
  @click.command("search")
212
314
  @click.argument("query")
213
315
  @click.option(
@@ -252,7 +252,7 @@ def check_initialize(client, endpoint, opts, state) -> str:
252
252
  return f"{info['name']} {info.get('version', '?')}, protocol {result['protocolVersion']}"
253
253
 
254
254
 
255
- TOOL_DEFINITION_BUDGET_CHARS = 16000
255
+ TOOL_DEFINITION_BUDGET_CHARS = 17000
256
256
 
257
257
 
258
258
  def check_tools(client, endpoint, opts, state) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhylthyme-cli-runner
3
- Version: 0.2.5a0
3
+ Version: 0.2.7a0
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
@@ -561,8 +561,17 @@ rhylthyme search "pad thai" -i spoonacular # then impo
561
561
  ```
562
562
 
563
563
  **Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
564
- `--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
565
- source from stdin (with `-i`).
564
+ `--stdout`, `--publish` (and `--open`), `--no-validate`, `--review`. `-` reads
565
+ pasted source from stdin (with `-i`).
566
+
567
+ `--review` sends the import and its source to a model on the server (GPT-5.6
568
+ Luna, the model that related steps best in the paper's evaluation) and prints
569
+ what looks wrong: a default duration on "simmer until thick", a step the
570
+ parser dropped, steps chained that the source runs in parallel, a total that
571
+ disagrees with the source. It needs `rhylthyme login`, spends one model call
572
+ (capped per day), and changes nothing: the findings are yours to apply. On
573
+ rhylthyme.com the same review runs automatically after every import when you
574
+ are signed in.
566
575
 
567
576
  ### `rhylthyme render`
568
577
 
@@ -240,3 +240,74 @@ def test_a_text_importer_gets_a_urls_body(fake, monkeypatch):
240
240
  )
241
241
  assert r.exit_code == 0, r.output
242
242
  assert fake.calls[-1] == ("text", "def run(protocol): pass\n", None)
243
+
244
+
245
+ def test_a_missing_local_file_says_so(fake, tmp_path):
246
+ r = CliRunner().invoke(
247
+ cli, ["import", str(tmp_path / "Neapolitan Pizza.cook"), "-i", "fake"]
248
+ )
249
+ assert r.exit_code != 0 and "No such file" in r.output
250
+ assert fake.calls == []
251
+
252
+
253
+ def test_review_needs_a_login_before_any_work(fake, monkeypatch):
254
+ monkeypatch.delenv("RHYLTHYME_TOKEN", raising=False)
255
+ from rhylthyme_cli_runner.remote import auth
256
+
257
+ monkeypatch.setattr(auth, "load_credentials", lambda path=None: None)
258
+ r = CliRunner().invoke(
259
+ cli, ["import", "https://fake.example/dinner", "--review", "--stdout"]
260
+ )
261
+ assert r.exit_code != 0 and "rhylthyme login" in r.output
262
+ assert fake.calls == [], "nothing was imported"
263
+
264
+
265
+ def test_review_sends_the_program_and_source_and_prints_findings(
266
+ fake, monkeypatch, tmp_path
267
+ ):
268
+ monkeypatch.setenv("RHYLTHYME_TOKEN", "tok-1")
269
+ from rhylthyme_cli_runner.remote import mcp_client as MC
270
+
271
+ seen = {}
272
+
273
+ class FakeMcp:
274
+ def __init__(self, url=None, **kw):
275
+ seen["url"] = url
276
+
277
+ def call_tool(self, name, args, timeout=None, **kw):
278
+ seen["tool"], seen["args"] = name, args
279
+ return MC.ToolResult(
280
+ content=[{"type": "text", "text": "x"}],
281
+ structured={
282
+ "summary": "One problem.",
283
+ "usable": False,
284
+ "findings": [
285
+ {
286
+ "severity": "error",
287
+ "stepId": "s1",
288
+ "message": "Too short.",
289
+ "suggestion": "90m",
290
+ },
291
+ {"severity": "note", "message": "Fine."},
292
+ ],
293
+ },
294
+ )
295
+
296
+ monkeypatch.setattr(MC, "McpClient", FakeMcp)
297
+ src = tmp_path / "dinner.cook"
298
+ src.write_text("Simmer @sauce for ~{90%minutes}.\n")
299
+ fake.supported_domains = []
300
+ r = CliRunner().invoke(
301
+ cli,
302
+ ["import", str(src), "-i", "fake", "--review", "-o", str(tmp_path / "d.json")],
303
+ )
304
+ assert r.exit_code == 0, r.output
305
+ assert seen["tool"] == "review_program"
306
+ assert seen["args"]["token"] == "tok-1"
307
+ assert seen["args"]["program"]["programId"] == "fake-dinner"
308
+ assert seen["args"]["source_text"].startswith("Simmer @sauce")
309
+ assert (
310
+ "One problem." in r.output
311
+ and "✗ [s1] Too short." in r.output
312
+ and "→ 90m" in r.output
313
+ )