Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/readme-code-to-registered-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@operatorstack/yield": patch
---

Explain how workflow code becomes a registered coding-agent skill and use a cleaner README mark.
74 changes: 66 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://yield.operatorstack.systems/">
<img src="https://yield.operatorstack.systems/favicon.svg" width="96" height="96" alt="Yield" />
<img src="assets/yield-mark.svg" width="96" height="96" alt="Yield" />
</a>
</p>

Expand Down Expand Up @@ -49,9 +49,14 @@ import { defineSkill } from "@operatorstack/yield";
type Review = { critical: number; summary: string };

defineSkill((ctx) => {
// Yield runs commands itself and records their output and exit status.
const tests = ctx.runCommand("test", "echo tests-ok", 300);

// A failed requirement stops the workflow and keeps its evidence.
ctx.require(tests.exit_code === 0, "the test command succeeds", tests);

// Review gives TypeScript its compile-time type. The JSON schema checks the
// coding agent's response at runtime before this workflow can continue.
const review = ctx.agentTask<Review>(
"review-release",
"Review this release. Report critical findings and a short summary.",
Expand All @@ -67,12 +72,16 @@ defineSkill((ctx) => {
);
ctx.require(review.critical === 0, "the review has no critical findings", review);

// Yield emits these fixed choices. A supported host may show native controls;
// otherwise the coding agent asks through its normal interface.
const approval = ctx.askUser("approve-publish", "Publish this package?", [
{ value: "yes", label: "Publish" },
{ value: "no", label: "Stop" },
]);
if (approval !== "yes") ctx.refused("the operator declined publication");

// Publishing cannot start before approval. Verification is a separate step,
// so completion requires evidence that the registry contains the release.
const publish = ctx.runCommand("publish", "echo publish-ok", 600);
ctx.require(publish.exit_code === 0, "the publish command succeeds", publish);

Expand All @@ -89,9 +98,11 @@ Replace them with the test, publish, and registry commands for your project.
The complete tested source is in
[`examples/release-checklist`](examples/release-checklist/).

## Install and create a workflow
## Use Yield in four steps

### 1. Install Yield

Install the TypeScript SDK and its repository-local CLI:
Install the TypeScript SDK and its repository-local CLI in your project:

```bash
npm install --save-exact @operatorstack/yield
Expand All @@ -102,22 +113,69 @@ npm exec -- yskill --version
use trusted publishing. The SDK package and all six runtime packages include
SLSA v1 provenance.

Create, test, and register a workflow:
### 2. Create the workflow

```bash
npm exec -- yskill init skills/release \
--language typescript \
--description "Test, review, approve, publish, and verify a package."
```

The command creates one canonical workflow inside your repository:

```text
skills/
└── release/
├── SKILL.md
├── fixtures/
│ ├── responses.json
│ └── test.json
├── main.ts
├── package.json
└── skill.json
```

# Replace the starter with your workflow and fixture.
Replace the starter in `skills/release/main.ts` with your workflow. Update
`skills/release/fixtures/responses.json` with deterministic answers for agent
and user operations.

### 3. Test the workflow

```bash
npm exec -- yskill doctor skills/release --test
```

This runs commands for real and supplies agent and user responses from the
fixture. A successful test reaches `completed` without leaving a run journal.

# Detect installed agents, or pass --agent cursor,codex,claude-code.
### 4. Register and use the skill

Registration is the discovery step. This command detects installed verified
agents and writes a small adapter for each one:

```bash
npm exec -- yskill register skills/release
```

Yield writes small adapters into each coding agent's project skill directory.
It does not copy the workflow or install its dependencies again.
Select verified agents explicitly when you do not want automatic detection:

```bash
npm exec -- yskill register skills/release \
--agent cursor,codex,claude-code
```

If all three are selected, Yield creates these generated files:

```text
.cursor/skills/release/SKILL.md # Cursor
.agents/skills/release/SKILL.md # Codex
.claude/skills/release/SKILL.md # Claude Code
```

The adapters point back to `skills/release`. They do not copy the workflow or
install its dependencies again. Start a new agent session after registration,
then invoke `/release` where slash skills are supported or ask the agent to use
the release skill.

## How Yield runs and resumes

Expand Down
13 changes: 13 additions & 0 deletions assets/yield-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions examples/release-checklist/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import { defineSkill } from "../../sdk/typescript/src/index.ts";
type Review = { critical: number; summary: string };

defineSkill((ctx) => {
// Yield runs commands itself and records their output and exit status.
const tests = ctx.runCommand("test", "echo tests-ok", 300);

// A failed requirement stops the workflow and keeps its evidence.
ctx.require(tests.exit_code === 0, "the test command succeeds", tests);

// Review gives TypeScript its compile-time type. The JSON schema checks the
// coding agent's response at runtime before this workflow can continue.
const review = ctx.agentTask<Review>(
"review-release",
"Review this release. Report critical findings and a short summary.",
Expand All @@ -24,12 +29,16 @@ defineSkill((ctx) => {
);
ctx.require(review.critical === 0, "the review has no critical findings", review);

// Yield emits these fixed choices. A supported host may show native controls;
// otherwise the coding agent asks through its normal interface.
const approval = ctx.askUser("approve-publish", "Publish this package?", [
{ value: "yes", label: "Publish" },
{ value: "no", label: "Stop" },
]);
if (approval !== "yes") ctx.refused("the operator declined publication");

// Publishing cannot start before approval. Verification is a separate step,
// so completion requires evidence that the registry contains the release.
const publish = ctx.runCommand("publish", "echo publish-ok", 600);
ctx.require(publish.exit_code === 0, "the publish command succeeds", publish);

Expand Down
3 changes: 3 additions & 0 deletions packaging/assemble.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ async function assembleNpm({ version, binaries, output }) {
const npm = join(output, "npm");
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")),
cp(join(root, "LICENSE"), join(main, "LICENSE")),
cp(join(root, "assets/yield-mark.svg"), join(main, "assets/yield-mark.svg")),
]);
const packageJson = await json(join(main, "package.json"));
packageJson.version = version;
Expand All @@ -62,6 +64,7 @@ async function assembleNpm({ version, binaries, output }) {
registry: "https://registry.npmjs.org/",
};
packageJson.optionalDependencies = Object.fromEntries(targets.map((target) => [npmPackage(target), version]));
packageJson.files = [...new Set([...(packageJson.files ?? []), "assets"])];
await writeFile(join(main, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);

for (const target of targets) {
Expand Down
5 changes: 5 additions & 0 deletions packaging/assemble.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ test("assembles one public npm package and six matching runtimes", async (t) =>
);
const assembledReadme = await readFile(join(output, "npm/yield/README.md"), "utf8");
assert.equal(assembledReadme, await readFile(join(import.meta.dirname, "../README.md"), "utf8"));
assert.equal(
await readFile(join(output, "npm/yield/assets/yield-mark.svg"), "utf8"),
await readFile(join(import.meta.dirname, "../assets/yield-mark.svg"), "utf8"),
);
assert.ok(main.files.includes("assets"));
assert.match(assembledReadme, /<h1 align="center">Yield<\/h1>/);
assert.match(await readFile(join(output, "npm/yield/LICENSE"), "utf8"), /MIT License/);

Expand Down
49 changes: 47 additions & 2 deletions scripts/readme.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,53 @@ test("README agent claims match the pinned registry", async () => {
assert.doesNotMatch(readme, /Agent Plugins and Yield/);
});

test("README uses the compact Yield mark", async () => {
test("README presents the workflow as four ordered steps", async () => {
const readme = await text("README.md");
assert.match(readme, /https:\/\/yield\.operatorstack\.systems\/favicon\.svg/);
const headings = [
"### 1. Install Yield",
"### 2. Create the workflow",
"### 3. Test the workflow",
"### 4. Register and use 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, /npm exec -- yskill doctor skills\/release --test/);
assert.match(readme, /npm exec -- yskill register skills\/release/);
assert.match(readme, /Registration is the discovery step\./);
});

test("README adapter paths match every verified agent", async () => {
const [readme, registryText] = await Promise.all([
text("README.md"),
text("cmd/yskill/registry/agents.json"),
]);
const registry = JSON.parse(registryText);

for (const agent of registry.agents.filter((entry) => entry.tier === "verified")) {
const adapter = `${agent.project_dir}/release/SKILL.md`;
assert.match(readme, new RegExp(adapter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
}

assert.match(readme, /--agent cursor,codex,claude-code/);
assert.match(readme, /If all three are selected/);
});

test("README uses the edge-cropped Yield mark", async () => {
const [readme, mark] = await Promise.all([
text("README.md"),
text("assets/yield-mark.svg"),
]);
assert.match(
readme,
/<img src="assets\/yield-mark\.svg" width="96" height="96" alt="Yield" \/>/,
);
assert.doesNotMatch(readme, /apple-touch-icon\.png/);
assert.match(mark, /viewBox="0 0 60 60"/);
assert.match(mark, /<rect x="1" y="1" width="58" height="58"/);
});