diff --git a/docs/CLOUD.md b/docs/CLOUD.md index d0418c1d..2ae19c83 100644 --- a/docs/CLOUD.md +++ b/docs/CLOUD.md @@ -455,9 +455,12 @@ decides which form is meant. `contains`, `events`, and the pull-request subscription opt-outs `reviews`, `checks`, `comments`), `gitlab` (`project`, `labels`, `contains`, `events`), `slack` (`channel`, `contains`), `linear` (`team`, `project`, `labels`, -`contains`), `jira` (`project`, `labels`, `contains`) or `shortcut` +`contains`, `events`), `jira` (`project`, `labels`, `contains`) or `shortcut` (`workspace`, `team`, `labels`, `contains`), each at most once. A Linear -`team` matches the team's name or its key. A GitHub source without +`team` matches the team's name or its key. A Linear `events` is `issues` (the +default — `issue.create`), `assigned` — `AppUserNotification.issueAssignedToYou`, +issues assigned to the connected app user, so assigning a ticket delegates it — +or `all` for both. A GitHub source without `repository` is scoped to `--repo`. `events` is `issues` (the default: `issues.opened` and `issues.labeled`) or `pull_request` — `merge_request` for `gitlab` — which wakes on a pull request being diff --git a/examples/README.md b/examples/README.md index 961b5fc0..4b91e985 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,7 @@ flow straight from this repo, shows its steps, and asks you to connect whatever | Flow | Trigger | Deploy | |---|---|---| | [pr-review-pipeline](pr-review-pipeline/) — three review lenses in parallel, then a consensus pass | every `pull_request` | [![Deploy Flow](https://agentrelay.com/launch-agent_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows%2Fblob%2Fmain%2Fexamples%2Fpr-review-pipeline%2Fpr-review-pipeline.flow.ts&on=github%3Aevents%3Dpull_request) | -| [software-factory](software-factory/) — implement → test → adversarial review → open a PR | every Linear issue in team `ENG` | [![Deploy Flow](https://agentrelay.com/launch-agent_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows%2Fblob%2Fmain%2Fexamples%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear%3Ateam%3DENG) | +| [software-factory](software-factory/) — implement → test → adversarial review → open a PR | Linear issues assigned to the connected app user | [![Deploy Flow](https://agentrelay.com/launch-agent_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows%2Fblob%2Fmain%2Fexamples%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear%3Aevents%3Dassigned) | | [stale-issues](stale-issues/) — triage every open issue, post a Slack digest | schedule (`flows schedule … --cron "0 9 * * 1-5"`) | — | Or from a checkout: `flows deploy --repo --on --approver `. diff --git a/examples/software-factory/README.md b/examples/software-factory/README.md index 6873559a..63b7012a 100644 --- a/examples/software-factory/README.md +++ b/examples/software-factory/README.md @@ -1,8 +1,8 @@ # software-factory -[![Deploy Flow](https://agentrelay.com/launch-agent_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows%2Fblob%2Fmain%2Fexamples%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear) +[![Deploy Flow](https://agentrelay.com/launch-agent_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows%2Fblob%2Fmain%2Fexamples%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear%3Aevents%3Dassigned) -One click deploys this flow to [Agent Relay Cloud](https://agentrelay.com/cloud), running on new Linear issues — set your team on the deploy page. +One click deploys this flow to [Agent Relay Cloud](https://agentrelay.com/cloud), running when a Linear issue is assigned to the connected app user — set your team on the deploy page. A ticket becomes a pull request: implementation agent → deterministic tests → adversarial review agent → PR opened for a human. The review verdict is a file @@ -12,16 +12,19 @@ neither can be talked into a green result. ```sh flows check examples/software-factory/software-factory.flow.ts flows deploy examples/software-factory/software-factory.flow.ts \ - --repo acme/api --on linear:team=TECH --approver you + --repo acme/api --on linear:events=assigned,team=TECH --approver you flows deployments ``` `--on` also takes `github:labels=agent`, `jira:project=OPS`, `shortcut:workspace=…` -or `slack:channel=#eng`, and `linear` accepts `team`, `project`, `labels` and -`contains` (`linear:team=TECH,labels=agent`); `team` matches the Linear team -name or its key, so `team=Engineering` and `team=TECH` are the same filter. The -deploy page shows every filter as an editable field — the badge only fixes the -provider; team, project and labels are filled in there. +or `slack:channel=#eng`, and `linear` accepts `team`, `project`, `labels`, +`contains` and `events` (`linear:team=TECH,labels=agent`); `team` matches the +Linear team name or its key, so `team=Engineering` and `team=TECH` are the same +filter. `events` is `issues` (the default — new issues), `assigned` (issues +assigned to the connected app user — the badge's choice, so assigning a ticket +delegates it), or `all` for both. The deploy page shows every filter as an +editable field — the badge only fixes the provider and `assigned`; team, +project and labels are filled in there. Each matching ticket launches one Cloud run in a fresh `relayflow/software-factory-` branch of `--repo`; a passing review opens a diff --git a/packages/sdk/src/cloud-deploy.ts b/packages/sdk/src/cloud-deploy.ts index 1339e1b4..d132039c 100644 --- a/packages/sdk/src/cloud-deploy.ts +++ b/packages/sdk/src/cloud-deploy.ts @@ -34,7 +34,9 @@ const PROVIDER_SETTINGS: Record = { github: ['repository', 'labels', 'contains', 'events', 'reviews', 'checks', 'comments'], gitlab: ['project', 'labels', 'contains', 'events'], slack: ['channel', 'contains'], - linear: ['team', 'project', 'labels', 'contains'], + // `events`: `issues` (default), `assigned` (issues assigned to the agent's + // connected app user), or `all` — which Linear records wake the listener. + linear: ['team', 'project', 'labels', 'contains', 'events'], jira: ['project', 'labels', 'contains'], shortcut: ['workspace', 'team', 'labels', 'contains'], }; @@ -137,7 +139,10 @@ export function parseTriggerSource(value: string): FlowTriggerSource { if (key === 'events') { // Cloud's enum is lowercase; send it that way whatever the shell typed. const events = setting.toLowerCase(); - const valid = provider === 'gitlab' ? ['issues', 'merge_request'] : ['issues', 'pull_request']; + const valid = + provider === 'gitlab' ? ['issues', 'merge_request'] + : provider === 'linear' ? ['issues', 'assigned', 'all'] + : ['issues', 'pull_request']; if (!valid.includes(events)) { throw new CloudFlowError('invalid_input', `${provider} events must be ${valid.map(v => `"${v}"`).join(' or ')}, got "${setting}".`); diff --git a/packages/sdk/tests/cloud-deploy.test.ts b/packages/sdk/tests/cloud-deploy.test.ts index 87d641e7..badce1de 100644 --- a/packages/sdk/tests/cloud-deploy.test.ts +++ b/packages/sdk/tests/cloud-deploy.test.ts @@ -64,6 +64,8 @@ describe('trigger source and repository parsing', () => { ['slack:channel=#eng', { provider: 'slack', settings: { channel: '#eng' } }], ['linear:team=ENG', { provider: 'linear', settings: { team: 'ENG' } }], ['linear:team=Engineering,labels=agent,contains=urgent', { provider: 'linear', settings: { team: 'Engineering', labels: 'agent', contains: 'urgent' } }], + ['linear:events=assigned', { provider: 'linear', settings: { events: 'assigned' } }], + ['linear:events=ALL,team=TECH', { provider: 'linear', settings: { events: 'all', team: 'TECH' } }], ['jira:project=OPS,labels=agent', { provider: 'jira', settings: { project: 'OPS', labels: 'agent' } }], ['shortcut:workspace=acme,team=Platform', { provider: 'shortcut', settings: { workspace: 'acme', team: 'Platform' } }], ['gitlab:project=acme/platform/web', { provider: 'gitlab', settings: { project: 'acme/platform/web' } }], @@ -75,7 +77,7 @@ describe('trigger source and repository parsing', () => { expect(parseTriggerSource(value)).toEqual(expected); }); - it.each(['github:channel=x', 'github:labels=', 'github:labels=a,labels=b', 'slack:labels=x', 'github:events=releases', 'gitlab:events=pull_request', 'gitlab:repository=acme/web', 'github:reviews=maybe', 'linear:events=issues']) + it.each(['github:channel=x', 'github:labels=', 'github:labels=a,labels=b', 'slack:labels=x', 'github:events=releases', 'gitlab:events=pull_request', 'gitlab:repository=acme/web', 'github:reviews=maybe', 'linear:events=mentions', 'linear:events=pull_request']) ('refuses %s', (value) => { expect(() => parseTriggerSource(value)).toThrow(expect.objectContaining({ code: 'invalid_input' })); });