Skip to content

Commit 1125980

Browse files
authored
Merge pull request #84 from speakeasy-api/feat/pi-openrouter-runtime
Run the drafting pipeline on pi and OpenRouter
2 parents 1e5db93 + d4ecac8 commit 1125980

36 files changed

Lines changed: 4395 additions & 836 deletions

.claude/skills/tune-pipeline/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ adding; no-change is a valid outcome.
1717
`doctrine/CHANGELOG.md` (what already changed, what was rejected —
1818
do not re-propose either without new evidence), then the current
1919
doctrine: every file in `doctrine/`, `doctrine/personas/`, and the
20-
Cursor SDK workflow under `pipeline/`.
20+
draft-guide workflow under `pipeline/`.
2121
2. **Read the signal**: everything in `retro/runs/` and `retro/notes/`
2222
not already cited by a changelog entry. If the corpus is large (more
2323
than ~10 run records), fan out reader subagents — one per review

.github/workflows/guide-draft.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
if: steps.preflight.outputs.refused != 'true'
8686
working-directory: pipeline
8787
env:
88-
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
88+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
8989
run: npm run factory -- distill
9090

9191
- name: Comment resolved intent
@@ -116,7 +116,7 @@ jobs:
116116
if: steps.preflight.outputs.refused != 'true' && success()
117117
working-directory: pipeline
118118
env:
119-
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
119+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
120120
PULSE_REGISTRY_KEY: ${{ secrets.PULSE_REGISTRY_KEY }}
121121
PULSE_REGISTRY_TENANT: ${{ secrets.PULSE_REGISTRY_TENANT }}
122122
SLUG: ${{ steps.distill.outputs.slug }}

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,3 @@ tools/pulse-catalog/pulse-catalog.json
77

88
# Local scratch / probe dirs
99
.tmp-*/
10-
.tmp-gram/
11-
.tmp-probe/
12-
.tmp-probe2/

FACTORY.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Guide draft factory
22

3-
Turn a GitHub issue into a draft MCP Setup Guide PR. Same Cursor SDK pipeline
3+
Turn a GitHub issue into a draft MCP Setup Guide PR. Same drafting pipeline
44
as `mise run draft-guide` / `npm run draft-guide`, driven by a label instead of
55
a local CLI.
66

@@ -15,7 +15,7 @@ Repo → **Settings → Secrets and variables → Actions**:
1515

1616
| Secret | Required? | What it is |
1717
| --- | --- | --- |
18-
| `CURSOR_API_KEY` | **Yes** | Cursor API key (Dashboard → Integrations / API Keys) |
18+
| `OPENROUTER_API_KEY` | **Yes** | OpenRouter API key (openrouter.ai → Keys) |
1919
| `AGENT_PAT` | Recommended | PAT with contents + issues + pull requests write on this repo. Falls back to `GITHUB_TOKEN` (PRs still work; label chaining is less reliable). |
2020
| `PULSE_REGISTRY_KEY` | Recommended | PulseMCP Sub-Registry API key — resolves Speakeasy MCP Catalog presence before research. Without it, `speakeasy_add_server: auto` guides keep both catalog/custom paths unless remotes are tenanted or the guide forces `custom-remote` / `catalog`. |
2121
| `PULSE_REGISTRY_TENANT` | Recommended with key | PulseMCP tenant slug (e.g. `gram-recommended`). Required together with the key for catalog lookup. |
@@ -40,7 +40,7 @@ The workflow creates these if missing. You can also create them by hand:
4040
2. Add the label **`guide:draft`**.
4141
3. Watch the issue comments and the **Actions** tab (`Guide draft` workflow).
4242

43-
Runs can take a long time (often 20–40+ minutes). Usage burns Cursor plan tokens.
43+
Runs can take a long time (often 20–40+ minutes). Usage burns OpenRouter credits.
4444

4545
### What you get
4646

@@ -103,7 +103,7 @@ A non-factory open PR that already `Closes #<issue>` (collaborator-authored) blo
103103
## Local equivalent
104104

105105
```bash
106-
export CURSOR_API_KEY=cursor_...
106+
export OPENROUTER_API_KEY=sk-or-...
107107
mise run draft-guide -- asana --overwrite --notes "drop secret-reset recovery branch"
108108
# Match factory: pause before draft when material OQs lack Decision N replies
109109
mise run draft-guide -- x --overwrite --pause-on-scope
@@ -169,7 +169,7 @@ be unavailable).
169169
branch. Refuse only for non-factory collaborator PRs that target the
170170
same issue.
171171
2. **Labels** — remove `guide:draft` + `guide:blocked`, add `guide:in-progress`.
172-
3. **Distill** — light Cursor agent reads title + body + issue comments (+
172+
3. **Distill** — light distill agent reads title + body + issue comments (+
173173
existing `guides/*` slugs) → structured JSON or `needs_clarification`.
174174
4. **Comment** — “Resolved as `slug` …” (or resume notice) summary.
175175
5. **Draft** — `npm run draft-guide -- <slug> --overwrite --pause-on-scope
@@ -210,6 +210,31 @@ Hard failures (exit `1`, missing artifacts) take the blocked path with no PR.
210210
- Distill `needs_clarification` and the post-research scope gate are the
211211
intentional stops before / mid heavy pipeline.
212212

213+
## Runtime facts that are easy to get wrong
214+
215+
The pipeline spawns the `pi` CLI against OpenRouter. Seven things are not
216+
obvious from reading the code, and each one has cost a run:
217+
218+
- **`pi` exits 0 on API errors.** Success is decided by `classifyPiRun`
219+
(`pi-stream.ts`), never by the exit code. Checking the code reports a guide
220+
that never generated as one that generated empty.
221+
- **Never edit the repo while a run is in flight.** The I7 tripwire captures its
222+
baseline *once*, before any agent runs, so a file you touch mid-run is
223+
indistinguishable from an agent breach and fails the phase.
224+
- **There is no container.** The env allowlist in `pi-guard.ts` and the
225+
`git status` tripwire in `runtime-pi.ts` are the entire boundary keeping
226+
secrets out of the agent and the agent inside `guides/<slug>/`.
227+
- **The research phase keeps `bash` deliberately.** `pi` ships no web-fetch
228+
tool, so `bash` + `curl` is research's only route to provider docs. Removing
229+
it does not tighten research, it disables it.
230+
- **Session continuity is one flag.** The same `--session <path>` creates the
231+
session on turn 1 and resumes it on turn 2, which is what lets remediation say
232+
"use the research you already gathered". `--no-session` breaks that.
233+
- **`model` feeds `input_digest`.** Changing the model slug goes cold on every
234+
committed lock, so the next run of each guide re-runs every phase.
235+
- **Secrets come from `mise`** via a gitignored `mise.local.toml`. If
236+
`OPENROUTER_API_KEY` reads empty the shell snapshot is stale — use `mise exec --`.
237+
213238
## Related
214239

215240
- [`README.md`](README.md) — short how-to (issue flow + local CLI)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ How it works (labels, scope checks, Decisions, resume): **[`FACTORY.md`](FACTORY
2424

2525
## Run locally
2626

27-
Requires Node ≥ 22.13 and a Cursor API key.
27+
Requires Node ≥ 22.13 and an OpenRouter API key.
2828

2929
```bash
30-
export CURSOR_API_KEY=cursor_... # Dashboard → Integrations / API Keys
30+
export OPENROUTER_API_KEY=sk-or-... # openrouter.ai → Keys
3131
# Optional — resolve Speakeasy catalog presence (same as mise run pull-catalog):
3232
# export PULSE_REGISTRY_KEY=...
3333
# export PULSE_REGISTRY_TENANT=gram-recommended

doctrine/CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,43 @@ evidence (Run Records / Retro Notes) behind it. Required by constitution
66
invariant I8; written by `/tune-pipeline` when a human approves a
77
proposal, or by hand for direct human edits.
88

9+
## 2026-07-31 — pipeline-lock examples follow the pi runtime
10+
11+
Files: `doctrine/pipeline-lock.md`.
12+
13+
- **Runtime naming (`:8`, `:114`, `:203`):** the contract described itself
14+
as honored by "the Cursor SDK workflow" and used `cursor-sdk` as the
15+
`runtime` example and in the illustrative lock. `7e635e1` replaced the
16+
`@cursor/sdk` runtime with a direct spawn of the `pi` CLI over
17+
OpenRouter, deleted `pipeline/src/runtime.ts`, and set the lock literal
18+
to `runtime: 'pi'` (`pipeline/src/workflow.ts`). Prose is now
19+
runtime-neutral ("the drafting pipeline"); the two examples say `pi`.
20+
- **Model slot alias (`:109`, `:111-112`):** the `review.<dim>` row read
21+
"default vs light/`sonnet` slot" and the example forbade "a slot alias
22+
like `sonnet`". No slot mechanism survives: `7e635e1` removed
23+
`Dimension.model`, `AgentOptions.model` and the `dim.model` spread along
24+
with `resolveModel`, which held the `'sonnet'` → light-model branch. The
25+
**rule is kept** — a resolved id, never an alias — because it still binds
26+
any future runtime that reintroduces slots; only its illustration moved.
27+
- **Model examples (`:209`, `:250`, `:297`):** `claude-fable-5` → the
28+
OpenRouter slug `openrouter/openai/gpt-5.6-sol`, which is what
29+
`modelId()` now returns and therefore what `input_digest` covers.
30+
31+
No normative rule changed. Digest exclusions, the `runtime`-is-observational
32+
constraint, and the resolved-id requirement all stand as written; this entry
33+
only corrects examples and one runtime name that `7e635e1` falsified.
34+
35+
Evidence: commits `7e635e1` (runtime cutover) and `e4fa3d7` (removal of the
36+
code left unused by it), PR #84; run records
37+
`retro/runs/2026-07-31T15:07:07Z-google-calendar.json` (converged fresh
38+
draft on pi, `runtime: "pi"`) and `…T14:58:27Z-…` (the failed first run).
39+
40+
Known divergence, deliberate: the illustrative lock at `:203` is labelled
41+
`guides/box/pipeline.lock.json`, and the real file still records
42+
`"runtime": "cursor-sdk"` — correct, as a historical record of a run that
43+
did use it. The sample follows current doctrine, not that file, until box
44+
next runs.
45+
946
## 2026-07-30 — first-class "no setup required" fact for consumers
1047

1148
Files: `schema/guide.v1.schema.json`, `guides/*/meta.yaml` (all 18),

doctrine/pipeline-lock.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Normative semantics for `guides/<slug>/pipeline.lock.json`. The JSON Schema is
55

66
This contract records, per guide, the **input fingerprints** that produced the
77
current artifacts so a later drafting run can **skip** steps whose inputs did
8-
not change. The Cursor SDK workflow (`pipeline/`) honors these rules;
8+
not change. The drafting pipeline (`pipeline/`) honors these rules;
99
`--force` bypasses skips. `--overwrite` / `-y` only skips the guide-exists
1010
prompt and still honors the lock.
1111

@@ -106,12 +106,12 @@ in the workflow is an implementation detail.
106106
| --- | --- | --- | --- | --- |
107107
| `research` | resolved default model | `doctrine/glossary.md`, `doctrine/shared.md`, `doctrine/roles/technical-research.md`, `doctrine/speakeasy-setup.md` | `[]` (sources are external) | `provider`, `notes` |
108108
| `draft` | resolved default model | `doctrine/glossary.md`, `doctrine/shared.md`, `doctrine/roles/writer.md`, `doctrine/personas/<id>.md` | stable digests of `research.md`, `meta.yaml` | `provider`, `notes`, `persona` |
109-
| `review.<dim>` | resolved model for that dimension (default vs light/`sonnet` slot) | `doctrine/glossary.md`, `doctrine/shared.md`, role doc (`fidelity.md` or `review.md`), plus persona file when the dimension uses a persona | stable digests of `research.md`, `meta.yaml`, `external.md`, `speakeasy.md` | `provider`, `notes`, `persona`, `dimension` |
109+
| `review.<dim>` | resolved model for that dimension | `doctrine/glossary.md`, `doctrine/shared.md`, role doc (`fidelity.md` or `review.md`), plus persona file when the dimension uses a persona | stable digests of `research.md`, `meta.yaml`, `external.md`, `speakeasy.md` | `provider`, `notes`, `persona`, `dimension` |
110110

111-
`model` is always the **resolved** model id (e.g. `claude-fable-5`), never a
112-
slot alias like `sonnet`.
111+
`model` is always the **resolved** model id (e.g.
112+
`openrouter/openai/gpt-5.6-sol`), never a slot alias.
113113

114-
Top-level `runtime` (e.g. `cursor-sdk`) is observational and **must not**
114+
Top-level `runtime` (e.g. `pi`) is observational and **must not**
115115
appear inside `inputs` or affect `input_digest`.
116116

117117
## Research unchanged
@@ -200,13 +200,13 @@ Illustrative `guides/box/pipeline.lock.json` (digests are placeholders):
200200
"schema_version": 1,
201201
"slug": "box",
202202
"persona": "it-admin",
203-
"runtime": "cursor-sdk",
203+
"runtime": "pi",
204204
"updated_at": "2026-07-23T16:00:00Z",
205205
"steps": {
206206
"research": {
207207
"input_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
208208
"inputs": {
209-
"model": "claude-fable-5",
209+
"model": "openrouter/openai/gpt-5.6-sol",
210210
"prompt_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
211211
"reading_list": [
212212
{
@@ -247,7 +247,7 @@ Illustrative `guides/box/pipeline.lock.json` (digests are placeholders):
247247
"draft": {
248248
"input_digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
249249
"inputs": {
250-
"model": "claude-fable-5",
250+
"model": "openrouter/openai/gpt-5.6-sol",
251251
"prompt_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
252252
"reading_list": [
253253
{
@@ -294,7 +294,7 @@ Illustrative `guides/box/pipeline.lock.json` (digests are placeholders):
294294
"review.fidelity": {
295295
"input_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
296296
"inputs": {
297-
"model": "claude-fable-5",
297+
"model": "openrouter/openai/gpt-5.6-sol",
298298
"prompt_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
299299
"reading_list": [
300300
{

mise.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ hide = true
1515
run = "npm install"
1616

1717
[tasks.draft-guide]
18-
description = "Draft a Guide via the Cursor SDK pipeline (pipeline/)"
18+
description = "Draft a Guide via the drafting pipeline (pipeline/)"
1919
dir = "pipeline"
2020
depends = ["_pipeline-install"]
2121
# Usage (from repo root): mise run draft-guide -- box [--overwrite] [--force] [--pause-on-scope] [...]
2222
# Exit: 0 converged, 2 unconverged/blocked/failed, 3 awaiting_scope (--pause-on-scope)
23-
# Requires CURSOR_API_KEY in the environment (user or team service-account key).
23+
# Requires OPENROUTER_API_KEY in the environment.
2424
run = "npm run draft-guide --"
2525

2626
[tasks.lint-guide]

0 commit comments

Comments
 (0)