diff --git a/README.md b/README.md index 7f124c3f..5df90134 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Ricky is designed around co-equal interfaces and onboarding surfaces. Current im - **CLI** — implemented as the primary local command surface. - **Local / BYOH** — implemented for local workflow generation, artifact execution, background monitoring, and status checks. - **Cloud API** — partially implemented for Cloud generation request/response contracts and CLI connection/status flows. +- **Proactive runtime agent** — implemented for scheduled background-run monitoring via `@agent-relay/agent`. - **Slack** — planned; no source handler is currently implemented in this repo. - **Web** — planned; no browser surface is currently implemented in this repo. @@ -102,6 +103,22 @@ ricky status --run When generation does not run the artifact, the CLI prints the artifact path plus foreground and background run commands. +## Proactive Runtime Monitoring + +Ricky now exposes a proactive runtime entrypoint at [`src/agent.ts`](./src/agent.ts). The scheduled agent wakes every 5 minutes with `agent({ schedule: "*/5 * * * *" })`, scans Ricky's persisted background-run state, and posts terminal run updates to `RICKY_MONITOR_CHANNEL` (default `#ricky`). + +Environment knobs for the scheduled agent: +- `RICKY_WORKSPACE_ID` — Relaycast/relayfile workspace name for the agent runtime. Defaults to `ricky`. +- `RICKY_MONITOR_CHANNEL` — channel that receives proactive run updates. Defaults to `#ricky`. +- `RICKY_MONITOR_REPO_ROOT` — repo root whose persisted run-state tree should be monitored. Defaults to the current working directory. Both `/.workflow-artifacts/ricky-local-runs/` and the XDG state-home tree are scanned. +- `RICKY_STATE_HOME` — optional override for the base state directory Ricky already uses for background runs. + +Run it locally with: + +```sh +tsx src/agent.ts +``` + ## CLI Onboarding Ricky's CLI should be intentionally welcoming and user-friendly. diff --git a/package-lock.json b/package-lock.json index 73407e35..f52710c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@agent-assistant/turn-context": "^0.4.31", + "@agent-relay/agent": "^6.0.18", "@agent-relay/cloud": "^6.0.15", "@agent-relay/sdk": "^6.0.15", "@agentworkforce/harness-kit": "^0.19.0", @@ -752,6 +753,42 @@ "integrity": "sha512-yRT0YMMwskDg5aEvwn6iUDQZxgYqD8AtbIL3dnb+cdHkyd5hoKDi9rQiHq7Mi+yBg/s9ja4cGks9kI1pNLemQA==", "license": "MIT" }, + "node_modules/@agent-relay/agent": { + "version": "6.0.18", + "resolved": "https://registry.npmjs.org/@agent-relay/agent/-/agent-6.0.18.tgz", + "integrity": "sha512-qSLTgMojzATyxKAWhNSSEwtrBTHc8SLkG6wikBz790W6KpWnM59hbJhpgYN03s4U7jxZK5C3yPqd0p1NyVWVVQ==", + "dependencies": { + "@agent-relay/events": "6.0.18", + "@relaycast/sdk": "1.1.2", + "@relayfile/sdk": "^0.7.2" + } + }, + "node_modules/@agent-relay/agent/node_modules/@relaycast/sdk": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-1.1.2.tgz", + "integrity": "sha512-s0QsKUd5UNS8PUZIds8EdIUJUSOTEN9cn6oQTY41rvdvcOu9UI8+TH0rP4LAYCqwutRP6v3KDmF1s/6qwk4LPg==", + "dependencies": { + "@relaycast/types": "1.1.2", + "zod": "^4.3.6" + } + }, + "node_modules/@agent-relay/agent/node_modules/@relaycast/types": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-1.1.2.tgz", + "integrity": "sha512-Lfx2oGDMpFNJx1AZNU7xaAE+3Fs5ZrUmVgAjZ2pMiHlbJx2gOHxpB57cQDjZY+JG4bM7C4Ao2JTOy3FqXbXlww==", + "dependencies": { + "zod": "^4.3.6" + } + }, + "node_modules/@agent-relay/agent/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@agent-relay/broker-darwin-arm64": { "version": "6.0.15", "resolved": "https://registry.npmjs.org/@agent-relay/broker-darwin-arm64/-/broker-darwin-arm64-6.0.15.tgz", @@ -840,6 +877,45 @@ "zod-to-json-schema": "^3.23.1" } }, + "node_modules/@agent-relay/events": { + "version": "6.0.18", + "resolved": "https://registry.npmjs.org/@agent-relay/events/-/events-6.0.18.tgz", + "integrity": "sha512-GjWYY/OXGEEdFCZrBK2uBacpO87d8fLOjw48TEhUFxY0J1FGGNe77g/jq0r8GzgDlb0+HG/g6oyBWHBdmwNDcQ==", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.2.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "peerDependencies": { + "@relayfile/adapter-github": "^0.2.5", + "@relayfile/adapter-jira": "^0.2.8", + "@relayfile/adapter-linear": "^0.2.5", + "@relayfile/adapter-notion": "^0.2.7", + "@relayfile/adapter-slack": "^0.2.7" + }, + "peerDependenciesMeta": { + "@relayfile/adapter-github": { + "optional": true + }, + "@relayfile/adapter-jira": { + "optional": true + }, + "@relayfile/adapter-linear": { + "optional": true + }, + "@relayfile/adapter-notion": { + "optional": true + }, + "@relayfile/adapter-slack": { + "optional": true + } + } + }, "node_modules/@agent-relay/github-primitive": { "version": "6.0.15", "resolved": "https://registry.npmjs.org/@agent-relay/github-primitive/-/github-primitive-6.0.15.tgz", @@ -1934,6 +2010,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2744,6 +2821,439 @@ ], "license": "MIT" }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", + "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", + "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.207.0.tgz", + "integrity": "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.207.0", + "@opentelemetry/otlp-transformer": "0.207.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.207.0.tgz", + "integrity": "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.207.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.207.0.tgz", + "integrity": "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.207.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.207.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.207.0.tgz", + "integrity": "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.207.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", + "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.7.1", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@relaycast/sdk": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-1.1.5.tgz", @@ -2780,21 +3290,21 @@ } }, "node_modules/@relayfile/core": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.5.3.tgz", - "integrity": "sha512-4R18T2XreImy/Hn+Jd9vcO4hy5Q8lmI63QklhBnSyj1hV0kTnPvSMuozDIFq+jEZtExg4LQqFmju0wmElwWrYQ==", + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.7.11.tgz", + "integrity": "sha512-4gkBmmsdLGEe2a4ZPO84eYFyiKNpD6GymYBg5Awsr/dnwRnBqhe2W8F6rEjdxRpa0Wi1cARFYcsfV4/B4SifOA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@relayfile/sdk": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.5.3.tgz", - "integrity": "sha512-9r9rybXK/f2n195oaGS7uDnAtwSFKuJ8jlWkMFawsjTZVXUYZIMg867TXdl9dbcQ8bUUnP4SGsqt8QeeyyXJ1Q==", + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.7.11.tgz", + "integrity": "sha512-cOxlp+N7rDEcAhvzX/J97ZTvzMLfBmKDzxzLz4rTnP9n3e3EwZRl02lJWese1caVBVBbHeUa9Ds7AHwg1x8uqA==", "license": "MIT", "dependencies": { - "@relayfile/core": "0.5.3" + "@relayfile/core": "0.7.11" }, "engines": { "node": ">=18" @@ -5131,6 +5641,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5934,6 +6450,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/protobufjs": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", diff --git a/package.json b/package.json index 5230863e..5c7dd7e7 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ }, "dependencies": { "@agent-assistant/turn-context": "^0.4.31", + "@agent-relay/agent": "^6.0.18", "@agent-relay/cloud": "^6.0.15", "@agent-relay/sdk": "^6.0.15", "@agentworkforce/harness-kit": "^0.19.0", diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 00000000..5c8ec32d --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,7 @@ +import { createRickyScheduledAgent } from "./scheduled-agent.js"; + +export * from "./scheduled-agent.js"; + +const rickyAgent = createRickyScheduledAgent(); + +export default rickyAgent; diff --git a/src/scheduled-agent.test.ts b/src/scheduled-agent.test.ts new file mode 100644 index 00000000..4cc6ca62 --- /dev/null +++ b/src/scheduled-agent.test.ts @@ -0,0 +1,126 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + listPersistedRunStates, + renderRunMonitorAlert, + shouldNotifyRunState, +} from "./scheduled-agent.js"; +import type { LocalExecutionStageResult } from "./local/entrypoint.js"; +import type { LocalRunMonitorState } from "./surfaces/cli/flows/local-run-monitor.js"; + +function buildRunState( + runId: string, + status: LocalRunMonitorState["status"], + overrides: Partial = {}, +): LocalRunMonitorState { + const base: LocalRunMonitorState = { + runId, + status, + artifactPath: `workflows/generated/${runId}.ts`, + artifactDir: `/tmp/${runId}`, + statePath: `/tmp/${runId}/state.json`, + logPath: `/tmp/${runId}/run.log`, + evidencePath: `/tmp/${runId}/evidence.json`, + fixesPath: `/tmp/${runId}/fixes.json`, + reattachCommand: `ricky status --run ${runId}`, + response: { + ok: status === "completed", + artifacts: [], + logs: [], + warnings: [], + nextActions: [], + exitCode: status === "completed" ? 0 : 1, + }, + }; + return { ...base, ...overrides }; +} + +async function writePersistedRun(root: string, state: LocalRunMonitorState): Promise { + const runDir = join(root, state.runId); + await mkdir(runDir, { recursive: true }); + await writeFile(join(runDir, "state.json"), JSON.stringify(state), "utf8"); +} + +describe("ricky scheduled agent helpers", () => { + const cleanupRoots: string[] = []; + + afterEach(async () => { + while (cleanupRoots.length) { + const root = cleanupRoots.pop()!; + await rm(root, { recursive: true, force: true }); + } + }); + + async function makeStateRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), "ricky-agent-state-")); + cleanupRoots.push(root); + return root; + } + + it("loads persisted background run state from disk", async () => { + const stateRoot = await makeStateRoot(); + await writePersistedRun(stateRoot, buildRunState("run-1", "failed")); + + const runs = await listPersistedRunStates(stateRoot); + expect(runs).toHaveLength(1); + expect(runs[0]?.runId).toBe("run-1"); + }); + + it("scans multiple state roots and dedupes by run id", async () => { + const xdgRoot = await makeStateRoot(); + const artifactRoot = await makeStateRoot(); + await writePersistedRun(xdgRoot, buildRunState("run-shared", "failed")); + await writePersistedRun(artifactRoot, buildRunState("run-shared", "blocked")); + await writePersistedRun(artifactRoot, buildRunState("run-artifact-only", "completed")); + + const runs = await listPersistedRunStates([xdgRoot, artifactRoot]); + expect(runs.map((run) => run.runId)).toEqual(["run-artifact-only", "run-shared"]); + expect(runs.find((run) => run.runId === "run-shared")?.status).toBe("failed"); + }); + + it("renders actionable alerts for terminal monitor states", () => { + const run = buildRunState("run-2", "blocked", { + artifactPath: "workflows/generated/release.ts", + artifactDir: "/tmp/run-2", + response: { + ok: false, + artifacts: [], + logs: [], + warnings: ["agent-relay missing"], + nextActions: [], + exitCode: 1, + }, + }); + + expect(shouldNotifyRunState(run)).toBe(true); + expect(renderRunMonitorAlert(run, "/repo")).toContain("Ricky monitor: background workflow needs attention."); + expect(renderRunMonitorAlert(run, "/repo")).toContain("ricky status --run run-2"); + }); + + it("renders a completion alert for completed runs", () => { + const execution = { + execution: { command: "ricky run workflows/generated/done.ts" }, + evidence: { outcome_summary: "all checks green" }, + } as unknown as LocalExecutionStageResult; + const run = buildRunState("run-3", "completed", { + response: { + ok: true, + artifacts: [], + logs: ["done"], + warnings: [], + nextActions: [], + exitCode: 0, + execution, + }, + }); + + expect(shouldNotifyRunState(run)).toBe(true); + const rendered = renderRunMonitorAlert(run, "/repo"); + expect(rendered).toContain("Ricky monitor: background workflow completed."); + expect(rendered).toContain("Outcome: all checks green"); + expect(rendered).toContain("Command: ricky run workflows/generated/done.ts"); + }); +}); diff --git a/src/scheduled-agent.ts b/src/scheduled-agent.ts new file mode 100644 index 00000000..49534493 --- /dev/null +++ b/src/scheduled-agent.ts @@ -0,0 +1,201 @@ +import { agent, type AgentHandle, type Context } from "@agent-relay/agent"; +import { readdir, readFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +import { localRunStateRoot, repoStateKey } from "./shared/state-paths.js"; +import type { LocalRunMonitorState } from "./surfaces/cli/flows/local-run-monitor.js"; + +const DEFAULT_RICKY_WORKSPACE = "ricky"; +const DEFAULT_MONITOR_CHANNEL = "#ricky"; +const DEFAULT_MONITOR_SCHEDULE = "*/5 * * * *"; + +/** Tuning knobs accepted by {@link createRickyScheduledAgent}. */ +export interface RickyScheduledAgentOptions { + /** Environment used to resolve workspace, channel, and state-root overrides. Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; + /** Repo whose persisted run-state should be scanned. Defaults to {@link defaultRickyRepoRoot}. */ + repoRoot?: string; + /** Channel where terminal-state alerts are posted. Defaults to {@link defaultRickyMonitorChannel}. */ + monitorChannel?: string; + /** Cron expression for the wake-up cadence. Defaults to every 5 minutes. */ + schedule?: string; +} + +/** Resolves the Relaycast/relayfile workspace name from `RICKY_WORKSPACE_ID`. */ +export function defaultRickyWorkspace(env: NodeJS.ProcessEnv = process.env): string { + const workspace = env.RICKY_WORKSPACE_ID?.trim(); + return workspace && workspace.length > 0 ? workspace : DEFAULT_RICKY_WORKSPACE; +} + +/** Resolves the proactive-monitor channel from `RICKY_MONITOR_CHANNEL`. */ +export function defaultRickyMonitorChannel(env: NodeJS.ProcessEnv = process.env): string { + const channel = env.RICKY_MONITOR_CHANNEL?.trim(); + return channel && channel.length > 0 ? channel : DEFAULT_MONITOR_CHANNEL; +} + +/** Resolves the repo root to monitor from `RICKY_MONITOR_REPO_ROOT` or the current working directory. */ +export function defaultRickyRepoRoot(env: NodeJS.ProcessEnv = process.env): string { + return resolve(env.RICKY_MONITOR_REPO_ROOT?.trim() || process.cwd()); +} + +/** + * Legacy in-repo artifact location where `startLocalRunMonitor` writes + * `state.json` when callers do not pass a `stateRoot` override. The + * scheduled agent must keep scanning this path because the production + * CLI flow at `src/surfaces/cli/flows/local-workflow-flow.ts` does not + * pass `stateRoot` and therefore lands here. + */ +export function legacyLocalArtifactRunStateRoot(repoRoot: string): string { + return resolve(repoRoot, ".workflow-artifacts", "ricky-local-runs"); +} + +async function readRunStatesInRoot(stateRoot: string): Promise { + try { + const entries = await readdir(stateRoot, { withFileTypes: true }); + const runs = await Promise.all( + entries + .filter((entry) => entry.isDirectory()) + .map(async (entry) => { + const statePath = join(stateRoot, entry.name, "state.json"); + try { + return JSON.parse(await readFile(statePath, "utf8")) as LocalRunMonitorState; + } catch { + return null; + } + }), + ); + + return runs.filter((run): run is LocalRunMonitorState => run !== null); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return []; + } + throw error; + } +} + +/** + * Loads persisted `LocalRunMonitorState` documents from one or more state + * roots. Mirrors the dual-path lookup used by `ricky status --run` so the + * monitor sees both the XDG state-home tree and the in-repo + * `.workflow-artifacts/ricky-local-runs/` tree that the local-run flow + * actually writes to. Runs that appear in multiple roots are deduped by + * `runId`, preferring the first occurrence. + */ +export async function listPersistedRunStates( + stateRoot: string | readonly string[], +): Promise { + const roots = Array.isArray(stateRoot) ? stateRoot : [stateRoot as string]; + const byRunId = new Map(); + for (const root of roots) { + for (const run of await readRunStatesInRoot(root)) { + if (!byRunId.has(run.runId)) { + byRunId.set(run.runId, run); + } + } + } + return [...byRunId.values()].sort((left, right) => left.runId.localeCompare(right.runId)); +} + +/** Returns true when a run state has reached a terminal status worth alerting on. */ +export function shouldNotifyRunState(state: LocalRunMonitorState): boolean { + return state.status === "blocked" || state.status === "failed" || state.status === "completed"; +} + +/** Renders a multi-line monitor alert summarizing a terminal background run. */ +export function renderRunMonitorAlert(state: LocalRunMonitorState, repoRoot: string): string { + const execution = state.response?.execution; + const outcome = + execution?.evidence?.outcome_summary + ?? state.response?.warnings?.[0] + ?? state.response?.logs?.[0] + ?? "No additional detail recorded."; + + const lines = [ + state.status === "completed" + ? "Ricky monitor: background workflow completed." + : "Ricky monitor: background workflow needs attention.", + "", + `Repo: ${repoRoot}`, + `Run id: ${state.runId}`, + `Status: ${state.status}`, + `Artifact: ${state.artifactPath}`, + `Outcome: ${outcome}`, + `Evidence: ${state.evidencePath}`, + `Fixes: ${state.fixesPath}`, + `Next: ${state.reattachCommand}`, + ]; + + if (execution?.execution.command) { + lines.splice(6, 0, `Command: ${execution.execution.command}`); + } + + return lines.join("\n"); +} + +/** + * Scans every supplied state root for persisted runs and posts a one-shot + * alert per `(repo, runId, status)` triple via `ctx.once`, so duplicate + * ticks or peer replicas do not produce duplicate notifications. + */ +export async function checkPersistedRuns( + ctx: Context, + input: { + stateRoots: readonly string[]; + repoRoot: string; + monitorChannel: string; + }, +): Promise { + const runs = await listPersistedRunStates(input.stateRoots); + for (const run of runs) { + if (!shouldNotifyRunState(run)) { + continue; + } + + await ctx.once( + `ricky-monitor:${repoStateKey(input.repoRoot)}:${run.runId}:${run.status}`, + async () => { + await ctx.messages.post( + input.monitorChannel, + renderRunMonitorAlert(run, input.repoRoot), + ); + return true; + }, + ); + } +} + +/** + * Creates the proactive Ricky monitor agent. Wakes on the configured cron + * schedule, scans both the XDG state root and the in-repo + * `.workflow-artifacts/ricky-local-runs/` tree, and posts a single alert + * per terminal run via `ctx.once`. + */ +export function createRickyScheduledAgent( + options: RickyScheduledAgentOptions = {}, +): AgentHandle { + const env = options.env ?? process.env; + const repoRoot = resolve(options.repoRoot ?? defaultRickyRepoRoot(env)); + const stateRoots = [ + localRunStateRoot(repoRoot, env), + legacyLocalArtifactRunStateRoot(repoRoot), + ]; + const monitorChannel = options.monitorChannel ?? defaultRickyMonitorChannel(env); + + return agent({ + workspace: defaultRickyWorkspace(env), + name: "ricky-monitor", + schedule: options.schedule ?? DEFAULT_MONITOR_SCHEDULE, + onEvent: async (ctx, event) => { + if (event.type !== "cron.tick") { + return; + } + + await checkPersistedRuns(ctx, { + stateRoots, + repoRoot, + monitorChannel, + }); + }, + }); +}