diff --git a/.changeset/rust-first-run-local-runtime.md b/.changeset/rust-first-run-local-runtime.md new file mode 100644 index 0000000..ab995d2 --- /dev/null +++ b/.changeset/rust-first-run-local-runtime.md @@ -0,0 +1,5 @@ +--- +"@operatorstack/yield": patch +--- + +Pin the installed runtime during Rust and Go workflow creation, and remove the obsolete private Cargo registry check from Rust diagnostics. diff --git a/cmd/yskill/agents.go b/cmd/yskill/agents.go index 533aa15..339805b 100644 --- a/cmd/yskill/agents.go +++ b/cmd/yskill/agents.go @@ -980,9 +980,6 @@ func languageDiagnostics(language, skillDir string) error { if !commandExists("cargo") { return fmt.Errorf("Rust workflow needs the cargo command") } - if _, err := os.Stat(filepath.Join(skillDir, ".cargo", "config.toml")); err != nil { - return fmt.Errorf("Rust workflow needs .cargo/config.toml in the workflow directory: %w", err) - } var missing []string if !commandExists("rustfmt") { missing = append(missing, "rustfmt") diff --git a/cmd/yskill/main_test.go b/cmd/yskill/main_test.go index 88b9cee..02f6e41 100644 --- a/cmd/yskill/main_test.go +++ b/cmd/yskill/main_test.go @@ -398,6 +398,54 @@ func TestLocalGoAndRustScaffoldsKeepTheInvokedRuntime(t *testing.T) { } } +func TestRustScaffoldPinsTheInvokedRuntimeWithoutPrivateRegistryConfig(t *testing.T) { + previousVersion := version + previousExecutable := currentExecutable + previousInspect := inspectRuntimeVersion + version = "0.1.37" + t.Cleanup(func() { + version = previousVersion + currentExecutable = previousExecutable + inspectRuntimeVersion = previousInspect + }) + + repo := t.TempDir() + writeTestFile(t, filepath.Join(repo, ".git"), "gitdir: fixture\n") + t.Chdir(repo) + sourceRoot := t.TempDir() + source := filepath.Join(sourceRoot, "yskill") + writeTestFile(t, source, "packaged runtime") + currentExecutable = func() (string, error) { return source, nil } + inspectRuntimeVersion = func(path string) (string, error) { + if path != source && path != localRuntimePath(repo) { + t.Fatalf("inspected unexpected runtime %s", path) + } + return "0.1.37", nil + } + + dir := filepath.Join("skills", "safe-change") + if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil { + t.Fatal(err) + } + if got := readTestFile(t, localRuntimePath(repo)); got != "packaged runtime" { + t.Fatalf("pinned runtime = %q", got) + } + if got := readTestFile(t, filepath.Join(repo, ".yield", ".gitignore")); got != "*\n" { + t.Fatalf("local state ignore = %q", got) + } + if _, err := os.Stat(filepath.Join(repo, dir, ".cargo", "config.toml")); !os.IsNotExist(err) { + t.Fatalf("Rust scaffold created private registry config: %v", err) + } + if err := languageDiagnostics("rust", dir); err != nil { + t.Fatalf("Rust diagnostics required private registry config: %v", err) + } + skill := readTestFile(t, filepath.Join(repo, dir, "SKILL.md")) + launcher := repositoryRuntimeLauncher(filepath.Join(".yield", "bin", filepath.Base(localRuntimePath(repo))), runtime.GOOS) + if !strings.Contains(skill, launcher+" run") { + t.Fatalf("SKILL.md does not use pinned runtime %q:\n%s", launcher, skill) + } +} + func TestPythonScaffoldUsesRelocatableInterpreter(t *testing.T) { previousVersion := version version = "0.1.9" diff --git a/cmd/yskill/scaffold.go b/cmd/yskill/scaffold.go index 6785ab9..d7f0aaf 100644 --- a/cmd/yskill/scaffold.go +++ b/cmd/yskill/scaffold.go @@ -51,7 +51,18 @@ func scaffoldCommand(language, dir string) (launcher, workflow string) { } root, ok := repositoryRootFromLocalRuntime() if !ok { - return launcher, workflow + var err error + root, err = findRepoRoot(dir, "") + if err != nil { + return launcher, workflow + } + root, err = filepath.Abs(root) + if err != nil { + return launcher, workflow + } + if verifyLocalRuntime(localRuntimePath(root), runtimeVersion(), language) != nil { + return launcher, workflow + } } absDir, err := filepath.Abs(dir) if err != nil || !within(root, absDir) { @@ -93,6 +104,11 @@ func scaffoldSkill(dir, language, sdkPath, description string) error { } else if err != nil && !os.IsNotExist(err) { return err } + if language == "go" || language == "rust" { + if err := pinCurrentRuntime(dir); err != nil { + return err + } + } writeIfAbsent := func(rel, content string) error { path := filepath.Join(dir, filepath.FromSlash(rel)) if _, err := os.Stat(path); err == nil { @@ -137,6 +153,76 @@ func scaffoldSkill(dir, language, sdkPath, description string) error { return nil } +func pinCurrentRuntime(dir string) error { + root, err := findRepoRoot(dir, "") + if err != nil { + return nil + } + source := strings.TrimSpace(os.Getenv("YIELD_LAUNCHER_PATH")) + if source == "" { + source, err = currentExecutable() + if err != nil { + return fmt.Errorf("locate current Yield runtime: %w", err) + } + } + source, err = filepath.Abs(source) + if err != nil { + return fmt.Errorf("resolve current Yield runtime: %w", err) + } + name := strings.ToLower(filepath.Base(source)) + if name != "yskill" && name != "yskill.exe" { + return nil + } + destination := localRuntimePath(root) + if filepath.Clean(source) == filepath.Clean(destination) { + return ensureLocalStateIgnored(root) + } + got, err := inspectRuntimeVersion(source) + if err != nil { + return fmt.Errorf("verify current Yield runtime: %w", err) + } + if expected := runtimeVersion(); got != expected { + return fmt.Errorf("current Yield launcher version is %s, but the runtime is %s", got, expected) + } + contents, err := os.ReadFile(source) + if err != nil { + return fmt.Errorf("read current Yield runtime: %w", err) + } + if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { + return err + } + temporary, err := os.CreateTemp(filepath.Dir(destination), ".yskill-pin-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if _, err := temporary.Write(contents); err != nil { + temporary.Close() + return err + } + if err := temporary.Chmod(0o755); err != nil { + temporary.Close() + return err + } + if err := temporary.Close(); err != nil { + return err + } + if runtime.GOOS == "windows" { + if err := os.Remove(destination); err != nil && !os.IsNotExist(err) { + return err + } + } + if err := os.Rename(temporaryPath, destination); err != nil { + return fmt.Errorf("pin repository-local Yield runtime: %w", err) + } + if err := ensureLocalStateIgnored(root); err != nil { + return err + } + fmt.Printf("init: pinned Yield %s at %s\n", got, filepath.ToSlash(destination)) + return nil +} + func scaffoldFiles(name, language, sdkPath string) map[string]string { v := packageVersion() switch language { @@ -159,9 +245,9 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string { } case "rust": return map[string]string{ - "Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\" }\nserde_json = \"1\"\n", name, v), - "src/main.rs": mainRust, - "skill.json": fmt.Sprintf("{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", name), + "Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\" }\nserde_json = \"1\"\n", name, v), + "src/main.rs": mainRust, + "skill.json": fmt.Sprintf("{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", name), } default: gomod := fmt.Sprintf("module %s\n\ngo 1.26.5\n\nrequire github.com/operatorstack/yield v%s\n", name, v) diff --git a/evals/results/latest.json b/evals/results/latest.json index 6fe888b..f0bf2f0 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-07T21:46:28.142Z", - "source_digest": "e70b0de3a9db5924311609a33e3ce91b539da02a6822acbf57d00471d8f6b63a", + "generated_at": "2026-08-07T22:52:34.977Z", + "source_digest": "b5c66a96530cc9d100c388a4df9bbbc8d0aea6e78ef0fe2cfa0009b8aa1da778", "status": "passed", "workflow_conformance": { "passed": 40,