From 162c9032ac41b45a68b752e9dd2bb65e16e6f22f Mon Sep 17 00:00:00 2001 From: bigboateng Date: Sat, 1 Aug 2026 04:18:48 +0100 Subject: [PATCH] =?UTF-8?q?feat(yield):=20convert-skill=20=E2=80=94=20the?= =?UTF-8?q?=20converter=20is=20itself=20a=20Yield=20skill=20(Locus-decided?= =?UTF-8?q?=20design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns an existing prose SKILL.md into a Yield program in the operator's chosen language (go/typescript/python/rust). Locus verdicts (docs/locus-converter.md): - supervisory-rw controllable: shipping an unverified conversion is preventable; nonblocking: every conversion ends verified or honestly blocked - DECIDED rival-design comparison: executed-verification satisfies forbidden-unreachable; completing on the model's transcription is rejected with the verbatim trace generate_program -> complete_untested -> SHIPPED_UNVERIFIED The program owns pipeline order, the language menu, the <=2 repair bound, and the evidence gate (nested 'yskill test' of the generated skill, executed by the supervisor). The model owns flow extraction and code writing. The scripted fixture exercises the full machinery including the nested test run (CI: YSKILL=/tmp/yskill). Disclosure-Reviewed: reviewed — public-safe only; private tree untouched by this commit --- .github/workflows/yield-lab.yml | 1 + labs/22-yield/public-readme/README.md | 6 + labs/22-yield/yield/docs/locus-converter.md | 39 +++ .../yield/docs/locus/convert-transcribed.json | 161 +++++++++++ .../yield/docs/locus/convert-verified.json | 57 ++++ ...eb89c3d812847490b9369cb006cf37625e3c6.json | 254 ++++++++++++++++++ .../yield/examples/convert-skill/SKILL.md | 26 ++ .../convert-skill/fixtures/responses.json | 19 ++ .../yield/examples/convert-skill/main.go | 126 +++++++++ 9 files changed, 689 insertions(+) create mode 100644 labs/22-yield/yield/docs/locus-converter.md create mode 100644 labs/22-yield/yield/docs/locus/convert-transcribed.json create mode 100644 labs/22-yield/yield/docs/locus/convert-verified.json create mode 100644 labs/22-yield/yield/docs/locus/drv-db7bca98dc05ca24898e309e3d0eb89c3d812847490b9369cb006cf37625e3c6.json create mode 100644 labs/22-yield/yield/examples/convert-skill/SKILL.md create mode 100644 labs/22-yield/yield/examples/convert-skill/fixtures/responses.json create mode 100644 labs/22-yield/yield/examples/convert-skill/main.go diff --git a/.github/workflows/yield-lab.yml b/.github/workflows/yield-lab.yml index 54bc30ae..5b42565c 100644 --- a/.github/workflows/yield-lab.yml +++ b/.github/workflows/yield-lab.yml @@ -41,6 +41,7 @@ jobs: /tmp/yskill test examples/release-checklist /tmp/yskill test examples/env-doctor /tmp/yskill test examples/data-migration + YSKILL=/tmp/yskill /tmp/yskill test examples/convert-skill # Shift-left projection gate: materialize the public surface exactly as # operatorstack/yield would receive it and compile+test it, so a broken diff --git a/labs/22-yield/public-readme/README.md b/labs/22-yield/public-readme/README.md index e0365ee4..72963f4b 100644 --- a/labs/22-yield/public-readme/README.md +++ b/labs/22-yield/public-readme/README.md @@ -59,6 +59,12 @@ four languages and asserts identical observable protocol behavior. Non-Go skills declare their runner in `skill.json`: `{"run": ["node", "main.ts"]}`. +Already have prose skills? `examples/convert-skill` is a converter — +itself a Yield skill — that extracts the implicit flow from an existing +`SKILL.md`, asks you which language you want, has the model write the +program, and completes only when the generated skill passes its own +fixture run. A conversion that was never executed is never "done". + ## Try it ``` diff --git a/labs/22-yield/yield/docs/locus-converter.md b/labs/22-yield/yield/docs/locus-converter.md new file mode 100644 index 00000000..b43bb739 --- /dev/null +++ b/labs/22-yield/yield/docs/locus-converter.md @@ -0,0 +1,39 @@ +# Locus derivation — the skill converter + +`examples/convert-skill` turns an existing prose `SKILL.md` into a Yield +program in the operator's chosen language (Go / TypeScript / Python / +Rust). The converter is itself a Yield skill — the pipeline that makes +skills reliable is the pipeline that converts them. + +## Verdicts (models in `docs/locus/`) + +| model | operator | verdict | +|---|---|---| +| `convert-verified.json` | control.supervisory-rw | **controllable**, no violations — shipping an unverified conversion is preventable | +| `convert-verified.json` | control.nonblockingness | **nonblocking** — every conversion ends at `SHIPPED_VERIFIED` or an honest `REPORTED_BLOCKED` | +| `convert-verified.json` vs `convert-transcribed.json` | verification.safety-reachability (rival designs, `drv-…`) | **decided** — the executed-verification design satisfies `forbidden-unreachable`; completing on the model's transcription is rejected with the verbatim trace `extract_flow → pick_language → generate_program → complete_untested → SHIPPED_UNVERIFIED` | + +The decided comparison is the design's spine: **a conversion that was +never executed is never "done".** `complete_untested` stays in the +alphabet with no transition — the program's `Require(test.exit_code == 0)` +is the refusing mechanism, and the two-attempt repair loop ends in an +honest `Blocked`, keeping the pipeline nonblocking. + +## Division of labor + +| code owns | model owns | +|---|---| +| pipeline order (read → extract → choose → generate → verify) | reading the prose and extracting the implicit flow | +| the language menu (`ask_user`, closed set) | writing the program, thin SKILL.md, runner manifest, fixtures | +| the repair bound (≤ 2 attempts) | repairing a failing generation | +| the evidence gate (`yskill test` exit code, observed by the supervisor) | — | + +## Verified end-to-end without a model + +`fixtures/responses.json` scripts a conversion whose destination is an +existing valid skill, so `yskill test examples/convert-skill` exercises +the full machinery — including the **nested** `yskill test` of the +"generated" skill (`${YSKILL:-yskill}` lets harnesses pin the binary). +What the scripted run cannot exercise is the model actually writing good +code; that is exactly the part the evidence gate exists to check at live +time. diff --git a/labs/22-yield/yield/docs/locus/convert-transcribed.json b/labs/22-yield/yield/docs/locus/convert-transcribed.json new file mode 100644 index 00000000..7ca00a7d --- /dev/null +++ b/labs/22-yield/yield/docs/locus/convert-transcribed.json @@ -0,0 +1,161 @@ +{ + "schema_version": 1, + "id": "convert-skill-transcribed-v1", + "subject": "Yield skill converter pipeline: extract flow from prose SKILL.md, operator picks the target language, the model generates the program, and the converter completes on the model's word that the code is correct.", + "evidence": [ + { + "path": "labs/22-yield/yield/cmd/yskill/main.go", + "note": "design B: no yskill test run; the generated program is returned as text and shipped on transcription" + } + ], + "states": [ + { + "id": "SOURCED" + }, + { + "id": "FLOW_EXTRACTED" + }, + { + "id": "LANG_CHOSEN" + }, + { + "id": "GENERATED" + }, + { + "id": "TEST_RUN" + }, + { + "id": "TEST_PASSED" + }, + { + "id": "TEST_FAILED" + }, + { + "id": "SHIPPED_VERIFIED", + "marked": true + }, + { + "id": "SHIPPED_UNVERIFIED" + }, + { + "id": "REPORTED_BLOCKED", + "marked": true + } + ], + "events": [ + { + "id": "read_prose", + "controllable": true, + "observable": true + }, + { + "id": "extract_flow", + "controllable": false, + "observable": true + }, + { + "id": "pick_language", + "controllable": false, + "observable": true + }, + { + "id": "generate_program", + "controllable": false, + "observable": true + }, + { + "id": "run_generated_test", + "controllable": true, + "observable": true + }, + { + "id": "test_passes", + "controllable": false, + "observable": true + }, + { + "id": "test_fails", + "controllable": false, + "observable": true + }, + { + "id": "regenerate", + "controllable": true, + "observable": true + }, + { + "id": "complete_with_evidence", + "controllable": true, + "observable": true + }, + { + "id": "complete_untested", + "controllable": true, + "observable": true + }, + { + "id": "declare_blocked", + "controllable": true, + "observable": true + }, + { + "id": "archive", + "controllable": false, + "observable": true + } + ], + "transitions": [ + { + "from": "SOURCED", + "event": "extract_flow", + "to": "FLOW_EXTRACTED", + "evidence": [ + 0 + ] + }, + { + "from": "FLOW_EXTRACTED", + "event": "pick_language", + "to": "LANG_CHOSEN", + "evidence": [ + 0 + ] + }, + { + "from": "LANG_CHOSEN", + "event": "generate_program", + "to": "GENERATED", + "evidence": [ + 0 + ] + }, + { + "from": "GENERATED", + "event": "complete_untested", + "to": "SHIPPED_UNVERIFIED", + "evidence": [ + 0 + ] + }, + { + "from": "SHIPPED_UNVERIFIED", + "event": "archive", + "to": "SHIPPED_UNVERIFIED", + "evidence": [ + 0 + ] + } + ], + "spec": { + "description": "A converted skill must never ship without its fixture run passing under the real supervisor.", + "forbidden_states": [ + "SHIPPED_UNVERIFIED" + ] + }, + "targets": { + "selector": "marked" + }, + "unknowns": [ + "Design B ships whatever the model asserts compiles; nothing observes the generated program executing." + ] +} \ No newline at end of file diff --git a/labs/22-yield/yield/docs/locus/convert-verified.json b/labs/22-yield/yield/docs/locus/convert-verified.json new file mode 100644 index 00000000..182cbe57 --- /dev/null +++ b/labs/22-yield/yield/docs/locus/convert-verified.json @@ -0,0 +1,57 @@ +{ + "schema_version": 1, + "id": "convert-skill-verified-v1", + "subject": "Yield skill converter pipeline: extract flow from prose SKILL.md, operator picks the target language, the model generates the program, and the converter completes only after the generated skill passes its own fixture run under yskill test.", + "evidence": [ + { "path": "labs/22-yield/yield/cmd/yskill/main.go", "note": "yskill test: scripted fixture run of a skill directory; exit code is observed fact" }, + { "path": "labs/22-yield/yield/internal/guard/guard.go", "note": "evidence-bound completion: a failed requirement prevents run.completed" } + ], + "states": [ + { "id": "SOURCED" }, + { "id": "FLOW_EXTRACTED" }, + { "id": "LANG_CHOSEN" }, + { "id": "GENERATED" }, + { "id": "TEST_RUN" }, + { "id": "TEST_PASSED" }, + { "id": "TEST_FAILED" }, + { "id": "SHIPPED_VERIFIED", "marked": true }, + { "id": "SHIPPED_UNVERIFIED" }, + { "id": "REPORTED_BLOCKED", "marked": true } + ], + "events": [ + { "id": "read_prose", "controllable": true, "observable": true }, + { "id": "extract_flow", "controllable": false, "observable": true }, + { "id": "pick_language", "controllable": false, "observable": true }, + { "id": "generate_program", "controllable": false, "observable": true }, + { "id": "run_generated_test", "controllable": true, "observable": true }, + { "id": "test_passes", "controllable": false, "observable": true }, + { "id": "test_fails", "controllable": false, "observable": true }, + { "id": "regenerate", "controllable": true, "observable": true }, + { "id": "complete_with_evidence", "controllable": true, "observable": true }, + { "id": "complete_untested", "controllable": true, "observable": true }, + { "id": "declare_blocked", "controllable": true, "observable": true }, + { "id": "archive", "controllable": false, "observable": true } + ], + "transitions": [ + { "from": "SOURCED", "event": "extract_flow", "to": "FLOW_EXTRACTED", "evidence": [0] }, + { "from": "FLOW_EXTRACTED", "event": "pick_language", "to": "LANG_CHOSEN", "evidence": [0] }, + { "from": "LANG_CHOSEN", "event": "generate_program", "to": "GENERATED", "evidence": [0] }, + { "from": "GENERATED", "event": "run_generated_test", "to": "TEST_RUN", "evidence": [0] }, + { "from": "TEST_RUN", "event": "test_passes", "to": "TEST_PASSED", "evidence": [0] }, + { "from": "TEST_RUN", "event": "test_fails", "to": "TEST_FAILED", "evidence": [0] }, + { "from": "TEST_FAILED", "event": "regenerate", "to": "GENERATED", "evidence": [0] }, + { "from": "TEST_FAILED", "event": "declare_blocked", "to": "REPORTED_BLOCKED", "evidence": [1] }, + { "from": "TEST_PASSED", "event": "complete_with_evidence", "to": "SHIPPED_VERIFIED", "evidence": [1] }, + { "from": "SHIPPED_VERIFIED", "event": "archive", "to": "SHIPPED_VERIFIED", "evidence": [0] }, + { "from": "REPORTED_BLOCKED", "event": "archive", "to": "REPORTED_BLOCKED", "evidence": [0] } + ], + "spec": { + "description": "A converted skill must never ship without its fixture run passing under the real supervisor.", + "forbidden_states": ["SHIPPED_UNVERIFIED"] + }, + "targets": { "selector": "marked" }, + "unknowns": [ + "Design A refuses complete_untested structurally: the event is in the alphabet, but the converter program offers no transition — Require(test.exit_code == 0) stands between GENERATED and completion.", + "Regeneration is bounded by a retry counter in the converter program; the model treats the bound as data, not modeled state." + ] +} diff --git a/labs/22-yield/yield/docs/locus/drv-db7bca98dc05ca24898e309e3d0eb89c3d812847490b9369cb006cf37625e3c6.json b/labs/22-yield/yield/docs/locus/drv-db7bca98dc05ca24898e309e3d0eb89c3d812847490b9369cb006cf37625e3c6.json new file mode 100644 index 00000000..c95a6ef5 --- /dev/null +++ b/labs/22-yield/yield/docs/locus/drv-db7bca98dc05ca24898e309e3d0eb89c3d812847490b9369cb006cf37625e3c6.json @@ -0,0 +1,254 @@ +{ + "schema_version": 1, + "id": "drv-db7bca98dc05ca24898e309e3d0eb89c3d812847490b9369cb006cf37625e3c6", + "goal": "Decide whether the Yield skill converter must verify generated skills by executing them under yskill test before completing", + "subject": "Converter completion evidence: executed fixture run vs model transcription", + "variants": [ + { + "id": "convert-skill-verified-v1", + "role": "design-candidate", + "model_id": "convert-skill-verified-v1", + "model_sha256": "02d96d55d4d932147d60ff31f44d835a67690afd835bd7a2056bde78db53b769", + "basis": { + "observed": 11, + "inferred": 0, + "assumed": 12 + } + }, + { + "id": "convert-skill-transcribed-v1", + "role": "design-candidate", + "model_id": "convert-skill-transcribed-v1", + "model_sha256": "a43832bbaed492a45ec71e49878278950ccdf1db691908c5bcfb7d3a33c064fb", + "basis": { + "observed": 5, + "inferred": 0, + "assumed": 12 + } + } + ], + "runs": [ + { + "operator_id": "verification.safety-reachability", + "operator_version": "0.1.0", + "variant_id": "convert-skill-verified-v1", + "input_sha256": "02d96d55d4d932147d60ff31f44d835a67690afd835bd7a2056bde78db53b769", + "output_sha256": "12eab977b5b4f258c8ce49ba91ee4bbc9dff86325aab43d551bf7ee697de1e78", + "verdict": { + "operator_id": "verification.safety-reachability", + "operator_version": "0.1.0", + "model_id": "convert-skill-verified-v1", + "decision": "applicable", + "satisfied": [ + { + "id": "automaton-states", + "check": "has-facet:states", + "description": "A discrete-event facet with declared states.", + "satisfied": true + }, + { + "id": "automaton-events", + "check": "has-facet:events", + "description": "A declared event alphabet.", + "satisfied": true + }, + { + "id": "automaton-transitions", + "check": "has-facet:transitions", + "description": "Plant transitions over the declared states and events.", + "satisfied": true + }, + { + "id": "spec-present", + "check": "has-spec-forbidden", + "description": "A specification naming forbidden states or forbidden (state, event) transitions — safety must be named, not assumed.", + "satisfied": true, + "obtainable": true + }, + { + "id": "transition-lineage", + "check": "evidence-present:transitions", + "description": "Transitions carry evidence references into the real system.", + "satisfied": true, + "obtainable": true + } + ] + }, + "output": { + "reachable": false, + "violating_trace": [], + "reachable_states": [ + "FLOW_EXTRACTED", + "GENERATED", + "LANG_CHOSEN", + "REPORTED_BLOCKED", + "SHIPPED_VERIFIED", + "SOURCED", + "TEST_FAILED", + "TEST_PASSED", + "TEST_RUN" + ] + }, + "verify": { + "schema_ok": true, + "invariants": [ + { + "invariant": "violating-trace-is-a-declared-path", + "passed": true + }, + { + "invariant": "violating-trace-ends-forbidden", + "passed": true + }, + { + "invariant": "reachable-iff-witness", + "passed": true + } + ], + "accepted": true + }, + "claim_status": "theorem-only" + }, + { + "operator_id": "verification.safety-reachability", + "operator_version": "0.1.0", + "variant_id": "convert-skill-transcribed-v1", + "input_sha256": "a43832bbaed492a45ec71e49878278950ccdf1db691908c5bcfb7d3a33c064fb", + "output_sha256": "aa377ce2c4f59cc78e208279ecc291cc0c09111bb2868866bb646a70d8bbf211", + "verdict": { + "operator_id": "verification.safety-reachability", + "operator_version": "0.1.0", + "model_id": "convert-skill-transcribed-v1", + "decision": "applicable", + "satisfied": [ + { + "id": "automaton-states", + "check": "has-facet:states", + "description": "A discrete-event facet with declared states.", + "satisfied": true + }, + { + "id": "automaton-events", + "check": "has-facet:events", + "description": "A declared event alphabet.", + "satisfied": true + }, + { + "id": "automaton-transitions", + "check": "has-facet:transitions", + "description": "Plant transitions over the declared states and events.", + "satisfied": true + }, + { + "id": "spec-present", + "check": "has-spec-forbidden", + "description": "A specification naming forbidden states or forbidden (state, event) transitions — safety must be named, not assumed.", + "satisfied": true, + "obtainable": true + }, + { + "id": "transition-lineage", + "check": "evidence-present:transitions", + "description": "Transitions carry evidence references into the real system.", + "satisfied": true, + "obtainable": true + } + ] + }, + "output": { + "reachable": true, + "violating_trace": [ + { + "from": "SOURCED", + "event": "extract_flow", + "to": "FLOW_EXTRACTED" + }, + { + "from": "FLOW_EXTRACTED", + "event": "pick_language", + "to": "LANG_CHOSEN" + }, + { + "from": "LANG_CHOSEN", + "event": "generate_program", + "to": "GENERATED" + }, + { + "from": "GENERATED", + "event": "complete_untested", + "to": "SHIPPED_UNVERIFIED" + } + ], + "reachable_states": [ + "FLOW_EXTRACTED", + "GENERATED", + "LANG_CHOSEN", + "SHIPPED_UNVERIFIED", + "SOURCED" + ] + }, + "verify": { + "schema_ok": true, + "invariants": [ + { + "invariant": "violating-trace-is-a-declared-path", + "passed": true + }, + { + "invariant": "violating-trace-ends-forbidden", + "passed": true + }, + { + "invariant": "reachable-iff-witness", + "passed": true + } + ], + "accepted": true + }, + "claim_status": "theorem-only" + } + ], + "comparisons": [ + { + "operator_id": "verification.safety-reachability", + "property": "forbidden-unreachable", + "status": "decided", + "satisfying": [ + "convert-skill-verified-v1" + ], + "rejected": [ + { + "variant_id": "convert-skill-transcribed-v1", + "counterexample": [ + { + "from": "SOURCED", + "event": "extract_flow", + "to": "FLOW_EXTRACTED" + }, + { + "from": "FLOW_EXTRACTED", + "event": "pick_language", + "to": "LANG_CHOSEN" + }, + { + "from": "LANG_CHOSEN", + "event": "generate_program", + "to": "GENERATED" + }, + { + "from": "GENERATED", + "event": "complete_untested", + "to": "SHIPPED_UNVERIFIED" + } + ] + } + ], + "undetermined": [] + } + ], + "unknowns": [ + "Design A refuses complete_untested structurally: the event is in the alphabet, but the converter program offers no transition — Require(test.exit_code == 0) stands between GENERATED and completion.", + "Regeneration is bounded by a retry counter in the converter program; the model treats the bound as data, not modeled state.", + "Design B ships whatever the model asserts compiles; nothing observes the generated program executing." + ] +} diff --git a/labs/22-yield/yield/examples/convert-skill/SKILL.md b/labs/22-yield/yield/examples/convert-skill/SKILL.md new file mode 100644 index 00000000..06a40c05 --- /dev/null +++ b/labs/22-yield/yield/examples/convert-skill/SKILL.md @@ -0,0 +1,26 @@ +--- +name: convert-skill +description: Convert an existing prose SKILL.md into a Yield program in the language of your choice, verified by executing the result. +--- + +Run: + + yskill run . + +Follow each returned operation exactly. + +- `ask_user`: ask the user using the host's normal interface. +- `agent_task`: perform the task and return schema-valid JSON. For + `write-skill` and `fix-generated-*`, actually create or edit the files + on disk — the next operation executes them. +- `run_command`: yskill executes it itself; you will not see this kind. + +Resume the run after each operation: + + yskill resume --response response.json --skill . + +Do not skip an operation or invent its response. The program owns the +pipeline: read the prose, extract the flow, pick the language, write the +program — and completion requires the generated skill to pass its own +fixture run under `yskill test`. A conversion that was never executed is +never "done". diff --git a/labs/22-yield/yield/examples/convert-skill/fixtures/responses.json b/labs/22-yield/yield/examples/convert-skill/fixtures/responses.json new file mode 100644 index 00000000..2fe70eb2 --- /dev/null +++ b/labs/22-yield/yield/examples/convert-skill/fixtures/responses.json @@ -0,0 +1,19 @@ +{ + "source-path": { "value": "../investigate" }, + "extract-flow": { + "summary": "Bounded incident investigation: collect evidence, form at least three hypotheses, probe cheapest-first, conclude with a causal chain or block honestly.", + "steps": [ + { "id": "collect-evidence", "kind": "agent_task", "description": "Gather observable evidence for the failure." }, + { "id": "form-hypotheses", "kind": "agent_task", "description": "At least three hypotheses, cheapest-to-disprove first." }, + { "id": "probe", "kind": "run_command", "description": "Run each hypothesis's disprove command." }, + { "id": "assess", "kind": "agent_task", "description": "Judge refutation; surviving hypothesis needs a causal chain." }, + { "id": "bound", "kind": "branch", "description": "At most three failed attempts, then block." }, + { "id": "causal-chain", "kind": "require", "description": "Completion requires a stated causal chain." } + ] + }, + "pick-language": { "value": "python" }, + "dest-path": { "value": "../env-doctor" }, + "write-skill": { + "files": ["main.py", "SKILL.md", "skill.json", "fixtures/responses.json"] + } +} diff --git a/labs/22-yield/yield/examples/convert-skill/main.go b/labs/22-yield/yield/examples/convert-skill/main.go new file mode 100644 index 00000000..665f0734 --- /dev/null +++ b/labs/22-yield/yield/examples/convert-skill/main.go @@ -0,0 +1,126 @@ +// convert-skill: the converter is itself a Yield skill. It turns an +// existing prose SKILL.md into a Yield program in the operator's chosen +// language, and it completes ONLY when the generated skill passes its own +// fixture run under yskill test — the Locus-decided design +// (docs/locus/convert-verified.json vs convert-transcribed.json: shipping +// on the model's transcription is rejected with a violating trace). +// +// Division of labor: the program owns the pipeline order, the language +// menu, the retry bound, and the evidence gate. The model owns reading +// the prose, extracting the flow, and writing the code. +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/operatorstack/yield/internal/protocol" + "github.com/operatorstack/yield/sdk/yield" +) + +const flowSchema = `{ + "type": "object", + "required": ["summary", "steps"], + "properties": { + "summary": {"type": "string", "minLength": 1}, + "steps": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "kind", "description"], + "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"], + "properties": { + "files": {"type": "array", "minItems": 2, "items": {"type": "string", "minLength": 1}} + } +}` + +// shellQuote single-quotes a value for sh -c interpolation. +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} + +func main() { + yield.Main(func(ctx *yield.Context) (yield.Outcome, error) { + source := ctx.AskUser("source-path", "Path to the prose skill directory (contains SKILL.md)?") + + prose := ctx.RunCommand("read-prose", fmt.Sprintf("cat %s/SKILL.md", shellQuote(source)), 60) + ctx.Require(prose.ExitCode == 0, "the source SKILL.md is readable", map[string]int{"exit_code": prose.ExitCode}) + + flowRaw := ctx.AgentTask("extract-flow", + "Read the prose skill below and extract its implicit control flow as ordered steps: "+ + "questions to the user (ask_user), model judgment (agent_task), commands (run_command), "+ + "branches, and verification points (require). Preserve the skill's intent; do not invent steps.", + map[string]string{"skill_md": prose.Stdout}, + json.RawMessage(flowSchema)) + + lang := ctx.AskUser("pick-language", "Target language for the generated program?", + protocol.Option{Value: "go", Label: "Go"}, + protocol.Option{Value: "typescript", Label: "TypeScript"}, + protocol.Option{Value: "python", Label: "Python"}, + protocol.Option{Value: "rust", Label: "Rust"}) + + dest := ctx.AskUser("dest-path", "Directory to write the converted skill into?") + + written := ctx.AgentTask("write-skill", + "Write the converted Yield skill into the destination directory: the program "+ + "(main.go / main.ts / main.py / src/main.rs per the chosen language, using that language's SDK "+ + "from this repository), a THIN SKILL.md (keep the original prose voice, delegate sequencing to "+ + "`yskill run .`), a skill.json runner manifest (omit for Go), and fixtures/responses.json with a "+ + "happy-path scripted response for every ask_user and agent_task operation. "+ + "Return {\"files\": [paths you actually wrote]}.", + map[string]any{"flow": json.RawMessage(flowRaw), "language": lang, "destination": dest}, + json.RawMessage(filesSchema)) + + // The evidence gate with a bounded repair loop: at most two repair + // attempts, then an honest blocked terminal. The generated skill + // must run under the REAL supervisor; ${YSKILL:-yskill} lets + // harnesses point at a specific binary. + verifyCmd := fmt.Sprintf(`"${YSKILL:-yskill}" test %s`, shellQuote(dest)) + test := ctx.RunCommand("verify-generated", verifyCmd, 600) + for attempt := 1; test.ExitCode != 0 && attempt <= 2; attempt++ { + ctx.AgentTask(fmt.Sprintf("fix-generated-%d", attempt), + "The generated skill failed its fixture run. Fix the files in place and return "+ + "{\"files\": [paths you changed]}.", + map[string]any{ + "destination": dest, + "stdout": test.Stdout, + "stderr": test.Stderr, + }, + json.RawMessage(filesSchema)) + test = ctx.RunCommand(fmt.Sprintf("verify-generated-retry-%d", attempt), verifyCmd, 600) + } + if test.ExitCode != 0 { + return yield.Outcome{}, ctx.Blocked( + "the generated skill still fails its fixture run after two repair attempts — the flow extraction or the fixtures need human review") + } + + ctx.Require(test.ExitCode == 0, + "the generated skill passes its fixture run under yskill test", + map[string]any{"exit_code": test.ExitCode}) + + var files struct { + Files []string `json:"files"` + } + _ = json.Unmarshal(written, &files) + return ctx.Complete(map[string]any{ + "language": lang, + "files": files.Files, + "verified": true, + "destination": dest, + }) + }) +}