From 47471c66bfe991218828b406cf44af6dc4deb5eb Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 10:23:00 +0100 Subject: [PATCH 01/34] fix: publish public releases from target package --- .github/workflows/publish.yml | 13 ++++++++-- opencode/LICENSE | 21 +++++++++++++++ opencode/README.md | 3 +++ opencode/package.json | 6 ++++- scripts/resolve-release-target.mjs | 41 ++++++++++++++++++++++++++++++ test/release-target.test.ts | 26 +++++++++++++++++++ 6 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 opencode/LICENSE create mode 100644 scripts/resolve-release-target.mjs create mode 100644 test/release-target.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36dfabd..8349c61 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,8 +18,17 @@ jobs: with: node-version: 24 registry-url: https://registry.npmjs.org - cache: npm - run: npm ci - run: npm run build - run: npm test - - run: npm publish --access public + - run: npm run validate:public + - name: Resolve release target + id: release + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: node scripts/resolve-release-target.mjs "$RELEASE_TAG" >> "$GITHUB_OUTPUT" + - name: Publish release target + run: >- + npm publish "${{ steps.release.outputs.package_spec }}" + --access public + --tag "${{ steps.release.outputs.dist_tag }}" diff --git a/opencode/LICENSE b/opencode/LICENSE new file mode 100644 index 0000000..68ff8cb --- /dev/null +++ b/opencode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 aictrl.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/opencode/README.md b/opencode/README.md index e211422..1a48b1d 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -14,3 +14,6 @@ you use the connected `implement-code-change` workflow. Use `npx @aictrl/opencode --project .` for project-local installation or `npx @aictrl/opencode --uninstall` to remove only AICtrl-managed skills and MCP configuration. + +Support: https://aictrl.dev/support · Privacy: https://aictrl.dev/privacy · +Terms: https://aictrl.dev/terms diff --git a/opencode/package.json b/opencode/package.json index dc32d65..acbd24d 100644 --- a/opencode/package.json +++ b/opencode/package.json @@ -1,6 +1,6 @@ { "name": "@aictrl/opencode", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Install AICtrl engineering skills and OAuth workflow MCP for OpenCode", "type": "module", "bin": { @@ -26,5 +26,9 @@ }, "engines": { "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "tag": "beta" } } diff --git a/scripts/resolve-release-target.mjs b/scripts/resolve-release-target.mjs new file mode 100644 index 0000000..2c181a6 --- /dev/null +++ b/scripts/resolve-release-target.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tag = process.argv[2]; + +const targets = [ + { + prefix: 'public-v', + packageSpec: './opencode', + packageJson: 'opencode/package.json', + distTag: 'beta', + }, + { + prefix: 'v', + packageSpec: '.', + packageJson: 'package.json', + distTag: 'latest', + }, +]; + +const target = targets.find((candidate) => tag?.startsWith(candidate.prefix)); +if (!target) { + fail(`Unsupported release tag: ${tag || '(missing)'}`); +} + +const packageMetadata = JSON.parse(readFileSync(join(root, target.packageJson), 'utf8')); +const expectedTag = `${target.prefix}${packageMetadata.version}`; +if (tag !== expectedTag) { + fail(`Release tag ${tag} does not match ${packageMetadata.name}@${packageMetadata.version}; expected ${expectedTag}`); +} + +process.stdout.write(`package_spec=${target.packageSpec}\n`); +process.stdout.write(`dist_tag=${target.distTag}\n`); + +function fail(message) { + console.error(message); + process.exit(1); +} diff --git a/test/release-target.test.ts b/test/release-target.test.ts new file mode 100644 index 0000000..c3d9d65 --- /dev/null +++ b/test/release-target.test.ts @@ -0,0 +1,26 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const resolver = resolve(repoRoot, 'scripts/resolve-release-target.mjs'); +const rootVersion = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')).version; +const publicVersion = JSON.parse(readFileSync(resolve(repoRoot, 'opencode/package.json'), 'utf8')).version; + +describe('release target resolver', () => { + it('routes public beta releases to the OpenCode package', () => { + expect(resolveTag(`public-v${publicVersion}`)).toBe('package_spec=./opencode\ndist_tag=beta\n'); + }); + + it('routes root package releases independently', () => { + expect(resolveTag(`v${rootVersion}`)).toBe('package_spec=.\ndist_tag=latest\n'); + }); + + it.each(['public-v0.0.0', 'v0.0.0', 'release-1'])('rejects an invalid or stale tag: %s', (tag) => { + expect(() => resolveTag(tag)).toThrow(); + }); +}); + +function resolveTag(tag: string): string { + return execFileSync(process.execPath, [resolver, tag], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); +} From f7f4d611a10dfa22af4444106f4fb8cba035d71a Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 10:59:26 +0100 Subject: [PATCH 02/34] docs: link canonical public skills source --- README.md | 7 ++++--- opencode/README.md | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b35a204..c7bedbe 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ Use `npx @aictrl/opencode --project .` for a project-local install or ## Reproducible skill source -`public-skills.lock.json` pins an immutable `aictrl-dev/skills` commit and the -digest of its checksum manifest. All three vendor packages contain byte-identical -copies of the eight launch skills. +`public-skills.lock.json` pins an immutable +[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) commit and the digest +of its checksum manifest. All three vendor packages contain byte-identical copies +of the eight launch skills. ```bash npm run assemble:public diff --git a/opencode/README.md b/opencode/README.md index 1a48b1d..a6b59c7 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -11,6 +11,10 @@ opencode mcp auth aictrl The skills work locally without an AICtrl account. OAuth is requested only when you use the connected `implement-code-change` workflow. +The package is assembled from the pinned, checksummed +[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) release; the canonical +skills remain free to install directly on any supported coding agent. + Use `npx @aictrl/opencode --project .` for project-local installation or `npx @aictrl/opencode --uninstall` to remove only AICtrl-managed skills and MCP configuration. From 3f910ed4459160076cc43723bf8727c8c0bcccc8 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 11:00:57 +0100 Subject: [PATCH 03/34] chore: refresh audited test toolchain --- package-lock.json | 300 +++++++++++++++++++++++----------------------- 1 file changed, 150 insertions(+), 150 deletions(-) diff --git a/package-lock.json b/package-lock.json index 588d11b..bc669a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,9 +24,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -41,9 +41,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -58,9 +58,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -75,9 +75,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -92,9 +92,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -109,9 +109,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -126,9 +126,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -143,9 +143,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -177,9 +177,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1192,15 +1192,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1209,13 +1209,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1236,9 +1236,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1249,13 +1249,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -1264,13 +1264,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1279,9 +1279,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1292,13 +1292,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -1452,9 +1452,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1465,32 +1465,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -1912,13 +1912,13 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -2010,20 +2010,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -2053,8 +2053,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, From 27c35ecfb40b0a75b1d5798b0966d2778219cbfe Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 11:57:39 +0100 Subject: [PATCH 04/34] test(codex): exercise public plugin lifecycle --- .github/workflows/ci.yml | 13 ++++ package.json | 1 + scripts/smoke-codex-public.mjs | 130 +++++++++++++++++++++++++++++++++ submission/codex/readiness.md | 2 + 4 files changed, 146 insertions(+) create mode 100644 scripts/smoke-codex-public.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37acbeb..d924e6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,19 @@ jobs: - run: npm test - run: npm run validate:public + codex-public: + name: E2E (Codex public marketplace) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported Codex CLI + run: npm install --global @openai/codex@0.144.1 + - name: Run clean install, repeat, upgrade, and removal smoke + run: npm run smoke:codex-public + e2e: name: E2E (OpenCode against production) runs-on: ubuntu-latest diff --git a/package.json b/package.json index ce67faf..eafbc49 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "assemble:public": "node scripts/assemble-public-packages.mjs", "verify:public": "node scripts/assemble-public-packages.mjs --check", "validate:public": "npm run verify:public && node scripts/validate-public-packages.mjs", + "smoke:codex-public": "node scripts/smoke-codex-public.mjs", "prepublishOnly": "npm run build" }, "files": [ diff --git a/scripts/smoke-codex-public.mjs b/scripts/smoke-codex-public.mjs new file mode 100644 index 0000000..706550d --- /dev/null +++ b/scripts/smoke-codex-public.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + cpSync, + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const codexHome = mkdtempSync(join(tmpdir(), 'aictrl-codex-public-')); +const marketplaceRoot = join(codexHome, 'marketplace'); +const marketplacePluginRoot = join(marketplaceRoot, 'plugins/aictrl'); +const codex = process.env.CODEX_BIN || 'codex'; +const env = { ...process.env, CODEX_HOME: codexHome }; +const pluginId = 'aictrl@aictrl-public'; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + cpSync(join(root, '.agents'), join(marketplaceRoot, '.agents'), { recursive: true }); + cpSync(join(root, 'plugins'), join(marketplaceRoot, 'plugins'), { recursive: true }); + + run(['plugin', 'marketplace', 'add', marketplaceRoot]); + assertIncludes(run(['plugin', 'marketplace', 'list']), 'aictrl-public', 'marketplace list'); + + const sourceManifest = json(join(marketplacePluginRoot, '.codex-plugin/plugin.json')); + run(['plugin', 'add', pluginId]); + assertInstalled(sourceManifest.version); + + // Re-installing the same package must be idempotent. + run(['plugin', 'add', pluginId]); + assertInstalled(sourceManifest.version); + + // A changed marketplace version must replace the installed cache entry. + const upgradeVersion = `${sourceManifest.version.split('-')[0]}-smoke.1`; + writeFileSync( + join(marketplacePluginRoot, '.codex-plugin/plugin.json'), + `${JSON.stringify({ ...sourceManifest, version: upgradeVersion }, null, 2)}\n`, + ); + run(['plugin', 'add', pluginId]); + assertInstalled(upgradeVersion); + + run(['plugin', 'remove', pluginId]); + assertIncludes(run(['plugin', 'list']), 'not installed', 'plugin list after removal'); + + const config = readFileSync(join(codexHome, 'config.toml'), 'utf8'); + if (config.includes(`[plugins."${pluginId}"]`)) { + throw new Error('Codex removal left the AICtrl plugin enabled in config.toml'); + } + assertIncludes(config, '[marketplaces.aictrl-public]', 'config after removal'); + + console.log('Codex public marketplace lifecycle smoke passed.'); +} finally { + rmSync(codexHome, { recursive: true, force: true }); +} + +function assertInstalled(expectedVersion) { + const listing = run(['plugin', 'list']); + assertIncludes(listing, pluginId, 'installed plugin list'); + assertIncludes(listing, 'installed, enabled', 'installed plugin status'); + + const installedRoot = join( + codexHome, + 'plugins/cache/aictrl-public/aictrl', + expectedVersion, + ); + const installedManifest = json(join(installedRoot, '.codex-plugin/plugin.json')); + if (installedManifest.name !== 'aictrl' || installedManifest.version !== expectedVersion) { + throw new Error('Installed Codex manifest does not match the source package'); + } + + const installedMcp = json(join(installedRoot, '.mcp.json')); + if (installedMcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { + throw new Error('Installed Codex plugin does not target the production workflow MCP'); + } + + const skillsRoot = join(installedRoot, 'skills'); + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed Codex skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed Codex skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(args) { + try { + return execFileSync(codex, args, { + cwd: root, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + throw new Error(`codex ${args.join(' ')} failed${stderr ? `: ${stderr.trim()}` : ''}`); + } +} + +function json(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +function assertIncludes(value, expected, context) { + if (!value.includes(expected)) { + throw new Error(`${context} did not include ${JSON.stringify(expected)}`); + } +} diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 973a72e..ac05c55 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -4,6 +4,8 @@ - [x] `.codex-plugin/plugin.json` passes local ingestion validation. - [x] Marketplace policy includes installation, authentication, and category. +- [x] Codex CLI 0.144.1 completes clean marketplace add, install, repeat install, + version upgrade, and removal in CI while preserving marketplace configuration. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. From 6c997d6c97c2be4c696f394a8f95c72cb1b1ca03 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 12:20:12 +0100 Subject: [PATCH 05/34] test(claude): exercise public plugin lifecycle --- .github/workflows/ci.yml | 15 ++++ README.md | 4 +- package.json | 1 + scripts/smoke-claude-public.mjs | 126 ++++++++++++++++++++++++++++++++ submission/codex/readiness.md | 2 + 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 scripts/smoke-claude-public.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d924e6c..b8d15f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,21 @@ jobs: - name: Run clean install, repeat, upgrade, and removal smoke run: npm run smoke:codex-public + claude-public: + name: E2E (Claude public marketplace) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported Claude Code CLI + run: npm install --global @anthropic-ai/claude-code@2.1.207 + - name: Run clean public install, repeat, and removal smoke + env: + AICTRL_CLAUDE_MARKETPLACE_SOURCE: ${{ github.event.pull_request.head.repo.full_name || github.repository }}@${{ github.head_ref || github.ref_name }} + run: npm run smoke:claude-public + e2e: name: E2E (OpenCode against production) runs-on: ubuntu-latest diff --git a/README.md b/README.md index c7bedbe..99472fe 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ npm run verify:public ``` CI rejects checksum mismatches, missing/extra skills, manual generated drift, -invalid Codex metadata, and package lifecycle regressions. +invalid Codex metadata, and package lifecycle regressions. Clean-client +lifecycle jobs exercise the real Claude Code and Codex CLIs against the public +marketplaces, including repeated installation and removal. ## Release status diff --git a/package.json b/package.json index eafbc49..65dde2d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "assemble:public": "node scripts/assemble-public-packages.mjs", "verify:public": "node scripts/assemble-public-packages.mjs --check", "validate:public": "npm run verify:public && node scripts/validate-public-packages.mjs", + "smoke:claude-public": "node scripts/smoke-claude-public.mjs", "smoke:codex-public": "node scripts/smoke-codex-public.mjs", "prepublishOnly": "npm run build" }, diff --git a/scripts/smoke-claude-public.mjs b/scripts/smoke-claude-public.mjs new file mode 100644 index 0000000..0a97a2c --- /dev/null +++ b/scripts/smoke-claude-public.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const configDir = mkdtempSync(join(tmpdir(), 'aictrl-claude-public-')); +const workDir = mkdtempSync(join(tmpdir(), 'aictrl-claude-work-')); +const claude = process.env.CLAUDE_BIN || 'claude'; +const source = process.env.AICTRL_CLAUDE_MARKETPLACE_SOURCE || 'aictrl-dev/aictrl-plugin'; +const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; +const marketplaceName = 'aictrl-public'; +const pluginId = `aictrl@${marketplaceName}`; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + run(['plugin', 'marketplace', 'add', source]); + const marketplaces = jsonOutput(['plugin', 'marketplace', 'list', '--json']); + const marketplace = marketplaces.find((entry) => entry.name === marketplaceName); + if (!marketplace || marketplace.source !== 'github') { + throw new Error(`Claude did not register ${marketplaceName} as a GitHub marketplace`); + } + + run(['plugin', 'install', pluginId, '--scope', 'user']); + assertInstalled(); + + // Re-installing the same public package must leave one enabled installation. + run(['plugin', 'install', pluginId, '--scope', 'user']); + assertInstalled(); + + run(['plugin', 'uninstall', pluginId, '--scope', 'user', '--yes']); + const afterUninstall = jsonOutput(['plugin', 'list', '--json']); + if (afterUninstall.some((plugin) => plugin.id === pluginId)) { + throw new Error('Claude uninstall left the AICtrl plugin installed'); + } + + run(['plugin', 'marketplace', 'remove', marketplaceName]); + const afterRemoval = jsonOutput(['plugin', 'marketplace', 'list', '--json']); + if (afterRemoval.some((entry) => entry.name === marketplaceName)) { + throw new Error('Claude marketplace removal left aictrl-public configured'); + } + + console.log('Claude public marketplace lifecycle smoke passed.'); +} finally { + rmSync(configDir, { recursive: true, force: true }); + rmSync(workDir, { recursive: true, force: true }); +} + +function assertInstalled() { + const installed = jsonOutput(['plugin', 'list', '--json']); + const matches = installed.filter((plugin) => plugin.id === pluginId); + if (matches.length !== 1 || !matches[0].enabled) { + throw new Error(`Expected one enabled ${pluginId} installation`); + } + + const installedRoot = resolve(matches[0].installPath); + if (!installedRoot.startsWith(`${resolve(configDir)}${sep}`)) { + throw new Error('Claude installed the plugin outside the isolated config directory'); + } + + const manifest = jsonFile(join(installedRoot, '.claude-plugin/plugin.json')); + if (manifest.name !== 'aictrl' || manifest.version !== matches[0].version) { + throw new Error('Installed Claude manifest does not match the plugin listing'); + } + + const mcp = jsonFile(join(installedRoot, '.mcp.json')); + if (mcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { + throw new Error('Installed Claude plugin does not target the production workflow MCP'); + } + + const skillsRoot = join(installedRoot, 'skills'); + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed Claude skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed Claude skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(args) { + try { + return execFileSync(claude, args, { + cwd: workDir, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + const stdout = error?.stdout?.toString?.() || ''; + const detail = stderr.trim() || stdout.trim(); + throw new Error(`claude ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); + } +} + +function jsonOutput(args) { + return JSON.parse(run(args)); +} + +function jsonFile(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index ac05c55..274815f 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -6,6 +6,8 @@ - [x] Marketplace policy includes installation, authentication, and category. - [x] Codex CLI 0.144.1 completes clean marketplace add, install, repeat install, version upgrade, and removal in CI while preserving marketplace configuration. +- [x] Claude Code 2.1.207 completes clean public marketplace add, install, repeat + install, and removal in CI. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. From b889c72cc76d3de96ebd573efe947617d5a6af99 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 12:30:19 +0100 Subject: [PATCH 06/34] test(opencode): gate public OAuth lifecycle --- .github/workflows/ci.yml | 15 +++ .github/workflows/publish.yml | 4 + README.md | 6 +- package.json | 1 + scripts/smoke-opencode-public.mjs | 158 ++++++++++++++++++++++++++++++ submission/codex/readiness.md | 2 + 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 scripts/smoke-opencode-public.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8d15f0..8084516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,21 @@ jobs: AICTRL_CLAUDE_MARKETPLACE_SOURCE: ${{ github.event.pull_request.head.repo.full_name || github.repository }}@${{ github.head_ref || github.ref_name }} run: npm run smoke:claude-public + opencode-public: + name: E2E (OpenCode public package) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install the supported OpenCode CLI + run: npm install --global opencode-ai@1.17.20 + - name: Run packed install, OAuth boundary, repeat, and removal smoke + env: + AICTRL_OPENCODE_CONNECTIVITY_URL: https://sandbox.aictrl.dev/mcp/workflows + run: npm run smoke:opencode-public + e2e: name: E2E (OpenCode against production) runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8349c61..c6784fc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,6 +22,10 @@ jobs: - run: npm run build - run: npm test - run: npm run validate:public + - name: Verify production OpenCode OAuth boundary + run: | + npm install --global opencode-ai@1.17.20 + npm run smoke:opencode-public - name: Resolve release target id: release env: diff --git a/README.md b/README.md index 99472fe..83e682a 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,10 @@ npm run verify:public CI rejects checksum mismatches, missing/extra skills, manual generated drift, invalid Codex metadata, and package lifecycle regressions. Clean-client -lifecycle jobs exercise the real Claude Code and Codex CLIs against the public -marketplaces, including repeated installation and removal. +lifecycle jobs exercise the real Claude Code, Codex, and OpenCode CLIs against +the packed or public distribution paths, including repeated installation and +removal. The npm release job also requires OpenCode's production OAuth boundary +to reach the expected unauthenticated state before publishing. ## Release status diff --git a/package.json b/package.json index 65dde2d..1b2433f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "validate:public": "npm run verify:public && node scripts/validate-public-packages.mjs", "smoke:claude-public": "node scripts/smoke-claude-public.mjs", "smoke:codex-public": "node scripts/smoke-codex-public.mjs", + "smoke:opencode-public": "node scripts/smoke-opencode-public.mjs", "prepublishOnly": "npm run build" }, "files": [ diff --git a/scripts/smoke-opencode-public.mjs b/scripts/smoke-opencode-public.mjs new file mode 100644 index 0000000..b7f8051 --- /dev/null +++ b/scripts/smoke-opencode-public.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tempRoot = mkdtempSync(join(tmpdir(), 'aictrl-opencode-public-')); +const packDir = join(tempRoot, 'pack'); +const configDir = join(tempRoot, 'config'); +const dataDir = join(tempRoot, 'data'); +const homeDir = join(tempRoot, 'home'); +const npm = process.env.NPM_BIN || 'npm'; +const opencode = process.env.OPENCODE_BIN || 'opencode'; +const productionMcpUrl = 'https://aictrl.dev/mcp/workflows'; +const connectivityUrl = process.env.AICTRL_OPENCODE_CONNECTIVITY_URL || productionMcpUrl; +const configFile = join(configDir, 'opencode/opencode.json'); +const skillsRoot = join(configDir, 'opencode/skills'); +const env = { + ...process.env, + HOME: homeDir, + XDG_CONFIG_HOME: configDir, + XDG_DATA_HOME: dataDir, +}; +const expectedSkills = [ + 'code-review', + 'create-bug', + 'create-issue', + 'create-workflow', + 'implement-code-change', + 'judge-review-findings', + 'reply-to-code-review', + 'spec-review', +]; + +try { + mkdirSync(packDir, { recursive: true }); + const tarballName = run(npm, [ + 'pack', + './opencode', + '--pack-destination', + packDir, + '--silent', + ]).trim(); + if (!tarballName.endsWith('.tgz')) { + throw new Error(`npm pack returned an unexpected filename: ${tarballName}`); + } + const tarball = join(packDir, tarballName); + + install(tarball); + assertInstalled(productionMcpUrl); + + // Re-running the packed public command must be idempotent. + install(tarball); + assertInstalled(productionMcpUrl); + + if (connectivityUrl !== productionMcpUrl) { + const config = jsonFile(configFile); + config.mcp.aictrl.url = connectivityUrl; + writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); + } + + const listing = run(opencode, ['mcp', 'list']); + if (!listing.includes('aictrl') || !listing.includes('needs authentication')) { + throw new Error(`OpenCode did not reach the expected OAuth boundary:\n${listing.trim()}`); + } + if (listing.includes('failed')) { + throw new Error(`OpenCode reported a failed MCP connection:\n${listing.trim()}`); + } + + runPackage(tarball, ['--uninstall']); + const afterUninstall = jsonFile(configFile); + if (afterUninstall.mcp?.aictrl) { + throw new Error('OpenCode uninstall left the AICtrl MCP entry configured'); + } + for (const skill of expectedSkills) { + if (existsSync(join(skillsRoot, skill))) { + throw new Error(`OpenCode uninstall left an AICtrl skill installed: ${skill}`); + } + } + + console.log(`OpenCode public package lifecycle smoke passed against ${connectivityUrl}.`); +} finally { + rmSync(tempRoot, { recursive: true, force: true }); +} + +function install(tarball) { + runPackage(tarball, []); +} + +function runPackage(tarball, args) { + return run(npm, [ + 'exec', + '--yes', + '--package', + tarball, + '--', + 'aictrl-opencode', + ...args, + ]); +} + +function assertInstalled(expectedUrl) { + const config = jsonFile(configFile); + if (config.$schema !== 'https://opencode.ai/config.json') { + throw new Error('OpenCode package did not write the canonical schema URL'); + } + if ( + config.mcp?.aictrl?.type !== 'remote' + || config.mcp.aictrl.url !== expectedUrl + || config.mcp.aictrl.enabled !== true + ) { + throw new Error('OpenCode package did not write the production OAuth MCP entry'); + } + + const installedSkills = readdirSync(skillsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + if (JSON.stringify(installedSkills) !== JSON.stringify(expectedSkills)) { + throw new Error(`Installed OpenCode skills differ: ${installedSkills.join(', ')}`); + } + for (const skill of expectedSkills) { + if (!existsSync(join(skillsRoot, skill, 'SKILL.md'))) { + throw new Error(`Installed OpenCode skill is missing SKILL.md: ${skill}`); + } + } +} + +function run(command, args) { + try { + return execFileSync(command, args, { + cwd: root, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const stderr = error?.stderr?.toString?.() || ''; + const stdout = error?.stdout?.toString?.() || ''; + const detail = stderr.trim() || stdout.trim(); + throw new Error(`${command} ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`); + } +} + +function jsonFile(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 274815f..b8e2b06 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -8,6 +8,8 @@ version upgrade, and removal in CI while preserving marketplace configuration. - [x] Claude Code 2.1.207 completes clean public marketplace add, install, repeat install, and removal in CI. +- [x] OpenCode 1.17.20 installs the packed npm artifact, discovers the sandbox + OAuth boundary, repeats idempotently, and removes only AICtrl-managed state. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. From f84fd6adcfa8af114a34a9e329c7fe6cffaf149b Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 12:32:21 +0100 Subject: [PATCH 07/34] fix(release): scope OpenCode gate to public tags --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6784fc..7f224ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,7 @@ jobs: - run: npm test - run: npm run validate:public - name: Verify production OpenCode OAuth boundary + if: startsWith(github.event.release.tag_name, 'public-v') run: | npm install --global opencode-ai@1.17.20 npm run smoke:opencode-public From b4008b46377f57262879aa5995d9823e80a5409a Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 14:01:40 +0100 Subject: [PATCH 08/34] fix(ci): restrict workflow token permissions --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8084516..27bb1d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: unit: name: Unit Tests From 70d41ac45eabe2c7bbf75b7091c91afc49b90a12 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 15:41:02 +0100 Subject: [PATCH 09/34] docs: add public release runbook --- README.md | 4 + docs/public-release-runbook.md | 203 +++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 docs/public-release-runbook.md diff --git a/README.md b/README.md index 83e682a..065a45f 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ the production `https://aictrl.dev/mcp/workflows` endpoint, OAuth hardening, clean-client lifecycle evidence, publisher verification, and vendor publication checks. Local skills do not require an AICtrl account or API key. +Release owners must follow the [public release runbook](docs/public-release-runbook.md), +including the skills re-pin, first npm publication, vendor smoke tests, evidence, +and rollback gates. + The existing `npx @aictrl/plugin` tenant installer remains supported and is not silently replaced by this public OAuth path. diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md new file mode 100644 index 0000000..c787e81 --- /dev/null +++ b/docs/public-release-runbook.md @@ -0,0 +1,203 @@ +# Public agent package release runbook + +Use this runbook for the AICtrl public packages distributed through Claude Code, +Codex/ChatGPT, and OpenCode. A release is complete only when the public artifact +installs from a clean client and the connected workflow reaches a verified +terminal result. Creating a tag, submitting a listing, or passing package CI is +not publication by itself. + +## Owners and access + +Before starting, record one primary and one backup owner for each item in the +release evidence: + +- AICtrl runtime deployment and rollback; +- `aictrl-dev/skills` release and checksums; +- `aictrl-dev/aictrl-plugin` release and GitHub environments; +- the `@aictrl` npm organization and package; +- the OpenAI publisher identity, plugin portal, domain, and reviewer account; +- the OpenCode Ecosystem contribution; +- incident response and customer communication. + +Confirm that the owners can use the production accounts before creating a +release. Never put a token, recovery code, reviewer password, or challenge value +in this repository or in release evidence. + +## Stop conditions + +Stop the release when any of these is true: + +- the pinned skills commit or checksum manifest does not match generated files; +- the public MCP catalog is not exactly the six documented workflow lifecycle + tools; +- a tool schema or annotation differs from deployed behavior; +- OAuth, tenant, repository, workflow, run, or revision authorization fails a + negative test; +- production health or the OpenCode OAuth-boundary smoke test fails; +- package, secret, lifecycle, or clean-install validation fails; +- support, privacy, terms, publisher identity, reviewer access, or rollback + ownership is incomplete. + +## Promotion order + +1. Merge the runtime and domain-verification changes to `sandbox` and wait for a + healthy deployment. +2. Apply the canonical `implement-code-change` workflow in sandbox. Verify its + immutable version, `repository` and `issue-id` inputs, exact-revision gate, + two task bounds, and no-merge/no-deploy boundary. +3. Merge `aictrl-dev/skills#4`, create a new semantic release, and verify the + published `CHECKSUMS.sha256` against the release commit. +4. Update `public-skills.lock.json` to that exact commit and checksum-manifest + digest. Run `npm run assemble:public`; do not hand-edit generated skill files. +5. Run all package verification below, then merge the plugin PR. +6. Promote the sandbox runtime batch to production and repeat the health, OAuth, + MCP catalog, schema, annotation, and connected-workflow checks. +7. Publish and verify the public Git, npm, and portal artifacts in the vendor + sections below. + +Do not reorder steps 3 and 4. The skills v1.0.0 connected instructions use the +obsolete `issue_id` key; the canonical workflow requires the exact `issue-id` +key published by `aictrl-dev/skills#4`. + +## Package verification + +Run from a clean checkout of the exact plugin release commit: + +```bash +npm ci +npm run build +npm test +npm run validate:public +npm run smoke:claude-public +npm run smoke:codex-public +npm run smoke:opencode-public +npm pack ./opencode --dry-run --json +``` + +Record the commit, skill source commit, checksum-manifest digest, package +versions, command results, client versions, and UTC time. The packed OpenCode +artifact must contain only the installer, README, license, skills manifest, and +the eight generated skills. + +## Claude Code publication + +1. Publish the final plugin commit and release notes in the public Git + repository. +2. From a clean Claude Code client, add `aictrl-dev/aictrl-plugin`, install + `aictrl@aictrl-public`, and start a new session. +3. Complete one local skill without authentication. +4. Complete native OAuth and the connected `implement-code-change` test through + result retrieval. +5. Repeat install, upgrade, uninstall, and marketplace removal while confirming + unrelated client state is preserved. + +Claude publication has no separate third-party directory gate unless Anthropic +publishes one. The public Git marketplace and clean external smoke test are the +release evidence. + +## OpenCode npm publication + +### First package publication + +`@aictrl/opencode` must exist before npm trusted publishing can be configured. +For the first beta only, an authenticated `@aictrl` npm owner publishes the +verified package from the merged release commit: + +```bash +npm whoami +npm publish ./opencode --access public --tag beta +``` + +The owner must satisfy npm's current 2FA policy. Do not add a long-lived npm +token to the repository. After the package exists, configure its trusted +publisher for GitHub organization `aictrl-dev`, repository `aictrl-plugin`, +workflow `publish.yml`, environment `release`, and the `npm publish` action. + +### Later beta publications + +Create a GitHub release whose tag exactly matches +`public-v`. The release workflow validates the +package and production OAuth boundary, then publishes `./opencode` with the +`beta` dist-tag through npm OIDC. + +For every npm publication, verify from an unauthenticated clean environment: + +```bash +npm view @aictrl/opencode version dist-tags --json +npx @aictrl/opencode --project . +opencode mcp list +npx @aictrl/opencode --project . --uninstall +``` + +Only after the package is publicly installable, submit the one-row AICtrl +OpenCode Ecosystem change. Keep its issue, PR, merge commit, and listing smoke +test in the release evidence. + +## Codex and ChatGPT publication + +1. Confirm the OpenAI organization has a verified developer or business + identity and the submitter has Apps Management write access. +2. Enter the production MCP URL, `https://aictrl.dev/mcp/workflows`, in the + plugin portal. +3. Store the portal-provided domain token as a new production secret version. + Verify that `/.well-known/openai-apps-challenge` returns only the exact token + as plain text, then remove the token from local shell history and evidence. +4. Scan tools and confirm exactly six tools with truthful schemas, + `readOnlyHint`, `openWorldHint`, and `destructiveHint` annotations. +5. Upload the final generated skill tree, listing assets, starter prompts, + release notes, and exactly five positive plus three negative reviewer cases. +6. Verify the reviewer account works without MFA, email confirmation, SMS, or + private-network access. Select only supported regions and complete policy + attestations after the final review. +7. Submit for review, answer findings against the deployed version, and record + approval. Publication still requires the release owner to select Publish in + the portal after approval. +8. Install from the universal plugin directory in a clean Codex/ChatGPT client; + complete one local skill and one connected workflow before closing the vendor + issue. + +## Connected beta evidence + +The launch proof must use an authorized fixture repository and record: + +- referral source, agent platform, skill and plugin versions, without source or + prompt content; +- OAuth start, completion, and negative/cancel paths; +- the resolved workflow version and idempotent start; +- a pause at the exact base revision and an approval using the unchanged + expected revision; +- terminal status, pull-request URL, exact head revision, redacted evidence, + checks, reported cost, and errors; +- elapsed time, with the beta median under five minutes. + +The workflow must stop at a merge-ready pull request. Never use launch evidence +that merged or deployed automatically. + +## Rollback and credential rotation + +If a public artifact or connected path is unsafe: + +1. Stop new connected starts at the runtime or policy boundary while preserving + run and approval audit history. +2. Roll back the runtime to the last verified revision and repeat health and + authorization smoke tests. +3. Publish a corrected Git package version. Do not rewrite an immutable skills + tag or checksum manifest. +4. Deprecate an affected npm version and move the `beta` dist-tag to the last + safe version. Use npm unpublish only when its current policy permits it and + the incident owner explicitly approves the irreversible action. +5. Unpublish or disable the Codex plugin in the portal when the listing itself + is unsafe; notify reviewers when an in-review version is withdrawn. +6. Rotate any affected OAuth client, npm publisher, GitHub release, reviewer, + domain-challenge, or runtime secret. Revoke the old credential before + restoring publication. +7. Publish an incident note through the support/status channel and link the + replacement release. + +## Post-publication monitoring + +For the first 24 hours, the release owner monitors production health, OAuth +completion/failure, workflow starts and terminal states, authorization denials, +latency, cost, and support reports. Re-run the public install and connected smoke +test after any runtime, skill, package, listing, or credential change. + From bbfd6eb103c5f0f6f087d4381dedb64bb6b8e7e2 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 15:42:33 +0100 Subject: [PATCH 10/34] ci: pin npm for trusted publishing --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7f224ef..28db3cc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,8 @@ jobs: with: node-version: 24 registry-url: https://registry.npmjs.org + - name: Install npm with trusted publishing support + run: npm install --global npm@11.18.0 - run: npm ci - run: npm run build - run: npm test From c17ad1140afd82045bae1e6b01cf64358fb858bc Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 15:45:59 +0100 Subject: [PATCH 11/34] ci: label legacy production smoke accurately --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27bb1d0..e991d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: run: npm run smoke:opencode-public e2e: - name: E2E (OpenCode against production) + name: E2E (legacy installer against production) runs-on: ubuntu-latest if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork steps: From 57db28893e1af79ebb1b2c4d645d71c2c20d2541 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 15:52:25 +0100 Subject: [PATCH 12/34] fix: align public package beta versions --- .claude-plugin/marketplace.json | 2 +- claude/aictrl/.claude-plugin/plugin.json | 2 +- plugins/aictrl/.codex-plugin/plugin.json | 2 +- scripts/validate-public-packages.mjs | 13 +++++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b403fde..e56e510 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "aictrl", "source": "./claude/aictrl", "description": "Eight portable engineering skills with optional controlled AICtrl workflow execution", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "author": { "name": "aictrl.dev" }, diff --git a/claude/aictrl/.claude-plugin/plugin.json b/claude/aictrl/.claude-plugin/plugin.json index 31396b2..38ebe84 100644 --- a/claude/aictrl/.claude-plugin/plugin.json +++ b/claude/aictrl/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aictrl", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Eight portable engineering skills with optional controlled AICtrl workflow execution", "author": { "name": "aictrl.dev", diff --git a/plugins/aictrl/.codex-plugin/plugin.json b/plugins/aictrl/.codex-plugin/plugin.json index c71e961..1b9d07b 100644 --- a/plugins/aictrl/.codex-plugin/plugin.json +++ b/plugins/aictrl/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aictrl", - "version": "0.1.0-beta.1", + "version": "0.1.0-beta.2", "description": "Portable engineering skills with controlled AICtrl workflow execution", "author": { "name": "aictrl.dev", diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 250a7ab..c287c28 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -10,12 +10,25 @@ const plugin = json('plugins/aictrl/.codex-plugin/plugin.json'); const marketplace = json('.agents/plugins/marketplace.json'); const mcp = json('plugins/aictrl/.mcp.json'); const opencode = json('opencode/package.json'); +const claudePlugin = json('claude/aictrl/.claude-plugin/plugin.json'); +const claudeMarketplace = json('.claude-plugin/marketplace.json'); required(plugin, ['name', 'version', 'description', 'author', 'skills', 'mcpServers', 'interface']); if (plugin.name !== 'aictrl') errors.push('Codex plugin name must be aictrl'); if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(plugin.version)) { errors.push('Codex plugin version must be strict semver'); } +const publicVersion = opencode.version; +if (plugin.version !== publicVersion) { + errors.push('Codex and OpenCode public package versions must match'); +} +if (claudePlugin.version !== publicVersion) { + errors.push('Claude and OpenCode public package versions must match'); +} +const claudeEntry = claudeMarketplace.plugins?.find((candidate) => candidate.name === 'aictrl'); +if (claudeEntry?.version !== publicVersion) { + errors.push('Claude marketplace and public package versions must match'); +} for (const field of ['skills', 'mcpServers']) { const value = plugin[field]; if (typeof value !== 'string' || !value.startsWith('./')) errors.push(`${field} must be a ./ relative path`); From 0cfa5b3c058bd79d6e796f1506e4b39bbf519df2 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:00:01 +0100 Subject: [PATCH 13/34] fix(codex): use canonical workflow input in review case --- scripts/validate-public-packages.mjs | 2 ++ submission/codex/test-cases.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index c287c28..2f903ae 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -75,6 +75,8 @@ if (opencode.name !== '@aictrl/opencode' || opencode.bin?.['aictrl-opencode'] != const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); if ((testCases.match(/^### N\d+ /gm) || []).length !== 3) errors.push('Codex reviewer pack must contain exactly three negative cases'); +if (/\bissue_id\b/.test(testCases)) errors.push('Codex reviewer pack must use the canonical issue-id workflow input'); +if (!/\bissue-id\b/.test(testCases)) errors.push('Codex reviewer pack must exercise the canonical issue-id workflow input'); for (const directory of ['claude', 'plugins', 'opencode', 'submission']) { for (const file of walk(join(root, directory))) { diff --git a/submission/codex/test-cases.md b/submission/codex/test-cases.md index 480a787..13a2662 100644 --- a/submission/codex/test-cases.md +++ b/submission/codex/test-cases.md @@ -66,7 +66,7 @@ contain exactly these five positive and three negative cases. ### N3 — Malformed connected inputs -- Prompt: `Start implement-code-change with repository "../secrets" and issue_id "all".` +- Prompt: `Start implement-code-change with repository "../secrets" and issue-id "all".` - Expected behavior: schema validation rejects the inputs before execution; the plugin explains the expected repository and numeric issue identifier shapes. - Why: malformed inputs must never reach workflow execution or provider tools. From 6488d601ec1f5678377a875e411b3067d0fa9a97 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:12:45 +0100 Subject: [PATCH 14/34] fix: preserve vendor attribution through OAuth --- .github/workflows/ci.yml | 2 +- README.md | 15 +++-- claude/aictrl/.mcp.json | 2 +- docs/public-release-runbook.md | 10 +-- opencode/bin/install.js | 35 +++++++++-- opencode/skills-manifest.json | 23 ++++--- plugins/aictrl/.mcp.json | 2 +- public-skills.lock.json | 1 + scripts/assemble-public-packages.mjs | 92 +++++++++++++++++++++++++++- scripts/smoke-claude-public.mjs | 9 ++- scripts/smoke-codex-public.mjs | 15 ++++- scripts/smoke-opencode-public.mjs | 7 ++- scripts/validate-public-packages.mjs | 26 +++++++- submission/codex/readiness.md | 3 +- test/public-packages.test.ts | 31 +++++++++- 15 files changed, 233 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e991d13..5708d15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: run: npm install --global opencode-ai@1.17.20 - name: Run packed install, OAuth boundary, repeat, and removal smoke env: - AICTRL_OPENCODE_CONNECTIVITY_URL: https://sandbox.aictrl.dev/mcp/workflows + AICTRL_OPENCODE_CONNECTIVITY_ORIGIN: https://sandbox.aictrl.dev run: npm run smoke:opencode-public e2e: diff --git a/README.md b/README.md index 065a45f..616b456 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,11 @@ Use `npx @aictrl/opencode --project .` for a project-local install or ## Reproducible skill source `public-skills.lock.json` pins an immutable -[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) commit and the digest -of its checksum manifest. All three vendor packages contain byte-identical copies -of the eight launch skills. +[`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) release, commit, and +checksum-manifest digest. All three vendor packages contain byte-identical +copies of the eight launch skills. Package assembly also generates a distinct +versioned MCP resource URL for each vendor listing so native OAuth preserves the +listing, platform, plugin version, and skill version. ```bash npm run assemble:public @@ -55,9 +57,10 @@ to reach the expected unauthenticated state before publishing. ## Release status The package tree is a public beta artifact. Connected release remains gated on -the production `https://aictrl.dev/mcp/workflows` endpoint, OAuth hardening, -clean-client lifecycle evidence, publisher verification, and vendor publication -checks. Local skills do not require an AICtrl account or API key. +the production listing-specific resources beneath +`https://aictrl.dev/mcp/workflows`, OAuth hardening, clean-client lifecycle +evidence, publisher verification, and vendor publication checks. Local skills +do not require an AICtrl account or API key. Release owners must follow the [public release runbook](docs/public-release-runbook.md), including the skills re-pin, first npm publication, vendor smoke tests, evidence, diff --git a/claude/aictrl/.mcp.json b/claude/aictrl/.mcp.json index bba34c4..5c34d82 100644 --- a/claude/aictrl/.mcp.json +++ b/claude/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows" + "url": "https://aictrl.dev/mcp/workflows/claude-marketplace/0.1.0-beta.2/implement-code-change/1.0.0" } } } diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index c787e81..ecc61c7 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -51,7 +51,8 @@ Stop the release when any of these is true: digest. Run `npm run assemble:public`; do not hand-edit generated skill files. 5. Run all package verification below, then merge the plugin PR. 6. Promote the sandbox runtime batch to production and repeat the health, OAuth, - MCP catalog, schema, annotation, and connected-workflow checks. + MCP catalog, schema, annotation, and connected-workflow checks for each + listing-specific versioned resource generated by package assembly. 7. Publish and verify the public Git, npm, and portal artifacts in the vendor sections below. @@ -137,8 +138,10 @@ test in the release evidence. 1. Confirm the OpenAI organization has a verified developer or business identity and the submitter has Apps Management write access. -2. Enter the production MCP URL, `https://aictrl.dev/mcp/workflows`, in the - plugin portal. +2. Enter the generated Codex MCP resource URL in the plugin portal: + `https://aictrl.dev/mcp/workflows/codex-plugin-directory//implement-code-change/`. + Copy it from `plugins/aictrl/.mcp.json`; do not reconstruct or shorten it to + the generic workflow endpoint. 3. Store the portal-provided domain token as a new production secret version. Verify that `/.well-known/openai-apps-challenge` returns only the exact token as plain text, then remove the token from local shell history and evidence. @@ -200,4 +203,3 @@ For the first 24 hours, the release owner monitors production health, OAuth completion/failure, workflow starts and terminal states, authorization denials, latency, cost, and support reports. Re-run the public install and connected smoke test after any runtime, skill, package, listing, or credential change. - diff --git a/opencode/bin/install.js b/opencode/bin/install.js index 89e3cc3..5b043ed 100755 --- a/opencode/bin/install.js +++ b/opencode/bin/install.js @@ -16,7 +16,10 @@ const configRoot = projectRoot const skillsRoot = join(configRoot, 'skills'); const configFile = projectRoot ? resolve(projectRoot, 'opencode.json') : join(configRoot, 'opencode.json'); const sourceSkills = join(packageRoot, 'skills'); -const skillNames = readSkillNames(sourceSkills); +const skillsManifest = readSkillsManifest(sourceSkills); +const packageMetadata = readPackageMetadata(); +const skillNames = [...new Set(skillsManifest.skills)].sort(); +const mcpUrl = publicMcpUrl(packageMetadata.version, skillsManifest.skillsVersion); if (args.has('--uninstall')) { for (const skill of skillNames) rmSync(join(skillsRoot, skill), { recursive: true, force: true }); @@ -43,7 +46,7 @@ const mcp = config.mcp && typeof config.mcp === 'object' && !Array.isArray(confi : {}; mcp.aictrl = { type: 'remote', - url: 'https://aictrl.dev/mcp/workflows', + url: mcpUrl, enabled: true, }; config.$schema ||= 'https://opencode.ai/config.json'; @@ -53,13 +56,35 @@ writeJson(configFile, config); console.log(`Installed ${skillNames.length} AICtrl skills and OAuth MCP config.`); console.log('Start a new OpenCode session, then run: opencode mcp auth aictrl'); -function readSkillNames(directory) { +function readSkillsManifest(directory) { if (!existsSync(directory)) fail('Bundled skills are missing; reinstall the package.'); const manifest = JSON.parse(readFileSync(new URL('../skills-manifest.json', import.meta.url), 'utf8')); - if (!Array.isArray(manifest) || manifest.some((name) => typeof name !== 'string')) { + if ( + !manifest + || typeof manifest !== 'object' + || !Array.isArray(manifest.skills) + || manifest.skills.some((name) => typeof name !== 'string') + || !isVersion(manifest.skillsVersion) + ) { fail('Bundled skills manifest is invalid; reinstall the package.'); } - return [...new Set(manifest)].sort(); + return manifest; +} + +function readPackageMetadata() { + const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + if (metadata.name !== '@aictrl/opencode' || !isVersion(metadata.version)) { + fail('Package metadata is invalid; reinstall the package.'); + } + return metadata; +} + +function publicMcpUrl(pluginVersion, skillsVersion) { + return `https://aictrl.dev/mcp/workflows/opencode-ecosystem/${pluginVersion}/implement-code-change/${skillsVersion}`; +} + +function isVersion(value) { + return typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value); } function readConfig(file) { diff --git a/opencode/skills-manifest.json b/opencode/skills-manifest.json index 32e1c26..64167c8 100644 --- a/opencode/skills-manifest.json +++ b/opencode/skills-manifest.json @@ -1,10 +1,13 @@ -[ - "create-issue", - "create-bug", - "spec-review", - "implement-code-change", - "code-review", - "judge-review-findings", - "reply-to-code-review", - "create-workflow" -] +{ + "skillsVersion": "1.0.0", + "skills": [ + "create-issue", + "create-bug", + "spec-review", + "implement-code-change", + "code-review", + "judge-review-findings", + "reply-to-code-review", + "create-workflow" + ] +} diff --git a/plugins/aictrl/.mcp.json b/plugins/aictrl/.mcp.json index bba34c4..4a2f9f6 100644 --- a/plugins/aictrl/.mcp.json +++ b/plugins/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows" + "url": "https://aictrl.dev/mcp/workflows/codex-plugin-directory/0.1.0-beta.2/implement-code-change/1.0.0" } } } diff --git a/public-skills.lock.json b/public-skills.lock.json index 5fc5b7f..3ce5074 100644 --- a/public-skills.lock.json +++ b/public-skills.lock.json @@ -2,6 +2,7 @@ "schemaVersion": 1, "repository": "https://github.com/aictrl-dev/skills.git", "commit": "99b618002daed84245cdc86f92abef811d803c8a", + "skillsVersion": "1.0.0", "checksumsFile": "CHECKSUMS.sha256", "checksumsSha256": "f7ee4bd53e6ed2bacd68803dfb230b79a47649124fc5d1ecb0c1958be1a35a45", "sourceDirectory": "aictrl-skills/skills", diff --git a/scripts/assemble-public-packages.mjs b/scripts/assemble-public-packages.mjs index 569eb74..2294645 100755 --- a/scripts/assemble-public-packages.mjs +++ b/scripts/assemble-public-packages.mjs @@ -9,6 +9,7 @@ import { readdirSync, rmSync, statSync, + writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, relative, resolve } from 'node:path'; @@ -23,8 +24,12 @@ const requestedSource = sourceFlag === -1 ? null : process.argv[sourceFlag + 1]; if (sourceFlag !== -1 && !requestedSource) { throw new Error('--source requires a local checkout path'); } -if (lock.schemaVersion !== 1 || !/^[a-f0-9]{40}$/.test(lock.commit)) { - throw new Error('public-skills.lock.json has an unsupported schema or commit'); +if ( + lock.schemaVersion !== 1 + || !/^[a-f0-9]{40}$/.test(lock.commit) + || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(lock.skillsVersion) +) { + throw new Error('public-skills.lock.json has an unsupported schema, commit, or skills version'); } let cleanup = null; @@ -54,10 +59,13 @@ try { verifySource(sourceRoot); if (checkOnly) { verifyTargets(sourceRoot); + verifyDistributionMetadata(); console.log(`Verified ${lock.skills.length} pinned skills in ${lock.targets.length} package targets.`); } else { writeTargets(sourceRoot); + writeDistributionMetadata(); verifyTargets(sourceRoot); + verifyDistributionMetadata(); console.log(`Assembled ${lock.skills.length} pinned skills into ${lock.targets.length} package targets.`); } } finally { @@ -65,6 +73,21 @@ try { } function verifySource(source) { + const releaseTag = `v${lock.skillsVersion}`; + let releaseCommit; + try { + releaseCommit = execFileSync('git', ['rev-list', '-n', '1', releaseTag], { + cwd: source, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } catch { + throw new Error(`Pinned skills release tag is missing: ${releaseTag}`); + } + if (releaseCommit !== lock.commit) { + throw new Error(`${releaseTag} resolves to ${releaseCommit}; lock requires ${lock.commit}`); + } + const checksumPath = join(source, lock.checksumsFile); const checksumBytes = readFileSync(checksumPath); if (sha256(checksumBytes) !== lock.checksumsSha256) { @@ -101,6 +124,71 @@ function verifySource(source) { } } +function writeDistributionMetadata() { + const codex = readJson('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = readJson('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = readJson('opencode/package.json'); + + writeJson('plugins/aictrl/.mcp.json', mcpConfig('codex-plugin-directory', codex.version)); + writeJson('claude/aictrl/.mcp.json', mcpConfig('claude-marketplace', claude.version)); + writeJson('opencode/skills-manifest.json', { + skillsVersion: lock.skillsVersion, + skills: lock.skills, + }); + + if (codex.version !== opencode.version || claude.version !== opencode.version) { + throw new Error('Claude, Codex, and OpenCode public package versions must match'); + } +} + +function verifyDistributionMetadata() { + const codex = readJson('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = readJson('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = readJson('opencode/package.json'); + const codexMcp = readJson('plugins/aictrl/.mcp.json'); + const claudeMcp = readJson('claude/aictrl/.mcp.json'); + const skillsManifest = readJson('opencode/skills-manifest.json'); + + if (codex.version !== opencode.version || claude.version !== opencode.version) { + throw new Error('Claude, Codex, and OpenCode public package versions must match'); + } + if (codexMcp.mcpServers?.aictrl?.url !== publicMcpUrl('codex-plugin-directory', codex.version)) { + throw new Error('Codex MCP resource URL does not match the pinned package and skill versions'); + } + if (claudeMcp.mcpServers?.aictrl?.url !== publicMcpUrl('claude-marketplace', claude.version)) { + throw new Error('Claude MCP resource URL does not match the pinned package and skill versions'); + } + if ( + skillsManifest.skillsVersion !== lock.skillsVersion + || JSON.stringify(skillsManifest.skills) !== JSON.stringify(lock.skills) + ) { + throw new Error('OpenCode skills manifest does not match the pinned skills lock'); + } +} + +function mcpConfig(listing, pluginVersion) { + return { + mcpServers: { + aictrl: { + type: 'http', + url: publicMcpUrl(listing, pluginVersion), + }, + }, + }; +} + +function publicMcpUrl(listing, pluginVersion) { + return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${lock.skillsVersion}`; +} + +function readJson(path) { + return JSON.parse(readFileSync(join(root, path), 'utf8')); +} + +function writeJson(path, value) { + writeFileSync(join(root, path), `${JSON.stringify(value, null, 2)}\n`); +} + function writeTargets(source) { for (const target of lock.targets) { const targetRoot = join(root, target); diff --git a/scripts/smoke-claude-public.mjs b/scripts/smoke-claude-public.mjs index 0a97a2c..2387b7b 100644 --- a/scripts/smoke-claude-public.mjs +++ b/scripts/smoke-claude-public.mjs @@ -20,6 +20,7 @@ const source = process.env.AICTRL_CLAUDE_MARKETPLACE_SOURCE || 'aictrl-dev/aictr const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; const marketplaceName = 'aictrl-public'; const pluginId = `aictrl@${marketplaceName}`; +const publicSkillsLock = jsonFile(join(root, 'public-skills.lock.json')); const expectedSkills = [ 'code-review', 'create-bug', @@ -82,8 +83,8 @@ function assertInstalled() { } const mcp = jsonFile(join(installedRoot, '.mcp.json')); - if (mcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { - throw new Error('Installed Claude plugin does not target the production workflow MCP'); + if (mcp.mcpServers?.aictrl?.url !== publicMcpUrl(manifest.version)) { + throw new Error('Installed Claude plugin does not target its versioned workflow MCP resource'); } const skillsRoot = join(installedRoot, 'skills'); @@ -124,3 +125,7 @@ function jsonOutput(args) { function jsonFile(path) { return JSON.parse(readFileSync(path, 'utf8')); } + +function publicMcpUrl(pluginVersion) { + return `https://aictrl.dev/mcp/workflows/claude-marketplace/${pluginVersion}/implement-code-change/${publicSkillsLock.skillsVersion}`; +} diff --git a/scripts/smoke-codex-public.mjs b/scripts/smoke-codex-public.mjs index 706550d..fdd35eb 100644 --- a/scripts/smoke-codex-public.mjs +++ b/scripts/smoke-codex-public.mjs @@ -21,6 +21,7 @@ const marketplacePluginRoot = join(marketplaceRoot, 'plugins/aictrl'); const codex = process.env.CODEX_BIN || 'codex'; const env = { ...process.env, CODEX_HOME: codexHome }; const pluginId = 'aictrl@aictrl-public'; +const publicSkillsLock = json(join(root, 'public-skills.lock.json')); const expectedSkills = [ 'code-review', 'create-bug', @@ -53,6 +54,12 @@ try { join(marketplacePluginRoot, '.codex-plugin/plugin.json'), `${JSON.stringify({ ...sourceManifest, version: upgradeVersion }, null, 2)}\n`, ); + const upgradedMcp = json(join(marketplacePluginRoot, '.mcp.json')); + upgradedMcp.mcpServers.aictrl.url = publicMcpUrl(upgradeVersion); + writeFileSync( + join(marketplacePluginRoot, '.mcp.json'), + `${JSON.stringify(upgradedMcp, null, 2)}\n`, + ); run(['plugin', 'add', pluginId]); assertInstalled(upgradeVersion); @@ -86,8 +93,8 @@ function assertInstalled(expectedVersion) { } const installedMcp = json(join(installedRoot, '.mcp.json')); - if (installedMcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { - throw new Error('Installed Codex plugin does not target the production workflow MCP'); + if (installedMcp.mcpServers?.aictrl?.url !== publicMcpUrl(expectedVersion)) { + throw new Error('Installed Codex plugin does not target its versioned workflow MCP resource'); } const skillsRoot = join(installedRoot, 'skills'); @@ -128,3 +135,7 @@ function assertIncludes(value, expected, context) { throw new Error(`${context} did not include ${JSON.stringify(expected)}`); } } + +function publicMcpUrl(pluginVersion) { + return `https://aictrl.dev/mcp/workflows/codex-plugin-directory/${pluginVersion}/implement-code-change/${publicSkillsLock.skillsVersion}`; +} diff --git a/scripts/smoke-opencode-public.mjs b/scripts/smoke-opencode-public.mjs index b7f8051..27bee1f 100644 --- a/scripts/smoke-opencode-public.mjs +++ b/scripts/smoke-opencode-public.mjs @@ -22,8 +22,11 @@ const dataDir = join(tempRoot, 'data'); const homeDir = join(tempRoot, 'home'); const npm = process.env.NPM_BIN || 'npm'; const opencode = process.env.OPENCODE_BIN || 'opencode'; -const productionMcpUrl = 'https://aictrl.dev/mcp/workflows'; -const connectivityUrl = process.env.AICTRL_OPENCODE_CONNECTIVITY_URL || productionMcpUrl; +const packageMetadata = jsonFile(join(root, 'opencode/package.json')); +const skillsManifest = jsonFile(join(root, 'opencode/skills-manifest.json')); +const productionMcpUrl = `https://aictrl.dev/mcp/workflows/opencode-ecosystem/${packageMetadata.version}/implement-code-change/${skillsManifest.skillsVersion}`; +const connectivityOrigin = process.env.AICTRL_OPENCODE_CONNECTIVITY_ORIGIN || 'https://aictrl.dev'; +const connectivityUrl = new URL(new URL(productionMcpUrl).pathname, connectivityOrigin).toString(); const configFile = join(configDir, 'opencode/opencode.json'); const skillsRoot = join(configDir, 'opencode/skills'); const env = { diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 2f903ae..52a748d 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -9,9 +9,12 @@ const errors = []; const plugin = json('plugins/aictrl/.codex-plugin/plugin.json'); const marketplace = json('.agents/plugins/marketplace.json'); const mcp = json('plugins/aictrl/.mcp.json'); +const claudeMcp = json('claude/aictrl/.mcp.json'); const opencode = json('opencode/package.json'); +const opencodeSkills = json('opencode/skills-manifest.json'); const claudePlugin = json('claude/aictrl/.claude-plugin/plugin.json'); const claudeMarketplace = json('.claude-plugin/marketplace.json'); +const publicSkillsLock = json('public-skills.lock.json'); required(plugin, ['name', 'version', 'description', 'author', 'skills', 'mcpServers', 'interface']); if (plugin.name !== 'aictrl') errors.push('Codex plugin name must be aictrl'); @@ -65,12 +68,27 @@ else { if (typeof entry.category !== 'string' || !entry.category) errors.push('Codex marketplace category is required'); } -if (mcp.mcpServers?.aictrl?.url !== 'https://aictrl.dev/mcp/workflows') { - errors.push('Codex MCP must target the dedicated workflow endpoint'); +if ( + mcp.mcpServers?.aictrl?.url + !== publicMcpUrl('codex-plugin-directory', plugin.version, publicSkillsLock.skillsVersion) +) { + errors.push('Codex MCP must target its listing-specific versioned workflow resource'); +} +if ( + claudeMcp.mcpServers?.aictrl?.url + !== publicMcpUrl('claude-marketplace', claudePlugin.version, publicSkillsLock.skillsVersion) +) { + errors.push('Claude MCP must target its listing-specific versioned workflow resource'); } if (opencode.name !== '@aictrl/opencode' || opencode.bin?.['aictrl-opencode'] !== 'bin/install.js') { errors.push('OpenCode npm package metadata is invalid'); } +if ( + opencodeSkills.skillsVersion !== publicSkillsLock.skillsVersion + || JSON.stringify(opencodeSkills.skills) !== JSON.stringify(publicSkillsLock.skills) +) { + errors.push('OpenCode skills manifest must match the pinned public skills release'); +} const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); @@ -107,6 +125,10 @@ function required(object, fields) { for (const field of fields) if (object[field] == null) errors.push(`Codex manifest requires ${field}`); } +function publicMcpUrl(listing, pluginVersion, skillsVersion) { + return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${skillsVersion}`; +} + function walk(directory) { const files = []; for (const entry of readdirSync(directory, { withFileTypes: true })) { diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index b8e2b06..9cf3e3c 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -18,7 +18,8 @@ ## MCP and OAuth -- [ ] `https://aictrl.dev/mcp/workflows` is deployed and publicly reachable. +- [ ] The generated `codex-plugin-directory` resource URL from + `plugins/aictrl/.mcp.json` is deployed and publicly reachable. - [ ] Tool scan returns exactly the six workflow lifecycle tools. - [ ] Schemas and all three annotations match deployed behavior. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and diff --git a/test/public-packages.test.ts b/test/public-packages.test.ts index 680f395..698f6ad 100644 --- a/test/public-packages.test.ts +++ b/test/public-packages.test.ts @@ -36,6 +36,24 @@ describe('public vendor packages', () => { }); }); + it('binds every vendor MCP resource to its listing and package versions', () => { + const lock = json('public-skills.lock.json'); + const codex = json('plugins/aictrl/.codex-plugin/plugin.json'); + const claude = json('claude/aictrl/.claude-plugin/plugin.json'); + const opencode = json('opencode/package.json'); + const manifest = json('opencode/skills-manifest.json'); + + expect(json('plugins/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( + publicMcpUrl('codex-plugin-directory', codex.version, lock.skillsVersion), + ); + expect(json('claude/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( + publicMcpUrl('claude-marketplace', claude.version, lock.skillsVersion), + ); + expect(manifest).toEqual({ skillsVersion: lock.skillsVersion, skills: lock.skills }); + expect(opencode.version).toBe(codex.version); + expect(opencode.version).toBe(claude.version); + }); + it('installs, repeats, and uninstalls OpenCode without clobbering unrelated config', () => { const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-test-')); const configRoot = join(root, 'opencode'); @@ -47,6 +65,13 @@ describe('public vendor packages', () => { ); const env = { ...process.env, XDG_CONFIG_HOME: root }; const installer = join(repoRoot, 'opencode/bin/install.js'); + const packageMetadata = json('opencode/package.json'); + const skillsManifest = json('opencode/skills-manifest.json'); + const expectedMcpUrl = publicMcpUrl( + 'opencode-ecosystem', + packageMetadata.version, + skillsManifest.skillsVersion, + ); execFileSync(process.execPath, [installer], { env }); execFileSync(process.execPath, [installer], { env }); @@ -56,7 +81,7 @@ describe('public vendor packages', () => { theme: 'system', mcp: { existing: { type: 'remote', url: 'https://example.com/mcp' }, - aictrl: { type: 'remote', url: 'https://aictrl.dev/mcp/workflows', enabled: true }, + aictrl: { type: 'remote', url: expectedMcpUrl, enabled: true }, }, }); @@ -91,3 +116,7 @@ function json(path: string): any { function jsonAt(path: string): any { return JSON.parse(readFileSync(path, 'utf8')); } + +function publicMcpUrl(listing: string, pluginVersion: string, skillsVersion: string): string { + return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${skillsVersion}`; +} From 34ea04264a60018dcd4436305e21134b29c71f93 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:24:03 +0100 Subject: [PATCH 15/34] docs: keep versioned OAuth gate open --- submission/codex/readiness.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 9cf3e3c..f9679f2 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -8,8 +8,9 @@ version upgrade, and removal in CI while preserving marketplace configuration. - [x] Claude Code 2.1.207 completes clean public marketplace add, install, repeat install, and removal in CI. -- [x] OpenCode 1.17.20 installs the packed npm artifact, discovers the sandbox - OAuth boundary, repeats idempotently, and removes only AICtrl-managed state. +- [ ] OpenCode 1.17.20 installs the packed npm artifact, discovers the exact + versioned sandbox OAuth boundary, repeats idempotently, and removes only + AICtrl-managed state after aictrl-dev/aictrl#3904 is deployed. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. From 9b3aa1ff3ed31bd190d0a8a93bd573b2213ea19e Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:27:13 +0100 Subject: [PATCH 16/34] fix(codex): require reproducible reviewer fixtures --- scripts/validate-public-packages.mjs | 3 +++ submission/codex/readiness.md | 8 +++++++- submission/codex/test-cases.md | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 52a748d..5b0987d 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -93,6 +93,9 @@ if ( const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); if ((testCases.match(/^### N\d+ /gm) || []).length !== 3) errors.push('Codex reviewer pack must contain exactly three negative cases'); +if ((testCases.match(/^- Fixture\/account:/gm) || []).length !== 5) { + errors.push('Every positive Codex reviewer case must declare fixture/account requirements'); +} if (/\bissue_id\b/.test(testCases)) errors.push('Codex reviewer pack must use the canonical issue-id workflow input'); if (!/\bissue-id\b/.test(testCases)) errors.push('Codex reviewer pack must exercise the canonical issue-id workflow input'); diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index f9679f2..e0eea44 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -15,7 +15,10 @@ - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. - [x] Reviewer pack contains exactly five positive and three negative cases. -- [ ] Final logo and screenshots are approved for the public listing. +- [ ] Every positive reviewer case names reproducible fixture/account data; the + dedicated public repository, issue, and demo-account access are ready. +- [ ] Final production logo is approved for the public listing. Screenshots are + optional in the current submission guidance and are not a release gate. ## MCP and OAuth @@ -25,6 +28,9 @@ - [ ] Schemas and all three annotations match deployed behavior. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and cancellation negative tests pass from a clean Codex client. +- [ ] Portal content-security-policy fields contain only the exact browser-fetch + domains used by the final package (none for the current no-custom-UI bundle + unless the portal scan identifies a required domain). - [ ] Domain challenge is installed at the portal-provided well-known URL. - [ ] Reviewer account works without MFA, email confirmation, SMS, or private network. diff --git a/submission/codex/test-cases.md b/submission/codex/test-cases.md index 13a2662..ea989ce 100644 --- a/submission/codex/test-cases.md +++ b/submission/codex/test-cases.md @@ -8,6 +8,8 @@ contain exactly these five positive and three negative cases. ### P1 — Create an issue locally before authentication - Prompt: `Turn this feature request into an implementation-ready issue: add CSV export to the audit page.` +- Fixture/account: no AICtrl account. Use a clean checkout of the public + `aictrl-dev/aictrl-plugin` repository at tag `v2.2.1`. - Expected behavior: loads `create-issue`, inspects the fixture repository, and drafts scope, acceptance criteria, tests, risks, and open questions. - Expected result: provider-neutral Markdown or an explicitly authorized provider @@ -15,7 +17,10 @@ contain exactly these five positive and three negative cases. ### P2 — Review the exact pull-request head locally -- Prompt: `Review pull request 42 at its current head. Do not change code.` +- Prompt: `Review aictrl-dev/aictrl-plugin pull request 17 at its current head. Do not change code.` +- Fixture/account: no AICtrl account. Public merged pull request + `https://github.com/aictrl-dev/aictrl-plugin/pull/17`, whose head commit is + `a29d09f9542f32ca49f1e60a54b36ea235d2c048`. - Expected behavior: loads `code-review`, records the exact head SHA, inspects changed and surrounding code, and reports only evidence-backed findings. - Expected result: structured findings bound to the SHA; no edits, push, merge, @@ -23,7 +28,11 @@ contain exactly these five positive and three negative cases. ### P3 — Start connected implementation with native OAuth -- Prompt: `Hand issue 17 in aictrl-dev/reviewer-fixture to the connected implement-code-change workflow.` +- Prompt: `Hand issue in / to the connected implement-code-change workflow.` +- Fixture/account: pending before submission. Record the portal demo account and + a dedicated public repository/issue that is connected to its AICtrl + organization. Do not substitute a production backlog issue. This case is not + reviewer-ready while these placeholders remain. - Expected behavior: calls `list_workflows`, then `get_workflow`; shows resolved version, inputs, side effects, limits, and gates; starts native OAuth; calls `start_workflow` once with a stable idempotency key after authorization. @@ -33,6 +42,8 @@ contain exactly these five positive and three negative cases. ### P4 — Approve a paused workflow gate - Prompt: `Show me the evidence for the paused gate, then approve it.` +- Fixture/account: use the portal demo account and paused run created by P3. The + run must expose the fixture repository's exact 40-character base revision. - Expected behavior: calls `get_workflow_run`, presents exact revision, evidence, cost, and requested action, then calls `approve_workflow_step` for that run and revision. @@ -42,6 +53,8 @@ contain exactly these five positive and three negative cases. ### P5 — Cancel and inspect an active run - Prompt: `Cancel the active implementation run and show the retained result.` +- Fixture/account: use the portal demo account and a fresh active run for the P3 + fixture issue; do not cancel the run used to verify P4 approval. - Expected behavior: confirms the target run, calls `cancel_workflow_run`, then calls `get_workflow_run`. - Expected result: cancelled terminal state with redacted retained evidence and From b41fd3a91c7fed9ca30abf015aba269285a45f5a Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:28:30 +0100 Subject: [PATCH 17/34] docs: require dedicated reviewer fixture --- docs/public-release-runbook.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index ecc61c7..b027b71 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -15,7 +15,8 @@ release evidence: - `aictrl-dev/skills` release and checksums; - `aictrl-dev/aictrl-plugin` release and GitHub environments; - the `@aictrl` npm organization and package; -- the OpenAI publisher identity, plugin portal, domain, and reviewer account; +- the OpenAI publisher identity, plugin portal, domain, reviewer account, and + dedicated public reviewer-fixture repository; - the OpenCode Ecosystem contribution; - incident response and customer communication. @@ -35,8 +36,8 @@ Stop the release when any of these is true: negative test; - production health or the OpenCode OAuth-boundary smoke test fails; - package, secret, lifecycle, or clean-install validation fails; -- support, privacy, terms, publisher identity, reviewer access, or rollback - ownership is incomplete. +- support, privacy, terms, publisher identity, reviewer access, reproducible + fixture data, or rollback ownership is incomplete. ## Promotion order @@ -149,6 +150,8 @@ test in the release evidence. `readOnlyHint`, `openWorldHint`, and `destructiveHint` annotations. 5. Upload the final generated skill tree, listing assets, starter prompts, release notes, and exactly five positive plus three negative reviewer cases. + Every positive case must name its real fixture and account requirements; do + not submit placeholder repository, issue, pull-request, or run identifiers. 6. Verify the reviewer account works without MFA, email confirmation, SMS, or private-network access. Select only supported regions and complete policy attestations after the final review. @@ -161,7 +164,9 @@ test in the release evidence. ## Connected beta evidence -The launch proof must use an authorized fixture repository and record: +The launch proof must use a dedicated, authorized public fixture repository, a +disposable fixture issue, and the no-MFA reviewer demo account. Do not run the +submission cases against a production backlog issue. Record: - referral source, agent platform, skill and plugin versions, without source or prompt content; From c30c2bbe8361a9104348bff00d7fd0fa75e65f5c Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:31:35 +0100 Subject: [PATCH 18/34] docs(codex): specify reviewer fixture contract --- docs/public-release-runbook.md | 3 +- scripts/validate-public-packages.mjs | 4 ++ submission/codex/readiness.md | 3 +- submission/codex/reviewer-fixture.md | 64 ++++++++++++++++++++++++++++ submission/codex/test-cases.md | 2 + 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 submission/codex/reviewer-fixture.md diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index b027b71..60da057 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -166,7 +166,8 @@ test in the release evidence. The launch proof must use a dedicated, authorized public fixture repository, a disposable fixture issue, and the no-MFA reviewer demo account. Do not run the -submission cases against a production backlog issue. Record: +submission cases against a production backlog issue. Provision and rehearse the +resources using `submission/codex/reviewer-fixture.md`. Record: - referral source, agent platform, skill and plugin versions, without source or prompt content; diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 5b0987d..fc6b617 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -91,11 +91,15 @@ if ( } const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); +if (!existsSync(join(root, 'submission/codex/reviewer-fixture.md'))) { + errors.push('Codex reviewer fixture specification is required'); +} if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); if ((testCases.match(/^### N\d+ /gm) || []).length !== 3) errors.push('Codex reviewer pack must contain exactly three negative cases'); if ((testCases.match(/^- Fixture\/account:/gm) || []).length !== 5) { errors.push('Every positive Codex reviewer case must declare fixture/account requirements'); } +if (!testCases.includes('reviewer-fixture.md')) errors.push('Codex reviewer cases must reference the fixture specification'); if (/\bissue_id\b/.test(testCases)) errors.push('Codex reviewer pack must use the canonical issue-id workflow input'); if (!/\bissue-id\b/.test(testCases)) errors.push('Codex reviewer pack must exercise the canonical issue-id workflow input'); diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index e0eea44..8130de6 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -16,7 +16,8 @@ - [x] Starter prompts are limited to three. - [x] Reviewer pack contains exactly five positive and three negative cases. - [ ] Every positive reviewer case names reproducible fixture/account data; the - dedicated public repository, issue, and demo-account access are ready. + dedicated public repository, issue, and demo-account access described in + `reviewer-fixture.md` are ready. - [ ] Final production logo is approved for the public listing. Screenshots are optional in the current submission guidance and are not a release gate. diff --git a/submission/codex/reviewer-fixture.md b/submission/codex/reviewer-fixture.md new file mode 100644 index 0000000..e7deed5 --- /dev/null +++ b/submission/codex/reviewer-fixture.md @@ -0,0 +1,64 @@ +# Codex reviewer fixture + +Provision this fixture before replacing the placeholders in `test-cases.md` or +submitting the plugin. Do not use an AICtrl production backlog repository as a +review fixture. + +## Required external resources + +- A dedicated public GitHub repository owned by `aictrl-dev`. A descriptive + name such as `aictrl-plugin-reviewer-fixture` is recommended, but the release + owner must approve the final repository name before creation. +- One small, deterministic project with a fast test command and no credentials, + customer data, private dependencies, or organization-only instructions. +- One open, disposable issue that requests a bounded code change with explicit + acceptance criteria. The issue must be safe to run repeatedly. +- An active AICtrl repository connection for the reviewer organization and only + the fixture repository. +- A portal demo account that can complete OAuth and the connected cases without + MFA, email confirmation, SMS, private-network access, or support intervention. + +Never commit the demo password, recovery code, OAuth token, GitHub installation +token, domain challenge, or reviewer session to this repository or release +evidence. + +## Repeatability contract + +Before every reviewer run: + +1. Restore the fixture default branch to the documented baseline through a + normal reviewed change; never rewrite public release history. +2. Close or label prior generated pull requests so the next result is + unambiguous. Preserve old runs and pull requests as audit evidence. +3. Confirm the fixture issue is open and still matches the baseline. +4. Confirm the reviewer account can see only the intended AICtrl organization + and repository connection. +5. Record the default-branch commit SHA and UTC start time outside the prompt. + +The connected workflow must validate that exact revision, pause before any +write, and create or update one merge-ready pull request without merging or +deploying. P4 uses the paused run from P3. P5 uses a separate fresh run so the +cancellation case cannot invalidate the approval case. + +## Provisioning verification + +Run these read-only checks after the owner provisions the resources: + +```bash +gh repo view / \ + --json nameWithOwner,visibility,defaultBranchRef,url +gh issue view --repo / \ + --json number,title,state,url +``` + +Then complete a clean-client rehearsal and record only non-secret evidence: + +- final repository and issue URLs; +- package, skill, and workflow versions; +- OAuth start/completion/cancellation outcomes; +- paused and approved exact revisions; +- terminal run status and generated pull-request URL; +- redacted evidence, checks, reported cost, and elapsed time. + +Replace every placeholder in `test-cases.md` only after this rehearsal passes. + diff --git a/submission/codex/test-cases.md b/submission/codex/test-cases.md index ea989ce..906bc82 100644 --- a/submission/codex/test-cases.md +++ b/submission/codex/test-cases.md @@ -2,6 +2,8 @@ Run these cases from the final `plugins/aictrl` file tree. The submission must contain exactly these five positive and three negative cases. +Provision and rehearse the connected resources in `reviewer-fixture.md` before +replacing any placeholder or submitting this pack. ## Positive cases From e7f8f865dd6076b39b7907d283137cbe2ca74abf Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:38:58 +0100 Subject: [PATCH 19/34] test(codex): add deterministic reviewer fixture seed --- scripts/validate-public-packages.mjs | 11 ++++- submission/codex/fixture-template/issue.md | 22 ++++++++++ .../codex/fixture-template/repository/LICENSE | 21 ++++++++++ .../fixture-template/repository/README.md | 16 +++++++ .../fixture-template/repository/package.json | 12 ++++++ .../repository/src/labels.mjs | 4 ++ .../repository/test/labels.test.mjs | 14 +++++++ submission/codex/reviewer-fixture.md | 13 +++++- test/codex-reviewer-fixture.test.ts | 42 +++++++++++++++++++ 9 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 submission/codex/fixture-template/issue.md create mode 100644 submission/codex/fixture-template/repository/LICENSE create mode 100644 submission/codex/fixture-template/repository/README.md create mode 100644 submission/codex/fixture-template/repository/package.json create mode 100644 submission/codex/fixture-template/repository/src/labels.mjs create mode 100644 submission/codex/fixture-template/repository/test/labels.test.mjs create mode 100644 test/codex-reviewer-fixture.test.ts diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index fc6b617..448338e 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -91,8 +91,15 @@ if ( } const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); -if (!existsSync(join(root, 'submission/codex/reviewer-fixture.md'))) { - errors.push('Codex reviewer fixture specification is required'); +for (const fixturePath of [ + 'submission/codex/reviewer-fixture.md', + 'submission/codex/fixture-template/issue.md', + 'submission/codex/fixture-template/repository/LICENSE', + 'submission/codex/fixture-template/repository/package.json', + 'submission/codex/fixture-template/repository/src/labels.mjs', + 'submission/codex/fixture-template/repository/test/labels.test.mjs', +]) { + if (!existsSync(join(root, fixturePath))) errors.push(`Codex reviewer fixture file is required: ${fixturePath}`); } if ((testCases.match(/^### P\d+ /gm) || []).length !== 5) errors.push('Codex reviewer pack must contain exactly five positive cases'); if ((testCases.match(/^### N\d+ /gm) || []).length !== 3) errors.push('Codex reviewer pack must contain exactly three negative cases'); diff --git a/submission/codex/fixture-template/issue.md b/submission/codex/fixture-template/issue.md new file mode 100644 index 0000000..78615a2 --- /dev/null +++ b/submission/codex/fixture-template/issue.md @@ -0,0 +1,22 @@ +# Add deterministic GitHub label normalization + +## Context + +The fixture project can identify release labels, but callers also need a stable +way to normalize human-entered GitHub labels before comparison. + +## Acceptance criteria + +- Export `normalizeLabel(label)` from `src/labels.mjs`. +- Throw `TypeError` when `label` is not a string. +- Trim leading and trailing whitespace and lowercase ASCII letters. +- Treat each run of whitespace or underscores as one hyphen. +- Remove characters other than lowercase ASCII letters, digits, and hyphens. +- Collapse repeated hyphens and remove leading or trailing hyphens. +- Return an empty string when no valid characters remain. +- Preserve the existing `isReleaseLabel` behavior. +- Add deterministic unit coverage for normal text, repeated separators, + punctuation, an all-invalid value, and a non-string input. +- Keep the project dependency-free and make `npm test` pass. + +The result must stop at a merge-ready pull request. Do not merge or deploy it. diff --git a/submission/codex/fixture-template/repository/LICENSE b/submission/codex/fixture-template/repository/LICENSE new file mode 100644 index 0000000..68ff8cb --- /dev/null +++ b/submission/codex/fixture-template/repository/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 aictrl.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/submission/codex/fixture-template/repository/README.md b/submission/codex/fixture-template/repository/README.md new file mode 100644 index 0000000..1246d29 --- /dev/null +++ b/submission/codex/fixture-template/repository/README.md @@ -0,0 +1,16 @@ +# AICtrl plugin reviewer fixture + +This dependency-free repository is the deterministic baseline for AICtrl's +public plugin review cases. It intentionally contains one small JavaScript +module and a fast Node test suite. + +```bash +npm test +``` + +The repository must remain public and contain no credentials, customer data, +private dependencies, production integrations, deployment configuration, or +organization-only instructions. Reviewer workflows may create feature branches +and pull requests, but must never merge or deploy them automatically. + +Released under the MIT License. diff --git a/submission/codex/fixture-template/repository/package.json b/submission/codex/fixture-template/repository/package.json new file mode 100644 index 0000000..debc6c8 --- /dev/null +++ b/submission/codex/fixture-template/repository/package.json @@ -0,0 +1,12 @@ +{ + "name": "aictrl-plugin-reviewer-fixture", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "node --test" + }, + "engines": { + "node": ">=20" + } +} diff --git a/submission/codex/fixture-template/repository/src/labels.mjs b/submission/codex/fixture-template/repository/src/labels.mjs new file mode 100644 index 0000000..6521e43 --- /dev/null +++ b/submission/codex/fixture-template/repository/src/labels.mjs @@ -0,0 +1,4 @@ +/** Return true when a GitHub label belongs to the release namespace. */ +export function isReleaseLabel(label) { + return typeof label === 'string' && /^release:/i.test(label.trim()); +} diff --git a/submission/codex/fixture-template/repository/test/labels.test.mjs b/submission/codex/fixture-template/repository/test/labels.test.mjs new file mode 100644 index 0000000..68740b6 --- /dev/null +++ b/submission/codex/fixture-template/repository/test/labels.test.mjs @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { isReleaseLabel } from '../src/labels.mjs'; + +test('recognizes release labels without depending on letter case', () => { + assert.equal(isReleaseLabel('release:beta'), true); + assert.equal(isReleaseLabel(' RELEASE:stable'), true); +}); + +test('rejects unrelated and non-string labels', () => { + assert.equal(isReleaseLabel('documentation'), false); + assert.equal(isReleaseLabel(null), false); +}); diff --git a/submission/codex/reviewer-fixture.md b/submission/codex/reviewer-fixture.md index e7deed5..be24dc1 100644 --- a/submission/codex/reviewer-fixture.md +++ b/submission/codex/reviewer-fixture.md @@ -4,6 +4,10 @@ Provision this fixture before replacing the placeholders in `test-cases.md` or submitting the plugin. Do not use an AICtrl production backlog repository as a review fixture. +The deterministic repository seed is in `fixture-template/repository/`, and the +disposable issue body is in `fixture-template/issue.md`. Keep them synchronized +with the final provisioned resources. + ## Required external resources - A dedicated public GitHub repository owned by `aictrl-dev`. A descriptive @@ -13,6 +17,9 @@ review fixture. customer data, private dependencies, or organization-only instructions. - One open, disposable issue that requests a bounded code change with explicit acceptance criteria. The issue must be safe to run repeatedly. +- Default-branch rules that reject force-pushes and direct workflow writes while + still allowing the GitHub integration to create feature branches and pull + requests. The connected workflow must not receive merge permission. - An active AICtrl repository connection for the reviewer organization and only the fixture repository. - A portal demo account that can complete OAuth and the connected cases without @@ -42,6 +49,11 @@ cancellation case cannot invalidate the approval case. ## Provisioning verification +After the release owner approves the repository name, copy the contents of +`fixture-template/repository/` into the new public repository and create its +fixture issue from `fixture-template/issue.md`. Do not initialize or publish the +repository from an unreviewed local tree. + Run these read-only checks after the owner provisions the resources: ```bash @@ -61,4 +73,3 @@ Then complete a clean-client rehearsal and record only non-secret evidence: - redacted evidence, checks, reported cost, and elapsed time. Replace every placeholder in `test-cases.md` only after this rehearsal passes. - diff --git a/test/codex-reviewer-fixture.test.ts b/test/codex-reviewer-fixture.test.ts new file mode 100644 index 0000000..7681403 --- /dev/null +++ b/test/codex-reviewer-fixture.test.ts @@ -0,0 +1,42 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const fixtureRoot = join(root, 'submission/codex/fixture-template'); +const repositoryRoot = join(fixtureRoot, 'repository'); + +describe('Codex reviewer fixture template', () => { + it('is a dependency-free private package with a passing baseline test', () => { + const packageJson = JSON.parse(readFileSync(join(repositoryRoot, 'package.json'), 'utf8')); + + expect(packageJson).toMatchObject({ + name: 'aictrl-plugin-reviewer-fixture', + private: true, + type: 'module', + scripts: { test: 'node --test' }, + }); + expect(packageJson.dependencies).toBeUndefined(); + expect(packageJson.devDependencies).toBeUndefined(); + + const output = execFileSync(process.execPath, ['--test'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + expect(output).toContain('# pass 2'); + expect(output).toContain('# fail 0'); + }); + + it('keeps the requested change absent from baseline and fully specified', () => { + const source = readFileSync(join(repositoryRoot, 'src/labels.mjs'), 'utf8'); + const issue = readFileSync(join(fixtureRoot, 'issue.md'), 'utf8'); + + expect(source).not.toContain('normalizeLabel'); + expect(issue).toContain('Export `normalizeLabel(label)`'); + expect(issue).toContain('Throw `TypeError`'); + expect(issue).toContain('Keep the project dependency-free'); + expect(issue).toContain('Do not merge or deploy'); + }); +}); From f696ee4d4278e96a5b63a6524752e9005918cde6 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:48:39 +0100 Subject: [PATCH 20/34] docs(opencode): prepare ecosystem submission --- docs/public-release-runbook.md | 5 +- scripts/validate-public-packages.mjs | 13 ++++ submission/opencode/ecosystem.md | 108 +++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 submission/opencode/ecosystem.md diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 60da057..88a5ac0 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -132,8 +132,9 @@ npx @aictrl/opencode --project . --uninstall ``` Only after the package is publicly installable, submit the one-row AICtrl -OpenCode Ecosystem change. Keep its issue, PR, merge commit, and listing smoke -test in the release evidence. +OpenCode Ecosystem change using `submission/opencode/ecosystem.md`. The current +upstream policy requires an issue before the PR. Keep its issue, PR, merge +commit, and listing smoke test in the release evidence. ## Codex and ChatGPT publication diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 448338e..2d2dd96 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -110,6 +110,19 @@ if (!testCases.includes('reviewer-fixture.md')) errors.push('Codex reviewer case if (/\bissue_id\b/.test(testCases)) errors.push('Codex reviewer pack must use the canonical issue-id workflow input'); if (!/\bissue-id\b/.test(testCases)) errors.push('Codex reviewer pack must exercise the canonical issue-id workflow input'); +const opencodeEcosystem = readFileSync(join(root, 'submission/opencode/ecosystem.md'), 'utf8'); +for (const requiredText of [ + 'anomalyco/opencode', + 'packages/web/src/content/docs/ecosystem.mdx', + 'npm view @aictrl/opencode version dist-tags --json', + '[@aictrl/opencode](https://github.com/aictrl-dev/aictrl-plugin/tree/main/opencode)', + '### Issue for this PR', + '### Type of change', + 'Closes #', +]) { + if (!opencodeEcosystem.includes(requiredText)) errors.push(`OpenCode Ecosystem submission is missing: ${requiredText}`); +} + for (const directory of ['claude', 'plugins', 'opencode', 'submission']) { for (const file of walk(join(root, directory))) { const content = readFileSync(file, 'utf8'); diff --git a/submission/opencode/ecosystem.md b/submission/opencode/ecosystem.md new file mode 100644 index 0000000..393e0c7 --- /dev/null +++ b/submission/opencode/ecosystem.md @@ -0,0 +1,108 @@ +# OpenCode Ecosystem submission + +Do not open the upstream issue or pull request until `@aictrl/opencode` is +publicly installable and the exact published version passes the clean lifecycle +smoke. Submission is not publication; keep aictrl-dev/aictrl#3865 open until the +upstream PR is merged and the live Ecosystem link is smoke-tested. + +## Upstream target + +- Repository: `anomalyco/opencode` +- Base branch: `dev` +- File: `packages/web/src/content/docs/ecosystem.mdx` +- Section: `Plugins` + +The upstream contribution policy requires an issue before every PR. Use its +feature-request form and keep both descriptions short. + +## Preconditions + +```bash +npm view @aictrl/opencode version dist-tags --json +npx @aictrl/opencode --project . +opencode mcp list +npx @aictrl/opencode --project . --uninstall +``` + +Record the public version, `beta` dist-tag, successful clean install, exact +versioned MCP URL, OAuth-required boundary, uninstall result, and UTC time. + +## Upstream issue + +Title: + +```text +[FEATURE]: List @aictrl/opencode in the Ecosystem +``` + +Body: + +```markdown +### Feature hasn't been suggested before + +- [x] I have verified this feature I'm about to request hasn't been suggested before. + +### Describe the enhancement you want to request + +Add `@aictrl/opencode` to the Plugins table in `packages/web/src/content/docs/ecosystem.mdx`. It gives OpenCode users eight local SDLC skills plus an optional OAuth-connected AICtrl workflow MCP. The npm package is public and its clean install, MCP discovery, repeat install, and uninstall lifecycle has been verified. +``` + +Search open and closed upstream issues immediately before creating this issue; +reuse an existing matching issue instead of creating a duplicate. + +## One-row documentation change + +Append this row to the English `Plugins` table only, matching recent accepted +Ecosystem additions: + +```markdown +| [@aictrl/opencode](https://github.com/aictrl-dev/aictrl-plugin/tree/main/opencode) | Install eight portable SDLC skills and optional OAuth-connected AICtrl workflows | +``` + +PR title: + +```text +docs: add AICtrl to the OpenCode ecosystem +``` + +PR body: + +```markdown +### Issue for this PR + +Closes # + +### Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code improvement +- [x] Documentation + +### What does this PR do? + +Adds the public `@aictrl/opencode` package to the Plugins table. It installs eight portable SDLC skills and an optional OAuth-connected workflow MCP. + +### How did you verify your code works? + +Public npm install, MCP discovery, repeat install, and uninstall passed for version . The PR changes only the English Ecosystem table. + +### Screenshots / recordings + +N/A — documentation-only change. + +### Checklist + +- [x] I have tested my changes locally +- [x] I have not included unrelated changes in this PR +``` + +Do not expand the PR beyond the one English Ecosystem row unless an upstream +maintainer explicitly requests translated pages or other changes. + +## After merge + +Verify the live Ecosystem page links to the public repository, install the npm +package from a clean OpenCode client, complete one local skill, complete the +connected OAuth workflow smoke, and record the upstream issue, PR, merge commit, +live URL, package version, and evidence on aictrl-dev/aictrl#3865. From fa4294c78c47f6bafbfb1d02c15af88ac7df6833 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Tue, 14 Jul 2026 18:51:24 +0100 Subject: [PATCH 21/34] test(opencode): cover package upgrades --- test/public-packages.test.ts | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/public-packages.test.ts b/test/public-packages.test.ts index 698f6ad..07da96f 100644 --- a/test/public-packages.test.ts +++ b/test/public-packages.test.ts @@ -92,6 +92,59 @@ describe('public vendor packages', () => { }); }); + it('upgrades AICtrl-managed OpenCode state without removing unrelated skills', () => { + const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-upgrade-')); + const configRoot = join(root, 'opencode'); + const configFile = join(configRoot, 'opencode.json'); + const skillsRoot = join(configRoot, 'skills'); + const managedSkill = join(skillsRoot, 'implement-code-change'); + const unrelatedSkill = join(skillsRoot, 'team-custom'); + mkdirSync(managedSkill, { recursive: true }); + mkdirSync(unrelatedSkill, { recursive: true }); + writeFileSync(join(managedSkill, 'SKILL.md'), 'stale managed skill\n'); + writeFileSync(join(unrelatedSkill, 'SKILL.md'), 'unrelated team skill\n'); + writeFileSync( + configFile, + JSON.stringify({ + theme: 'system', + mcp: { + existing: { type: 'remote', url: 'https://example.com/mcp' }, + aictrl: { + type: 'remote', + url: 'https://aictrl.dev/mcp/workflows/opencode-ecosystem/0.1.0-beta.1/implement-code-change/1.0.0', + enabled: true, + }, + }, + }), + ); + + execFileSync(process.execPath, [join(repoRoot, 'opencode/bin/install.js')], { + env: { ...process.env, XDG_CONFIG_HOME: root }, + }); + + const packageMetadata = json('opencode/package.json'); + const skillsManifest = json('opencode/skills-manifest.json'); + expect(jsonAt(configFile)).toMatchObject({ + theme: 'system', + mcp: { + existing: { type: 'remote', url: 'https://example.com/mcp' }, + aictrl: { + type: 'remote', + url: publicMcpUrl( + 'opencode-ecosystem', + packageMetadata.version, + skillsManifest.skillsVersion, + ), + enabled: true, + }, + }, + }); + expect(readFileSync(join(managedSkill, 'SKILL.md'), 'utf8')).toBe( + readFileSync(join(repoRoot, 'opencode/skills/implement-code-change/SKILL.md'), 'utf8'), + ); + expect(readFileSync(join(unrelatedSkill, 'SKILL.md'), 'utf8')).toBe('unrelated team skill\n'); + }); + it('fails closed instead of overwriting malformed OpenCode config', () => { const root = mkdtempSync(join(tmpdir(), 'aictrl-opencode-invalid-')); const configRoot = join(root, 'opencode'); From 4140707fe72a72f8e72369234c8a838302e9cf6b Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 07:45:37 +0100 Subject: [PATCH 22/34] fix(mcp): use the canonical aictrl server URL --- README.md | 14 ++++++-------- claude/aictrl/.mcp.json | 2 +- docs/public-release-runbook.md | 10 ++++------ opencode/bin/install.js | 9 ++------- plugins/aictrl/.mcp.json | 2 +- scripts/assemble-public-packages.mjs | 20 ++++++++++---------- scripts/smoke-claude-public.mjs | 10 +++------- scripts/smoke-codex-public.mjs | 16 +++------------- scripts/smoke-opencode-public.mjs | 4 +--- scripts/validate-public-packages.mjs | 12 ++++++------ submission/codex/readiness.md | 8 ++++---- submission/opencode/ecosystem.md | 4 ++-- test/public-packages.test.ts | 26 +++++++------------------- 13 files changed, 50 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 616b456..e5dd04a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Install eight portable engineering skills across Claude Code, Codex, and OpenCode. Every skill works locally; connected `implement-code-change` adds -versioned remote execution, approvals, evidence, history, and policy controls. +remote workflow execution, approvals, evidence, history, and policy controls. ## Public packages @@ -38,9 +38,8 @@ Use `npx @aictrl/opencode --project .` for a project-local install or `public-skills.lock.json` pins an immutable [`aictrl-dev/skills`](https://github.com/aictrl-dev/skills) release, commit, and checksum-manifest digest. All three vendor packages contain byte-identical -copies of the eight launch skills. Package assembly also generates a distinct -versioned MCP resource URL for each vendor listing so native OAuth preserves the -listing, platform, plugin version, and skill version. +copies of the eight launch skills and connect the same `aictrl` server identity +to the canonical public workflow endpoint. ```bash npm run assemble:public @@ -57,10 +56,9 @@ to reach the expected unauthenticated state before publishing. ## Release status The package tree is a public beta artifact. Connected release remains gated on -the production listing-specific resources beneath -`https://aictrl.dev/mcp/workflows`, OAuth hardening, clean-client lifecycle -evidence, publisher verification, and vendor publication checks. Local skills -do not require an AICtrl account or API key. +the production `https://aictrl.dev/mcp` resource, OAuth hardening, clean-client +lifecycle evidence, publisher verification, and vendor publication checks. +Local skills do not require an AICtrl account or API key. Release owners must follow the [public release runbook](docs/public-release-runbook.md), including the skills re-pin, first npm publication, vendor smoke tests, evidence, diff --git a/claude/aictrl/.mcp.json b/claude/aictrl/.mcp.json index 5c34d82..544d452 100644 --- a/claude/aictrl/.mcp.json +++ b/claude/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows/claude-marketplace/0.1.0-beta.2/implement-code-change/1.0.0" + "url": "https://aictrl.dev/mcp" } } } diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 88a5ac0..1977d5d 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -52,8 +52,8 @@ Stop the release when any of these is true: digest. Run `npm run assemble:public`; do not hand-edit generated skill files. 5. Run all package verification below, then merge the plugin PR. 6. Promote the sandbox runtime batch to production and repeat the health, OAuth, - MCP catalog, schema, annotation, and connected-workflow checks for each - listing-specific versioned resource generated by package assembly. + MCP catalog, schema, annotation, and connected-workflow checks against the + canonical `https://aictrl.dev/mcp` resource. 7. Publish and verify the public Git, npm, and portal artifacts in the vendor sections below. @@ -140,10 +140,8 @@ commit, and listing smoke test in the release evidence. 1. Confirm the OpenAI organization has a verified developer or business identity and the submitter has Apps Management write access. -2. Enter the generated Codex MCP resource URL in the plugin portal: - `https://aictrl.dev/mcp/workflows/codex-plugin-directory//implement-code-change/`. - Copy it from `plugins/aictrl/.mcp.json`; do not reconstruct or shorten it to - the generic workflow endpoint. +2. Enter the canonical Codex MCP resource URL from `plugins/aictrl/.mcp.json` + in the plugin portal: `https://aictrl.dev/mcp`. 3. Store the portal-provided domain token as a new production secret version. Verify that `/.well-known/openai-apps-challenge` returns only the exact token as plain text, then remove the token from local shell history and evidence. diff --git a/opencode/bin/install.js b/opencode/bin/install.js index 5b043ed..03c6c6e 100755 --- a/opencode/bin/install.js +++ b/opencode/bin/install.js @@ -17,9 +17,9 @@ const skillsRoot = join(configRoot, 'skills'); const configFile = projectRoot ? resolve(projectRoot, 'opencode.json') : join(configRoot, 'opencode.json'); const sourceSkills = join(packageRoot, 'skills'); const skillsManifest = readSkillsManifest(sourceSkills); -const packageMetadata = readPackageMetadata(); +readPackageMetadata(); const skillNames = [...new Set(skillsManifest.skills)].sort(); -const mcpUrl = publicMcpUrl(packageMetadata.version, skillsManifest.skillsVersion); +const mcpUrl = 'https://aictrl.dev/mcp'; if (args.has('--uninstall')) { for (const skill of skillNames) rmSync(join(skillsRoot, skill), { recursive: true, force: true }); @@ -76,11 +76,6 @@ function readPackageMetadata() { if (metadata.name !== '@aictrl/opencode' || !isVersion(metadata.version)) { fail('Package metadata is invalid; reinstall the package.'); } - return metadata; -} - -function publicMcpUrl(pluginVersion, skillsVersion) { - return `https://aictrl.dev/mcp/workflows/opencode-ecosystem/${pluginVersion}/implement-code-change/${skillsVersion}`; } function isVersion(value) { diff --git a/plugins/aictrl/.mcp.json b/plugins/aictrl/.mcp.json index 4a2f9f6..544d452 100644 --- a/plugins/aictrl/.mcp.json +++ b/plugins/aictrl/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "aictrl": { "type": "http", - "url": "https://aictrl.dev/mcp/workflows/codex-plugin-directory/0.1.0-beta.2/implement-code-change/1.0.0" + "url": "https://aictrl.dev/mcp" } } } diff --git a/scripts/assemble-public-packages.mjs b/scripts/assemble-public-packages.mjs index 2294645..a13795b 100755 --- a/scripts/assemble-public-packages.mjs +++ b/scripts/assemble-public-packages.mjs @@ -129,8 +129,8 @@ function writeDistributionMetadata() { const claude = readJson('claude/aictrl/.claude-plugin/plugin.json'); const opencode = readJson('opencode/package.json'); - writeJson('plugins/aictrl/.mcp.json', mcpConfig('codex-plugin-directory', codex.version)); - writeJson('claude/aictrl/.mcp.json', mcpConfig('claude-marketplace', claude.version)); + writeJson('plugins/aictrl/.mcp.json', mcpConfig()); + writeJson('claude/aictrl/.mcp.json', mcpConfig()); writeJson('opencode/skills-manifest.json', { skillsVersion: lock.skillsVersion, skills: lock.skills, @@ -152,11 +152,11 @@ function verifyDistributionMetadata() { if (codex.version !== opencode.version || claude.version !== opencode.version) { throw new Error('Claude, Codex, and OpenCode public package versions must match'); } - if (codexMcp.mcpServers?.aictrl?.url !== publicMcpUrl('codex-plugin-directory', codex.version)) { - throw new Error('Codex MCP resource URL does not match the pinned package and skill versions'); + if (codexMcp.mcpServers?.aictrl?.url !== publicMcpUrl()) { + throw new Error('Codex MCP resource URL is not the canonical public workflow endpoint'); } - if (claudeMcp.mcpServers?.aictrl?.url !== publicMcpUrl('claude-marketplace', claude.version)) { - throw new Error('Claude MCP resource URL does not match the pinned package and skill versions'); + if (claudeMcp.mcpServers?.aictrl?.url !== publicMcpUrl()) { + throw new Error('Claude MCP resource URL is not the canonical public workflow endpoint'); } if ( skillsManifest.skillsVersion !== lock.skillsVersion @@ -166,19 +166,19 @@ function verifyDistributionMetadata() { } } -function mcpConfig(listing, pluginVersion) { +function mcpConfig() { return { mcpServers: { aictrl: { type: 'http', - url: publicMcpUrl(listing, pluginVersion), + url: publicMcpUrl(), }, }, }; } -function publicMcpUrl(listing, pluginVersion) { - return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${lock.skillsVersion}`; +function publicMcpUrl() { + return 'https://aictrl.dev/mcp'; } function readJson(path) { diff --git a/scripts/smoke-claude-public.mjs b/scripts/smoke-claude-public.mjs index 2387b7b..84e4ad2 100644 --- a/scripts/smoke-claude-public.mjs +++ b/scripts/smoke-claude-public.mjs @@ -20,7 +20,7 @@ const source = process.env.AICTRL_CLAUDE_MARKETPLACE_SOURCE || 'aictrl-dev/aictr const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; const marketplaceName = 'aictrl-public'; const pluginId = `aictrl@${marketplaceName}`; -const publicSkillsLock = jsonFile(join(root, 'public-skills.lock.json')); +const publicMcpUrl = 'https://aictrl.dev/mcp'; const expectedSkills = [ 'code-review', 'create-bug', @@ -83,8 +83,8 @@ function assertInstalled() { } const mcp = jsonFile(join(installedRoot, '.mcp.json')); - if (mcp.mcpServers?.aictrl?.url !== publicMcpUrl(manifest.version)) { - throw new Error('Installed Claude plugin does not target its versioned workflow MCP resource'); + if (mcp.mcpServers?.aictrl?.url !== publicMcpUrl) { + throw new Error('Installed Claude plugin does not target the canonical public workflow endpoint'); } const skillsRoot = join(installedRoot, 'skills'); @@ -125,7 +125,3 @@ function jsonOutput(args) { function jsonFile(path) { return JSON.parse(readFileSync(path, 'utf8')); } - -function publicMcpUrl(pluginVersion) { - return `https://aictrl.dev/mcp/workflows/claude-marketplace/${pluginVersion}/implement-code-change/${publicSkillsLock.skillsVersion}`; -} diff --git a/scripts/smoke-codex-public.mjs b/scripts/smoke-codex-public.mjs index fdd35eb..47bdc0b 100644 --- a/scripts/smoke-codex-public.mjs +++ b/scripts/smoke-codex-public.mjs @@ -21,7 +21,7 @@ const marketplacePluginRoot = join(marketplaceRoot, 'plugins/aictrl'); const codex = process.env.CODEX_BIN || 'codex'; const env = { ...process.env, CODEX_HOME: codexHome }; const pluginId = 'aictrl@aictrl-public'; -const publicSkillsLock = json(join(root, 'public-skills.lock.json')); +const publicMcpUrl = 'https://aictrl.dev/mcp'; const expectedSkills = [ 'code-review', 'create-bug', @@ -54,12 +54,6 @@ try { join(marketplacePluginRoot, '.codex-plugin/plugin.json'), `${JSON.stringify({ ...sourceManifest, version: upgradeVersion }, null, 2)}\n`, ); - const upgradedMcp = json(join(marketplacePluginRoot, '.mcp.json')); - upgradedMcp.mcpServers.aictrl.url = publicMcpUrl(upgradeVersion); - writeFileSync( - join(marketplacePluginRoot, '.mcp.json'), - `${JSON.stringify(upgradedMcp, null, 2)}\n`, - ); run(['plugin', 'add', pluginId]); assertInstalled(upgradeVersion); @@ -93,8 +87,8 @@ function assertInstalled(expectedVersion) { } const installedMcp = json(join(installedRoot, '.mcp.json')); - if (installedMcp.mcpServers?.aictrl?.url !== publicMcpUrl(expectedVersion)) { - throw new Error('Installed Codex plugin does not target its versioned workflow MCP resource'); + if (installedMcp.mcpServers?.aictrl?.url !== publicMcpUrl) { + throw new Error('Installed Codex plugin does not target the canonical public workflow endpoint'); } const skillsRoot = join(installedRoot, 'skills'); @@ -135,7 +129,3 @@ function assertIncludes(value, expected, context) { throw new Error(`${context} did not include ${JSON.stringify(expected)}`); } } - -function publicMcpUrl(pluginVersion) { - return `https://aictrl.dev/mcp/workflows/codex-plugin-directory/${pluginVersion}/implement-code-change/${publicSkillsLock.skillsVersion}`; -} diff --git a/scripts/smoke-opencode-public.mjs b/scripts/smoke-opencode-public.mjs index 27bee1f..a26606b 100644 --- a/scripts/smoke-opencode-public.mjs +++ b/scripts/smoke-opencode-public.mjs @@ -22,9 +22,7 @@ const dataDir = join(tempRoot, 'data'); const homeDir = join(tempRoot, 'home'); const npm = process.env.NPM_BIN || 'npm'; const opencode = process.env.OPENCODE_BIN || 'opencode'; -const packageMetadata = jsonFile(join(root, 'opencode/package.json')); -const skillsManifest = jsonFile(join(root, 'opencode/skills-manifest.json')); -const productionMcpUrl = `https://aictrl.dev/mcp/workflows/opencode-ecosystem/${packageMetadata.version}/implement-code-change/${skillsManifest.skillsVersion}`; +const productionMcpUrl = 'https://aictrl.dev/mcp'; const connectivityOrigin = process.env.AICTRL_OPENCODE_CONNECTIVITY_ORIGIN || 'https://aictrl.dev'; const connectivityUrl = new URL(new URL(productionMcpUrl).pathname, connectivityOrigin).toString(); const configFile = join(configDir, 'opencode/opencode.json'); diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index 2d2dd96..a475343 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -70,15 +70,15 @@ else { if ( mcp.mcpServers?.aictrl?.url - !== publicMcpUrl('codex-plugin-directory', plugin.version, publicSkillsLock.skillsVersion) + !== publicMcpUrl() ) { - errors.push('Codex MCP must target its listing-specific versioned workflow resource'); + errors.push('Codex MCP must target the canonical public workflow endpoint'); } if ( claudeMcp.mcpServers?.aictrl?.url - !== publicMcpUrl('claude-marketplace', claudePlugin.version, publicSkillsLock.skillsVersion) + !== publicMcpUrl() ) { - errors.push('Claude MCP must target its listing-specific versioned workflow resource'); + errors.push('Claude MCP must target the canonical public workflow endpoint'); } if (opencode.name !== '@aictrl/opencode' || opencode.bin?.['aictrl-opencode'] !== 'bin/install.js') { errors.push('OpenCode npm package metadata is invalid'); @@ -152,8 +152,8 @@ function required(object, fields) { for (const field of fields) if (object[field] == null) errors.push(`Codex manifest requires ${field}`); } -function publicMcpUrl(listing, pluginVersion, skillsVersion) { - return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${skillsVersion}`; +function publicMcpUrl() { + return 'https://aictrl.dev/mcp'; } function walk(directory) { diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 8130de6..df90761 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -8,8 +8,8 @@ version upgrade, and removal in CI while preserving marketplace configuration. - [x] Claude Code 2.1.207 completes clean public marketplace add, install, repeat install, and removal in CI. -- [ ] OpenCode 1.17.20 installs the packed npm artifact, discovers the exact - versioned sandbox OAuth boundary, repeats idempotently, and removes only +- [x] OpenCode 1.17.20 installs the packed npm artifact, discovers the canonical + sandbox OAuth boundary, repeats idempotently, and removes only AICtrl-managed state after aictrl-dev/aictrl#3904 is deployed. - [x] Eight skills are byte-pinned and checksum-verified. - [x] Website, support, privacy, and terms URLs return HTTP 200. @@ -23,8 +23,8 @@ ## MCP and OAuth -- [ ] The generated `codex-plugin-directory` resource URL from - `plugins/aictrl/.mcp.json` is deployed and publicly reachable. +- [x] The canonical resource URL from `plugins/aictrl/.mcp.json` is deployed and + publicly reachable. - [ ] Tool scan returns exactly the six workflow lifecycle tools. - [ ] Schemas and all three annotations match deployed behavior. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and diff --git a/submission/opencode/ecosystem.md b/submission/opencode/ecosystem.md index 393e0c7..91cac4c 100644 --- a/submission/opencode/ecosystem.md +++ b/submission/opencode/ecosystem.md @@ -24,8 +24,8 @@ opencode mcp list npx @aictrl/opencode --project . --uninstall ``` -Record the public version, `beta` dist-tag, successful clean install, exact -versioned MCP URL, OAuth-required boundary, uninstall result, and UTC time. +Record the public version, `beta` dist-tag, successful clean install, canonical +MCP URL, OAuth-required boundary, uninstall result, and UTC time. ## Upstream issue diff --git a/test/public-packages.test.ts b/test/public-packages.test.ts index 07da96f..c58f779 100644 --- a/test/public-packages.test.ts +++ b/test/public-packages.test.ts @@ -36,7 +36,7 @@ describe('public vendor packages', () => { }); }); - it('binds every vendor MCP resource to its listing and package versions', () => { + it('uses one canonical workflow MCP resource for every vendor package', () => { const lock = json('public-skills.lock.json'); const codex = json('plugins/aictrl/.codex-plugin/plugin.json'); const claude = json('claude/aictrl/.claude-plugin/plugin.json'); @@ -44,10 +44,10 @@ describe('public vendor packages', () => { const manifest = json('opencode/skills-manifest.json'); expect(json('plugins/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( - publicMcpUrl('codex-plugin-directory', codex.version, lock.skillsVersion), + publicMcpUrl(), ); expect(json('claude/aictrl/.mcp.json').mcpServers.aictrl.url).toBe( - publicMcpUrl('claude-marketplace', claude.version, lock.skillsVersion), + publicMcpUrl(), ); expect(manifest).toEqual({ skillsVersion: lock.skillsVersion, skills: lock.skills }); expect(opencode.version).toBe(codex.version); @@ -65,13 +65,7 @@ describe('public vendor packages', () => { ); const env = { ...process.env, XDG_CONFIG_HOME: root }; const installer = join(repoRoot, 'opencode/bin/install.js'); - const packageMetadata = json('opencode/package.json'); - const skillsManifest = json('opencode/skills-manifest.json'); - const expectedMcpUrl = publicMcpUrl( - 'opencode-ecosystem', - packageMetadata.version, - skillsManifest.skillsVersion, - ); + const expectedMcpUrl = publicMcpUrl(); execFileSync(process.execPath, [installer], { env }); execFileSync(process.execPath, [installer], { env }); @@ -122,19 +116,13 @@ describe('public vendor packages', () => { env: { ...process.env, XDG_CONFIG_HOME: root }, }); - const packageMetadata = json('opencode/package.json'); - const skillsManifest = json('opencode/skills-manifest.json'); expect(jsonAt(configFile)).toMatchObject({ theme: 'system', mcp: { existing: { type: 'remote', url: 'https://example.com/mcp' }, aictrl: { type: 'remote', - url: publicMcpUrl( - 'opencode-ecosystem', - packageMetadata.version, - skillsManifest.skillsVersion, - ), + url: publicMcpUrl(), enabled: true, }, }, @@ -170,6 +158,6 @@ function jsonAt(path: string): any { return JSON.parse(readFileSync(path, 'utf8')); } -function publicMcpUrl(listing: string, pluginVersion: string, skillsVersion: string): string { - return `https://aictrl.dev/mcp/workflows/${listing}/${pluginVersion}/implement-code-change/${skillsVersion}`; +function publicMcpUrl(): string { + return 'https://aictrl.dev/mcp'; } From b85f42fcc0f392b64a9c3fb6ff11c3b87cc0a2e4 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 08:01:40 +0100 Subject: [PATCH 23/34] chore(skills): pin public catalog v1.0.1 --- claude/aictrl/skills/code-review/SKILL.md | 2 +- claude/aictrl/skills/create-bug/SKILL.md | 2 +- claude/aictrl/skills/create-issue/SKILL.md | 2 +- claude/aictrl/skills/create-workflow/SKILL.md | 13 +++++++------ .../create-workflow/reference/authoring-guide.md | 5 ++++- .../reference/v1/workflow.schema.json | 2 +- .../create-workflow/reference/workflow.schema.json | 4 ++-- claude/aictrl/skills/implement-code-change/SKILL.md | 10 ++++++---- claude/aictrl/skills/judge-review-findings/SKILL.md | 2 +- claude/aictrl/skills/reply-to-code-review/SKILL.md | 2 +- claude/aictrl/skills/spec-review/SKILL.md | 2 +- opencode/skills-manifest.json | 2 +- opencode/skills/code-review/SKILL.md | 2 +- opencode/skills/create-bug/SKILL.md | 2 +- opencode/skills/create-issue/SKILL.md | 2 +- opencode/skills/create-workflow/SKILL.md | 13 +++++++------ .../create-workflow/reference/authoring-guide.md | 5 ++++- .../reference/v1/workflow.schema.json | 2 +- .../create-workflow/reference/workflow.schema.json | 4 ++-- opencode/skills/implement-code-change/SKILL.md | 10 ++++++---- opencode/skills/judge-review-findings/SKILL.md | 2 +- opencode/skills/reply-to-code-review/SKILL.md | 2 +- opencode/skills/spec-review/SKILL.md | 2 +- plugins/aictrl/skills/code-review/SKILL.md | 2 +- plugins/aictrl/skills/create-bug/SKILL.md | 2 +- plugins/aictrl/skills/create-issue/SKILL.md | 2 +- plugins/aictrl/skills/create-workflow/SKILL.md | 13 +++++++------ .../create-workflow/reference/authoring-guide.md | 5 ++++- .../reference/v1/workflow.schema.json | 2 +- .../create-workflow/reference/workflow.schema.json | 4 ++-- .../aictrl/skills/implement-code-change/SKILL.md | 10 ++++++---- .../aictrl/skills/judge-review-findings/SKILL.md | 2 +- plugins/aictrl/skills/reply-to-code-review/SKILL.md | 2 +- plugins/aictrl/skills/spec-review/SKILL.md | 2 +- public-skills.lock.json | 6 +++--- 35 files changed, 82 insertions(+), 64 deletions(-) diff --git a/claude/aictrl/skills/code-review/SKILL.md b/claude/aictrl/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/claude/aictrl/skills/code-review/SKILL.md +++ b/claude/aictrl/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/claude/aictrl/skills/create-bug/SKILL.md b/claude/aictrl/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/claude/aictrl/skills/create-bug/SKILL.md +++ b/claude/aictrl/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/claude/aictrl/skills/create-issue/SKILL.md b/claude/aictrl/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/claude/aictrl/skills/create-issue/SKILL.md +++ b/claude/aictrl/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/claude/aictrl/skills/create-workflow/SKILL.md b/claude/aictrl/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/claude/aictrl/skills/create-workflow/SKILL.md +++ b/claude/aictrl/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/claude/aictrl/skills/create-workflow/reference/authoring-guide.md b/claude/aictrl/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/claude/aictrl/skills/create-workflow/reference/authoring-guide.md +++ b/claude/aictrl/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json b/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json +++ b/claude/aictrl/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/claude/aictrl/skills/create-workflow/reference/workflow.schema.json b/claude/aictrl/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/claude/aictrl/skills/create-workflow/reference/workflow.schema.json +++ b/claude/aictrl/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/claude/aictrl/skills/implement-code-change/SKILL.md b/claude/aictrl/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/claude/aictrl/skills/implement-code-change/SKILL.md +++ b/claude/aictrl/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/claude/aictrl/skills/judge-review-findings/SKILL.md b/claude/aictrl/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/claude/aictrl/skills/judge-review-findings/SKILL.md +++ b/claude/aictrl/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/claude/aictrl/skills/reply-to-code-review/SKILL.md b/claude/aictrl/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/claude/aictrl/skills/reply-to-code-review/SKILL.md +++ b/claude/aictrl/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/claude/aictrl/skills/spec-review/SKILL.md b/claude/aictrl/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/claude/aictrl/skills/spec-review/SKILL.md +++ b/claude/aictrl/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/opencode/skills-manifest.json b/opencode/skills-manifest.json index 64167c8..2a334c1 100644 --- a/opencode/skills-manifest.json +++ b/opencode/skills-manifest.json @@ -1,5 +1,5 @@ { - "skillsVersion": "1.0.0", + "skillsVersion": "1.0.1", "skills": [ "create-issue", "create-bug", diff --git a/opencode/skills/code-review/SKILL.md b/opencode/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/opencode/skills/code-review/SKILL.md +++ b/opencode/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/opencode/skills/create-bug/SKILL.md b/opencode/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/opencode/skills/create-bug/SKILL.md +++ b/opencode/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/opencode/skills/create-issue/SKILL.md b/opencode/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/opencode/skills/create-issue/SKILL.md +++ b/opencode/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/opencode/skills/create-workflow/SKILL.md b/opencode/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/opencode/skills/create-workflow/SKILL.md +++ b/opencode/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/opencode/skills/create-workflow/reference/authoring-guide.md b/opencode/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/opencode/skills/create-workflow/reference/authoring-guide.md +++ b/opencode/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/opencode/skills/create-workflow/reference/v1/workflow.schema.json b/opencode/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/opencode/skills/create-workflow/reference/v1/workflow.schema.json +++ b/opencode/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/opencode/skills/create-workflow/reference/workflow.schema.json b/opencode/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/opencode/skills/create-workflow/reference/workflow.schema.json +++ b/opencode/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/opencode/skills/implement-code-change/SKILL.md b/opencode/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/opencode/skills/implement-code-change/SKILL.md +++ b/opencode/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/opencode/skills/judge-review-findings/SKILL.md b/opencode/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/opencode/skills/judge-review-findings/SKILL.md +++ b/opencode/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/opencode/skills/reply-to-code-review/SKILL.md b/opencode/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/opencode/skills/reply-to-code-review/SKILL.md +++ b/opencode/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/opencode/skills/spec-review/SKILL.md b/opencode/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/opencode/skills/spec-review/SKILL.md +++ b/opencode/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/plugins/aictrl/skills/code-review/SKILL.md b/plugins/aictrl/skills/code-review/SKILL.md index 5a09c15..6eaebb0 100644 --- a/plugins/aictrl/skills/code-review/SKILL.md +++ b/plugins/aictrl/skills/code-review/SKILL.md @@ -46,4 +46,4 @@ Produce high-signal findings for the exact pull or merge request revision. Revie - Separate verified defects from residual risk and untested hypotheses. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=code-review) diff --git a/plugins/aictrl/skills/create-bug/SKILL.md b/plugins/aictrl/skills/create-bug/SKILL.md index 17130bf..1c714be 100644 --- a/plugins/aictrl/skills/create-bug/SKILL.md +++ b/plugins/aictrl/skills/create-bug/SKILL.md @@ -61,4 +61,4 @@ Turn symptoms, logs, or an observed regression into a bug that another engineer - Do not implement the fix, commit, push, or change issue state unless the user asks. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-bug&utm_listing=github-skills&utm_platform=portable&utm_skill=create-bug) diff --git a/plugins/aictrl/skills/create-issue/SKILL.md b/plugins/aictrl/skills/create-issue/SKILL.md index 8e50dde..b26fa91 100644 --- a/plugins/aictrl/skills/create-issue/SKILL.md +++ b/plugins/aictrl/skills/create-issue/SKILL.md @@ -64,4 +64,4 @@ so that . - Never include secrets, credentials, private customer data, or unnecessary source excerpts. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-issue&utm_listing=github-skills&utm_platform=portable&utm_skill=create-issue) diff --git a/plugins/aictrl/skills/create-workflow/SKILL.md b/plugins/aictrl/skills/create-workflow/SKILL.md index 9eae3f0..00d56dd 100644 --- a/plugins/aictrl/skills/create-workflow/SKILL.md +++ b/plugins/aictrl/skills/create-workflow/SKILL.md @@ -5,7 +5,7 @@ description: Create and validate an AICtrl workflow v2 YAML file with typed para # Create an AICtrl Workflow -Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. +Author a reviewable `.aictrl/workflows/.yaml` file. Treat this directory like `.github/workflows/`: create the workflow in the repository whose automation the user is defining. Installing a skill or plugin does not publish a workflow for that repository. Workflow v2 with inline `task` nodes is the default because it keeps task configuration portable in Git. ## Workflow @@ -37,17 +37,18 @@ Author a reviewable `.aictrl/workflows/.yaml` file. Workflow v2 with schemaVersion: aictrl/workflow/v2 name: implement-change parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } nodes: - id: implement type: task skill: implement-code-change@1.0.0 taskType: general prompt: Implement the requested issue and produce a merge-ready pull request. + timeoutMinutes: 10 parameters: - - { name: repository, type: string, required: true } - - { name: issue-id, type: number, required: true } + - { name: repository, type: repository, required: true } + - { name: issue-id, type: number, required: true, validation: { min: 1 } } inputs: repository: { from: input, name: repository } issue-id: { from: input, name: issue-id } @@ -64,4 +65,4 @@ nodes: - Canvas positions and runtime fields are platform-owned and must not be authored. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=create-workflow&utm_listing=github-skills&utm_platform=portable&utm_skill=create-workflow) diff --git a/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md b/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md index 3497c04..f9bffc1 100644 --- a/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md +++ b/plugins/aictrl/skills/create-workflow/reference/authoring-guide.md @@ -39,7 +39,7 @@ triggers: [...] # optional; up to 10 file-declared event tri the platform on import. Authoring them is a schema error (`additionalProperties` is `false` at the top level and on every object). -## Parameter types (12 total) +## Parameter types (13 total) Used for workflow-level `parameters`, `user-input` node `parameters`, and inline `task` node `parameters`: @@ -54,6 +54,7 @@ Used for workflow-level `parameters`, `user-input` node `parameters`, and inline | `select` | string | Requires `options: [...]` | | `multi-select` | list | Requires `options: [...]` | | `json` | dyn | Arbitrary JSON | +| `repository` | string | Connected repository in owner/name form | | `pull-request` | string | PR URL | | `story` | string | Story/ticket URL | | `image` | string | Image URL or base64 | @@ -88,6 +89,7 @@ layer-1 error. skill: code-review@1.0.0 # required; pin when a version is resolvable taskType: code-review # required; executor tool boundary prompt: Review the current pull request head and return structured findings. + timeoutMinutes: 10 # optional; executor runtime, integer 1-60 parameters: - { name: pull-request, type: pull-request, required: true } inputs: @@ -101,6 +103,7 @@ Inline task nodes make the task configuration portable in the workflow file. Their `inputs` are checked against the in-file `parameters`; their declared `outputs` may be mapped by downstream nodes. Treat `prompt` as instructions for the selected skill, not as a way to relax workflow policy or tool boundaries. +`timeoutMinutes` is an optional hard executor bound from 1 to 60 minutes. ### `template` — run a task template ```yaml diff --git a/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json b/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json index 442541b..be88a9a 100644 --- a/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json +++ b/plugins/aictrl/skills/create-workflow/reference/v1/workflow.schema.json @@ -34,7 +34,7 @@ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "label": { "type": "string" }, "type": { - "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "pull-request", "story", "image", "github-issue"] + "enum": ["string", "text", "number", "boolean", "url", "select", "multi-select", "json", "repository", "pull-request", "story", "image", "github-issue"] }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, diff --git a/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json b/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json index 0902b84..16fe7f0 100644 --- a/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json +++ b/plugins/aictrl/skills/create-workflow/reference/workflow.schema.json @@ -87,8 +87,8 @@ "then": { "required": ["parameters"], "not": { "anyOf": [ { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] }, { "required": ["skill"] }, { "required": ["taskType"] }, { "required": ["prompt"] }, { "required": ["outputs"] } ] } } }, { "if": { "properties": { "type": { "const": "task" } }, "required": ["type"] }, - "then": { "required": ["skill"], "not": { "anyOf": [ - { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["timeoutMinutes"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } + "then": { "required": ["skill"], "properties": { "timeoutMinutes": { "maximum": 60 } }, "not": { "anyOf": [ + { "required": ["template"] }, { "required": ["templateVersion"] }, { "required": ["workflow"] }, { "required": ["workflowVersion"] }, { "required": ["maxIterations"] }, { "required": ["until"] }, { "required": ["while"] }, { "required": ["onMaxIterations"] }, { "required": ["body"] }, { "required": ["signalSource"] }, { "required": ["checklistItems"] }, { "required": ["assignee"] } ] } } } ] }, "inputMapping": { "$ref": "../v1/workflow.schema.json#/$defs/inputMapping" }, diff --git a/plugins/aictrl/skills/implement-code-change/SKILL.md b/plugins/aictrl/skills/implement-code-change/SKILL.md index 4c18632..bd8e270 100644 --- a/plugins/aictrl/skills/implement-code-change/SKILL.md +++ b/plugins/aictrl/skills/implement-code-change/SKILL.md @@ -28,12 +28,14 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge ## Connected workflow -1. Call `list_workflows` and confirm `implement-code-change` is available. -2. Call `get_workflow` and show the resolved version, required `{ repository, issue_id }` inputs, side effects, limits, and approval gates. +Connected workflows are repository-owned configuration, analogous to GitHub Actions. Installing this skill or its plugin does not provision one. If the organization has no suitable published workflow, explain that the user can invoke `create-workflow` in the repository they want to automate; do not create or publish a workflow without a separate request. + +1. Call `list_workflows` and confirm a suitable `implement-code-change` workflow is available. If it is absent, stop the connected path and offer the repository-owned `create-workflow` path. +2. Call `get_workflow` and show the resolved immutable version, required `{ repository, issue-id }` inputs, side effects, limits, and approval gates. Preserve the hyphenated `issue-id` key exactly in tool input. 3. Obtain explicit confirmation to start if the user has not already authorized connected execution. 4. Call `start_workflow` with the resolved workflow/version, validated inputs, and a stable idempotency key derived from repository and issue. 5. Poll with `get_workflow_run`; report actual status and required action without inventing progress. -6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice. +6. For a paused gate, show the revision, evidence, cost, and requested decision. Use `approve_workflow_step` only for the user's explicit approve/reject choice, passing `decision` and the unchanged 40-character `expected_revision` returned by `get_workflow_run`. If the revision changed or is absent, stop and retrieve the run again instead of deciding the gate. 7. Use `cancel_workflow_run` when explicitly requested or when the documented safety boundary requires termination. 8. Finish with the exact revision, PR/result link, checks, evidence, cost, and any actionable terminal failure. @@ -46,4 +48,4 @@ Take one engineering issue to a verified, merge-ready pull request. Never merge - The result is merge-ready, not automatically merged or deployed. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=implement-code-change&utm_listing=github-skills&utm_platform=portable&utm_skill=implement-code-change) diff --git a/plugins/aictrl/skills/judge-review-findings/SKILL.md b/plugins/aictrl/skills/judge-review-findings/SKILL.md index d5ad8ae..9f30f17 100644 --- a/plugins/aictrl/skills/judge-review-findings/SKILL.md +++ b/plugins/aictrl/skills/judge-review-findings/SKILL.md @@ -41,4 +41,4 @@ For every `DEFER`, name the follow-up issue or return a complete follow-up draft - Do not apply a judgment to a different head revision. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=judge-review-findings&utm_listing=github-skills&utm_platform=portable&utm_skill=judge-review-findings) diff --git a/plugins/aictrl/skills/reply-to-code-review/SKILL.md b/plugins/aictrl/skills/reply-to-code-review/SKILL.md index a819360..8d2f8d0 100644 --- a/plugins/aictrl/skills/reply-to-code-review/SKILL.md +++ b/plugins/aictrl/skills/reply-to-code-review/SKILL.md @@ -42,4 +42,4 @@ Turn review feedback into verified remediation and concise, evidence-backed resp - Do not create endless automated reviewer loops. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=reply-to-code-review&utm_listing=github-skills&utm_platform=portable&utm_skill=reply-to-code-review) diff --git a/plugins/aictrl/skills/spec-review/SKILL.md b/plugins/aictrl/skills/spec-review/SKILL.md index 09564e4..e809bfa 100644 --- a/plugins/aictrl/skills/spec-review/SKILL.md +++ b/plugins/aictrl/skills/spec-review/SKILL.md @@ -56,4 +56,4 @@ Decide whether an issue is ready to implement by comparing its claims and accept - If the issue targets a different revision or repository, stop and resolve the mismatch. --- -**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review) +**Built by [aictrl.dev](https://aictrl.dev/?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review).** This skill teaches the workflow; aictrl *operationalizes* it — grounded in your backlog, team standards, and codebase knowledge graph. [See how →](https://aictrl.dev/features?utm_source=oss-skills&utm_medium=skill&utm_campaign=spec-review&utm_listing=github-skills&utm_platform=portable&utm_skill=spec-review) diff --git a/public-skills.lock.json b/public-skills.lock.json index 3ce5074..b6cdcaf 100644 --- a/public-skills.lock.json +++ b/public-skills.lock.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, "repository": "https://github.com/aictrl-dev/skills.git", - "commit": "99b618002daed84245cdc86f92abef811d803c8a", - "skillsVersion": "1.0.0", + "commit": "01cf2fa9b9bcf3b3b48cb00f3e48764c292ecf18", + "skillsVersion": "1.0.1", "checksumsFile": "CHECKSUMS.sha256", - "checksumsSha256": "f7ee4bd53e6ed2bacd68803dfb230b79a47649124fc5d1ecb0c1958be1a35a45", + "checksumsSha256": "a09b2075fbe54f2615aa8b8a2786162c1cdd1676e5944a000116786feda451b4", "sourceDirectory": "aictrl-skills/skills", "skills": [ "create-issue", From 8a95929240dff671f3e165ef5c77ec8bb1d3ccfa Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 08:12:25 +0100 Subject: [PATCH 24/34] docs(release): enforce runtime-first publication --- docs/public-release-runbook.md | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 1977d5d..e31f187 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -46,20 +46,25 @@ Stop the release when any of these is true: 2. Apply the canonical `implement-code-change` workflow in sandbox. Verify its immutable version, `repository` and `issue-id` inputs, exact-revision gate, two task bounds, and no-merge/no-deploy boundary. -3. Merge `aictrl-dev/skills#4`, create a new semantic release, and verify the - published `CHECKSUMS.sha256` against the release commit. -4. Update `public-skills.lock.json` to that exact commit and checksum-manifest - digest. Run `npm run assemble:public`; do not hand-edit generated skill files. -5. Run all package verification below, then merge the plugin PR. +3. Publish the canonical skills commit as a semantic release and verify the + published `CHECKSUMS.sha256` against that exact release commit. For this beta, + use `aictrl-dev/skills@v1.0.1` at commit + `01cf2fa9b9bcf3b3b48cb00f3e48764c292ecf18`. +4. Pin that exact commit and checksum-manifest digest in + `public-skills.lock.json`. Run `npm run assemble:public`; do not hand-edit + generated skill files. +5. Run all package verification below, but do not merge or publish the plugin + yet. 6. Promote the sandbox runtime batch to production and repeat the health, OAuth, MCP catalog, schema, annotation, and connected-workflow checks against the - canonical `https://aictrl.dev/mcp` resource. -7. Publish and verify the public Git, npm, and portal artifacts in the vendor - sections below. - -Do not reorder steps 3 and 4. The skills v1.0.0 connected instructions use the -obsolete `issue_id` key; the canonical workflow requires the exact `issue-id` -key published by `aictrl-dev/skills#4`. + canonical `https://aictrl.dev/mcp` resource. Stop if the authenticated + `tools/list` result is not exactly the six documented lifecycle tools. +7. Merge the plugin PR only after step 6 passes, then publish and verify the + public Git, npm, and portal artifacts in the vendor sections below. + +Do not reorder steps 3 and 4 or steps 6 and 7. The package must contain the +hyphenated `issue-id` workflow input, and its canonical MCP URL must be deployed +and verified before the public plugin is merged. ## Package verification From 87823b8a2cb7462c5784cbd512cd65ac62d9eab9 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 09:32:40 +0100 Subject: [PATCH 25/34] docs(codex): record public reviewer fixture --- submission/codex/readiness.md | 7 ++--- submission/codex/reviewer-fixture.md | 39 ++++++++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index df90761..0d461d3 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -15,9 +15,10 @@ - [x] Website, support, privacy, and terms URLs return HTTP 200. - [x] Starter prompts are limited to three. - [x] Reviewer pack contains exactly five positive and three negative cases. -- [ ] Every positive reviewer case names reproducible fixture/account data; the - dedicated public repository, issue, and demo-account access described in - `reviewer-fixture.md` are ready. +- [x] The dedicated public reviewer repository, protected baseline, and bounded + fixture issue described in `reviewer-fixture.md` are provisioned. +- [ ] Every positive connected reviewer case names the rehearsed no-MFA demo + account and run data; the AICtrl connection and account remain pending. - [ ] Final production logo is approved for the public listing. Screenshots are optional in the current submission guidance and are not a release gate. diff --git a/submission/codex/reviewer-fixture.md b/submission/codex/reviewer-fixture.md index be24dc1..82bb90f 100644 --- a/submission/codex/reviewer-fixture.md +++ b/submission/codex/reviewer-fixture.md @@ -8,21 +8,33 @@ The deterministic repository seed is in `fixture-template/repository/`, and the disposable issue body is in `fixture-template/issue.md`. Keep them synchronized with the final provisioned resources. +## Provisioned GitHub fixture + +- Repository: [`aictrl-dev/aictrl-plugin-reviewer-fixture`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture) +- Baseline revision: `09b5d36ae163a39fe6b3f56ce347a8cb026afd2c` +- Fixture issue: [`aictrl-dev/aictrl-plugin-reviewer-fixture#1`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture/issues/1) +- Baseline verification: dependency-free `npm test` passes two tests. +- Default branch: `main`, protected with one approval, stale-review dismissal, + last-push approval, conversation resolution, and admin enforcement. Force + pushes, branch deletion, merge commits, and automatic merge are disabled. + +The AICtrl repository connection and no-MFA portal demo account remain pending. +Do not replace the connected test-case placeholders or submit the reviewer pack +until those resources are provisioned and the clean-client rehearsal passes. + ## Required external resources -- A dedicated public GitHub repository owned by `aictrl-dev`. A descriptive - name such as `aictrl-plugin-reviewer-fixture` is recommended, but the release - owner must approve the final repository name before creation. -- One small, deterministic project with a fast test command and no credentials, +- [x] A dedicated public GitHub repository owned by `aictrl-dev`. +- [x] One small, deterministic project with a fast test command and no credentials, customer data, private dependencies, or organization-only instructions. -- One open, disposable issue that requests a bounded code change with explicit +- [x] One open, disposable issue that requests a bounded code change with explicit acceptance criteria. The issue must be safe to run repeatedly. -- Default-branch rules that reject force-pushes and direct workflow writes while +- [x] Default-branch rules that reject force-pushes and direct workflow writes while still allowing the GitHub integration to create feature branches and pull requests. The connected workflow must not receive merge permission. -- An active AICtrl repository connection for the reviewer organization and only +- [ ] An active AICtrl repository connection for the reviewer organization and only the fixture repository. -- A portal demo account that can complete OAuth and the connected cases without +- [ ] A portal demo account that can complete OAuth and the connected cases without MFA, email confirmation, SMS, private-network access, or support intervention. Never commit the demo password, recovery code, OAuth token, GitHub installation @@ -49,17 +61,16 @@ cancellation case cannot invalidate the approval case. ## Provisioning verification -After the release owner approves the repository name, copy the contents of -`fixture-template/repository/` into the new public repository and create its -fixture issue from `fixture-template/issue.md`. Do not initialize or publish the -repository from an unreviewed local tree. +The release owner approved and provisioned the repository from the reviewed +`fixture-template/repository/` tree and created its fixture issue from +`fixture-template/issue.md`. Run these read-only checks after the owner provisions the resources: ```bash -gh repo view / \ +gh repo view aictrl-dev/aictrl-plugin-reviewer-fixture \ --json nameWithOwner,visibility,defaultBranchRef,url -gh issue view --repo / \ +gh issue view 1 --repo aictrl-dev/aictrl-plugin-reviewer-fixture \ --json number,title,state,url ``` From 94343166e90fcc044145e37333c0a272246754d8 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 09:43:06 +0100 Subject: [PATCH 26/34] docs(codex): track protected fixture workflow --- submission/codex/readiness.md | 3 +++ submission/codex/reviewer-fixture.md | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 0d461d3..b24bebb 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -17,6 +17,9 @@ - [x] Reviewer pack contains exactly five positive and three negative cases. - [x] The dedicated public reviewer repository, protected baseline, and bounded fixture issue described in `reviewer-fixture.md` are provisioned. +- [ ] The repository-owned fixture workflow in + `aictrl-dev/aictrl-plugin-reviewer-fixture#2` is independently reviewed and + merged through protected `main`. - [ ] Every positive connected reviewer case names the rehearsed no-MFA demo account and run data; the AICtrl connection and account remain pending. - [ ] Final production logo is approved for the public listing. Screenshots are diff --git a/submission/codex/reviewer-fixture.md b/submission/codex/reviewer-fixture.md index 82bb90f..c384c5c 100644 --- a/submission/codex/reviewer-fixture.md +++ b/submission/codex/reviewer-fixture.md @@ -13,6 +13,9 @@ with the final provisioned resources. - Repository: [`aictrl-dev/aictrl-plugin-reviewer-fixture`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture) - Baseline revision: `09b5d36ae163a39fe6b3f56ce347a8cb026afd2c` - Fixture issue: [`aictrl-dev/aictrl-plugin-reviewer-fixture#1`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture/issues/1) +- Repository-owned workflow: [`aictrl-dev/aictrl-plugin-reviewer-fixture#2`](https://github.com/aictrl-dev/aictrl-plugin-reviewer-fixture/pull/2), + schema/DAG-valid at `77dc71a2e0cd857e09c0fe56055eb0db95ff4961` + and pending the required independent approval and merge. - Baseline verification: dependency-free `npm test` passes two tests. - Default branch: `main`, protected with one approval, stale-review dismissal, last-push approval, conversation resolution, and admin enforcement. Force @@ -32,6 +35,8 @@ until those resources are provisioned and the clean-client rehearsal passes. - [x] Default-branch rules that reject force-pushes and direct workflow writes while still allowing the GitHub integration to create feature branches and pull requests. The connected workflow must not receive merge permission. +- [ ] The repository-owned `implement-code-change` workflow is independently + reviewed and merged through the protected default branch. - [ ] An active AICtrl repository connection for the reviewer organization and only the fixture repository. - [ ] A portal demo account that can complete OAuth and the connected cases without From 3024cf8410fd7ff292cc227fcbe0fdb0232ef6d1 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 10:05:30 +0100 Subject: [PATCH 27/34] chore(ci): refresh supported agent CLIs --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish.yml | 2 +- submission/codex/readiness.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5708d15..39e82fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: with: node-version: 22 - name: Install the supported Codex CLI - run: npm install --global @openai/codex@0.144.1 + run: npm install --global @openai/codex@0.144.4 - name: Run clean install, repeat, upgrade, and removal smoke run: npm run smoke:codex-public @@ -46,7 +46,7 @@ jobs: with: node-version: 22 - name: Install the supported Claude Code CLI - run: npm install --global @anthropic-ai/claude-code@2.1.207 + run: npm install --global @anthropic-ai/claude-code@2.1.210 - name: Run clean public install, repeat, and removal smoke env: AICTRL_CLAUDE_MARKETPLACE_SOURCE: ${{ github.event.pull_request.head.repo.full_name || github.repository }}@${{ github.head_ref || github.ref_name }} @@ -61,7 +61,7 @@ jobs: with: node-version: 22 - name: Install the supported OpenCode CLI - run: npm install --global opencode-ai@1.17.20 + run: npm install --global opencode-ai@1.18.1 - name: Run packed install, OAuth boundary, repeat, and removal smoke env: AICTRL_OPENCODE_CONNECTIVITY_ORIGIN: https://sandbox.aictrl.dev diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 28db3cc..63ef60d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: - name: Verify production OpenCode OAuth boundary if: startsWith(github.event.release.tag_name, 'public-v') run: | - npm install --global opencode-ai@1.17.20 + npm install --global opencode-ai@1.18.1 npm run smoke:opencode-public - name: Resolve release target id: release diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index b24bebb..9118065 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -4,11 +4,11 @@ - [x] `.codex-plugin/plugin.json` passes local ingestion validation. - [x] Marketplace policy includes installation, authentication, and category. -- [x] Codex CLI 0.144.1 completes clean marketplace add, install, repeat install, +- [x] Codex CLI 0.144.4 completes clean marketplace add, install, repeat install, version upgrade, and removal in CI while preserving marketplace configuration. -- [x] Claude Code 2.1.207 completes clean public marketplace add, install, repeat +- [x] Claude Code 2.1.210 completes clean public marketplace add, install, repeat install, and removal in CI. -- [x] OpenCode 1.17.20 installs the packed npm artifact, discovers the canonical +- [x] OpenCode 1.18.1 installs the packed npm artifact, discovers the canonical sandbox OAuth boundary, repeats idempotently, and removes only AICtrl-managed state after aictrl-dev/aictrl#3904 is deployed. - [x] Eight skills are byte-pinned and checksum-verified. From c19364bc6ca5672da2f531ff546a703840f1cf95 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 10:15:41 +0100 Subject: [PATCH 28/34] docs(release): late-bind OpenAI challenge token --- docs/public-release-runbook.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index e31f187..0b393c3 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -147,9 +147,14 @@ commit, and listing smoke test in the release evidence. identity and the submitter has Apps Management write access. 2. Enter the canonical Codex MCP resource URL from `plugins/aictrl/.mcp.json` in the plugin portal: `https://aictrl.dev/mcp`. -3. Store the portal-provided domain token as a new production secret version. - Verify that `/.well-known/openai-apps-challenge` returns only the exact token - as plain text, then remove the token from local shell history and evidence. +3. After the portal issues the domain token, temporarily set + `OPENAI_APPS_CHALLENGE_TOKEN` directly on the production Cloud Run service + using the backend OAuth runbook. Verify that + `/.well-known/openai-apps-challenge` returns only the exact token as plain + text. Keep the override only while OpenAI may need to verify it, reapply it + after any intervening Terraform deployment, and remove it from Cloud Run + after review. Never add the token to Terraform, Secret Manager, CI, shell + history, or release evidence. 4. Scan tools and confirm exactly six tools with truthful schemas, `readOnlyHint`, `openWorldHint`, and `destructiveHint` annotations. 5. Upload the final generated skill tree, listing assets, starter prompts, From 284721312a11826699b01ebf7e97aca0a7e349f2 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 10:20:11 +0100 Subject: [PATCH 29/34] fix(release): verify first npm publication --- .github/workflows/publish.yml | 6 ++ docs/public-release-runbook.md | 4 ++ scripts/verify-release-publication.mjs | 94 ++++++++++++++++++++++++++ test/release-publication.test.ts | 59 ++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 scripts/verify-release-publication.mjs create mode 100644 test/release-publication.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 63ef60d..b5c5e88 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,13 @@ jobs: env: RELEASE_TAG: ${{ github.event.release.tag_name }} run: node scripts/resolve-release-target.mjs "$RELEASE_TAG" >> "$GITHUB_OUTPUT" + - name: Verify existing publication + id: registry + run: >- + node scripts/verify-release-publication.mjs + "${{ steps.release.outputs.package_spec }}" >> "$GITHUB_OUTPUT" - name: Publish release target + if: steps.registry.outputs.published != 'true' run: >- npm publish "${{ steps.release.outputs.package_spec }}" --access public diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 0b393c3..54c78a1 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -119,6 +119,10 @@ The owner must satisfy npm's current 2FA policy. Do not add a long-lived npm token to the repository. After the package exists, configure its trusted publisher for GitHub organization `aictrl-dev`, repository `aictrl-plugin`, workflow `publish.yml`, environment `release`, and the `npm publish` action. +Then publish the matching `public-v` GitHub +release from the same commit. The workflow verifies that the manually published +package has the exact local package integrity and skips the duplicate publish; +an existing version with different contents fails the release. ### Later beta publications diff --git a/scripts/verify-release-publication.mjs b/scripts/verify-release-publication.mjs new file mode 100644 index 0000000..4a2bf65 --- /dev/null +++ b/scripts/verify-release-publication.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, isAbsolute, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const defaultRegistry = 'https://registry.npmjs.org/'; + +export async function inspectReleasePublication( + packageSpec, + { + fetchImpl = fetch, + pack = packPackage, + registry = process.env.NPM_CONFIG_REGISTRY || defaultRegistry, + } = {}, +) { + const packageDirectory = resolve(root, packageSpec); + const relativePackageDirectory = relative(root, packageDirectory); + if (relativePackageDirectory.startsWith('..') || isAbsolute(relativePackageDirectory)) { + throw new Error(`Package target must stay inside the repository: ${packageSpec}`); + } + + const packageMetadata = JSON.parse(readFileSync(resolve(packageDirectory, 'package.json'), 'utf8')); + const { name, version } = packageMetadata; + if (typeof name !== 'string' || typeof version !== 'string') { + throw new Error(`Package target is missing a valid name or version: ${packageSpec}`); + } + + const registryBase = registry.endsWith('/') ? registry : `${registry}/`; + const metadataUrl = `${registryBase}${encodeURIComponent(name)}/${encodeURIComponent(version)}`; + const response = await fetchImpl(metadataUrl, { + headers: { accept: 'application/json' }, + }); + + if (response.status === 404) { + return { published: false, name, version }; + } + if (!response.ok) { + throw new Error(`npm registry returned HTTP ${response.status} for ${name}@${version}`); + } + + const publishedMetadata = await response.json(); + const publishedIntegrity = publishedMetadata?.dist?.integrity; + if (typeof publishedIntegrity !== 'string' || publishedIntegrity.length === 0) { + throw new Error(`Published ${name}@${version} has no registry integrity`); + } + + const packed = await pack(packageSpec); + if (packed.name !== name || packed.version !== version || typeof packed.integrity !== 'string') { + throw new Error(`Local package metadata does not match ${name}@${version}`); + } + if (packed.integrity !== publishedIntegrity) { + throw new Error(`Published ${name}@${version} does not match the local release package`); + } + + return { published: true, name, version }; +} + +async function packPackage(packageSpec) { + const output = execFileSync('npm', ['pack', packageSpec, '--dry-run', '--json'], { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + const result = JSON.parse(output)?.[0]; + if (!result) { + throw new Error(`npm pack returned no package for ${packageSpec}`); + } + return result; +} + +async function main() { + const packageSpec = process.argv[2]; + if (!packageSpec) { + throw new Error('Usage: verify-release-publication.mjs '); + } + + const result = await inspectReleasePublication(packageSpec); + process.stdout.write(`published=${result.published}\n`); + process.stderr.write( + result.published + ? `${result.name}@${result.version} is already published with matching integrity.\n` + : `${result.name}@${result.version} is not yet published.\n`, + ); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); +} diff --git a/test/release-publication.test.ts b/test/release-publication.test.ts new file mode 100644 index 0000000..22b661f --- /dev/null +++ b/test/release-publication.test.ts @@ -0,0 +1,59 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { vi } from 'vitest'; + +import { inspectReleasePublication } from '../scripts/verify-release-publication.mjs'; + +const packageSpec = './opencode'; +const packageMetadata = JSON.parse( + readFileSync(resolve(import.meta.dirname, '..', 'opencode', 'package.json'), 'utf8'), +) as { name: string; version: string }; +const packageIdentity = { name: packageMetadata.name, version: packageMetadata.version }; + +describe('release publication verifier', () => { + it('allows publication when the exact version is absent', async () => { + const pack = vi.fn(); + const result = await inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue(new Response(null, { status: 404 })), + pack, + registry: 'https://registry.example.test', + }); + + expect(result).toEqual({ published: false, ...packageIdentity }); + expect(pack).not.toHaveBeenCalled(); + }); + + it('skips publication only when the published and local integrities match', async () => { + const result = await inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue( + Response.json({ dist: { integrity: 'sha512-matching' } }), + ), + pack: vi.fn().mockResolvedValue({ ...packageIdentity, integrity: 'sha512-matching' }), + registry: 'https://registry.example.test/', + }); + + expect(result).toEqual({ published: true, ...packageIdentity }); + }); + + it('rejects an existing version with different package contents', async () => { + await expect( + inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue( + Response.json({ dist: { integrity: 'sha512-published' } }), + ), + pack: vi.fn().mockResolvedValue({ ...packageIdentity, integrity: 'sha512-local' }), + registry: 'https://registry.example.test', + }), + ).rejects.toThrow('does not match the local release package'); + }); + + it('fails closed on registry errors', async () => { + await expect( + inspectReleasePublication(packageSpec, { + fetchImpl: vi.fn().mockResolvedValue(new Response(null, { status: 503 })), + pack: vi.fn(), + registry: 'https://registry.example.test', + }), + ).rejects.toThrow('npm registry returned HTTP 503'); + }); +}); From fba3f2c9d28b61bb6b050acae3e2f12050e0eec5 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 10:27:16 +0100 Subject: [PATCH 30/34] docs(release): pin first-publish npm packer --- docs/public-release-runbook.md | 12 ++++++++---- test/release-publication.test.ts | 7 ++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 54c78a1..1d655d7 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -111,14 +111,18 @@ For the first beta only, an authenticated `@aictrl` npm owner publishes the verified package from the merged release commit: ```bash +npm install --global npm@11.18.0 +npm --version npm whoami npm publish ./opencode --access public --tag beta ``` -The owner must satisfy npm's current 2FA policy. Do not add a long-lived npm -token to the repository. After the package exists, configure its trusted -publisher for GitHub organization `aictrl-dev`, repository `aictrl-plugin`, -workflow `publish.yml`, environment `release`, and the `npm publish` action. +Use the same pinned npm version as the release workflow so the package integrity +is reproducible. The owner must satisfy npm's current 2FA policy. Do not add a +long-lived npm token to the repository. After the package exists, configure its +trusted publisher for GitHub organization `aictrl-dev`, repository +`aictrl-plugin`, workflow `publish.yml`, environment `release`, and the +`npm publish` action. Then publish the matching `public-v` GitHub release from the same commit. The workflow verifies that the manually published package has the exact local package integrity and skips the duplicate publish; diff --git a/test/release-publication.test.ts b/test/release-publication.test.ts index 22b661f..e7ab7c0 100644 --- a/test/release-publication.test.ts +++ b/test/release-publication.test.ts @@ -13,13 +13,18 @@ const packageIdentity = { name: packageMetadata.name, version: packageMetadata.v describe('release publication verifier', () => { it('allows publication when the exact version is absent', async () => { const pack = vi.fn(); + const fetchImpl = vi.fn().mockResolvedValue(new Response(null, { status: 404 })); const result = await inspectReleasePublication(packageSpec, { - fetchImpl: vi.fn().mockResolvedValue(new Response(null, { status: 404 })), + fetchImpl, pack, registry: 'https://registry.example.test', }); expect(result).toEqual({ published: false, ...packageIdentity }); + expect(fetchImpl).toHaveBeenCalledWith( + `https://registry.example.test/%40aictrl%2Fopencode/${packageIdentity.version}`, + { headers: { accept: 'application/json' } }, + ); expect(pack).not.toHaveBeenCalled(); }); From dadbcd8e6d18276c828bbc747bdb8b14be73b62f Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 14:38:26 +0100 Subject: [PATCH 31/34] test: gate plugin release on production MCP catalog --- .github/workflows/ci.yml | 14 ++ docs/public-release-runbook.md | 5 +- package.json | 1 + scripts/smoke-production-mcp.mjs | 190 ++++++++++++++++++++++++++++ test/production-mcp-catalog.test.ts | 74 +++++++++++ 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 scripts/smoke-production-mcp.mjs create mode 100644 test/production-mcp-catalog.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e82fe..564f1f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,20 @@ jobs: AICTRL_OPENCODE_CONNECTIVITY_ORIGIN: https://sandbox.aictrl.dev run: npm run smoke:opencode-public + production-mcp: + name: Smoke (production MCP catalog) + runs-on: ubuntu-latest + if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Verify the authenticated six-tool production catalog + env: + AICTRL_API_KEY: ${{ secrets.AICTRL_API_KEY }} + run: npm run smoke:mcp-production + e2e: name: E2E (legacy installer against production) runs-on: ubuntu-latest diff --git a/docs/public-release-runbook.md b/docs/public-release-runbook.md index 1d655d7..43e1245 100644 --- a/docs/public-release-runbook.md +++ b/docs/public-release-runbook.md @@ -58,7 +58,10 @@ Stop the release when any of these is true: 6. Promote the sandbox runtime batch to production and repeat the health, OAuth, MCP catalog, schema, annotation, and connected-workflow checks against the canonical `https://aictrl.dev/mcp` resource. Stop if the authenticated - `tools/list` result is not exactly the six documented lifecycle tools. + `tools/list` result is not exactly the six documented lifecycle tools. The + plugin CI performs the catalog, schema, and annotation scan with the + protected `AICTRL_API_KEY`; release owners can repeat it without printing the + key by running `npm run smoke:mcp-production` with that variable exported. 7. Merge the plugin PR only after step 6 passes, then publish and verify the public Git, npm, and portal artifacts in the vendor sections below. diff --git a/package.json b/package.json index 1b2433f..41cced1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "smoke:claude-public": "node scripts/smoke-claude-public.mjs", "smoke:codex-public": "node scripts/smoke-codex-public.mjs", "smoke:opencode-public": "node scripts/smoke-opencode-public.mjs", + "smoke:mcp-production": "node scripts/smoke-production-mcp.mjs", "prepublishOnly": "npm run build" }, "files": [ diff --git a/scripts/smoke-production-mcp.mjs b/scripts/smoke-production-mcp.mjs new file mode 100644 index 0000000..8dae1de --- /dev/null +++ b/scripts/smoke-production-mcp.mjs @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const PRODUCTION_MCP_URL = 'https://aictrl.dev/mcp'; + +export const EXPECTED_CATALOG = [ + { + name: 'list_workflows', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + limit: { type: 'integer', minimum: 1, maximum: 100 }, + }, + required: ['organization_id'], + additionalProperties: false, + }, + }, + { + name: 'get_workflow', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + workflow_id: { type: 'string', minLength: 1 }, + }, + required: ['organization_id', 'workflow_id'], + additionalProperties: false, + }, + }, + { + name: 'start_workflow', + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + workflow_id: { type: 'string', minLength: 1 }, + idempotency_key: { type: 'string', minLength: 8, maxLength: 200 }, + inputs: { type: 'object', additionalProperties: true }, + }, + required: ['organization_id', 'workflow_id', 'idempotency_key'], + additionalProperties: false, + }, + }, + { + name: 'get_workflow_run', + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + }, + required: ['organization_id', 'run_id'], + additionalProperties: false, + }, + }, + { + name: 'approve_workflow_step', + annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + decision: { type: 'string', enum: ['approve', 'reject'] }, + expected_revision: { type: 'string', pattern: '^[0-9a-fA-F]{40}$' }, + note: { type: 'string', maxLength: 2000 }, + }, + required: ['organization_id', 'run_id', 'decision', 'expected_revision'], + additionalProperties: false, + }, + }, + { + name: 'cancel_workflow_run', + annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, + inputSchema: { + type: 'object', + properties: { + organization_id: { type: 'string', minLength: 1 }, + run_id: { type: 'string', minLength: 1 }, + reason: { type: 'string', maxLength: 2000 }, + }, + required: ['organization_id', 'run_id'], + additionalProperties: false, + }, + }, +]; + +export function normalizeCatalog(tools) { + if (!Array.isArray(tools)) { + throw new Error('Production MCP tools/list did not return a tools array.'); + } + + return tools.map((tool) => ({ + name: tool?.name, + annotations: tool?.annotations, + inputSchema: stripDescriptions(tool?.inputSchema), + })); +} + +export function assertProductionCatalog(tools) { + assert.deepStrictEqual( + normalizeCatalog(tools), + EXPECTED_CATALOG, + 'Production MCP catalog differs from the approved six-tool lifecycle contract.', + ); + + for (const tool of tools) { + if (typeof tool.description !== 'string' || tool.description.trim() === '') { + throw new Error(`Production MCP tool ${tool.name ?? ''} has no description.`); + } + } +} + +export async function scanProductionCatalog({ + apiKey, + url = PRODUCTION_MCP_URL, + fetchImpl = fetch, +} = {}) { + if (!apiKey) { + throw new Error('AICTRL_API_KEY is required for the authenticated production MCP scan.'); + } + + const response = await fetchImpl(url, { + method: 'POST', + redirect: 'error', + signal: AbortSignal.timeout(20_000), + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + 'MCP-Protocol-Version': '2024-11-05', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }), + }); + + const body = await response.text(); + if (!response.ok) { + throw new Error(`Production MCP scan returned HTTP ${response.status}.`); + } + + const message = parseMcpResponse(body, response.headers.get('content-type')); + if (message?.error) { + throw new Error(`Production MCP tools/list returned JSON-RPC error ${message.error.code}.`); + } + assertProductionCatalog(message?.result?.tools); + return message.result.tools; +} + +function stripDescriptions(value) { + if (Array.isArray(value)) return value.map(stripDescriptions); + if (!value || typeof value !== 'object') return value; + + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => key !== 'description') + .map(([key, child]) => [key, stripDescriptions(child)]), + ); +} + +function parseMcpResponse(body, contentType = '') { + if (!contentType?.includes('text/event-stream')) { + return JSON.parse(body); + } + + const messages = body + .split(/\r?\n/) + .filter((line) => line.startsWith('data:')) + .map((line) => JSON.parse(line.slice(5).trim())); + const response = messages.find((message) => message?.id === 1); + if (!response) throw new Error('Production MCP event stream contained no tools/list response.'); + return response; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + try { + const tools = await scanProductionCatalog({ apiKey: process.env.AICTRL_API_KEY }); + console.log(`Production MCP catalog smoke passed: ${tools.map((tool) => tool.name).join(', ')}.`); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/test/production-mcp-catalog.test.ts b/test/production-mcp-catalog.test.ts new file mode 100644 index 0000000..7fb539e --- /dev/null +++ b/test/production-mcp-catalog.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + EXPECTED_CATALOG, + assertProductionCatalog, + scanProductionCatalog, +} from '../scripts/smoke-production-mcp.mjs'; + +function liveShape() { + return EXPECTED_CATALOG.map((tool) => ({ + ...tool, + annotations: { ...tool.annotations }, + description: `${tool.name} description`, + inputSchema: addDescriptions(tool.inputSchema), + })); +} + +function addDescriptions(value: unknown): unknown { + if (Array.isArray(value)) return value.map(addDescriptions); + if (!value || typeof value !== 'object') return value; + return { + description: 'ignored contract documentation', + ...Object.fromEntries( + Object.entries(value).map(([key, child]) => [key, addDescriptions(child)]), + ), + }; +} + +describe('production MCP catalog smoke', () => { + it('accepts exactly the six approved tools while ignoring descriptions', () => { + expect(() => assertProductionCatalog(liveShape())).not.toThrow(); + }); + + it('rejects extra tools, schema drift, annotation drift, and missing descriptions', () => { + const extra = [...liveShape(), liveShape()[0]]; + expect(() => assertProductionCatalog(extra)).toThrow(/differs/); + + const schemaDrift = liveShape(); + schemaDrift[2].inputSchema.properties.idempotency_key.minLength = 1; + expect(() => assertProductionCatalog(schemaDrift)).toThrow(/differs/); + + const annotationDrift = liveShape(); + annotationDrift[4].annotations.destructiveHint = false; + expect(() => assertProductionCatalog(annotationDrift)).toThrow(/differs/); + + const missingDescription = liveShape(); + missingDescription[0].description = ''; + expect(() => assertProductionCatalog(missingDescription)).toThrow(/has no description/); + }); + + it('uses API-key authentication and accepts an MCP event-stream response', async () => { + const tools = liveShape(); + const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => { + expect(init.headers).toMatchObject({ 'X-API-Key': 'secret-key' }); + expect(String(init.body)).not.toContain('secret-key'); + return new Response(`event: message\ndata: ${JSON.stringify({ + jsonrpc: '2.0', id: 1, result: { tools }, + })}\n\n`, { status: 200, headers: { 'content-type': 'text/event-stream' } }); + }); + + await expect(scanProductionCatalog({ + apiKey: 'secret-key', + fetchImpl: fetchImpl as typeof fetch, + })).resolves.toHaveLength(6); + expect(fetchImpl).toHaveBeenCalledOnce(); + }); + + it('fails closed when auth is absent or production rejects it', async () => { + await expect(scanProductionCatalog()).rejects.toThrow(/AICTRL_API_KEY is required/); + await expect(scanProductionCatalog({ + apiKey: 'invalid', + fetchImpl: vi.fn(async () => new Response('unauthorized', { status: 401 })) as typeof fetch, + })).rejects.toThrow(/HTTP 401/); + }); +}); From d0d972c556ea4abec654dc93a39aec088b3b6377 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 14:56:04 +0100 Subject: [PATCH 32/34] docs: record production MCP submission evidence --- submission/codex/readiness.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 9118065..fd99de5 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -29,8 +29,11 @@ - [x] The canonical resource URL from `plugins/aictrl/.mcp.json` is deployed and publicly reachable. -- [ ] Tool scan returns exactly the six workflow lifecycle tools. -- [ ] Schemas and all three annotations match deployed behavior. +- [x] The protected-key production scan returns exactly the six workflow + lifecycle tools from `https://aictrl.dev/mcp` (CI run `29420053568`, job + `87367910414`). +- [x] The same production scan matches the approved input schemas, all three + safety annotations, and non-empty tool descriptions. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and cancellation negative tests pass from a clean Codex client. - [ ] Portal content-security-policy fields contain only the exact browser-fetch From e586e579226bc3b3122bd529e087b59f454e5414 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 15:09:09 +0100 Subject: [PATCH 33/34] docs: harden Codex submission listing --- scripts/smoke-production-mcp.mjs | 6 ++ scripts/validate-public-packages.mjs | 37 +++++++++++ submission/codex/listing.md | 91 ++++++++++++++++++++++++++++ submission/codex/readiness.md | 16 +++-- test/production-mcp-catalog.test.ts | 10 ++- 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 submission/codex/listing.md diff --git a/scripts/smoke-production-mcp.mjs b/scripts/smoke-production-mcp.mjs index 8dae1de..e140496 100644 --- a/scripts/smoke-production-mcp.mjs +++ b/scripts/smoke-production-mcp.mjs @@ -116,6 +116,12 @@ export function assertProductionCatalog(tools) { if (typeof tool.description !== 'string' || tool.description.trim() === '') { throw new Error(`Production MCP tool ${tool.name ?? ''} has no description.`); } + if ( + typeof tool?._meta?.ui?.resourceUri === 'string' + || typeof tool?._meta?.['openai/outputTemplate'] === 'string' + ) { + throw new Error(`Production MCP tool ${tool.name ?? ''} unexpectedly links to custom UI.`); + } } } diff --git a/scripts/validate-public-packages.mjs b/scripts/validate-public-packages.mjs index a475343..14ed5ea 100755 --- a/scripts/validate-public-packages.mjs +++ b/scripts/validate-public-packages.mjs @@ -91,6 +91,43 @@ if ( } const testCases = readFileSync(join(root, 'submission/codex/test-cases.md'), 'utf8'); +const listing = readFileSync(join(root, 'submission/codex/listing.md'), 'utf8'); +for (const value of [ + plugin.interface?.displayName, + plugin.interface?.shortDescription, + plugin.interface?.longDescription, + plugin.interface?.developerName, + plugin.interface?.category, + plugin.interface?.websiteURL, + plugin.interface?.privacyPolicyURL, + plugin.interface?.termsOfServiceURL, + plugin.interface?.brandColor, + plugin.interface?.logo, + publicMcpUrl(), + 'https://aictrl.dev/support', +]) { + if (typeof value !== 'string' || !value || !listing.includes(`\`${value}\``)) { + errors.push(`Codex submission listing is missing the manifest-matched value: ${value}`); + } +} +for (const prompt of prompts || []) { + if (!listing.includes(`\`${prompt}\``)) { + errors.push(`Codex submission listing is missing starter prompt: ${prompt}`); + } +} +if (!listing.includes('| Browser fetch domains | None |')) { + errors.push('Codex submission listing must record that the no-custom-UI bundle has no browser fetch domains'); +} +if (existsSync(join(root, 'plugins/aictrl/.app.json'))) { + errors.push('Codex submission listing declares no custom UI, but plugins/aictrl/.app.json exists'); +} +const logoPath = join(root, 'plugins/aictrl', plugin.interface?.logo || '__missing_logo__'); +if (existsSync(logoPath)) { + const logo = readFileSync(logoPath, 'utf8'); + if (/<(?:script|foreignObject|text|filter)\b|(?:href|xlink:href)\s*=|url\s*\(/i.test(logo)) { + errors.push('Codex production logo must not contain script, external resources, embedded text, or filters'); + } +} for (const fixturePath of [ 'submission/codex/reviewer-fixture.md', 'submission/codex/fixture-template/issue.md', diff --git a/submission/codex/listing.md b/submission/codex/listing.md new file mode 100644 index 0000000..6d74ece --- /dev/null +++ b/submission/codex/listing.md @@ -0,0 +1,91 @@ +# Codex plugin listing + +Use these values in the OpenAI plugin submission portal. They mirror the +published package manifest so the portal listing and install surface do not +drift. Do not submit while any owner-only item below remains unresolved. + +## Info + +| Portal field | Value | +| --- | --- | +| Plugin name | `AICtrl` | +| Short description | `Engineering skills and controlled workflows` | +| Long description | `Use eight portable engineering skills locally, then hand implementation work to versioned AICtrl workflows with approvals, evidence, history, and policy controls.` | +| Category | `Productivity` | +| Developer name | `aictrl.dev` | +| Website | `https://aictrl.dev` | +| Support | `https://aictrl.dev/support` | +| Privacy policy | `https://aictrl.dev/privacy` | +| Terms of service | `https://aictrl.dev/terms` | +| Brand color | `#4c6ef5` | +| Logo | `./assets/icon.svg` (source: `plugins/aictrl/assets/icon.svg`) | + +The bundled logo is the canonical 64-unit indigo hex mark from the AICtrl +brand system. It has no script, external resource, embedded text, filter, +shadow, or background-dependent color. The release owner must still approve +the portal-rendered production preview before submission. + +Select the verified `aictrl.dev` business identity in the portal. Stop if that +identity is absent, does not match the public URLs above, or the submitter lacks +Apps Management write access. + +## MCP + +| Portal field | Value | +| --- | --- | +| Production MCP URL | `https://aictrl.dev/mcp` | +| Authentication | OAuth 2.1 | +| Custom UI | None | +| Browser fetch domains | None | +| Content-security-policy allowlists | Empty | + +The package contains no `.app.json`, web component, iframe, browser script, or +browser-side fetch. Do not add `aictrl.dev` or unrelated analytics domains to +the CSP merely because the MCP server or website uses them. The MCP URL and +domain challenge are separate portal fields. Stop if the portal scan discovers +a browser-fetch dependency; document that dependency and add only its exact +origin before continuing. + +After the portal provides the domain challenge, follow +`docs/public-release-runbook.md`: temporarily configure the exact value on the +production Cloud Run service, verify the well-known endpoint, and never place +the value in Git, CI, Terraform, Secret Manager, shell history, or release +evidence. + +Scan the production server and require exactly these tools: + +1. `list_workflows` +2. `get_workflow` +3. `start_workflow` +4. `get_workflow_run` +5. `approve_workflow_step` +6. `cancel_workflow_run` + +The protected-key CI scan is the source of truth for tool schemas, safety +annotations, and non-empty descriptions. + +## Skills and prompts + +Upload the final `plugins/aictrl/skills/` tree from the release commit. Use +exactly these three starter prompts from the manifest without adding portal-only +variants: + +1. `Turn this request into an implementation-ready issue.` +2. `Implement this issue and prepare a merge-ready pull request.` +3. `Review this pull request at its current head revision.` + +Upload the five positive and three negative cases from `test-cases.md` only +after every connected positive case contains the rehearsed fixture, account, +and run data described in `reviewer-fixture.md`. + +## Global and submit + +The release owner selects only countries or regions covered by current product, +support, privacy, and legal commitments. Record that decision in +`readiness.md`; do not infer availability from website reachability. + +Use `release-notes.md` for the initial-submission notes. Complete policy +attestations only after the final listing, server scan, skills, prompts, tests, +demo account, and availability decision are all accurate. Submission starts +review; after approval, the owner must explicitly publish and complete the +universal-directory smoke test. diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index fd99de5..8bd0321 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -22,8 +22,12 @@ merged through protected `main`. - [ ] Every positive connected reviewer case names the rehearsed no-MFA demo account and run data; the AICtrl connection and account remain pending. -- [ ] Final production logo is approved for the public listing. Screenshots are - optional in the current submission guidance and are not a release gate. +- [x] `listing.md` records the manifest-matched portal copy and canonical AICtrl + logo asset; the SVG has no script, external resource, embedded text, + filter, shadow, or background-dependent color. +- [ ] The release owner approves the logo as rendered in the final portal + preview. Screenshots are optional in the current submission guidance and + are not a release gate. ## MCP and OAuth @@ -36,9 +40,11 @@ safety annotations, and non-empty tool descriptions. - [ ] Dynamic registration, PKCE, client/redirect binding, replay, refresh, and cancellation negative tests pass from a clean Codex client. -- [ ] Portal content-security-policy fields contain only the exact browser-fetch - domains used by the final package (none for the current no-custom-UI bundle - unless the portal scan identifies a required domain). +- [x] `listing.md` records empty browser content-security-policy allowlists: the + final package has no `.app.json`, custom UI, iframe, browser script, or + browser-side fetch. +- [ ] The final portal scan accepts those empty allowlists and does not identify + a browser-fetch dependency. - [ ] Domain challenge is installed at the portal-provided well-known URL. - [ ] Reviewer account works without MFA, email confirmation, SMS, or private network. diff --git a/test/production-mcp-catalog.test.ts b/test/production-mcp-catalog.test.ts index 7fb539e..f6c3b05 100644 --- a/test/production-mcp-catalog.test.ts +++ b/test/production-mcp-catalog.test.ts @@ -30,7 +30,7 @@ describe('production MCP catalog smoke', () => { expect(() => assertProductionCatalog(liveShape())).not.toThrow(); }); - it('rejects extra tools, schema drift, annotation drift, and missing descriptions', () => { + it('rejects extra tools, contract drift, missing descriptions, and custom UI', () => { const extra = [...liveShape(), liveShape()[0]]; expect(() => assertProductionCatalog(extra)).toThrow(/differs/); @@ -45,6 +45,14 @@ describe('production MCP catalog smoke', () => { const missingDescription = liveShape(); missingDescription[0].description = ''; expect(() => assertProductionCatalog(missingDescription)).toThrow(/has no description/); + + const standardUi = liveShape(); + standardUi[0]._meta = { ui: { resourceUri: 'ui://aictrl/workflows.html' } }; + expect(() => assertProductionCatalog(standardUi)).toThrow(/unexpectedly links to custom UI/); + + const compatibilityUi = liveShape(); + compatibilityUi[0]._meta = { 'openai/outputTemplate': 'ui://aictrl/workflows.html' }; + expect(() => assertProductionCatalog(compatibilityUi)).toThrow(/unexpectedly links to custom UI/); }); it('uses API-key authentication and accepts an MCP event-stream response', async () => { From 45c581d5a218ce6e22491c1f552f36fd9f2e4ae3 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Wed, 15 Jul 2026 15:20:10 +0100 Subject: [PATCH 34/34] docs: record OpenAI publisher readiness --- submission/codex/readiness.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/submission/codex/readiness.md b/submission/codex/readiness.md index 8bd0321..60f31e0 100644 --- a/submission/codex/readiness.md +++ b/submission/codex/readiness.md @@ -50,8 +50,10 @@ ## Publisher and publication -- [ ] Publisher identity is verified in the owning OpenAI organization. -- [ ] Submitter has Apps Management write permission. +- [x] The release owner confirmed on 2026-07-15 that the publisher identity is + verified in the owning OpenAI organization. +- [x] The release owner confirmed on 2026-07-15 that the submitter has Apps + Management write permission in that same organization. - [ ] Availability regions and policy attestations are approved. - [ ] Plugin is submitted, approved, explicitly published, and smoke-tested from the universal plugin directory.