Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:d93bea787e01382dad70d63cf2d8519e8ddabe61a0a6144953fb6e356cbfa7df; version: 0.1.38 -->
<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:818ccb012cbc8c35c04c3d854954b9ee64039b3b93e103ea4fd57218142c9e02; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:
Expand Down
7 changes: 7 additions & 0 deletions .changeset/self-serve-private-mirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@operatorstack/yield": patch
---

Make non-Git onboarding commands explicit, generate commit-ready Rust lockfiles,
add exact local-runtime recovery to Go and Rust adapters, and require verified
Artifact Registry parity for stable releases.
2 changes: 1 addition & 1 deletion .claude/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:d93bea787e01382dad70d63cf2d8519e8ddabe61a0a6144953fb6e356cbfa7df; version: 0.1.38 -->
<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:818ccb012cbc8c35c04c3d854954b9ee64039b3b93e103ea4fd57218142c9e02; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:
Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/release-yield/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: release-yield
description: "Release Yield through its protected GitHub workflows and verify every public registry."
---

<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:d93bea787e01382dad70d63cf2d8519e8ddabe61a0a6144953fb6e356cbfa7df; version: 0.1.38 -->
<!-- generated-by: yskill; source: skills/release-yield; digest: sha256:818ccb012cbc8c35c04c3d854954b9ee64039b3b93e103ea4fd57218142c9e02; version: 0.1.38 -->

This adapter exposes the canonical Yield workflow at `skills/release-yield`.
Read its SKILL.md, then run from the repository root:
Expand Down
138 changes: 138 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,144 @@ jobs:
path: dist/crates-receipt/
if-no-files-found: error

private-mirror:
name: Mirror stable release to Artifact Registry
needs: [resolve, build, npm, pypi, crates]
if: needs.resolve.outputs.channel == 'stable'
runs-on: ubuntu-latest
environment: private-production
permissions:
contents: read
id-token: write
env:
VERSION: ${{ needs.resolve.outputs.version }}
SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
AR_NPM_URL: https://${{ vars.AR_LOCATION }}-npm.pkg.dev/${{ vars.AR_PROJECT }}/${{ vars.AR_NPM_REPO }}/
AR_PYTHON_URL: https://${{ vars.AR_LOCATION }}-python.pkg.dev/${{ vars.AR_PROJECT }}/${{ vars.AR_PYTHON_REPO }}/
steps:
- name: Check out mirror control code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ github.sha }}
path: control
- name: Check out exact release source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
path: source
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: crates-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/crates
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
- id: auth
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.DEPLOYER_SA_EMAIL }}
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3
with:
install_components: package-go-module
- name: Inspect immutable release unit
run: >-
node control/packaging/private-mirror.mjs inspect
--version "$VERSION"
--source-sha "$SOURCE_SHA"
--release-unit dist/release-unit
--crates dist/crates
--manifest "$RUNNER_TEMP/private-mirror-manifest.json"
- id: remote
name: Refuse drift and select missing private artifacts
run: >-
node control/packaging/private-mirror.mjs status
--manifest "$RUNNER_TEMP/private-mirror-manifest.json"
--status "$RUNNER_TEMP/private-mirror-status.json"
--output "$GITHUB_OUTPUT"
- name: Configure Artifact Registry npm authentication
if: steps.remote.outputs.npm_state != 'matched'
run: |
gcloud artifacts print-settings npm \
--project="${{ vars.AR_PROJECT }}" --location="${{ vars.AR_LOCATION }}" \
--repository="${{ vars.AR_NPM_REPO }}" --scope=@operatorstack > "$RUNNER_TEMP/yield.npmrc"
npx -y google-artifactregistry-auth "$RUNNER_TEMP/yield.npmrc"
- name: Mirror exact npm archives
if: steps.remote.outputs.npm_state != 'matched'
env:
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/yield.npmrc
shell: bash
run: |
set -euo pipefail
jq -r '.missing.npm[].file' "$RUNNER_TEMP/private-mirror-status.json" | while read -r file; do
npm publish "dist/release-unit/npm/${file}" --registry="$AR_NPM_URL"
done
- name: Mirror exact Python wheels
if: steps.remote.outputs.python_state != 'matched'
run: |
python -m pip install --disable-pip-version-check twine==6.2.0 keyrings.google-artifactregistry-auth==1.1.2
jq -r '.missing.python[].file' "$RUNNER_TEMP/private-mirror-status.json" | while read -r file; do
python -m twine upload --repository-url "$AR_PYTHON_URL" "dist/release-unit/pypi/${file}"
done
- name: Mirror exact tagged Go source
if: steps.remote.outputs.go_state == 'missing'
shell: bash
run: |
set -euo pipefail
test "$(git -C source rev-parse HEAD)" = "$SOURCE_SHA"
source_dir="$(mktemp -d)"
git -C source archive "$SOURCE_SHA" | tar -x -C "$source_dir"
gcloud artifacts go upload \
--project="${{ vars.AR_PROJECT }}" --location="${{ vars.AR_LOCATION }}" \
--repository="${{ vars.AR_GO_REPO }}" \
--module-path=github.com/operatorstack/yield \
--version="v${VERSION}" --source="$source_dir"
- name: Prepare exact crates.io mirror records
if: steps.remote.outputs.rust_state != 'matched'
run: >-
node control/packaging/private-mirror.mjs prepare-cargo
--manifest "$RUNNER_TEMP/private-mirror-manifest.json"
--status "$RUNNER_TEMP/private-mirror-status.json"
--crates dist/crates
--output "$RUNNER_TEMP/private-cargo"
- name: Mirror exact Rust crate archives and index records
if: steps.remote.outputs.rust_state != 'matched'
run: >-
gcloud artifacts generic upload
--project="${{ vars.AR_PROJECT }}" --location="${{ vars.AR_LOCATION }}"
--repository="${{ vars.AR_GENERIC_REPO }}"
--package=yield-rust --version="$VERSION"
--source-directory="$RUNNER_TEMP/private-cargo"
- name: Verify complete private mirror
shell: bash
run: |
set -euo pipefail
for attempt in {1..12}; do
if node control/packaging/private-mirror.mjs verify \
--manifest "$RUNNER_TEMP/private-mirror-manifest.json" \
--receipt "$RUNNER_TEMP/private-mirror.json"; then
exit 0
fi
test "$attempt" -lt 12
sleep 10
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: private-mirror-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: ${{ runner.temp }}/private-mirror.json
if-no-files-found: error

selfhost-canary:
name: Release skill against exact canary
needs: [resolve, npm]
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/release-finalize.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,14 @@ jobs:
version="${TAG#v}"
package_receipt="packages-${version}-${SOURCE_SHA}"
crates_receipt="crates-${version}-${SOURCE_SHA}"
private_receipt="private-mirror-${version}-${SOURCE_SHA}"
run_id=""
while read -r candidate; do
test -n "$candidate" || continue
artifact_names="$(gh api "/repos/${GITHUB_REPOSITORY}/actions/runs/${candidate}/artifacts?per_page=100" --jq '.artifacts[].name')"
if grep -Fqx "$package_receipt" <<< "$artifact_names" && grep -Fqx "$crates_receipt" <<< "$artifact_names"; then
if grep -Fqx "$package_receipt" <<< "$artifact_names" && \
grep -Fqx "$crates_receipt" <<< "$artifact_names" && \
grep -Fqx "$private_receipt" <<< "$artifact_names"; then
run_id="$candidate"
break
fi
Expand All @@ -108,6 +111,18 @@ jobs:
--name "$package_receipt" --dir "$RUNNER_TEMP/release-unit"
gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
--name "$crates_receipt" --dir "$RUNNER_TEMP/crates-receipt"
gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
--name "$private_receipt" --dir "$RUNNER_TEMP/private-mirror"
jq -e \
--arg version "$version" --arg source_sha "$SOURCE_SHA" \
'.schema_version == 1 and .version == $version and .source_sha == $source_sha and
(.targets | length == 6) and
(.packages.npm | length == 8) and (.packages.python | length == 6) and
(.packages.go | length == 1) and (.packages.rust | length == 7) and
([.states[]] | all(. == "matched")) and
([.endpoints.npm, .endpoints.python, .endpoints.go,
.endpoints.rust_index, .endpoints.rust_download] | all(type == "string"))' \
"$RUNNER_TEMP/private-mirror/private-mirror.json"
for package in \
@operatorstack/yield \
@operatorstack/create-yield \
Expand All @@ -131,7 +146,6 @@ jobs:
--source-sha "$SOURCE_SHA" \
--attempts 3 \
--delay-ms 10000
node evals/scripts/run.mjs --check
test "$(git rev-list -n 1 "$TAG")" = "$SOURCE_SHA"
assets="$RUNNER_TEMP/release-assets"
mkdir -p "$assets"
Expand Down
22 changes: 2 additions & 20 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ jobs:
- run: go test ./...

examples:
name: Example workflows and evaluations
name: Example workflows
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand All @@ -166,29 +166,11 @@ jobs:
examples/data-migration
examples/library/rust
- run: npm ci --ignore-scripts
- name: Rerun first-party evaluations
working-directory: evals
run: |
npm ci
npm test
- name: Validate semantic conversion receipt when required
working-directory: evals
env:
EVAL_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
EVAL_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: npm run test:conversion
- name: Run every example fixture
run: |
base_tag="$(git tag --merged HEAD --list 'v[0-9]*' --sort=-v:refname | head -n1)"
test -n "$base_tag"
pending_changesets="$(git diff --name-only --diff-filter=A "$base_tag"..HEAD -- '.changeset/*.md')"
if [[ -n "$pending_changesets" ]]; then
plan="$RUNNER_TEMP/example-release-plan.env"
node scripts/release-plan.mjs --bump auto --output "$plan" --notes "$RUNNER_TEMP/example-release-notes.md"
. "$plan"
else
version="${base_tag#v}"
fi
version="${base_tag#v}"
go build -ldflags "-X main.version=$version" -o "$RUNNER_TEMP/yskill" ./cmd/yskill
"$RUNNER_TEMP/yskill" test examples/investigate
"$RUNNER_TEMP/yskill" test examples/release-checklist
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ Registration is the discovery step. This command detects installed verified
agents and writes a small adapter for each one:

```bash
npm exec -- yskill register skills/release
npm exec -- yskill register skills/release --root .
```

Select verified agents explicitly when you do not want automatic detection:

```bash
npm exec -- yskill register skills/release \
npm exec -- yskill register skills/release --root . \
--agent cursor,codex,claude-code
```

Expand Down Expand Up @@ -239,8 +239,8 @@ helper:

| Language | Command |
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
| TypeScript | `npm exec -- yskill helper install --language typescript` |
| Python | `python -m yieldskill helper install --language python` |
| TypeScript | `npm exec -- yskill helper install --root . --language typescript` |
| Python | `python -m yieldskill helper install --root . --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` |

Expand Down
60 changes: 54 additions & 6 deletions cmd/yskill/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func cmdRegisterAll(args []string) error {
if launcherErr != nil {
return launcherErr
}
content := renderAdapter(metadata, sourceRel, digest, launcher)
content := renderAdapter(metadata, sourceRel, digest, launcher, manifest.Language)
for _, agent := range selectedAgents {
path := filepath.Join(repoRoot, filepath.FromSlash(agent.ProjectDir), metadata.Name, "SKILL.md")
plan := plansByPath[path]
Expand Down Expand Up @@ -345,7 +345,7 @@ func registerSkill(skillArg, rootArg string, requested []string) ([]registration
return nil, err
}
}
content := renderAdapter(metadata, sourceRel, digest, launcher)
content := renderAdapter(metadata, sourceRel, digest, launcher, manifest.Language)
byDestination := map[string][]string{}
for _, agent := range selected {
destination := filepath.Join(repoRoot, filepath.FromSlash(agent.ProjectDir), metadata.Name, "SKILL.md")
Expand Down Expand Up @@ -687,12 +687,16 @@ func verifyLocalRuntime(path, expected, language string) error {
}

func localRuntimeInstallCommand(language, expected string) string {
return localRuntimeInstallCommandFor(language, expected, runtime.GOOS)
}

func localRuntimeInstallCommandFor(language, expected, goos string) string {
switch language {
case "go":
if runtime.GOOS == "windows" {
return fmt.Sprintf(`New-Item -ItemType Directory -Force .yield\bin | Out-Null; $env:GOBIN="$PWD\.yield\bin"; $env:GOPROXY="https://get.operatorstack.systems/go,direct"; go install github.com/operatorstack/yield/cmd/yskill@v%s`, expected)
if goos == "windows" {
return fmt.Sprintf(`New-Item -ItemType Directory -Force .yield\bin | Out-Null; $env:GOBIN="$PWD\.yield\bin"; $env:GOPROXY="https://proxy.golang.org,direct"; go install github.com/operatorstack/yield/cmd/yskill@v%s`, expected)
}
return fmt.Sprintf(`mkdir -p .yield/bin && GOBIN="$PWD/.yield/bin" GOPROXY=https://get.operatorstack.systems/go,direct go install github.com/operatorstack/yield/cmd/yskill@v%s`, expected)
return fmt.Sprintf(`mkdir -p .yield/bin && GOBIN="$PWD/.yield/bin" GOPROXY=https://proxy.golang.org,direct go install github.com/operatorstack/yield/cmd/yskill@v%s`, expected)
case "rust":
return fmt.Sprintf(`cargo install yieldskill@%s --root .yield --locked`, expected)
default:
Expand Down Expand Up @@ -799,8 +803,52 @@ func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}

func renderAdapter(metadata skillMetadata, sourceRel, digest, launcher string) string {
func renderAdapter(metadata skillMetadata, sourceRel, digest, launcher, language string) string {
path := shellQuote(sourceRel)
if language == "go" || language == "rust" {
posixInstall := localRuntimeInstallCommandFor(language, runtimeVersion(), "linux")
windowsInstall := localRuntimeInstallCommandFor(language, runtimeVersion(), "windows")
return fmt.Sprintf(`---
name: %s
description: %s
---

%s%s; digest: %s; version: %s -->

This adapter exposes the canonical Yield workflow at %s.
Read its SKILL.md, then run from the repository root.

On macOS or Linux, if .yield/bin/yskill is missing, run:

%s

Then start or resume the workflow with .yield/bin/yskill.

On Windows PowerShell, if .yield\bin\yskill.exe is missing, run:

%s

Then start or resume the workflow with .\.yield\bin\yskill.exe.

Start the workflow:

.yield/bin/yskill run %s

On Windows PowerShell use:

.\.yield\bin\yskill.exe run %s

If installation was required, retry the matching run command above.
Follow each returned operation exactly. Answer each operation directly with
the same launcher:

<launcher> respond <run-id> --value <answer> --skill %s

For structured agent results, use --result-json instead of --value.

Do not skip an operation or invent its response.
`, metadata.Name, yamlString(metadata.Description), generatedAdapterPrefix, sourceRel, digest, runtimeVersion(), "`"+sourceRel+"`", posixInstall, windowsInstall, path, path, path)
}
return fmt.Sprintf(`---
name: %s
description: %s
Expand Down
Loading