diff --git a/.changeset/python-readme-first-workflow.md b/.changeset/python-readme-first-workflow.md new file mode 100644 index 0000000..e08b6d8 --- /dev/null +++ b/.changeset/python-readme-first-workflow.md @@ -0,0 +1,5 @@ +--- +"@operatorstack/yield": patch +--- + +Add a complete Python onboarding guide, use the public PyPI package in generated requirements, and keep npm documentation links and registry badges specific to npm. diff --git a/README.md b/README.md index 1cf1b95..fe34587 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,20 @@

npm version + PyPI version + Build status - MIT license + MIT license

Website · Documentation · npm · + PyPI · + GitHub

@@ -98,7 +102,7 @@ defineSkill((ctx) => { The example uses harmless commands so its fixture can run in any checkout. Replace them with the test, publish, and registry commands for your project. The complete tested source is in -[`examples/release-checklist`](examples/release-checklist/). +[`examples/release-checklist`](https://github.com/operatorstack/yield/tree/main/examples/release-checklist/). ## Use Yield in five steps @@ -213,8 +217,8 @@ forking. Every side effect crosses one of these primitives: | `require` | Bind a required claim to recorded evidence. | | `blocked` / `refused` | Stop honestly when work cannot or must not continue. | -See the [primitive guides](docs/primitives/README.md) and -[runtime reference](docs/reference/cli.md) for the full contract. +See the [primitive guides](https://github.com/operatorstack/yield/blob/main/docs/primitives/README.md) and +[runtime reference](https://github.com/operatorstack/yield/blob/main/docs/reference/cli.md) for the full contract. ## Languages and coding agents @@ -223,10 +227,10 @@ the same program in every language and compares observable behavior. | Language | SDK | Example | |---|---|---| -| TypeScript | [`@operatorstack/yield`](sdk/typescript/) | [`release-checklist`](examples/release-checklist/) | -| Python | [`yieldskill`](sdk/python/) | [`env-doctor`](examples/env-doctor/) | -| Go | [`sdk/yield`](sdk/yield/) | [`investigate`](examples/investigate/) | -| Rust | [`yieldskill`](sdk/rust/) | [`data-migration`](examples/data-migration/) | +| TypeScript | [`@operatorstack/yield`](https://github.com/operatorstack/yield/tree/main/sdk/typescript/) | [`release-checklist`](https://github.com/operatorstack/yield/tree/main/examples/release-checklist/) | +| Python | [`yieldskill`](https://github.com/operatorstack/yield/tree/main/sdk/python/) | [`env-doctor`](https://github.com/operatorstack/yield/tree/main/examples/env-doctor/) | +| Go | [`sdk/yield`](https://github.com/operatorstack/yield/tree/main/sdk/yield/) | [`investigate`](https://github.com/operatorstack/yield/tree/main/examples/investigate/) | +| Rust | [`yieldskill`](https://github.com/operatorstack/yield/tree/main/sdk/rust/) | [`data-migration`](https://github.com/operatorstack/yield/tree/main/examples/data-migration/) | Cursor, Codex, and Claude Code are verified integrations. Yield also includes registry-backed project paths for 73 more coding agents. Those paths support @@ -250,12 +254,12 @@ loop, multi-agent orchestrator, or security sandbox. ## Documentation and development - [Read the public documentation](https://yield.operatorstack.systems/docs/) -- [What a skill workflow is](docs/skill-workflows.md) -- [Ten-minute TypeScript quickstart](docs/quickstart.md) -- [Working examples in all four languages](docs/examples.md) -- [Coding-agent setup](docs/agent-setup.md) -- [Testing workflow effects](docs/testing-fixtures.md) -- [Guarantees and evaluation results](evals/README.md) +- [What a skill workflow is](https://github.com/operatorstack/yield/blob/main/docs/skill-workflows.md) +- [Ten-minute TypeScript quickstart](https://github.com/operatorstack/yield/blob/main/docs/quickstart.md) +- [Working examples in all four languages](https://github.com/operatorstack/yield/blob/main/docs/examples.md) +- [Coding-agent setup](https://github.com/operatorstack/yield/blob/main/docs/agent-setup.md) +- [Testing workflow effects](https://github.com/operatorstack/yield/blob/main/docs/testing-fixtures.md) +- [Guarantees and evaluation results](https://github.com/operatorstack/yield/blob/main/evals/README.md) Run the main checks from the repository root: @@ -264,7 +268,7 @@ go test ./... npm run test:release ``` -The [example library](examples/library/) contains ten common workflows in all +The [example library](https://github.com/operatorstack/yield/tree/main/examples/library/) contains ten common workflows in all four SDKs, including code review, failure investigation, CI repair, dependency updates, database migration, security audit, and package release. diff --git a/cmd/yskill/main_test.go b/cmd/yskill/main_test.go index 26042f2..8491daa 100644 --- a/cmd/yskill/main_test.go +++ b/cmd/yskill/main_test.go @@ -240,6 +240,9 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) { if !strings.Contains(manifest, tt.pin) { t.Fatalf("manifest does not contain %q:\n%s", tt.pin, manifest) } + if tt.language == "python" && strings.Contains(manifest, "--index-url") { + t.Fatalf("public Python scaffold contains a private package index:\n%s", manifest) + } }) } if tidyCalls != 1 { diff --git a/cmd/yskill/scaffold.go b/cmd/yskill/scaffold.go index 10cb362..77503f6 100644 --- a/cmd/yskill/scaffold.go +++ b/cmd/yskill/scaffold.go @@ -154,7 +154,7 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string { case "python": return map[string]string{ "main.py": mainPython, - "requirements.txt": fmt.Sprintf("--index-url https://get.operatorstack.systems/pip/simple/\nyieldskill==%s\n", v), + "requirements.txt": fmt.Sprintf("yieldskill==%s\n", v), "skill.json": "{\"version\":1,\"language\":\"python\",\"run\":[\"python\",\"main.py\"]}\n", } case "rust": diff --git a/evals/results/latest.json b/evals/results/latest.json index f74d0ff..d5785ae 100644 --- a/evals/results/latest.json +++ b/evals/results/latest.json @@ -1,8 +1,8 @@ { "schema_version": 2, "methodology_version": "1.1", - "generated_at": "2026-08-07T20:19:15.755Z", - "source_digest": "1695983e8279e9580deefe615b29814c8cc2777ce043cc73dd47caf992d7f269", + "generated_at": "2026-08-07T21:08:41.456Z", + "source_digest": "e702eb9bc1362b9a4d77fd41c9860bad97405c7aab3b40aba3a6ba3046c43933", "status": "passed", "workflow_conformance": { "passed": 40, diff --git a/examples/env-doctor/main.py b/examples/env-doctor/main.py index 21e5221..b6361db 100644 --- a/examples/env-doctor/main.py +++ b/examples/env-doctor/main.py @@ -9,8 +9,9 @@ from yieldskill import define_skill # noqa: E402 +# README_EXAMPLE_START def program(ctx): - probe = ctx.run_command("probe-python", "python3 --version", timeout_seconds=60) + probe = ctx.run_command("probe-python", "python3 --version || python --version", timeout_seconds=60) diagnosis = ctx.agent_task( "diagnose", @@ -35,7 +36,7 @@ def program(ctx): ) if answer != "done": ctx.blocked("the environment fix was not applied") - recheck = ctx.run_command("recheck-python", "python3 --version", timeout_seconds=60) + recheck = ctx.run_command("recheck-python", "python3 --version || python --version", timeout_seconds=60) ctx.require(recheck.exit_code == 0, "the environment probe passes after the fix", recheck) return {"healthy": True, "fixed": True} @@ -44,3 +45,4 @@ def program(ctx): define_skill(program) +# README_EXAMPLE_END diff --git a/packaging/assemble.mjs b/packaging/assemble.mjs index ba395c8..865a709 100644 --- a/packaging/assemble.mjs +++ b/packaging/assemble.mjs @@ -13,6 +13,10 @@ export function isPackageVersion(value) { return stableVersion.test(value) || canaryVersion.test(value); } +export function npmReadme(readme) { + return readme.replace(/\s*[\s\S]*?/g, ""); +} + function parseArgs(argv) { const values = {}; for (let index = 0; index < argv.length; index += 2) values[argv[index]?.replace(/^--/, "")] = argv[index + 1]; @@ -51,11 +55,12 @@ async function assembleNpm({ version, binaries, output }) { const main = join(npm, "yield"); await cp(join(root, "sdk/typescript"), main, { recursive: true, filter: (source) => !source.includes("node_modules") && !source.includes("/dist") }); await mkdir(join(main, "assets"), { recursive: true }); - await Promise.all([ - cp(join(root, "README.md"), join(main, "README.md")), + const [readme] = await Promise.all([ + readFile(join(root, "README.md"), "utf8"), cp(join(root, "LICENSE"), join(main, "LICENSE")), cp(join(root, "assets/yield-mark.svg"), join(main, "assets/yield-mark.svg")), ]); + await writeFile(join(main, "README.md"), npmReadme(readme)); const packageJson = await json(join(main, "package.json")); packageJson.version = version; packageJson.publishConfig = { diff --git a/packaging/assemble.test.mjs b/packaging/assemble.test.mjs index 75d4dfb..8548a3d 100644 --- a/packaging/assemble.test.mjs +++ b/packaging/assemble.test.mjs @@ -44,7 +44,10 @@ test("assembles one public npm package and six matching npm and Python runtimes" Object.fromEntries(targets.map((target) => [npmPackage(target), "1.2.3"])), ); const assembledReadme = await readFile(join(output, "npm/yield/README.md"), "utf8"); - assert.equal(assembledReadme, await readFile(join(import.meta.dirname, "../README.md"), "utf8")); + const repositoryReadme = await readFile(join(import.meta.dirname, "../README.md"), "utf8"); + assert.match(repositoryReadme, /pypi\.org\/project\/yieldskill/); + assert.match(assembledReadme, /npmjs\.com\/package\/@operatorstack\/yield/); + assert.doesNotMatch(assembledReadme, /pypi\.org|PyPI version|npm-exclude/); assert.equal( await readFile(join(output, "npm/yield/assets/yield-mark.svg"), "utf8"), await readFile(join(import.meta.dirname, "../assets/yield-mark.svg"), "utf8"), diff --git a/scripts/readme.test.mjs b/scripts/readme.test.mjs index 3b662f5..21d8b6e 100644 --- a/scripts/readme.test.mjs +++ b/scripts/readme.test.mjs @@ -31,6 +31,56 @@ test("README release example matches the tested TypeScript source", async () => assert.equal(readmeProgram, sourceMatch[1].trim()); }); +test("Python README example matches the tested environment doctor", async () => { + const [readme, source] = await Promise.all([ + text("sdk/python/README.md"), + text("examples/env-doctor/main.py"), + ]); + + const readmeMatch = readme.match( + /\s*```python\n([\s\S]*?)\n```\s*/, + ); + assert.ok(readmeMatch, "Python README example markers are missing"); + + const sourceMatch = source.match( + /# README_EXAMPLE_START\n([\s\S]*?)\n# README_EXAMPLE_END/, + ); + assert.ok(sourceMatch, "Python source example markers are missing"); + + const readmeProgram = readmeMatch[1] + .replace(/^from yieldskill import define_skill\n+/, "") + .trim(); + assert.equal(readmeProgram, sourceMatch[1].trim()); +}); + +test("Python README presents a public five-step workflow", async () => { + const readme = await text("sdk/python/README.md"); + const headings = [ + "### 1. Install Yield", + "### 2. Create the workflow", + "### 3. Test the workflow", + "### 4. Register the skill", + "### 5. Run the skill", + ]; + + let previous = -1; + for (const heading of headings) { + const current = readme.indexOf(heading); + assert.ok(current > previous, `${heading} is missing or out of order`); + previous = current; + } + + assert.match(readme, /python -m pip install yieldskill/); + assert.match(readme, /python -m yieldskill init skills\/env-doctor/); + assert.match(readme, /python -m yieldskill doctor skills\/env-doctor --test/); + assert.match(readme, /python -m yieldskill register skills\/env-doctor/); + assert.match(readme, /^\/env-doctor$/m); + assert.match(readme, /https:\/\/github\.com\/operatorstack\/yield\/blob\/main\/docs\/reference\/cli\.md/); + assert.doesNotMatch(readme, /get\.operatorstack\.systems\/pip/); + assert.doesNotMatch(readme, /npmjs\.com|npm version/); + assert.doesNotMatch(readme, /(?:href|src)="(?!https:\/\/)/); +}); + test("README agent claims match the pinned registry", async () => { const [readme, registryText] = await Promise.all([ text("README.md"), @@ -132,3 +182,12 @@ test("README and quickstart use the public documentation and package registries" assert.match(quickstart, /^\/review$/m); assert.match(agentSetup, /^## Run the registered skill$/m); }); + +test("root README links survive npm package rendering", async () => { + const readme = await text("README.md"); + assert.match(readme, /https:\/\/github\.com\/operatorstack\/yield\/blob\/main\/docs\/skill-workflows\.md/); + assert.match(readme, /https:\/\/github\.com\/operatorstack\/yield\/blob\/main\/evals\/README\.md/); + assert.match(readme, /https:\/\/github\.com\/operatorstack\/yield\/tree\/main\/examples\/library\//); + assert.doesNotMatch(readme, /\]\((?!https?:\/\/|#|mailto:)[^)]+\)/); + assert.doesNotMatch(readme, /href="(?!https?:\/\/|#|mailto:)[^"]+"/); +}); diff --git a/sdk/python/README.md b/sdk/python/README.md index 29014ad..7bbe555 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -1,51 +1,224 @@ -# yieldskill — Yield skill workflow SDK for Python +

+ + Yield + +

-The Python implementation of the yield.v1 SDK execution contract (see -`ir/README.md`). The import name is `yieldskill` because `yield` is a -Python keyword. +

Yield for Python

-Create a virtual environment before installation: +

Move repeatable coding-agent instructions from words into Python.

+ +

+ Build typed, resumable workflows that stay beside the code they operate on. +

+ +

+ PyPI version + Python versions + Build status + MIT license +

+ +

+ Website · + Documentation · + PyPI · + GitHub +

+ +The package name and import name are both `yieldskill`. Python reserves +`yield` as a keyword. + +## Build a Python skill in five steps + +### 1. Install Yield + +Yield supports Python 3.10 or later on macOS, Linux, and Windows. Create a +virtual environment and install the public package: ```bash -# macOS and Linux python3 -m venv .venv source .venv/bin/activate python -m pip install yieldskill +python -m yieldskill --version ``` +On Windows PowerShell: + ```powershell -# Windows PowerShell py -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install yieldskill +python -m yieldskill --version ``` -[Public PyPI releases](https://pypi.org/project/yieldskill/) use trusted -publishing. Every wheel contains the matching Yield runtime for its platform. +Each wheel contains the matching `yskill` runtime for its platform. You do not +need Go, Node.js, or a separate CLI installation. + +### 2. Create the workflow -The launcher preserves the selected Python environment for `RunCommand`, even -when an adapter starts that interpreter without activating the environment. +Create a Python workflow inside your repository: +```bash +python -m yieldskill init skills/env-doctor \ + --language python \ + --description "Check the Python environment and explain the required fix." +``` + +Replace `skills/env-doctor/main.py` with this tested workflow: + + ```python from yieldskill import define_skill def program(ctx): - answer = ctx.ask_user("confirm-start", "Ready to start?") - if answer != "yes": - ctx.refused("user declined to start") - tests = ctx.run_command("run-tests", "true", timeout_seconds=60) - ctx.require(tests.exit_code == 0, "the test command passes", tests) - return {"status": "ok"} + probe = ctx.run_command("probe-python", "python3 --version || python --version", timeout_seconds=60) + + diagnosis = ctx.agent_task( + "diagnose", + "Given the probe output, is this environment healthy for the project? " + "If not, state the single most likely fix.", + context={"exit_code": probe.exit_code, "stdout": probe.stdout, "stderr": probe.stderr}, + schema={ + "type": "object", + "required": ["healthy"], + "properties": { + "healthy": {"type": "boolean"}, + "fix_hint": {"type": "string"}, + }, + }, + ) + + if not diagnosis["healthy"]: + answer = ctx.ask_user( + "apply-fix", + f"The environment needs a fix: {diagnosis.get('fix_hint', 'unknown')}. Apply it now and reply done.", + options=[{"value": "done"}, {"value": "skip"}], + ) + if answer != "done": + ctx.blocked("the environment fix was not applied") + recheck = ctx.run_command("recheck-python", "python3 --version || python --version", timeout_seconds=60) + ctx.require(recheck.exit_code == 0, "the environment probe passes after the fix", recheck) + return {"healthy": True, "fixed": True} + + ctx.require(probe.exit_code == 0, "the environment probe passes", probe) + return {"healthy": True, "fixed": False} + define_skill(program) ``` + + +The generated `skill.json` declares Python as the runner. The generated +`SKILL.md` tells a coding agent how to start and resume the workflow. -Declare the runner in the skill's `skill.json`: +### 3. Test the workflow + +Use deterministic fixture responses during tests. Save this as +`skills/env-doctor/fixtures/responses.json`: ```json -{ "run": ["python3", "main.py"] } +{ + "diagnose": { "healthy": true } +} +``` + +Then test the workflow: + +```bash +python -m yieldskill doctor skills/env-doctor --test ``` -Programs must be deterministic between yields — same journal, same -operations, every execution. Clocks, RNGs, and filesystem reads are side -effects: cross them through a yielded operation or leave them out. +Yield runs commands for real and supplies agent and user responses from the +fixture. A successful test reaches `completed` without leaving a run journal. + +### 4. Register the skill + +Registration lets installed coding agents discover the workflow: + +```bash +python -m yieldskill register skills/env-doctor +``` + +Select the verified agents explicitly when you do not want automatic +detection: + +```bash +python -m yieldskill register skills/env-doctor \ + --agent cursor,codex,claude-code +``` + +The generated adapters point back to `skills/env-doctor`. They do not copy the +workflow or install its dependencies again. + +### 5. Run the skill + +Start a new coding-agent session so it discovers the registered skill. Where +slash skills are supported, run: + +```text +/env-doctor +``` + +Otherwise, ask the agent in plain language: + +```text +Use the env-doctor skill to check this project. +``` + +The agent follows the adapter, starts the canonical Python workflow, and asks +for each required agent or user response. + +## How Yield runs and resumes + +1. Your Python function emits one typed operation. +2. Yield records the request and exits. It does not run a daemon. +3. The coding agent, user, or CLI supplies the result. +4. Yield replays the function from its journal until it reaches the next + operation. + +Replay must produce the same operation sequence. Yield reports divergence +instead of giving a recorded response to a different operation. + +| Python primitive | Purpose | +|---|---| +| `ctx.run_command()` | Execute a command and record its exit code and output. | +| `ctx.agent_task()` | Ask the coding agent for schema-valid JSON. | +| `ctx.ask_user()` | Request an explicit human decision. | +| `ctx.require()` | Bind a required claim to recorded evidence. | +| `ctx.blocked()` / `ctx.refused()` | Stop honestly when work cannot or must not continue. | + +See the [primitive guides](https://yield.operatorstack.systems/docs/primitives/) +and [CLI reference](https://github.com/operatorstack/yield/blob/main/docs/reference/cli.md) +for the complete contract. + +## Guarantees and limits + +Yield provides deterministic control flow, typed requests and responses, +persistent run state, replay with divergence detection, stale and duplicate +response rejection, and evidence-bound completion. + +Schema validity is not truth. Yield cannot prove that a coding agent performed +only the requested work. `run_command` is different: the Yield CLI executes the +command, so its recorded exit code and output are observed facts. + +Programs must remain deterministic between operations. Do not read clocks, +random values, environment variables, or changing files to choose the next +operation. Cross those boundaries through a Yield operation instead. + +Yield is not a daemon, hosted runtime, workflow DSL, marketplace, coding-agent +loop, multi-agent orchestrator, or security sandbox. + +## Coding agents and source + +Cursor, Codex, and Claude Code are verified integrations. Yield also provides +registry-backed project paths for other coding agents; those paths are not +presented as end-to-end verified. + +- [Read the documentation](https://yield.operatorstack.systems/docs/) +- [Explore tested examples](https://github.com/operatorstack/yield/tree/main/examples) +- [View the Python source](https://github.com/operatorstack/yield/tree/main/sdk/python) +- [Report an issue](https://github.com/operatorstack/yield/issues) + +Yield is available under the +[MIT license](https://github.com/operatorstack/yield/blob/main/LICENSE).