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
11 changes: 11 additions & 0 deletions .github/workflows/yield-lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ jobs:
- name: Build and smoke-test the packed TypeScript SDK
working-directory: labs/22-yield/yield/sdk/typescript
run: |
npm test
npm run build
tarball="$RUNNER_TEMP/$(npm pack --silent --pack-destination "$RUNNER_TEMP")"
smoke_dir="$(mktemp -d)"
cd "$smoke_dir"
npm init -y >/dev/null
npm install "$tarball" >/dev/null
node --input-type=module -e 'import { defineSkill } from "@operatorstack/yield"; if (typeof defineSkill !== "function") process.exit(1)'
- name: Verify package launchers and release metadata
working-directory: labs/22-yield/yield
run: |
node --test packaging/*.test.mjs
python -m unittest discover -s sdk/python -p 'test_*.py'
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
Expand Down Expand Up @@ -99,6 +105,11 @@ jobs:
npm init -y >/dev/null
npm install "$tarball" >/dev/null
node --input-type=module -e 'import { defineSkill } from "@operatorstack/yield"; if (typeof defineSkill !== "function") process.exit(1)'
- name: Verify projected package launchers and release metadata
working-directory: ${{ runner.temp }}/yield-projected
run: |
node --test packaging/*.test.mjs
python -m unittest discover -s sdk/python -p 'test_*.py'
- name: Build and test the projected module (incl. 4-language conformance)
working-directory: ${{ runner.temp }}/yield-projected
env:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# One package per language

- Install one TypeScript, Python, Go, or Rust package to get both the SDK and
the matching `yskill` runtime.
- Add `yskill --version`, `yskill version`, and language-aware `yskill init`
scaffolds pinned to the installed version.
- Carry immutable Go runtimes for macOS, Linux, and Windows on amd64 and arm64.
- Fail clearly on unsupported or incomplete installations. The wrappers never
download another runtime or search `PATH`.
- Verify the package launchers, runtime checksums, and shared IR behavior before
publishing the public packages.
29 changes: 27 additions & 2 deletions labs/22-yield/public-readme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ and moves the part prose loses under context pressure — order, branching,
retries, approval, state, completion — into a deterministic program. The
model keeps reasoning, exploration, editing, and judgment.

## Install

Choose one language package. It includes the SDK and the matching `yskill`
runtime.

```bash
# TypeScript
npm install @operatorstack/yield --registry=https://get.operatorstack.systems/npm/
npm exec -- yskill --version

# Python
python -m pip install yieldskill --index-url https://get.operatorstack.systems/pip/simple/
python -m yieldskill --version

# Go
GOPROXY=https://get.operatorstack.systems/go,direct \
go install github.com/operatorstack/yield/cmd/yskill@latest
yskill --version

# Rust
cargo install yieldskill \
--index sparse+https://get.operatorstack.systems/cargo/index/ --locked
yskill --version
```

## How it works

Deterministic re-execution: on every run/resume, `yskill` re-executes the
Expand All @@ -25,7 +50,7 @@ and the process exits — no daemon. A replayed step that produces a
different operation than the journal recorded is a divergence and fails
the run loudly; it never silently forks.

- **`yskill` (supervisor)** owns the append-only run log
- **`yskill`** owns the append-only run log
(`.yield/runs/<id>.jsonl`), sequence and digest binding, response
validation, and every refusal (stale, duplicate, wrong-run,
schema-invalid, digest-mismatch, completion-unproven).
Expand All @@ -45,7 +70,7 @@ Five primitives, two exits:
## Four languages, one protocol

Write the skill program in Go, TypeScript, Python, or Rust — the
supervisor doesn't care. Every SDK implements the same certified
runtime doesn't care. Every SDK implements the same certified
execution contract over the canonical `ir/yield.v1` schemas, and the
conformance suite (`internal/conformance`) runs the *same program* in all
four languages and asserts identical observable protocol behavior.
Expand Down
98 changes: 23 additions & 75 deletions labs/22-yield/yield/cmd/yskill/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"

"github.com/operatorstack/yield/internal/engine"
Expand All @@ -19,14 +21,30 @@ const usage = `yskill — turn SKILL.md workflows into resumable programs

Usage:
yskill init <dir> scaffold a skill (or wrap an existing prose skill)
[--language typescript|python|go|rust]
yskill run <skill-dir> [--input file] start a run; prints the first operation envelope
yskill resume <run-id> --response file feed a response; prints the next operation
[--skill dir] [--accept-new-digest]
yskill inspect <run-id> [--skill dir] print the run's event log
yskill replay <run-id> [--skill dir] re-derive the run from its log; verify determinism
yskill test <skill-dir> run the skill against fixtures/responses.json
yskill version print the runtime version and target
`

// version is set from the release tag with -ldflags "-X main.version=<version>".
var version = "dev"
var readBuildInfo = debug.ReadBuildInfo

func runtimeVersion() string {
if version != "" && version != "dev" {
return strings.TrimPrefix(version, "v")
}
if info, ok := readBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
return strings.TrimPrefix(info.Main.Version, "v")
}
return "dev"
}

func main() {
if len(os.Args) < 2 {
fmt.Fprint(os.Stderr, usage)
Expand All @@ -46,6 +64,8 @@ func main() {
err = cmdReplay(os.Args[2:])
case "test":
err = cmdTest(os.Args[2:])
case "version", "--version":
fmt.Printf("yskill %s %s/%s\n", runtimeVersion(), runtime.GOOS, runtime.GOARCH)
case "help", "-h", "--help":
fmt.Print(usage)
default:
Expand Down Expand Up @@ -258,84 +278,12 @@ func compact(raw json.RawMessage) string {
func cmdInit(args []string) error {
fs := flag.NewFlagSet("init", flag.ExitOnError)
sdkPath := fs.String("sdk", "", "filesystem path to the yield module (written as a go.mod replace directive)")
if err := fs.Parse(args); err != nil {
language := fs.String("language", defaultLanguage(), "workflow language: typescript, python, go, or rust")
if err := parseOnePositional(fs, args); err != nil {
return err
}
if fs.NArg() != 1 {
return fmt.Errorf("init takes exactly one directory")
}
dir := fs.Arg(0)
name := filepath.Base(dir)
if err := os.MkdirAll(filepath.Join(dir, "fixtures"), 0o755); err != nil {
return err
}
writeIfAbsent := func(rel, content string) error {
path := filepath.Join(dir, rel)
if _, err := os.Stat(path); err == nil {
fmt.Printf("init: %s exists, preserved\n", rel)
return nil
}
return os.WriteFile(path, []byte(content), 0o644)
}
if err := writeIfAbsent("SKILL.md", fmt.Sprintf(skillMD, name)); err != nil {
return err
}
if err := writeIfAbsent("main.go", mainGo); err != nil {
return err
}
if err := writeIfAbsent("fixtures/responses.json", "{\n \"confirm-start\": {\"value\": \"yes\"}\n}\n"); err != nil {
return err
}
gomod := fmt.Sprintf("module %s\n\ngo 1.26.5\n\nrequire github.com/operatorstack/yield v0.0.0\n", name)
if *sdkPath != "" {
gomod += fmt.Sprintf("\nreplace github.com/operatorstack/yield => %s\n", *sdkPath)
} else {
gomod += "\n// Point this at your yield checkout:\n// replace github.com/operatorstack/yield => ../path/to/yield\n"
}
if err := writeIfAbsent("go.mod", gomod); err != nil {
return err
}
fmt.Printf("init: skill %q scaffolded in %s\n", name, dir)
return nil
return scaffoldSkill(fs.Arg(0), *language, *sdkPath)
}

const skillMD = `---
name: %s
description: TODO — one line on what this skill does.
---

Run:

yskill run .

Follow each returned operation exactly.

- ` + "`ask_user`" + `: ask the user using the host's normal interface.
- ` + "`agent_task`" + `: perform the task and return schema-valid JSON.
- ` + "`run_command`" + `: yskill executes it itself; you will not see this kind.

Resume the run after each operation:

yskill resume <run-id> --response response.json

Do not skip an operation or invent its response.
`

const mainGo = `package main

import (
"github.com/operatorstack/yield/sdk/yield"
)

func main() {
yield.Main(func(ctx *yield.Context) (yield.Outcome, error) {
answer := ctx.AskUser("confirm-start", "Ready to start?")
if answer != "yes" {
return yield.Outcome{}, ctx.Refused("user declined to start")
}
tests := ctx.RunCommand("run-tests", "true", 60)
ctx.Require(tests.ExitCode == 0, "the test command passes", tests)
return ctx.Complete(map[string]string{"status": "ok"})
})
}
`
118 changes: 118 additions & 0 deletions labs/22-yield/yield/cmd/yskill/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@ package main

import (
"flag"
"os"
"path/filepath"
"runtime/debug"
"strings"
"testing"
)

func TestRuntimeVersionUsesGoModuleVersion(t *testing.T) {
previousVersion := version
previousReadBuildInfo := readBuildInfo
version = "dev"
readBuildInfo = func() (*debug.BuildInfo, bool) {
return &debug.BuildInfo{Main: debug.Module{Version: "v1.2.3"}}, true
}
t.Cleanup(func() {
version = previousVersion
readBuildInfo = previousReadBuildInfo
})
if got := runtimeVersion(); got != "1.2.3" {
t.Fatalf("runtimeVersion = %q, want 1.2.3", got)
}
}

func TestParseOnePositionalAllowsDocumentedFlagOrder(t *testing.T) {
fs := flag.NewFlagSet("resume", flag.ContinueOnError)
response := fs.String("response", "", "response file")
Expand All @@ -30,3 +50,101 @@ func TestParseOnePositionalKeepsFlagFirstOrder(t *testing.T) {
t.Fatalf("args = %q response = %q", fs.Args(), *response)
}
}

func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
previousVersion := version
version = "0.1.9"
t.Cleanup(func() { version = previousVersion })

tests := []struct {
language string
files []string
command string
pin string
}{
{"typescript", []string{"main.ts", "package.json", "skill.json"}, "npm exec -- yskill run .", `"@operatorstack/yield": "0.1.9"`},
{"python", []string{"main.py", "requirements.txt", "skill.json"}, "python -m yieldskill run .", "yieldskill==0.1.9"},
{"go", []string{"main.go", "go.mod"}, "yskill run .", "github.com/operatorstack/yield v0.1.9"},
{"rust", []string{"src/main.rs", "Cargo.toml", ".cargo/config.toml", "skill.json"}, "yskill run .", `version = "=0.1.9"`},
}
for _, tt := range tests {
t.Run(tt.language, func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "My Skill")
if err := scaffoldSkill(dir, tt.language, ""); err != nil {
t.Fatal(err)
}
for _, rel := range append(tt.files, "SKILL.md", "fixtures/responses.json") {
if _, err := os.Stat(filepath.Join(dir, filepath.FromSlash(rel))); err != nil {
t.Fatalf("%s: %v", rel, err)
}
}
skill := readTestFile(t, filepath.Join(dir, "SKILL.md"))
if !strings.Contains(skill, tt.command) {
t.Fatalf("SKILL.md does not contain %q:\n%s", tt.command, skill)
}
var manifest string
switch tt.language {
case "typescript":
manifest = readTestFile(t, filepath.Join(dir, "package.json"))
case "python":
manifest = readTestFile(t, filepath.Join(dir, "requirements.txt"))
case "go":
manifest = readTestFile(t, filepath.Join(dir, "go.mod"))
case "rust":
manifest = readTestFile(t, filepath.Join(dir, "Cargo.toml"))
}
if !strings.Contains(manifest, tt.pin) {
t.Fatalf("manifest does not contain %q:\n%s", tt.pin, manifest)
}
})
}
}

func TestPythonScaffoldUsesInvokingInterpreter(t *testing.T) {
previousVersion := version
version = "0.1.9"
t.Cleanup(func() { version = previousVersion })
t.Setenv("YIELD_PYTHON", "/opt/yield/.venv/bin/python")
dir := filepath.Join(t.TempDir(), "python-skill")
if err := scaffoldSkill(dir, "python", ""); err != nil {
t.Fatal(err)
}
skill := readTestFile(t, filepath.Join(dir, "skill.json"))
if !strings.Contains(skill, `"/opt/yield/.venv/bin/python"`) {
t.Fatalf("skill.json does not use the invoking interpreter: %s", skill)
}
}

func TestScaffoldSkillPreservesExistingSkillAndRejectsUnknownLanguage(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("keep me\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := scaffoldSkill(dir, "typescript", ""); err != nil {
t.Fatal(err)
}
if got := readTestFile(t, filepath.Join(dir, "SKILL.md")); got != "keep me\n" {
t.Fatalf("existing SKILL.md changed: %q", got)
}
if err := scaffoldSkill(t.TempDir(), "java", ""); err == nil || !strings.Contains(err.Error(), "unsupported language") {
t.Fatalf("unknown language error = %v", err)
}
}

func TestPackageVersionFallsBackForDevelopmentBuilds(t *testing.T) {
previousVersion := version
version = "dev"
t.Cleanup(func() { version = previousVersion })
if got := packageVersion(); got != "0.0.0" {
t.Fatalf("packageVersion = %q", got)
}
}

func readTestFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return string(b)
}
Loading
Loading