rhylthyme-cli-runner 0.2.6a0__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.6a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.7a0}/PKG-INFO +12 -3
  2. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/README.md +11 -2
  3. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/setup.py +1 -1
  4. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
  5. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/import_cli.py +101 -3
  6. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/checks.py +1 -1
  7. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +12 -3
  8. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_import_cli.py +63 -0
  9. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/LICENSE +0 -0
  10. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/pyproject.toml +0 -0
  11. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/setup.cfg +0 -0
  12. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/cli.py +0 -0
  13. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
  14. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
  15. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
  16. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
  17. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
  18. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
  19. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
  20. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
  21. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
  22. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
  23. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
  24. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
  25. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
  26. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
  27. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
  28. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
  29. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
  30. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
  31. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
  32. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
  33. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
  34. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
  35. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
  36. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
  37. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
  38. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
  39. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
  40. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
  41. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
  42. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/render.py +0 -0
  43. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
  44. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
  45. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
  46. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
  47. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
  48. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
  49. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
  50. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
  51. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
  52. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
  53. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
  54. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/cli.py +0 -0
  55. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
  56. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/render_cli.py +0 -0
  57. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
  58. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +0 -0
  59. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
  60. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
  61. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
  62. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
  63. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_calibrate.py +0 -0
  64. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_cli.py +0 -0
  65. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_baseline.py +0 -0
  66. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_harness.py +0 -0
  67. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_providers.py +0 -0
  68. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_eval_scorer.py +0 -0
  69. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_evaluate.py +0 -0
  70. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_example_validation.py +0 -0
  71. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_examples_integration.py +0 -0
  72. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_expand_replicates.py +0 -0
  73. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_hash_parity.py +0 -0
  74. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_history_report.py +0 -0
  75. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_mcp_checks.py +0 -0
  76. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_planner_time_strings.py +0 -0
  77. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_predict.py +0 -0
  78. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_program_runner.py +0 -0
  79. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_remote.py +0 -0
  80. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_render_cli.py +0 -0
  81. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_replay.py +0 -0
  82. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_run_recorder.py +0 -0
  83. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_skill.py +0 -0
  84. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_synth.py +0 -0
  85. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_usable.py +0 -0
  86. {rhylthyme_cli_runner-0.2.6a0 → rhylthyme_cli_runner-0.2.7a0}/tests/test_validate_examples_ci.py +0 -0
  87. {rhylthyme_cli_runner-0.2.6a0 → 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.6a0
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.6a0",
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.6a0"
9
+ __version__ = "0.2.7a0"
10
10
  __author__ = "Rhylthyme Team"
11
11
  __description__ = "CLI runner for Rhylthyme real-time program schedules"
12
12
 
@@ -134,8 +134,21 @@ def _validation(program: dict):
134
134
  @click.option(
135
135
  "--no-validate", is_flag=True, help="Skip validation of the imported program."
136
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
+ )
137
143
  def import_command(
138
- 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,
139
152
  ):
140
153
  """Import a recipe, protocol or deck as a program.
141
154
 
@@ -145,6 +158,7 @@ def import_command(
145
158
  .cook or .pptx file. `-` reads the source text from stdin (with -i).
146
159
  rhylthyme import https://www.seriouseats.com/the-best-chili-recipe
147
160
  rhylthyme import 52772 -i themealdb --publish
161
+ rhylthyme import recipe.cook -i cooklang --review # a second opinion on durations, order, gaps
148
162
  rhylthyme import https://raw.githubusercontent.com/Opentrons/Protocols/develop/protocols/007992/rna_isolation.ot2.apiv2.py
149
163
  rhylthyme importers # what is installed
150
164
  """
@@ -157,8 +171,22 @@ def import_command(
157
171
  )
158
172
  text = sys.stdin.read()
159
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
+ )
160
185
  click.echo(f"Importing with {importer.name}…", err=True)
161
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
+ )
162
190
  if not result.success:
163
191
  raise click.ClickException(result.error or "import failed")
164
192
  program = result.program
@@ -167,9 +195,10 @@ def import_command(
167
195
  v = _validation(program)
168
196
  errors = v.get("errors") or v.get("logic_errors") or []
169
197
  if not v.get("is_valid", v.get("valid", True)):
170
- for e in errors[:8]:
198
+ for finding in errors[:8]:
171
199
  click.echo(
172
- 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,
173
202
  )
174
203
  raise click.ClickException(
175
204
  "The imported program does not validate; not published. Re-run with --no-validate to keep it anyway."
@@ -191,6 +220,14 @@ def import_command(
191
220
  target.write_text(payload + "\n")
192
221
  click.echo(f"Saved {target}", err=True)
193
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
+
194
231
  if do_publish:
195
232
  from .remote.cli import publish as publish_command
196
233
 
@@ -212,6 +249,67 @@ def import_command(
212
249
  Path(tmp_path).unlink(missing_ok=True)
213
250
 
214
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
+
215
313
  @click.command("search")
216
314
  @click.argument("query")
217
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.6a0
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
 
@@ -248,3 +248,66 @@ def test_a_missing_local_file_says_so(fake, tmp_path):
248
248
  )
249
249
  assert r.exit_code != 0 and "No such file" in r.output
250
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
+ )