rote-cli 0.3.0__tar.gz → 0.5.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 (33) hide show
  1. {rote_cli-0.3.0 → rote_cli-0.5.0}/PKG-INFO +63 -23
  2. {rote_cli-0.3.0 → rote_cli-0.5.0}/README.md +62 -22
  3. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/__init__.py +1 -1
  4. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/adapters/__init__.py +23 -2
  5. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/adapters/_common.py +54 -0
  6. rote_cli-0.5.0/src/rote/adapters/_py_common.py +642 -0
  7. rote_cli-0.5.0/src/rote/adapters/_ts_common.py +338 -0
  8. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/adapters/cloudflare.py +21 -302
  9. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/adapters/dbos.py +73 -586
  10. rote_cli-0.5.0/src/rote/adapters/dbos_ts.py +906 -0
  11. rote_cli-0.5.0/src/rote/adapters/inngest.py +996 -0
  12. rote_cli-0.5.0/src/rote/adapters/python.py +671 -0
  13. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/adapters/temporal.py +6 -5
  14. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/cli.py +107 -19
  15. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/ir.py +99 -0
  16. rote_cli-0.5.0/src/rote/serve/backends.py +343 -0
  17. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/serve/registry.py +49 -2
  18. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/serve/server.py +65 -0
  19. rote_cli-0.3.0/src/rote/serve/backends.py +0 -187
  20. {rote_cli-0.3.0 → rote_cli-0.5.0}/.gitignore +0 -0
  21. {rote_cli-0.3.0 → rote_cli-0.5.0}/LICENSE +0 -0
  22. {rote_cli-0.3.0 → rote_cli-0.5.0}/pyproject.toml +0 -0
  23. {rote_cli-0.3.0 → rote_cli-0.5.0}/skills/rote-graduate/SKILL.md +0 -0
  24. {rote_cli-0.3.0 → rote_cli-0.5.0}/skills/rote-graduate/references/crystallization-heuristics.md +0 -0
  25. {rote_cli-0.3.0 → rote_cli-0.5.0}/skills/rote-graduate/references/ir-schema.md +0 -0
  26. {rote_cli-0.3.0 → rote_cli-0.5.0}/skills/rote-graduate/references/llm-judge-extraction.md +0 -0
  27. {rote_cli-0.3.0 → rote_cli-0.5.0}/skills/rote-graduate/references/node-kinds.md +0 -0
  28. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/graduator/__init__.py +0 -0
  29. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/graduator/drivers/__init__.py +0 -0
  30. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/graduator/drivers/anthropic_api.py +0 -0
  31. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/graduator/drivers/claude.py +0 -0
  32. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/graduator/drivers/codex.py +0 -0
  33. {rote_cli-0.3.0 → rote_cli-0.5.0}/src/rote/serve/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rote-cli
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -59,8 +59,13 @@ applies a structured graduation rubric, and emits:
59
59
  - and runtime code for your durable execution engine of choice.
60
60
 
61
61
  ```sh
62
- # Two runtimes shipped today emit Python for Temporal, or TypeScript
63
- # for Cloudflare Workflows:
62
+ pip install rote-cli # or zero-install: uvx --from rote-cli rote ...
63
+
64
+ # Default target is DBOS — durable execution as a plain Python library,
65
+ # no orchestrator to run, SQLite for dev / Postgres for prod:
66
+ rote graduate ./examples/bdr-outreach/skill --out ./graduated/
67
+
68
+ # Or target Temporal (Python) or Cloudflare Workflows (TypeScript):
64
69
  rote graduate ./examples/bdr-outreach/skill --runtime temporal --out ./graduated/
65
70
  rote graduate ./examples/bdr-outreach/skill --runtime cloudflare --out ./graduated/
66
71
  ```
@@ -140,6 +145,10 @@ The graduator independently:
140
145
  After the agent finishes, a runtime adapter consumes the IR and emits
141
146
  the target runtime's native code shape:
142
147
 
148
+ - The **DBOS** adapter (the default) emits a single `main.py` — one
149
+ `@DBOS.workflow` DAG plus a `@DBOS.step` per node, checkpointing to
150
+ SQLite or Postgres. `python main.py` *is* the runtime; there is no
151
+ orchestrator to deploy.
143
152
  - The **Temporal** adapter emits `workflow.py` (the orchestration
144
153
  class with `@workflow.defn` and signal handlers for the HITL gates)
145
154
  and `activities.py` (one `@activity.defn` per node, lazy-importing
@@ -150,6 +159,24 @@ the target runtime's native code shape:
150
159
  `signatures/*.ts` (Zod schemas + Anthropic SDK calls) and the
151
160
  supporting `wrangler.jsonc` / `package.json` / `tsconfig.json`. The
152
161
  output is `wrangler deploy`-ready.
162
+ - The **DBOS (TypeScript)** adapter (`--runtime dbos-ts`) emits a
163
+ Node.js app for DBOS Transact: `src/main.ts` registers one durable
164
+ workflow (`DBOS.registerWorkflow`) running the DAG waves and one
165
+ `DBOS.registerStep` per node, with `DBOS.recv(...)` parking each
166
+ HITL gate durably in Postgres until `DBOS.send(...)` resumes it.
167
+ Zero-orchestrator like the Python DBOS target — `node dist/main.js`
168
+ *is* the runtime — but note the TS SDK is Postgres-only (no SQLite
169
+ mode; `npx dbos postgres start` covers local dev).
170
+ - The **Inngest** adapter (`--runtime inngest`) emits a TypeScript
171
+ Inngest app for teams whose graduated skill should live inside the
172
+ Node/Next.js service they already deploy: `src/inngest/pipeline.ts`
173
+ is one durable `inngest.createFunction` running the DAG waves
174
+ (`step.run` per node, `Promise.all` fan-out, `step.waitForEvent`
175
+ parking each HITL gate until its namespaced resume event), plus a
176
+ framework-neutral `inngest/node` serve entrypoint. The emitted
177
+ README shows the drop-in Next.js mount (`inngest/next`). Note:
178
+ Inngest v4 retries are function-level only — the emitter sets the
179
+ max across per-node policies and documents the deltas as comments.
153
180
 
154
181
  None of the emitted code references an MCP runtime, in either
155
182
  language — the agent's crystallization step replaces tool calls with
@@ -297,11 +324,12 @@ The repository includes a real BDR outreach skill in
297
324
  `examples/bdr-outreach/skill/`. Graduate it:
298
325
 
299
326
  ```sh
300
- rote graduate examples/bdr-outreach/skill \
301
- --runtime temporal \
302
- --out /tmp/bdr-graduated
327
+ rote graduate examples/bdr-outreach/skill --out /tmp/bdr-graduated
303
328
  ```
304
329
 
330
+ This targets DBOS by default; pass `--runtime temporal` or
331
+ `--runtime cloudflare` for the other adapters.
332
+
305
333
  By default `rote` auto-detects an available agent driver in this
306
334
  order: `claude` (Claude Code CLI) → `codex` (Codex CLI) → `api`
307
335
  (Anthropic SDK with `ANTHROPIC_API_KEY`). Override with `--agent`:
@@ -320,10 +348,12 @@ The output directory is structured as:
320
348
  │ ├── signatures/*.py # typed LLM-judge signatures
321
349
  │ ├── evals/*.jsonl # seed eval examples
322
350
  │ └── graduation-report.md # human-readable summary
323
- └── runtime/temporal/ # produced by the adapter
324
- ├── workflow.py
325
- ├── activities.py
326
- └── __init__.py
351
+ └── runtime/dbos/ # produced by the adapter
352
+ ├── main.py # @DBOS.workflow + one @DBOS.step per node
353
+ ├── extracted/*.py # copied deterministic functions
354
+ ├── signatures/*.py # generated Pydantic + vendor-SDK judges
355
+ ├── dbos-config.yaml
356
+ └── README.md # how to run, signal HITL gates, deploy
327
357
  ```
328
358
 
329
359
  ### Render an IR without re-running the agent
@@ -332,6 +362,7 @@ If you already have a `pipeline.yaml` (hand-written or from a previous
332
362
  graduation), `rote emit` runs just the adapter step:
333
363
 
334
364
  ```sh
365
+ rote emit /path/to/pipeline.yaml --out /tmp/emitted/ # dbos (default)
335
366
  rote emit /path/to/pipeline.yaml --runtime temporal --out /tmp/emitted/
336
367
  ```
337
368
 
@@ -393,10 +424,12 @@ Opus's extra reasoning earns its cost.
393
424
  ## Status
394
425
 
395
426
  `rote` is **pre-1.0**. The end-to-end flow works on the BDR example
396
- and the test suite covers each layer (133 tests in the fast suite,
397
- plus 3 slow tests that compile the emitted Cloudflare TypeScript
398
- against the real `@cloudflare/workers-types` definitions and drive a
399
- real workflow instance through both HITL gates via `wrangler dev`).
427
+ and the test suite covers each layer (231 tests in the fast suite,
428
+ plus 5 slow tests that run the emitted code against real runtimes:
429
+ a DBOS runtime over SQLite, Temporal's time-skipping test server, the
430
+ emitted Cloudflare TypeScript compiled against real
431
+ `@cloudflare/workers-types` and driven through both HITL gates via
432
+ `wrangler dev`, and the MCP server over a real stdio transport).
400
433
  Run `pytest tests/` for the fast suite; `pytest tests/ -m slow` for
401
434
  the toolchain-dependent integration tests.
402
435
 
@@ -406,15 +439,18 @@ the toolchain-dependent integration tests.
406
439
  | Temporal adapter | working (validated with mocked-activities e2e test) |
407
440
  | Cloudflare Workflows adapter | working (validated with `tsc --noEmit` over the real emitted output) |
408
441
  | DBOS adapter | working (validated against a real DBOS runtime over SQLite in the e2e test) |
442
+ | DBOS TypeScript adapter (`dbos-ts`) | working (validated with `tsc --noEmit` over the real emitted output and a live run on the DBOS TS runtime against Docker Postgres) |
443
+ | Raw Python adapter (`python`) | working (emitted script executed as a subprocess in the e2e test: wave order, payload threading, and a live retry loop; refuses `hitl_gate` pipelines at emit time) |
409
444
  | Graduator orchestrator | working |
410
445
  | `rote graduate` / `rote emit` CLI commands | working |
411
446
  | `claude` driver | working |
412
447
  | `api` (Anthropic SDK) driver | working |
413
448
  | `codex` driver | stub (`is_available` works; `run` not implemented) |
414
- | Inngest / Restate adapters | planned |
449
+ | Inngest adapter (`inngest`) | working (validated with `tsc --noEmit` over the real emitted output and a live run through both HITL gates against the Inngest dev server) |
450
+ | Restate adapter | planned |
415
451
  | Real implementations of the extracted modules | the agent produces stubs that raise `NotImplementedError`; humans fill them in with real API client code |
416
- | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all three adapters (Temporal, Cloudflare, DBOS) thread real payloads through the DAG, validated in the runtime e2e tests |
417
- | Distribution via PyPI | not yet published install from source. The release pipeline (tag-driven, Trusted Publishing) is in place; see [docs/releasing.md](docs/releasing.md) |
452
+ | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all five adapters (Temporal, Cloudflare, DBOS, DBOS-TS, Inngest) thread real payloads through the DAG, validated in the runtime e2e tests |
453
+ | Distribution via PyPI | published as [`rote-cli`](https://pypi.org/project/rote-cli/) (`pip install rote-cli`); tag-driven Trusted Publishing releases see [docs/releasing.md](docs/releasing.md) |
418
454
 
419
455
  The project explicitly **does not** depend on `claude-agent-sdk`.
420
456
  Anthropic's terms of service forbid third-party agents built on the
@@ -523,11 +559,15 @@ In rough priority order:
523
559
  graduator the new field, but no real graduator run has produced
524
560
  one yet. Re-running `rote graduate examples/bdr-outreach/skill`
525
561
  should produce the structured form natively.
526
- 3. **A third runtime adapter.** Probably Inngest, since its
527
- programming model is meaningfully different from both Temporal and
528
- Cloudflare. Each new adapter is also a stress test on whether the
529
- IR is genuinely runtime-agnostic vs. accidentally shaped like one
530
- of the existing targets.
562
+ 3. **A third runtime adapter.** *(Done — the Inngest adapter shipped,
563
+ alongside DBOS Python/TS.)* Its programming model is meaningfully
564
+ different from both Temporal and Cloudflare retries are
565
+ function-level, the platform drives execution from outside the
566
+ process — and the IR survived without changes (the per-node retry
567
+ gap is documented at emission, not papered over in the IR). Each
568
+ new adapter remains a stress test on whether the IR is genuinely
569
+ runtime-agnostic vs. accidentally shaped like one of the existing
570
+ targets.
531
571
  4. **Pre-filter as `pure_function` node.** Today the rubric lifts
532
572
  hard thresholds into a Python `forward()` method, which works for
533
573
  Temporal but not for Cloudflare. Modeling the pre-filter as a
@@ -561,7 +601,7 @@ The most useful contributions right now are:
561
601
  needs to be tested against more.
562
602
  - **Add a runtime adapter.** The Temporal adapter in
563
603
  `src/rote/adapters/temporal.py` is ~450 lines and follows a clear
564
- pattern. Inngest, Restate, and Hatchet are all good targets.
604
+ pattern. Restate and Hatchet are both good targets.
565
605
  - **Add a graduator driver.** The Protocol in
566
606
  `src/rote/graduator/drivers/__init__.py` is simple. Aider, Gemini
567
607
  CLI, and Cursor Agent are reasonable additions.
@@ -13,8 +13,13 @@ applies a structured graduation rubric, and emits:
13
13
  - and runtime code for your durable execution engine of choice.
14
14
 
15
15
  ```sh
16
- # Two runtimes shipped today emit Python for Temporal, or TypeScript
17
- # for Cloudflare Workflows:
16
+ pip install rote-cli # or zero-install: uvx --from rote-cli rote ...
17
+
18
+ # Default target is DBOS — durable execution as a plain Python library,
19
+ # no orchestrator to run, SQLite for dev / Postgres for prod:
20
+ rote graduate ./examples/bdr-outreach/skill --out ./graduated/
21
+
22
+ # Or target Temporal (Python) or Cloudflare Workflows (TypeScript):
18
23
  rote graduate ./examples/bdr-outreach/skill --runtime temporal --out ./graduated/
19
24
  rote graduate ./examples/bdr-outreach/skill --runtime cloudflare --out ./graduated/
20
25
  ```
@@ -94,6 +99,10 @@ The graduator independently:
94
99
  After the agent finishes, a runtime adapter consumes the IR and emits
95
100
  the target runtime's native code shape:
96
101
 
102
+ - The **DBOS** adapter (the default) emits a single `main.py` — one
103
+ `@DBOS.workflow` DAG plus a `@DBOS.step` per node, checkpointing to
104
+ SQLite or Postgres. `python main.py` *is* the runtime; there is no
105
+ orchestrator to deploy.
97
106
  - The **Temporal** adapter emits `workflow.py` (the orchestration
98
107
  class with `@workflow.defn` and signal handlers for the HITL gates)
99
108
  and `activities.py` (one `@activity.defn` per node, lazy-importing
@@ -104,6 +113,24 @@ the target runtime's native code shape:
104
113
  `signatures/*.ts` (Zod schemas + Anthropic SDK calls) and the
105
114
  supporting `wrangler.jsonc` / `package.json` / `tsconfig.json`. The
106
115
  output is `wrangler deploy`-ready.
116
+ - The **DBOS (TypeScript)** adapter (`--runtime dbos-ts`) emits a
117
+ Node.js app for DBOS Transact: `src/main.ts` registers one durable
118
+ workflow (`DBOS.registerWorkflow`) running the DAG waves and one
119
+ `DBOS.registerStep` per node, with `DBOS.recv(...)` parking each
120
+ HITL gate durably in Postgres until `DBOS.send(...)` resumes it.
121
+ Zero-orchestrator like the Python DBOS target — `node dist/main.js`
122
+ *is* the runtime — but note the TS SDK is Postgres-only (no SQLite
123
+ mode; `npx dbos postgres start` covers local dev).
124
+ - The **Inngest** adapter (`--runtime inngest`) emits a TypeScript
125
+ Inngest app for teams whose graduated skill should live inside the
126
+ Node/Next.js service they already deploy: `src/inngest/pipeline.ts`
127
+ is one durable `inngest.createFunction` running the DAG waves
128
+ (`step.run` per node, `Promise.all` fan-out, `step.waitForEvent`
129
+ parking each HITL gate until its namespaced resume event), plus a
130
+ framework-neutral `inngest/node` serve entrypoint. The emitted
131
+ README shows the drop-in Next.js mount (`inngest/next`). Note:
132
+ Inngest v4 retries are function-level only — the emitter sets the
133
+ max across per-node policies and documents the deltas as comments.
107
134
 
108
135
  None of the emitted code references an MCP runtime, in either
109
136
  language — the agent's crystallization step replaces tool calls with
@@ -251,11 +278,12 @@ The repository includes a real BDR outreach skill in
251
278
  `examples/bdr-outreach/skill/`. Graduate it:
252
279
 
253
280
  ```sh
254
- rote graduate examples/bdr-outreach/skill \
255
- --runtime temporal \
256
- --out /tmp/bdr-graduated
281
+ rote graduate examples/bdr-outreach/skill --out /tmp/bdr-graduated
257
282
  ```
258
283
 
284
+ This targets DBOS by default; pass `--runtime temporal` or
285
+ `--runtime cloudflare` for the other adapters.
286
+
259
287
  By default `rote` auto-detects an available agent driver in this
260
288
  order: `claude` (Claude Code CLI) → `codex` (Codex CLI) → `api`
261
289
  (Anthropic SDK with `ANTHROPIC_API_KEY`). Override with `--agent`:
@@ -274,10 +302,12 @@ The output directory is structured as:
274
302
  │ ├── signatures/*.py # typed LLM-judge signatures
275
303
  │ ├── evals/*.jsonl # seed eval examples
276
304
  │ └── graduation-report.md # human-readable summary
277
- └── runtime/temporal/ # produced by the adapter
278
- ├── workflow.py
279
- ├── activities.py
280
- └── __init__.py
305
+ └── runtime/dbos/ # produced by the adapter
306
+ ├── main.py # @DBOS.workflow + one @DBOS.step per node
307
+ ├── extracted/*.py # copied deterministic functions
308
+ ├── signatures/*.py # generated Pydantic + vendor-SDK judges
309
+ ├── dbos-config.yaml
310
+ └── README.md # how to run, signal HITL gates, deploy
281
311
  ```
282
312
 
283
313
  ### Render an IR without re-running the agent
@@ -286,6 +316,7 @@ If you already have a `pipeline.yaml` (hand-written or from a previous
286
316
  graduation), `rote emit` runs just the adapter step:
287
317
 
288
318
  ```sh
319
+ rote emit /path/to/pipeline.yaml --out /tmp/emitted/ # dbos (default)
289
320
  rote emit /path/to/pipeline.yaml --runtime temporal --out /tmp/emitted/
290
321
  ```
291
322
 
@@ -347,10 +378,12 @@ Opus's extra reasoning earns its cost.
347
378
  ## Status
348
379
 
349
380
  `rote` is **pre-1.0**. The end-to-end flow works on the BDR example
350
- and the test suite covers each layer (133 tests in the fast suite,
351
- plus 3 slow tests that compile the emitted Cloudflare TypeScript
352
- against the real `@cloudflare/workers-types` definitions and drive a
353
- real workflow instance through both HITL gates via `wrangler dev`).
381
+ and the test suite covers each layer (231 tests in the fast suite,
382
+ plus 5 slow tests that run the emitted code against real runtimes:
383
+ a DBOS runtime over SQLite, Temporal's time-skipping test server, the
384
+ emitted Cloudflare TypeScript compiled against real
385
+ `@cloudflare/workers-types` and driven through both HITL gates via
386
+ `wrangler dev`, and the MCP server over a real stdio transport).
354
387
  Run `pytest tests/` for the fast suite; `pytest tests/ -m slow` for
355
388
  the toolchain-dependent integration tests.
356
389
 
@@ -360,15 +393,18 @@ the toolchain-dependent integration tests.
360
393
  | Temporal adapter | working (validated with mocked-activities e2e test) |
361
394
  | Cloudflare Workflows adapter | working (validated with `tsc --noEmit` over the real emitted output) |
362
395
  | DBOS adapter | working (validated against a real DBOS runtime over SQLite in the e2e test) |
396
+ | DBOS TypeScript adapter (`dbos-ts`) | working (validated with `tsc --noEmit` over the real emitted output and a live run on the DBOS TS runtime against Docker Postgres) |
397
+ | Raw Python adapter (`python`) | working (emitted script executed as a subprocess in the e2e test: wave order, payload threading, and a live retry loop; refuses `hitl_gate` pipelines at emit time) |
363
398
  | Graduator orchestrator | working |
364
399
  | `rote graduate` / `rote emit` CLI commands | working |
365
400
  | `claude` driver | working |
366
401
  | `api` (Anthropic SDK) driver | working |
367
402
  | `codex` driver | stub (`is_available` works; `run` not implemented) |
368
- | Inngest / Restate adapters | planned |
403
+ | Inngest adapter (`inngest`) | working (validated with `tsc --noEmit` over the real emitted output and a live run through both HITL gates against the Inngest dev server) |
404
+ | Restate adapter | planned |
369
405
  | Real implementations of the extracted modules | the agent produces stubs that raise `NotImplementedError`; humans fill them in with real API client code |
370
- | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all three adapters (Temporal, Cloudflare, DBOS) thread real payloads through the DAG, validated in the runtime e2e tests |
371
- | Distribution via PyPI | not yet published install from source. The release pipeline (tag-driven, Trusted Publishing) is in place; see [docs/releasing.md](docs/releasing.md) |
406
+ | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all five adapters (Temporal, Cloudflare, DBOS, DBOS-TS, Inngest) thread real payloads through the DAG, validated in the runtime e2e tests |
407
+ | Distribution via PyPI | published as [`rote-cli`](https://pypi.org/project/rote-cli/) (`pip install rote-cli`); tag-driven Trusted Publishing releases see [docs/releasing.md](docs/releasing.md) |
372
408
 
373
409
  The project explicitly **does not** depend on `claude-agent-sdk`.
374
410
  Anthropic's terms of service forbid third-party agents built on the
@@ -477,11 +513,15 @@ In rough priority order:
477
513
  graduator the new field, but no real graduator run has produced
478
514
  one yet. Re-running `rote graduate examples/bdr-outreach/skill`
479
515
  should produce the structured form natively.
480
- 3. **A third runtime adapter.** Probably Inngest, since its
481
- programming model is meaningfully different from both Temporal and
482
- Cloudflare. Each new adapter is also a stress test on whether the
483
- IR is genuinely runtime-agnostic vs. accidentally shaped like one
484
- of the existing targets.
516
+ 3. **A third runtime adapter.** *(Done — the Inngest adapter shipped,
517
+ alongside DBOS Python/TS.)* Its programming model is meaningfully
518
+ different from both Temporal and Cloudflare retries are
519
+ function-level, the platform drives execution from outside the
520
+ process — and the IR survived without changes (the per-node retry
521
+ gap is documented at emission, not papered over in the IR). Each
522
+ new adapter remains a stress test on whether the IR is genuinely
523
+ runtime-agnostic vs. accidentally shaped like one of the existing
524
+ targets.
485
525
  4. **Pre-filter as `pure_function` node.** Today the rubric lifts
486
526
  hard thresholds into a Python `forward()` method, which works for
487
527
  Temporal but not for Cloudflare. Modeling the pre-filter as a
@@ -515,7 +555,7 @@ The most useful contributions right now are:
515
555
  needs to be tested against more.
516
556
  - **Add a runtime adapter.** The Temporal adapter in
517
557
  `src/rote/adapters/temporal.py` is ~450 lines and follows a clear
518
- pattern. Inngest, Restate, and Hatchet are all good targets.
558
+ pattern. Restate and Hatchet are both good targets.
519
559
  - **Add a graduator driver.** The Protocol in
520
560
  `src/rote/graduator/drivers/__init__.py` is simple. Aider, Gemini
521
561
  CLI, and Cursor Agent are reasonable additions.
@@ -1,3 +1,3 @@
1
1
  """rote — graduate fuzzy AI skills into deterministic workflows."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
@@ -6,8 +6,8 @@ adapters are template substitution.
6
6
 
7
7
  The "two adapters minimum" rule applies: until at least two adapters
8
8
  work end-to-end, assume the IR shape is secretly leaking the first
9
- runtime's mental model. Inngest is the planned second target after
10
- Temporal.
9
+ runtime's mental model. (Satisfied five adapters share the IR today:
10
+ Temporal, Cloudflare, DBOS Python, DBOS TypeScript, and Inngest.)
11
11
 
12
12
  Adapters are registered in :data:`ADAPTERS` and dispatched by name from
13
13
  the CLI (``rote emit --runtime <name>``).
@@ -48,12 +48,33 @@ def _dbos_adapter_factory() -> Adapter:
48
48
  return DbosAdapter()
49
49
 
50
50
 
51
+ def _dbos_ts_adapter_factory() -> Adapter:
52
+ from rote.adapters.dbos_ts import DbosTsAdapter
53
+
54
+ return DbosTsAdapter()
55
+
56
+
57
+ def _inngest_adapter_factory() -> Adapter:
58
+ from rote.adapters.inngest import InngestAdapter
59
+
60
+ return InngestAdapter()
61
+
62
+
63
+ def _python_adapter_factory() -> Adapter:
64
+ from rote.adapters.python import PythonAdapter
65
+
66
+ return PythonAdapter()
67
+
68
+
51
69
  #: Name → factory. Keep the values as zero-arg callables so adapters can
52
70
  #: lazy-import their heavy dependencies.
53
71
  ADAPTERS: dict[str, Callable[[], Adapter]] = {
54
72
  "temporal": _temporal_adapter_factory,
55
73
  "cloudflare": _cloudflare_adapter_factory,
56
74
  "dbos": _dbos_adapter_factory,
75
+ "dbos-ts": _dbos_ts_adapter_factory,
76
+ "inngest": _inngest_adapter_factory,
77
+ "python": _python_adapter_factory,
57
78
  }
58
79
 
59
80
 
@@ -13,9 +13,63 @@ from __future__ import annotations
13
13
 
14
14
  import hashlib
15
15
  import re
16
+ from pathlib import Path
16
17
 
17
18
  from rote.ir import Node, Pipeline, parse_input_ref
18
19
 
20
+ # ───────── Prose → safe code comment/docstring ─────────
21
+
22
+
23
+ def safe_docstring_line(text: str, fallback: str = "") -> str:
24
+ """First line of ``text``, escaped so it cannot break out of a docstring.
25
+
26
+ Node ``description`` is prose (it legitimately contains quotes and
27
+ backslashes), yet adapters splice its first line straight into a
28
+ ``\"\"\"…\"\"\"`` Python docstring / block comment. An unescaped
29
+ ``\"\"\"`` — or a trailing backslash that escapes the closing quote —
30
+ lets a crafted pipeline.yaml close the docstring early and inject
31
+ code. Escaping backslashes and double-quotes neutralizes both without
32
+ charset-restricting prose at the IR layer.
33
+ """
34
+ stripped = text.strip() if text else ""
35
+ first = stripped.splitlines()[0] if stripped else fallback
36
+ return first.replace("\\", "\\\\").replace('"', '\\"')
37
+
38
+
39
+ def safe_block_comment_line(text: str, fallback: str = "") -> str:
40
+ """First line of ``text``, safe to splice into a ``/* … */`` block comment.
41
+
42
+ The TypeScript adapter emits ``description`` first-lines into JSDoc
43
+ comments. An unescaped ``*/`` would close the comment early and let a
44
+ crafted pipeline.yaml inject code after it; neutralize the sequence by
45
+ inserting a space (``*/`` → ``* /``). Newlines are already dropped by
46
+ taking the first line.
47
+ """
48
+ stripped = text.strip() if text else ""
49
+ first = stripped.splitlines()[0] if stripped else fallback
50
+ return first.replace("*/", "* /")
51
+
52
+
53
+ # ───────── Output-path containment ─────────
54
+
55
+
56
+ def resolve_within(base: Path, *parts: str) -> Path:
57
+ """Join ``parts`` onto ``base`` and assert the result stays inside it.
58
+
59
+ Defense-in-depth for emitted-file paths. IR validation already pins
60
+ ``node.id`` to an identifier, but this guard means any future field
61
+ that reaches a filename cannot escape the output directory via an
62
+ absolute segment or ``..`` traversal — it fails loudly at emit time.
63
+ """
64
+ base_resolved = base.resolve()
65
+ target = base_resolved.joinpath(*parts).resolve()
66
+ if target != base_resolved and base_resolved not in target.parents:
67
+ raise ValueError(
68
+ f"Refusing to write outside output directory: {target} is not within {base_resolved}"
69
+ )
70
+ return target
71
+
72
+
19
73
  # ───────── Case conversion ─────────
20
74
 
21
75