diff --git a/.trajectories/completed/2026-05/traj_iole5zdt9orr.json b/.trajectories/completed/2026-05/traj_iole5zdt9orr.json new file mode 100644 index 000000000..fbf67cfdf --- /dev/null +++ b/.trajectories/completed/2026-05/traj_iole5zdt9orr.json @@ -0,0 +1,70 @@ +{ + "id": "traj_iole5zdt9orr", + "version": 1, + "task": { + "title": "Fix PR 831 CI conflicts" + }, + "status": "completed", + "startedAt": "2026-05-10T15:18:12.326Z", + "completedAt": "2026-05-10T15:29:41.840Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-10T15:19:15.412Z" + } + ], + "chapters": [ + { + "id": "chap_22qw7tjy9jeo", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-10T15:19:15.412Z", + "endedAt": "2026-05-10T15:29:41.840Z", + "events": [ + { + "ts": 1778426355413, + "type": "decision", + "content": "Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14: Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14", + "raw": { + "question": "Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14", + "chosen": "Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14", + "alternatives": [], + "reasoning": "PR added @agent-relay/cloud exports from the SDK, while main released packages at 6.0.14; mismatched internal package versions were breaking install/typecheck CI." + }, + "significance": "high" + }, + { + "ts": 1778426981747, + "type": "reflection", + "content": "Resolved PR 831 merge and CI failures: SDK now maps and builds cloud dependency, telemetry knows schedule event, CJS build externalizes ssh2 native addon, CLI command count matches new schedule commands. Local validation green.", + "raw": { + "focalPoints": ["merge-conflict", "typescript", "package-validation", "test-suite"], + "confidence": 0.9 + }, + "significance": "high", + "tags": [ + "focal:merge-conflict", + "focal:typescript", + "focal:package-validation", + "focal:test-suite", + "confidence:0.9" + ] + } + ] + } + ], + "retrospective": { + "summary": "Merged main into PR 831, resolved SDK package/version conflict, refreshed package-lock, fixed SDK cloud path/build configuration, added cloud schedule telemetry typing, externalized ssh2 for CJS bundling, and updated CLI bootstrap command-count test. Validated SDK check, build, workers safety, workflow reliability, package validation, imports, lint, and full npm test.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-pr831-ci-fix", + "tags": [], + "_trace": { + "startRef": "d72030fb36b6ae14bed192e30a6f4b1d2fd1f504", + "endRef": "d72030fb36b6ae14bed192e30a6f4b1d2fd1f504" + } +} diff --git a/.trajectories/completed/2026-05/traj_iole5zdt9orr.md b/.trajectories/completed/2026-05/traj_iole5zdt9orr.md new file mode 100644 index 000000000..817dd4fd8 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_iole5zdt9orr.md @@ -0,0 +1,34 @@ +# Trajectory: Fix PR 831 CI conflicts + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 10, 2026 at 05:18 PM +> **Completed:** May 10, 2026 at 05:29 PM + +--- + +## Summary + +Merged main into PR 831, resolved SDK package/version conflict, refreshed package-lock, fixed SDK cloud path/build configuration, added cloud schedule telemetry typing, externalized ssh2 for CJS bundling, and updated CLI bootstrap command-count test. Validated SDK check, build, workers safety, workflow reliability, package validation, imports, lint, and full npm test. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14 + +- **Chose:** Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14 +- **Reasoning:** PR added @agent-relay/cloud exports from the SDK, while main released packages at 6.0.14; mismatched internal package versions were breaking install/typecheck CI. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14: Resolved package merge by keeping SDK cloud dependency and aligning internal versions to 6.0.14 +- Resolved PR 831 merge and CI failures: SDK now maps and builds cloud dependency, telemetry knows schedule event, CJS build externalizes ssh2 native addon, CLI command count matches new schedule commands. Local validation green. diff --git a/.trajectories/completed/2026-05/traj_oyc528j7suvo.json b/.trajectories/completed/2026-05/traj_oyc528j7suvo.json new file mode 100644 index 000000000..f2b19e6ed --- /dev/null +++ b/.trajectories/completed/2026-05/traj_oyc528j7suvo.json @@ -0,0 +1,53 @@ +{ + "id": "traj_oyc528j7suvo", + "version": 1, + "task": { + "title": "Expose cloud workflow scheduling through Relay SDK" + }, + "status": "completed", + "startedAt": "2026-05-09T19:43:34.805Z", + "completedAt": "2026-05-09T19:44:00.107Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-09T19:43:56.286Z" + } + ], + "chapters": [ + { + "id": "chap_iwcqe1x74aoc", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-09T19:43:56.286Z", + "endedAt": "2026-05-09T19:44:00.107Z", + "events": [ + { + "ts": 1778355836288, + "type": "decision", + "content": "Expose workflow schedule helpers from @agent-relay/sdk/workflows: Expose workflow schedule helpers from @agent-relay/sdk/workflows", + "raw": { + "question": "Expose workflow schedule helpers from @agent-relay/sdk/workflows", + "chosen": "Expose workflow schedule helpers from @agent-relay/sdk/workflows", + "alternatives": [], + "reasoning": "Ricky and similar products should consume scheduling from Relay SDK instead of duplicating Cloud endpoint calls. The SDK re-exports the existing @agent-relay/cloud scheduling implementation." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Re-exported Cloud workflow schedule helpers from @agent-relay/sdk/workflows and verified SDK build plus cloud CLI schedule tests.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "repo:relay", + "tags": [], + "_trace": { + "startRef": "729a7ec321ba0860b2ff1463af159cdd0e8917cf", + "endRef": "729a7ec321ba0860b2ff1463af159cdd0e8917cf" + } +} diff --git a/.trajectories/completed/2026-05/traj_oyc528j7suvo.md b/.trajectories/completed/2026-05/traj_oyc528j7suvo.md new file mode 100644 index 000000000..01140f951 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_oyc528j7suvo.md @@ -0,0 +1,33 @@ +# Trajectory: Expose cloud workflow scheduling through Relay SDK + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 9, 2026 at 09:43 PM +> **Completed:** May 9, 2026 at 09:44 PM + +--- + +## Summary + +Re-exported Cloud workflow schedule helpers from @agent-relay/sdk/workflows and verified SDK build plus cloud CLI schedule tests. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Expose workflow schedule helpers from @agent-relay/sdk/workflows + +- **Chose:** Expose workflow schedule helpers from @agent-relay/sdk/workflows +- **Reasoning:** Ricky and similar products should consume scheduling from Relay SDK instead of duplicating Cloud endpoint calls. The SDK re-exports the existing @agent-relay/cloud scheduling implementation. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Expose workflow schedule helpers from @agent-relay/sdk/workflows: Expose workflow schedule helpers from @agent-relay/sdk/workflows diff --git a/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json b/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json new file mode 100644 index 000000000..ad8c44864 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json @@ -0,0 +1,25 @@ +{ + "id": "traj_u4ixmbqqm2y1", + "version": 1, + "task": { + "title": "Add cloud workflow schedule CLI" + }, + "status": "completed", + "startedAt": "2026-05-09T19:26:42.106Z", + "completedAt": "2026-05-09T19:29:18.024Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Added agent-relay cloud schedule/schedules commands backed by the Cloud workflow schedule API.", + "approach": "Standard approach", + "confidence": 0.84 + }, + "commits": [], + "filesChanged": [], + "projectId": "repo:relay", + "tags": [], + "_trace": { + "startRef": "f341463e0e66cf70566eff2dbb05e977e410a016", + "endRef": "f341463e0e66cf70566eff2dbb05e977e410a016" + } +} diff --git a/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.md b/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.md new file mode 100644 index 000000000..09898b00d --- /dev/null +++ b/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.md @@ -0,0 +1,14 @@ +# Trajectory: Add cloud workflow schedule CLI + +> **Status:** ✅ Completed +> **Confidence:** 84% +> **Started:** May 9, 2026 at 09:26 PM +> **Completed:** May 9, 2026 at 09:29 PM + +--- + +## Summary + +Added agent-relay cloud schedule/schedules commands backed by the Cloud workflow schedule API. + +**Approach:** Standard approach diff --git a/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.json b/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.json new file mode 100644 index 000000000..b8576fb06 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.json @@ -0,0 +1,53 @@ +{ + "id": "traj_uf8y40ewrfh0", + "version": 1, + "task": { + "title": "Address PR 831 review feedback and conflicts" + }, + "status": "completed", + "startedAt": "2026-05-09T19:59:24.197Z", + "completedAt": "2026-05-09T19:59:24.403Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-09T19:59:24.306Z" + } + ], + "chapters": [ + { + "id": "chap_edsard7458dp", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-09T19:59:24.306Z", + "endedAt": "2026-05-09T19:59:24.403Z", + "events": [ + { + "ts": 1778356764308, + "type": "decision", + "content": "Merged main instead of rebasing PR 831: Merged main instead of rebasing PR 831", + "raw": { + "question": "Merged main instead of rebasing PR 831", + "chosen": "Merged main instead of rebasing PR 831", + "alternatives": [], + "reasoning": "The user asked to fix conflicts; a merge commit preserves the already-reviewed branch history while resolving the dirty PR state against origin/main." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Merged origin/main into PR 831 and addressed scheduling review feedback around trajectory path privacy, invalid --at errors, schedule guards, and one-time schedule tests.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "repo:relay", + "tags": [], + "_trace": { + "startRef": "d66aeb21e2e6613a9bfcdc4b91a58fe49a8274d9", + "endRef": "d66aeb21e2e6613a9bfcdc4b91a58fe49a8274d9" + } +} diff --git a/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.md b/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.md new file mode 100644 index 000000000..36a5de0ea --- /dev/null +++ b/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.md @@ -0,0 +1,31 @@ +# Trajectory: Address PR 831 review feedback and conflicts + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 9, 2026 at 09:59 PM +> **Completed:** May 9, 2026 at 09:59 PM + +--- + +## Summary + +Merged origin/main into PR 831 and addressed scheduling review feedback around trajectory path privacy, invalid --at errors, schedule guards, and one-time schedule tests. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Merged main instead of rebasing PR 831 +- **Chose:** Merged main instead of rebasing PR 831 +- **Reasoning:** The user asked to fix conflicts; a merge commit preserves the already-reviewed branch history while resolving the dirty PR state against origin/main. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Merged main instead of rebasing PR 831: Merged main instead of rebasing PR 831 diff --git a/.trajectories/index.json b/.trajectories/index.json index 3184b1416..a577d252d 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-09T10:02:49.911Z", + "lastUpdated": "2026-05-10T15:29:41.965Z", "trajectories": { "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", @@ -325,6 +325,34 @@ "completedAt": "2026-05-09T08:47:54.686Z", "path": ".trajectories/completed/2026-05/traj_lieyyspidhfj.json", "compactedInto": "compact_iueavxbs31gn" + }, + "traj_u4ixmbqqm2y1": { + "title": "Add cloud workflow schedule CLI", + "status": "completed", + "startedAt": "2026-05-09T19:26:42.106Z", + "completedAt": "2026-05-09T19:29:18.024Z", + "path": ".trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json" + }, + "traj_oyc528j7suvo": { + "title": "Expose cloud workflow scheduling through Relay SDK", + "status": "completed", + "startedAt": "2026-05-09T19:43:34.805Z", + "completedAt": "2026-05-09T19:44:00.107Z", + "path": ".trajectories/completed/2026-05/traj_oyc528j7suvo.json" + }, + "traj_uf8y40ewrfh0": { + "title": "Address PR 831 review feedback and conflicts", + "status": "completed", + "startedAt": "2026-05-09T19:59:24.197Z", + "completedAt": "2026-05-09T19:59:24.403Z", + "path": ".trajectories/completed/2026-05/traj_uf8y40ewrfh0.json" + }, + "traj_iole5zdt9orr": { + "title": "Fix PR 831 CI conflicts", + "status": "completed", + "startedAt": "2026-05-10T15:18:12.326Z", + "completedAt": "2026-05-10T15:29:41.840Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-pr831-ci-fix/.trajectories/completed/2026-05/traj_iole5zdt9orr.json" } } -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac3e7624..8fe38bd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,11 +41,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.14] - 2026-05-10 ### Product Perspective + #### User-Impacting Fixes + - Reclaim agent on 409 instead of crashing the broker (#797) (#830) (#797) ### Technical Perspective + #### Releases + - v6.0.14 --- @@ -53,15 +57,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.13] - 2026-05-09 ### Product Perspective + #### User-Facing Features & Improvements + - **Re-export github primitive from root entry (#823)** (#823) - **Make reliability repair-aware by default (#827)** (#827) #### User-Impacting Fixes + - Wait for matching broker tarball before install (#829) (#829) ### Technical Perspective + #### Releases + - v6.0.13 --- @@ -69,11 +78,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.12] - 2026-05-09 ### Product Perspective + #### User-Impacting Fixes + - Finish agentToken doc cleanup in types.ts (#822) (#822) ### Technical Perspective + #### Releases + - v6.0.12 --- @@ -81,26 +94,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.10] - 2026-05-08 ### Product Perspective + #### User-Facing Features & Improvements + - **Spawn agents from named AgentWorkforce personas** - **Add @agentrelay/personas pack (#816)** (#816) #### User-Impacting Fixes + - Stop stamping default_workspace_id into RELAYFILE_WORKSPACE (#821) (#821) - Stop stamping relaycast workspace id into RELAYFILE_WORKSPACE (#820) (#820) -- Trust at_live_* agent tokens, drop probe-then-rotate (#819) (#819) +- Trust at*live*\* agent tokens, drop probe-then-rotate (#819) (#819) - Address PR review (Windows paths, TOCTOU, harness validation) - Tighten validator robustness - Regenerate lockfile and address review nits ### Technical Perspective + #### Performance & Reliability + - Skip personas package in dist-files check #### Dependencies & Tooling + - Align with @agent-relay scope and lockstep versioning #### Releases + - v6.0.10 --- @@ -108,14 +128,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.9] - 2026-05-05 ### Product Perspective + #### User-Facing Features & Improvements + - **Add WorkflowBuilder.paths() for multi-repo cloud workflows (#814)** (#814) #### User-Impacting Fixes + - Align communicate transport with current Relaycast API (#813) (#813) ### Technical Perspective + #### Releases + - v6.0.9 --- @@ -123,15 +148,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.8] - 2026-05-04 ### Product Perspective + #### User-Facing Features & Improvements + - **Surface phase C multi-repo push results in cloud CLI (#775)** (#775) - **Phase B multi-path tarball upload for cloud workflows (#774)** (#774) #### User-Impacting Fixes + - Exclude volatile workflow files when applying sync patches (#811) (#811) ### Technical Perspective + #### Releases + - v6.0.8 --- @@ -139,7 +169,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.7] - 2026-05-01 ### Technical Perspective + #### Releases + - v6.0.7 --- @@ -147,12 +179,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.6] - 2026-04-30 ### Product Perspective + #### User-Impacting Fixes + - Add repository metadata for workflow types (#809) (#809) - Publish SDK internal deps before sdk (#806) (#806) ### Technical Perspective + #### Releases + - v6.0.6 --- @@ -160,12 +196,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.4] - 2026-04-30 ### Product Perspective + #### User-Impacting Fixes + - Publish SDK workflow types before SDK (#807) (#807) - Pack github-primitive + workflow-types in smoke; publish workflow-types (#804) (#804) ### Technical Perspective + #### Releases + - v6.0.4 --- @@ -173,16 +213,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.3] - 2026-04-29 ### Product Perspective + #### User-Facing Features & Improvements + - **Expose connectProvider() in @agent-relay/cloud SDK (#798)** (#798) - **Expose runScriptWorkflow() in @agent-relay/sdk/workflows (#799)** (#799) - **Bundle @agent-relay/github-primitive at /github subpath (#782)** (#782) #### User-Impacting Fixes + - Update codegen-models workflow to use new Python output path (#780) (#780) ### Technical Perspective + #### Releases + - v6.0.3 --- diff --git a/package-lock.json b/package-lock.json index 82da6ac92..a2472a675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-relay", - "version": "6.0.12", + "version": "6.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-relay", - "version": "6.0.12", + "version": "6.0.14", "bundleDependencies": [ "@relaycast/sdk", "@relayfile/local-mount" @@ -18,14 +18,14 @@ "web" ], "dependencies": { - "@agent-relay/cloud": "6.0.12", - "@agent-relay/config": "6.0.12", - "@agent-relay/hooks": "6.0.12", - "@agent-relay/sdk": "6.0.12", - "@agent-relay/telemetry": "6.0.12", - "@agent-relay/trajectory": "6.0.12", - "@agent-relay/user-directory": "6.0.12", - "@agent-relay/utils": "6.0.12", + "@agent-relay/cloud": "6.0.14", + "@agent-relay/config": "6.0.14", + "@agent-relay/hooks": "6.0.14", + "@agent-relay/sdk": "6.0.14", + "@agent-relay/telemetry": "6.0.14", + "@agent-relay/trajectory": "6.0.14", + "@agent-relay/user-directory": "6.0.14", + "@agent-relay/utils": "6.0.14", "@aws-sdk/client-s3": "3.1020.0", "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", @@ -15593,10 +15593,10 @@ }, "packages/acp-bridge": { "name": "@agent-relay/acp-bridge", - "version": "6.0.12", + "version": "6.0.14", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.12", + "@agent-relay/sdk": "6.0.14", "@agentclientprotocol/sdk": "^0.12.0" }, "bin": { @@ -15612,38 +15612,38 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "6.0.12" + "version": "6.0.14" }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/browser-primitive": { "name": "@agent-relay/browser-primitive", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/sdk": "6.0.12", + "@agent-relay/sdk": "6.0.14", "playwright": "^1.51.1" }, "bin": { @@ -15657,9 +15657,9 @@ }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12", + "@agent-relay/config": "6.0.14", "@aws-sdk/client-s3": "3.1020.0", "ignore": "^7.0.5", "tar": "^7.5.10" @@ -15675,7 +15675,7 @@ }, "packages/config": { "name": "@agent-relay/config", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { "zod": "^3.23.8", "zod-to-json-schema": "^3.23.1" @@ -15687,7 +15687,7 @@ }, "packages/credential-proxy": { "name": "@agent-relay/credential-proxy", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { "hono": "^4.11.4", "jose": "^6.1.3" @@ -15698,9 +15698,9 @@ }, "packages/gateway": { "name": "@agent-relay/gateway", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/sdk": "6.0.12" + "@agent-relay/sdk": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15709,9 +15709,9 @@ }, "packages/github-primitive": { "name": "@agent-relay/github-primitive", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/workflow-types": "6.0.12" + "@agent-relay/workflow-types": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15721,11 +15721,11 @@ }, "packages/hooks": { "name": "@agent-relay/hooks", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12", - "@agent-relay/sdk": "6.0.12", - "@agent-relay/trajectory": "6.0.12" + "@agent-relay/config": "6.0.14", + "@agent-relay/sdk": "6.0.14", + "@agent-relay/trajectory": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15734,9 +15734,9 @@ }, "packages/memory": { "name": "@agent-relay/memory", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/hooks": "6.0.12" + "@agent-relay/hooks": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15745,11 +15745,11 @@ }, "packages/openclaw": { "name": "@agent-relay/openclaw", - "version": "6.0.12", + "version": "6.0.14", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.12", + "@agent-relay/sdk": "6.0.14", "@relaycast/sdk": "^1.0.0", "ws": "^8.0.0" }, @@ -16514,14 +16514,14 @@ }, "packages/personas": { "name": "@agent-relay/personas", - "version": "6.0.12", + "version": "6.0.14", "license": "MIT" }, "packages/policy": { "name": "@agent-relay/policy", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12" + "@agent-relay/config": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16530,12 +16530,13 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12", - "@agent-relay/github-primitive": "6.0.12", - "@agent-relay/slack-primitive": "6.0.12", - "@agent-relay/workflow-types": "6.0.12", + "@agent-relay/cloud": "6.0.14", + "@agent-relay/config": "6.0.14", + "@agent-relay/github-primitive": "6.0.14", + "@agent-relay/slack-primitive": "6.0.14", + "@agent-relay/workflow-types": "6.0.14", "@agentworkforce/harness-kit": "^0.11.0", "@agentworkforce/workload-router": "^0.11.0", "@relaycast/sdk": "^1.1.0", @@ -16555,14 +16556,14 @@ "@types/ws": "^8.5.10" }, "optionalDependencies": { - "@agent-relay/broker-darwin-arm64": "6.0.12", - "@agent-relay/broker-darwin-x64": "6.0.12", - "@agent-relay/broker-linux-arm64": "6.0.12", - "@agent-relay/broker-linux-x64": "6.0.12", - "@agent-relay/broker-win32-x64": "6.0.12" + "@agent-relay/broker-darwin-arm64": "6.0.14", + "@agent-relay/broker-darwin-x64": "6.0.14", + "@agent-relay/broker-linux-arm64": "6.0.14", + "@agent-relay/broker-linux-x64": "6.0.14", + "@agent-relay/broker-win32-x64": "6.0.14" }, "peerDependencies": { - "@agent-relay/credential-proxy": "6.0.12", + "@agent-relay/credential-proxy": "6.0.14", "@anthropic-ai/claude-agent-sdk": ">=0.1.0", "@google/adk": ">=0.5.0", "@langchain/langgraph": ">=1.2.0", @@ -16600,13 +16601,13 @@ }, "packages/slack-primitive": { "name": "@agent-relay/slack-primitive", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/workflow-types": "6.0.12", + "@agent-relay/workflow-types": "6.0.14", "@slack/web-api": "^7.15.2" }, "devDependencies": { - "@agent-relay/github-primitive": "6.0.12", + "@agent-relay/github-primitive": "6.0.14", "@types/node": "^22.19.3", "typescript": "^5.9.3", "vitest": "^3.2.4" @@ -16614,7 +16615,7 @@ }, "packages/telemetry": { "name": "@agent-relay/telemetry", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { "posthog-node": "^5.29.2" }, @@ -16649,9 +16650,9 @@ }, "packages/trajectory": { "name": "@agent-relay/trajectory", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12" + "@agent-relay/config": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16660,9 +16661,9 @@ }, "packages/user-directory": { "name": "@agent-relay/user-directory", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/utils": "6.0.12" + "@agent-relay/utils": "6.0.14" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16671,9 +16672,9 @@ }, "packages/utils": { "name": "@agent-relay/utils", - "version": "6.0.12", + "version": "6.0.14", "dependencies": { - "@agent-relay/config": "6.0.12", + "@agent-relay/config": "6.0.14", "compare-versions": "^6.1.1" }, "devDependencies": { @@ -16683,7 +16684,7 @@ }, "packages/workflow-types": { "name": "@agent-relay/workflow-types", - "version": "6.0.12" + "version": "6.0.14" }, "web": { "version": "0.0.1", diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts index ba4c9121e..ff9b40437 100644 --- a/packages/cloud/src/index.ts +++ b/packages/cloud/src/index.ts @@ -16,6 +16,8 @@ export { export { runWorkflow, + scheduleWorkflow, + listWorkflowSchedules, getRunStatus, getRunLogs, cancelWorkflow, @@ -57,6 +59,8 @@ export { type AuthSessionResponse, type WorkflowFileType, type RunWorkflowResponse, + type WorkflowSchedule, + type ScheduleWorkflowOptions, type WorkflowLogsResponse, type SyncPatchResponse, SUPPORTED_PROVIDERS, diff --git a/packages/cloud/src/types.ts b/packages/cloud/src/types.ts index 4ef647b63..fd550b50e 100644 --- a/packages/cloud/src/types.ts +++ b/packages/cloud/src/types.ts @@ -95,6 +95,35 @@ export type RunWorkflowResponse = { [key: string]: unknown; }; +export type WorkflowSchedule = { + id: string; + relaycronScheduleId: string; + userId: string; + workspaceId: string; + organizationId: string; + name: string; + description: string | null; + scheduleType: 'once' | 'cron'; + cronExpression: string | null; + scheduledAt: string | null; + timezone: string; + status: string; + lastTriggeredRunId: string | null; + lastTriggeredAt: string | null; + createdAt: string; + updatedAt: string; +}; + +export type ScheduleWorkflowOptions = { + apiUrl?: string; + fileType?: WorkflowFileType; + name?: string; + description?: string; + cron?: string; + at?: string; + timezone?: string; +}; + export type WorkflowLogsResponse = { content: string; offset: number; diff --git a/packages/cloud/src/workflows.test.ts b/packages/cloud/src/workflows.test.ts index 9e7081d63..370f40435 100644 --- a/packages/cloud/src/workflows.test.ts +++ b/packages/cloud/src/workflows.test.ts @@ -29,7 +29,14 @@ vi.mock('./auth.js', () => ({ authorizedApiFetch: (...args: unknown[]) => authorizedApiFetchMock(...args), })); -import { parseGitHubRemote, parseWorkflowPaths, relativizeWorkflowPath, runWorkflow } from './workflows.js'; +import { + listWorkflowSchedules, + parseGitHubRemote, + parseWorkflowPaths, + relativizeWorkflowPath, + runWorkflow, + scheduleWorkflow, +} from './workflows.js'; describe('relativizeWorkflowPath', () => { let tmpRoot: string; @@ -419,3 +426,177 @@ describe('runWorkflow code sync', () => { expect((runBodies[0] as { paths?: unknown }).paths).toBeUndefined(); }); }); + +describe('workflow schedules', () => { + let tmpRoot: string; + let originalCwd: string; + + beforeEach(async () => { + originalCwd = process.cwd(); + tmpRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), 'cloud-schedule-workflow-'))); + process.chdir(tmpRoot); + ensureAuthenticatedMock.mockResolvedValue({ accessToken: 'token' }); + }); + + afterEach(async () => { + process.chdir(originalCwd); + await rm(tmpRoot, { recursive: true, force: true }); + vi.clearAllMocks(); + }); + + async function writeScheduleWorkflow(): Promise { + const workflowPath = path.join(tmpRoot, 'workflow.yaml'); + await writeFile( + workflowPath, + ['version: "1.0"', 'name: eval', 'swarm:', ' pattern: dag', 'agents: []', 'workflows: []'].join('\n') + ); + return workflowPath; + } + + function scheduleRecord(overrides: Record = {}): Record { + return { + id: 'sched-1', + relaycronScheduleId: 'relaycron-sched-1', + userId: 'user-1', + workspaceId: 'workspace-1', + organizationId: 'org-1', + name: 'Hourly eval', + description: null, + scheduleType: 'cron', + cronExpression: '0 * * * *', + scheduledAt: null, + timezone: 'UTC', + status: 'active', + lastTriggeredRunId: null, + lastTriggeredAt: null, + createdAt: '2026-05-09T00:00:00.000Z', + updatedAt: '2026-05-09T00:00:00.000Z', + ...overrides, + }; + } + + it('creates a cron schedule without one-time code sync fields', async () => { + const workflowPath = await writeScheduleWorkflow(); + const scheduleBodies: unknown[] = []; + authorizedApiFetchMock.mockImplementation(async (_auth, requestPath, init) => { + expect(requestPath).toBe('/api/v1/workflows/schedules'); + scheduleBodies.push(JSON.parse(String(init?.body))); + return { + auth: { accessToken: 'token' }, + response: new Response( + JSON.stringify({ + schedule: scheduleRecord(), + }), + { status: 201, headers: { 'Content-Type': 'application/json' } } + ), + }; + }); + + const result = await scheduleWorkflow(workflowPath, { + cron: '0 * * * *', + name: 'Hourly eval', + }); + + expect(result.id).toBe('sched-1'); + expect(scheduleBodies[0]).toMatchObject({ + name: 'Hourly eval', + schedule_type: 'cron', + cron_expression: '0 * * * *', + timezone: 'UTC', + workflowRequest: { + fileType: 'yaml', + }, + }); + expect( + (scheduleBodies[0] as { workflowRequest: Record }).workflowRequest.runId + ).toBeUndefined(); + expect( + (scheduleBodies[0] as { workflowRequest: Record }).workflowRequest.s3CodeKey + ).toBeUndefined(); + }); + + it('creates a one-time schedule without one-time code sync fields', async () => { + const workflowPath = await writeScheduleWorkflow(); + const scheduleBodies: unknown[] = []; + authorizedApiFetchMock.mockImplementation(async (_auth, requestPath, init) => { + expect(requestPath).toBe('/api/v1/workflows/schedules'); + scheduleBodies.push(JSON.parse(String(init?.body))); + return { + auth: { accessToken: 'token' }, + response: new Response( + JSON.stringify({ + schedule: scheduleRecord({ + name: 'One-off eval', + scheduleType: 'once', + cronExpression: null, + scheduledAt: '2026-05-10T09:00:00.000Z', + }), + }), + { status: 201, headers: { 'Content-Type': 'application/json' } } + ), + }; + }); + + const result = await scheduleWorkflow(workflowPath, { + at: '2026-05-10T09:00:00Z', + name: 'One-off eval', + }); + + expect(result.id).toBe('sched-1'); + expect(scheduleBodies[0]).toMatchObject({ + name: 'One-off eval', + schedule_type: 'once', + scheduled_at: '2026-05-10T09:00:00.000Z', + timezone: 'UTC', + workflowRequest: { + fileType: 'yaml', + }, + }); + expect((scheduleBodies[0] as { cron_expression?: unknown }).cron_expression).toBeUndefined(); + expect( + (scheduleBodies[0] as { workflowRequest: Record }).workflowRequest.runId + ).toBeUndefined(); + expect( + (scheduleBodies[0] as { workflowRequest: Record }).workflowRequest.s3CodeKey + ).toBeUndefined(); + }); + + it('rejects invalid schedule option combinations', async () => { + await expect( + scheduleWorkflow('workflow.yaml', { cron: '0 * * * *', at: '2026-05-10T09:00:00Z' }) + ).rejects.toThrow('Provide exactly one of --cron or --at.'); + await expect(scheduleWorkflow('workflow.yaml', {})).rejects.toThrow( + 'Provide exactly one of --cron or --at.' + ); + }); + + it('rejects invalid one-time schedule timestamps with a clear error', async () => { + const workflowPath = await writeScheduleWorkflow(); + + await expect(scheduleWorkflow(workflowPath, { at: 'next tuesday' })).rejects.toThrow( + 'Invalid date for --at: next tuesday' + ); + }); + + it('lists workflow schedules', async () => { + authorizedApiFetchMock.mockResolvedValueOnce({ + auth: { accessToken: 'token' }, + response: new Response( + JSON.stringify({ + schedules: [scheduleRecord(), scheduleRecord({ id: 123 })], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ), + }); + + const schedules = await listWorkflowSchedules(); + + expect(authorizedApiFetchMock).toHaveBeenCalledWith( + { accessToken: 'token' }, + '/api/v1/workflows/schedules', + expect.objectContaining({ headers: { Accept: 'application/json' } }) + ); + expect(schedules).toHaveLength(1); + expect(schedules[0].id).toBe('sched-1'); + }); +}); diff --git a/packages/cloud/src/workflows.ts b/packages/cloud/src/workflows.ts index bf53967fd..f079615d1 100644 --- a/packages/cloud/src/workflows.ts +++ b/packages/cloud/src/workflows.ts @@ -11,7 +11,9 @@ import { defaultApiUrl, type WorkflowFileType, type RunWorkflowResponse, + type ScheduleWorkflowOptions, type WorkflowLogsResponse, + type WorkflowSchedule, type SyncPatchResponse, type PathSubmission, } from './types.js'; @@ -654,6 +656,94 @@ export async function runWorkflow( return payload as RunWorkflowResponse; } +export async function scheduleWorkflow( + workflowArg: string, + options: ScheduleWorkflowOptions = {} +): Promise { + const hasCron = typeof options.cron === 'string' && options.cron.trim().length > 0; + const hasAt = typeof options.at === 'string' && options.at.trim().length > 0; + if (hasCron === hasAt) { + throw new Error('Provide exactly one of --cron or --at.'); + } + + const apiUrl = options.apiUrl ?? defaultApiUrl(); + const auth = await ensureAuthenticated(apiUrl); + const input = await resolveWorkflowInput(workflowArg, options.fileType); + + if (input.fileType === 'ts') { + await validateTypeScriptWorkflow(input.workflow); + } else if (input.fileType === 'yaml') { + console.error('Validating workflow...'); + validateYamlWorkflow(input.workflow); + } + + const requestBody: Record = { + name: options.name?.trim() || path.basename(workflowArg), + schedule_type: hasCron ? 'cron' : 'once', + timezone: options.timezone?.trim() || 'UTC', + workflowRequest: { + workflow: input.workflow, + fileType: input.fileType, + ...(input.sourceFileType ? { sourceFileType: input.sourceFileType } : {}), + }, + }; + if (options.description?.trim()) { + requestBody.description = options.description.trim(); + } + if (hasCron) { + requestBody.cron_expression = options.cron?.trim(); + } else { + const scheduledAt = new Date(String(options.at)); + if (Number.isNaN(scheduledAt.getTime())) { + throw new Error(`Invalid date for --at: ${options.at}`); + } + requestBody.scheduled_at = scheduledAt.toISOString(); + } + + const { response } = await authorizedApiFetch(auth, '/api/v1/workflows/schedules', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + const payload = await readJsonResponse(response); + if (!response.ok) { + throw new Error(`Workflow schedule failed: ${describeResponseError(response, payload)}`); + } + + if (!isWorkflowScheduleEnvelope(payload)) { + throw new Error('Workflow schedule response was not valid JSON.'); + } + + return payload.schedule; +} + +export async function listWorkflowSchedules(options: { apiUrl?: string } = {}): Promise { + const apiUrl = options.apiUrl ?? defaultApiUrl(); + const auth = await ensureAuthenticated(apiUrl); + const { response } = await authorizedApiFetch(auth, '/api/v1/workflows/schedules', { + headers: { Accept: 'application/json' }, + }); + + const payload = await readJsonResponse(response); + if (!response.ok) { + throw new Error(`Schedule list failed: ${describeResponseError(response, payload)}`); + } + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + throw new Error('Schedule list response was not valid JSON.'); + } + + const schedules = (payload as { schedules?: unknown }).schedules; + if (!Array.isArray(schedules)) { + throw new Error('Schedule list response was not valid JSON.'); + } + + return schedules.filter(isWorkflowSchedule); +} + export async function getRunStatus( runId: string, options: { apiUrl?: string } = {} @@ -829,6 +919,42 @@ function describeResponseError(response: Response, payload: unknown): string { return `${response.status} ${response.statusText}`; } +function isWorkflowScheduleEnvelope(payload: unknown): payload is { schedule: WorkflowSchedule } { + return ( + Boolean(payload) && + typeof payload === 'object' && + !Array.isArray(payload) && + isWorkflowSchedule((payload as { schedule?: unknown }).schedule) + ); +} + +function isWorkflowSchedule(value: unknown): value is WorkflowSchedule { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + const record = value as Record; + const hasNullableString = (field: string): boolean => + record[field] === null || typeof record[field] === 'string'; + return ( + typeof record.id === 'string' && + typeof record.relaycronScheduleId === 'string' && + typeof record.userId === 'string' && + typeof record.workspaceId === 'string' && + typeof record.organizationId === 'string' && + typeof record.name === 'string' && + hasNullableString('description') && + (record.scheduleType === 'once' || record.scheduleType === 'cron') && + hasNullableString('cronExpression') && + hasNullableString('scheduledAt') && + typeof record.timezone === 'string' && + typeof record.status === 'string' && + hasNullableString('lastTriggeredRunId') && + hasNullableString('lastTriggeredAt') && + typeof record.createdAt === 'string' && + typeof record.updatedAt === 'string' + ); +} + function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT'); } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 4e65e05e8..8d607d74f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -130,7 +130,7 @@ "directory": "packages/sdk" }, "scripts": { - "prebuild": "npm --prefix ../workflow-types run build && npm --prefix ../github-primitive run build && npm --prefix ../slack-primitive run build && npm --prefix ../config run build", + "prebuild": "npm --prefix ../workflow-types run build && npm --prefix ../github-primitive run build && npm --prefix ../slack-primitive run build && npm --prefix ../config run build && npm --prefix ../cloud run build", "build": "npx tsc -p tsconfig.build.json", "build:full": "tsc -p tsconfig.json && npm run bundle:binary", "bundle:binary": "node ./scripts/bundle-agent-relay.mjs", @@ -149,6 +149,7 @@ "@types/ws": "^8.5.10" }, "dependencies": { + "@agent-relay/cloud": "6.0.14", "@agent-relay/config": "6.0.14", "@agent-relay/github-primitive": "6.0.14", "@agent-relay/slack-primitive": "6.0.14", diff --git a/packages/sdk/src/workflows/cloud-schedules.ts b/packages/sdk/src/workflows/cloud-schedules.ts new file mode 100644 index 000000000..49b8ae0b4 --- /dev/null +++ b/packages/sdk/src/workflows/cloud-schedules.ts @@ -0,0 +1,3 @@ +export { listWorkflowSchedules, scheduleWorkflow } from '@agent-relay/cloud'; + +export type { ScheduleWorkflowOptions, WorkflowSchedule } from '@agent-relay/cloud'; diff --git a/packages/sdk/src/workflows/index.ts b/packages/sdk/src/workflows/index.ts index e507a2d67..95a8ed6dd 100644 --- a/packages/sdk/src/workflows/index.ts +++ b/packages/sdk/src/workflows/index.ts @@ -33,6 +33,7 @@ export { export * from './memory-db.js'; export * from './file-db.js'; export * from './run.js'; +export * from './cloud-schedules.js'; export * from './builder.js'; export * from './coordinator.js'; export * from './barrier.js'; diff --git a/packages/sdk/tsconfig.build.json b/packages/sdk/tsconfig.build.json index b07ef52c7..6cf7d0bce 100644 --- a/packages/sdk/tsconfig.build.json +++ b/packages/sdk/tsconfig.build.json @@ -7,6 +7,8 @@ "paths": { "@agent-relay/config": ["../config/dist/index.d.ts"], "@agent-relay/config/*": ["../config/dist/*"], + "@agent-relay/cloud": ["../cloud/dist/index.d.ts"], + "@agent-relay/cloud/*": ["../cloud/dist/*"], "@agent-relay/workflow-types": ["../workflow-types/dist/index.d.ts"], "@agent-relay/github-primitive": ["../github-primitive/dist/index.d.ts"], "@agent-relay/github-primitive/workflow-step": ["../github-primitive/dist/workflow-step.d.ts"], diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 3323ac367..aa1075d3e 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -5,6 +5,8 @@ "baseUrl": ".", "paths": { "@agent-relay/config": ["../config/src/index.ts"], + "@agent-relay/config/*": ["../config/src/*"], + "@agent-relay/cloud": ["../cloud/src/index.ts"], "@agent-relay/workflow-types": ["../workflow-types/src/index.ts"], "@agent-relay/github-primitive": ["../github-primitive/src/index.ts"], "@agent-relay/github-primitive/workflow-step": ["../github-primitive/src/workflow-step.ts"], diff --git a/packages/telemetry/src/events.ts b/packages/telemetry/src/events.ts index 86525d6d0..bcc57a935 100644 --- a/packages/telemetry/src/events.ts +++ b/packages/telemetry/src/events.ts @@ -257,6 +257,24 @@ export interface CloudWorkflowRunEvent { error_class?: string; } +/** + * cloud_workflow_schedule - Emitted when `agent-relay cloud schedule` creates a workflow schedule. + */ +export interface CloudWorkflowScheduleEvent { + /** Schedule trigger type requested by the user */ + schedule_type: 'cron' | 'once'; + /** Whether --file-type was explicitly set by the user */ + has_explicit_file_type: boolean; + /** True if --json output mode was requested */ + json_output: boolean; + /** True if the schedule was created successfully */ + success: boolean; + /** Wall-clock duration in milliseconds */ + duration_ms: number; + /** Error constructor name on failure */ + error_class?: string; +} + /** * provider_auth - Emitted for `agent-relay auth ` (SSH-based provider login). */ @@ -340,6 +358,7 @@ export type TelemetryEventName = | 'workflow_run' | 'cloud_auth' | 'cloud_workflow_run' + | 'cloud_workflow_schedule' | 'provider_auth' | 'setup_init' | 'swarm_run' @@ -358,6 +377,7 @@ export interface TelemetryEventMap { workflow_run: WorkflowRunEvent; cloud_auth: CloudAuthEvent; cloud_workflow_run: CloudWorkflowRunEvent; + cloud_workflow_schedule: CloudWorkflowScheduleEvent; provider_auth: ProviderAuthEvent; setup_init: SetupInitEvent; swarm_run: SwarmRunEvent; diff --git a/scripts/build-cjs.mjs b/scripts/build-cjs.mjs index 7e73a3137..e283039bb 100644 --- a/scripts/build-cjs.mjs +++ b/scripts/build-cjs.mjs @@ -15,7 +15,7 @@ await build({ target: 'node18', logLevel: 'info', // Exclude native dependencies from bundle - they're loaded dynamically at runtime - external: ['better-sqlite3'], + external: ['better-sqlite3', 'ssh2'], banner: { js: "const import_meta_url = require('node:url').pathToFileURL(__filename).href;", }, diff --git a/src/cli/bootstrap.test.ts b/src/cli/bootstrap.test.ts index 029fc04cd..08f9902fd 100644 --- a/src/cli/bootstrap.test.ts +++ b/src/cli/bootstrap.test.ts @@ -42,6 +42,8 @@ const expectedLeafCommands = [ 'cloud whoami', 'cloud connect', 'cloud run', + 'cloud schedule', + 'cloud schedules', 'cloud status', 'cloud logs', 'cloud sync', diff --git a/src/cli/commands/cloud.test.ts b/src/cli/commands/cloud.test.ts index aa1119643..9df584b5f 100644 --- a/src/cli/commands/cloud.test.ts +++ b/src/cli/commands/cloud.test.ts @@ -3,6 +3,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const cloudMocks = vi.hoisted(() => ({ runWorkflow: vi.fn(), + scheduleWorkflow: vi.fn(), + listWorkflowSchedules: vi.fn(), getRunStatus: vi.fn(), syncWorkflowPatch: vi.fn(), })); @@ -20,8 +22,10 @@ vi.mock('@agent-relay/cloud', () => ({ 'anthropic (alias: claude), openai (alias: codex), google (alias: gemini), cursor, opencode, droid', getRunLogs: vi.fn(), getRunStatus: (...args: unknown[]) => cloudMocks.getRunStatus(...args), + listWorkflowSchedules: (...args: unknown[]) => cloudMocks.listWorkflowSchedules(...args), readStoredAuth: vi.fn(), runWorkflow: (...args: unknown[]) => cloudMocks.runWorkflow(...args), + scheduleWorkflow: (...args: unknown[]) => cloudMocks.scheduleWorkflow(...args), syncWorkflowPatch: (...args: unknown[]) => cloudMocks.syncWorkflowPatch(...args), })); @@ -65,6 +69,8 @@ describe('registerCloudCommands', () => { 'whoami', 'connect', 'run', + 'schedule', + 'schedules', 'status', 'logs', 'sync', @@ -109,6 +115,96 @@ describe('registerCloudCommands', () => { expect(optionNames).toContain('--json'); }); + it('schedule creates repeatable workflow schedules', async () => { + const { program, deps } = createHarness(); + cloudMocks.scheduleWorkflow.mockResolvedValueOnce({ + id: 'sched-1', + name: 'Hourly eval', + scheduleType: 'cron', + cronExpression: '0 * * * *', + timezone: 'UTC', + status: 'active', + lastTriggeredRunId: null, + }); + + await program.parseAsync([ + 'node', + 'agent-relay', + 'cloud', + 'schedule', + 'workflow.yaml', + '--cron', + '0 * * * *', + '--name', + 'Hourly eval', + ]); + + expect(cloudMocks.scheduleWorkflow).toHaveBeenCalledWith( + 'workflow.yaml', + expect.objectContaining({ + cron: '0 * * * *', + name: 'Hourly eval', + }) + ); + expect(deps.log).toHaveBeenCalledWith('Schedule created: sched-1'); + }); + + it('schedule creates one-time workflow schedules', async () => { + const { program, deps } = createHarness(); + cloudMocks.scheduleWorkflow.mockResolvedValueOnce({ + id: 'sched-at-1', + name: 'One-off eval', + scheduleType: 'once', + scheduledAt: '2026-05-10T09:00:00.000Z', + timezone: 'UTC', + status: 'active', + lastTriggeredRunId: null, + }); + + await program.parseAsync([ + 'node', + 'agent-relay', + 'cloud', + 'schedule', + 'workflow.yaml', + '--at', + '2026-05-10T09:00:00Z', + '--name', + 'One-off eval', + ]); + + expect(cloudMocks.scheduleWorkflow).toHaveBeenCalledWith( + 'workflow.yaml', + expect.objectContaining({ + at: '2026-05-10T09:00:00Z', + name: 'One-off eval', + }) + ); + expect(cloudMocks.scheduleWorkflow.mock.calls[0][1]).not.toHaveProperty('cron'); + expect(deps.log).toHaveBeenCalledWith('Schedule created: sched-at-1'); + }); + + it('schedules lists repeatable workflow schedules', async () => { + const { program, deps } = createHarness(); + cloudMocks.listWorkflowSchedules.mockResolvedValueOnce([ + { + id: 'sched-1', + name: 'Hourly eval', + scheduleType: 'cron', + cronExpression: '0 * * * *', + timezone: 'UTC', + status: 'active', + lastTriggeredRunId: 'run-1', + }, + ]); + + await program.parseAsync(['node', 'agent-relay', 'cloud', 'schedules']); + + expect(cloudMocks.listWorkflowSchedules).toHaveBeenCalledWith(expect.objectContaining({})); + expect(deps.log).toHaveBeenCalledWith(expect.stringContaining('sched-1')); + expect(deps.log).toHaveBeenCalledWith(expect.stringContaining('run-1')); + }); + it('logs has --follow and --poll-interval options', () => { const { program } = createHarness(); const cloud = program.commands.find((command) => command.name() === 'cloud'); diff --git a/src/cli/commands/cloud.ts b/src/cli/commands/cloud.ts index 69cd7a280..1bbb7af9a 100644 --- a/src/cli/commands/cloud.ts +++ b/src/cli/commands/cloud.ts @@ -13,6 +13,8 @@ import { AUTH_FILE_PATH, REFRESH_WINDOW_MS, runWorkflow, + scheduleWorkflow, + listWorkflowSchedules, getRunStatus, getRunLogs, syncWorkflowPatch, @@ -22,6 +24,7 @@ import { normalizeProvider, type WhoAmIResponse, type WorkflowFileType, + type WorkflowSchedule, } from '@agent-relay/cloud'; import { defaultExit } from '../lib/exit.js'; @@ -128,6 +131,36 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +function formatScheduleCadence(schedule: WorkflowSchedule): string { + if (schedule.scheduleType === 'cron') { + return `${schedule.cronExpression ?? 'cron'} (${schedule.timezone})`; + } + return schedule.scheduledAt + ? `${schedule.scheduledAt} (${schedule.timezone})` + : `once (${schedule.timezone})`; +} + +function renderSchedule(schedule: WorkflowSchedule, log: (...args: unknown[]) => void): void { + log(`Schedule created: ${schedule.id}`); + log(`Name: ${schedule.name}`); + log(`Status: ${schedule.status}`); + log(`Cadence: ${formatScheduleCadence(schedule)}`); +} + +function renderScheduleList(schedules: WorkflowSchedule[], log: (...args: unknown[]) => void): void { + if (schedules.length === 0) { + log('No workflow schedules found.'); + return; + } + + for (const schedule of schedules) { + const lastRun = schedule.lastTriggeredRunId ? ` last run ${schedule.lastTriggeredRunId}` : ' no runs yet'; + log( + `${schedule.id} ${schedule.status} ${formatScheduleCadence(schedule)} ${schedule.name} (${lastRun})` + ); + } +} + // ── Command registration ───────────────────────────────────────────────────── export function registerCloudCommands(program: Command, overrides: Partial = {}): void { @@ -375,6 +408,76 @@ export function registerCloudCommands(program: Command, overrides: Partial', 'Workflow file path or inline workflow content') + .option('--api-url ', 'Cloud API base URL') + .option('--file-type ', 'Workflow type: yaml, ts, or py', parseWorkflowFileType) + .option('--cron ', 'Cron expression, for example "0 * * * *"') + .option('--at ', 'One-time ISO timestamp, for example 2026-05-10T09:00:00Z') + .option('--timezone ', 'IANA timezone for cron schedules', 'UTC') + .option('--name ', 'Schedule name') + .option('--description ', 'Schedule description') + .option('--json', 'Print raw JSON response', false) + .action( + async ( + workflow: string, + options: { + apiUrl?: string; + fileType?: WorkflowFileType; + cron?: string; + at?: string; + timezone?: string; + name?: string; + description?: string; + json?: boolean; + } + ) => { + const started = Date.now(); + let success = false; + let errorClass: string | undefined; + try { + const result = await scheduleWorkflow(workflow, options); + if (options.json) { + deps.log(JSON.stringify(result, null, 2)); + } else { + renderSchedule(result, deps.log); + deps.log('\nList schedules: agent-relay cloud schedules'); + } + success = true; + } catch (err) { + errorClass = errorClassName(err); + throw err; + } finally { + track('cloud_workflow_schedule', { + schedule_type: options.cron ? 'cron' : 'once', + has_explicit_file_type: Boolean(options.fileType), + json_output: Boolean(options.json), + success, + duration_ms: Date.now() - started, + ...(errorClass ? { error_class: errorClass } : {}), + }); + } + } + ); + + cloudCommand + .command('schedules') + .description('List scheduled workflow runs') + .option('--api-url ', 'Cloud API base URL') + .option('--json', 'Print raw JSON response', false) + .action(async (options: { apiUrl?: string; json?: boolean }) => { + const schedules = await listWorkflowSchedules(options); + if (options.json) { + deps.log(JSON.stringify({ schedules }, null, 2)); + return; + } + renderScheduleList(schedules, deps.log); + }); + // ── status ───────────────────────────────────────────────────────────────── cloudCommand