From 9e4a079e24bb7ba3315e56cefc1cc1fca0a67283 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Sun, 9 Aug 2026 11:52:14 +0100 Subject: [PATCH 1/3] feat: add optional Yield developer helper --- .changeset/agent-first-bootstrap.md | 5 +- README.md | 54 ++-- cmd/yskill/bootstrap.go | 11 +- cmd/yskill/bootstrap_templates.go | 298 +++++++++++++------- cmd/yskill/bootstrap_test.go | 152 +++++++++- cmd/yskill/main.go | 17 +- docs/README.md | 4 +- docs/agent-setup.md | 24 +- docs/convert-existing-skill.md | 9 +- docs/quickstart.md | 92 +++--- docs/reference/cli.md | 17 +- docs/skill-workflows.md | 15 +- packaging/assemble.test.mjs | 13 +- packaging/create-yield.test.mjs | 5 +- packaging/create-yield/README.md | 6 +- packaging/create-yield/bin/create-yield.mjs | 2 +- scripts/readme.test.mjs | 34 +-- sdk/python/README.md | 34 +-- sdk/rust/README.md | 35 +-- sdk/yield/README.md | 34 +-- 20 files changed, 577 insertions(+), 284 deletions(-) diff --git a/.changeset/agent-first-bootstrap.md b/.changeset/agent-first-bootstrap.md index ed6437e..106f5ad 100644 --- a/.changeset/agent-first-bootstrap.md +++ b/.changeset/agent-first-bootstrap.md @@ -2,4 +2,7 @@ "@operatorstack/yield": minor --- -Add the agent-first bootstrap command, the tested workflow builder for all four SDKs, and the `@operatorstack/create-yield` initializer. +Add the explicit `yskill helper install` command and the optional developer +helper for all four SDKs. Keep `yskill bootstrap` and +`npm create @operatorstack/yield` as compatibility aliases. Package +installation does not create skills or coding-agent adapters. diff --git a/README.md b/README.md index 6cd55fc..2ed4295 100644 --- a/README.md +++ b/README.md @@ -46,37 +46,6 @@ coding-agent judgment, then continue with structured data in normal code. Verified with Cursor, Codex, and Claude Code. Registry-backed project paths are available for 73 more coding agents. -## Start with your coding agent - -Run the command for your project: - -| Language | Command | -| ---------- | -------------------------------------------------------------------------------------------------------------- | -| TypeScript | `npm create @operatorstack/yield@latest` | -| Python | `uvx --from yieldskill yskill bootstrap --language python` | -| Rust | `cargo install yieldskill --root .yield --locked`, then `.yield/bin/yskill bootstrap --root . --language rust` | -| Go | `go run github.com/operatorstack/yield/cmd/yskill@latest bootstrap --root . --language go` | - -Yield detects the repository, language, and installed coding agents. It shows -every proposed file and dependency change. It asks before it writes. It then -installs, tests, and registers the `yield-workflow-builder` skill workflow. - -Restart your coding agent. To create a new skill workflow, ask: - -```text -Use Yield to create a tested skill workflow for releasing my package. -``` - -To convert an existing `SKILL.md`, ask: - -```text -Use Yield to convert my existing release SKILL.md into a tested skill workflow. -``` - -The builder can create a skill workflow from a description. It can also -convert an existing `SKILL.md`. Yield does not use install hooks to change the -repository. - ## Move repeated instructions into code A release skill often starts as prose: @@ -167,7 +136,7 @@ lane. Stable release execution remains pinned to an exact public version. -## Advanced: build manually +## Create a workflow ### 1. Install Yield @@ -262,6 +231,27 @@ Use the release skill to publish this package. The agent follows the generated adapter, runs the canonical workflow in `skills/release`, and asks for each required agent or user response. +## Optional: install the developer helper + +Package installation does not create skills or coding-agent adapters. After +you understand the manual flow above, you can explicitly install the guided +helper: + +| Language | Command | +| ---------- | ------------------------------------------------------------------------------------------------------------------- | +| TypeScript | `npm exec -- yskill helper install --language typescript` | +| Python | `uvx --from yieldskill yskill helper install --language python` | +| Rust | `cargo install yieldskill --root .yield --locked`, then `.yield/bin/yskill helper install --root . --language rust` | +| Go | `go run github.com/operatorstack/yield/cmd/yskill@latest helper install --root . --language go` | + +The helper is installed as `skills/yield-workflow-builder`. It can explain, +create, convert, check, repair, upgrade, and register skill workflows. It shows +the relevant primitive, exact files, and commands before any mutation and asks +for approval. Restart your coding agent after installation. + +`yskill bootstrap` and `npm create @operatorstack/yield@latest` remain +compatibility aliases for `yskill helper install`. + ## How Yield runs and resumes 1. Your workflow emits one typed operation. diff --git a/cmd/yskill/bootstrap.go b/cmd/yskill/bootstrap.go index 1da0297..a8f82f9 100644 --- a/cmd/yskill/bootstrap.go +++ b/cmd/yskill/bootstrap.go @@ -65,7 +65,7 @@ func cmdBootstrap(args []string) error { return err } if fs.NArg() != 0 { - return fmt.Errorf("bootstrap takes no positional arguments") + return fmt.Errorf("helper install takes no positional arguments") } plan, err := makeBootstrapPlan(*root, *language, requested) if err != nil { @@ -73,11 +73,11 @@ func cmdBootstrap(args []string) error { } printBootstrapPlan(plan) if *dryRun { - fmt.Println("bootstrap: dry run complete; no files changed") + fmt.Println("helper: dry run complete; no files changed") return nil } if !*yes && !confirmBootstrap(bootstrapInput) { - fmt.Println("bootstrap: cancelled; no files changed") + fmt.Println("helper: cancelled; no files changed") return nil } if err := applyBootstrapPlan(plan); err != nil { @@ -100,8 +100,9 @@ func cmdBootstrap(args []string) error { if err := bootstrapDoctor(plan.SkillDir, plan.Root, ids); err != nil { return fmt.Errorf("verify workflow builder adapters: %w", err) } - fmt.Println("bootstrap: workflow builder is ready") + fmt.Println("helper: Yield developer helper is ready") fmt.Println("next: restart your coding agent, then ask it to create or convert a skill workflow") + fmt.Println("learn: Use Yield to explain how coded skill workflows work.") fmt.Println("create: Use Yield to create a tested skill workflow for releasing my package.") fmt.Println("convert: Use Yield to convert my existing release SKILL.md into a tested skill workflow.") return nil @@ -220,7 +221,7 @@ func preflightBootstrapAdapter(root, path string) error { } func printBootstrapPlan(plan bootstrapPlan) { - fmt.Printf("bootstrap plan: language=%s root=%s\n", plan.Language, plan.Root) + fmt.Printf("helper install plan: language=%s root=%s\n", plan.Language, plan.Root) paths := make([]string, 0, len(plan.Files)+len(plan.Adapters)+1) for rel := range plan.Files { paths = append(paths, filepath.Join(plan.SkillDir, filepath.FromSlash(rel))) diff --git a/cmd/yskill/bootstrap_templates.go b/cmd/yskill/bootstrap_templates.go index 9e9fe7c..e28c477 100644 --- a/cmd/yskill/bootstrap_templates.go +++ b/cmd/yskill/bootstrap_templates.go @@ -25,7 +25,7 @@ func renderBootstrapSkill(language string, profile bootstrapProfile) (map[string files := map[string]string{ "SKILL.md": fmt.Sprintf(`--- name: yield-workflow-builder -description: Create a tested Yield skill workflow from a description, or convert an existing SKILL.md into one. +description: Learn Yield, create or convert skill workflows, check and repair them, upgrade dependencies, and register coding-agent adapters. --- @@ -42,6 +42,12 @@ Use --result-json for structured agent work. When an operation asks you to write or repair files, edit the repository before returning the JSON result. Do not skip an operation or invent its response. +The helper teaches the relevant Yield primitive and previews exact files and +commands before any mutation. A declined plan ends without applying it. Learn +and check modes do not write files; check runs static doctor without fixtures. +The helper cannot upgrade itself during an active run. Exit and run +yskill helper install to refresh it. + For convert mode, the builder first applies a semantic-disposition projection: C = clauses(S) @@ -95,12 +101,21 @@ const bootstrapFixtureResponses = `{ "description": "Create a harmless fixture workflow.", "language": "go", "destination": "skills/yield-workflow-builder-fixture", - "source_path": "" + "source_path": "", + "target_path": "", + "requested_version": "" }, "extract-flow": { "summary": "Ask for confirmation and complete.", "steps": [{"id":"confirm","kind":"ask_user","description":"Ask for confirmation."}] }, + "teach-and-plan": { + "summary": "Create one harmless fixture workflow.", + "primitives": ["AgentTask for bounded judgment; Require for verified completion."], + "files": ["skills/yield-workflow-builder-fixture/SKILL.md","skills/yield-workflow-builder-fixture/main.go"], + "commands": ["yskill doctor skills/yield-workflow-builder-fixture --test"] + }, + "approve-change": {"value": "apply"}, "write-workflow": {"files":["skills/yield-workflow-builder-fixture/SKILL.md","skills/yield-workflow-builder-fixture/main.go"]} } ` @@ -118,114 +133,182 @@ import { dirname, isAbsolute, normalize, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { defineSkill } from "@operatorstack/yield"; -type Specification = { name: string; description: string; language: "typescript"|"python"|"go"|"rust"; destination: string; source_path?: string }; -type Files = { files: string[] }; -type Projection = { ready: boolean; unresolved: string[]; clauses: unknown[] }; -const specSchema = {type:"object",required:["name","description","language","destination","source_path"],additionalProperties:false,properties:{name:{type:"string",pattern:"^[a-z0-9]+(?:-[a-z0-9]+)*$"},description:{type:"string",minLength:1},language:{enum:["typescript","python","go","rust"]},destination:{type:"string",minLength:1},source_path:{type:"string"}}}; +type Mode = "learn"|"create"|"convert"|"check"|"repair"|"upgrade"|"register"; +type Specification = { name:string; description:string; language:"typescript"|"python"|"go"|"rust"; destination:string; source_path:string; target_path:string; requested_version:string }; +type Files = { files:string[] }; +type Projection = { ready:boolean; unresolved:string[]; clauses:unknown[] }; +type Plan = { summary:string; primitives:string[]; files:string[]; commands:string[] }; +const helperPath = "skills/yield-workflow-builder"; +const specSchema = {type:"object",required:["name","description","language","destination","source_path","target_path","requested_version"],additionalProperties:false,properties:{name:{type:"string"},description:{type:"string"},language:{enum:["typescript","python","go","rust"]},destination:{type:"string"},source_path:{type:"string"},target_path:{type:"string"},requested_version:{type:"string"}}}; const projectionSchema = {type:"object",required:["clauses","ready","unresolved"],additionalProperties:false,properties:{clauses:{type:"array",minItems:1,items:{type:"object",required:["source_clause","disposition","destinations","reason"],additionalProperties:false,properties:{source_clause:{type:"string",minLength:1},disposition:{enum:["control","guidance","both","excluded"]},destinations:{type:"array",items:{type:"object",required:["kind","target"],additionalProperties:false,properties:{kind:{enum:["code","skill","agent_task"]},target:{type:"string",minLength:1}}}},reason:{type:"string"}}}},ready:{type:"boolean"},unresolved:{type:"array",items:{type:"string",minLength:1}}}}; const flowSchema = {type:"object",required:["summary","steps"],additionalProperties:false,properties:{summary:{type:"string",minLength:1},steps:{type:"array",minItems:1,items:{type:"object",required:["id","kind","description"],additionalProperties:false,properties:{id:{type:"string",minLength:1},kind:{enum:["ask_user","agent_task","run_command","branch","require"]},description:{type:"string",minLength:1}}}}}}; -const filesSchema = {type:"object",required:["files"],additionalProperties:false,properties:{files:{type:"array",minItems:2,items:{type:"string",minLength:1}}}}; -const config = JSON.parse(readFileSync(new URL("./builder.json", import.meta.url), "utf8")) as {launcher:string;agents:string[]}; +const filesSchema = {type:"object",required:["files"],additionalProperties:false,properties:{files:{type:"array",minItems:1,items:{type:"string",minLength:1}}}}; +const planSchema = {type:"object",required:["summary","primitives","files","commands"],additionalProperties:false,properties:{summary:{type:"string",minLength:1},primitives:{type:"array",minItems:1,items:{type:"string",minLength:1}},files:{type:"array",minItems:1,items:{type:"string",minLength:1}},commands:{type:"array",minItems:1,items:{type:"string",minLength:1}}}}; +const guideSchema = {type:"object",required:["summary","primitives","manual_steps","docs"],additionalProperties:false,properties:{summary:{type:"string",minLength:1},primitives:{type:"array",minItems:1,items:{type:"string",minLength:1}},manual_steps:{type:"array",minItems:1,items:{type:"string",minLength:1}},docs:{type:"array",minItems:1,items:{type:"string",minLength:1}}}}; +const config = JSON.parse(readFileSync(new URL("./builder.json", import.meta.url), "utf8")) as {launcher:string;agents:string[];yield_version:string}; const root = realpathSync(fileURLToPath(new URL("../../", import.meta.url))); const quote = (v:string) => "'" + v.replaceAll("'", "'\\''") + "'"; const inside = (p:string) => p === root || p.startsWith(root+sep); -const safe = (p:string, destination=false) => !isAbsolute(p) && !normalize(p).split(sep).includes("..") && normalize(p).startsWith("skills"+sep) && inside(realpathSync(destination ? dirname(resolve(root,p)) : resolve(root,p))); +const safe = (p:string, destination=false) => !!p && !isAbsolute(p) && !normalize(p).split(sep).includes("..") && normalize(p).startsWith("skills"+sep) && inside(realpathSync(destination ? dirname(resolve(root,p)) : resolve(root,p))); +const filesStayInside = (files:string[], target:string) => files.every((file) => normalize(file) === normalize(target) || normalize(file).startsWith(normalize(target)+sep)); defineSkill((ctx) => { - const mode = ctx.askUser("select-mode", "Create a workflow from a description, or convert an existing SKILL.md?", [{value:"create",label:"Create"},{value:"convert",label:"Convert"}]); - const spec = ctx.agentTask("collect-specification", "Use the user's current request. Return a safe kebab-case name, a short description, the target language, a new destination under skills/, and source_path for convert mode. Do not write files.", {mode}, specSchema); - if (!safe(spec.destination, true) || spec.destination === "skills/yield-workflow-builder") ctx.blocked("the destination must be a new path under skills/"); - const available = ctx.runCommand("check-destination", "cd ../.. && test ! -e "+quote(spec.destination), 30); - if (available.exit_code !== 0) ctx.blocked("the destination must be a new path under skills/"); + const mode = ctx.askUser("select-mode", "What do you want to do with Yield?", [ + {value:"learn",label:"Learn"},{value:"create",label:"Create"},{value:"convert",label:"Convert"},{value:"check",label:"Check"}, + {value:"repair",label:"Repair"},{value:"upgrade",label:"Upgrade"},{value:"register",label:"Register"}, + ]) as Mode; + if (mode === "learn") { + const guide = ctx.agentTask("teach-yield", "Teach the smallest relevant Yield concept for the user's request. Explain what remains in SKILL.md, what moves into code, fixtures, doctor, and registration. Give manual steps before mentioning this helper. Do not edit files or run commands.", {mode}, guideSchema); + return {mode,changed:false,guide}; + } + const spec = ctx.agentTask("collect-specification", "Use the user's current request. For create or convert, return a safe kebab-case name, description, target language, new destination under skills/, and source_path for convert. For check, repair, upgrade, or register, return target_path for one existing workflow under skills/. Set requested_version only when the user explicitly requests one. Use empty strings for fields that do not apply. Do not write files.", {mode}, specSchema); + const creates = mode === "create" || mode === "convert"; + const target = creates ? spec.destination : spec.target_path; + if (creates) { + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(spec.name) || !spec.description || !safe(target,true) || target === helperPath) ctx.blocked("the destination must be a new named path under skills/"); + const available = ctx.runCommand("check-destination", "cd ../.. && test ! -e "+quote(target), 30); + if (available.exit_code !== 0) ctx.blocked("the destination must be a new path under skills/"); + } else if (!safe(target)) { + ctx.blocked("the target workflow must be an existing path under skills/"); + } + if (mode === "upgrade" && target === helperPath) ctx.blocked("the helper cannot upgrade itself during an active run; exit and run yskill helper install"); + if (mode === "upgrade" && spec.requested_version && spec.requested_version !== config.yield_version) ctx.blocked("unsupported Yield version change; exit and run yskill helper install for the requested version"); + const fixture = target === "skills/yield-workflow-builder-fixture"; + if (mode === "check") { + const checked = ctx.runCommand("check-workflow", fixture?"printf fixture-check-ok":"cd ../.. && "+config.launcher+" doctor "+quote(target)+" --root .", 120); + return {mode,target,changed:false,healthy:checked.exit_code===0,stdout:checked.stdout,stderr:checked.stderr}; + } let source = ""; let projection: Projection|null = null; if (mode === "convert") { - if (!spec.source_path || !safe(spec.source_path)) ctx.blocked("the source skill must be inside the repository"); + if (!safe(spec.source_path)) ctx.blocked("the source skill must be inside the repository"); const sourceFile = resolve(root,spec.source_path,"SKILL.md"); if (!existsSync(sourceFile)) ctx.blocked("the source SKILL.md does not exist"); - source = readFileSync(sourceFile, "utf8"); + source = readFileSync(sourceFile,"utf8"); projection = ctx.agentTask("project-semantics", "Map every source clause exactly once using Pi(S)={(c,d,T,r)|c in clauses(S)}. YAML frontmatter is metadata, not a clause. Each top-level bullet is exactly one clause, including a compound sentence. For prose without bullets, treat each paragraph as one clause. Use disposition control, guidance, both, or excluded. Control needs a reachable code destination. Guidance needs a reachable skill or agent_task destination. Both needs both kinds. Excluded needs no destination and a non-empty reason. Name concrete destinations that the writer can create. Use the target language's canonical entrypoint for code: main.ts, main.py, main.go, or src/main.rs. Report uncertainty in unresolved and set ready false. Do not write files.", {source,spec}, projectionSchema); if (!projection.ready || projection.unresolved.length > 0) ctx.blocked("the semantic projection has unresolved source clauses"); } - const flow = ctx.agentTask("extract-flow", "Extract or design the minimal workflow control flow. Follow the semantic projection. Keep model judgment in agent_task operations. Put order, branches, commands, approvals, evidence requirements, and finish rules in code.", {mode,spec,source,projection}, flowSchema); - let written = ctx.agentTask("write-workflow", "Create the complete Yield skill workflow at the destination. Follow every semantic disposition. Write the language program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. A thin SKILL.md removes duplicated sequencing, not useful guidance. Do not edit files outside the destination. Return every file written.", {mode,spec,source,projection,flow}, filesSchema); - const fixture = spec.destination === "skills/yield-workflow-builder-fixture"; - const base = fixture ? "printf fixture-ok" : "cd ../.. && "+config.launcher+" doctor "+quote(spec.destination)+" --test"; - let checked = ctx.runCommand("verify-generated", base, 600); + let flow:unknown = null; + let inspection:unknown = null; + if (creates) flow = ctx.agentTask("extract-flow", "Extract or design the minimal workflow. Keep model judgment in agent_task. Put order, branches, commands, approvals, evidence, and finish rules in code.", {mode,spec,source,projection}, flowSchema); + else inspection = ctx.runCommand("inspect-workflow", fixture?"printf fixture-inspect-ok":"cd ../.. && "+config.launcher+" doctor "+quote(target)+" --root .", 120); + const plan = ctx.agentTask("teach-and-plan", "Teach the relevant Yield primitives, then return the exact files and commands proposed for this operation. For register, explain generated adapters. For repair, use the doctor evidence. For upgrade, target the installed Yield version and include dependency and lockfile changes. Do not edit files or run commands.", {mode,spec,target,flow,inspection,yield_version:config.yield_version}, planSchema); + const approval = ctx.askUser("approve-change", plan.summary+" Apply this plan?", [{value:"apply",label:"Apply"},{value:"stop",label:"Stop"}]); + if (approval !== "apply") ctx.refused("the developer declined the proposed Yield changes"); + const agentFlag = config.agents.join(","); + const verify = fixture ? "printf fixture-ok" : "cd ../.. && "+config.launcher+" doctor "+quote(target)+" --root . --test"; + if (mode === "register") { + const checked = ctx.runCommand("verify-before-register",verify,600); + ctx.require(checked.exit_code===0,"the workflow passes its fixture run",{exit_code:checked.exit_code}); + const registered = ctx.runCommand("register-workflow",fixture?"printf fixture-register-ok":"cd ../.. && "+config.launcher+" register "+quote(target)+" --root . --agent "+quote(agentFlag),120); + ctx.require(registered.exit_code===0,"the workflow is registered",{exit_code:registered.exit_code}); + const adapters = ctx.runCommand("verify-adapters",fixture?"printf fixture-adapters-ok":"cd ../.. && "+config.launcher+" doctor "+quote(target)+" --root . --agent "+quote(agentFlag)+" --test",600); + ctx.require(adapters.exit_code===0,"the generated adapters pass verification",{exit_code:adapters.exit_code}); + return {mode,target,changed:true,verified:true}; + } + let written = ctx.agentTask(creates?"write-workflow":mode+"-workflow", creates?"Create the complete Yield workflow at the destination. Follow every semantic disposition. Write the program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. Keep useful model-facing guidance in SKILL.md; remove only duplicated sequencing. Do not edit outside the destination.":"Apply only the approved operation to the target workflow. For upgrade, update its manifest and language dependency to the supplied exact Yield version. Do not edit outside the target. Return every changed file.", {mode,spec,target,source,projection,flow,plan,yield_version:config.yield_version}, filesSchema); + if (!filesStayInside(written.files,target)) ctx.blocked("the reported changes escape the approved workflow directory"); + let checked = ctx.runCommand("verify-workflow",verify,600); for (let attempt=1; checked.exit_code!==0 && attempt<=2; attempt++) { - written = ctx.agentTask("repair-generated-"+attempt, "The generated workflow failed verification. Fix only the destination files. Preserve the source semantics and projection. Return every changed file.", {spec,source,projection,stdout:checked.stdout,stderr:checked.stderr}, filesSchema); - checked = ctx.runCommand("verify-generated-retry-"+attempt, base, 600); + written = ctx.agentTask("repair-workflow-"+attempt,"Verification failed. Repair only the approved workflow directory and return every changed file.",{mode,target,stdout:checked.stdout,stderr:checked.stderr},filesSchema); + if (!filesStayInside(written.files,target)) ctx.blocked("the reported repair escapes the approved workflow directory"); + checked = ctx.runCommand("verify-workflow-retry-"+attempt,verify,600); } - if (checked.exit_code!==0) ctx.blocked("the generated workflow still fails after two repair attempts"); - ctx.require(checked.exit_code===0, "the generated workflow passes its fixture run", {exit_code:checked.exit_code}); - const agentFlag = config.agents.join(","); - const register = fixture ? "printf fixture-register-ok" : "cd ../.. && "+config.launcher+" register "+quote(spec.destination)+" --agent "+quote(agentFlag); - const registered = ctx.runCommand("register-generated", register, 120); - ctx.require(registered.exit_code===0, "the generated workflow is registered for the selected coding agents", {exit_code:registered.exit_code}); - const verifyAdapters = fixture ? "printf fixture-adapters-ok" : "cd ../.. && "+config.launcher+" doctor "+quote(spec.destination)+" --agent "+quote(agentFlag)+" --test"; - const adapters = ctx.runCommand("verify-adapters", verifyAdapters, 600); - ctx.require(adapters.exit_code===0, "the generated coding-agent adapters pass verification", {exit_code:adapters.exit_code}); - return {mode,language:spec.language,destination:spec.destination,files:written.files,verified:true}; + if (checked.exit_code!==0) ctx.blocked("the workflow still fails after two repair attempts"); + ctx.require(checked.exit_code===0,"the workflow passes its fixture run",{exit_code:checked.exit_code}); + const registered = ctx.runCommand("register-workflow",fixture?"printf fixture-register-ok":"cd ../.. && "+config.launcher+" register "+quote(target)+" --root . --agent "+quote(agentFlag),120); + ctx.require(registered.exit_code===0,"the workflow is registered",{exit_code:registered.exit_code}); + const adapters = ctx.runCommand("verify-adapters",fixture?"printf fixture-adapters-ok":"cd ../.. && "+config.launcher+" doctor "+quote(target)+" --root . --agent "+quote(agentFlag)+" --test",600); + ctx.require(adapters.exit_code===0,"the generated adapters pass verification",{exit_code:adapters.exit_code}); + return {mode,target,language:spec.language,files:written.files,changed:true,verified:true}; }); ` const bootstrapPython = `from pathlib import Path import json -import os from yieldskill import define_skill ROOT = Path(__file__).resolve().parents[2].resolve() CONFIG = json.loads((Path(__file__).parent / "builder.json").read_text()) -SPEC_SCHEMA = {"type":"object","required":["name","description","language","destination","source_path"],"additionalProperties":False,"properties":{"name":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"description":{"type":"string","minLength":1},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string","minLength":1},"source_path":{"type":"string"}}} +HELPER_PATH = "skills/yield-workflow-builder" +SPEC_SCHEMA = {"type":"object","required":["name","description","language","destination","source_path","target_path","requested_version"],"additionalProperties":False,"properties":{"name":{"type":"string"},"description":{"type":"string"},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string"},"source_path":{"type":"string"},"target_path":{"type":"string"},"requested_version":{"type":"string"}}} PROJECTION_SCHEMA = {"type":"object","required":["clauses","ready","unresolved"],"additionalProperties":False,"properties":{"clauses":{"type":"array","minItems":1,"items":{"type":"object","required":["source_clause","disposition","destinations","reason"],"additionalProperties":False,"properties":{"source_clause":{"type":"string","minLength":1},"disposition":{"enum":["control","guidance","both","excluded"]},"destinations":{"type":"array","items":{"type":"object","required":["kind","target"],"additionalProperties":False,"properties":{"kind":{"enum":["code","skill","agent_task"]},"target":{"type":"string","minLength":1}}}},"reason":{"type":"string"}}}},"ready":{"type":"boolean"},"unresolved":{"type":"array","items":{"type":"string","minLength":1}}}} FLOW_SCHEMA = {"type":"object","required":["summary","steps"],"additionalProperties":False,"properties":{"summary":{"type":"string","minLength":1},"steps":{"type":"array","minItems":1,"items":{"type":"object","required":["id","kind","description"],"additionalProperties":False,"properties":{"id":{"type":"string","minLength":1},"kind":{"enum":["ask_user","agent_task","run_command","branch","require"]},"description":{"type":"string","minLength":1}}}}}} -FILES_SCHEMA = {"type":"object","required":["files"],"additionalProperties":False,"properties":{"files":{"type":"array","minItems":2,"items":{"type":"string","minLength":1}}}} +FILES_SCHEMA = {"type":"object","required":["files"],"additionalProperties":False,"properties":{"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}} +PLAN_SCHEMA = {"type":"object","required":["summary","primitives","files","commands"],"additionalProperties":False,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"commands":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}} +GUIDE_SCHEMA = {"type":"object","required":["summary","primitives","manual_steps","docs"],"additionalProperties":False,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"manual_steps":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"docs":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}} def safe(path, destination=False): candidate = Path(path) - if candidate.is_absolute() or ".." in candidate.parts or len(candidate.parts) <= 1 or candidate.parts[0] != "skills": return False + if not path or candidate.is_absolute() or ".." in candidate.parts or len(candidate.parts) <= 1 or candidate.parts[0] != "skills": return False resolved = ((ROOT / candidate).parent if destination else (ROOT / candidate)).resolve() return resolved == ROOT or ROOT in resolved.parents -def quote(value): - return "'" + value.replace("'", "'\\''") + "'" +def quote(value): return "'" + value.replace("'", "'\\''") + "'" +def files_stay_inside(files, target): + base = Path(target) + return all(Path(file) == base or base in Path(file).parents for file in files) +def receipt(result): return {"exit_code":result.exit_code,"stdout":result.stdout,"stderr":result.stderr} def program(ctx): - mode = ctx.ask_user("select-mode", "Create a workflow from a description, or convert an existing SKILL.md?", options=[{"value":"create","label":"Create"},{"value":"convert","label":"Convert"}]) - spec = ctx.agent_task("collect-specification", "Use the user's current request. Return a safe kebab-case name, a short description, the target language, a new destination under skills/, and source_path for convert mode. Do not write files.", context={"mode":mode}, schema=SPEC_SCHEMA) - destination = ROOT / spec["destination"] - if not safe(spec["destination"], True) or spec["destination"] == "skills/yield-workflow-builder": - ctx.blocked("the destination must be a new path under skills/") - available = ctx.run_command("check-destination", "cd ../.. && test ! -e " + quote(spec["destination"]), timeout_seconds=30) - if available.exit_code != 0: ctx.blocked("the destination must be a new path under skills/") + options = [{"value":value,"label":value.title()} for value in ["learn","create","convert","check","repair","upgrade","register"]] + mode = ctx.ask_user("select-mode", "What do you want to do with Yield?", options=options) + if mode == "learn": + guide = ctx.agent_task("teach-yield", "Teach the smallest relevant Yield concept for the user's request. Explain what remains in SKILL.md, what moves into code, fixtures, doctor, and registration. Give manual steps before mentioning this helper. Do not edit files or run commands.", context={"mode":mode}, schema=GUIDE_SCHEMA) + return {"mode":mode,"changed":False,"guide":guide} + spec = ctx.agent_task("collect-specification", "Use the user's current request. For create or convert, return a safe kebab-case name, description, target language, new destination under skills/, and source_path for convert. For check, repair, upgrade, or register, return target_path for one existing workflow under skills/. Set requested_version only when the user explicitly requests one. Use empty strings for fields that do not apply. Do not write files.", context={"mode":mode}, schema=SPEC_SCHEMA) + creates = mode in ["create","convert"] + target = spec["destination"] if creates else spec["target_path"] + if creates: + valid_name = spec["name"] and all(part.isalnum() and part.lower() == part for part in spec["name"].split("-")) + if not valid_name or not spec["description"] or not safe(target, True) or target == HELPER_PATH: ctx.blocked("the destination must be a new named path under skills/") + available = ctx.run_command("check-destination", "cd ../.. && test ! -e " + quote(target), timeout_seconds=30) + if available.exit_code != 0: ctx.blocked("the destination must be a new path under skills/") + elif not safe(target): ctx.blocked("the target workflow must be an existing path under skills/") + if mode == "upgrade" and target == HELPER_PATH: ctx.blocked("the helper cannot upgrade itself during an active run; exit and run yskill helper install") + if mode == "upgrade" and spec["requested_version"] and spec["requested_version"] != CONFIG["yield_version"]: ctx.blocked("unsupported Yield version change; exit and run yskill helper install for the requested version") + fixture = target == "skills/yield-workflow-builder-fixture" + if mode == "check": + checked = ctx.run_command("check-workflow", "printf fixture-check-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(target) + " --root .", timeout_seconds=120) + return {"mode":mode,"target":target,"changed":False,"healthy":checked.exit_code == 0,"stdout":checked.stdout,"stderr":checked.stderr} source = "" projection = None if mode == "convert": - if not spec.get("source_path") or not safe(spec["source_path"]): ctx.blocked("the source skill must be inside the repository") + if not safe(spec["source_path"]): ctx.blocked("the source skill must be inside the repository") source_file = ROOT / spec["source_path"] / "SKILL.md" if not source_file.is_file(): ctx.blocked("the source SKILL.md does not exist") source = source_file.read_text() projection = ctx.agent_task("project-semantics", "Map every source clause exactly once using Pi(S)={(c,d,T,r)|c in clauses(S)}. YAML frontmatter is metadata, not a clause. Each top-level bullet is exactly one clause, including a compound sentence. For prose without bullets, treat each paragraph as one clause. Use disposition control, guidance, both, or excluded. Control needs a reachable code destination. Guidance needs a reachable skill or agent_task destination. Both needs both kinds. Excluded needs no destination and a non-empty reason. Name concrete destinations that the writer can create. Use the target language's canonical entrypoint for code: main.ts, main.py, main.go, or src/main.rs. Report uncertainty in unresolved and set ready false. Do not write files.", context={"source":source,"spec":spec}, schema=PROJECTION_SCHEMA) if not projection["ready"] or projection["unresolved"]: ctx.blocked("the semantic projection has unresolved source clauses") - flow = ctx.agent_task("extract-flow", "Extract or design the minimal workflow control flow. Follow the semantic projection. Keep model judgment in agent_task operations. Put order, branches, commands, approvals, evidence requirements, and finish rules in code.", context={"mode":mode,"spec":spec,"source":source,"projection":projection}, schema=FLOW_SCHEMA) - written = ctx.agent_task("write-workflow", "Create the complete Yield skill workflow at the destination. Follow every semantic disposition. Write the language program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. A thin SKILL.md removes duplicated sequencing, not useful guidance. Do not edit files outside the destination. Return every file written.", context={"mode":mode,"spec":spec,"source":source,"projection":projection,"flow":flow}, schema=FILES_SCHEMA) - fixture = spec["destination"] == "skills/yield-workflow-builder-fixture" - base = "printf fixture-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(spec["destination"]) + " --test" - checked = ctx.run_command("verify-generated", base, timeout_seconds=600) + flow = ctx.agent_task("extract-flow", "Extract or design the minimal workflow. Keep model judgment in agent_task. Put order, branches, commands, approvals, evidence, and finish rules in code.", context={"mode":mode,"spec":spec,"source":source,"projection":projection}, schema=FLOW_SCHEMA) if creates else None + inspection_result = None if creates else ctx.run_command("inspect-workflow", "printf fixture-inspect-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(target) + " --root .", timeout_seconds=120) + inspection = None if inspection_result is None else receipt(inspection_result) + plan = ctx.agent_task("teach-and-plan", "Teach the relevant Yield primitives, then return the exact files and commands proposed. For register, explain adapters. For repair, use doctor evidence. For upgrade, target the installed Yield version and include dependency and lockfile changes. Do not edit files or run commands.", context={"mode":mode,"spec":spec,"target":target,"flow":flow,"inspection":inspection,"yield_version":CONFIG["yield_version"]}, schema=PLAN_SCHEMA) + approval = ctx.ask_user("approve-change", plan["summary"] + " Apply this plan?", options=[{"value":"apply","label":"Apply"},{"value":"stop","label":"Stop"}]) + if approval != "apply": ctx.refused("the developer declined the proposed Yield changes") + agents = ",".join(CONFIG["agents"]) + verify = "printf fixture-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(target) + " --root . --test" + register = "printf fixture-register-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " register " + quote(target) + " --root . --agent " + quote(agents) + verify_adapters = "printf fixture-adapters-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(target) + " --root . --agent " + quote(agents) + " --test" + if mode == "register": + checked = ctx.run_command("verify-before-register", verify, timeout_seconds=600); ctx.require(checked.exit_code == 0, "the workflow passes its fixture run", {"exit_code":checked.exit_code}) + registered = ctx.run_command("register-workflow", register, timeout_seconds=120); ctx.require(registered.exit_code == 0, "the workflow is registered", {"exit_code":registered.exit_code}) + adapters = ctx.run_command("verify-adapters", verify_adapters, timeout_seconds=600); ctx.require(adapters.exit_code == 0, "the generated adapters pass verification", {"exit_code":adapters.exit_code}) + return {"mode":mode,"target":target,"changed":True,"verified":True} + prompt = "Create the complete Yield workflow at the destination. Follow every semantic disposition. Write the program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. Keep useful model-facing guidance in SKILL.md; remove only duplicated sequencing. Do not edit outside the destination." if creates else "Apply only the approved operation to the target. For upgrade, update its manifest and language dependency to the supplied exact Yield version. Do not edit outside the target. Return every changed file." + written = ctx.agent_task("write-workflow" if creates else mode + "-workflow", prompt, context={"mode":mode,"spec":spec,"target":target,"source":source,"projection":projection,"flow":flow,"plan":plan,"yield_version":CONFIG["yield_version"]}, schema=FILES_SCHEMA) + if not files_stay_inside(written["files"], target): ctx.blocked("the reported changes escape the approved workflow directory") + checked = ctx.run_command("verify-workflow", verify, timeout_seconds=600) for attempt in range(1, 3): if checked.exit_code == 0: break - written = ctx.agent_task(f"repair-generated-{attempt}", "The generated workflow failed verification. Fix only the destination files. Preserve the source semantics and projection. Return every changed file.", context={"spec":spec,"source":source,"projection":projection,"stdout":checked.stdout,"stderr":checked.stderr}, schema=FILES_SCHEMA) - checked = ctx.run_command(f"verify-generated-retry-{attempt}", base, timeout_seconds=600) - if checked.exit_code != 0: ctx.blocked("the generated workflow still fails after two repair attempts") - ctx.require(checked.exit_code == 0, "the generated workflow passes its fixture run", {"exit_code":checked.exit_code}) - agent_flag = ",".join(CONFIG["agents"]) - register = "printf fixture-register-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " register " + quote(spec["destination"]) + " --agent " + quote(agent_flag) - registered = ctx.run_command("register-generated", register, timeout_seconds=120) - ctx.require(registered.exit_code == 0, "the generated workflow is registered for the selected coding agents", {"exit_code":registered.exit_code}) - verify = "printf fixture-adapters-ok" if fixture else "cd ../.. && " + CONFIG["launcher"] + " doctor " + quote(spec["destination"]) + " --agent " + quote(agent_flag) + " --test" - adapters = ctx.run_command("verify-adapters", verify, timeout_seconds=600) - ctx.require(adapters.exit_code == 0, "the generated coding-agent adapters pass verification", {"exit_code":adapters.exit_code}) - return {"mode":mode,"language":spec["language"],"destination":spec["destination"],"files":written["files"],"verified":True} + written = ctx.agent_task(f"repair-workflow-{attempt}", "Verification failed. Repair only the approved workflow directory and return every changed file.", context={"mode":mode,"target":target,"stdout":checked.stdout,"stderr":checked.stderr}, schema=FILES_SCHEMA) + if not files_stay_inside(written["files"], target): ctx.blocked("the reported repair escapes the approved workflow directory") + checked = ctx.run_command(f"verify-workflow-retry-{attempt}", verify, timeout_seconds=600) + if checked.exit_code != 0: ctx.blocked("the workflow still fails after two repair attempts") + ctx.require(True, "the workflow passes its fixture run", {"exit_code":checked.exit_code}) + registered = ctx.run_command("register-workflow", register, timeout_seconds=120); ctx.require(registered.exit_code == 0, "the workflow is registered", {"exit_code":registered.exit_code}) + adapters = ctx.run_command("verify-adapters", verify_adapters, timeout_seconds=600); ctx.require(adapters.exit_code == 0, "the generated adapters pass verification", {"exit_code":adapters.exit_code}) + return {"mode":mode,"target":target,"language":spec["language"],"files":written["files"],"changed":True,"verified":True} define_skill(program) ` @@ -237,59 +320,84 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/operatorstack/yield/sdk/yield" ) -type specification struct { Name string ` + "`json:\"name\"`" + `; Description string ` + "`json:\"description\"`" + `; Language string ` + "`json:\"language\"`" + `; Destination string ` + "`json:\"destination\"`" + `; SourcePath string ` + "`json:\"source_path\"`" + ` } +type specification struct { Name string ` + "`json:\"name\"`" + `; Description string ` + "`json:\"description\"`" + `; Language string ` + "`json:\"language\"`" + `; Destination string ` + "`json:\"destination\"`" + `; SourcePath string ` + "`json:\"source_path\"`" + `; TargetPath string ` + "`json:\"target_path\"`" + `; RequestedVersion string ` + "`json:\"requested_version\"`" + ` } type fileResult struct { Files []string ` + "`json:\"files\"`" + ` } type projectionResult struct { Ready bool ` + "`json:\"ready\"`" + `; Unresolved []string ` + "`json:\"unresolved\"`" + ` } -type config struct { Launcher string ` + "`json:\"launcher\"`" + `; Agents []string ` + "`json:\"agents\"`" + ` } -const specSchema = ` + "`" + `{"type":"object","required":["name","description","language","destination","source_path"],"additionalProperties":false,"properties":{"name":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"description":{"type":"string","minLength":1},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string","minLength":1},"source_path":{"type":"string"}}}` + "`" + ` +type planResult struct { Summary string ` + "`json:\"summary\"`" + ` } +type config struct { Launcher string ` + "`json:\"launcher\"`" + `; Agents []string ` + "`json:\"agents\"`" + `; YieldVersion string ` + "`json:\"yield_version\"`" + ` } +const helperPath = "skills/yield-workflow-builder" +const specSchema = ` + "`" + `{"type":"object","required":["name","description","language","destination","source_path","target_path","requested_version"],"additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string"},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string"},"source_path":{"type":"string"},"target_path":{"type":"string"},"requested_version":{"type":"string"}}}` + "`" + ` const projectionSchema = ` + "`" + `{"type":"object","required":["clauses","ready","unresolved"],"additionalProperties":false,"properties":{"clauses":{"type":"array","minItems":1,"items":{"type":"object","required":["source_clause","disposition","destinations","reason"],"additionalProperties":false,"properties":{"source_clause":{"type":"string","minLength":1},"disposition":{"enum":["control","guidance","both","excluded"]},"destinations":{"type":"array","items":{"type":"object","required":["kind","target"],"additionalProperties":false,"properties":{"kind":{"enum":["code","skill","agent_task"]},"target":{"type":"string","minLength":1}}}},"reason":{"type":"string"}}}},"ready":{"type":"boolean"},"unresolved":{"type":"array","items":{"type":"string","minLength":1}}}}` + "`" + ` const flowSchema = ` + "`" + `{"type":"object","required":["summary","steps"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"steps":{"type":"array","minItems":1,"items":{"type":"object","required":["id","kind","description"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1},"kind":{"enum":["ask_user","agent_task","run_command","branch","require"]},"description":{"type":"string","minLength":1}}}}}}` + "`" + ` -const filesSchema = ` + "`" + `{"type":"object","required":["files"],"additionalProperties":false,"properties":{"files":{"type":"array","minItems":2,"items":{"type":"string","minLength":1}}}}` + "`" + ` +const filesSchema = ` + "`" + `{"type":"object","required":["files"],"additionalProperties":false,"properties":{"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}` + "`" + ` +const planSchema = ` + "`" + `{"type":"object","required":["summary","primitives","files","commands"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"commands":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}` + "`" + ` +const guideSchema = ` + "`" + `{"type":"object","required":["summary","primitives","manual_steps","docs"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"manual_steps":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"docs":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}` + "`" + ` func quote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" } -func safe(root,value string,destination bool) bool { if filepath.IsAbs(value) { return false }; clean:=filepath.Clean(value); parts:=strings.Split(filepath.ToSlash(clean), "/"); if len(parts)<=1||parts[0]!="skills"||strings.Contains(filepath.ToSlash(clean), "../"){return false};probe:=filepath.Join(root,clean);if destination{probe=filepath.Dir(probe)};canonical,err:=filepath.EvalSymlinks(probe);return err==nil&&strings.HasPrefix(canonical+string(filepath.Separator),root+string(filepath.Separator)) } +func safe(root,value string,destination bool) bool { if value==""||filepath.IsAbs(value) { return false }; clean:=filepath.Clean(value); parts:=strings.Split(filepath.ToSlash(clean), "/"); if len(parts)<=1||parts[0]!="skills"||strings.Contains(filepath.ToSlash(clean), "../"){return false};probe:=filepath.Join(root,clean);if destination{probe=filepath.Dir(probe)};canonical,err:=filepath.EvalSymlinks(probe);return err==nil&&(canonical==root||strings.HasPrefix(canonical+string(filepath.Separator),root+string(filepath.Separator))) } +func filesStayInside(files []string,target string) bool { base:=filepath.Clean(target);for _,file:=range files{clean:=filepath.Clean(file);if clean!=base&&!strings.HasPrefix(clean,base+string(filepath.Separator)){return false}};return true } func main() { yield.Main(func(ctx *yield.Context) (yield.Outcome,error) { rawConfig,err:=os.ReadFile("builder.json"); if err!=nil{return yield.Outcome{},err}; var cfg config; if err=json.Unmarshal(rawConfig,&cfg);err!=nil{return yield.Outcome{},err} - mode:=ctx.AskUser("select-mode","Create a workflow from a description, or convert an existing SKILL.md?",yield.Option{Value:"create",Label:"Create"},yield.Option{Value:"convert",Label:"Convert"}) - raw:=ctx.AgentTask("collect-specification","Use the user's current request. Return a safe kebab-case name, a short description, the target language, a new destination under skills/, and source_path for convert mode. Do not write files.",map[string]any{"mode":mode},json.RawMessage(specSchema)); var spec specification; if err=json.Unmarshal(raw,&spec);err!=nil{return yield.Outcome{},err} - root,err:=filepath.Abs(filepath.Join("..","..")); if err!=nil{return yield.Outcome{},err};root,err=filepath.EvalSymlinks(root);if err!=nil{return yield.Outcome{},err}; if !safe(root,spec.Destination,true)||spec.Destination=="skills/yield-workflow-builder" { return yield.Outcome{},ctx.Blocked("the destination must be a new path under skills/") };available:=ctx.RunCommand("check-destination","cd ../.. && test ! -e "+quote(spec.Destination),30);if available.ExitCode!=0{return yield.Outcome{},ctx.Blocked("the destination must be a new path under skills/")} - source:=""; var projection json.RawMessage; if mode=="convert" { if spec.SourcePath==""||!safe(root,spec.SourcePath,false){return yield.Outcome{},ctx.Blocked("the source skill must be inside the repository")}; b,readErr:=os.ReadFile(filepath.Join(root,spec.SourcePath,"SKILL.md"));if readErr!=nil{return yield.Outcome{},ctx.Blocked("the source SKILL.md does not exist")};source=string(b); projection=ctx.AgentTask("project-semantics","Map every source clause exactly once using Pi(S)={(c,d,T,r)|c in clauses(S)}. YAML frontmatter is metadata, not a clause. Each top-level bullet is exactly one clause, including a compound sentence. For prose without bullets, treat each paragraph as one clause. Use disposition control, guidance, both, or excluded. Control needs a reachable code destination. Guidance needs a reachable skill or agent_task destination. Both needs both kinds. Excluded needs no destination and a non-empty reason. Name concrete destinations that the writer can create. Use the target language's canonical entrypoint for code: main.ts, main.py, main.go, or src/main.rs. Report uncertainty in unresolved and set ready false. Do not write files.",map[string]any{"source":source,"spec":spec},json.RawMessage(projectionSchema));var projected projectionResult;if err=json.Unmarshal(projection,&projected);err!=nil{return yield.Outcome{},err};if !projected.Ready||len(projected.Unresolved)>0{return yield.Outcome{},ctx.Blocked("the semantic projection has unresolved source clauses")} } - flow:=ctx.AgentTask("extract-flow","Extract or design the minimal workflow control flow. Follow the semantic projection. Keep model judgment in agent_task operations. Put order, branches, commands, approvals, evidence requirements, and finish rules in code.",map[string]any{"mode":mode,"spec":spec,"source":source,"projection":projection},json.RawMessage(flowSchema)) - writtenRaw:=ctx.AgentTask("write-workflow","Create the complete Yield skill workflow at the destination. Follow every semantic disposition. Write the language program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. A thin SKILL.md removes duplicated sequencing, not useful guidance. Do not edit files outside the destination. Return every file written.",map[string]any{"mode":mode,"spec":spec,"source":source,"projection":projection,"flow":json.RawMessage(flow)},json.RawMessage(filesSchema));var written fileResult;_ = json.Unmarshal(writtenRaw,&written) - fixture:=spec.Destination=="skills/yield-workflow-builder-fixture"; command:="printf fixture-ok";if !fixture{command="cd ../.. && "+cfg.Launcher+" doctor "+quote(spec.Destination)+" --test"};checked:=ctx.RunCommand("verify-generated",command,600) - for attempt:=1;checked.ExitCode!=0&&attempt<=2;attempt++{ repair:=ctx.AgentTask(fmt.Sprintf("repair-generated-%d",attempt),"The generated workflow failed verification. Fix only the destination files. Preserve the source semantics and projection. Return every changed file.",map[string]any{"spec":spec,"source":source,"projection":projection,"stdout":checked.Stdout,"stderr":checked.Stderr},json.RawMessage(filesSchema));_ = json.Unmarshal(repair,&written);checked=ctx.RunCommand(fmt.Sprintf("verify-generated-retry-%d",attempt),command,600) } - if checked.ExitCode!=0{return yield.Outcome{},ctx.Blocked("the generated workflow still fails after two repair attempts")};ctx.Require(checked.ExitCode==0,"the generated workflow passes its fixture run",map[string]any{"exit_code":checked.ExitCode}) - agentFlag:=strings.Join(cfg.Agents,",");register:="printf fixture-register-ok";if !fixture{register="cd ../.. && "+cfg.Launcher+" register "+quote(spec.Destination)+" --agent "+quote(agentFlag)};registered:=ctx.RunCommand("register-generated",register,120);ctx.Require(registered.ExitCode==0,"the generated workflow is registered for the selected coding agents",map[string]any{"exit_code":registered.ExitCode}) - verify:="printf fixture-adapters-ok";if !fixture{verify="cd ../.. && "+cfg.Launcher+" doctor "+quote(spec.Destination)+" --agent "+quote(agentFlag)+" --test"};adapters:=ctx.RunCommand("verify-adapters",verify,600);ctx.Require(adapters.ExitCode==0,"the generated coding-agent adapters pass verification",map[string]any{"exit_code":adapters.ExitCode}) - return ctx.Complete(map[string]any{"mode":mode,"language":spec.Language,"destination":spec.Destination,"files":written.Files,"verified":true}) + mode:=ctx.AskUser("select-mode","What do you want to do with Yield?",yield.Option{Value:"learn",Label:"Learn"},yield.Option{Value:"create",Label:"Create"},yield.Option{Value:"convert",Label:"Convert"},yield.Option{Value:"check",Label:"Check"},yield.Option{Value:"repair",Label:"Repair"},yield.Option{Value:"upgrade",Label:"Upgrade"},yield.Option{Value:"register",Label:"Register"}) + if mode=="learn" { guide:=ctx.AgentTask("teach-yield","Teach the smallest relevant Yield concept for the user's request. Explain what remains in SKILL.md, what moves into code, fixtures, doctor, and registration. Give manual steps before mentioning this helper. Do not edit files or run commands.",map[string]any{"mode":mode},json.RawMessage(guideSchema));return ctx.Complete(map[string]any{"mode":mode,"changed":false,"guide":json.RawMessage(guide)}) } + raw:=ctx.AgentTask("collect-specification","Use the user's current request. For create or convert, return a safe kebab-case name, description, target language, new destination under skills/, and source_path for convert. For check, repair, upgrade, or register, return target_path for one existing workflow under skills/. Set requested_version only when the user explicitly requests one. Use empty strings for fields that do not apply. Do not write files.",map[string]any{"mode":mode},json.RawMessage(specSchema)); var spec specification; if err=json.Unmarshal(raw,&spec);err!=nil{return yield.Outcome{},err} + root,err:=filepath.Abs(filepath.Join("..","..")); if err!=nil{return yield.Outcome{},err};root,err=filepath.EvalSymlinks(root);if err!=nil{return yield.Outcome{},err} + creates:=mode=="create"||mode=="convert";target:=spec.TargetPath;if creates{target=spec.Destination;if !regexp.MustCompile(` + "`" + `^[a-z0-9]+(?:-[a-z0-9]+)*$` + "`" + `).MatchString(spec.Name)||spec.Description==""||!safe(root,target,true)||target==helperPath{return yield.Outcome{},ctx.Blocked("the destination must be a new named path under skills/")};available:=ctx.RunCommand("check-destination","cd ../.. && test ! -e "+quote(target),30);if available.ExitCode!=0{return yield.Outcome{},ctx.Blocked("the destination must be a new path under skills/")}}else if !safe(root,target,false){return yield.Outcome{},ctx.Blocked("the target workflow must be an existing path under skills/")} + if mode=="upgrade"&&target==helperPath{return yield.Outcome{},ctx.Blocked("the helper cannot upgrade itself during an active run; exit and run yskill helper install")} + if mode=="upgrade"&&spec.RequestedVersion!=""&&spec.RequestedVersion!=cfg.YieldVersion{return yield.Outcome{},ctx.Blocked("unsupported Yield version change; exit and run yskill helper install for the requested version")};fixture:=target=="skills/yield-workflow-builder-fixture" + if mode=="check" { command:="cd ../.. && "+cfg.Launcher+" doctor "+quote(target)+" --root .";if fixture{command="printf fixture-check-ok"};checked:=ctx.RunCommand("check-workflow",command,120);return ctx.Complete(map[string]any{"mode":mode,"target":target,"changed":false,"healthy":checked.ExitCode==0,"stdout":checked.Stdout,"stderr":checked.Stderr}) } + source:="";var projection json.RawMessage;if mode=="convert"{if !safe(root,spec.SourcePath,false){return yield.Outcome{},ctx.Blocked("the source skill must be inside the repository")};b,readErr:=os.ReadFile(filepath.Join(root,spec.SourcePath,"SKILL.md"));if readErr!=nil{return yield.Outcome{},ctx.Blocked("the source SKILL.md does not exist")};source=string(b);projection=ctx.AgentTask("project-semantics","Map every source clause exactly once using Pi(S)={(c,d,T,r)|c in clauses(S)}. YAML frontmatter is metadata, not a clause. Each top-level bullet is exactly one clause, including a compound sentence. For prose without bullets, treat each paragraph as one clause. Use disposition control, guidance, both, or excluded. Control needs a reachable code destination. Guidance needs a reachable skill or agent_task destination. Both needs both kinds. Excluded needs no destination and a non-empty reason. Name concrete destinations that the writer can create. Use the target language's canonical entrypoint for code: main.ts, main.py, main.go, or src/main.rs. Report uncertainty in unresolved and set ready false. Do not write files.",map[string]any{"source":source,"spec":spec},json.RawMessage(projectionSchema));var projected projectionResult;if err=json.Unmarshal(projection,&projected);err!=nil{return yield.Outcome{},err};if !projected.Ready||len(projected.Unresolved)>0{return yield.Outcome{},ctx.Blocked("the semantic projection has unresolved source clauses")}} + var flow json.RawMessage;var inspection any;if creates{flow=ctx.AgentTask("extract-flow","Extract or design the minimal workflow. Keep model judgment in agent_task. Put order, branches, commands, approvals, evidence, and finish rules in code.",map[string]any{"mode":mode,"spec":spec,"source":source,"projection":projection},json.RawMessage(flowSchema))}else{command:="cd ../.. && "+cfg.Launcher+" doctor "+quote(target)+" --root .";if fixture{command="printf fixture-inspect-ok"};observed:=ctx.RunCommand("inspect-workflow",command,120);inspection=map[string]any{"exit_code":observed.ExitCode,"stdout":observed.Stdout,"stderr":observed.Stderr}} + planRaw:=ctx.AgentTask("teach-and-plan","Teach the relevant Yield primitives, then return the exact files and commands proposed. For register, explain adapters. For repair, use doctor evidence. For upgrade, target the installed Yield version and include dependency and lockfile changes. Do not edit files or run commands.",map[string]any{"mode":mode,"spec":spec,"target":target,"flow":flow,"inspection":inspection,"yield_version":cfg.YieldVersion},json.RawMessage(planSchema));var plan planResult;if err=json.Unmarshal(planRaw,&plan);err!=nil{return yield.Outcome{},err} + if ctx.AskUser("approve-change",plan.Summary+" Apply this plan?",yield.Option{Value:"apply",Label:"Apply"},yield.Option{Value:"stop",Label:"Stop"})!="apply"{return yield.Outcome{},ctx.Refused("the developer declined the proposed Yield changes")} + agents:=strings.Join(cfg.Agents,",");verify:="printf fixture-ok";register:="printf fixture-register-ok";verifyAdapters:="printf fixture-adapters-ok";if !fixture{verify="cd ../.. && "+cfg.Launcher+" doctor "+quote(target)+" --root . --test";register="cd ../.. && "+cfg.Launcher+" register "+quote(target)+" --root . --agent "+quote(agents);verifyAdapters="cd ../.. && "+cfg.Launcher+" doctor "+quote(target)+" --root . --agent "+quote(agents)+" --test"} + if mode=="register"{checked:=ctx.RunCommand("verify-before-register",verify,600);ctx.Require(checked.ExitCode==0,"the workflow passes its fixture run",map[string]any{"exit_code":checked.ExitCode});registered:=ctx.RunCommand("register-workflow",register,120);ctx.Require(registered.ExitCode==0,"the workflow is registered",map[string]any{"exit_code":registered.ExitCode});adapters:=ctx.RunCommand("verify-adapters",verifyAdapters,600);ctx.Require(adapters.ExitCode==0,"the generated adapters pass verification",map[string]any{"exit_code":adapters.ExitCode});return ctx.Complete(map[string]any{"mode":mode,"target":target,"changed":true,"verified":true})} + task:="write-workflow";prompt:="Create the complete Yield workflow at the destination. Follow every semantic disposition. Write the program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. Keep useful model-facing guidance in SKILL.md; remove only duplicated sequencing. Do not edit outside the destination.";if !creates{task=mode+"-workflow";prompt="Apply only the approved operation to the target. For upgrade, update its manifest and language dependency to the supplied exact Yield version. Do not edit outside the target. Return every changed file."} + writtenRaw:=ctx.AgentTask(task,prompt,map[string]any{"mode":mode,"spec":spec,"target":target,"source":source,"projection":projection,"flow":flow,"plan":json.RawMessage(planRaw),"yield_version":cfg.YieldVersion},json.RawMessage(filesSchema));var written fileResult;if err=json.Unmarshal(writtenRaw,&written);err!=nil{return yield.Outcome{},err};if !filesStayInside(written.Files,target){return yield.Outcome{},ctx.Blocked("the reported changes escape the approved workflow directory")} + checked:=ctx.RunCommand("verify-workflow",verify,600);for attempt:=1;checked.ExitCode!=0&&attempt<=2;attempt++{repair:=ctx.AgentTask(fmt.Sprintf("repair-workflow-%d",attempt),"Verification failed. Repair only the approved workflow directory and return every changed file.",map[string]any{"mode":mode,"target":target,"stdout":checked.Stdout,"stderr":checked.Stderr},json.RawMessage(filesSchema));if err=json.Unmarshal(repair,&written);err!=nil{return yield.Outcome{},err};if !filesStayInside(written.Files,target){return yield.Outcome{},ctx.Blocked("the reported repair escapes the approved workflow directory")};checked=ctx.RunCommand(fmt.Sprintf("verify-workflow-retry-%d",attempt),verify,600)} + if checked.ExitCode!=0{return yield.Outcome{},ctx.Blocked("the workflow still fails after two repair attempts")};ctx.Require(true,"the workflow passes its fixture run",map[string]any{"exit_code":checked.ExitCode});registered:=ctx.RunCommand("register-workflow",register,120);ctx.Require(registered.ExitCode==0,"the workflow is registered",map[string]any{"exit_code":registered.ExitCode});adapters:=ctx.RunCommand("verify-adapters",verifyAdapters,600);ctx.Require(adapters.ExitCode==0,"the generated adapters pass verification",map[string]any{"exit_code":adapters.ExitCode}) + return ctx.Complete(map[string]any{"mode":mode,"target":target,"language":spec.Language,"files":written.Files,"changed":true,"verified":true}) }) } ` const bootstrapRust = `use serde_json::{json, Value}; use std::{fs, path::{Path, PathBuf}}; use yieldskill::{define_skill, Context, SkillResult}; -const SPEC_SCHEMA:&str=r#"{"type":"object","required":["name","description","language","destination","source_path"],"additionalProperties":false,"properties":{"name":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"description":{"type":"string","minLength":1},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string","minLength":1},"source_path":{"type":"string"}}}"#; +const HELPER_PATH:&str="skills/yield-workflow-builder"; +const SPEC_SCHEMA:&str=r#"{"type":"object","required":["name","description","language","destination","source_path","target_path","requested_version"],"additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string"},"language":{"enum":["typescript","python","go","rust"]},"destination":{"type":"string"},"source_path":{"type":"string"},"target_path":{"type":"string"},"requested_version":{"type":"string"}}}"#; const PROJECTION_SCHEMA:&str=r#"{"type":"object","required":["clauses","ready","unresolved"],"additionalProperties":false,"properties":{"clauses":{"type":"array","minItems":1,"items":{"type":"object","required":["source_clause","disposition","destinations","reason"],"additionalProperties":false,"properties":{"source_clause":{"type":"string","minLength":1},"disposition":{"enum":["control","guidance","both","excluded"]},"destinations":{"type":"array","items":{"type":"object","required":["kind","target"],"additionalProperties":false,"properties":{"kind":{"enum":["code","skill","agent_task"]},"target":{"type":"string","minLength":1}}}},"reason":{"type":"string"}}}},"ready":{"type":"boolean"},"unresolved":{"type":"array","items":{"type":"string","minLength":1}}}}"#; const FLOW_SCHEMA:&str=r#"{"type":"object","required":["summary","steps"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"steps":{"type":"array","minItems":1,"items":{"type":"object","required":["id","kind","description"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1},"kind":{"enum":["ask_user","agent_task","run_command","branch","require"]},"description":{"type":"string","minLength":1}}}}}}"#; -const FILES_SCHEMA:&str=r#"{"type":"object","required":["files"],"additionalProperties":false,"properties":{"files":{"type":"array","minItems":2,"items":{"type":"string","minLength":1}}}}"#; -fn safe(root:&Path,value:&str,destination:bool)->bool{let p=Path::new(value);if p.is_absolute()||p.components().any(|c|matches!(c,std::path::Component::ParentDir))||p.components().next().map(|c|c.as_os_str()!="skills").unwrap_or(true)||p.components().count()<=1{return false}let joined=root.join(p);let probe=if destination{joined.parent().unwrap_or(root)}else{joined.as_path()};probe.canonicalize().map(|resolved|resolved.starts_with(root)).unwrap_or(false)} +const FILES_SCHEMA:&str=r#"{"type":"object","required":["files"],"additionalProperties":false,"properties":{"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}"#; +const PLAN_SCHEMA:&str=r#"{"type":"object","required":["summary","primitives","files","commands"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"files":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"commands":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}"#; +const GUIDE_SCHEMA:&str=r#"{"type":"object","required":["summary","primitives","manual_steps","docs"],"additionalProperties":false,"properties":{"summary":{"type":"string","minLength":1},"primitives":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"manual_steps":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}},"docs":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}}}"#; +fn safe(root:&Path,value:&str,destination:bool)->bool{let p=Path::new(value);if value.is_empty()||p.is_absolute()||p.components().any(|c|matches!(c,std::path::Component::ParentDir))||p.components().next().map(|c|c.as_os_str()!="skills").unwrap_or(true)||p.components().count()<=1{return false}let joined=root.join(p);let probe=if destination{joined.parent().unwrap_or(root)}else{joined.as_path()};probe.canonicalize().map(|resolved|resolved.starts_with(root)).unwrap_or(false)} +fn valid_name(value:&str)->bool{!value.is_empty()&&value.split('-').all(|part|!part.is_empty()&&part.bytes().all(|b|b.is_ascii_lowercase()||b.is_ascii_digit()))} fn quote(value:&str)->String{format!("'{}'",value.replace('\'',"'\\''"))} +fn files_stay_inside(value:&Value,target:&str)->bool{value["files"].as_array().map(|files|files.iter().all(|file|file.as_str().map(|path|{let p=Path::new(path);p==Path::new(target)||p.starts_with(Path::new(target))}).unwrap_or(false))).unwrap_or(false)} fn program(ctx:&mut Context)->SkillResult{ let cfg:Value=serde_json::from_slice(&fs::read("builder.json").expect("builder.json must be readable")).expect("builder.json must be valid JSON"); - let mode=ctx.ask_user("select-mode","Create a workflow from a description, or convert an existing SKILL.md?",&[("create","Create"),("convert","Convert")]); - let spec=ctx.agent_task("collect-specification","Use the user's current request. Return a safe kebab-case name, a short description, the target language, a new destination under skills/, and source_path for convert mode. Do not write files.",Some(json!({"mode":mode})),Some(serde_json::from_str(SPEC_SCHEMA).unwrap())); - let destination=spec["destination"].as_str().unwrap_or("");let root=PathBuf::from("../..").canonicalize().expect("repository root must be readable");if !safe(&root,destination,true)||destination=="skills/yield-workflow-builder"{return Err(ctx.blocked("the destination must be a new path under skills/"))}let available=ctx.run_command("check-destination",&format!("cd ../.. && test ! -e {}",quote(destination)),30);if available.exit_code!=0{return Err(ctx.blocked("the destination must be a new path under skills/"))} + let mode=ctx.ask_user("select-mode","What do you want to do with Yield?",&[("learn","Learn"),("create","Create"),("convert","Convert"),("check","Check"),("repair","Repair"),("upgrade","Upgrade"),("register","Register")]); + if mode=="learn"{let guide=ctx.agent_task("teach-yield","Teach the smallest relevant Yield concept for the user's request. Explain what remains in SKILL.md, what moves into code, fixtures, doctor, and registration. Give manual steps before mentioning this helper. Do not edit files or run commands.",Some(json!({"mode":mode})),Some(serde_json::from_str(GUIDE_SCHEMA).unwrap()));return Ok(json!({"mode":mode,"changed":false,"guide":guide}))} + let spec=ctx.agent_task("collect-specification","Use the user's current request. For create or convert, return a safe kebab-case name, description, target language, new destination under skills/, and source_path for convert. For check, repair, upgrade, or register, return target_path for one existing workflow under skills/. Set requested_version only when the user explicitly requests one. Use empty strings for fields that do not apply. Do not write files.",Some(json!({"mode":mode})),Some(serde_json::from_str(SPEC_SCHEMA).unwrap())); + let root=PathBuf::from("../..").canonicalize().expect("repository root must be readable");let creates=mode=="create"||mode=="convert";let target=if creates{spec["destination"].as_str().unwrap_or("")}else{spec["target_path"].as_str().unwrap_or("")}; + if creates{if !valid_name(spec["name"].as_str().unwrap_or(""))||spec["description"].as_str().unwrap_or("").is_empty()||!safe(&root,target,true)||target==HELPER_PATH{return Err(ctx.blocked("the destination must be a new named path under skills/"))}let available=ctx.run_command("check-destination",&format!("cd ../.. && test ! -e {}",quote(target)),30);if available.exit_code!=0{return Err(ctx.blocked("the destination must be a new path under skills/"))}}else if !safe(&root,target,false){return Err(ctx.blocked("the target workflow must be an existing path under skills/"))} + if mode=="upgrade"&&target==HELPER_PATH{return Err(ctx.blocked("the helper cannot upgrade itself during an active run; exit and run yskill helper install"))} + if mode=="upgrade"&&!spec["requested_version"].as_str().unwrap_or("").is_empty()&&spec["requested_version"]!=cfg["yield_version"]{return Err(ctx.blocked("unsupported Yield version change; exit and run yskill helper install for the requested version"))}let fixture=target=="skills/yield-workflow-builder-fixture"; + let launcher=cfg["launcher"].as_str().unwrap();if mode=="check"{let command=if fixture{"printf fixture-check-ok".to_string()}else{format!("cd ../.. && {} doctor {} --root .",launcher,quote(target))};let checked=ctx.run_command("check-workflow",&command,120);return Ok(json!({"mode":mode,"target":target,"changed":false,"healthy":checked.exit_code==0,"stdout":checked.stdout,"stderr":checked.stderr}))} let mut source=String::new();let mut projection=Value::Null;if mode=="convert"{let source_path=spec["source_path"].as_str().unwrap_or("");if !safe(&root,source_path,false){return Err(ctx.blocked("the source skill must be inside the repository"))};source=fs::read_to_string(root.join(source_path).join("SKILL.md")).map_err(|_|ctx.blocked("the source SKILL.md does not exist"))?;projection=ctx.agent_task("project-semantics","Map every source clause exactly once using Pi(S)={(c,d,T,r)|c in clauses(S)}. YAML frontmatter is metadata, not a clause. Each top-level bullet is exactly one clause, including a compound sentence. For prose without bullets, treat each paragraph as one clause. Use disposition control, guidance, both, or excluded. Control needs a reachable code destination. Guidance needs a reachable skill or agent_task destination. Both needs both kinds. Excluded needs no destination and a non-empty reason. Name concrete destinations that the writer can create. Use the target language's canonical entrypoint for code: main.ts, main.py, main.go, or src/main.rs. Report uncertainty in unresolved and set ready false. Do not write files.",Some(json!({"source":source,"spec":spec})),Some(serde_json::from_str(PROJECTION_SCHEMA).unwrap()));if !projection["ready"].as_bool().unwrap_or(false)||projection["unresolved"].as_array().map(|v|!v.is_empty()).unwrap_or(true){return Err(ctx.blocked("the semantic projection has unresolved source clauses"))}} - let flow=ctx.agent_task("extract-flow","Extract or design the minimal workflow control flow. Follow the semantic projection. Keep model judgment in agent_task operations. Put order, branches, commands, approvals, evidence requirements, and finish rules in code.",Some(json!({"mode":mode,"spec":spec,"source":source,"projection":projection})),Some(serde_json::from_str(FLOW_SCHEMA).unwrap())); - let mut written=ctx.agent_task("write-workflow","Create the complete Yield skill workflow at the destination. Follow every semantic disposition. Write the language program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. A thin SKILL.md removes duplicated sequencing, not useful guidance. Do not edit files outside the destination. Return every file written.",Some(json!({"mode":mode,"spec":spec,"source":source,"projection":projection,"flow":flow})),Some(serde_json::from_str(FILES_SCHEMA).unwrap())); - let fixture=destination=="skills/yield-workflow-builder-fixture";let launcher=cfg["launcher"].as_str().unwrap();let command=if fixture{"printf fixture-ok".to_string()}else{format!("cd ../.. && {} doctor {} --test",launcher,quote(destination))};let mut checked=ctx.run_command("verify-generated",&command,600); - for attempt in 1..=2{if checked.exit_code==0{break}written=ctx.agent_task(&format!("repair-generated-{attempt}"),"The generated workflow failed verification. Fix only the destination files. Preserve the source semantics and projection. Return every changed file.",Some(json!({"spec":spec,"source":source,"projection":projection,"stdout":checked.stdout,"stderr":checked.stderr})),Some(serde_json::from_str(FILES_SCHEMA).unwrap()));checked=ctx.run_command(&format!("verify-generated-retry-{attempt}"),&command,600)} - if checked.exit_code!=0{return Err(ctx.blocked("the generated workflow still fails after two repair attempts"))}ctx.require(true,"the generated workflow passes its fixture run",Some(&json!({"exit_code":checked.exit_code}))); - let agents=cfg["agents"].as_array().unwrap().iter().filter_map(|v|v.as_str()).collect::>().join(",");let register=if fixture{"printf fixture-register-ok".to_string()}else{format!("cd ../.. && {} register {} --agent {}",launcher,quote(destination),quote(&agents))};let registered=ctx.run_command("register-generated",®ister,120);ctx.require(registered.exit_code==0,"the generated workflow is registered for the selected coding agents",Some(&json!({"exit_code":registered.exit_code}))); - let verify=if fixture{"printf fixture-adapters-ok".to_string()}else{format!("cd ../.. && {} doctor {} --agent {} --test",launcher,quote(destination),quote(&agents))};let adapters=ctx.run_command("verify-adapters",&verify,600);ctx.require(adapters.exit_code==0,"the generated coding-agent adapters pass verification",Some(&json!({"exit_code":adapters.exit_code}))); - Ok(json!({"mode":mode,"language":spec["language"],"destination":destination,"files":written["files"],"verified":true})) + let flow=if creates{ctx.agent_task("extract-flow","Extract or design the minimal workflow. Keep model judgment in agent_task. Put order, branches, commands, approvals, evidence, and finish rules in code.",Some(json!({"mode":mode,"spec":spec,"source":source,"projection":projection})),Some(serde_json::from_str(FLOW_SCHEMA).unwrap()))}else{Value::Null}; + let inspection=if creates{Value::Null}else{let command=if fixture{"printf fixture-inspect-ok".to_string()}else{format!("cd ../.. && {} doctor {} --root .",launcher,quote(target))};let observed=ctx.run_command("inspect-workflow",&command,120);json!({"exit_code":observed.exit_code,"stdout":observed.stdout,"stderr":observed.stderr})}; + let plan=ctx.agent_task("teach-and-plan","Teach the relevant Yield primitives, then return the exact files and commands proposed. For register, explain adapters. For repair, use doctor evidence. For upgrade, target the installed Yield version and include dependency and lockfile changes. Do not edit files or run commands.",Some(json!({"mode":mode,"spec":spec,"target":target,"flow":flow,"inspection":inspection,"yield_version":cfg["yield_version"]})),Some(serde_json::from_str(PLAN_SCHEMA).unwrap())); + let approval=ctx.ask_user("approve-change",&format!("{} Apply this plan?",plan["summary"].as_str().unwrap_or("Apply the proposed Yield changes.")),&[("apply","Apply"),("stop","Stop")]);if approval!="apply"{return Err(ctx.refused("the developer declined the proposed Yield changes"))} + let agents=cfg["agents"].as_array().unwrap().iter().filter_map(|v|v.as_str()).collect::>().join(",");let verify=if fixture{"printf fixture-ok".to_string()}else{format!("cd ../.. && {} doctor {} --root . --test",launcher,quote(target))};let register=if fixture{"printf fixture-register-ok".to_string()}else{format!("cd ../.. && {} register {} --root . --agent {}",launcher,quote(target),quote(&agents))};let verify_adapters=if fixture{"printf fixture-adapters-ok".to_string()}else{format!("cd ../.. && {} doctor {} --root . --agent {} --test",launcher,quote(target),quote(&agents))}; + if mode=="register"{let checked=ctx.run_command("verify-before-register",&verify,600);ctx.require(checked.exit_code==0,"the workflow passes its fixture run",Some(&json!({"exit_code":checked.exit_code})));let registered=ctx.run_command("register-workflow",®ister,120);ctx.require(registered.exit_code==0,"the workflow is registered",Some(&json!({"exit_code":registered.exit_code})));let adapters=ctx.run_command("verify-adapters",&verify_adapters,600);ctx.require(adapters.exit_code==0,"the generated adapters pass verification",Some(&json!({"exit_code":adapters.exit_code})));return Ok(json!({"mode":mode,"target":target,"changed":true,"verified":true}))} + let task=if creates{"write-workflow"}else if mode=="repair"{"repair-workflow"}else{"upgrade-workflow"};let prompt=if creates{"Create the complete Yield workflow at the destination. Follow every semantic disposition. Write the program, SKILL.md, exact-version dependencies, skill.json, and self-contained fixtures. Keep useful model-facing guidance in SKILL.md; remove only duplicated sequencing. Do not edit outside the destination."}else{"Apply only the approved operation to the target. For upgrade, update its manifest and language dependency to the supplied exact Yield version. Do not edit outside the target. Return every changed file."};let mut written=ctx.agent_task(task,prompt,Some(json!({"mode":mode,"spec":spec,"target":target,"source":source,"projection":projection,"flow":flow,"plan":plan,"yield_version":cfg["yield_version"]})),Some(serde_json::from_str(FILES_SCHEMA).unwrap()));if !files_stay_inside(&written,target){return Err(ctx.blocked("the reported changes escape the approved workflow directory"))} + let mut checked=ctx.run_command("verify-workflow",&verify,600);for attempt in 1..=2{if checked.exit_code==0{break}written=ctx.agent_task(&format!("repair-workflow-{attempt}"),"Verification failed. Repair only the approved workflow directory and return every changed file.",Some(json!({"mode":mode,"target":target,"stdout":checked.stdout,"stderr":checked.stderr})),Some(serde_json::from_str(FILES_SCHEMA).unwrap()));if !files_stay_inside(&written,target){return Err(ctx.blocked("the reported repair escapes the approved workflow directory"))}checked=ctx.run_command(&format!("verify-workflow-retry-{attempt}"),&verify,600)} + if checked.exit_code!=0{return Err(ctx.blocked("the workflow still fails after two repair attempts"))}ctx.require(true,"the workflow passes its fixture run",Some(&json!({"exit_code":checked.exit_code})));let registered=ctx.run_command("register-workflow",®ister,120);ctx.require(registered.exit_code==0,"the workflow is registered",Some(&json!({"exit_code":registered.exit_code})));let adapters=ctx.run_command("verify-adapters",&verify_adapters,600);ctx.require(adapters.exit_code==0,"the generated adapters pass verification",Some(&json!({"exit_code":adapters.exit_code}))); + Ok(json!({"mode":mode,"target":target,"language":spec["language"],"files":written["files"],"changed":true,"verified":true})) } fn main(){define_skill(program);} ` diff --git a/cmd/yskill/bootstrap_test.go b/cmd/yskill/bootstrap_test.go index 7b9862d..e00f4d2 100644 --- a/cmd/yskill/bootstrap_test.go +++ b/cmd/yskill/bootstrap_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "os" "os/exec" "path/filepath" @@ -38,6 +39,20 @@ func TestBootstrapDryRunDoesNotWrite(t *testing.T) { } } +func TestHelperInstallUsesBootstrapContract(t *testing.T) { + withBootstrapTestState(t) + root := t.TempDir() + if err := cmdHelper([]string{"install", "--root", root, "--language", "typescript", "--agent", "codex", "--dry-run"}); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(root, "skills")); !os.IsNotExist(err) { + t.Fatalf("helper dry run wrote skills directory: %v", err) + } + if err := cmdHelper(nil); err == nil || !strings.Contains(err.Error(), "requires a subcommand") { + t.Fatalf("missing helper subcommand returned %v", err) + } +} + func TestBootstrapCancellationDoesNotWrite(t *testing.T) { withBootstrapTestState(t) bootstrapInput = bytes.NewBufferString("no\n") @@ -119,8 +134,9 @@ func TestBootstrapRefusesAdapterSymlinkEscape(t *testing.T) { func TestBuilderTemplatesExposeEquivalentOperations(t *testing.T) { profile := bootstrapProfile{YieldVersion: "1.2.3", Agents: []string{"codex"}} - want := []string{"select-mode", "collect-specification", "check-destination", "project-semantics", "extract-flow", "write-workflow", "verify-generated", "repair-generated-", "register-generated", "verify-adapters"} + want := []string{"learn", "create", "convert", "check", "repair", "upgrade", "register", "select-mode", "teach-yield", "collect-specification", "check-destination", "project-semantics", "extract-flow", "teach-and-plan", "approve-change", "write-workflow", "verify-workflow", "repair-workflow-", "register-workflow", "verify-adapters", "yskill helper install"} projectionContract := []string{"source_clause", "disposition", "destinations", "reason", "control", "guidance", "both", "excluded", "ready", "unresolved"} + repairLimit := map[string]string{"typescript": "attempt<=2", "python": "range(1, 3)", "go": "attempt<=2", "rust": "1..=2"} for _, language := range []string{"typescript", "python", "go", "rust"} { files, _, err := renderBootstrapSkill(language, profile) if err != nil { @@ -142,6 +158,17 @@ func TestBuilderTemplatesExposeEquivalentOperations(t *testing.T) { t.Errorf("%s builder projection is missing %s", language, field) } } + if !strings.Contains(program, repairLimit[language]) || strings.Contains(program, "repair-workflow-3") { + t.Errorf("%s builder does not enforce the two-attempt repair limit", language) + } + skill := files["SKILL.md"] + parts := strings.SplitN(skill, "---", 3) + if len(parts) != 3 || strings.Count(parts[1], "\n") != 3 || !strings.Contains(parts[1], "\nname: yield-workflow-builder\n") || !strings.Contains(parts[1], "\ndescription: ") { + t.Errorf("%s generated SKILL.md frontmatter is invalid: %q", language, parts[1]) + } + if strings.Count(skill, "\n") >= 500 { + t.Errorf("%s generated SKILL.md exceeds the concise skill limit", language) + } for _, downstream := range []string{"source,projection", `"source":source,"projection":projection`} { if strings.Contains(program, downstream) { goto hasProjectionContext @@ -274,7 +301,7 @@ func TestBuilderTemplatesCompile(t *testing.T) { } } -func TestBuilderFixturesReachCompletedAcrossLanguages(t *testing.T) { +func TestBuilderModeFixturesAcrossLanguages(t *testing.T) { oldVersion := version version = "0.1.0" t.Cleanup(func() { version = oldVersion }) @@ -342,13 +369,130 @@ func TestBuilderFixturesReachCompletedAcrossLanguages(t *testing.T) { } runTestCommand(t, repoRoot, "go", "build", "-ldflags", "-X main.version=0.1.0", "-o", runtimePath, "./cmd/yskill") } - if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err != nil { - t.Fatalf("%s builder fixture did not complete: %v", language, err) + source := filepath.Join(root, "skills", "source-fixture") + if err := os.MkdirAll(source, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(source, "SKILL.md"), []byte("---\nname: source-fixture\ndescription: Test then ask before publishing.\n---\n\nRun tests. Ask before publishing.\n"), 0o644); err != nil { + t.Fatal(err) + } + for _, mode := range []string{"learn", "create", "convert"} { + writeBuilderResponses(t, dir, builderModeResponses(t, mode, "apply")) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err != nil { + t.Fatalf("%s %s fixture did not complete: %v", language, mode, err) + } + } + target := filepath.Join(root, "skills", "yield-workflow-builder-fixture") + if err := os.MkdirAll(target, 0o755); err != nil { + t.Fatal(err) + } + marker := filepath.Join(target, "SKILL.md") + if err := os.WriteFile(marker, []byte("fixture-owned\n"), 0o644); err != nil { + t.Fatal(err) + } + for _, mode := range []string{"check", "repair", "upgrade", "register"} { + writeBuilderResponses(t, dir, builderModeResponses(t, mode, "apply")) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err != nil { + t.Fatalf("%s %s fixture did not complete: %v", language, mode, err) + } + } + writeBuilderResponses(t, dir, builderModeResponses(t, "repair", "stop")) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err == nil || !strings.Contains(err.Error(), "terminal status refused") { + t.Fatalf("%s declined mutation returned %v", language, err) + } + if got := readTestFile(t, marker); got != "fixture-owned\n" { + t.Fatalf("%s declined mutation changed the target: %q", language, got) + } + unsupported := strings.Replace(builderModeResponses(t, "upgrade", "apply"), `"requested_version": "0.1.0"`, `"requested_version": "9.9.9"`, 1) + writeBuilderResponses(t, dir, unsupported) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err == nil || !strings.Contains(err.Error(), "unsupported Yield version change") { + t.Fatalf("%s unsupported upgrade returned %v", language, err) + } + selfUpgrade := strings.Replace(builderModeResponses(t, "upgrade", "apply"), `"target_path": "skills/yield-workflow-builder-fixture"`, `"target_path": "skills/yield-workflow-builder"`, 1) + writeBuilderResponses(t, dir, selfUpgrade) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err == nil || !strings.Contains(err.Error(), "cannot upgrade itself") { + t.Fatalf("%s self-upgrade returned %v", language, err) + } + escape := strings.Replace(builderModeResponses(t, "check", "apply"), `"target_path": "skills/yield-workflow-builder-fixture"`, `"target_path": "../outside"`, 1) + writeBuilderResponses(t, dir, escape) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err == nil || !strings.Contains(err.Error(), "existing path under skills") { + t.Fatalf("%s path escape returned %v", language, err) + } + writeBuilderResponses(t, dir, builderModeResponses(t, "create", "apply")) + if err := cmdDoctor([]string{dir, "--root", root, "--test"}); err == nil || !strings.Contains(err.Error(), "new path under skills") { + t.Fatalf("%s existing destination returned %v", language, err) } }) } } +func writeBuilderResponses(t *testing.T, dir, responses string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, "fixtures", "responses.json"), []byte(responses), 0o644); err != nil { + t.Fatal(err) + } +} + +func builderModeResponses(t *testing.T, mode, approval string) string { + t.Helper() + responses := map[string]any{"select-mode": map[string]string{"value": mode}} + if mode == "learn" { + responses["teach-yield"] = map[string]any{ + "summary": "Keep control flow in code.", "primitives": []string{"runCommand"}, + "manual_steps": []string{"init", "write fixtures", "doctor --test", "register"}, "docs": []string{"docs/quickstart.md"}, + } + return marshalBuilderResponses(t, responses) + } + spec := map[string]any{ + "name": "", "description": "", "language": "go", "destination": "", "source_path": "", + "target_path": "skills/yield-workflow-builder-fixture", "requested_version": "", + } + if mode == "create" || mode == "convert" { + spec["name"] = "yield-workflow-builder-fixture" + spec["description"] = "Create a harmless fixture workflow." + spec["destination"] = "skills/yield-workflow-builder-fixture" + if mode == "convert" { + spec["source_path"] = "skills/source-fixture" + responses["project-semantics"] = map[string]any{ + "clauses": []any{map[string]any{"source_clause": "Run tests.", "disposition": "control", "destinations": []any{map[string]string{"kind": "code", "target": "main"}}, "reason": "Executable gate."}}, + "ready": true, "unresolved": []string{}, + } + } + responses["extract-flow"] = map[string]any{ + "summary": "Check then complete.", "steps": []any{map[string]string{"id": "check", "kind": "run_command", "description": "Run a check."}}, + } + } + if mode == "upgrade" { + spec["requested_version"] = "0.1.0" + } + responses["collect-specification"] = spec + if mode == "check" { + return marshalBuilderResponses(t, responses) + } + responses["teach-and-plan"] = map[string]any{ + "summary": "Apply the fixture plan.", "primitives": []string{"Require binds completion to evidence."}, + "files": []string{"skills/yield-workflow-builder-fixture/SKILL.md"}, "commands": []string{"yskill doctor --test"}, + } + responses["approve-change"] = map[string]string{"value": approval} + if approval == "apply" && mode != "register" { + task := mode + "-workflow" + if mode == "create" || mode == "convert" { + task = "write-workflow" + } + responses[task] = map[string]any{"files": []string{"skills/yield-workflow-builder-fixture/SKILL.md"}} + } + return marshalBuilderResponses(t, responses) +} + +func marshalBuilderResponses(t *testing.T, responses map[string]any) string { + t.Helper() + b, err := json.MarshalIndent(responses, "", " ") + if err != nil { + t.Fatal(err) + } + return string(b) + "\n" +} + func runTestCommand(t *testing.T, dir, name string, args ...string) { t.Helper() if _, err := exec.LookPath(name); err != nil { diff --git a/cmd/yskill/main.go b/cmd/yskill/main.go index 152fe09..832e905 100644 --- a/cmd/yskill/main.go +++ b/cmd/yskill/main.go @@ -27,9 +27,10 @@ import ( const usage = `yskill — run and resume skill workflows Usage: - yskill bootstrap install the governed workflow builder + yskill helper install install the optional Yield developer helper [--language typescript|python|go|rust] [--agent cursor,codex,...|auto] [--root repo] [--dry-run] [--yes] + yskill bootstrap compatibility alias for helper install yskill init scaffold a skill workflow (or wrap an existing prose skill) [--language typescript|python|go|rust] [--description text] yskill register expose one skill workflow to coding agents @@ -91,6 +92,8 @@ func main() { } var err error switch os.Args[1] { + case "helper": + err = cmdHelper(os.Args[2:]) case "bootstrap": err = cmdBootstrap(os.Args[2:]) case "init": @@ -131,6 +134,18 @@ func main() { } } +func cmdHelper(args []string) error { + if len(args) == 0 { + return fmt.Errorf("helper requires a subcommand; use 'yskill helper install'") + } + switch args[0] { + case "install": + return cmdBootstrap(args[1:]) + default: + return fmt.Errorf("unknown helper subcommand %q; use 'yskill helper install'", args[0]) + } +} + func cmdRun(args []string) error { fs := flag.NewFlagSet("run", flag.ExitOnError) input := fs.String("input", "", "path to a JSON input file") diff --git a/docs/README.md b/docs/README.md index 877942f..91efe67 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,8 +28,8 @@ and start it. ## Start here -1. [Bootstrap the workflow builder](quickstart.md) — install, test, and - register it for your coding agent. +1. [Create your first workflow](quickstart.md) — install the runtime, write a + workflow and fixture, test it, and register it. 2. [Understand skill workflows](skill-workflows.md) — the canonical workflow, generated adapter, and execution boundary. 3. [Read the public guide](https://yield.operatorstack.systems/docs/) — the diff --git a/docs/agent-setup.md b/docs/agent-setup.md index b852a05..6ecd3d0 100644 --- a/docs/agent-setup.md +++ b/docs/agent-setup.md @@ -69,26 +69,27 @@ Use the review skill to check the current branch. The host owns how the request is presented. The generated adapter starts the canonical workflow under `skills/review`; it does not contain a second copy. -## Set up the workflow builder +## Optional: set up the developer helper -Run the native bootstrap command from the repository root: +Package installation does not create skills or adapters. After using the +manual workflow, install the guided helper explicitly from the repository root: ```bash # TypeScript -npm create @operatorstack/yield@latest +npm exec -- yskill helper install --language typescript # Python -uvx --from yieldskill yskill bootstrap --language python +uvx --from yieldskill yskill helper install --language python # Rust cargo install yieldskill --root .yield --locked -.yield/bin/yskill bootstrap --root . --language rust +.yield/bin/yskill helper install --root . --language rust # Go -go run github.com/operatorstack/yield/cmd/yskill@latest bootstrap --root . --language go +go run github.com/operatorstack/yield/cmd/yskill@latest helper install --root . --language go ``` -Bootstrap shows every proposed change. Confirm the plan. Restart the coding +The installer shows every proposed change. Confirm the plan. Restart the coding agent after registration. To create a new skill workflow, ask: ```text @@ -101,8 +102,13 @@ To convert an existing `SKILL.md`, ask: Use Yield to convert my existing release SKILL.md into a tested skill workflow. ``` -The builder collects the specification, writes the skill workflow, runs its -fixture, allows two repair attempts, registers adapters, and verifies them. +The helper can teach, create, convert, check, repair, upgrade, and register. +Before a mutation it explains the primitive, previews exact files and commands, +and asks for approval. It allows two repair attempts and verifies the workflow +and adapters. + +`yskill bootstrap` and `npm create @operatorstack/yield@latest` remain +compatibility aliases. ## Questions and agent results diff --git a/docs/convert-existing-skill.md b/docs/convert-existing-skill.md index 8c3c1ef..5409c69 100644 --- a/docs/convert-existing-skill.md +++ b/docs/convert-existing-skill.md @@ -3,19 +3,20 @@ The workflow builder can convert an existing `SKILL.md` into a tested skill workflow. The source and destination must stay inside the repository. -## 1. Install the builder +## 1. Install the optional helper -Run the bootstrap command for the project language. For example: +Package installation does not add the helper. Install it explicitly for the +project language. For example: ```bash -npm create @operatorstack/yield@latest +npm exec -- yskill helper install --language typescript ``` See the [quickstart](quickstart.md) for Python, Rust, and Go commands. ## 2. Restart the coding agent -Restart the session after bootstrap registers the adapter. +Restart the session after the installer registers the adapter. ## 3. Request the conversion diff --git a/docs/quickstart.md b/docs/quickstart.md index c3ff512..b159de2 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,67 +1,91 @@ # Create your first skill workflow -Bootstrap installs a tested workflow builder for your coding agent. Run one -command from the repository root. +Start with the runtime and manual workflow. Package installation alone never +creates a skill or coding-agent adapter. -## 1. Run bootstrap +## 1. Install Yield -Choose the command for the project language: +Choose the package for your project: ```bash # TypeScript -npm create @operatorstack/yield@latest +npm install --save-exact @operatorstack/yield # Python -uvx --from yieldskill yskill bootstrap --language python +python -m pip install yieldskill # Rust cargo install yieldskill --root .yield --locked -.yield/bin/yskill bootstrap --root . --language rust # Go -go run github.com/operatorstack/yield/cmd/yskill@latest bootstrap --root . --language go +mkdir -p .yield/bin +GOBIN="$PWD/.yield/bin" go install github.com/operatorstack/yield/cmd/yskill@latest ``` -Bootstrap detects installed Codex, Claude Code, and Cursor project adapters. -Use `--agent codex,claude-code,cursor` to select them explicitly. +Use `npm exec -- yskill`, `python -m yieldskill`, or `.yield/bin/yskill` as the +launcher in the following steps. The examples below use TypeScript; substitute +your launcher and language when using another SDK. -## 2. Review the plan +## 2. Create the workflow and fixture -Yield prints every file, dependency, and command that it will change. Confirm -the plan to continue. Use `--dry-run` to stop after the plan. Use `--yes` only -when another trusted process already approved the changes. +```bash +npm exec -- yskill init skills/release \ + --language typescript \ + --description "Test, review, approve, publish, and verify a package." +``` -Yield writes the builder under `skills/yield-workflow-builder`. It stores local -bootstrap state under ignored `.yield/`. It does not use an install hook. +Edit the generated program under `skills/release/`. Keep deterministic command +execution, approval, gates, and finish rules in code. Put fixture answers for +agent and user operations in `skills/release/fixtures/responses.json`. -## 3. Restart the coding agent +## 3. Test it -Restart the coding-agent session after registration. This lets the agent find -the new adapter. +```bash +npm exec -- yskill doctor skills/release --test +``` -## 4. Ask for the skill workflow +This runs the fixture to a terminal outcome without leaving a run journal. -To create a new skill workflow, ask: +## 4. Register it -```text -Use Yield to create a tested skill workflow for releasing my package. +```bash +npm exec -- yskill register skills/release +npm exec -- yskill doctor skills/release --agent codex,cursor,claude-code --test ``` -To convert an existing `SKILL.md`, ask: +Registration creates only small discovery adapters. The canonical workflow, +dependencies, and fixtures remain under `skills/release/`. Restart the coding +agent after registration. + +## 5. Run it + +Ask the coding agent to use the registered skill: ```text -Use Yield to convert my existing release SKILL.md into a tested skill workflow. +Use the release skill to publish this package. ``` -The builder can start from a description. It can also convert an existing -`SKILL.md`. It writes the workflow, runs `doctor --test`, allows two repair -attempts, registers adapters, and verifies them. +## Optional: install the developer helper + +After learning the manual flow, install guided assistance explicitly: -The workflow remains under `skills/`. Generated agent adapters contain only -the commands that start and resume it. +```bash +# TypeScript +npm exec -- yskill helper install --language typescript + +# Python +uvx --from yieldskill yskill helper install --language python + +# Rust +.yield/bin/yskill helper install --root . --language rust + +# Go +go run github.com/operatorstack/yield/cmd/yskill@latest helper install --root . --language go +``` -## Advanced: build manually +Review the printed files and commands, approve the plan, then restart the +coding agent. The optional `yield-workflow-builder` can teach the primitives +and guide create, convert, check, repair, upgrade, and register operations. -Use [`yskill init`](reference/cli.md#init) when you want to write the program -and fixtures yourself. See the [primitive guides](primitives/README.md) and -[working examples](examples.md). +`yskill bootstrap` and `npm create @operatorstack/yield@latest` remain +compatibility aliases. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ed21173..6e452d7 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3,10 +3,10 @@ `yskill` runs and resumes skill workflows. It owns run logs, validates responses, executes commands, and starts the skill workflow. It comes with each language package. -## `bootstrap` +## `helper install` ```bash -yskill bootstrap +yskill helper install [--language typescript|python|rust|go] [--agent auto|cursor,codex,claude-code] [--root repository] @@ -14,16 +14,18 @@ yskill bootstrap [--yes] ``` -Detects the repository, language, and installed coding agents. It shows every +Explicitly installs the optional developer helper. Package installation alone +does not create skills or adapters. The command detects the repository, +language, and installed coding agents. It shows every proposed change before it writes. It asks for confirmation unless `--yes` is set. It installs and tests `skills/yield-workflow-builder`, registers the selected adapters, and verifies them. -Bootstrap stores local state under ignored `.yield/`. It refuses paths outside +The installer stores local state under ignored `.yield/`. It refuses paths outside the repository, symlink escapes, existing destinations, and user-owned adapter files. Use `--root` for a directory that is not a Git repository. -Bootstrap can change only these repository locations: +The installer can change only these repository locations: - `.yield/.gitignore` and `.yield/bootstrap.json` - `.yield/bin/yskill` for Go and Rust @@ -31,9 +33,12 @@ Bootstrap can change only these repository locations: - selected generated agent adapter paths The TypeScript dependency install also creates ignored `node_modules/` content. -Bootstrap runs only the dependency preparation command shown in the plan, +The installer runs only the dependency preparation command shown in the plan, `doctor --test`, and adapter registration. It does not use install hooks. +`yskill bootstrap` is a compatibility alias. The +`npm create @operatorstack/yield@latest` initializer calls the same installer. + ## `init` ```bash diff --git a/docs/skill-workflows.md b/docs/skill-workflows.md index 270d345..0ac6b31 100644 --- a/docs/skill-workflows.md +++ b/docs/skill-workflows.md @@ -41,13 +41,14 @@ Yield does not replace skills. It gives repeatable skill behavior an executable boundary that can be tested, paused, resumed, and exposed to more than one coding agent. -## Build one with a coding agent - -`yskill bootstrap` installs the `yield-workflow-builder` skill workflow. The -builder accepts a description or an existing `SKILL.md`. It extracts the -control flow, writes one language implementation, runs its fixture, repairs at -most twice, and verifies the generated adapters. It refuses success when any -verification step is missing. +## Optional developer helper + +After learning the manual workflow, run `yskill helper install` to add the +optional `yield-workflow-builder`. Package installation alone does not add it. +The helper can teach the primitives and guide create, convert, check, repair, +upgrade, and register operations. It previews mutations and requests approval, +repairs at most twice, and requires workflow and adapter verification before +success. Next: [create your first skill workflow](quickstart.md) or [register an existing one](agent-setup.md). diff --git a/packaging/assemble.test.mjs b/packaging/assemble.test.mjs index 5113a9e..0cf3c38 100644 --- a/packaging/assemble.test.mjs +++ b/packaging/assemble.test.mjs @@ -49,10 +49,12 @@ test("assembles two public npm packages and six matching npm and Python runtimes assert.equal(initializer.version, "1.2.3") assert.equal(initializer.dependencies["@operatorstack/yield"], "1.2.3") assert.equal(initializer.publishConfig.provenance, true) + assert.equal(main.scripts?.postinstall, undefined) + assert.equal(initializer.scripts?.postinstall, undefined) assert.match(await readFile(join(output, "npm/create-yield/LICENSE"), "utf8"), /MIT License/) assert.match( await readFile(join(output, "npm/create-yield/bin/create-yield.mjs"), "utf8"), - /bootstrap/, + /helper", "install/, ) const initializerPack = JSON.parse( execFileSync("npm", ["pack", "--dry-run", "--json"], { @@ -77,6 +79,9 @@ test("assembles two public npm packages and six matching npm and Python runtimes assert.match(assembledReadme, /

Yield<\/h1>/) assert.match(await readFile(join(output, "npm/yield/LICENSE"), "utf8"), /MIT License/) await assert.rejects(access(join(output, "npm/yield/skills/release-yield")), { code: "ENOENT" }) + await assert.rejects(access(join(output, "npm/yield/skills/yield-workflow-builder")), { + code: "ENOENT", + }) await assert.rejects(access(join(output, "npm/yield/.agents")), { code: "ENOENT" }) await assert.rejects(access(join(output, "npm/yield/.cursor")), { code: "ENOENT" }) await assert.rejects(access(join(output, "npm/yield/.claude")), { code: "ENOENT" }) @@ -110,6 +115,9 @@ test("assembles two public npm packages and six matching npm and Python runtimes assert.match(await readFile(join(pythonRoot, "pyproject.toml"), "utf8"), /version = "1\.2\.3"/) assert.match(await readFile(join(pythonRoot, "setup.py"), "utf8"), new RegExp(target.pythonTag)) assert.match(await readFile(join(pythonRoot, "LICENSE"), "utf8"), /MIT License/) + await assert.rejects(access(join(pythonRoot, "skills/yield-workflow-builder")), { + code: "ENOENT", + }) const pythonRuntime = target.goos === "windows" ? "yskill.exe" : "yskill" assert.equal( await readFile(join(pythonRoot, "yieldskill/_runtime", pythonRuntime), "utf8"), @@ -127,6 +135,9 @@ test("assembles two public npm packages and six matching npm and Python runtimes /installed automatically by `yieldskill`/, ) assert.match(await readFile(join(rustRoot, "LICENSE"), "utf8"), /MIT License/) + await assert.rejects(access(join(rustRoot, "skills/yield-workflow-builder")), { + code: "ENOENT", + }) const rustRuntime = target.goos === "windows" ? "yskill.exe" : "yskill" assert.equal((await stat(join(rustRoot, "runtime", rustRuntime))).mode & 0o111, 0) } diff --git a/packaging/create-yield.test.mjs b/packaging/create-yield.test.mjs index c11ccc1..5a8c7fc 100644 --- a/packaging/create-yield.test.mjs +++ b/packaging/create-yield.test.mjs @@ -5,7 +5,7 @@ import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" -test("npm initializer forwards bootstrap and user arguments to the matching CLI", async (t) => { +test("npm initializer forwards helper installation and user arguments to the matching CLI", async (t) => { const root = await mkdtemp(join(tmpdir(), "create-yield-")) t.after(() => rm(root, { recursive: true, force: true })) const initializer = join(root, "node_modules/@operatorstack/create-yield") @@ -40,7 +40,8 @@ test("npm initializer forwards bootstrap and user arguments to the matching CLI" }, ) assert.deepEqual(JSON.parse(await readFile(receipt, "utf8")), [ - "bootstrap", + "helper", + "install", "--language", "typescript", "--root", diff --git a/packaging/create-yield/README.md b/packaging/create-yield/README.md index 09a9095..8ed4f2a 100644 --- a/packaging/create-yield/README.md +++ b/packaging/create-yield/README.md @@ -1,9 +1,11 @@ # Create Yield -Create and register the Yield workflow builder in a repository: +Compatibility initializer for the optional Yield developer helper: ```sh npm create @operatorstack/yield@latest ``` -The command shows every proposed change. It asks for confirmation before it writes files. +The command is an alias for `yskill helper install`. It shows every proposed +change and asks for confirmation before it writes files. Installing the Yield +SDK alone does not run this initializer. diff --git a/packaging/create-yield/bin/create-yield.mjs b/packaging/create-yield/bin/create-yield.mjs index dc21c5a..33879ea 100644 --- a/packaging/create-yield/bin/create-yield.mjs +++ b/packaging/create-yield/bin/create-yield.mjs @@ -9,7 +9,7 @@ const sdkEntry = require.resolve("@operatorstack/yield") const cli = resolve(dirname(sdkEntry), "../bin/yskill.mjs") const result = spawnSync( process.execPath, - [cli, "bootstrap", "--language", "typescript", ...process.argv.slice(2)], + [cli, "helper", "install", "--language", "typescript", ...process.argv.slice(2)], { stdio: "inherit", }, diff --git a/scripts/readme.test.mjs b/scripts/readme.test.mjs index ecc6da1..3bbb3a0 100644 --- a/scripts/readme.test.mjs +++ b/scripts/readme.test.mjs @@ -370,32 +370,32 @@ test("README and quickstart use the public documentation and package registries" docsIndex, /\[public documentation\]\(https:\/\/yield\.operatorstack\.systems\/docs\/\)/, ) - const commands = [ - "npm create @operatorstack/yield@latest", - "uvx --from yieldskill yskill bootstrap --language python", - ".yield/bin/yskill bootstrap --root . --language rust", - "go run github.com/operatorstack/yield/cmd/yskill@latest bootstrap --root . --language go", + const helperCommands = [ + "npm exec -- yskill helper install --language typescript", + "uvx --from yieldskill yskill helper install --language python", + ".yield/bin/yskill helper install --root . --language rust", + "go run github.com/operatorstack/yield/cmd/yskill@latest helper install --root . --language go", ] - for (const command of commands) { + for (const command of helperCommands) { assert.ok(readme.includes(command), `README is missing ${command}`) assert.ok(quickstart.includes(command), `quickstart is missing ${command}`) assert.ok(agentSetup.includes(command), `agent setup is missing ${command}`) } assert.doesNotMatch(quickstart, /get\.operatorstack\.systems\/npm|@operatorstack\/yield@0\./) - const createRequest = "Use Yield to create a tested skill workflow for releasing my package." - const convertRequest = - "Use Yield to convert my existing release SKILL.md into a tested skill workflow." - for (const document of [readme, quickstart, agentSetup, pythonReadme, rustReadme, goReadme]) { + for (const document of [readme, quickstart, pythonReadme, rustReadme, goReadme]) { assert.ok( - document.includes(createRequest), - "agent-first documentation is missing the create request", - ) - assert.ok( - document.includes(convertRequest), - "agent-first documentation is missing the convert request", + document.indexOf("Install Yield") < document.indexOf("Optional developer helper") || + document.indexOf("Install Yield") < + document.indexOf("Optional: install the developer helper"), + "documentation must teach the manual workflow before the optional helper", ) } - assert.match(quickstart, /^## Advanced: build manually$/m) + assert.match(readme, /Package installation does not create skills or coding-agent adapters/) + assert.match( + quickstart, + /Package installation alone never\s+creates a skill or coding-agent adapter/, + ) + assert.match(quickstart, /^## Optional: install the developer helper$/m) assert.match(agentSetup, /^## Run the registered skill$/m) }) diff --git a/sdk/python/README.md b/sdk/python/README.md index 69bd2f7..35698aa 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -29,26 +29,7 @@ The package name and import name are both `yieldskill`. Python reserves `yield` as a keyword. -## Start with your coding agent - -```bash -uvx --from yieldskill yskill bootstrap --language python -``` - -Review and confirm the plan. Restart your coding agent. Then ask it to create -a new skill workflow: - -```text -Use Yield to create a tested skill workflow for releasing my package. -``` - -To convert an existing `SKILL.md`, ask: - -```text -Use Yield to convert my existing release SKILL.md into a tested skill workflow. -``` - -## Advanced: build manually +## Create a workflow ### 1. Install Yield @@ -242,6 +223,19 @@ operation. Cross those boundaries through a Yield operation instead. Yield is not a daemon, hosted runtime, workflow DSL, marketplace, coding-agent loop, multi-agent orchestrator, or security sandbox. +## Optional developer helper + +Installing `yieldskill` does not create skills or coding-agent adapters. After +learning the manual workflow above, install guided assistance explicitly: + +```bash +uvx --from yieldskill yskill helper install --language python +``` + +Review the plan and restart the coding agent after installation. The helper +can teach, create, convert, check, repair, upgrade, and register workflows. +`yskill bootstrap` remains a compatibility alias. + ## Coding agents and source Cursor, Codex, and Claude Code are verified integrations. Yield also provides diff --git a/sdk/rust/README.md b/sdk/rust/README.md index 0739bc7..d36714a 100644 --- a/sdk/rust/README.md +++ b/sdk/rust/README.md @@ -30,27 +30,7 @@ The crate and library names are both `yieldskill`. The installed command is `yskill`. -## Start with your coding agent - -```bash -cargo install yieldskill --root .yield --locked -.yield/bin/yskill bootstrap --root . --language rust -``` - -Review and confirm the plan. Restart your coding agent. Then ask it to create -a new skill workflow: - -```text -Use Yield to create a tested skill workflow for releasing my package. -``` - -To convert an existing `SKILL.md`, ask: - -```text -Use Yield to convert my existing release SKILL.md into a tested skill workflow. -``` - -## Advanced: build manually +## Create a workflow ### 1. Install Yield @@ -252,6 +232,19 @@ operation. Cross those boundaries through a Yield operation instead. Yield is not a daemon, hosted runtime, workflow DSL, marketplace, coding-agent loop, multi-agent orchestrator, or security sandbox. +## Optional developer helper + +Installing `yieldskill` does not create skills or coding-agent adapters. After +learning the manual workflow above, install guided assistance explicitly: + +```bash +.yield/bin/yskill helper install --root . --language rust +``` + +Review the plan and restart the coding agent after installation. The helper +can teach, create, convert, check, repair, upgrade, and register workflows. +`yskill bootstrap` remains a compatibility alias. + ## Coding agents and source Cursor, Codex, and Claude Code are verified integrations. Yield also provides diff --git a/sdk/yield/README.md b/sdk/yield/README.md index 64e82b4..98fed48 100644 --- a/sdk/yield/README.md +++ b/sdk/yield/README.md @@ -29,26 +29,7 @@ The Go module is `github.com/operatorstack/yield`. Import the SDK as `github.com/operatorstack/yield/sdk/yield`. The installed command is `yskill`. -## Start with your coding agent - -```bash -go run github.com/operatorstack/yield/cmd/yskill@latest bootstrap --root . --language go -``` - -Review and confirm the plan. Restart your coding agent. Then ask it to create -a new skill workflow: - -```text -Use Yield to create a tested skill workflow for releasing my package. -``` - -To convert an existing `SKILL.md`, ask: - -```text -Use Yield to convert my existing release SKILL.md into a tested skill workflow. -``` - -## Advanced: build manually +## Create a workflow ### 1. Install Yield @@ -323,6 +304,19 @@ Yield is not a daemon, hosted runtime, workflow DSL, marketplace, coding-agent replacement, or permission sandbox. Your operating system, repository, and coding-agent permissions remain the security boundary. +## Optional developer helper + +Installing the Go runtime does not create skills or coding-agent adapters. +After learning the manual workflow above, install guided assistance explicitly: + +```bash +.yield/bin/yskill helper install --root . --language go +``` + +Review the plan and restart the coding agent after installation. The helper +can teach, create, convert, check, repair, upgrade, and register workflows. +`yskill bootstrap` remains a compatibility alias. + ## Coding-agent support Yield verifies adapters for Cursor, Codex, and Claude Code. Registry-backed From c6a60d96a31a16cbe07585c456c6bba10aacde02 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Sun, 9 Aug 2026 12:04:42 +0100 Subject: [PATCH 2/3] test: refresh evaluation receipts --- evals/results/latest-conversion.json | 12 ++++++------ evals/results/latest.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/evals/results/latest-conversion.json b/evals/results/latest-conversion.json index 4213480..ea6dfd4 100644 --- a/evals/results/latest-conversion.json +++ b/evals/results/latest-conversion.json @@ -1,8 +1,8 @@ { "schema_version": 1, "methodology_version": "semantic-disposition-v1", - "generated_at": "2026-08-09T09:21:44.116Z", - "source_hash": "b87e914bc9406c400e82b8dd35c8e2b35df38664cfcf375ea6eba1884fc1adb3", + "generated_at": "2026-08-09T11:04:09.871Z", + "source_hash": "482396102d8479837f1987975785b866e4c37f65e22ab389fce23c88a7a0e998", "fixture_source_hash": "9ab03fffe6716da8298b461f79c9eebae7c7bb01151328686ec51ed9c0b77fe5", "status": "passed", "model": { @@ -13,10 +13,10 @@ }, "sessions": 2, "token_usage": { - "input_tokens": 795134, - "cached_input_tokens": 732165, - "output_tokens": 10401, - "reasoning_output_tokens": 2874 + "input_tokens": 465554, + "cached_input_tokens": 419093, + "output_tokens": 8791, + "reasoning_output_tokens": 2428 }, "clause_counts": { "total": 4, diff --git a/evals/results/latest.json b/evals/results/latest.json index dcbc4bc..7d37fb0 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-09T09:13:26.116Z", - "source_digest": "255b4fcf353708369ba9aaf49d41273c4f7114747418f72d94e8bc7a08c32cad", + "generated_at": "2026-08-09T11:00:29.852Z", + "source_digest": "5e1f9958a62e551c926ece48effaf6655b2fa71c87e8bf67a0f7535120ebf56d", "status": "passed", "workflow_conformance": { "passed": 40, From 32d3897fbd82372e367e5627d3eeb7746876f683 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Sun, 9 Aug 2026 12:12:12 +0100 Subject: [PATCH 3/3] test: bind examples to helper release --- .changeset/agent-first-bootstrap.md | 5 +---- .changeset/optional-developer-helper.md | 8 ++++++++ evals/results/latest-conversion.json | 12 ++++++------ evals/results/latest.json | 4 ++-- evals/scripts/run.mjs | 2 +- examples/convert-skill/skill.json | 2 +- examples/data-migration/skill.json | 2 +- examples/env-doctor/skill.json | 2 +- examples/investigate/skill.json | 2 +- examples/library/go/audit-security/skill.json | 2 +- examples/library/go/investigate-failure/skill.json | 2 +- examples/library/go/migrate-database/skill.json | 2 +- examples/library/go/publish-ios/skill.json | 2 +- examples/library/go/qa-web-change/skill.json | 2 +- examples/library/go/release-package/skill.json | 2 +- examples/library/go/repair-ci/skill.json | 2 +- examples/library/go/review-branch/skill.json | 2 +- examples/library/go/triage-issue/skill.json | 2 +- examples/library/go/upgrade-dependency/skill.json | 2 +- examples/library/python/audit-security/skill.json | 2 +- .../library/python/investigate-failure/skill.json | 2 +- examples/library/python/migrate-database/skill.json | 2 +- examples/library/python/publish-ios/skill.json | 2 +- examples/library/python/qa-web-change/skill.json | 2 +- examples/library/python/release-package/skill.json | 2 +- examples/library/python/repair-ci/skill.json | 2 +- examples/library/python/review-branch/skill.json | 2 +- examples/library/python/triage-issue/skill.json | 2 +- .../library/python/upgrade-dependency/skill.json | 2 +- examples/library/rust/audit-security/skill.json | 2 +- examples/library/rust/investigate-failure/skill.json | 2 +- examples/library/rust/migrate-database/skill.json | 2 +- examples/library/rust/publish-ios/skill.json | 2 +- examples/library/rust/qa-web-change/skill.json | 2 +- examples/library/rust/release-package/skill.json | 2 +- examples/library/rust/repair-ci/skill.json | 2 +- examples/library/rust/review-branch/skill.json | 2 +- examples/library/rust/triage-issue/skill.json | 2 +- examples/library/rust/upgrade-dependency/skill.json | 2 +- .../library/typescript/audit-security/skill.json | 2 +- .../typescript/investigate-failure/skill.json | 2 +- .../library/typescript/migrate-database/skill.json | 2 +- examples/library/typescript/publish-ios/skill.json | 2 +- examples/library/typescript/qa-web-change/skill.json | 2 +- .../library/typescript/release-package/skill.json | 2 +- examples/library/typescript/repair-ci/skill.json | 2 +- examples/library/typescript/review-branch/skill.json | 2 +- examples/library/typescript/triage-issue/skill.json | 2 +- .../library/typescript/upgrade-dependency/skill.json | 2 +- examples/release-checklist/skill.json | 2 +- 50 files changed, 63 insertions(+), 58 deletions(-) create mode 100644 .changeset/optional-developer-helper.md diff --git a/.changeset/agent-first-bootstrap.md b/.changeset/agent-first-bootstrap.md index 106f5ad..ed6437e 100644 --- a/.changeset/agent-first-bootstrap.md +++ b/.changeset/agent-first-bootstrap.md @@ -2,7 +2,4 @@ "@operatorstack/yield": minor --- -Add the explicit `yskill helper install` command and the optional developer -helper for all four SDKs. Keep `yskill bootstrap` and -`npm create @operatorstack/yield` as compatibility aliases. Package -installation does not create skills or coding-agent adapters. +Add the agent-first bootstrap command, the tested workflow builder for all four SDKs, and the `@operatorstack/create-yield` initializer. diff --git a/.changeset/optional-developer-helper.md b/.changeset/optional-developer-helper.md new file mode 100644 index 0000000..106f5ad --- /dev/null +++ b/.changeset/optional-developer-helper.md @@ -0,0 +1,8 @@ +--- +"@operatorstack/yield": minor +--- + +Add the explicit `yskill helper install` command and the optional developer +helper for all four SDKs. Keep `yskill bootstrap` and +`npm create @operatorstack/yield` as compatibility aliases. Package +installation does not create skills or coding-agent adapters. diff --git a/evals/results/latest-conversion.json b/evals/results/latest-conversion.json index ea6dfd4..2d338ab 100644 --- a/evals/results/latest-conversion.json +++ b/evals/results/latest-conversion.json @@ -1,8 +1,8 @@ { "schema_version": 1, "methodology_version": "semantic-disposition-v1", - "generated_at": "2026-08-09T11:04:09.871Z", - "source_hash": "482396102d8479837f1987975785b866e4c37f65e22ab389fce23c88a7a0e998", + "generated_at": "2026-08-09T11:11:00.278Z", + "source_hash": "e5f48a27c174816531e8dbb36d6987f54fe091a071ce5b5adf9fe19d747232e1", "fixture_source_hash": "9ab03fffe6716da8298b461f79c9eebae7c7bb01151328686ec51ed9c0b77fe5", "status": "passed", "model": { @@ -13,10 +13,10 @@ }, "sessions": 2, "token_usage": { - "input_tokens": 465554, - "cached_input_tokens": 419093, - "output_tokens": 8791, - "reasoning_output_tokens": 2428 + "input_tokens": 648932, + "cached_input_tokens": 588148, + "output_tokens": 9945, + "reasoning_output_tokens": 2646 }, "clause_counts": { "total": 4, diff --git a/evals/results/latest.json b/evals/results/latest.json index 7d37fb0..40838d1 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-09T11:00:29.852Z", - "source_digest": "5e1f9958a62e551c926ece48effaf6655b2fa71c87e8bf67a0f7535120ebf56d", + "generated_at": "2026-08-09T11:08:29.697Z", + "source_digest": "b9a11df3a7a86c95932dd5e54fad84e897e99f4058f1ca03109ce7ae269e5545", "status": "passed", "workflow_conformance": { "passed": 40, diff --git a/evals/scripts/run.mjs b/evals/scripts/run.mjs index 4313d73..fb8392c 100644 --- a/evals/scripts/run.mjs +++ b/evals/scripts/run.mjs @@ -59,7 +59,7 @@ const runtimeCases = [ "changed source is refused until the user accepts the change", ], ] -const workflowRuntimeVersion = "0.3.0" +const workflowRuntimeVersion = "0.5.0" const excludedDirectories = new Set([ ".git", ".yield", diff --git a/examples/convert-skill/skill.json b/examples/convert-skill/skill.json index fcda65e..c09c290 100644 --- a/examples/convert-skill/skill.json +++ b/examples/convert-skill/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": ["go", "run", "."] } diff --git a/examples/data-migration/skill.json b/examples/data-migration/skill.json index 7e01f78..88e0ff9 100644 --- a/examples/data-migration/skill.json +++ b/examples/data-migration/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": ["cargo", "run", "--quiet"] } diff --git a/examples/env-doctor/skill.json b/examples/env-doctor/skill.json index 827a3f8..d08bec8 100644 --- a/examples/env-doctor/skill.json +++ b/examples/env-doctor/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": ["python3", "main.py"] } diff --git a/examples/investigate/skill.json b/examples/investigate/skill.json index fcda65e..c09c290 100644 --- a/examples/investigate/skill.json +++ b/examples/investigate/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": ["go", "run", "."] } diff --git a/examples/library/go/audit-security/skill.json b/examples/library/go/audit-security/skill.json index 4fee228..4239fa7 100644 --- a/examples/library/go/audit-security/skill.json +++ b/examples/library/go/audit-security/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/investigate-failure/skill.json b/examples/library/go/investigate-failure/skill.json index e3acb26..1da451b 100644 --- a/examples/library/go/investigate-failure/skill.json +++ b/examples/library/go/investigate-failure/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/migrate-database/skill.json b/examples/library/go/migrate-database/skill.json index 0d8449c..d0a864d 100644 --- a/examples/library/go/migrate-database/skill.json +++ b/examples/library/go/migrate-database/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/publish-ios/skill.json b/examples/library/go/publish-ios/skill.json index 48ef012..7a79980 100644 --- a/examples/library/go/publish-ios/skill.json +++ b/examples/library/go/publish-ios/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/qa-web-change/skill.json b/examples/library/go/qa-web-change/skill.json index 1931421..27c23cd 100644 --- a/examples/library/go/qa-web-change/skill.json +++ b/examples/library/go/qa-web-change/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/release-package/skill.json b/examples/library/go/release-package/skill.json index dc6f57b..fcb1a7f 100644 --- a/examples/library/go/release-package/skill.json +++ b/examples/library/go/release-package/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/repair-ci/skill.json b/examples/library/go/repair-ci/skill.json index 308ef6b..bdeb6f4 100644 --- a/examples/library/go/repair-ci/skill.json +++ b/examples/library/go/repair-ci/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/review-branch/skill.json b/examples/library/go/review-branch/skill.json index 5ee30d4..3024736 100644 --- a/examples/library/go/review-branch/skill.json +++ b/examples/library/go/review-branch/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/triage-issue/skill.json b/examples/library/go/triage-issue/skill.json index e44b945..b3edb96 100644 --- a/examples/library/go/triage-issue/skill.json +++ b/examples/library/go/triage-issue/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/go/upgrade-dependency/skill.json b/examples/library/go/upgrade-dependency/skill.json index ba6c366..44274b5 100644 --- a/examples/library/go/upgrade-dependency/skill.json +++ b/examples/library/go/upgrade-dependency/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "go", "run": [ "go", diff --git a/examples/library/python/audit-security/skill.json b/examples/library/python/audit-security/skill.json index 9b101d2..1d95a1d 100644 --- a/examples/library/python/audit-security/skill.json +++ b/examples/library/python/audit-security/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/investigate-failure/skill.json b/examples/library/python/investigate-failure/skill.json index bc9d66c..3427672 100644 --- a/examples/library/python/investigate-failure/skill.json +++ b/examples/library/python/investigate-failure/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/migrate-database/skill.json b/examples/library/python/migrate-database/skill.json index 3763248..2d25dff 100644 --- a/examples/library/python/migrate-database/skill.json +++ b/examples/library/python/migrate-database/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/publish-ios/skill.json b/examples/library/python/publish-ios/skill.json index 0903444..b7369ee 100644 --- a/examples/library/python/publish-ios/skill.json +++ b/examples/library/python/publish-ios/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/qa-web-change/skill.json b/examples/library/python/qa-web-change/skill.json index 7007368..edcfc44 100644 --- a/examples/library/python/qa-web-change/skill.json +++ b/examples/library/python/qa-web-change/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/release-package/skill.json b/examples/library/python/release-package/skill.json index a7249a8..f628c14 100644 --- a/examples/library/python/release-package/skill.json +++ b/examples/library/python/release-package/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/repair-ci/skill.json b/examples/library/python/repair-ci/skill.json index ad61227..82174dd 100644 --- a/examples/library/python/repair-ci/skill.json +++ b/examples/library/python/repair-ci/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/review-branch/skill.json b/examples/library/python/review-branch/skill.json index 6db4a49..0a095cd 100644 --- a/examples/library/python/review-branch/skill.json +++ b/examples/library/python/review-branch/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/triage-issue/skill.json b/examples/library/python/triage-issue/skill.json index d4c0400..89bff56 100644 --- a/examples/library/python/triage-issue/skill.json +++ b/examples/library/python/triage-issue/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/python/upgrade-dependency/skill.json b/examples/library/python/upgrade-dependency/skill.json index b4258dc..1fc34e6 100644 --- a/examples/library/python/upgrade-dependency/skill.json +++ b/examples/library/python/upgrade-dependency/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "python", "run": [ "python3", diff --git a/examples/library/rust/audit-security/skill.json b/examples/library/rust/audit-security/skill.json index 35915ed..a37b7eb 100644 --- a/examples/library/rust/audit-security/skill.json +++ b/examples/library/rust/audit-security/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/investigate-failure/skill.json b/examples/library/rust/investigate-failure/skill.json index d7a1e0e..554551a 100644 --- a/examples/library/rust/investigate-failure/skill.json +++ b/examples/library/rust/investigate-failure/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/migrate-database/skill.json b/examples/library/rust/migrate-database/skill.json index 2b0607a..5b49899 100644 --- a/examples/library/rust/migrate-database/skill.json +++ b/examples/library/rust/migrate-database/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/publish-ios/skill.json b/examples/library/rust/publish-ios/skill.json index 5a710c8..4d5db27 100644 --- a/examples/library/rust/publish-ios/skill.json +++ b/examples/library/rust/publish-ios/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/qa-web-change/skill.json b/examples/library/rust/qa-web-change/skill.json index 6aa1de4..41f1063 100644 --- a/examples/library/rust/qa-web-change/skill.json +++ b/examples/library/rust/qa-web-change/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/release-package/skill.json b/examples/library/rust/release-package/skill.json index f38e0af..5a29e89 100644 --- a/examples/library/rust/release-package/skill.json +++ b/examples/library/rust/release-package/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/repair-ci/skill.json b/examples/library/rust/repair-ci/skill.json index 17c4f70..5d4df6c 100644 --- a/examples/library/rust/repair-ci/skill.json +++ b/examples/library/rust/repair-ci/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/review-branch/skill.json b/examples/library/rust/review-branch/skill.json index 1cf6da9..b780c9b 100644 --- a/examples/library/rust/review-branch/skill.json +++ b/examples/library/rust/review-branch/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/triage-issue/skill.json b/examples/library/rust/triage-issue/skill.json index 8d08d8c..6e54199 100644 --- a/examples/library/rust/triage-issue/skill.json +++ b/examples/library/rust/triage-issue/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/rust/upgrade-dependency/skill.json b/examples/library/rust/upgrade-dependency/skill.json index 520fb82..f1af2ac 100644 --- a/examples/library/rust/upgrade-dependency/skill.json +++ b/examples/library/rust/upgrade-dependency/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "rust", "run": [ "cargo", diff --git a/examples/library/typescript/audit-security/skill.json b/examples/library/typescript/audit-security/skill.json index 30e5cb4..5285e40 100644 --- a/examples/library/typescript/audit-security/skill.json +++ b/examples/library/typescript/audit-security/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/investigate-failure/skill.json b/examples/library/typescript/investigate-failure/skill.json index 2a63740..7e6901a 100644 --- a/examples/library/typescript/investigate-failure/skill.json +++ b/examples/library/typescript/investigate-failure/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/migrate-database/skill.json b/examples/library/typescript/migrate-database/skill.json index 9aa252a..19723b6 100644 --- a/examples/library/typescript/migrate-database/skill.json +++ b/examples/library/typescript/migrate-database/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/publish-ios/skill.json b/examples/library/typescript/publish-ios/skill.json index cf410e6..f6aa62a 100644 --- a/examples/library/typescript/publish-ios/skill.json +++ b/examples/library/typescript/publish-ios/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/qa-web-change/skill.json b/examples/library/typescript/qa-web-change/skill.json index fbee5af..4b5e274 100644 --- a/examples/library/typescript/qa-web-change/skill.json +++ b/examples/library/typescript/qa-web-change/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/release-package/skill.json b/examples/library/typescript/release-package/skill.json index 1865cd1..b9bea81 100644 --- a/examples/library/typescript/release-package/skill.json +++ b/examples/library/typescript/release-package/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/repair-ci/skill.json b/examples/library/typescript/repair-ci/skill.json index 5790274..1f3cd3f 100644 --- a/examples/library/typescript/repair-ci/skill.json +++ b/examples/library/typescript/repair-ci/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/review-branch/skill.json b/examples/library/typescript/review-branch/skill.json index cac7334..99f2765 100644 --- a/examples/library/typescript/review-branch/skill.json +++ b/examples/library/typescript/review-branch/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/triage-issue/skill.json b/examples/library/typescript/triage-issue/skill.json index 5bb4537..b2b577d 100644 --- a/examples/library/typescript/triage-issue/skill.json +++ b/examples/library/typescript/triage-issue/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/library/typescript/upgrade-dependency/skill.json b/examples/library/typescript/upgrade-dependency/skill.json index 32ace21..c92e909 100644 --- a/examples/library/typescript/upgrade-dependency/skill.json +++ b/examples/library/typescript/upgrade-dependency/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": [ "node", diff --git a/examples/release-checklist/skill.json b/examples/release-checklist/skill.json index 1f7903e..1ceedde 100644 --- a/examples/release-checklist/skill.json +++ b/examples/release-checklist/skill.json @@ -1,6 +1,6 @@ { "version": 1, - "yield_version": "0.3.0", + "yield_version": "0.5.0", "language": "typescript", "run": ["node", "main.ts"] }