skilly. Buy ad slot
All skills
Backend · Python · Typescript / AGENT SKILL

pipelex-integrate

Pipelex/pipelex-plugins
0 installs 0 GitHub stars
0

Integrate MTHDS methods into Python or TypeScript applications with generated types and typed SDK calls.
Wire an MTHDS method into a Python or TypeScript codebase with generated, drift-proof types and one typed call site that runs it through @pipelex/sdk or pipelex-sdk. Use when the user says "use this method in my app", "call this from my code", "generate types for this method", "wire the method into my project", "add this pipeline to my service", "typed client for this method", "integrate the method", "refresh the generated types", "regenerate the types", "the types are stale", or wants application code that runs a .mthds method — from a local bundle, a catalog id (mt_…) or a published method_ref address. Also the refresh path after a bundle edit. Not for authoring or editing the method itself (/pipelex-design, /pipelex-edit), and not for a project that does not exist yet (/pipelex-scaffold).

BEFORE YOU INSTALL

Understand the trade-offs.

SECURITY REVIEW

Not yet assessed

Review the original instructions and requested permissions before installing.

No security review is available for this catalog entry yet.

SKILL QUALITY

Not yet assessed

How clearly the skill guides your agent, how complete its workflow is, and how you can check the outcome.

No quality assessment is available for this catalog entry yet.

The full skill.

Original instructions from the publisher’s SKILL.md

# Integrate an MTHDS method into a codebase

Take a method — a local `.mthds` bundle, a published address (`method_ref`), or a catalog id (`method_id`) — and a Python or TypeScript project, and leave the project able to call the method with types that cannot silently drift from it. Concretely:

1. pick the codegen target that matches the project's language **and audience**;
2. have the Pipelex workshop write the generated tree into a dedicated directory per method, through `mthds_codegen`'s write arm, so no generated byte ever passes through you;
3. make the project's formatters and linters leave that tree and the drift-check script this skill copies in alone, while its type checker keeps covering the tree;
4. record how the tree was generated in a small sidecar beside the lock, so the next run knows what to refresh and a bundle edit is detectable;
5. install the offline drift check and wire it into the gate the project already runs;
6. write one typed call-site module per method, running it through `@pipelex/sdk` or `pipelex-sdk` and narrowing its output with the generated binder or model;
7. verify with the project's own type checker and the gate you just installed.

Re-running on a project that already carries a generated tree is **[refresh mode](#refresh-mode)**, the common case. A project that already **owns a codegen harness** — one made from a Pipelex template — keeps it: see [that section](#a-project-that-owns-a-codegen-harness) before generating anything.

**What this skill is not.** Not a build tool (no watch mode, no per-project harness — the workshop is the harness), not a runner (`/pipelex-inputs` prepares inputs and offers a run), not a design skill (a method that does not validate or cannot run goes back to `/pipelex-design`). It writes no tests, routes, UI or CLI commands and edits no existing business code: it stops at one callable module per method plus one shared helper, and a user who wants more says so.

## Requirements — the Pipelex MCP tools

This skill generates through **`mthds_codegen`**, proves the method through **`mthds_validate`**, and reads a pipe's inputs through **`mthds_inputs_template`** on its fallback path — all served by the plugin's `pipelex` MCP server. They are required: never hand-write a generated file, and never derive a signature from the `.mthds` source when the verdict carries it.

- **If a tool is absent from this session** (the MCP server isn't connected), STOP and tell the user in one line: *"The Pipelex MCP server isn't connected — on Mistral Vibe the local workshop (`npx -y @pipelex/mcp@latest`) is not auto-spawned: append the `[[mcp_servers]]` entry from `mcp/vibe-mcp.toml` in the `pipelex-vibe` bundle (beside its `skills/` directory) to the end of `~/.vibe/config.toml`, after deleting any `mcp_servers = []` line and any hand-registered `pipelex` entry there, write your `PIPELEX_API_KEY` into its `env` table, then retry."*
- **If a call returns `status: "error"` with an error of class `config`**, STOP the same way and surface the error's `hint` verbatim. Two `config` errors deserve a precise reading: a **403** on `mthds_codegen` is a feature gate, not a key problem — its hint says code generation is not enabled for the organization on the hosted API; never answer it with "check your key" — and `kind: "paywall"` is the plan limit, whose hint points at billing.
- The server authenticates to the API with **`PIPELEX_API_KEY`** from its `env` table in `~/.vibe/config.toml`, never from the session environment: Mistral Vibe passes no shell variables to a stdio MCP server, so an exported key reaches the plugin's validation hook but not the server.
- **`mthds_list_methods`** is optional: it resolves a catalog method the user names without its `mt_…` id. When it is absent, integrate by id, address or files; never stop for it.

## Mode

Automatic by default: state the target, the destination and the generator in one line before writing anything, decide the routine calls yourself, and pause only for a genuinely ambiguous decision (which app in a monorepo; which of two Python audiences; a `method_id` source). Explicit user signals win — "just do it" is automatic, "walk me through" is interactive, and in interactive mode the dependency additions and the tooling edits are confirmed before they happen. Every MCP call branches on the structured verdict, never on transport.

## The rules that never bend

- **The write arm, always.** Every `mthds_codegen` call passes `output_dir`. A refused or failed write is handled as a refusal — never by calling again without `output_dir` and writing the returned bytes yourself. A generated file re-emitted through the conversation is one trailing newline away from a broken stamp, and the whole point of the trust chain is that the tree on disk is byte-identical to what the engine emitted.
- **Generated files are never opened for editing, never formatted, never linted.** Each artifact carries a stamp with its own content hash and the lock hashes every artifact; a reformat, a trimmed newline or a re-serialized lock turns the offline check red. This is why the tooling exclusions are made **before** the tree exists.
- **One directory per method.** After writing, the workshop reports any stamped file the new lock does not list as an orphan and never deletes it; two methods in one directory therefore read as permanently non-current, by design. You never delete an orphan either, and you never offer "clean up the orphans" — the moment two methods share a directory, that advice deletes real files.
- **Never generate from one source and run from another.** Types from a local bundle over a call site that runs by `method_id` is the shape that drifts silently. The sidecar records the selector; the call site uses the same one.
- **A project that owns a codegen harness keeps it.** Never write a second generated layout beside the one the project already has.
- **No `dropWireNulls` / `wireOutput` helper.** The ts-zod emitter projects optional fields as `.nullish()`, so a generated schema parses the runtime's explicit `null`s directly; a null-stripping helper is lossy (it removes legitimate nulls inside opaque fields) and must not be written into a project.

## Process

### Step 1: Identify the method and the project

**The method** comes from the conversation, in one of three selector forms — pass exactly one to every tool call, never two:

- **Local files**, the recommended shape: every `.mthds` file of the bundle, as `files` items. The bundle is a **directory**, and every `.mthds` file beneath it, at any depth, belongs to it: that is the set step 9's call site loads, so it is the set you pass, record and hash — never a hand-picked subset. Prefer `{path: <absolute path>}` per file (the workshop resolves a path against **its own** working directory, wherever the harness launched it, so pass absolute paths); `{content, uri}` is the inline fallback. A bundle that lives **outside the project** (a bundle directory elsewhere on disk) is copied into the project under `methods/<method>/` first, because the call site loads it at runtime and the sources must be versioned with the code; tell the user. A bundle already inside the project stays where it is, with one exception the Python reference names: a **packaged** project (a `[build-system]` table and an import package) needs the bundle *inside* that package for the wheel to ship it beside the call site that loads it — propose the move, say why, and make it with `git mv` so it reads as one rename.
- **A published address** — `method_ref: "github.com/<owner>/<repo>[/<selector>][@<tag>]"`. The tag is the pin, and it is how an address is given for an integration meant to stay put. **An address with no tag is accepted and floats**: it resolves to the default branch at its head, so the types this integration commits are the ones the address resolved to on the day it ran, and a later refresh can project different ones with nothing in the repository to say why. Say that in one line, recommend the tag, and proceed — the offline gate compares the generated tree against the sidecar, not against the address, so it cannot see an upstream that moved.
- **A catalog id** — `method_id: "mt_…"` (resolve a name through `mthds_list_methods` when present). The catalog is unversioned: an edit to the stored method silently invalidates committed types with nothing offline to detect it. Say so in one line, recommend committing the source or publishing an address, and proceed only on the user's say-so.

**The project** is the nearest directory holding a `package.json` or a `pyproject.toml` (or `setup.py` / `requirements.txt`) above the working area. A workspace holding several (a monorepo, a full-stack repo) is a question — which app? — never a guess. **No project at all** → this is not an integration yet: offer the `pipelex-scaffold` skill (open `../pipelex-scaffold/SKILL.md`), which creates one and hands it back here.

Then look for a **codegen harness**: a `codegen` script in `package.json` or a `codegen` Makefile target, a `sources.json` carrying a `derived` map, `docs/codegen.md` or `docs/add-method.md`, a `methods/` directory beside `src/generated/` or `<package>/generated/`. Either of the first two decides; the rest only corroborate. If the project has one, follow [A project that owns a codegen harness](#a-project-that-owns-a-codegen-harness) from here.

If a `sources.json` with `"generator": "pipelex-integrate"` already names this method, this is [refresh mode](#refresh-mode).

### Step 2: Prove the method is integrable

Call **`mthds_validate`** with the selector. Branch:

- `status: "ok"`, `is_valid: true`, `is_runnable: true`, `pending_signatures: []` → integrable; keep the verdict, step 3 reads from it.
- `is_valid: true` but **not runnable** or `pending_signatures` non-empty → a scaffold with a concept set but no runnable pipes; integrating it produces a call site that cannot succeed. STOP: finish the method with `/pipelex-design` first. Nothing is generated.
- `is_valid: false` → route the `validation_errors[]` to `/pipelex-design` or `/pipelex-edit`; a by-id method's stored content is fixed where it is edited, not here.
- `status: "error"` → class `config` stops per the Requirements; class `input_domain` at `method_ref` / `method_id` is reported in the tool's own words (an unknown or foreign-organization id — the catalog is org-scoped, so another org's method reads exactly like a miss — an unfetchable address, a registry-form ref); class `runtime` → retry once, then report.

### Step 3: Read the pipe's signature — from the verdict

A valid verdict carries **`main_pipe`** in its structured content: `pipe_ref` (the namespaced `domain.pipe_code`), `inputs[]` — each with `name`, `concept_ref`, `multiplicity` (`single` / `variable` / `fixed` with `item_count`) and `required` — and `output` with `concept_ref`, `multiplicity` and `optional`. That is everything the call site is typed against, for every selector alike; record it. **`pipe_ref` and the run's `pipe_code` are not the same string**: `pipe_ref` is namespaced (`summarize.summarize_pdf`) and the run route takes the code alone (`summarize_pdf`), so strip the domain when the signature becomes the call site's `PIPE_CODE`. The sidecar records the namespaced `pipe_ref`, the call site passes the bare code, and nothing catches a confusion between them — a namespaced `pipe_code` type-checks, passes the offline gate, and fails only when the method is actually run, which neither gate in step 11 does. Type and run the call site against **that** pipe: `main_pipe.pipe_ref` is the pipe a run with no pipe selector executes — for a published package, the entry its `METHODS.toml` names, which can differ from the `main_pipe` its bundle declares.

**The fallback, when `main_pipe` is absent.** The workshop omits it whole — never a partial signature, and the verdict is unaffected — in three cases: the method settles **no entry pipe** (the bundle declares no `main_pipe`, or a published package's `METHODS.toml` names a pipe the closure does not declare or declares in several domains, which is exactly when a run with no pipe selector would fail too); the entry pipe's contract did not come back whole; or the workshop **predates the signature** (`@pipelex/mcp` 0.13.0 and earlier; `npx -y @pipelex/mcp@latest` refreshes it). The one-line signature in the verdict's text summary is missing in the same cases — so it is the **same fact on a second channel, not a fourth cause**, and that is what makes it useful: when the structured field did not reach you but a `## Main pipe` line did, read the signature from there rather than treating the method as unsignatured. A host that does not surface structured content, or a cached tool schema, is the usual reason, and it is not a property of the method. Then, for a **files source**: take the pipe the bundle declares as `main_pipe`, or ask which pipe to integrate when it declares none; call **`mthds_inputs_template`** with the selector, that `pipe_ref`, and **`explicit: true`** — one of the two calls in this plugin that want the ceremonial `{concept, content}` envelope, because it is the concept ref per input you need (`/pipelex-explain` makes the other, for a method whose source it cannot read) — and read the pipe's `output` declaration from the bundle. For a **`method_ref` or `method_id` source** the output concept has no in-context channel: STOP and say the verdict carries no signature to type this integration exactly — the workshop may predate it, or the method settles no entry pipe — rather than guessing.

### Step 4: Choose the target, the destination and the generator

State the three in one line before writing. The rule for the target is about **audience**, not language:

| Project | Target | Emits |
|---|---|---|
| `package.json` with a TypeScript build (a `tsconfig.json`, or a runtime/bundler that strips types) | `ts-zod` | `types.ts` (zod schemas + inferred types, depends only on `zod`) and `binder.ts` (`parse<Concept>` / `serialize<Concept>`); keep both |
| `pyproject.toml`, `pipelex` **not** among the dependencies | `python-pydantic` | `models.py`, plain `BaseModel`s, no Pipelex import — for a consumer of the hosted API |
| `pyproject.toml`, `pipelex` **is** a dependency and the code uses `@pipe_func` or `StructuredContent` | `python-structures` | `structures.py`, runtime `StructuredContent` classes — only for a Pipelex host; it imports the runtime and would not even load elsewhere |

`pipelex` present but neither signal in the code → one question, `python-structures` offered first. A JavaScript project with no TypeScript build is asked, because `types.ts` needs one. Field keys are wire-native snake_case in every target, TypeScript included. The destination is one dedicated directory per method — `src/generated/<method>/` or `<package>/generated/<method>/` by default, beside any generated code the project already keeps; the method's directory name is the method's, in the language's casing (`summarize-pdf` in TypeScript, `summarize_pdf` in Python). **A directory that already holds a `codegen.lock` is this method's only when a `sources.json` beside it names this method.** A lock with no sidecar, or a sidecar naming another method, means the directory belongs to another generation — even when the user names it — and every method of a target emits the same file names, so generating into it would silently overwrite the other method's stamped files rather than report an orphan: choose another directory, say why, and let the user overrule only by naming the method that directory is for. **One lock-without-sidecar is this method's own, and relocating is the wrong answer for it**: the lock is written in step 6 and the sidecar in step 7, so every stop between them — an `is_current: false` that step 6 does not let through, a partial write whose retry also failed — leaves exactly that state for the method you are integrating now. When the destination is the one this method would have chosen and the tree's artifacts are the target's, treat it as an interrupted run of your own: regenerate in place, which overwrites its own stamped files, and write the sidecar that was missing. Two trees for one method is the fragmentation this rule exists to prevent, not a way out of it. Where you cannot tell whose the tree is, ask — never relocate silently, and never clear it. A harness-owned layout is a further exception, named in its own section. The generator is the workshop's write arm — or the project's harness, per its section. Detection detail: [references/typescript.md](references/typescript.md), [references/python.md](references/python.md).

### Step 5: Make the tooling leave the tree alone — before the tree exists

Add the generated directory to the formatter's and linter's ignore lists per the language reference (`.prettierignore`, an ESLint flat-config `ignores`, Biome; `[tool.ruff] exclude`, Black, isort), and with it the gate script step 10 will copy — `scripts/codegen-check.mjs` for `ts-zod`, `scripts/codegen_check.py` for `python-pydantic`; `python-structures` gets no script. Confirm the type checker's include **still covers the tree** (an exclusion that would drop it is not added) while leaving its coverage of the script as it is, and confirm the tree is not gitignored. Do this **before** step 6: the first project-wide `format` run after generation would otherwise rewrite the stamps and turn the check red. The script's entry goes in now as well, before the script exists, for the same reason: it is copied verbatim and refresh compares it byte for byte with the reference, so a reformat would make every refresh re-copy it and leave the project's own format check failing.

### Step 6: Generate

Call **`mthds_codegen`** with the selector, `target`, and **`output_dir`** — the generated directory's path **relative to the workshop's working directory**, which is the directory the harness was launched in (the launcher does not `cd`); never absolute. **Check containment before the call rather than waiting for an error**, because a wrong `output_dir` can be perfectly legal: the workshop's working directory is the directory this session started in, so the project is reachable only when its root is that directory or below it. Take the path from the workshop's working directory to the generated directory and read it — one that has to climb out (`../`) means the project is not under the workshop, and a project root elsewhere on disk means the same thing even when some path inside the workshop would be accepted. **Resolve both sides before comparing them** (`pwd -P`, `realpath`), because a lexical reading is wrong in both directions: a symlink inside the workshop pointing at a project outside it reads as contained, and on macOS a project under `/tmp` or `/var` reads as outside when the workshop's own `process.cwd()` is the `/private/...` form of the same place. **Both inputs are known as soon as step 1 names the project, so do this reading there** and only restate it here: by the time you reach this step you have already copied a bundle into the project, possibly staged a `git mv`, and written the step-5 exclusions, so a stop now leaves all of that on disk — if you arrive here anyway, say in the stop exactly what is already written, because none of it is yours to revert. Then STOP with the instruction to relaunch the harness from the project root (or register the workshop with that working directory) — do not ride content instead, do not pass a climbing path, and **never write the tree into the workshop's own directory and move it across afterwards**: the bytes would survive the move, but every later refresh meets the same mismatch and the sidecar's project-relative paths describe a project the workshop cannot see.

Branch on the structured result:

- `status: "ok"`, `is_valid: true`, and `output_dir` present in the result → written. Confirm **`is_current: true`** and an empty **`orphans[]`** — a clean tree is both, and the two branches below carry the two cases where it is not. Every `artifacts[]` entry and the `lock` carry `written_to`; nothing carries `content`.
- `is_valid: false` → the method regressed since step 2 — back to step 2's repair route.
- `status: "error"`, class `input_domain` located at `output_dir`:
  - the message says `output_dir already holds …, which this tool does not own` → a foreign file, a symlink or a directory sits at an artifact path. The whole write was refused and the tree is byte-identical; this is not a dedicated generated directory. Choose or ask for one that is. **Never delete, move or "clear" the named file, and never offer to** — the file is the user's and not in the way; the directory choice was wrong. The same holds when you notice such a file before calling the tool: pick or ask for another directory.
  - the hint says files stay inside the directory the host started the server in → the containment escape above: STOP with the relaunch instruction.
- `status: "error"`, class `runtime`, `retryable: true` (a partial write) → call again **once** with the same `output_dir`, as the hint says: regeneration overwrites its own stamped files. Then report what landed, in the tool's words.
- Success with **`orphans[]` non-empty and `drifts[]` empty** → **carry on to step 7 and report the orphans.** Read that condition off those two lists and never from `is_current`, which is `false` on this branch and on the one below alike: the verdict goes false the moment the directory holds a stamped file the new lock does not list, which is why a directory holding more than one generation reads as permanently non-current. **`drifts[]` is present only when it is non-empty, so a result carrying no `drifts` at all is the empty case and belongs on this branch**, not on the one below — `orphans[]` is always there and empty when clean, `drifts[]` only when something other than an orphan drifted. The empty `drifts[]` is what makes this case the benign one — every fault the check found is an orphan, so nothing the write itself produced is broken — and the causes are an earlier generation of this method, a target switch, an engine version that renamed an artifact. Finish the integration — the sidecar, the dependencies, the call site — and never delete an orphan. **The report says what the signal means and claims nothing it cannot show**: the directory holds artifacts this generation does not list, named by path, none of them deleted, and a dedicated directory per generation is the fix, in the tool's own words. Never say the write may have overwritten another method's files — an orphan cannot indicate that, because overlapping file names are overwritten without being reported as orphans at all (step 4), and step 4's refusal is what guards that case. Say instead that the gate of step 10, where the project has one, counts every orphan as a drift in either language, so that check exits non-zero on this directory until it holds one generation, and that deleting an orphan to turn it green is the one remedy forbidden above. `orphans_truncated: true` → say orphan detection was partial rather than reporting a clean tree. **The branch below ends the run and this one does not, and the whole of the asymmetry is that stated condition**: the check disowns both trees, so one non-orphan drift beside the orphans puts the result on that branch and not on this one.
- Success with **`is_current: false` for any reason the branch above does not claim** — a non-empty `drifts[]`, with or without orphans beside it → a write the check faults for something it produced. Report the `drifts[]` verbatim (`path`, `category`, `detail`) and stop; never commit a tree the check rejects.

### Step 7: Write the sidecar

Write an **unstamped `sources.json`** beside the lock — the only state this skill keeps. The lock signs the artifacts, not their sources; without the sidecar the next run re-derives everything and a bundle edit is undetectable. Shape:

```json
{
  "comment": "Written by /pipelex-integrate. `method` and `target` are how this tree was generated — re-run the skill to refresh it. `bundle_dir` is the directory the call site loads and `sources` the SHA-256 of every .mthds file under it, so a bundle change that was never regenerated — a file edited, removed or added — is detectable. Not part of the codegen lock; do not hand-edit.",
  "generator": "pipelex-integrate",
  "method": { "files": ["methods/summarize-pdf/main.mthds"] },
  "target": "ts-zod",
  "pipe": {
    "pipe_ref": "summarize.summarize_pdf",
    "inputs": { "document": "native.Document", "context": "native.Text?" },
    "output": "summarize.DocumentSummary"
  },
  "bundle_dir": "methods/summarize-pdf",
  "sources": { "methods/summarize-pdf/main.mthds": "<sha256 of the file's bytes>" }
}
```

`method` is exactly one of `{files}`, `{method_ref}`, `{method_id}`, as passed. Paths are relative to the **project root**, not to the workshop's working directory. `pipe` records what the call site was typed against — `Concept` single, `Concept[]` a list, `Concept?` optional — so refresh mode can tell a signature change from a body change. **`bundle_dir` is the directory step 9's call site loads the bundle from** — its `BUNDLE_DIR`, relative to the project root — and **`sources` carries one hash for every `.mthds` file beneath it**, which is the same set `method.files` names. The hashes alone would prove only that the recorded files are unchanged: the call site loads whatever `.mthds` file sits under its directory, so a file added there changes what runs while every recorded hash still matches, and the gate of step 10 catches that addition by listing `bundle_dir`, which it can only do when the sidecar records it. For a `method_ref` / `method_id` source there is no `bundle_dir` and `sources` is empty. **A `method_ref` is recorded exactly as it was passed, an absent tag included** — the sidecar never resolves a floating address into the tag or commit it happened to land on, because that would record a pin the integration does not have and the next refresh would silently stop following the branch the user asked for. So a sidecar naming an untagged address is the record that this tree floats, and it is what step 12 reports. Hashes are over the raw bytes: `shasum -a 256 <file>` / `sha256sum <file>` / `hashlib.sha256(path.read_bytes())`.

### Step 8: Add the dependencies the generated code needs

With the project's own package manager (read the lockfile): `zod` and `@pipelex/sdk` for TypeScript — at least `@pipelex/sdk` 0.18.0, the first release carrying everything step 9's call site and step 10's gate use (the run-results surface the call site hands back — the artifact operations, `summarizeUsage`, and `working_memory` and `graph_assembly_error` on `RunResults` — is the last to arrive), so an older pin already in the project is raised and the report says so; `pydantic` and `pipelex-sdk` for Python — at least `pipelex-sdk` 0.10.2 for a `python-pydantic` consumer, which carries everything step 9's call site and step 10's gate use (the offline check, and the run-results surface the call site hands back — the artifact stack, `summarize_usage`, and `working_memory` and `graph_assembly_error` on `RunResults`) and matches what `pipelex-starter-python` declares, so code read from that reference project runs on what was installed; an older pin already in the project is raised and the report says so (`python-structures` needs `pipelex`, which is already present by the time that target is chosen). State what is added; interactive mode confirms first.

### Step 9: Write the call site

**One new module per method**, placed by the project's convention (its service / action / client layer when it has one; else `src/pipelex/<method>.ts` or `<package>/pipelex/<method>.py`). It exports one async function named after the method whose parameters are the pipe's inputs, typed from the signature, and whose return type carries the narrowed output beside the run's whole results — TypeScript `type <Method>Run = { output: <Concept>; results: RunResults }`, Python a frozen dataclass of the same two fields — so a caller who wants only the output writes `.output` and a caller who wants anything else reads it off `results` without the module being edited. It loads **every `.mthds` file of the committed bundle** at call time (files source), from the directory step 7 records as `bundle_dir` and from no other — a bundle is one closure, so a main file that imports a sibling needs that sibling in the same `mthds_contents`, and a call site that submits `main.mthds` alone fails at load time on exactly the methods codegen handled correctly — or names the `method_ref` exactly as the sidecar records it / the `method_id`, runs it through the SDK's self-healing lifecycle call (`startAndWaitForResult` / `start_and_wait` — the durable path on the hosted API, blocking on a bare runner), and narrows `main_stuff` through the generated binder (`parse<Concept>(results.main_stuff)`) or model (`Model.model_validate(results.main_stuff)`), returning that narrowed value as `output` beside the `results` it was narrowed from. **Narrow according to the output's `multiplicity`, which step 3 recorded** — the single-concept form above is right only for `single`. A `variable` or `fixed` output arrives as the envelope `{"items": […]}` — the runtime's `ListContent` serialised — and **not** as a top-level array, so a generated single-object parser applied to `main_stuff` whole rejects it and a run that succeeded fails in the narrowing. Read `items` off the object and map the parser over its members, so `output` is `T[]` / `list[T]` (`z.object({ items: z.array(z.unknown()) }).parse(results.main_stuff).items.map(parse<Concept>)`, `TypeAdapter(list[Model]).validate_python(results.main_stuff["items"])`). Codegen emits a parser per concept and no wrapper type for the envelope, so nothing in the generated tree names it and this step is where the integrator learns it exists. For `fixed`, say in the report that the declared `item_count` is not checked unless you check it. `main_stuff` is `unknown` / `Any`, so the type checker does not catch this one either — it surfaces only on a real run, which step 11 never does. Credentials come from the environment through the SDK's own defaults (`PIPELEX_API_KEY`, `PIPELEX_BASE_URL`); the module never reads them itself.

**What the returned results carry, and what reads each field.** These are the reason the module hands back the whole `RunResults` rather than the output alone: each one is a field the SDK already parses and a call away for the caller, so none of them is a reason to write a second module into the project. `@pipelex/sdk` documents every field in its `docs/run-results.md`; name the ones the method actually produces rather than reciting the list.

**Where the SDK's own pages are, and which address to write down.** Three pages of `@pipelex/sdk`'s own documentation are named in this skill — `docs/run-results.md`, `docs/artifact-download.md` and `docs/run-usage.md` — and each is published at `https://github.com/Pipelex/pipelex-sdk-js/blob/main/docs/<page>.md`. That URL is what to write down wherever the reference will be read outside this session — the report, the module's own docstring, any comment pointing a user at a page — because a path into `node_modules` means nothing to somebody reading the project's source. **Those three pages and no others**: a `docs/…` path named for the project being integrated is that project's own file and keeps its own address, and `pipelex-sdk`, the Python SDK, has its pages under `https://github.com/Pipelex/pipelex-sdk-python/blob/main/docs/` — it ships none in its wheel, so a Python reference is that URL in every position. To read one of the three yourself, look first for `node_modules/@pipelex/sdk/docs/<page>.md`, the copy the project actually installed, which is there from the release that first shipped the pages inside the package. An install without a `docs/` directory has not got that release, which is the common case rather than a missing page: fetch the page at its URL and say nothing to the user about their install being out of date.

- **`results.pipeline_run_id` is the durable handle.** It outlives the process that started the run, so a caller that persists it can read the same run later through the SDK's lifecycle calls — which is also what a `RunTimeoutError` leaves a caller holding, the run itself still going.
- **A produced file comes back as a `pipelex-storage://` reference, and the `public_url` beside it is short-lived.** The reference is what belongs in a record or a database; the signed link expires on the storage provider's own schedule and a row or a rendered page that kept it stops working with no warning. To read the bytes, `client.downloadArtifacts` saves everything a run produced under a directory and `client.resolveArtifacts` mints fresh links for a whole list of references — **both are methods on the client rather than imports**, since each one calls the API, and the barrel exports only `collectArtifacts`, which lists a result's references without touching the network, and `artifactFilename`. They are documented on the SDK's `docs/artifact-download.md`, and none of them is re-implemented in the project.
- **`summarizeUsage(results)` reads what the run cost.** It folds the `tokens_usages` / `usage_assembly_error` pair into one run-level reading with a per-pipe rollup, under the rules its `docs/run-usage.md` states — an unrated cost is not a zero one — and it is pure: no client, no network. Never total the records by hand.
- **`results.graph_spec` is the graph the run actually executed**, one node per pipe with its status, timings and attributed cost. It is typed `unknown` because its canonical declaration is `GraphSpec` in `@pipelex/mthds-ui`, which is also where the viewer that renders it lives; a project that wants the graph on screen takes that package and loads the viewer client-side only.
- **`results.working_memory` holds every named stuff of the run**, root and aliases, which is what repopulates the inputs of a run read back later — the whole run, where `main_stuff` is its output alone.

On a Python project every one of these has its Python twin in the `pipelex-sdk` 0.10.2 step 8 installs, under the same names in snake case — `client.download_artifacts` and `client.resolve_artifacts` on the client, `pipelex_sdk.artifacts.collect_artifacts` for the references alone, `pipelex_sdk.usage.summarize_usage` for the cost, `results.graph_spec` and `results.working_memory` on `RunResults` — and `references/python.md` documents each with the page it is read from, under `https://github.com/Pipelex/pipelex-sdk-python/blob/main/docs/`; the report names them for a Python project exactly as it does for TypeScript, from the Python pages.

| Declared concept | TypeScript parameter | Python parameter |
|---|---|---|
| `native.Text` (or a refinement) | `string` | `str` |
| `native.Number` | `number` | `float` |
| `native.YesNo` | `boolean` | `bool` |
| `native.Date` | `string` (ISO 8601) | `str` (ISO 8601) |
| `native.Image`, `native.Document` | `{ url: string }` — an `http(s)` URL or a `pipelex-storage://` reference | `dict[str, Any]` with a `url` key |
| a structured concept, or a composite native (`Page`, `TextAndImages`, `JSON`) | the generated type from `types.ts` | the generated model from `models.py` |
| `Concept[]` / `Concept[N]` | `T[]` | `list[T]` |
| not required | optional parameter (`?`) | `T \| None = None` |

**Exactly one shared helper, created once per project and reused by every later method:** a client factory (`getPipelexClient()` / a `PipelexAPIClient` construction that matches how the project builds its other clients), and nothing else. There is no second one — the wire-output helper that would have been it was struck for being lossy, and writing one is forbidden below, so read "one" as the whole budget and not as a floor. If the project already has a Pipelex client module, use it. The module **does not upload**: its docstring points callers holding local files or bytes at the SDK's `prepareInputs` / `prepare_inputs`, which is the SDK's own signature-driven upload — and carries the SDK's warning that `prepareInputs` treats any string it does not recognise as `data:`, `http(s)://` or `pipelex-storage://` as a **local filesystem path** it reads and uploads, so a public endpoint must gate schemes before handing values to it. Python projects that are synchronous throughout get a thin `asyncio.run` wrapper beside the async function; async-native projects get the async function alone. Follow the project's conventions where you can see them (module style, quoting, error handling) and the SDK's defaults where you cannot; let the SDK's typed errors propagate. Module templates: the language references.

### Step 10: Wire the offline drift gate

- **TypeScript**: copy [references/codegen-check.mjs](references/codegen-check.mjs) **verbatim** to `scripts/codegen-check.mjs` — **never formatted or linted**, which step 5's exclusion keeps true — register `"codegen:check": "node scripts/codegen-check.mjs <generated dir> …"` in `package.json`, and **extend the project's existing aggregate gate** — a `check` / `ci` / `validate` / `verify` script, a Makefile `check` target, the lint or test step of an existing workflow — rather than inventing a new one. The script runs `@pipelex/sdk`'s `runCodegenCheck` over each directory, compares the sidecar's source hashes against the committed `.mthds` files and its `sources` against every `.mthds` file under its `bundle_dir`, and exits `0` current / `1` drift or stale source / `2` no verdict. **A gate that cannot run gives no verdict, never drift**: the script resolves `@pipelex/sdk` from where it sits, so keep it inside the project and run it after the dependencies are installed; a missing `@pipelex/sdk`, one older than step 8's 0.18.0 floor that lacks the check, or a check that throws, exits `2` and says why. A project with no aggregate gate gets the script and one sentence in the report saying where to call it.
- **Python, `python-pydantic`**: copy [references/codegen_check.py](references/codegen_check.py) **verbatim** to `scripts/codegen_check.py` — **never formatted or linted**, which step 5's exclusion keeps true — and **extend the project's existing aggregate gate** with `python scripts/codegen_check.py <generated dir> …` — a Makefile `check` target, a step of an existing workflow, a local `.pre-commit-config.yaml` hook, the `nox` / `tox` session that already lints — rather than inventing a new one. **Run it with the project's own environment**, where step 8 put `pipelex-sdk`: `uv run python` under uv, `poetry run python` under poetry, `pipenv run python` under pipenv, the active environment's `python` otherwise. **A gate that cannot run gives no verdict, never drift**: an interpreter without the SDK, or a check that raises, exits `2` and says why. The script runs `pipelex-sdk`'s `run_codegen_check` over each directory — no engine, no network, no key and no `pipelex` runtime, so never add `pipelex` as a dependency to get a gate — compares the sidecar's source hashes against the committed `.mthds` files and its `sources` against every `.mthds` file under its `bundle_dir`, and exits `0` current / `1` drift or stale source / `2` no verdict, exactly as the TypeScript script does. A project with no aggregate gate gets the script and one sentence in the report saying where to call it.
- **Python, `python-structures`**: the project already depends on `pipelex`, so `pipelex codegen check <dir>` (exit `0` / `1` / `2`) is wired into its existing gate.

**None of the three refuses an address without a tag, and none of them can see one move.** A gate reads the sidecar's `sources` and `bundle_dir` and never its `method`, so a by-ref integration — tagged or not — takes the source half's "records no sources" branch and is current on that count; what the gate still proves is that the generated tree matches its own lock. An upstream that moved under a floating address is therefore invisible here by construction, which is why it is said in words at step 12 instead: the remedy is the tag, or re-running this skill.

### Step 11: Verify

Run the project's formatter **on the files you wrote only** — never on the generated tree or the gate script step 10 copied; the step-5 exclusions are what make a later project-wide run safe — then its type checker, then the gate you installed, with the command step 10 wired (for Python, in the project's own environment). A failure in your own code is yours to fix before reporting; a failure inside the generated tree is reported, never patched. **A gate that exits non-zero only because the directory holds the orphans step 6 let through is neither, and it is not yours to fix**: it is the expected state of a run that continued, it counts each orphan as a drift exactly as step 6 said it would, and it is reported in step 12's words rather than cleared. Deleting an orphan to turn it green is the one remedy the rules that never bend forbid; a dedicated directory per generation is the real one, and it is the user's to take.

### Step 12: Report

What was generated and where; the target and why; the call site's signature; what changed in the tooling config; how to refresh (this skill again after a bundle edit); any orphans step 6 reported — their paths, that the directory holds artifacts this generation does not list and that none of them were deleted, that the gate of step 10, where the project has one, counts each one as a drift so that check stays non-zero until the directory holds one generation, and a dedicated directory per generation as the fix — restated here rather than left behind at step 6, because this is where the user reads it; where the drift gate was wired and the command that runs it — or, on a project with no aggregate gate, where to call it; for a `method_id` source, that the catalog is unversioned; **for a `method_ref` with no tag, that the types committed here are the ones the address resolved to on the day they were generated** — the selector record keeps the address untagged — this skill's sidecar, or the harness's own manifest where the project owns one — no gate can tell that the upstream has moved, and a refresh is what picks up whatever the default branch holds then. Then the hand-off: `/pipelex-inputs` prepares inputs and offers a run.

## Refresh mode

Entered when the user asks to refresh, regenerate or update the types; when `/pipelex-edit` or `/pipelex-design` hand off after editing a bundle a sidecar names; or when step 1 finds a sidecar for the method. **Re-derive nothing the sidecar already records, regenerate in place, bring the drift gate up to step 10, and leave alone everything else the regeneration did not invalidate.**

| Taken from disk | Re-derived and re-checked | Left alone |
|---|---|---|
| the selector, target, destination and `pipe` record — from the sidecar, except a files source's **file set, which is re-read from `bundle_dir` and never taken from `method.files`**: every `.mthds` file beneath that directory, as the call site loads it, so a file added since the last generation is passed and a file removed is not (a sidecar with no `bundle_dir`, written before the skill recorded one, takes the directory from the call site module's `BUNDLE_DIR`); the previous `crate_fingerprint` — from the lock | the source hashes (files source), compared **before** regenerating against that re-read file set, so the report can say whether the bundle actually changed and how — a file edited, removed or added; the signature, through `mthds_validate` again, compared to the sidecar's `pipe` record; **the drift gate of the refreshed method's language**, re-checked against step 10 on every refresh, because a project integrated by an earlier version of this skill carries an earlier gate or none — `scripts/codegen-check.mjs` for `ts-zod`, `scripts/codegen_check.py` for `python-pydantic`, each compared and copied only after the tooling exclusions are verified to name it: **a missing script is installed and wired as step 10 does**, **a script that differs byte for byte from the reference is re-copied verbatim**, and **the refreshed method's generated directory is added to the gate command's arguments when it is not among them** (or, where the project has no command to register it in, named in the report's sentence saying where to call the script), each said in the report; for `python-structures` there is no script to copy, so only the last applies, to its `pipelex codegen check` wiring | the tooling exclusions for the generated directory and, where step 10 copies one, the gate script (verified before the gate is re-checked, re-added only if missing), the dependencies (except an `@pipelex/sdk`, or a `python-pydantic` project's `pipelex-sdk`, pinned below step 8's floor, raised as step 8 raises it, with a line in the report), the client helper, tests, other methods' trees and their registrations in the gate |

One `mthds_codegen` call with the recorded arguments. Read what happened from the lock's **`artifacts[].content_hash`**, not from the fingerprint: `crate_fingerprint` covers the whole bundle, so a prompt-only edit moves it — and with it the one stamp line at the head of every artifact — while the projected code is byte-identical. Content hashes unchanged therefore means a pure restamp: say so, and leave that one-line-per-file diff to the user's commit. A content hash that moved is the concept set actually moving. **A non-empty `orphans[]` with an empty `drifts[]` does not end a refresh either**, on step 6's condition and read the same way — off the two lists and never from `is_current`, which is `false` here as there: every fault the check found is an orphan, so the regeneration itself is sound, and `drifts[]` is present only when non-empty here too, so a result with no `drifts` at all is the empty case. The refresh runs on through the type checker, the call site and the sidecar, and the report names the orphan paths, never deletes them, says the directory holds artifacts this generation does not list — an earlier engine's file names, a target switch, another method's tree in a directory that was never dedicated — gives a dedicated directory per generation as the fix, and says the gate stays non-zero on that directory until it holds one generation, where a gate was installed at all. None of those causes is the write overwriting another method's files, which an orphan cannot show here any more than in step 6, and none of them is a reason to delete anything. `orphans_truncated: true` → say detection was partial. Any other `is_current: false` still ends the refresh, there as here. Then the type checker. **The call site is edited if it no longer type-checks, if the `pipe` record no longer matches the signature** — a renamed input, a reshaped output — **or if it still returns the output alone instead of the results beside it**, which is what a project integrated by an earlier version of this skill does. That last trigger is spelled out because neither of the first two fires on it: returning the narrowed output still type-checks, and a return shape is not part of the `pipe` record — so without it a refresh would raise the pin to step 8's floor and leave the project on an SDK whose whole run-results surface its own module does not hand back. The edit is the minimal one, stated in the report. **That third trigger changes the module's public return type, so the project's own callers change with it**: find every call of the exported function — and of its `_sync` twin on Python — and migrate each to `.output`, except where the caller wants one of the extras the results now carry. **Then run the type checker again, after the call site and the callers have moved and not only before them**, because the run that preceded the edit passed on the old shape and says nothing about the new one; on a Python project, where the type checker is whatever the project already wires and may be nothing, the callers are found by name and the report says how. A caller left behind does not fail the way the rest of this skill fails: on TypeScript the project stops compiling, and on Python it type-checks and then raises `AttributeError` at runtime on the field it used to read. The report names the return-type change as a breaking one and lists the callers that moved. The sidecar is rewritten last, with its `bundle_dir`, and with `method.files` and `sources` naming the file set this refresh passed. A directory holding a `codegen.lock` with no sidecar naming this method is not this method's (step 4): choose another directory name and say why, or let the user overrule by naming the method that directory is for, as step 4 allows. The exception is a harness-owned layout, which keeps no sidecar by design — see that section.

## A project that owns a codegen harness

A project made from the method app (`pipelex-method-apps`' `webapp-js/`), `pipelex-starter-js` or `pipelex-starter-python`, or one that adopted their pattern, regenerates every method in one place, checks them in one place, and keeps its own sidecar. Writing this skill's tree beside that would leave two regeneration paths, two sidecar dialects and files the workshop never emits. So on such a project:

- **a local bundle is one command when the project's `make add-method` takes a bundle path**, which its usage in `make help` says, as the method app's does: `make add-method METHOD=<path to the bundle>` copies the bundle into `methods/<name>/` and writes the whole slice, call site included, so nothing below is done by hand except the verification;
- **otherwise, place the method where the project keeps them** — `methods/<name>/main.mthds`, or the project's manifest form for a catalog or published method. **A `method_ref` written into that manifest goes in exactly as it was passed, an absent tag included**: step 7's rule is about the selector, not about the file it lands in, and resolving a floating address into the tag it happened to land on would pin the project to a version the user never asked for — here by hand, where no tool is there to stop it;
- **run the project's generator** — `make add-method METHOD=…` when the project has it and the method is remote, its `codegen` script or Makefile target otherwise;
- **write the call site the way the project's docs and existing methods do** (`docs/codegen.md`, `docs/add-method.md`, the existing actions or CLI commands), not the shape of step 9;
- **skip steps 5, 7, 8 and 10** — the exclusions, dependencies, sidecar and gate already exist — and verify with the project's own aggregate gate (`make check` / `make all`);
- **refresh is the project's `codegen` script**, and you say so instead of calling `mthds_codegen`.

The harness owns the layout and the check; its generator is preferred, not mandatory. When it cannot run — the Python starter's `make codegen` stops when no `PIPELEX_API_KEY` reaches the project, although the workshop holds a key of its own — call `mthds_codegen` with `output_dir` set to **the harness's own layout** (`<package>/generated/<module>/`, `<module>` being the method directory's name with its dashes turned into underscores, as the Python reference spells out), which is byte-identical there (same engine, same stamps, same lock, and that starter keeps no sidecar), and say the project's `make codegen` is the refresh once its prerequisite is met. Never a second layout beside the first. **This is the one destination step 4's sidecar rule does not govern**: the harness owns the directory and deliberately keeps no `sources.json`, so on every run after the first it holds a lock with no sidecar — which would otherwise read as another generation's. The harness branch is entered at step 1, before that rule applies; write into the layout again rather than inventing a second directory name, which is the thing this whole section exists to prevent.

## When something goes wrong

| Condition | Do this |
|---|---|
| a required tool is absent | STOP with the one-line MCP-connection message above |
| `status: "error"`, class `config` — including the codegen **403** feature gate | STOP, surface `hint` verbatim; never say "check your key" for a 403 |
| `status: "error"`, class `config`, `kind: "paywall"` | STOP, surface the plan-limit hint |
| `is_valid: false` | route `validation_errors[]` to `/pipelex-design` or `/pipelex-edit` |
| not runnable, or `pending_signatures` non-empty | STOP: finish the method with `/pipelex-design`; nothing generated |
| `input_domain` at `output_dir`, containment escape | STOP: relaunch the harness from the project root; never ride content |
| the project root is not the workshop's working directory or below it (the harness was launched elsewhere) | STOP **before** generating, with the same relaunch instruction — the tool cannot catch this, because a path inside the workshop is legal wherever it points; never write beside the wrong project and move the tree over |
| `input_domain` at `output_dir`, a file this tool does not own | not a dedicated generated directory — choose another or ask; never delete, move or clear the named file, and never offer to |
| `input_domain` at `method_ref` / `method_id` | report the selector failure in the tool's words |
| `runtime`, `retryable: true` after a partial write | call again once with the same `output_dir`; then report what landed |
| success with `orphans[]` non-empty **and `drifts[]` empty** | **continue to step 7** — read the two lists and never `is_current`, which is `false` here too; `drifts[]` is present only when non-empty, so no `drifts` at all is the empty case. Report the orphan paths by name, never delete one, say the directory holds artifacts this generation does not list and that the step-10 gate, where the project has one, counts each as a drift until it holds one generation; a dedicated directory per generation is the fix. Never claim the write overwrote another method's files — an orphan cannot show that (step 4). `orphans_truncated: true` → say detection was partial. Unlike the row below, this one does not end the run, and only this exact condition does not |
| success with `is_current: false` for any other reason | report `drifts[]` verbatim and stop; do not commit a tree the check rejects |
| `main_pipe` absent on a by-ref / by-id source | first look for the one-line signature in the verdict's **text summary** — it carries the same thing and reaches a host that does not surface structured content. Genuinely absent → STOP: all three causes of step 3, and the remedy differs by cause — the workshop predates it (refresh `@pipelex/mcp` and retry), the method settles no entry pipe, or the entry pipe's contract did not come back whole, which comes from the runner behind `/v1/validate` and no refresh fixes. Never guess the output concept |
| the project's type check fails after the call site is written | your code — fix and re-run; a failure inside the generated tree is reported, not patched |
| the TypeScript gate exits `2` saying `@pipelex/sdk` is not importable, could not be loaded, or predates the offline check | the gate ran where the project's dependencies are not installed (a CI job that skips the install step), from a copy outside the project, or against a `@pipelex/sdk` below 0.18.0 — install the dependencies, run it through the package manager as step 10 registers it, and raise the pin per step 8; never regenerate, because the tree is not what failed |
| the TypeScript or the Python gate exits `2` with `no verdict: the lock check failed` or `the source check failed` | a check threw or raised instead of reaching a verdict, which says nothing about the tree — report the message verbatim; never regenerate to clear it |
| the Python gate exits `2` saying `pipelex-sdk` is not importable, or that it failed while loading | not importable: the command runs outside the project's environment, or the project pins `pipelex-sdk` below 0.10.2 — run it the way step 10 says and raise the pin per step 8. Failed while loading: the SDK is installed but raised as it loaded (a pydantic-core that does not match pydantic, a corrupt file), so the project's environment is broken — reinstall its dependencies rather than reaching for another interpreter, and report the exception the message names. Either way never regenerate, because the tree is not what failed, and never swap the gate for `pipelex codegen check`, which needs the runtime a consumer does not have |
| a gate reports `stale-source: … — added to the bundle since the types were generated`, or a `stale-source` saying `sources.json` records sources but no `bundle_dir` | the bundle gained a `.mthds` file the types were not generated from, or the sidecar predates `bundle_dir` and cannot rule that out — refresh mode, which re-reads the file set from the bundle directory and rewrites the sidecar with it; never add the file's hash to `sources` by hand, which turns the gate green over types that still do not cover it |
| `TS2835` on a relative import, in **your own** call-site module | yours to fix, per step 11: on a project whose resolution demands extensions, the module's own imports of the generated tree and its client helper take `.js` (`from "../generated/<method>/binder.js"`). Do not route this to the row below — that one is about the stamped tree only |
| `TS2835` in the generated `binder.ts`: a relative import needs a file extension | a known defect of the ts-zod emitter, which writes `from "./types"`. On this project shape the integration does not merely fail a check — the compiled code cannot load the module at all (`ERR_MODULE_NOT_FOUND`), so the report says the integration cannot execute until the emitter is fixed or the user changes `moduleResolution`, not that a check is red. It bites a plain Node ESM project (`"type": "module"` with `moduleResolution` `nodenext` or `node16`) and not a bundler one. Report it — the fix is upstream; never patch the stamped file (a regeneration loses the patch and the stamp is hashed) and never drop the tree from the type checker. Changing the project's `moduleResolution` is the user's call to make, not yours |
| `mthds_list_methods` absent | integrate by id, address or files; never stop for it |

## Reference

- [references/typescript.md](references/typescript.md) — detecting a TypeScript project (build, package manager, generated root, Prettier / ESLint / Biome exclusions, `tsconfig` coverage, the aggregate gate, the call-site location), the call-site module and client helper templates, the `codegen:check` wiring, the harness a starter-derived project owns.
- [references/python.md](references/python.md) — the same for Python (import package, `python-pydantic` vs `python-structures`, uv / poetry / pipenv / pip, Ruff / Black / isort exclusions, pyright / mypy coverage, `__init__.py` and package data, the async module plus its sync wrapper, the offline gate's wiring for a consumer, `pipelex codegen check` for the structures audience).
- [references/codegen-check.mjs](references/codegen-check.mjs) — the offline gate copied verbatim into TypeScript projects.
- [references/codegen_check.py](references/codegen_check.py) — its Python twin, over `pipelex-sdk`'s `run_codegen_check`, copied verbatim into `python-pydantic` projects.
- [MTHDS Language Reference](../shared/mthds-reference.md) — for reading a bundle's `main_pipe` and `output` declarations on the fallback path.