From 984eb7b75805a80207014bc1a17cdcda572f2189 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Thu, 8 Oct 2026 12:06:23 -0700 Subject: [PATCH 1/4] feat(flows): serve the generated Software Garden from the catalog (2/2 for #174) Point recommended-flow catalog flows.0 (software-factory, "Software Garden") at the published generator artifact, AgentWorkforce/agentrelay.com@e3f5442 web/public/flows/software-garden/v1.flow.ts (sha256 b89a36e2...), instead of flows examples/software-factory v2.0.42. Cloud's wizard, dashboard and the catalog API now serve the same bytes /flows onboarding hands off. The entry's version moves 4 -> 5 for the new source; catalogVersion stays 4. - verify-recommended-flow-catalog: a generated agentrelay.com source has no release tag; its release names a manifest version (software-garden-v) whose recorded and local digests must equal the pin, and the raw fetch at the pinned commit is still hashed. - recommended-flow-contract: the title / exact closing-reference guard now checks the generated code that implements it. - Tests: the artifact satisfies the contract, and the catalog pins a published version by path, release and digest. Co-Authored-By: Claude Opus 5.5 (1M context) Session-Id: eae07e25-3482-4ffc-9475-760e93b49aeb --- web/content/docs/relayflows/recommended.mdx | 8 ++--- web/data/recommended-flow-catalog.v1.json | 16 ++++----- web/lib/test/recommended-flow-catalog.test.ts | 18 +++++----- web/lib/test/software-garden-artifact.test.ts | 21 ++++++++++++ web/scripts/recommended-flow-contract.mjs | 16 +++++---- .../verify-recommended-flow-catalog.mjs | 33 ++++++++++++++----- 6 files changed, 76 insertions(+), 36 deletions(-) diff --git a/web/content/docs/relayflows/recommended.mdx b/web/content/docs/relayflows/recommended.mdx index e4f33afe..40fa01ef 100644 --- a/web/content/docs/relayflows/recommended.mdx +++ b/web/content/docs/relayflows/recommended.mdx @@ -9,7 +9,7 @@ Recommended flows are maintained starting points. The catalog is public, version Software Garden is the display name of the first recommended flow. Its stable catalog and authored flow ID is `software-factory`: a GitHub issue starts implementation, deterministic repository checks, adversarial review, and a pull request for a human decision. -The released source uses Claude Code for implementation and review, so the Software Garden entry allows and defaults only that harness. A future catalog version can point at a new released source with a different requirement; clients do not rewrite the authored flow. +Its source is the one the [/flows](/flows) onboarding generates for GitHub issues, Claude Code and the Traditional workflow, published by agentrelay.com as a versioned, SHA-256-pinned file (`web/public/flows/software-garden/v.flow.ts`, recorded in its `manifest.json`). Every surface that deploys Software Garden deploys those same bytes. That source uses Claude Code for implementation and review, so the entry allows and defaults only that harness. A future catalog version can point at a newer published version with a different requirement; clients do not rewrite the flow. Software Garden currently supports GitHub repositories. Deploy one listener per repository using the direct-source API below. @@ -31,13 +31,13 @@ GET https://agentrelay.com/api/v1/flows/catalog/babysitter The list response starts with `schemaVersion: 1` and `catalogVersion: 4`. Every entry has a stable `id`, its own numeric `version`, display copy, repository-host support, and an immutable source reference. Flow entries carry trigger defaults and activation inputs. Extension entries instead name their base flow and plugin contract. An extension is not activatable unless its gate is `ready` and every declared dependency has merge and deployment evidence. -The catalog does not copy or generate the flow body. `source` names the canonical `AgentWorkforce/flows` owner, repository, path, release tag, full commit SHA, GitHub blob and raw URLs, media type, and SHA-256 content digest. Both URLs contain the commit SHA, never a mutable branch: +The catalog does not copy the flow body. `source` names the GitHub owner, repository, path, release, full commit SHA, blob and raw URLs, media type, and SHA-256 content digest. Both URLs contain the commit SHA, never a mutable branch. For Software Garden the release is the published version (`software-garden-v1`) and the path is that version's file in `AgentWorkforce/agentrelay.com`: ```text -https://github.com/AgentWorkforce/flows/blob/8b33ebab8347514f80d9da5a81206a087f641714/examples/software-factory/software-factory.flow.ts +https://github.com/AgentWorkforce/agentrelay.com/blob//web/public/flows/software-garden/v1.flow.ts ``` -CI and the production release workflow resolve the release tag, fetch the pinned raw file with a size bound, and verify the digest. A broken, moved, mutable, or drifted source therefore fails before the catalog can ship. For the direct-source deploy API, the client downloads `source.rawUrl`, verifies the bytes against `source.sha256`, and submits that source text; Cloud stores it as the deployment's source snapshot. +CI and the production release workflow resolve the release (a tag for an `AgentWorkforce/flows` source, the manifest entry for a published Software Garden version), fetch the pinned raw file with a size bound, and verify the digest. A broken, moved, mutable, or drifted source therefore fails before the catalog can ship. For the direct-source deploy API, the client downloads `source.rawUrl`, verifies the bytes against `source.sha256`, and submits that source text; Cloud stores it as the deployment's source snapshot. ## Activation contract diff --git a/web/data/recommended-flow-catalog.v1.json b/web/data/recommended-flow-catalog.v1.json index d097b8fe..8aca213d 100644 --- a/web/data/recommended-flow-catalog.v1.json +++ b/web/data/recommended-flow-catalog.v1.json @@ -5,7 +5,7 @@ { "id": "software-factory", "kind": "flow", - "version": 4, + "version": 5, "name": "Software Garden", "summary": "Turn GitHub issues into reviewed, tested pull requests.", "description": "Implement a ticket, run the repository checks outside the agent, put the change through adversarial review, and open a pull request for a human decision.", @@ -61,14 +61,14 @@ "source": { "kind": "github", "owner": "AgentWorkforce", - "repo": "flows", - "path": "examples/software-factory/software-factory.flow.ts", - "release": "v2.0.42", - "ref": "3c58ee16d10a9e2400db980f5bbafac84e437f20", - "url": "https://github.com/AgentWorkforce/flows/blob/3c58ee16d10a9e2400db980f5bbafac84e437f20/examples/software-factory/software-factory.flow.ts", - "rawUrl": "https://raw.githubusercontent.com/AgentWorkforce/flows/3c58ee16d10a9e2400db980f5bbafac84e437f20/examples/software-factory/software-factory.flow.ts", + "repo": "agentrelay.com", + "path": "web/public/flows/software-garden/v1.flow.ts", + "release": "software-garden-v1", + "ref": "e3f5442652edd7651225fd1f5435f0e65378ec4b", + "url": "https://github.com/AgentWorkforce/agentrelay.com/blob/e3f5442652edd7651225fd1f5435f0e65378ec4b/web/public/flows/software-garden/v1.flow.ts", + "rawUrl": "https://raw.githubusercontent.com/AgentWorkforce/agentrelay.com/e3f5442652edd7651225fd1f5435f0e65378ec4b/web/public/flows/software-garden/v1.flow.ts", "mediaType": "text/typescript", - "sha256": "4339c0c45a4fc928092a3e32275c061000887ed95acdc2f898966c35aca91a2d" + "sha256": "b89a36e2cf9054036a7fc53fba20011a42ef0275ffbaaa907b080738ab46ad27" } }, { diff --git a/web/lib/test/recommended-flow-catalog.test.ts b/web/lib/test/recommended-flow-catalog.test.ts index 0917c4ee..d4450993 100644 --- a/web/lib/test/recommended-flow-catalog.test.ts +++ b/web/lib/test/recommended-flow-catalog.test.ts @@ -15,7 +15,7 @@ describe('recommended flow catalog', () => { flows: [{ id: 'software-factory', kind: 'flow', - version: 4, + version: 5, name: 'Software Garden', summary: expect.any(String), description: expect.any(String), @@ -64,14 +64,14 @@ describe('recommended flow catalog', () => { source: { kind: 'github', owner: 'AgentWorkforce', - repo: 'flows', - path: 'examples/software-factory/software-factory.flow.ts', - release: 'v2.0.42', - ref: '3c58ee16d10a9e2400db980f5bbafac84e437f20', - url: 'https://github.com/AgentWorkforce/flows/blob/3c58ee16d10a9e2400db980f5bbafac84e437f20/examples/software-factory/software-factory.flow.ts', - rawUrl: 'https://raw.githubusercontent.com/AgentWorkforce/flows/3c58ee16d10a9e2400db980f5bbafac84e437f20/examples/software-factory/software-factory.flow.ts', + repo: 'agentrelay.com', + path: 'web/public/flows/software-garden/v1.flow.ts', + release: 'software-garden-v1', + ref: 'e3f5442652edd7651225fd1f5435f0e65378ec4b', + url: 'https://github.com/AgentWorkforce/agentrelay.com/blob/e3f5442652edd7651225fd1f5435f0e65378ec4b/web/public/flows/software-garden/v1.flow.ts', + rawUrl: 'https://raw.githubusercontent.com/AgentWorkforce/agentrelay.com/e3f5442652edd7651225fd1f5435f0e65378ec4b/web/public/flows/software-garden/v1.flow.ts', mediaType: 'text/typescript', - sha256: '4339c0c45a4fc928092a3e32275c061000887ed95acdc2f898966c35aca91a2d', + sha256: 'b89a36e2cf9054036a7fc53fba20011a42ef0275ffbaaa907b080738ab46ad27', }, }, { id: 'babysitter', @@ -158,7 +158,7 @@ describe('recommended flow catalog HTTP surface', () => { await expect(detail.json()).resolves.toMatchObject({ id: 'software-factory', name: 'Software Garden', - source: { ref: '3c58ee16d10a9e2400db980f5bbafac84e437f20' }, + source: { ref: 'e3f5442652edd7651225fd1f5435f0e65378ec4b' }, extensions: [{ id: 'babysitter', activation: { state: 'blocked' } }], }); diff --git a/web/lib/test/software-garden-artifact.test.ts b/web/lib/test/software-garden-artifact.test.ts index d25305fa..ae69c148 100644 --- a/web/lib/test/software-garden-artifact.test.ts +++ b/web/lib/test/software-garden-artifact.test.ts @@ -4,6 +4,7 @@ import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; import catalog from '../../data/recommended-flow-catalog.v1.json'; import { cloudConnectionsHref, DEFAULT_FACTORY, type FactoryDraft } from '../flow-onboarding'; +import { assertRecommendedFlowSourceContract } from '../../scripts/recommended-flow-contract.mjs'; import { SOFTWARE_GARDEN_ARTIFACT_DIR, SOFTWARE_GARDEN_DRAFT, @@ -90,4 +91,24 @@ describe('published Software Garden artifact', () => { expect(SOFTWARE_GARDEN_DRAFT.sourceSettings).toEqual(entry.defaultTrigger.settings); expect(SOFTWARE_GARDEN_DRAFT).toMatchObject({ workflow: 'traditional', task: '', otherAgent: '' }); }); + + it('keeps the ticket-title and exact closing-reference contract the catalog requires', () => { + const entry = catalog.flows.find(flow => flow.id === 'software-factory')!; + expect(() => assertRecommendedFlowSourceContract(entry, softwareGardenSource())).not.toThrow(); + }); + + it('is what the catalog serves as Software Garden, pinned at a published version', () => { + const { source } = catalog.flows.find(flow => flow.id === 'software-factory')!; + const published = manifest().versions.find(entry => `web/${softwareGardenArtifactPath(entry.version)}` === source.path); + expect(published, `catalog source ${source.path} is not a published Software Garden version`).toBeDefined(); + expect(source).toMatchObject({ + kind: 'github', + owner: 'AgentWorkforce', + repo: 'agentrelay.com', + release: `software-garden-v${published!.version}`, + sha256: published!.sha256, + mediaType: 'text/typescript', + }); + expect(sha256(artifact(published!.version))).toBe(source.sha256); + }); }); diff --git a/web/scripts/recommended-flow-contract.mjs b/web/scripts/recommended-flow-contract.mjs index 25f9fd1a..264af08a 100644 --- a/web/scripts/recommended-flow-contract.mjs +++ b/web/scripts/recommended-flow-contract.mjs @@ -1,11 +1,15 @@ +// The Software Garden the catalog serves is the /flows generator's output +// (web/public/flows/software-garden, agentrelay.com#174). These are the +// generated lines that derive the pull-request title from the ticket and +// require exactly one GitHub closing reference before a PR is opened. export const SOFTWARE_FACTORY_METADATA_SNIPPETS = Object.freeze([ 'const normalizedTitle = issue.title.trim().replace(/\\s+/g, " ");', - 'const title = Array.from(normalizedTitle).slice(0, 240).join("").trim();', - '? `Fixes ${issueIdentifier}`', - 'expected="Fixes $identifier"', - 'count=$(grep -xcF "$expected"', - 'elif [ "$count" -ne 1 ]', - '--title ${shellWord(title)} --body-file ${WORK}/pr-body.md', + 'const changeTitle = Array.from(normalizedTitle).slice(0, 240).join("").trim();', + '? "Fixes " + issueIdentifier', + 'expected=\\"Fixes $identifier\\"', + 'count=$(grep -xcF \\"$expected\\"', + 'elif [ \\"$count\\" -ne 1 ]', + '" --title " + shellQuote(changeTitle) + " --body-file .relayflow/pr-body.md"', ]); export function assertRecommendedFlowSourceContract(flow, sourceText) { diff --git a/web/scripts/verify-recommended-flow-catalog.mjs b/web/scripts/verify-recommended-flow-catalog.mjs index 23a05bf6..34c579f5 100644 --- a/web/scripts/verify-recommended-flow-catalog.mjs +++ b/web/scripts/verify-recommended-flow-catalog.mjs @@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile); const catalogUrl = new URL('../data/recommended-flow-catalog.v1.json', import.meta.url); const catalog = JSON.parse(await readFile(catalogUrl, 'utf8')); const MAX_SOURCE_BYTES = 1024 * 1024; +const gardenManifest = JSON.parse(await readFile(new URL('../public/flows/software-garden/manifest.json', import.meta.url), 'utf8')); if (catalog.schemaVersion !== 1 || catalog.catalogVersion !== 4 || !Array.isArray(catalog.flows)) { throw new Error('recommended-flow catalog must be schemaVersion 1, catalogVersion 4, with a flows array'); @@ -22,7 +23,8 @@ for (const flow of catalog.flows) { } if (!/^[0-9a-f]{40}$/.test(source.ref)) throw new Error(`${flow.id}: source.ref must be a full commit SHA`); if (!/^[0-9a-f]{64}$/.test(source.sha256)) throw new Error(`${flow.id}: source.sha256 must be lowercase hex`); - if (!/^v\d+\.\d+\.\d+$/.test(source.release)) throw new Error(`${flow.id}: source.release must be a version tag`); + const generated = source.owner === 'AgentWorkforce' && source.repo === 'agentrelay.com'; + if (!generated && !/^v\d+\.\d+\.\d+$/.test(source.release)) throw new Error(`${flow.id}: source.release must be a version tag`); if (!source.path || source.path.startsWith('/') || source.path.includes('..')) throw new Error(`${flow.id}: invalid source.path`); const blobUrl = `https://github.com/${source.owner}/${source.repo}/blob/${source.ref}/${source.path}`; @@ -31,14 +33,27 @@ for (const flow of catalog.flows) { throw new Error(`${flow.id}: source URLs do not match owner/repo/ref/path`); } - const tagRef = `refs/tags/${source.release}`; - const { stdout } = await execFileAsync('git', [ - 'ls-remote', `https://github.com/${source.owner}/${source.repo}.git`, tagRef, `${tagRef}^{}`, - ], { maxBuffer: 1024 * 1024, timeout: 30_000 }); - const remoteRefs = new Map(stdout.trim().split('\n').filter(Boolean).map(line => line.split(/\s+/, 2).reverse())); - const releasedCommit = remoteRefs.get(`${tagRef}^{}`) ?? remoteRefs.get(tagRef); - if (releasedCommit !== source.ref) { - throw new Error(`${flow.id}: ${source.release} resolves to ${releasedCommit ?? 'nothing'}, not ${source.ref}`); + if (generated) { + // The generated Software Garden has no release tag. Its release names a + // version recorded in this checkout's manifest, whose published file must + // hash to the catalog's digest; the raw fetch below then proves the pinned + // commit carries those same bytes. + const published = gardenManifest.versions.find(entry => `web/public/flows/software-garden/v${entry.version}.flow.ts` === source.path); + if (!published || source.release !== `software-garden-v${published.version}` || published.sha256 !== source.sha256) { + throw new Error(`${flow.id}: ${source.path} at ${source.release} is not a published Software Garden version with sha256 ${source.sha256}`); + } + const local = createHash('sha256').update(await readFile(new URL(`../${source.path.slice('web/'.length)}`, import.meta.url))).digest('hex'); + if (local !== source.sha256) throw new Error(`${flow.id}: local ${source.path} hashes to ${local}, expected ${source.sha256}`); + } else { + const tagRef = `refs/tags/${source.release}`; + const { stdout } = await execFileAsync('git', [ + 'ls-remote', `https://github.com/${source.owner}/${source.repo}.git`, tagRef, `${tagRef}^{}`, + ], { maxBuffer: 1024 * 1024, timeout: 30_000 }); + const remoteRefs = new Map(stdout.trim().split('\n').filter(Boolean).map(line => line.split(/\s+/, 2).reverse())); + const releasedCommit = remoteRefs.get(`${tagRef}^{}`) ?? remoteRefs.get(tagRef); + if (releasedCommit !== source.ref) { + throw new Error(`${flow.id}: ${source.release} resolves to ${releasedCommit ?? 'nothing'}, not ${source.ref}`); + } } const response = await fetch(source.rawUrl, { redirect: 'follow', signal: AbortSignal.timeout(30_000) }); From f60226ff0244035300ef26b6d41127c8af52cdaa Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Thu, 8 Oct 2026 14:07:22 -0700 Subject: [PATCH 2/4] fix(flows): advance catalogVersion to 5 for the new Software Garden source Cloud's upgradeRecommendedFlowActivation refuses to move an activation onto a different source identity at the catalogVersion it was deployed from ("The catalog changed an artifact without advancing its version"). Every existing Garden activation is stamped with catalogVersion 4, so repointing the source at 4 would strand them. Serve the generated source at catalogVersion 5, and record each served source against its catalogVersion in a test so a later pin move cannot skip the bump. Co-Authored-By: Claude Opus 5.5 (1M context) Session-Id: eae07e25-3482-4ffc-9475-760e93b49aeb --- web/content/docs/relayflows/recommended.mdx | 2 +- web/data/recommended-flow-catalog.v1.json | 2 +- web/lib/test/recommended-flow-catalog.test.ts | 4 ++-- web/lib/test/software-garden-artifact.test.ts | 16 ++++++++++++++++ web/scripts/verify-catalog-gates.mjs | 4 ++-- web/scripts/verify-recommended-flow-catalog.mjs | 4 ++-- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/web/content/docs/relayflows/recommended.mdx b/web/content/docs/relayflows/recommended.mdx index 40fa01ef..646daef0 100644 --- a/web/content/docs/relayflows/recommended.mdx +++ b/web/content/docs/relayflows/recommended.mdx @@ -29,7 +29,7 @@ GET https://agentrelay.com/api/v1/flows/catalog/software-factory GET https://agentrelay.com/api/v1/flows/catalog/babysitter ``` -The list response starts with `schemaVersion: 1` and `catalogVersion: 4`. Every entry has a stable `id`, its own numeric `version`, display copy, repository-host support, and an immutable source reference. Flow entries carry trigger defaults and activation inputs. Extension entries instead name their base flow and plugin contract. An extension is not activatable unless its gate is `ready` and every declared dependency has merge and deployment evidence. +The list response starts with `schemaVersion: 1` and `catalogVersion: 5`. The catalog version advances whenever an entry's source changes, because Cloud upgrades an activation onto a different source only from an older catalog version. Every entry has a stable `id`, its own numeric `version`, display copy, repository-host support, and an immutable source reference. Flow entries carry trigger defaults and activation inputs. Extension entries instead name their base flow and plugin contract. An extension is not activatable unless its gate is `ready` and every declared dependency has merge and deployment evidence. The catalog does not copy the flow body. `source` names the GitHub owner, repository, path, release, full commit SHA, blob and raw URLs, media type, and SHA-256 content digest. Both URLs contain the commit SHA, never a mutable branch. For Software Garden the release is the published version (`software-garden-v1`) and the path is that version's file in `AgentWorkforce/agentrelay.com`: diff --git a/web/data/recommended-flow-catalog.v1.json b/web/data/recommended-flow-catalog.v1.json index 8aca213d..74f043c4 100644 --- a/web/data/recommended-flow-catalog.v1.json +++ b/web/data/recommended-flow-catalog.v1.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "catalogVersion": 4, + "catalogVersion": 5, "flows": [ { "id": "software-factory", diff --git a/web/lib/test/recommended-flow-catalog.test.ts b/web/lib/test/recommended-flow-catalog.test.ts index d4450993..9cc91be8 100644 --- a/web/lib/test/recommended-flow-catalog.test.ts +++ b/web/lib/test/recommended-flow-catalog.test.ts @@ -11,7 +11,7 @@ describe('recommended flow catalog', () => { const catalog = getRecommendedFlowCatalog(); expect(catalog).toMatchObject({ schemaVersion: 1, - catalogVersion: 4, + catalogVersion: 5, flows: [{ id: 'software-factory', kind: 'flow', @@ -142,7 +142,7 @@ describe('recommended flow catalog HTTP surface', () => { expect(list.headers.get('access-control-allow-origin')).toBe('*'); await expect(list.json()).resolves.toMatchObject({ schemaVersion: 1, - catalogVersion: 4, + catalogVersion: 5, flows: [{ id: 'software-factory' }, { id: 'babysitter', kind: 'extension', diff --git a/web/lib/test/software-garden-artifact.test.ts b/web/lib/test/software-garden-artifact.test.ts index ae69c148..4cd38130 100644 --- a/web/lib/test/software-garden-artifact.test.ts +++ b/web/lib/test/software-garden-artifact.test.ts @@ -111,4 +111,20 @@ describe('published Software Garden artifact', () => { }); expect(sha256(artifact(published!.version))).toBe(source.sha256); }); + + it('advances catalogVersion whenever the Software Garden source changes', () => { + // Cloud refuses to upgrade an activation onto a different source at the + // catalogVersion it was deployed from ("The catalog changed an artifact + // without advancing its version", upgradeRecommendedFlowActivation in + // cloud's flow-activations.ts). So every source the catalog has served + // keeps the catalogVersion it was served at; moving the pin adds a row. + const SOURCE_BY_CATALOG_VERSION: Record = { + 4: '3c58ee16d10a9e2400db980f5bbafac84e437f20:4339c0c45a4fc928092a3e32275c061000887ed95acdc2f898966c35aca91a2d', + 5: 'e3f5442652edd7651225fd1f5435f0e65378ec4b:b89a36e2cf9054036a7fc53fba20011a42ef0275ffbaaa907b080738ab46ad27', + }; + const { source } = catalog.flows.find(flow => flow.id === 'software-factory')!; + expect(new Set(Object.values(SOURCE_BY_CATALOG_VERSION)).size).toBe(Object.keys(SOURCE_BY_CATALOG_VERSION).length); + expect(Math.max(...Object.keys(SOURCE_BY_CATALOG_VERSION).map(Number)), 'record the new source here').toBe(catalog.catalogVersion); + expect(SOURCE_BY_CATALOG_VERSION[catalog.catalogVersion], 'the pinned source changed: advance catalogVersion and record it').toBe(`${source.ref}:${source.sha256}`); + }); }); diff --git a/web/scripts/verify-catalog-gates.mjs b/web/scripts/verify-catalog-gates.mjs index 7bde5f0e..4812b152 100644 --- a/web/scripts/verify-catalog-gates.mjs +++ b/web/scripts/verify-catalog-gates.mjs @@ -180,8 +180,8 @@ function validateActivationGate(gate, label, requiredDependencies) { if (pluginCatalog.version !== 3 || !Array.isArray(pluginCatalog.plugins)) { fail('flow plugin catalog must be version 3'); } -if (recommendedCatalog.schemaVersion !== 1 || recommendedCatalog.catalogVersion !== 4 || !Array.isArray(recommendedCatalog.flows)) { - fail('recommended flow catalog must be schemaVersion 1 and catalogVersion 4'); +if (recommendedCatalog.schemaVersion !== 1 || recommendedCatalog.catalogVersion !== 5 || !Array.isArray(recommendedCatalog.flows)) { + fail('recommended flow catalog must be schemaVersion 1 and catalogVersion 5'); } const plugin = pluginCatalog.plugins.find(entry => entry.name === 'babysitter'); diff --git a/web/scripts/verify-recommended-flow-catalog.mjs b/web/scripts/verify-recommended-flow-catalog.mjs index 34c579f5..48bd5bfc 100644 --- a/web/scripts/verify-recommended-flow-catalog.mjs +++ b/web/scripts/verify-recommended-flow-catalog.mjs @@ -11,8 +11,8 @@ const catalog = JSON.parse(await readFile(catalogUrl, 'utf8')); const MAX_SOURCE_BYTES = 1024 * 1024; const gardenManifest = JSON.parse(await readFile(new URL('../public/flows/software-garden/manifest.json', import.meta.url), 'utf8')); -if (catalog.schemaVersion !== 1 || catalog.catalogVersion !== 4 || !Array.isArray(catalog.flows)) { - throw new Error('recommended-flow catalog must be schemaVersion 1, catalogVersion 4, with a flows array'); +if (catalog.schemaVersion !== 1 || catalog.catalogVersion !== 5 || !Array.isArray(catalog.flows)) { + throw new Error('recommended-flow catalog must be schemaVersion 1, catalogVersion 5, with a flows array'); } for (const flow of catalog.flows) { From c28880ac39f794ad2fe8445f4b15e5c9d318d723 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Thu, 8 Oct 2026 14:08:13 -0700 Subject: [PATCH 3/4] fix(flows): check the pinned Garden against the full closing-reference contract - Require the zero-match branch (missing-github-closing-reference), so a source that accepts a PR body without its closing reference fails. - Check the bytes the catalog pins, not only the generator's output. - Scope the docs' same-bytes claim to catalog deploys and name Cloud's recovery copy (AgentWorkforce/cloud#4308). Co-Authored-By: Claude Opus 5.5 (1M context) Session-Id: eae07e25-3482-4ffc-9475-760e93b49aeb --- web/content/docs/relayflows/recommended.mdx | 2 +- web/lib/test/software-garden-artifact.test.ts | 3 +++ web/scripts/recommended-flow-contract.mjs | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web/content/docs/relayflows/recommended.mdx b/web/content/docs/relayflows/recommended.mdx index 646daef0..2d3ec79f 100644 --- a/web/content/docs/relayflows/recommended.mdx +++ b/web/content/docs/relayflows/recommended.mdx @@ -9,7 +9,7 @@ Recommended flows are maintained starting points. The catalog is public, version Software Garden is the display name of the first recommended flow. Its stable catalog and authored flow ID is `software-factory`: a GitHub issue starts implementation, deterministic repository checks, adversarial review, and a pull request for a human decision. -Its source is the one the [/flows](/flows) onboarding generates for GitHub issues, Claude Code and the Traditional workflow, published by agentrelay.com as a versioned, SHA-256-pinned file (`web/public/flows/software-garden/v.flow.ts`, recorded in its `manifest.json`). Every surface that deploys Software Garden deploys those same bytes. That source uses Claude Code for implementation and review, so the entry allows and defaults only that harness. A future catalog version can point at a newer published version with a different requirement; clients do not rewrite the flow. +Its source is the one the [/flows](/flows) onboarding generates for GitHub issues, Claude Code and the Traditional workflow, published by agentrelay.com as a versioned, SHA-256-pinned file (`web/public/flows/software-garden/v.flow.ts`, recorded in its `manifest.json`). Everything that deploys Software Garden from this catalog (Cloud's deploy wizard and dashboard, and clients of the API below) deploys those same bytes, and Cloud's bundled recovery copy, used only when the catalog or GitHub is unreachable, is pinned to the same file (AgentWorkforce/cloud#4308). That source uses Claude Code for implementation and review, so the entry allows and defaults only that harness. A future catalog version can point at a newer published version with a different requirement; clients do not rewrite the flow. Software Garden currently supports GitHub repositories. Deploy one listener per repository using the direct-source API below. diff --git a/web/lib/test/software-garden-artifact.test.ts b/web/lib/test/software-garden-artifact.test.ts index 4cd38130..29e4d321 100644 --- a/web/lib/test/software-garden-artifact.test.ts +++ b/web/lib/test/software-garden-artifact.test.ts @@ -94,6 +94,9 @@ describe('published Software Garden artifact', () => { it('keeps the ticket-title and exact closing-reference contract the catalog requires', () => { const entry = catalog.flows.find(flow => flow.id === 'software-factory')!; + // Both what the catalog serves and what the generator would publish next. + const pinned = readFileSync(path.join(web, entry.source.path.replace(/^web\//, '')), 'utf8'); + expect(() => assertRecommendedFlowSourceContract(entry, pinned)).not.toThrow(); expect(() => assertRecommendedFlowSourceContract(entry, softwareGardenSource())).not.toThrow(); }); diff --git a/web/scripts/recommended-flow-contract.mjs b/web/scripts/recommended-flow-contract.mjs index 264af08a..0f9bb96c 100644 --- a/web/scripts/recommended-flow-contract.mjs +++ b/web/scripts/recommended-flow-contract.mjs @@ -8,6 +8,7 @@ export const SOFTWARE_FACTORY_METADATA_SNIPPETS = Object.freeze([ '? "Fixes " + issueIdentifier', 'expected=\\"Fixes $identifier\\"', 'count=$(grep -xcF \\"$expected\\"', + 'if [ \\"$count\\" -eq 0 ]; then echo missing-github-closing-reference', 'elif [ \\"$count\\" -ne 1 ]', '" --title " + shellQuote(changeTitle) + " --body-file .relayflow/pr-body.md"', ]); From c44d18fbceeaeddc418276a83587673dc6aac312 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Thu, 8 Oct 2026 15:28:46 -0700 Subject: [PATCH 4/4] test(flows): let a catalog-wide bump keep the Software Garden source A catalogVersion bump that leaves the Garden unchanged repeats its source in the next row; only a source change without a new row is wrong. Co-Authored-By: Claude Opus 5.5 (1M context) Session-Id: eae07e25-3482-4ffc-9475-760e93b49aeb --- web/lib/test/software-garden-artifact.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/lib/test/software-garden-artifact.test.ts b/web/lib/test/software-garden-artifact.test.ts index 29e4d321..e5061ac3 100644 --- a/web/lib/test/software-garden-artifact.test.ts +++ b/web/lib/test/software-garden-artifact.test.ts @@ -126,7 +126,8 @@ describe('published Software Garden artifact', () => { 5: 'e3f5442652edd7651225fd1f5435f0e65378ec4b:b89a36e2cf9054036a7fc53fba20011a42ef0275ffbaaa907b080738ab46ad27', }; const { source } = catalog.flows.find(flow => flow.id === 'software-factory')!; - expect(new Set(Object.values(SOURCE_BY_CATALOG_VERSION)).size).toBe(Object.keys(SOURCE_BY_CATALOG_VERSION).length); + // A catalog-wide bump may keep this source, so a row may repeat the one + // before it; what it may not do is change the source without a new row. expect(Math.max(...Object.keys(SOURCE_BY_CATALOG_VERSION).map(Number)), 'record the new source here').toBe(catalog.catalogVersion); expect(SOURCE_BY_CATALOG_VERSION[catalog.catalogVersion], 'the pinned source changed: advance catalogVersion and record it').toBe(`${source.ref}:${source.sha256}`); });