From 0ce8804e0b5e7b9c8cc9ede2bf605516890b8a3e Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Tue, 8 Sep 2026 08:26:16 +0200 Subject: [PATCH 1/2] feat(publish): add unscoped `relayflows` CLI package and wire it into publish.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm install -g relayflows now installs the flows CLI directly — the package is a thin wrapper that forwards to @relayflows/sdk's real CLI, resolved by filesystem path since the SDK's package exports doesn't list that subpath. Versioned and released in lockstep with @relayflows/sdk (version-packages.mjs, pack-release.mjs), and published last in publish.yml's dependency-ordered publish step, after a real `flows check` smoke test against the packed SDK tarball. Also resolves a README.md merge conflict left by a background pull of origin/main, keeping both the new Cloud review swarm section and the Get Started section it collided with. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Dhjfz8SqjdpjFrWnYagtHv --- .github/workflows/publish.yml | 65 ++++++++--- README.md | 12 ++ packages/relayflows/README.md | 20 ++++ packages/relayflows/bin/flows.js | 23 ++++ packages/relayflows/package-lock.json | 161 ++++++++++++++++++++++++++ packages/relayflows/package.json | 24 ++++ scripts/pack-release.mjs | 11 +- scripts/publish.test.mjs | 36 +++++- scripts/version-packages.mjs | 2 +- 9 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 packages/relayflows/README.md create mode 100755 packages/relayflows/bin/flows.js create mode 100644 packages/relayflows/package-lock.json create mode 100644 packages/relayflows/package.json diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 903ea473f..6b1461011 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ on: description: Package to publish (single-package selections are dry-run only) required: true type: choice - options: [all, surface, sdk, runtime-linux-x64] + options: [all, surface, sdk, runtime-linux-x64, relayflows] default: all version: description: Version bump type @@ -108,7 +108,26 @@ jobs: ./node_modules/.bin/tsc node scripts/make-cli-executable.mjs - name: Pack and assert SDK + id: sdk run: node scripts/pack-release.mjs sdk + # relayflows only re-exposes the SDK's CLI under the unscoped name, so it + # builds against the packed SDK the same way the SDK builds against the + # packed surface — and it's cheap enough to smoke-test for real here + # rather than only asserting the tarball's shape in pack-release.mjs. + - name: Build relayflows CLI wrapper against packed SDK + env: + SDK_TARBALL: ${{ steps.sdk.outputs.tarball }} + working-directory: packages/relayflows + run: | + npm install --no-save --package-lock=false --ignore-scripts "$SDK_TARBALL" + test ! -L node_modules/@relayflows/sdk + report=$(node bin/flows.js check --json ../../testdata/hello-deterministic.flow.yaml) + echo "$report" | node -e ' + const report = JSON.parse(require("fs").readFileSync(0, "utf8")); + if (report.ok !== true) { console.error(report); process.exit(1); } + ' + - name: Pack and assert relayflows CLI wrapper + run: node scripts/pack-release.mjs relayflows - name: Build relayflowd working-directory: kernel run: cargo build --locked --release -p relayflowd @@ -152,13 +171,19 @@ jobs: with: name: build-output path: dist/build-output + # relayflows is the one unscoped package, so `npm pack` names its tarball + # `relayflows-.tgz`, not `relayflows-relayflows-.tgz`. - name: Restore built packages env: NEW_VERSION: ${{ needs.build.outputs.new_version }} run: | - for package in surface sdk runtime-linux-x64; do - tar -xzf "dist/build-output/relayflows-${package}-${NEW_VERSION}.tgz" \ - --strip-components=1 -C "packages/$package" + for package in surface sdk runtime-linux-x64 relayflows; do + if [[ "$package" == relayflows ]]; then + tarball="relayflows-${NEW_VERSION}.tgz" + else + tarball="relayflows-${package}-${NEW_VERSION}.tgz" + fi + tar -xzf "dist/build-output/$tarball" --strip-components=1 -C "packages/$package" done # Repack and check EVERYTHING before the first publish. Tar archives # preserve executable bits across Actions artifact upload/download. @@ -167,18 +192,24 @@ jobs: node scripts/pack-release.mjs surface node scripts/pack-release.mjs sdk node scripts/pack-release.mjs runtime-linux-x64 - - name: Publish to NPM (surface before SDK) + node scripts/pack-release.mjs relayflows + - name: Publish to NPM (surface before SDK, relayflows last) env: PACKAGE: ${{ inputs.package }} NEW_VERSION: ${{ needs.build.outputs.new_version }} DRY_RUN: ${{ inputs.dry_run }} NPM_TAG: ${{ inputs.tag }} run: | - for package in surface sdk runtime-linux-x64; do + for package in surface sdk runtime-linux-x64 relayflows; do if [[ "$PACKAGE" != all && "$PACKAGE" != "$package" ]]; then continue; fi + if [[ "$package" == relayflows ]]; then + tarball="relayflows-${NEW_VERSION}.tgz" + else + tarball="relayflows-${package}-${NEW_VERSION}.tgz" + fi args=() if [[ "$DRY_RUN" == true ]]; then args+=(--dry-run); fi - npm publish "dist/publish/relayflows-${package}-${NEW_VERSION}.tgz" \ + npm publish "dist/publish/$tarball" \ --access public --provenance --ignore-scripts --tag "$NPM_TAG" "${args[@]}" done - name: Regenerate release lockfiles @@ -188,17 +219,24 @@ jobs: run: | npm install --prefix packages/surface --package-lock-only --ignore-scripts npm install --prefix packages/sdk --package-lock-only --ignore-scripts --save-exact "@relayflows/surface@$NEW_VERSION" + npm install --prefix packages/relayflows --package-lock-only --ignore-scripts --save-exact "@relayflows/sdk@$NEW_VERSION" node --input-type=module - <<'NODE' import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; - const lock = JSON.parse(readFileSync('packages/sdk/package-lock.json', 'utf8')); - const surface = lock.packages['node_modules/@relayflows/surface']; - assert.equal(surface.version, process.env.NEW_VERSION); - assert.match(surface.resolved, /^https:\/\/registry\.npmjs\.org\//); - assert(!surface.link, 'release lockfile must resolve the published surface'); + const version = process.env.NEW_VERSION; + const check = (lockPath, depNodeModulesPath) => { + const lock = JSON.parse(readFileSync(lockPath, 'utf8')); + const dep = lock.packages[depNodeModulesPath]; + assert.equal(dep.version, version); + assert.match(dep.resolved, /^https:\/\/registry\.npmjs\.org\//); + assert(!dep.link, `release lockfile must resolve the published dependency (${lockPath})`); + }; + check('packages/sdk/package-lock.json', 'node_modules/@relayflows/surface'); + check('packages/relayflows/package-lock.json', 'node_modules/@relayflows/sdk'); NODE npm ci --prefix packages/surface --dry-run --ignore-scripts npm ci --prefix packages/sdk --dry-run --ignore-scripts + npm ci --prefix packages/relayflows --dry-run --ignore-scripts - name: Commit version bump and create tag if: ${{ !inputs.dry_run }} env: @@ -209,7 +247,8 @@ jobs: git config user.email 'actions@github.com' git add packages/surface/package.json packages/surface/package-lock.json \ packages/sdk/package.json packages/sdk/package-lock.json \ - packages/runtime-linux-x64/package.json + packages/runtime-linux-x64/package.json \ + packages/relayflows/package.json packages/relayflows/package-lock.json if ! git diff --staged --quiet; then git commit -m "chore(release): v${NEW_VERSION}" fi diff --git a/README.md b/README.md index 257773825..7faa8f2e6 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,15 @@ resolver that picks a messaging workspace, while the API key authenticates `POST /api/v1/workflows/prepare` — which `--sync-code` requires, and `--sync-code` is how the swarm receives the PR diff. Given no API key, the CLI falls back to an interactive device login that no runner can approve and exits after the grant expires. + +# Get Started + +Installation: +``` +npm install -g relayflows +``` + +Give your agent a skill to write a flow: +``` +npx skills add https://github.com/agentworkforce/skills --skill writing-relayflows +``` diff --git a/packages/relayflows/README.md b/packages/relayflows/README.md new file mode 100644 index 000000000..54888a9d3 --- /dev/null +++ b/packages/relayflows/README.md @@ -0,0 +1,20 @@ +# relayflows + +The `flows` CLI, published under the unscoped name so the install is just: + +```sh +npm install -g relayflows +``` + +This package carries no logic of its own. It declares `@relayflows/sdk` as a +normal dependency and `bin/flows.js` forwards argv and stdio straight to that +dependency's `dist/cli.js`, resolved by filesystem path from this package's +own `node_modules` rather than by import specifier — the SDK's package +`exports` doesn't list that subpath, so a bare `import` would be refused. + +Versioned and released in lockstep with `@relayflows/sdk`: the two always +carry the same version number, and this package pins its dependency to that +exact version rather than a range, so `npm install -g relayflows` always +resolves the SDK build it shipped with. + +See `@relayflows/sdk` and `docs/SURFACE.md` for what the CLI actually does. diff --git a/packages/relayflows/bin/flows.js b/packages/relayflows/bin/flows.js new file mode 100755 index 000000000..585c4bf1d --- /dev/null +++ b/packages/relayflows/bin/flows.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Not `import '@relayflows/sdk/dist/cli.js'`: that subpath isn't in the +// SDK's package "exports", so specifier resolution would refuse it. Locating +// the installed dependency's real CLI file directly sidesteps that — this +// package's only job is finding it and forwarding argv/stdio. +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const sdkCli = join(packageRoot, 'node_modules', '@relayflows', 'sdk', 'dist', 'cli.js'); + +if (!existsSync(sdkCli)) { + process.stderr.write( + `relayflows: could not find @relayflows/sdk at ${sdkCli}\n` + + 'Reinstall with `npm install -g relayflows`.\n', + ); + process.exit(1); +} + +const result = spawnSync(process.execPath, [sdkCli, ...process.argv.slice(2)], { stdio: 'inherit' }); +process.exit(result.status ?? 1); diff --git a/packages/relayflows/package-lock.json b/packages/relayflows/package-lock.json new file mode 100644 index 000000000..1e787f6d8 --- /dev/null +++ b/packages/relayflows/package-lock.json @@ -0,0 +1,161 @@ +{ + "name": "relayflows", + "version": "2.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "relayflows", + "version": "2.0.1", + "license": "Apache-2.0", + "dependencies": { + "@relayflows/sdk": "2.0.1" + }, + "bin": { + "flows": "bin/flows.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@relayflows/sdk": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@relayflows/sdk/-/sdk-2.0.1.tgz", + "integrity": "sha512-veI1dFVBXtSTrOjKrUJ6mv4mHUtt005SoAHoIq5st3ZmOEDLuiJEp+r0G1dum5z7wgFngCJI0WHOy0UD1jSGPw==", + "license": "Apache-2.0", + "dependencies": { + "@relayflows/surface": "2.0.1", + "@types/js-yaml": "^4.0.9", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "js-yaml": "^5.4.1", + "yaml": "^2.5.1" + }, + "bin": { + "flows": "dist/cli.js" + } + }, + "node_modules/@relayflows/surface": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@relayflows/surface/-/surface-2.0.1.tgz", + "integrity": "sha512-I8ItRJhUHSfW8Tl33SvgExc7AAmtD4Qg/IiMBLOzg7BucZ5nOJfqvsaibfM03C7krBT+HI7ok3sg2VcCK+eyBA==", + "license": "Apache-2.0" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "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/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "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/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", + "funding": [ + { + "type": "github", + "url": "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/sponsors/puzrin" + }, + { + "type": "github", + "url": "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/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "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/sponsors/eemeli" + } + } + } +} diff --git a/packages/relayflows/package.json b/packages/relayflows/package.json new file mode 100644 index 000000000..dd7aa0c5b --- /dev/null +++ b/packages/relayflows/package.json @@ -0,0 +1,24 @@ +{ + "name": "relayflows", + "version": "2.0.1", + "description": "Relayflow v2 CLI. Installs the `flows` command — a thin wrapper around @relayflows/sdk, published under the unscoped name for `npm install -g relayflows`.", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/flows.git", + "directory": "packages/relayflows" + }, + "files": [ + "bin" + ], + "bin": { + "flows": "./bin/flows.js" + }, + "dependencies": { + "@relayflows/sdk": "2.0.1" + }, + "engines": { + "node": ">=20" + } +} diff --git a/scripts/pack-release.mjs b/scripts/pack-release.mjs index 0808748ef..09a583c56 100644 --- a/scripts/pack-release.mjs +++ b/scripts/pack-release.mjs @@ -5,7 +5,10 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; const [name, output = 'dist/publish'] = process.argv.slice(2); -assert(['surface', 'sdk', 'runtime-linux-x64'].includes(name), 'unknown release package'); +assert(['surface', 'sdk', 'runtime-linux-x64', 'relayflows'].includes(name), 'unknown release package'); +// Every release package is scoped (@relayflows/) except the CLI alias, +// which is published unscoped so `npm install -g relayflows` names it directly. +const expectedName = name === 'relayflows' ? 'relayflows' : `@relayflows/${name}`; const directory = resolve(`packages/${name}`); const destination = resolve(output); mkdirSync(destination, { recursive: true }); @@ -18,7 +21,7 @@ try { execFileSync('tar', ['-xzf', archive, '-C', unpacked]); const root = join(unpacked, 'package'); const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); - assert.equal(pkg.name, `@relayflows/${name}`); + assert.equal(pkg.name, expectedName); const expected = JSON.parse(readFileSync('packages/sdk/package.json', 'utf8')).version; assert.equal(pkg.version, expected, 'package version differs from SDK anchor'); for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { @@ -29,7 +32,9 @@ try { } const required = name === 'runtime-linux-x64' ? ['bin/relayflowd', 'bin/flows'] - : ['dist/index.js', 'dist/index.d.ts']; + : name === 'relayflows' + ? ['bin/flows.js'] + : ['dist/index.js', 'dist/index.d.ts']; if (name === 'surface') required.push('dist/runtime.js', 'dist/runtime.d.ts'); if (name === 'sdk') required.push('dist/cli.js'); for (const file of required) { diff --git a/scripts/publish.test.mjs b/scripts/publish.test.mjs index a32db7e54..a303bf6fa 100644 --- a/scripts/publish.test.mjs +++ b/scripts/publish.test.mjs @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; @@ -11,7 +11,7 @@ const read = (path) => JSON.parse(readFileSync(path, 'utf8')); function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'flows-publish-test-')); t.after(() => rmSync(root, { recursive: true, force: true })); - for (const name of ['surface', 'sdk', 'runtime-linux-x64']) { + for (const name of ['surface', 'sdk', 'runtime-linux-x64', 'relayflows']) { mkdirSync(join(root, 'packages', name), { recursive: true }); const path = join(root, 'packages', name, 'package.json'); cpSync(`packages/${name}/package.json`, path); @@ -47,7 +47,7 @@ test('one SDK anchor rewrites all internal dependency types and preserves extern writeFileSync(path, JSON.stringify(pkg)); const result = version(root, { CUSTOM_VERSION: '3.0.0-rc.2' }); assert.equal(result.status, 0, result.stderr); - for (const name of ['sdk', 'surface', 'runtime-linux-x64']) { + for (const name of ['sdk', 'surface', 'runtime-linux-x64', 'relayflows']) { assert.equal(read(join(root, 'packages', name, 'package.json')).version, '3.0.0-rc.2'); } const updated = read(path); @@ -56,6 +56,8 @@ test('one SDK anchor rewrites all internal dependency types and preserves extern } assert.equal(updated.optionalDependencies['@relayflows/runtime-linux-x64'], '3.0.0-rc.2'); assert.equal(updated.dependencies.yaml, pkg.dependencies.yaml); + const relayflows = read(join(root, 'packages/relayflows/package.json')); + assert.equal(relayflows.dependencies['@relayflows/sdk'], '3.0.0-rc.2'); }); test('prerelease bumps use the SDK anchor and output the resolved version', (t) => { @@ -101,6 +103,34 @@ test('actual npm tarballs reject missing dist and local dependencies, then accep assert.match(built.stdout, /PACK_OK @relayflows\/surface@2.0.0/); }); +test('relayflows tarball publishes unscoped and rejects a missing bin', (t) => { + const root = fixture(t); + // fixture() pins every package to '2.0.0' independently; align relayflows's + // committed dependency pin the way version-packages.mjs would for a real + // release, so this test exercises the bin/executable checks, not the + // (separately real) local-dependency-drift assertion. + const path = join(root, 'packages/relayflows/package.json'); + const pkg = read(path); + pkg.dependencies['@relayflows/sdk'] = '2.0.0'; + writeFileSync(path, JSON.stringify(pkg)); + const run = () => spawnSync(process.execPath, [packScript, 'relayflows'], { cwd: root, encoding: 'utf8' }); + const missing = run(); + assert.notEqual(missing.status, 0); + assert.match(missing.stderr, /missing package\/bin\/flows.js/); + const bin = join(root, 'packages/relayflows/bin'); + mkdirSync(bin); + writeFileSync(join(bin, 'flows.js'), '#!/usr/bin/env node\n'); + const built = run(); + // Not yet executable: same failure pack-release.mjs gives for surface/sdk/runtime. + assert.notEqual(built.status, 0); + assert.match(built.stderr, /non-executable \.\/bin\/flows\.js/); + execFileSync('chmod', ['+x', join(bin, 'flows.js')]); + const executable = run(); + assert.equal(executable.status, 0, executable.stderr); + // Unscoped — not `@relayflows/relayflows`, unlike every other release package. + assert.match(executable.stdout, /PACK_OK relayflows@2.0.0/); +}); + test('runtime tarball refuses an unstaged binary package', (t) => { const root = fixture(t); const result = spawnSync(process.execPath, [packScript, 'runtime-linux-x64'], { diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index 47a518cec..99d696d5e 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; // The SDK is the version anchor; no package independently computes a bump. -const paths = ['surface', 'sdk', 'runtime-linux-x64'].map((name) => `packages/${name}/package.json`); +const paths = ['surface', 'sdk', 'runtime-linux-x64', 'relayflows'].map((name) => `packages/${name}/package.json`); if (process.env.CUSTOM_VERSION && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(process.env.CUSTOM_VERSION)) { throw new Error('custom_version must be a semantic version'); } From f0261bad4646542294d3171c2b5c1f12aa9917bb Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Tue, 8 Sep 2026 08:28:07 +0200 Subject: [PATCH 2/2] tweak readme --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index 7faa8f2e6..cccf073af 100644 --- a/README.md +++ b/README.md @@ -38,26 +38,6 @@ can run autonomously over days and weeks. Every agent session is observable and - Pull request review pipeline with different agents looking at the pull request from different angles (security, optimization etc) and agents communicate when needed to reach consensus — [`examples/pr-review-pipeline/`](examples/pr-review-pipeline/) - Dependency upgrade bot: deterministic check flags a dependency out of date which fires an agent who does the upgrade in a sandbox. This upgrade is gated on another agent verifying the entire application with computer use in another sandbox. If completely verified a pull request is opened up — [`examples/dependency-upgrade-bot/`](examples/dependency-upgrade-bot/) -## Cloud review swarm - -Every pull request launches the cloud review swarm. Repository administrators -must configure two Actions secrets. The workflow fails during preflight, in -seconds and before submitting a run, when either is absent. - -| Secret | What it is | How to obtain it | -|---|---|---| -| `RELAY_WORKSPACE_KEY` | Selects the messaging workspace the swarm runs in. | `agent-relay workspace key --reveal-secrets` | -| `CLOUD_API_KEY` | The Cloud API key for workflow invocation. | Follow `AgentWorkforce/cloud` → `docs/runbooks/relay-ci-workflow-credential.md`, profile `workflow-invoke` | - -`CLOUD_API_URL` is not secret; the workflow defaults it and it can be -overridden with a repository variable of the same name. - -A workspace key alone cannot run the swarm. `agent-relay cloud run` authenticates -to the Cloud API using `CLOUD_API_KEY`: the workspace key is read only by the -resolver that picks a messaging workspace, while the API key authenticates -`POST /api/v1/workflows/prepare` — which `--sync-code` requires, and `--sync-code` -is how the swarm receives the PR diff. Given no API key, the CLI falls back to an -interactive device login that no runner can approve and exits after the grant expires. # Get Started