Skip to content

Commit df6841f

Browse files
committed
feat(yield): bundle runtime with each language package
Disclosure-Reviewed: reviewed — public-safe only, private facet kept out of this commit
1 parent c8b753a commit df6841f

22 files changed

Lines changed: 878 additions & 98 deletions

File tree

.github/workflows/yield-lab.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@ jobs:
3030
- name: Build and smoke-test the packed TypeScript SDK
3131
working-directory: labs/22-yield/yield/sdk/typescript
3232
run: |
33+
npm test
3334
npm run build
3435
tarball="$RUNNER_TEMP/$(npm pack --silent --pack-destination "$RUNNER_TEMP")"
3536
smoke_dir="$(mktemp -d)"
3637
cd "$smoke_dir"
3738
npm init -y >/dev/null
3839
npm install "$tarball" >/dev/null
3940
node --input-type=module -e 'import { defineSkill } from "@operatorstack/yield"; if (typeof defineSkill !== "function") process.exit(1)'
41+
- name: Verify package launchers and release metadata
42+
working-directory: labs/22-yield/yield
43+
run: |
44+
node --test packaging/*.test.mjs
45+
python -m unittest discover -s sdk/python -p 'test_*.py'
4046
- uses: Swatinem/rust-cache@v2
4147
with:
4248
workspaces: |
@@ -99,6 +105,11 @@ jobs:
99105
npm init -y >/dev/null
100106
npm install "$tarball" >/dev/null
101107
node --input-type=module -e 'import { defineSkill } from "@operatorstack/yield"; if (typeof defineSkill !== "function") process.exit(1)'
108+
- name: Verify projected package launchers and release metadata
109+
working-directory: ${{ runner.temp }}/yield-projected
110+
run: |
111+
node --test packaging/*.test.mjs
112+
python -m unittest discover -s sdk/python -p 'test_*.py'
102113
- name: Build and test the projected module (incl. 4-language conformance)
103114
working-directory: ${{ runner.temp }}/yield-projected
104115
env:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# One package per language
2+
3+
- Install one TypeScript, Python, Go, or Rust package to get both the SDK and
4+
the matching `yskill` runtime.
5+
- Add `yskill --version`, `yskill version`, and language-aware `yskill init`
6+
scaffolds pinned to the installed version.
7+
- Carry immutable Go runtimes for macOS, Linux, and Windows on amd64 and arm64.
8+
- Fail clearly on unsupported or incomplete installations. The wrappers never
9+
download another runtime or search `PATH`.
10+
- Verify the package launchers, runtime checksums, and shared IR behavior before
11+
publishing the public packages.

labs/22-yield/public-readme/README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@ and moves the part prose loses under context pressure — order, branching,
1616
retries, approval, state, completion — into a deterministic program. The
1717
model keeps reasoning, exploration, editing, and judgment.
1818

19+
## Install
20+
21+
Choose one language package. It includes the SDK and the matching `yskill`
22+
runtime.
23+
24+
```bash
25+
# TypeScript
26+
npm install @operatorstack/yield --registry=https://get.operatorstack.systems/npm/
27+
npm exec -- yskill --version
28+
29+
# Python
30+
python -m pip install yieldskill --index-url https://get.operatorstack.systems/pip/simple/
31+
python -m yieldskill --version
32+
33+
# Go
34+
GOPROXY=https://get.operatorstack.systems/go,direct \
35+
go install github.com/operatorstack/yield/cmd/yskill@latest
36+
yskill --version
37+
38+
# Rust
39+
cargo install yieldskill \
40+
--index sparse+https://get.operatorstack.systems/cargo/index/ --locked
41+
yskill --version
42+
```
43+
1944
## How it works
2045

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

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

4772
Write the skill program in Go, TypeScript, Python, or Rust — the
48-
supervisor doesn't care. Every SDK implements the same certified
73+
runtime doesn't care. Every SDK implements the same certified
4974
execution contract over the canonical `ir/yield.v1` schemas, and the
5075
conformance suite (`internal/conformance`) runs the *same program* in all
5176
four languages and asserts identical observable protocol behavior.

labs/22-yield/yield/cmd/yskill/main.go

Lines changed: 23 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"runtime"
13+
"runtime/debug"
1214
"strings"
1315

1416
"github.com/operatorstack/yield/internal/engine"
@@ -19,14 +21,30 @@ const usage = `yskill — turn SKILL.md workflows into resumable programs
1921
2022
Usage:
2123
yskill init <dir> scaffold a skill (or wrap an existing prose skill)
24+
[--language typescript|python|go|rust]
2225
yskill run <skill-dir> [--input file] start a run; prints the first operation envelope
2326
yskill resume <run-id> --response file feed a response; prints the next operation
2427
[--skill dir] [--accept-new-digest]
2528
yskill inspect <run-id> [--skill dir] print the run's event log
2629
yskill replay <run-id> [--skill dir] re-derive the run from its log; verify determinism
2730
yskill test <skill-dir> run the skill against fixtures/responses.json
31+
yskill version print the runtime version and target
2832
`
2933

34+
// version is set from the release tag with -ldflags "-X main.version=<version>".
35+
var version = "dev"
36+
var readBuildInfo = debug.ReadBuildInfo
37+
38+
func runtimeVersion() string {
39+
if version != "" && version != "dev" {
40+
return strings.TrimPrefix(version, "v")
41+
}
42+
if info, ok := readBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
43+
return strings.TrimPrefix(info.Main.Version, "v")
44+
}
45+
return "dev"
46+
}
47+
3048
func main() {
3149
if len(os.Args) < 2 {
3250
fmt.Fprint(os.Stderr, usage)
@@ -46,6 +64,8 @@ func main() {
4664
err = cmdReplay(os.Args[2:])
4765
case "test":
4866
err = cmdTest(os.Args[2:])
67+
case "version", "--version":
68+
fmt.Printf("yskill %s %s/%s\n", runtimeVersion(), runtime.GOOS, runtime.GOARCH)
4969
case "help", "-h", "--help":
5070
fmt.Print(usage)
5171
default:
@@ -258,84 +278,12 @@ func compact(raw json.RawMessage) string {
258278
func cmdInit(args []string) error {
259279
fs := flag.NewFlagSet("init", flag.ExitOnError)
260280
sdkPath := fs.String("sdk", "", "filesystem path to the yield module (written as a go.mod replace directive)")
261-
if err := fs.Parse(args); err != nil {
281+
language := fs.String("language", defaultLanguage(), "workflow language: typescript, python, go, or rust")
282+
if err := parseOnePositional(fs, args); err != nil {
262283
return err
263284
}
264285
if fs.NArg() != 1 {
265286
return fmt.Errorf("init takes exactly one directory")
266287
}
267-
dir := fs.Arg(0)
268-
name := filepath.Base(dir)
269-
if err := os.MkdirAll(filepath.Join(dir, "fixtures"), 0o755); err != nil {
270-
return err
271-
}
272-
writeIfAbsent := func(rel, content string) error {
273-
path := filepath.Join(dir, rel)
274-
if _, err := os.Stat(path); err == nil {
275-
fmt.Printf("init: %s exists, preserved\n", rel)
276-
return nil
277-
}
278-
return os.WriteFile(path, []byte(content), 0o644)
279-
}
280-
if err := writeIfAbsent("SKILL.md", fmt.Sprintf(skillMD, name)); err != nil {
281-
return err
282-
}
283-
if err := writeIfAbsent("main.go", mainGo); err != nil {
284-
return err
285-
}
286-
if err := writeIfAbsent("fixtures/responses.json", "{\n \"confirm-start\": {\"value\": \"yes\"}\n}\n"); err != nil {
287-
return err
288-
}
289-
gomod := fmt.Sprintf("module %s\n\ngo 1.26.5\n\nrequire github.com/operatorstack/yield v0.0.0\n", name)
290-
if *sdkPath != "" {
291-
gomod += fmt.Sprintf("\nreplace github.com/operatorstack/yield => %s\n", *sdkPath)
292-
} else {
293-
gomod += "\n// Point this at your yield checkout:\n// replace github.com/operatorstack/yield => ../path/to/yield\n"
294-
}
295-
if err := writeIfAbsent("go.mod", gomod); err != nil {
296-
return err
297-
}
298-
fmt.Printf("init: skill %q scaffolded in %s\n", name, dir)
299-
return nil
288+
return scaffoldSkill(fs.Arg(0), *language, *sdkPath)
300289
}
301-
302-
const skillMD = `---
303-
name: %s
304-
description: TODO — one line on what this skill does.
305-
---
306-
307-
Run:
308-
309-
yskill run .
310-
311-
Follow each returned operation exactly.
312-
313-
- ` + "`ask_user`" + `: ask the user using the host's normal interface.
314-
- ` + "`agent_task`" + `: perform the task and return schema-valid JSON.
315-
- ` + "`run_command`" + `: yskill executes it itself; you will not see this kind.
316-
317-
Resume the run after each operation:
318-
319-
yskill resume <run-id> --response response.json
320-
321-
Do not skip an operation or invent its response.
322-
`
323-
324-
const mainGo = `package main
325-
326-
import (
327-
"github.com/operatorstack/yield/sdk/yield"
328-
)
329-
330-
func main() {
331-
yield.Main(func(ctx *yield.Context) (yield.Outcome, error) {
332-
answer := ctx.AskUser("confirm-start", "Ready to start?")
333-
if answer != "yes" {
334-
return yield.Outcome{}, ctx.Refused("user declined to start")
335-
}
336-
tests := ctx.RunCommand("run-tests", "true", 60)
337-
ctx.Require(tests.ExitCode == 0, "the test command passes", tests)
338-
return ctx.Complete(map[string]string{"status": "ok"})
339-
})
340-
}
341-
`

labs/22-yield/yield/cmd/yskill/main_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,29 @@ package main
22

33
import (
44
"flag"
5+
"os"
6+
"path/filepath"
7+
"runtime/debug"
8+
"strings"
59
"testing"
610
)
711

12+
func TestRuntimeVersionUsesGoModuleVersion(t *testing.T) {
13+
previousVersion := version
14+
previousReadBuildInfo := readBuildInfo
15+
version = "dev"
16+
readBuildInfo = func() (*debug.BuildInfo, bool) {
17+
return &debug.BuildInfo{Main: debug.Module{Version: "v1.2.3"}}, true
18+
}
19+
t.Cleanup(func() {
20+
version = previousVersion
21+
readBuildInfo = previousReadBuildInfo
22+
})
23+
if got := runtimeVersion(); got != "1.2.3" {
24+
t.Fatalf("runtimeVersion = %q, want 1.2.3", got)
25+
}
26+
}
27+
828
func TestParseOnePositionalAllowsDocumentedFlagOrder(t *testing.T) {
929
fs := flag.NewFlagSet("resume", flag.ContinueOnError)
1030
response := fs.String("response", "", "response file")
@@ -30,3 +50,101 @@ func TestParseOnePositionalKeepsFlagFirstOrder(t *testing.T) {
3050
t.Fatalf("args = %q response = %q", fs.Args(), *response)
3151
}
3252
}
53+
54+
func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
55+
previousVersion := version
56+
version = "0.1.9"
57+
t.Cleanup(func() { version = previousVersion })
58+
59+
tests := []struct {
60+
language string
61+
files []string
62+
command string
63+
pin string
64+
}{
65+
{"typescript", []string{"main.ts", "package.json", "skill.json"}, "npm exec -- yskill run .", `"@operatorstack/yield": "0.1.9"`},
66+
{"python", []string{"main.py", "requirements.txt", "skill.json"}, "python -m yieldskill run .", "yieldskill==0.1.9"},
67+
{"go", []string{"main.go", "go.mod"}, "yskill run .", "github.com/operatorstack/yield v0.1.9"},
68+
{"rust", []string{"src/main.rs", "Cargo.toml", ".cargo/config.toml", "skill.json"}, "yskill run .", `version = "=0.1.9"`},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.language, func(t *testing.T) {
72+
dir := filepath.Join(t.TempDir(), "My Skill")
73+
if err := scaffoldSkill(dir, tt.language, ""); err != nil {
74+
t.Fatal(err)
75+
}
76+
for _, rel := range append(tt.files, "SKILL.md", "fixtures/responses.json") {
77+
if _, err := os.Stat(filepath.Join(dir, filepath.FromSlash(rel))); err != nil {
78+
t.Fatalf("%s: %v", rel, err)
79+
}
80+
}
81+
skill := readTestFile(t, filepath.Join(dir, "SKILL.md"))
82+
if !strings.Contains(skill, tt.command) {
83+
t.Fatalf("SKILL.md does not contain %q:\n%s", tt.command, skill)
84+
}
85+
var manifest string
86+
switch tt.language {
87+
case "typescript":
88+
manifest = readTestFile(t, filepath.Join(dir, "package.json"))
89+
case "python":
90+
manifest = readTestFile(t, filepath.Join(dir, "requirements.txt"))
91+
case "go":
92+
manifest = readTestFile(t, filepath.Join(dir, "go.mod"))
93+
case "rust":
94+
manifest = readTestFile(t, filepath.Join(dir, "Cargo.toml"))
95+
}
96+
if !strings.Contains(manifest, tt.pin) {
97+
t.Fatalf("manifest does not contain %q:\n%s", tt.pin, manifest)
98+
}
99+
})
100+
}
101+
}
102+
103+
func TestPythonScaffoldUsesInvokingInterpreter(t *testing.T) {
104+
previousVersion := version
105+
version = "0.1.9"
106+
t.Cleanup(func() { version = previousVersion })
107+
t.Setenv("YIELD_PYTHON", "/opt/yield/.venv/bin/python")
108+
dir := filepath.Join(t.TempDir(), "python-skill")
109+
if err := scaffoldSkill(dir, "python", ""); err != nil {
110+
t.Fatal(err)
111+
}
112+
skill := readTestFile(t, filepath.Join(dir, "skill.json"))
113+
if !strings.Contains(skill, `"/opt/yield/.venv/bin/python"`) {
114+
t.Fatalf("skill.json does not use the invoking interpreter: %s", skill)
115+
}
116+
}
117+
118+
func TestScaffoldSkillPreservesExistingSkillAndRejectsUnknownLanguage(t *testing.T) {
119+
dir := t.TempDir()
120+
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("keep me\n"), 0o644); err != nil {
121+
t.Fatal(err)
122+
}
123+
if err := scaffoldSkill(dir, "typescript", ""); err != nil {
124+
t.Fatal(err)
125+
}
126+
if got := readTestFile(t, filepath.Join(dir, "SKILL.md")); got != "keep me\n" {
127+
t.Fatalf("existing SKILL.md changed: %q", got)
128+
}
129+
if err := scaffoldSkill(t.TempDir(), "java", ""); err == nil || !strings.Contains(err.Error(), "unsupported language") {
130+
t.Fatalf("unknown language error = %v", err)
131+
}
132+
}
133+
134+
func TestPackageVersionFallsBackForDevelopmentBuilds(t *testing.T) {
135+
previousVersion := version
136+
version = "dev"
137+
t.Cleanup(func() { version = previousVersion })
138+
if got := packageVersion(); got != "0.0.0" {
139+
t.Fatalf("packageVersion = %q", got)
140+
}
141+
}
142+
143+
func readTestFile(t *testing.T, path string) string {
144+
t.Helper()
145+
b, err := os.ReadFile(path)
146+
if err != nil {
147+
t.Fatal(err)
148+
}
149+
return string(b)
150+
}

0 commit comments

Comments
 (0)