rote-cli 0.8.0__tar.gz → 0.10.0__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 (73) hide show
  1. {rote_cli-0.8.0 → rote_cli-0.10.0}/PKG-INFO +77 -7
  2. {rote_cli-0.8.0 → rote_cli-0.10.0}/README.md +70 -6
  3. {rote_cli-0.8.0 → rote_cli-0.10.0}/pyproject.toml +8 -0
  4. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/SKILL.md +26 -1
  5. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/references/crystallization-heuristics.md +34 -0
  6. rote_cli-0.10.0/skills/rote-graduate/references/implementation.md +109 -0
  7. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/references/ir-schema.md +43 -0
  8. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/references/node-kinds.md +12 -4
  9. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/__init__.py +1 -1
  10. rote_cli-0.10.0/src/rote/__main__.py +12 -0
  11. rote_cli-0.10.0/src/rote/_dbos.py +65 -0
  12. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/__init__.py +28 -11
  13. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/_common.py +47 -0
  14. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/_py_common.py +79 -16
  15. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/_ts_common.py +670 -0
  16. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/cloudflare.py +363 -43
  17. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/dbos.py +243 -48
  18. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/dbos_ts.py +240 -22
  19. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/inngest.py +259 -15
  20. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/python.py +5 -2
  21. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/adapters/temporal.py +103 -29
  22. rote_cli-0.10.0/src/rote/app_registry.py +85 -0
  23. rote_cli-0.10.0/src/rote/cli.py +3899 -0
  24. rote_cli-0.10.0/src/rote/cloud_auth.py +296 -0
  25. rote_cli-0.10.0/src/rote/cloud_graduate.py +628 -0
  26. rote_cli-0.10.0/src/rote/config.py +222 -0
  27. rote_cli-0.10.0/src/rote/deploy.py +206 -0
  28. rote_cli-0.10.0/src/rote/deploy_rote_cloud.py +182 -0
  29. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/__init__.py +4 -0
  30. rote_cli-0.10.0/src/rote/eval/baseline.py +655 -0
  31. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/empirical.py +51 -35
  32. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/scorecard.py +66 -0
  33. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/graduator/__init__.py +165 -4
  34. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/graduator/drivers/__init__.py +22 -0
  35. rote_cli-0.10.0/src/rote/graduator/drivers/_fs_tools.py +280 -0
  36. rote_cli-0.10.0/src/rote/graduator/drivers/anthropic_api.py +472 -0
  37. rote_cli-0.10.0/src/rote/graduator/drivers/claude.py +544 -0
  38. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/graduator/drivers/codex.py +13 -2
  39. rote_cli-0.10.0/src/rote/graduator/drivers/openai_api.py +431 -0
  40. rote_cli-0.10.0/src/rote/graduator/events.py +255 -0
  41. rote_cli-0.10.0/src/rote/init_wizard.py +168 -0
  42. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/ir.py +121 -0
  43. rote_cli-0.10.0/src/rote/mcp/__init__.py +52 -0
  44. rote_cli-0.10.0/src/rote/mcp/_runtime_helper.py +346 -0
  45. rote_cli-0.10.0/src/rote/mcp/auth.py +166 -0
  46. rote_cli-0.10.0/src/rote/mcp/registry.py +124 -0
  47. rote_cli-0.10.0/src/rote/mcp/release.py +317 -0
  48. rote_cli-0.10.0/src/rote/mcp/tokens.py +292 -0
  49. rote_cli-0.10.0/src/rote/probe.py +301 -0
  50. rote_cli-0.10.0/src/rote/runners/__init__.py +449 -0
  51. rote_cli-0.10.0/src/rote/runners/_node.py +87 -0
  52. rote_cli-0.10.0/src/rote/runners/cloudflare.py +172 -0
  53. rote_cli-0.10.0/src/rote/runners/dbos_ts.py +257 -0
  54. rote_cli-0.10.0/src/rote/runners/inngest.py +259 -0
  55. rote_cli-0.10.0/src/rote/runners/temporal.py +160 -0
  56. rote_cli-0.8.0/src/rote/cli.py +0 -1352
  57. rote_cli-0.8.0/src/rote/graduator/drivers/anthropic_api.py +0 -423
  58. rote_cli-0.8.0/src/rote/graduator/drivers/claude.py +0 -325
  59. {rote_cli-0.8.0 → rote_cli-0.10.0}/.gitignore +0 -0
  60. {rote_cli-0.8.0 → rote_cli-0.10.0}/LICENSE +0 -0
  61. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/references/eval-estimates.md +0 -0
  62. {rote_cli-0.8.0 → rote_cli-0.10.0}/skills/rote-graduate/references/llm-judge-extraction.md +0 -0
  63. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/estimate.py +0 -0
  64. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/pricing.py +0 -0
  65. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/priors.py +0 -0
  66. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/sidecar.py +0 -0
  67. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/eval/tokens.py +0 -0
  68. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/graduator/update.py +0 -0
  69. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/serve/__init__.py +0 -0
  70. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/serve/backends.py +0 -0
  71. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/serve/registry.py +0 -0
  72. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/serve/server.py +0 -0
  73. {rote_cli-0.8.0 → rote_cli-0.10.0}/src/rote/skill_source.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rote-cli
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Graduate fuzzy AI skills into deterministic, reliable workflows
5
5
  Project-URL: Homepage, https://github.com/trevhud/rote
6
6
  Project-URL: Repository, https://github.com/trevhud/rote
@@ -32,11 +32,17 @@ Requires-Dist: dbos>=2.26; extra == 'dev'
32
32
  Requires-Dist: fastmcp>=3.4.2; extra == 'dev'
33
33
  Requires-Dist: httpx>=0.28; extra == 'dev'
34
34
  Requires-Dist: mypy>=1.10; extra == 'dev'
35
+ Requires-Dist: openai>=2.45; extra == 'dev'
35
36
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.0; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.6; extra == 'dev'
38
39
  Requires-Dist: temporalio>=1.7; extra == 'dev'
39
40
  Requires-Dist: types-pyyaml; extra == 'dev'
41
+ Provides-Extra: mcp
42
+ Requires-Dist: fastmcp>=3.4.2; extra == 'mcp'
43
+ Requires-Dist: httpx>=0.28; extra == 'mcp'
44
+ Provides-Extra: openai-api
45
+ Requires-Dist: openai>=2.45; extra == 'openai-api'
40
46
  Provides-Extra: serve
41
47
  Requires-Dist: fastmcp>=3.4.2; extra == 'serve'
42
48
  Requires-Dist: httpx>=0.28; extra == 'serve'
@@ -221,8 +227,8 @@ emitted code + a README on how to run, signal gates, and deploy).
221
227
 
222
228
  ### Other commands
223
229
 
224
- - **`rote emit <pipeline.yaml>`** — run just the adapter step on an
225
- existing IR (no LLM, no cost). The cheap inner loop while iterating on
230
+ - **`rote emit <pipeline.yaml> --out <dir>`** — run just the adapter step
231
+ on an existing IR (no LLM, no cost). The cheap inner loop while iterating on
226
232
  adapters or IR shapes. Re-emitting is safe: a `.rote-manifest.json`
227
233
  tracks what `rote` wrote, and files you've edited are left untouched
228
234
  (the fresh version lands as `<name>.new`).
@@ -231,6 +237,61 @@ emitted code + a README on how to run, signal gates, and deploy).
231
237
  `provenance.json` and re-derives only the nodes whose source sections
232
238
  changed; unchanged nodes keep their ids (so in-flight durable workflows
233
239
  aren't orphaned) and implemented stubs are kept. No change → no agent run.
240
+ - **`rote run <path>`** — one-off local execution of either side. A
241
+ skill directory runs as an agent via `claude -p` (your registered MCP
242
+ servers injected, read-only tool gate unless `--allow-writes`); an
243
+ emitted runtime directory — or a `graduate --out` directory — runs
244
+ the pipeline itself — all six runtimes (`python`/`dbos`/`temporal`
245
+ in-process or on a managed local dev server, `cloudflare` under
246
+ `wrangler dev`, `inngest` against a managed `inngest-cli dev`,
247
+ `dbos-ts` against your Postgres or a throwaway Docker one). HITL
248
+ gate payloads via `--signal name='{...}'` or an interactive prompt.
249
+ Runtimes that bundle a dev UI surface it: temporal runs print a live
250
+ Temporal Web UI URL and inngest runs print the dev-server dashboard,
251
+ both live for the duration of the run. Output JSON on stdout, status
252
+ on stderr, so it pipes.
253
+ - **`rote deploy <path>`** — push an emitted pipeline where it runs:
254
+ `cloudflare` wraps `npx wrangler deploy` (with `--dry-run`), `dbos` /
255
+ `dbos-ts` wrap `npx dbos-cloud app deploy` — the vendor CLI owns auth
256
+ and output; rote adds detection and preflights (including surfacing
257
+ *which* account your wrangler session belongs to before uploading).
258
+ Runtimes with no push model (temporal, inngest, python) print honest
259
+ hosting guidance with doc links instead of a fake action.
260
+ `--target rote-cloud` bundles a cloudflare-emitted app (esbuild via
261
+ npx) and uploads it to a hosted rote-cloud instance — with a stored
262
+ `rote login`, no flags or env vars needed (`--url`/`--token` and
263
+ `$ROTE_CLOUD_URL`/`$ROTE_CLOUD_TOKEN` still override).
264
+ - **`rote login`** — connect the CLI to a rote-cloud account via the
265
+ OAuth device flow: your browser opens with a one-time code pre-filled
266
+ (over SSH, `--device` prints the code + URL instead), you click
267
+ Approve, and the CLI stores a tenant API key at
268
+ `~/.local/share/rote/cloud.json` (mode 0600). Once logged in,
269
+ `rote graduate` runs **on rote cloud by default**: the skill bundle
270
+ syncs up (sha-diffed, so unchanged files don't re-upload), the
271
+ platform runs the graduation server-side, live progress streams back
272
+ through the same renderer as a local run, the result auto-deploys,
273
+ and the artifacts download into your `--out` directory in the exact
274
+ local layout. `--local` keeps the graduation on your machine (then
275
+ the cloudflare-emit + auto-deploy flow applies), `--no-deploy` or a
276
+ config opt-out (`runtime:` pinned to a local target, or
277
+ `deploy: none`) keeps everything local; `--cloud` forces the server
278
+ even where config says otherwise. Logged out, everything works
279
+ locally exactly as before. `rote whoami` shows the account (verified
280
+ live); `rote logout` revokes the key server-side and clears the store.
281
+ - **`rote init`** — one-time interactive onboarding: pick where
282
+ graduated pipelines run (rote cloud — with login offered inline — or
283
+ a local runtime, with a one-line pitch for each), which graduator
284
+ driver does the work (availability probed live), and optionally a
285
+ model. Answers are saved to `~/.config/rote/config.yaml`
286
+ (`--project` writes a `./rote.yaml` that overrides it per-repo) and
287
+ every later command reads them. It's the only interactive command
288
+ besides login — CI never hits a prompt.
289
+ - **`rote config`** — print every configurable default with its
290
+ effective value *and the layer that set it*. Resolution everywhere is
291
+ `flag > ROTE_* env (ROTE_RUNTIME, ROTE_DEPLOY, ROTE_AGENT,
292
+ ROTE_MODEL) > project rote.yaml > user config > built-in`. Config
293
+ files are strict: a typo'd key or value is a loud error, never a
294
+ silent fallback. `--json` for automation.
234
295
  - **`rote eval <graduated>`** — render the before/after scorecard (wall
235
296
  clock, cost across the current model lineup at live prices, and how
236
297
  much of the run is still LLM-decided). `rote graduate` writes this to
@@ -266,9 +327,12 @@ way, prefer the more deterministic kind.
266
327
 
267
328
  ## Runtimes
268
329
 
269
- Pick with `--runtime`; the same IR drives all of them. None of the
270
- emitted code references MCP — the crystallization step replaces tool
271
- calls with direct vendor API calls.
330
+ Pick with `--runtime`; the same IR drives all of them. Under
331
+ `--backend api`, none of the emitted code references MCP — the
332
+ crystallization step replaces tool calls with direct vendor API calls.
333
+ Under the default `--backend mcp`, tool-using nodes emit a working MCP
334
+ client call (with durable park-on-auth on every MCP-capable runtime);
335
+ see [`docs/mcp-client.md`](docs/mcp-client.md).
272
336
 
273
337
  | Runtime | `--runtime` | Language | Shape | Notes |
274
338
  | --- | --- | --- | --- | --- |
@@ -348,7 +412,7 @@ Trusted Publishing ([docs/releasing.md](docs/releasing.md)).
348
412
 
349
413
  ```
350
414
  rote/
351
- ├── docs/ agent-runtime · mcp-trigger · releasing
415
+ ├── docs/ agent-runtime · mcp-client · mcp-trigger · releasing
352
416
  ├── skills/rote-graduate/ the graduator agent (SKILL.md + 4 reference files)
353
417
  ├── src/rote/
354
418
  │ ├── cli.py rote graduate / emit / eval / serve
@@ -367,9 +431,15 @@ rote/
367
431
 
368
432
  ## Documentation
369
433
 
434
+ - [`AGENTS.md`](AGENTS.md) — operating manual for a coding agent *driving*
435
+ `rote` as an installed tool (invocation contract, the slow/costs-money
436
+ `graduate` flow, auth, failure recovery, the stub-filling job, `--json`)
370
437
  - [`docs/agent-runtime.md`](docs/agent-runtime.md) — design record for the
371
438
  driver abstraction (the `claude -p` env gotcha; the non-use of
372
439
  `claude-agent-sdk`)
440
+ - [`docs/mcp-client.md`](docs/mcp-client.md) — the OAuth MCP client
441
+ emitted code uses under `--backend mcp`: endpoint/credential
442
+ resolution and durable park-on-auth across every MCP-capable runtime
373
443
  - [`docs/mcp-trigger.md`](docs/mcp-trigger.md) — `rote register` +
374
444
  `rote serve`: graduated pipelines as MCP tools (FastMCP 3.x)
375
445
  - [`docs/releasing.md`](docs/releasing.md) — tag-driven PyPI Trusted
@@ -175,8 +175,8 @@ emitted code + a README on how to run, signal gates, and deploy).
175
175
 
176
176
  ### Other commands
177
177
 
178
- - **`rote emit <pipeline.yaml>`** — run just the adapter step on an
179
- existing IR (no LLM, no cost). The cheap inner loop while iterating on
178
+ - **`rote emit <pipeline.yaml> --out <dir>`** — run just the adapter step
179
+ on an existing IR (no LLM, no cost). The cheap inner loop while iterating on
180
180
  adapters or IR shapes. Re-emitting is safe: a `.rote-manifest.json`
181
181
  tracks what `rote` wrote, and files you've edited are left untouched
182
182
  (the fresh version lands as `<name>.new`).
@@ -185,6 +185,61 @@ emitted code + a README on how to run, signal gates, and deploy).
185
185
  `provenance.json` and re-derives only the nodes whose source sections
186
186
  changed; unchanged nodes keep their ids (so in-flight durable workflows
187
187
  aren't orphaned) and implemented stubs are kept. No change → no agent run.
188
+ - **`rote run <path>`** — one-off local execution of either side. A
189
+ skill directory runs as an agent via `claude -p` (your registered MCP
190
+ servers injected, read-only tool gate unless `--allow-writes`); an
191
+ emitted runtime directory — or a `graduate --out` directory — runs
192
+ the pipeline itself — all six runtimes (`python`/`dbos`/`temporal`
193
+ in-process or on a managed local dev server, `cloudflare` under
194
+ `wrangler dev`, `inngest` against a managed `inngest-cli dev`,
195
+ `dbos-ts` against your Postgres or a throwaway Docker one). HITL
196
+ gate payloads via `--signal name='{...}'` or an interactive prompt.
197
+ Runtimes that bundle a dev UI surface it: temporal runs print a live
198
+ Temporal Web UI URL and inngest runs print the dev-server dashboard,
199
+ both live for the duration of the run. Output JSON on stdout, status
200
+ on stderr, so it pipes.
201
+ - **`rote deploy <path>`** — push an emitted pipeline where it runs:
202
+ `cloudflare` wraps `npx wrangler deploy` (with `--dry-run`), `dbos` /
203
+ `dbos-ts` wrap `npx dbos-cloud app deploy` — the vendor CLI owns auth
204
+ and output; rote adds detection and preflights (including surfacing
205
+ *which* account your wrangler session belongs to before uploading).
206
+ Runtimes with no push model (temporal, inngest, python) print honest
207
+ hosting guidance with doc links instead of a fake action.
208
+ `--target rote-cloud` bundles a cloudflare-emitted app (esbuild via
209
+ npx) and uploads it to a hosted rote-cloud instance — with a stored
210
+ `rote login`, no flags or env vars needed (`--url`/`--token` and
211
+ `$ROTE_CLOUD_URL`/`$ROTE_CLOUD_TOKEN` still override).
212
+ - **`rote login`** — connect the CLI to a rote-cloud account via the
213
+ OAuth device flow: your browser opens with a one-time code pre-filled
214
+ (over SSH, `--device` prints the code + URL instead), you click
215
+ Approve, and the CLI stores a tenant API key at
216
+ `~/.local/share/rote/cloud.json` (mode 0600). Once logged in,
217
+ `rote graduate` runs **on rote cloud by default**: the skill bundle
218
+ syncs up (sha-diffed, so unchanged files don't re-upload), the
219
+ platform runs the graduation server-side, live progress streams back
220
+ through the same renderer as a local run, the result auto-deploys,
221
+ and the artifacts download into your `--out` directory in the exact
222
+ local layout. `--local` keeps the graduation on your machine (then
223
+ the cloudflare-emit + auto-deploy flow applies), `--no-deploy` or a
224
+ config opt-out (`runtime:` pinned to a local target, or
225
+ `deploy: none`) keeps everything local; `--cloud` forces the server
226
+ even where config says otherwise. Logged out, everything works
227
+ locally exactly as before. `rote whoami` shows the account (verified
228
+ live); `rote logout` revokes the key server-side and clears the store.
229
+ - **`rote init`** — one-time interactive onboarding: pick where
230
+ graduated pipelines run (rote cloud — with login offered inline — or
231
+ a local runtime, with a one-line pitch for each), which graduator
232
+ driver does the work (availability probed live), and optionally a
233
+ model. Answers are saved to `~/.config/rote/config.yaml`
234
+ (`--project` writes a `./rote.yaml` that overrides it per-repo) and
235
+ every later command reads them. It's the only interactive command
236
+ besides login — CI never hits a prompt.
237
+ - **`rote config`** — print every configurable default with its
238
+ effective value *and the layer that set it*. Resolution everywhere is
239
+ `flag > ROTE_* env (ROTE_RUNTIME, ROTE_DEPLOY, ROTE_AGENT,
240
+ ROTE_MODEL) > project rote.yaml > user config > built-in`. Config
241
+ files are strict: a typo'd key or value is a loud error, never a
242
+ silent fallback. `--json` for automation.
188
243
  - **`rote eval <graduated>`** — render the before/after scorecard (wall
189
244
  clock, cost across the current model lineup at live prices, and how
190
245
  much of the run is still LLM-decided). `rote graduate` writes this to
@@ -220,9 +275,12 @@ way, prefer the more deterministic kind.
220
275
 
221
276
  ## Runtimes
222
277
 
223
- Pick with `--runtime`; the same IR drives all of them. None of the
224
- emitted code references MCP — the crystallization step replaces tool
225
- calls with direct vendor API calls.
278
+ Pick with `--runtime`; the same IR drives all of them. Under
279
+ `--backend api`, none of the emitted code references MCP — the
280
+ crystallization step replaces tool calls with direct vendor API calls.
281
+ Under the default `--backend mcp`, tool-using nodes emit a working MCP
282
+ client call (with durable park-on-auth on every MCP-capable runtime);
283
+ see [`docs/mcp-client.md`](docs/mcp-client.md).
226
284
 
227
285
  | Runtime | `--runtime` | Language | Shape | Notes |
228
286
  | --- | --- | --- | --- | --- |
@@ -302,7 +360,7 @@ Trusted Publishing ([docs/releasing.md](docs/releasing.md)).
302
360
 
303
361
  ```
304
362
  rote/
305
- ├── docs/ agent-runtime · mcp-trigger · releasing
363
+ ├── docs/ agent-runtime · mcp-client · mcp-trigger · releasing
306
364
  ├── skills/rote-graduate/ the graduator agent (SKILL.md + 4 reference files)
307
365
  ├── src/rote/
308
366
  │ ├── cli.py rote graduate / emit / eval / serve
@@ -321,9 +379,15 @@ rote/
321
379
 
322
380
  ## Documentation
323
381
 
382
+ - [`AGENTS.md`](AGENTS.md) — operating manual for a coding agent *driving*
383
+ `rote` as an installed tool (invocation contract, the slow/costs-money
384
+ `graduate` flow, auth, failure recovery, the stub-filling job, `--json`)
324
385
  - [`docs/agent-runtime.md`](docs/agent-runtime.md) — design record for the
325
386
  driver abstraction (the `claude -p` env gotcha; the non-use of
326
387
  `claude-agent-sdk`)
388
+ - [`docs/mcp-client.md`](docs/mcp-client.md) — the OAuth MCP client
389
+ emitted code uses under `--backend mcp`: endpoint/credential
390
+ resolution and durable park-on-auth across every MCP-capable runtime
327
391
  - [`docs/mcp-trigger.md`](docs/mcp-trigger.md) — `rote register` +
328
392
  `rote serve`: graduated pipelines as MCP tools (FastMCP 3.x)
329
393
  - [`docs/releasing.md`](docs/releasing.md) — tag-driven PyPI Trusted
@@ -50,6 +50,9 @@ temporal = [
50
50
  api = [
51
51
  "anthropic>=0.89",
52
52
  ]
53
+ openai-api = [
54
+ "openai>=2.45",
55
+ ]
53
56
  dbos = [
54
57
  "dbos>=2.26",
55
58
  ]
@@ -57,6 +60,10 @@ serve = [
57
60
  "fastmcp>=3.4.2",
58
61
  "httpx>=0.28",
59
62
  ]
63
+ mcp = [
64
+ "fastmcp>=3.4.2",
65
+ "httpx>=0.28",
66
+ ]
60
67
  dev = [
61
68
  "pytest>=8.0",
62
69
  "pytest-asyncio>=0.23",
@@ -65,6 +72,7 @@ dev = [
65
72
  "types-pyyaml",
66
73
  "temporalio>=1.7",
67
74
  "anthropic>=0.89",
75
+ "openai>=2.45",
68
76
  "dbos>=2.26",
69
77
  "fastmcp>=3.4.2",
70
78
  "httpx>=0.28",
@@ -46,6 +46,19 @@ re-deriving a known-fixed procedure at runtime is waste.
46
46
 
47
47
  ## Pipeline
48
48
 
49
+ **Progress markers.** At the START of each Phase N, rewrite
50
+ `progress.ndjson` in the work directory so it holds one JSON line per
51
+ phase entered so far, in order:
52
+
53
+ ```
54
+ {"phase": 1, "name": "Intake"}
55
+ {"phase": 2, "name": "Node Classification"}
56
+ ```
57
+
58
+ Rewrite the whole file each time (append the new phase's line to the
59
+ ones already there). This file is machine-read for live progress
60
+ display; it is not a deliverable and never affects the IR.
61
+
49
62
  ### Phase 1: Intake
50
63
 
51
64
  Read the target directory exhaustively:
@@ -86,7 +99,13 @@ justification per row.
86
99
 
87
100
  ### Phase 3: Codification Scan
88
101
 
89
- Read `references/crystallization-heuristics.md` before starting this phase.
102
+ Read `references/crystallization-heuristics.md` and
103
+ `references/implementation.md` before starting this phase. The second
104
+ file governs how far to take each extracted module: contract-documented
105
+ stubs by default, **working implementations plus golden-fixture tests**
106
+ when ground truth is available (a `probe-context/` directory of observed
107
+ MCP traffic in your work dir, and/or web research tools to verify
108
+ current vendor API shapes).
90
109
 
91
110
  For each node classified as `pure_function` or `external_call`, surface every
92
111
  codification opportunity:
@@ -174,6 +193,12 @@ Checklist before declaring Phase 5 done:
174
193
 
175
194
  - Every `pure_function` / `external_call` node has an `impl:` pointing
176
195
  at a real file you created in Phase 3.
196
+ - Every step that calls an MCP tool in the source skill carries the
197
+ matching `mcp:` binding (see ir-schema.md — and cross-check against
198
+ `probe-context/observed-tools.json` when it exists: observed traffic
199
+ with no binding is a graduation bug).
200
+ - If you wrote working implementations, `tests/test_extracted.py`
201
+ exists, is deterministic and offline, and you believe it passes.
177
202
  - Every `llm_judge` node has a `signature:` pointing at a real file
178
203
  you created in Phase 4.
179
204
  - Every `agent_loop` node has `tools:` listed.
@@ -110,6 +110,17 @@ code makes skipping impossible.
110
110
  "always", "must", "never skip", "do not skip", "be sure to". Audit
111
111
  every hit.
112
112
 
113
+ **Calibration — err toward marking.** On real production skills MOST
114
+ deterministic nodes end up `mandatory: true`, not a handful: every
115
+ fixed threshold, every filter/exclusion rule, every data pull the
116
+ output cannot be correct without, every fixed template render. (The
117
+ first agent run on the ops-report example marked 1 of 13 nodes when
118
+ 9 of 10 was right — a daily report is *made of* rules that must all
119
+ fire.) Ask per node: "if the agent version silently skipped this
120
+ step, would the output be wrong or dangerous?" If yes, mark it.
121
+ Reserve unmarked for genuinely optional enrichment — a nice-to-have
122
+ field, a best-effort lookup with a fallback.
123
+
113
124
  ---
114
125
 
115
126
  ### Pattern 4 — Fixed string templates for reports and outputs
@@ -200,6 +211,29 @@ but they're actually hard thresholds.
200
211
  **How to detect:** look for numeric comparisons in rubrics ("below X",
201
212
  "above Y", "at least Z"). Every one is a potential pre-filter.
202
213
 
214
+ ### Pattern 8 — Independent data pulls hiding in sequential prose
215
+
216
+ **What it looks like:** the skill lists data-gathering steps in
217
+ sequence ("First pull the Slack channel. Then run the Gmail
218
+ searches.") but no step consumes another's output — the ordering
219
+ exists only because an agent executes one tool call at a time.
220
+
221
+ **What to do:** give the independent pulls no connecting edges so
222
+ they land in the same execution wave, and list every independent
223
+ entry point in `entry_nodes` (plural!). Adapters run same-wave nodes
224
+ concurrently — the graduated pipeline should be *faster* than the
225
+ agent, and parallel data pulls are usually the single biggest
226
+ wall-clock win available.
227
+
228
+ **How to detect:** for each pair of adjacent data-pull steps, ask
229
+ "does step B's input mention anything step A produced?" If not, they
230
+ are parallel regardless of the prose order. The deal-monitor example
231
+ is the canonical case: the Slack intake pull and the fixed Gmail
232
+ searches share nothing and must be two entry nodes — the first agent
233
+ run serialized them into one entry and left the speedup on the table.
234
+
235
+ ---
236
+
203
237
  ## When NOT to crystallize
204
238
 
205
239
  The hunt for crystallization should be aggressive, but not blind. Leave
@@ -0,0 +1,109 @@
1
+ # Writing Extracted Implementations
2
+
3
+ Phase 3 produces `extracted/*.py` modules. This file governs **how much
4
+ of each module you implement** and how to prove what you wrote is
5
+ right. The bar depends on what evidence you have.
6
+
7
+ ## The two regimes
8
+
9
+ ### Regime 1 — No ground truth (default)
10
+
11
+ No `probe-context/` directory exists in your work dir, and you have no
12
+ web research tools. Emit **contract-documented stubs**: the function
13
+ signature, a docstring stating the input/output contract as precisely
14
+ as the skill text allows, and a `raise NotImplementedError(...)` body.
15
+ Do NOT write vendor API calls from memory — SDK and endpoint shapes
16
+ drift faster than any model's training data, and a plausible-but-wrong
17
+ implementation is worse than an honest stub because it fails at
18
+ runtime instead of at review.
19
+
20
+ ### Regime 2 — Ground truth available (implement for real)
21
+
22
+ `probe-context/` exists (observed MCP traffic + inferred schemas from
23
+ an instrumented run of this exact skill), and/or you have WebSearch /
24
+ WebFetch. Now you are expected to write **working implementations**:
25
+
26
+ 1. **Contract from observation.** The observed input payload keys are
27
+ your function parameters; the observed result shape is your return
28
+ contract. If the skill prose disagrees with the observed traffic,
29
+ trust the traffic and note the discrepancy in the graduation report.
30
+ 2. **Endpoint from current docs, never memory.** Before writing a
31
+ vendor call, use WebSearch/WebFetch to confirm the current endpoint,
32
+ auth scheme, request/response shape, and official SDK name. Cite the
33
+ doc URL in the module docstring. If you cannot verify a shape, fall
34
+ back to Regime 1 for that function and say why in the report.
35
+ 3. **Credentials via environment variables only.** One variable per
36
+ vendor, named `<VENDOR>_API_KEY` (or the vendor's own documented
37
+ convention, e.g. `GOOGLE_APPLICATION_CREDENTIALS`). Read it at call
38
+ time, and raise a clear `RuntimeError` naming the variable when it
39
+ is missing. NEVER hardcode a credential, NEVER invent a
40
+ plausible-looking value, NEVER default to a dummy that would send a
41
+ request with fake auth.
42
+ 4. **Minimal dependencies.** Prefer the vendor's official Python SDK
43
+ when one exists and is current; otherwise plain `httpx`. Every
44
+ third-party dependency you introduce must be listed in the module
45
+ docstring so the adapter's requirements emission can pick it up.
46
+ 5. **Honest error surfaces.** No silent fallbacks, no bare excepts, no
47
+ returning empty results on failure. A 4xx/5xx should raise with the
48
+ response body attached — errors are how the durable runtime knows
49
+ to retry or park.
50
+
51
+ ## Tests are part of the deliverable
52
+
53
+ When you implement for real (Regime 2), also write
54
+ `tests/test_extracted.py` in your work dir:
55
+
56
+ - **Golden-fixture tests from observed payloads.** Every parse /
57
+ transform / filter function gets called with a real observed payload
58
+ (copy it from `probe-context/observed-tools.json` into the test as a
59
+ fixture literal) and asserted against the real result. These are the
60
+ highest-value tests you can write: they prove the implementation
61
+ handles what production actually sends.
62
+ - **Network functions get a transport-stubbed test.** Monkeypatch the
63
+ HTTP layer (or inject a fake client) and assert the function builds
64
+ the documented request and parses the documented response — using
65
+ the observed payloads as the canned response body. **No test may
66
+ touch the network**, require credentials, or be nondeterministic:
67
+ they must pass with plain `pytest` in an offline environment.
68
+ - **Constants get pinned.** Every constant lifted in Phase 3 (batch
69
+ sizes, thresholds, day windows) gets one assertion, so a silent edit
70
+ fails a test instead of shipping.
71
+
72
+ Structure the file so `pytest tests/` passes from the work dir root —
73
+ imports as `from extracted.<module> import <fn>` with a conftest or
74
+ sys.path line if needed. If a test would need something you can't
75
+ provide deterministically, don't write a flaky approximation; note the
76
+ gap in the graduation report instead.
77
+
78
+ ## What NOT to do, in any regime
79
+
80
+ - Do not write to `signatures/` from this file's rules — judge modules
81
+ are governed by `llm-judge-extraction.md`.
82
+ - Do not call MCP at runtime from extracted code. The `mcp:` binding on
83
+ the node (see `ir-schema.md`) is how the MCP backend wires itself;
84
+ your `extracted/` implementation is the *direct-API* backend the
85
+ user selects with `--backend api`. Keep both true to the same
86
+ contract.
87
+ - Do not invent data the observation doesn't show. An inferred schema
88
+ from one sample marks fields it saw; absence of a field is not
89
+ license to add speculative ones.
90
+ - Do not soften a failing contract to make a test pass. The observed
91
+ payload is the spec; if your implementation can't reproduce it,
92
+ the implementation is wrong.
93
+
94
+ ## BDR worked example
95
+
96
+ `enrich_contact_batch` under Regime 2, with an observed
97
+ `zoominfo_enrich_contacts` call in probe-context:
98
+
99
+ - Function takes `contacts: list[dict]` (the observed input key),
100
+ chunks by the lifted `BATCH_SIZE = 10`, calls the documented
101
+ ZoomInfo enrich endpoint with `ZOOMINFO_API_KEY` from the env, and
102
+ returns the observed response shape (`{"contacts": [...]}`)
103
+ normalized per the inferred schema.
104
+ - `tests/test_extracted.py` asserts: chunking math at the boundary
105
+ (10, 11, 20 contacts), request shape against a stubbed transport
106
+ using the observed input, response parsing against the observed
107
+ result payload verbatim, and `BATCH_SIZE == 10`.
108
+ - Module docstring cites the ZoomInfo enrich doc URL fetched during
109
+ implementation and lists `httpx` as the dependency.
@@ -99,8 +99,51 @@ kind. The validator enforces kind-specific requirements.
99
99
  strategy: persistent
100
100
  ttl: 30d
101
101
  fan_out: false # optional, if true node invoked per input element
102
+ mcp: # external_call only — see "MCP bindings" below
103
+ server: hubspot # logical MCP server name (identifier)
104
+ tool: hubspot_batch_upsert # exact MCP tool name the skill calls
102
105
  ```
103
106
 
107
+ ### MCP bindings (`mcp:`) — REQUIRED when the source step is an MCP tool call
108
+
109
+ If the skill step you are graduating calls an MCP tool — and in
110
+ production skills, almost every data pull and vendor write does — the
111
+ `external_call` node **must** carry an `mcp:` binding recording the
112
+ logical server name and the exact tool name:
113
+
114
+ ```yaml
115
+ - id: fetch_intake_messages
116
+ kind: external_call
117
+ impl: extracted/slack.py:fetch_intake_messages
118
+ mcp:
119
+ server: slack # the MCP server the skill's tool lives on
120
+ tool: slack_read_channel # the tool name, exactly as the skill calls it
121
+ ```
122
+
123
+ This is not documentation — the binding is load-bearing:
124
+
125
+ - With `--backend mcp` (the default), adapters emit a **working call to
126
+ that tool** instead of a NotImplementedError stub; without the binding
127
+ the user has to hand-write the integration you could have wired.
128
+ - The pipeline's required-MCP-servers manifest, preflight auth
129
+ recommendations, and durable park-on-auth all derive from bindings —
130
+ an unbound node is invisible to all three.
131
+ - `rote graduate` cross-checks bindings against traffic observed in a
132
+ baseline run; a tool the skill demonstrably calls with no binding is
133
+ reported as a missed requirement (i.e., a graduation bug).
134
+
135
+ Rules:
136
+
137
+ - One binding per node — the server + tool pair the step actually uses.
138
+ If a step uses two tools, it's two nodes (or an `agent_loop`).
139
+ - `server` is the *logical* name (identifier: letters/digits/underscore);
140
+ it resolves to an endpoint at runtime via the rote registry — do not
141
+ put URLs in the binding unless the skill pins an explicit endpoint.
142
+ - Keep `impl:` too: the binding and the extracted stub are the two
143
+ backends (`mcp` vs `api`) of the same node.
144
+ - Steps that are *not* MCP tool calls (plain HTTP the skill documents,
145
+ browser automation, file I/O) take no binding — never invent one.
146
+
104
147
  ### Data-flow bindings (`inputs:`)
105
148
 
106
149
  `input:` documents *types*; `inputs:` binds *where the values come
@@ -66,10 +66,18 @@ deterministic. If the agent decides which tool to call and what to pass
66
66
  each iteration, it's an `agent_loop`.
67
67
 
68
68
  **MCP → deterministic API:** this is the kind that most often originates
69
- as an MCP tool call in the source skill. The graduation step is to record
70
- the underlying vendor API the tool wraps (e.g.,
71
- `POST /crm/v3/objects/contacts/batch/upsert`) in the `impl`'s docstring so
72
- the trace from skill → MCP tool → REST endpoint is visible in the code.
69
+ as an MCP tool call in the source skill. Graduating it has TWO required
70
+ outputs, not one:
71
+
72
+ 1. An **`mcp:` binding on the node** — `server` + `tool`, exactly as the
73
+ skill calls it (see "MCP bindings" in ir-schema.md). This is
74
+ load-bearing: the default backend emits a working call to that tool
75
+ from the binding, and requirements/auth features derive from it.
76
+ Omitting it on a step that calls an MCP tool is a graduation bug.
77
+ 2. The underlying vendor API the tool wraps (e.g.,
78
+ `POST /crm/v3/objects/contacts/batch/upsert`) recorded in the
79
+ `impl`'s docstring, so the trace from skill → MCP tool → REST
80
+ endpoint is visible in the code.
73
81
 
74
82
  ---
75
83
 
@@ -1,3 +1,3 @@
1
1
  """rote — graduate fuzzy AI skills into deterministic workflows."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.10.0"
@@ -0,0 +1,12 @@
1
+ """``python -m rote`` — the same CLI as the ``rote`` console script.
2
+
3
+ Exists so generated configs can invoke rote through a known
4
+ interpreter (``sys.executable -m rote``) instead of guessing at PATH —
5
+ the eval harness's ``headersHelper`` wiring depends on this.
6
+ """
7
+
8
+ import sys
9
+
10
+ from rote.cli import main
11
+
12
+ sys.exit(main())
@@ -0,0 +1,65 @@
1
+ """Shared introspection helpers for emitted DBOS apps.
2
+
3
+ Used anywhere rote needs to talk to an emitted app's system database
4
+ from outside the app process: the eval harness (gate signaling) and the
5
+ MCP auth-release path (``rote mcp login`` waking parked workflows).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+
17
+ def _load_dbos_config(app_dir: Path) -> dict[str, object]:
18
+ config = yaml.safe_load((app_dir / "dbos-config.yaml").read_text(encoding="utf-8"))
19
+ return config if isinstance(config, dict) else {}
20
+
21
+
22
+ def dbos_system_database_url(app_dir: Path) -> str:
23
+ """The system database URL an emitted DBOS *Python* app will use.
24
+
25
+ Must mirror the emitted main.py's resolution order exactly —
26
+ ``DBOS_SYSTEM_DATABASE_URL`` env override first, then the default
27
+ SQLite file in the app dir — or messages get delivered to a
28
+ database the app isn't watching.
29
+ """
30
+ override = os.environ.get("DBOS_SYSTEM_DATABASE_URL")
31
+ if override:
32
+ return override
33
+ config = _load_dbos_config(app_dir)
34
+ name = str(config.get("name", "pipeline"))
35
+ return f"sqlite:///{(app_dir / f'{name}.dbos.sqlite').resolve()}"
36
+
37
+
38
+ def dbos_ts_system_database_url(app_dir: Path) -> str:
39
+ """The system database URL an emitted DBOS *TypeScript* app will use.
40
+
41
+ Mirrors the TS SDK's resolution (verified against dbos-transact-ts
42
+ ``src/config.ts``, SDK 4.23.x): the emitted main.ts passes
43
+ ``process.env.DBOS_SYSTEM_DATABASE_URL`` to ``DBOS.setConfig``; a
44
+ ``system_database_url`` key in dbos-config.yaml (with ``${VAR}``
45
+ substitution — unset vars become empty, exactly like the SDK) wins
46
+ next; otherwise the SDK default: Postgres at ``PGHOST``/``PGPORT``
47
+ with ``PGUSER``/``PGPASSWORD`` (falling back to
48
+ ``postgres:dbos@localhost:5432``) and database ``<name>_dbos_sys``.
49
+ The TS SDK is Postgres-only — there is no SQLite branch.
50
+ """
51
+ override = os.environ.get("DBOS_SYSTEM_DATABASE_URL")
52
+ if override:
53
+ return override
54
+ config = _load_dbos_config(app_dir)
55
+ configured = config.get("system_database_url")
56
+ if isinstance(configured, str) and configured:
57
+ expanded = re.sub(r"\$\{(\w+)\}", lambda m: os.environ.get(m.group(1), ""), configured)
58
+ if expanded:
59
+ return expanded
60
+ name = re.sub(r"[^a-zA-Z0-9_]", "_", str(config.get("name", "pipeline")))
61
+ host = os.environ.get("PGHOST", "localhost")
62
+ port = os.environ.get("PGPORT", "5432")
63
+ user = os.environ.get("PGUSER", "postgres")
64
+ password = os.environ.get("PGPASSWORD", "dbos")
65
+ return f"postgresql://{user}:{password}@{host}:{port}/{name}_dbos_sys"