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
24 changes: 24 additions & 0 deletions cmd/yskill/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ func applyBootstrapPlan(plan bootstrapPlan) error {
}
sort.Strings(keys)
for _, rel := range keys {
if rel == ".gitignore" {
if err := writeBootstrapFileIfAbsent(filepath.Join(plan.SkillDir, filepath.FromSlash(rel)), plan.Files[rel]); err != nil {
return err
}
continue
}
if err := writeBootstrapFile(filepath.Join(plan.SkillDir, filepath.FromSlash(rel)), plan.Files[rel]); err != nil {
return err
}
Expand Down Expand Up @@ -359,6 +365,24 @@ func writeBootstrapFile(path, content string) error {
return os.Rename(temporaryPath, path)
}

func writeBootstrapFileIfAbsent(path, content string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if errors.Is(err, fs.ErrExist) {
return nil
}
if err != nil {
return err
}
if _, err := file.WriteString(content); err != nil {
file.Close()
return err
}
return file.Close()
}

func readBootstrapProfile(repoRoot string) (bootstrapProfile, error) {
path := filepath.Join(repoRoot, ".yield", "bootstrap.json")
b, err := os.ReadFile(path)
Expand Down
1 change: 1 addition & 0 deletions cmd/yskill/bootstrap_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ log. It is not a generated destination file.
files["skill.json"] = fmt.Sprintf("{\"version\":1,\"yield_version\":%q,\"language\":\"go\",\"run\":[\"go\",\"run\",\"-mod=readonly\",\".\"]}\n", version)
dependency = "go mod tidy (inside skills/yield-workflow-builder)"
case "rust":
files[".gitignore"] = rustSkillGitignore
files["src/main.rs"] = bootstrapRust
files["Cargo.toml"] = fmt.Sprintf("[package]\nname = \"yield-workflow-builder\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\" }\nserde_json = \"1\"\n", version)
files["skill.json"] = fmt.Sprintf("{\"version\":1,\"yield_version\":%q,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\"]}\n", version)
Expand Down
43 changes: 43 additions & 0 deletions cmd/yskill/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,49 @@ func TestBuilderTemplatesExposeEquivalentOperations(t *testing.T) {
}
}

func TestBootstrapRustTemplateAddsAndPreservesSkillGitignore(t *testing.T) {
profile := bootstrapProfile{YieldVersion: "1.2.3", Agents: []string{"codex"}}
files, _, err := renderBootstrapSkill("rust", profile)
if err != nil {
t.Fatal(err)
}
if got := files[".gitignore"]; got != rustSkillGitignore {
t.Fatalf("Rust bootstrap .gitignore = %q, want %q", got, rustSkillGitignore)
}
for _, language := range []string{"typescript", "python", "go"} {
files, _, err := renderBootstrapSkill(language, profile)
if err != nil {
t.Fatal(err)
}
if _, found := files[".gitignore"]; found {
t.Fatalf("%s bootstrap template created Rust-specific .gitignore", language)
}
}

root := t.TempDir()
skillDir := filepath.Join(root, "skills", bootstrapSkillName)
if err := os.MkdirAll(skillDir, 0o755); err != nil {
t.Fatal(err)
}
const existing = "user-owned-rule/\n"
if err := os.WriteFile(filepath.Join(skillDir, ".gitignore"), []byte(existing), 0o644); err != nil {
t.Fatal(err)
}
plan := bootstrapPlan{
Root: root,
Language: "python",
SkillDir: skillDir,
Profile: bootstrapProfile{Version: 1, YieldVersion: "1.2.3", Language: "python"},
Files: map[string]string{".gitignore": rustSkillGitignore},
}
if err := applyBootstrapPlan(plan); err != nil {
t.Fatal(err)
}
if got := readTestFile(t, filepath.Join(skillDir, ".gitignore")); got != existing {
t.Fatalf("existing bootstrap .gitignore changed: %q", got)
}
}

func TestBuilderCreateFixtureDoesNotRequireProjection(t *testing.T) {
if strings.Contains(bootstrapFixtureResponses, `"project-semantics"`) {
t.Fatal("create mode fixture must remain unchanged by conversion projection")
Expand Down
70 changes: 66 additions & 4 deletions cmd/yskill/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,12 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
files []string
command string
pin string
ignore 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", "skill.json"}, "yskill run .", "github.com/operatorstack/yield v0.1.9"},
{"rust", []string{"src/main.rs", "Cargo.toml", "skill.json"}, "yskill run .", `version = "=0.1.9"`},
{"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", "skill.json"}, "yskill run .", "github.com/operatorstack/yield v0.1.9", ""},
{"rust", []string{"src/main.rs", "Cargo.toml", "skill.json", ".gitignore"}, "yskill run .", `version = "=0.1.9"`, rustSkillGitignore},
}
for _, tt := range tests {
t.Run(tt.language, func(t *testing.T) {
Expand Down Expand Up @@ -246,13 +247,36 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
if tt.language == "rust" && strings.Contains(manifest, "registry =") {
t.Fatalf("public Rust scaffold contains a private package registry:\n%s", manifest)
}
ignorePath := filepath.Join(dir, ".gitignore")
if tt.ignore == "" {
if _, err := os.Stat(ignorePath); !os.IsNotExist(err) {
t.Fatalf("%s scaffold created Rust-specific .gitignore: %v", tt.language, err)
}
} else if got := readTestFile(t, ignorePath); got != tt.ignore {
t.Fatalf(".gitignore = %q, want %q", got, tt.ignore)
}
})
}
if tidyCalls != 1 {
t.Fatalf("go mod tidy calls = %d, want 1", tidyCalls)
}
}

func TestRustScaffoldPreservesExistingGitignore(t *testing.T) {
dir := filepath.Join(t.TempDir(), "safe-change")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
const existing = "user-owned-rule/\n"
writeTestFile(t, filepath.Join(dir, ".gitignore"), existing)
if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil {
t.Fatal(err)
}
if got := readTestFile(t, filepath.Join(dir, ".gitignore")); got != existing {
t.Fatalf("existing .gitignore changed: %q", got)
}
}

func TestPackageScaffoldsPrintCreatedWorkflowInNextCommands(t *testing.T) {
previousVersion := version
version = "0.1.28"
Expand Down Expand Up @@ -458,6 +482,44 @@ func TestRustScaffoldPinsTheInvokedRuntimeWithoutPrivateRegistryConfig(t *testin
}
}

func TestCmdInitRustScaffoldIsDoctorValid(t *testing.T) {
if _, err := exec.LookPath("cargo"); err != nil {
t.Skip("cargo is unavailable")
}
previousVersion := version
previousExecutable := currentExecutable
previousInspect := inspectRuntimeVersion
version = "0.1.37"
t.Cleanup(func() {
version = previousVersion
currentExecutable = previousExecutable
inspectRuntimeVersion = previousInspect
})

root := t.TempDir()
writeTestFile(t, filepath.Join(root, ".git"), "gitdir: fixture\n")
writeTestFile(t, localRuntimePath(root), "packaged runtime")
currentExecutable = func() (string, error) { return localRuntimePath(root), nil }
inspectRuntimeVersion = func(path string) (string, error) {
resolved, err := filepath.EvalSymlinks(localRuntimePath(root))
if err != nil {
t.Fatal(err)
}
if path != localRuntimePath(root) && path != resolved {
t.Fatalf("inspected unexpected runtime %s", path)
}
return "0.1.37", nil
}

dir := filepath.Join(root, "skills", "safe-change")
if err := cmdInit([]string{"--language", "rust", "--description", "Check a safe change before applying it.", dir}); err != nil {
t.Fatal(err)
}
if err := cmdDoctor([]string{dir, "--root", root}); err != nil {
t.Fatalf("doctor rejected Rust scaffold: %v", err)
}
}

func TestPythonScaffoldUsesRelocatableInterpreter(t *testing.T) {
previousVersion := version
version = "0.1.9"
Expand Down
4 changes: 4 additions & 0 deletions cmd/yskill/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
)

var releaseVersionPattern = regexp.MustCompile(`^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$`)

const rustSkillGitignore = "target/\n.yield/\n"

var tidyGoModule = func(dir string) error {
cmd := exec.Command("go", "mod", "tidy")
cmd.Dir = dir
Expand Down Expand Up @@ -256,6 +259,7 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string {
}
case "rust":
return map[string]string{
".gitignore": rustSkillGitignore,
"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,\"yield_version\":%q,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", v, name),
Expand Down
12 changes: 6 additions & 6 deletions evals/results/latest-conversion.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": 1,
"methodology_version": "semantic-disposition-v1",
"generated_at": "2026-08-08T22:03:02.341Z",
"source_hash": "172b2e12624cb922d7f15f03f745d46107e42297b9a3da3c9ca9e4549f46e465",
"generated_at": "2026-08-09T09:21:44.116Z",
"source_hash": "b87e914bc9406c400e82b8dd35c8e2b35df38664cfcf375ea6eba1884fc1adb3",
"fixture_source_hash": "9ab03fffe6716da8298b461f79c9eebae7c7bb01151328686ec51ed9c0b77fe5",
"status": "passed",
"model": {
Expand All @@ -13,10 +13,10 @@
},
"sessions": 2,
"token_usage": {
"input_tokens": 425017,
"cached_input_tokens": 382499,
"output_tokens": 8032,
"reasoning_output_tokens": 1802
"input_tokens": 795134,
"cached_input_tokens": 732165,
"output_tokens": 10401,
"reasoning_output_tokens": 2874
},
"clause_counts": {
"total": 4,
Expand Down
4 changes: 2 additions & 2 deletions evals/results/latest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": 2,
"methodology_version": "1.1",
"generated_at": "2026-08-08T21:55:26.622Z",
"source_digest": "a29a1bff252107e06d7ff43bd14d10abf0cc20a9eb57e3115c250caaf7ef1b19",
"generated_at": "2026-08-09T09:13:26.116Z",
"source_digest": "255b4fcf353708369ba9aaf49d41273c4f7114747418f72d94e8bc7a08c32cad",
"status": "passed",
"workflow_conformance": {
"passed": 40,
Expand Down