feat: implement the six showcase proactive agents#1
Conversation
Add a typed persona.ts + agent.ts handler for each agent described in the READMEs, plus project scaffold (package.json, tsconfig, package-lock) and a README listing them. - granola new note (Nango sync) → detect prospect → Linear issue → PR - hn-monitor 2×/day → HN topics digest → Slack - linear labelled issue / comment → implement → GitHub PR - review PR opened/updated/reviewed/CI → review, fix, resolve CI, merge on approval - spotify-releases daily → new releases from followed artists → Slack DM - vendor-monitor weekday AM → npm vendor bumps → team Slack Personas use definePersona (per-provider trigger autocomplete + deploy-time lint). Integrations are VFS-backed via the typed ctx clients. Code-touching agents don't clone — the cloud materializes the repo into ctx.sandbox.cwd via relayfile; the linear agent also parses a repo named in the issue. `npm run typecheck` passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 49 minutes and 13 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR introduces a new Agent Workforce repository containing six autonomous agents for external system integration. Each agent combines a Granola persona configuration with an event-triggered handler, executed in the cloud with integration to services like Linear, GitHub, Slack, and public APIs. The repository includes project scaffolding and documentation. ChangesAgent Workforce Agents
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes The PR introduces six independent but architecturally similar agents across multiple integration points (Linear, GitHub, Slack, Hacker News, Spotify, npm). Each agent requires understanding of its trigger mechanism (webhook vs. cron), configuration pattern (persona definition), handler logic (multi-step workflows), and memory/state management. The complexity stems from heterogeneous external APIs, conditional logic branches, error handling for missing integrations, and distributed state persistence across different personas. Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a collection of proactive, deployable agents (such as granola, hn-monitor, linear, review, spotify-releases, and vendor-monitor) built using @agentworkforce/persona-kit and @agentworkforce/runtime. The code review identified several critical improvements to enhance security, performance, and robustness. Key feedback includes addressing a potential command injection vulnerability in the review agent, refining regex parsing to avoid false positives in the Linear agent, parallelizing sequential API requests in the Spotify agent, handling markdown-wrapped JSON from LLM responses in the Granola agent, and adding error handling for network requests in the monitor agents to prevent crashes.
| const number = pr?.number ?? p?.number; | ||
| const owner = p?.repository?.owner?.login; | ||
| const repo = p?.repository?.name; | ||
| if (!number || !owner || !repo) return undefined; |
There was a problem hiding this comment.
Since payload is typed as unknown and cast directly, there is no runtime guarantee that number is actually a number. If a malicious or malformed payload contains a string with shell metacharacters, it could lead to a command injection vulnerability when interpolated directly into the unquoted shell command gh pr merge ${pr.number}. Adding runtime type validation for number is highly recommended.
| const number = pr?.number ?? p?.number; | |
| const owner = p?.repository?.owner?.login; | |
| const repo = p?.repository?.name; | |
| if (!number || !owner || !repo) return undefined; | |
| const number = pr?.number ?? p?.number; | |
| const owner = p?.repository?.owner?.login; | |
| const repo = p?.repository?.name; | |
| if (typeof number !== 'number' || !Number.isInteger(number) || !owner || !repo) return undefined; |
| function parseRepo(issue: { title: string; description: string | null }): string | undefined { | ||
| const text = `${issue.title}\n${issue.description ?? ''}`; | ||
| const url = text.match(/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git|[)\s/]|$)/i); | ||
| if (url) return url[1]; | ||
| return text.match(/(?:^|\s)([\w.-]+\/[\w.-]+)(?:\s|$)/)?.[1]; | ||
| } |
There was a problem hiding this comment.
The regex /(?:^|\s)([\w.-]+\/[\w.-]+)(?:\s|$)/ used to parse bare repository slugs is too permissive and will incorrectly match common slash-separated phrases like and/or, client/server, yes/no, or either/or if they appear in the issue title or description. This will pass an invalid repository name to the coding agent. Filtering out these common false positives improves parsing robustness.
| function parseRepo(issue: { title: string; description: string | null }): string | undefined { | |
| const text = `${issue.title}\n${issue.description ?? ''}`; | |
| const url = text.match(/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git|[)\s/]|$)/i); | |
| if (url) return url[1]; | |
| return text.match(/(?:^|\s)([\w.-]+\/[\w.-]+)(?:\s|$)/)?.[1]; | |
| } | |
| function parseRepo(issue: { title: string; description: string | null }): string | undefined { | |
| const text = `${issue.title}\n${issue.description ?? ''}`; | |
| const url = text.match(/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git|[)\s/]|$)/i); | |
| if (url) return url[1]; | |
| const slugMatch = text.match(/(?:^|\s)([\w.-]+\/[\w.-]+)(?:\s|$)/); | |
| if (slugMatch) { | |
| const slug = slugMatch[1]; | |
| const lower = slug.toLowerCase(); | |
| if (lower !== 'and/or' && lower !== 'client/server' && lower !== 'yes/no' && lower !== 'either/or') { | |
| return slug; | |
| } | |
| } | |
| return undefined; | |
| } |
| const since = await loadLastCheck(ctx); | ||
| const releases: Release[] = []; | ||
| for (const artist of await followedArtists(token)) { | ||
| for (const r of await latestReleases(token, artist)) { | ||
| if (r.date > since) releases.push(r); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fetching releases sequentially in a nested loop for each followed artist can be extremely slow and prone to hitting rate limits or timing out if the user follows many artists. Running these requests in parallel using Promise.allSettled is much more efficient and ensures that a single failing artist fetch does not crash the entire release check.
const since = await loadLastCheck(ctx);
const artists = await followedArtists(token);
const results = await Promise.allSettled(
artists.map(async (artist) => {
const artistReleases = await latestReleases(token, artist);
return artistReleases.filter((r) => r.date > since);
})
);
const releases: Release[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
releases.push(...result.value);
} else {
ctx.log('error', 'Failed to fetch releases for an artist', { error: result.reason });
}
}| try { | ||
| return JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Ask; | ||
| } catch { | ||
| return { isProspect: false, title: '', summary: '' }; | ||
| } |
There was a problem hiding this comment.
LLMs frequently wrap JSON responses in markdown code blocks (e.g., ```json ... ```). If this happens, a direct JSON.parse will throw an error and silently return { isProspect: false }. Stripping any markdown code block formatting before parsing makes the JSON extraction significantly more robust.
try {
const response = await ctx.llm.complete(prompt, { maxTokens: 400 });
const cleaned = response.replace(/```json\s*|```/g, '').trim();
return JSON.parse(cleaned) as Ask;
} catch {
return { isProspect: false, title: '', summary: '' };
}| async function latestVersion(pkg: string): Promise<string | undefined> { | ||
| const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`); | ||
| if (!res.ok) return undefined; | ||
| return ((await res.json()) as { version?: string }).version; | ||
| } |
There was a problem hiding this comment.
If the network request to the npm registry fails (e.g., due to a temporary network glitch, DNS resolution failure, or registry downtime), fetch will throw an unhandled exception and crash the entire cron job. Wrapping the fetch in a try-catch block ensures the agent handles network failures gracefully.
async function latestVersion(pkg: string): Promise<string | undefined> {
try {
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`);
if (!res.ok) return undefined;
return ((await res.json()) as { version?: string }).version;
} catch {
return undefined;
}
}| async function fetchFrontPage(): Promise<Story[]> { | ||
| const res = await fetch('https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=30'); | ||
| const data = (await res.json()) as { hits: Array<{ objectID: string; title: string; url: string | null; points: number }> }; | ||
| return data.hits.map((h) => ({ | ||
| id: Number(h.objectID), | ||
| title: h.title, | ||
| url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`, | ||
| points: h.points | ||
| })); | ||
| } |
There was a problem hiding this comment.
If the Hacker News Algolia API is temporarily down or a network error occurs, the unhandled exception will crash the cron job. Wrapping the API call in a try-catch block and returning an empty array on failure makes the monitor much more resilient.
async function fetchFrontPage(): Promise<Story[]> {
try {
const res = await fetch('https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=30');
if (!res.ok) return [];
const data = (await res.json()) as { hits: Array<{ objectID: string; title: string; url: string | null; points: number }> };
return data.hits.map((h) => ({
id: Number(h.objectID),
title: h.title,
url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`,
points: h.points
}));
} catch {
return [];
}
}Instead of always requiring LINEAR_TEAM_ID, resolve it from the teams the `fetch-teams` sync materializes at /linear/teams/*.json: an explicit LINEAR_TEAM_ID still wins, otherwise auto-pick when there's exactly one team, and block with a helpful list when it's ambiguous. LINEAR_TEAM_ID is now an optional input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
10 issues found across 17 files
You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="vendor-monitor/agent.ts">
<violation number="1" location="vendor-monitor/agent.ts:40">
P2: Handle network/parse failures in `latestVersion`; an exception from one package currently aborts the entire monitor run.</violation>
</file>
<file name="linear/agent.ts">
<violation number="1" location="linear/agent.ts:48">
P1: `readIssueId` uses `data.id` first, which is a comment ID for `comment.create` events; this can make `getIssue` query the wrong ID. Prefer issue-specific fields (`data.issueId` / `data.issue.id`) before falling back.</violation>
</file>
<file name="granola/persona.ts">
<violation number="1" location="granola/persona.ts:24">
P2: The GitHub repo scope is still a placeholder (`your-org/your-repo`), which can prevent this agent from operating on the intended repository.</violation>
</file>
<file name="review/agent.ts">
<violation number="1" location="review/agent.ts:22">
P2: Merging is triggered by any approval event, without validating the approving reviewer identity.</violation>
<violation number="2" location="review/agent.ts:52">
P2: Readiness detection is too permissive and can mark `NOT READY` output as ready. Use an exact sentinel check instead of a word-match regex.</violation>
</file>
<file name="linear/persona.ts">
<violation number="1" location="linear/persona.ts:21">
P1: Scope `comment.create` to the same label as `issue.create`; otherwise any Linear comment can trigger this agent, including the comment it posts back to the issue.</violation>
</file>
<file name="spotify-releases/agent.ts">
<violation number="1" location="spotify-releases/agent.ts:31">
P2: Do not compare `release_date` as raw strings; normalize/parse based on precision before filtering.</violation>
<violation number="2" location="spotify-releases/agent.ts:40">
P2: Handle pagination when fetching followed artists; the current implementation only checks the first 50 artists.</violation>
<violation number="3" location="spotify-releases/agent.ts:47">
P2: Paginate artist albums instead of reading only the first page; otherwise newer matching releases can be missed.</violation>
</file>
<file name="spotify-releases/persona.ts">
<violation number="1" location="spotify-releases/persona.ts:19">
P2: Avoid requiring a raw Spotify bearer token here; this agent is bypassing the repo's VFS-backed integration model and managing a secret directly.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| linear: { | ||
| triggers: [ | ||
| { on: 'issue.create', match: 'agentrelay' }, // only issues labelled "agentrelay" | ||
| { on: 'comment.create' } // …or a new comment on an issue |
There was a problem hiding this comment.
P1: Scope comment.create to the same label as issue.create; otherwise any Linear comment can trigger this agent, including the comment it posts back to the issue.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At linear/persona.ts, line 21:
<comment>Scope `comment.create` to the same label as `issue.create`; otherwise any Linear comment can trigger this agent, including the comment it posts back to the issue.</comment>
<file context>
@@ -0,0 +1,42 @@
+ linear: {
+ triggers: [
+ { on: 'issue.create', match: 'agentrelay' }, // only issues labelled "agentrelay"
+ { on: 'comment.create' } // …or a new comment on an issue
+ ]
+ },
</file context>
|
|
||
| /** Latest published version from the npm registry. */ | ||
| async function latestVersion(pkg: string): Promise<string | undefined> { | ||
| const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`); |
There was a problem hiding this comment.
P2: Handle network/parse failures in latestVersion; an exception from one package currently aborts the entire monitor run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vendor-monitor/agent.ts, line 40:
<comment>Handle network/parse failures in `latestVersion`; an exception from one package currently aborts the entire monitor run.</comment>
<file context>
@@ -0,0 +1,64 @@
+
+/** Latest published version from the npm registry. */
+async function latestVersion(pkg: string): Promise<string | undefined> {
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`);
+ if (!res.ok) return undefined;
+ return ((await res.json()) as { version?: string }).version;
</file context>
| linear: {}, | ||
| // The cloud materializes this repo into the sandbox (ctx.sandbox.cwd) via | ||
| // relayfile — the agent never clones it. | ||
| github: { scope: { repo: 'your-org/your-repo' } } |
There was a problem hiding this comment.
P2: The GitHub repo scope is still a placeholder (your-org/your-repo), which can prevent this agent from operating on the intended repository.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At granola/persona.ts, line 24:
<comment>The GitHub repo scope is still a placeholder (`your-org/your-repo`), which can prevent this agent from operating on the intended repository.</comment>
<file context>
@@ -0,0 +1,42 @@
+ linear: {},
+ // The cloud materializes this repo into the sandbox (ctx.sandbox.cwd) via
+ // relayfile — the agent never clones it.
+ github: { scope: { repo: 'your-org/your-repo' } }
+ },
+
</file context>
| } | ||
|
|
||
| async function latestReleases(token: string, artist: { id: string; name: string }): Promise<Release[]> { | ||
| const data = (await spotify(token, `/artists/${artist.id}/albums?include_groups=album,single&limit=5`)) as { |
There was a problem hiding this comment.
P2: Paginate artist albums instead of reading only the first page; otherwise newer matching releases can be missed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spotify-releases/agent.ts, line 47:
<comment>Paginate artist albums instead of reading only the first page; otherwise newer matching releases can be missed.</comment>
<file context>
@@ -0,0 +1,86 @@
+}
+
+async function latestReleases(token: string, artist: { id: string; name: string }): Promise<Release[]> {
+ const data = (await spotify(token, `/artists/${artist.id}/albums?include_groups=album,single&limit=5`)) as {
+ items?: Array<{ name: string; release_date: string; external_urls: { spotify: string } }>;
+ };
</file context>
| const releases: Release[] = []; | ||
| for (const artist of await followedArtists(token)) { | ||
| for (const r of await latestReleases(token, artist)) { | ||
| if (r.date > since) releases.push(r); |
There was a problem hiding this comment.
P2: Do not compare release_date as raw strings; normalize/parse based on precision before filtering.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spotify-releases/agent.ts, line 31:
<comment>Do not compare `release_date` as raw strings; normalize/parse based on precision before filtering.</comment>
<file context>
@@ -0,0 +1,86 @@
+ const releases: Release[] = [];
+ for (const artist of await followedArtists(token)) {
+ for (const r of await latestReleases(token, artist)) {
+ if (r.date > since) releases.push(r);
+ }
+ }
</file context>
| }); | ||
|
|
||
| async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> { | ||
| const data = (await spotify(token, '/me/following?type=artist&limit=50')) as { |
There was a problem hiding this comment.
P2: Handle pagination when fetching followed artists; the current implementation only checks the first 50 artists.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spotify-releases/agent.ts, line 40:
<comment>Handle pagination when fetching followed artists; the current implementation only checks the first 50 artists.</comment>
<file context>
@@ -0,0 +1,86 @@
+});
+
+async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> {
+ const data = (await spotify(token, '/me/following?type=artist&limit=50')) as {
+ artists?: { items?: Array<{ id: string; name: string }> };
+ };
</file context>
|
|
||
| inputs: { | ||
| SLACK_USER: { description: 'Your Slack user id — releases are DMed here.', env: 'SLACK_USER' }, | ||
| SPOTIFY_TOKEN: { description: 'Spotify OAuth token with the user-follow-read scope.', env: 'SPOTIFY_TOKEN' } |
There was a problem hiding this comment.
P2: Avoid requiring a raw Spotify bearer token here; this agent is bypassing the repo's VFS-backed integration model and managing a secret directly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spotify-releases/persona.ts, line 19:
<comment>Avoid requiring a raw Spotify bearer token here; this agent is bypassing the repo's VFS-backed integration model and managing a secret directly.</comment>
<file context>
@@ -0,0 +1,27 @@
+
+ inputs: {
+ SLACK_USER: { description: 'Your Slack user id — releases are DMed here.', env: 'SLACK_USER' },
+ SPOTIFY_TOKEN: { description: 'Spotify OAuth token with the user-follow-read scope.', env: 'SPOTIFY_TOKEN' }
+ },
+
</file context>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="vendor-monitor/agent.ts">
<violation number="1" location="vendor-monitor/agent.ts:40">
P2: Handle network/parse failures in `latestVersion`; an exception from one package currently aborts the entire monitor run.</violation>
</file>
<file name="granola/persona.ts">
<violation number="1" location="granola/persona.ts:24">
P2: The GitHub repo scope is still a placeholder (`your-org/your-repo`), which can prevent this agent from operating on the intended repository.</violation>
</file>
<file name="linear/persona.ts">
<violation number="1" location="linear/persona.ts:21">
P1: Scope `comment.create` to the same label as `issue.create`; otherwise any Linear comment can trigger this agent, including the comment it posts back to the issue.</violation>
</file>
<file name="spotify-releases/agent.ts">
<violation number="1" location="spotify-releases/agent.ts:31">
P2: Do not compare `release_date` as raw strings; normalize/parse based on precision before filtering.</violation>
<violation number="2" location="spotify-releases/agent.ts:40">
P2: Handle pagination when fetching followed artists; the current implementation only checks the first 50 artists.</violation>
<violation number="3" location="spotify-releases/agent.ts:47">
P2: Paginate artist albums instead of reading only the first page; otherwise newer matching releases can be missed.</violation>
</file>
<file name="spotify-releases/persona.ts">
<violation number="1" location="spotify-releases/persona.ts:19">
P2: Avoid requiring a raw Spotify bearer token here; this agent is bypassing the repo's VFS-backed integration model and managing a secret directly.</violation>
</file>
<file name="granola/agent.ts">
<violation number="1" location="granola/agent.ts:106">
P1: Avoid unquoted shell interpolation for `RELAYFILE_MOUNT_ROOT` when building the `find` command; quote/escape the path to prevent command injection and path parsing errors.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| async function listLinearTeams(ctx: WorkforceCtx): Promise<Array<{ id: string; name: string }>> { | ||
| const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? ''; | ||
| const { output } = await ctx.sandbox.exec( | ||
| `find ${root}/linear/teams -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true` |
There was a problem hiding this comment.
P1: Avoid unquoted shell interpolation for RELAYFILE_MOUNT_ROOT when building the find command; quote/escape the path to prevent command injection and path parsing errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At granola/agent.ts, line 106:
<comment>Avoid unquoted shell interpolation for `RELAYFILE_MOUNT_ROOT` when building the `find` command; quote/escape the path to prevent command injection and path parsing errors.</comment>
<file context>
@@ -82,6 +81,42 @@ async function classify(ctx: WorkforceCtx, transcript: string): Promise<Ask> {
+async function listLinearTeams(ctx: WorkforceCtx): Promise<Array<{ id: string; name: string }>> {
+ const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? '';
+ const { output } = await ctx.sandbox.exec(
+ `find ${root}/linear/teams -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true`
+ );
+ const teams: Array<{ id: string; name: string }> = [];
</file context>
…parsing Prompts/flow: - linear, granola: implement comprehensively (not "small"); open the PR via the GitHub integration, not `gh`/git (repo is a tarball mount, no .git). - linear: comment path now only fires on an @-mention of the agent (configurable MENTION) and never on its own reply; correct issue id for comment events (issueId/issue.id before data.id); repo parsing restricted to github URLs to avoid false positives like "client/server". Robustness: - hn-monitor, vendor-monitor: try/catch around fetch so a transient outage doesn't crash the run. - spotify-releases: fetch artists in parallel via Promise.allSettled. - granola: strip ```json fences before JSON.parse. review: - validate the PR number is an integer before interpolating into the merge command (command-injection guard). - exact READY sentinel (last line) instead of a permissive /READY/ match. - post to a Slack channel referencing the PR author instead of DMing a fixed user; honor approvals only from an optional APPROVERS allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review — addressed in the latest commits: Prompts / flow
Robustness
review
Deliberately left
|
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@granola/agent.ts`:
- Around line 69-81: The classify function parses LLM JSON into an Ask but
doesn’t validate fields; update classify to validate the parsed object from
ctx.llm.complete before returning it: after JSON.parse, confirm the result is an
object with boolean isProspect and string title and summary (and sanitize/trim
them), return that validated Ask only if checks pass, otherwise fall back to {
isProspect: false, title: '', summary: '' }; keep error handling for parse
failures and ensure any non-conforming values (e.g., missing or wrong types)
trigger the safe fallback.
- Around line 104-107: The code interpolates RELAYFILE_MOUNT_ROOT into a shell
string passed to ctx.sandbox.exec (via the root constant), which is a
command-injection risk; replace this by constructing a safe, validated path and
passing it as an argument instead of raw string interpolation: compute a
normalized path with path.resolve/path.join (use RELAYFILE_MOUNT_ROOT -> root),
validate it contains no shell metacharacters (or use a shell-escaping utility),
and call ctx.sandbox.exec in a way that supplies the path as a single argument
(or use an execFile-style API) rather than embedding it inside the backticked
command string `find ${root}/...`. Ensure you update the call site that builds
the `find` command to use the sanitized/escaped path variable or argument form
so ctx.sandbox.exec never receives untrusted interpolated shell content.
In `@linear/agent.ts`:
- Line 58: The current fallback regex in linear/agent.ts (the return statement
using text.match(/(?:^|\s)([\w.-]+\/[\w.-]+)(?:\s|$)/)?.[1]) is too permissive
and matches many non-repo tokens; tighten it by replacing that match with a
stricter pattern that either recognizes full GitHub URLs
(https?://(www\.)?github\.com/owner/repo) or a standalone owner/repo where owner
is limited to alphanumerics and hyphens and repo to alphanumerics, dots,
underscores or hyphens (e.g. owner: [A-Za-z0-9-]+, repo: [A-Za-z0-9._-]+), and
ensure the match is not part of an email or prefixed with '@'; update the return
expression that calls text.match(...) accordingly so only valid GitHub repo
slugs are returned.
In `@linear/persona.ts`:
- Around line 20-22: The comment-trigger is unscoped: add the same label gating
used for { on: 'issue.create', match: 'agentrelay' } to the comment path or
enforce it in the handler; specifically either change the trigger object { on:
'comment.create' } to { on: 'comment.create', match: 'agentrelay' } or, if you
prefer handler-side checks, inspect the comment's referenced issue labels (look
for 'agentrelay') before calling the harness to ensure only comments on issues
labeled 'agentrelay' invoke the PR-generation harness.
In `@review/agent.ts`:
- Around line 72-84: readPr currently returns a Pr using loose truthy checks;
add runtime validation to ensure number is a positive integer and owner/repo are
non-empty safe strings (no whitespace, shell-special chars, or path separators)
before returning. Update the readPr function to (1) validate number via
Number.isInteger(number) && number > 0, (2) validate owner and repo with a
simple whitelist regex (e.g. /^[A-Za-z0-9_.-]+$/) and non-empty checks, and (3)
return undefined if any check fails so malformed payloads never reach
ctx.sandbox.exec; keep references to the existing properties (p.pull_request,
p.check_run.pull_requests[0], p.number, p.repository.owner.login,
p.repository.name) when locating fields.
- Around line 22-25: The current handler calls mergePr for any approved review
(event.type === 'pull_request_review.submitted' && isApproval(event.payload));
add an authorization guard so only a specific approver triggers the merge:
implement isAuthorizedApprover(ctx, payload) that extracts reviewer login from
payload.review.user.login, reads allowed approver via input(ctx,
'GITHUB_APPROVER'), and compares both lowercased; then change the condition to
require both isApproval(event.payload) and isAuthorizedApprover(ctx,
event.payload) before calling readPr/mergePr so only the configured approver can
cause mergePr to run.
In `@spotify-releases/agent.ts`:
- Around line 39-44: The followedArtists function currently fetches only the
first page (/me/following?type=artist&limit=50) and misses additional pages;
update followedArtists to loop/paginate using the Spotify pagination fields
(response.artists.next or response.artists.cursors.after) by repeatedly calling
spotify(token, nextUrlOrPath) until no next is returned, accumulating items from
each page into a single array and returning that array; be sure to handle
missing artists or items safely (artists?.items) and preserve the same return
type Array<{ id: string; name: string }>.
- Line 31: The code compares r.date (a Spotify release_date string) to since
directly, which fails for precision values "month" or "year"; update the mapping
that creates r (the release object) to include r.release_date_precision, and
change the filter so it normalizes/parses r.date according to
r.release_date_precision (treat "year" as YYYY-01-01 and "month" as YYYY-MM-01
or use Date.UTC) into a Date (or ISO full date) before comparing to the since
Date; ensure the comparison uses the parsed Date (e.g., releaseDate.getTime())
rather than raw strings so releases with precision "month" or "year" are
correctly included.
In `@vendor-monitor/agent.ts`:
- Around line 39-43: The latestVersion function currently lets fetch/json throw
and abort the whole scan; wrap the network call and JSON parsing in a try/catch
inside latestVersion so per-package failures are caught, log the error (e.g.,
console.error or the existing logger) including the package name and error, and
return undefined on failure so the scan continues for other packages; ensure you
still encodeURIComponent(pkg) and keep the same return type
(Promise<string|undefined>).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c77f134e-8b9e-45f8-8e3d-7e7409ea9807
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (16)
.gitignoreREADME.mdgranola/agent.tsgranola/persona.tshn-monitor/agent.tshn-monitor/persona.tslinear/agent.tslinear/persona.tspackage.jsonreview/agent.tsreview/persona.tsspotify-releases/agent.tsspotify-releases/persona.tstsconfig.jsonvendor-monitor/agent.tsvendor-monitor/persona.ts
| async function classify(ctx: WorkforceCtx, transcript: string): Promise<Ask> { | ||
| const prompt = [ | ||
| 'Read this meeting transcript. Decide if it is a sales/prospect call where the', | ||
| 'prospect asked for a feature or change. Reply with JSON only:', | ||
| '{"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"}', | ||
| '', | ||
| transcript.slice(0, 8000) | ||
| ].join('\n'); | ||
| try { | ||
| return JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Ask; | ||
| } catch { | ||
| return { isProspect: false, title: '', summary: '' }; | ||
| } |
There was a problem hiding this comment.
Validate LLM output shape before treating it as Ask.
JSON.parse success doesn’t guarantee required fields exist or have correct types; isProspect: true with missing/invalid title/summary can break or degrade issue creation.
Proposed fix
async function classify(ctx: WorkforceCtx, transcript: string): Promise<Ask> {
@@
try {
- return JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Ask;
+ const raw = JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Partial<Ask>;
+ if (
+ typeof raw?.isProspect === 'boolean' &&
+ typeof raw?.title === 'string' &&
+ typeof raw?.summary === 'string'
+ ) {
+ return {
+ isProspect: raw.isProspect,
+ title: raw.title.trim(),
+ summary: raw.summary.trim()
+ };
+ }
+ return { isProspect: false, title: '', summary: '' };
} catch {
return { isProspect: false, title: '', summary: '' };
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function classify(ctx: WorkforceCtx, transcript: string): Promise<Ask> { | |
| const prompt = [ | |
| 'Read this meeting transcript. Decide if it is a sales/prospect call where the', | |
| 'prospect asked for a feature or change. Reply with JSON only:', | |
| '{"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"}', | |
| '', | |
| transcript.slice(0, 8000) | |
| ].join('\n'); | |
| try { | |
| return JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Ask; | |
| } catch { | |
| return { isProspect: false, title: '', summary: '' }; | |
| } | |
| async function classify(ctx: WorkforceCtx, transcript: string): Promise<Ask> { | |
| const prompt = [ | |
| 'Read this meeting transcript. Decide if it is a sales/prospect call where the', | |
| 'prospect asked for a feature or change. Reply with JSON only:', | |
| '{"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"}', | |
| '', | |
| transcript.slice(0, 8000) | |
| ].join('\n'); | |
| try { | |
| const raw = JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Partial<Ask>; | |
| if ( | |
| typeof raw?.isProspect === 'boolean' && | |
| typeof raw?.title === 'string' && | |
| typeof raw?.summary === 'string' | |
| ) { | |
| return { | |
| isProspect: raw.isProspect, | |
| title: raw.title.trim(), | |
| summary: raw.summary.trim() | |
| }; | |
| } | |
| return { isProspect: false, title: '', summary: '' }; | |
| } catch { | |
| return { isProspect: false, title: '', summary: '' }; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@granola/agent.ts` around lines 69 - 81, The classify function parses LLM JSON
into an Ask but doesn’t validate fields; update classify to validate the parsed
object from ctx.llm.complete before returning it: after JSON.parse, confirm the
result is an object with boolean isProspect and string title and summary (and
sanitize/trim them), return that validated Ask only if checks pass, otherwise
fall back to { isProspect: false, title: '', summary: '' }; keep error handling
for parse failures and ensure any non-conforming values (e.g., missing or wrong
types) trigger the safe fallback.
| const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? ''; | ||
| const { output } = await ctx.sandbox.exec( | ||
| `find ${root}/linear/teams -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true` | ||
| ); |
There was a problem hiding this comment.
Avoid interpolating env-derived paths directly into a shell command.
RELAYFILE_MOUNT_ROOT is injected into ctx.sandbox.exec(...) without escaping, which is a command-injection risk.
Proposed fix
async function listLinearTeams(ctx: WorkforceCtx): Promise<Array<{ id: string; name: string }>> {
const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? '';
+ const teamsDir = `${root}/linear/teams`;
+ const safeTeamsDir = JSON.stringify(teamsDir);
const { output } = await ctx.sandbox.exec(
- `find ${root}/linear/teams -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true`
+ `find ${safeTeamsDir} -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true`
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? ''; | |
| const { output } = await ctx.sandbox.exec( | |
| `find ${root}/linear/teams -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true` | |
| ); | |
| const root = process.env.RELAYFILE_MOUNT_ROOT?.replace(/\/$/, '') ?? ''; | |
| const teamsDir = `${root}/linear/teams`; | |
| const safeTeamsDir = JSON.stringify(teamsDir); | |
| const { output } = await ctx.sandbox.exec( | |
| `find ${safeTeamsDir} -maxdepth 1 -name '*.json' -not -name '_index.json' 2>/dev/null || true` | |
| ); |
🧰 Tools
🪛 OpenGrep (1.22.0)
[ERROR] 105-107: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@granola/agent.ts` around lines 104 - 107, The code interpolates
RELAYFILE_MOUNT_ROOT into a shell string passed to ctx.sandbox.exec (via the
root constant), which is a command-injection risk; replace this by constructing
a safe, validated path and passing it as an argument instead of raw string
interpolation: compute a normalized path with path.resolve/path.join (use
RELAYFILE_MOUNT_ROOT -> root), validate it contains no shell metacharacters (or
use a shell-escaping utility), and call ctx.sandbox.exec in a way that supplies
the path as a single argument (or use an execFile-style API) rather than
embedding it inside the backticked command string `find ${root}/...`. Ensure you
update the call site that builds the `find` command to use the sanitized/escaped
path variable or argument form so ctx.sandbox.exec never receives untrusted
interpolated shell content.
| { on: 'issue.create', match: 'agentrelay' }, // only issues labelled "agentrelay" | ||
| { on: 'comment.create' } // …or a new comment on an issue | ||
| ] |
There was a problem hiding this comment.
comment.create is currently unscoped and can trigger on unrelated issues.
This allows PR-generation runs outside the intended agentrelay workflow. Add equivalent label gating for comment-driven runs (trigger-side or handler-side) before invoking the harness.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@linear/persona.ts` around lines 20 - 22, The comment-trigger is unscoped: add
the same label gating used for { on: 'issue.create', match: 'agentrelay' } to
the comment path or enforce it in the handler; specifically either change the
trigger object { on: 'comment.create' } to { on: 'comment.create', match:
'agentrelay' } or, if you prefer handler-side checks, inspect the comment's
referenced issue labels (look for 'agentrelay') before calling the harness to
ensure only comments on issues labeled 'agentrelay' invoke the PR-generation
harness.
| function readPr(payload: unknown): Pr | undefined { | ||
| const p = payload as { | ||
| number?: number; | ||
| pull_request?: { number?: number; html_url?: string }; | ||
| check_run?: { pull_requests?: Array<{ number?: number; html_url?: string }> }; | ||
| repository?: { name?: string; owner?: { login?: string } }; | ||
| } | null; | ||
| const pr = p?.pull_request ?? p?.check_run?.pull_requests?.[0]; | ||
| const number = pr?.number ?? p?.number; | ||
| const owner = p?.repository?.owner?.login; | ||
| const repo = p?.repository?.name; | ||
| if (!number || !owner || !repo) return undefined; | ||
| return { owner, repo, number, url: pr?.html_url ?? `https://github.com/${owner}/${repo}/pull/${number}` }; |
There was a problem hiding this comment.
Validate webhook fields strictly before constructing CLI commands.
readPr currently relies on truthy checks only. Add runtime type validation (number as positive integer, owner/repo as safe strings) before returning Pr to prevent malformed payload data flowing into ctx.sandbox.exec.
Suggested hardening
function readPr(payload: unknown): Pr | undefined {
const p = payload as {
number?: number;
pull_request?: { number?: number; html_url?: string };
check_run?: { pull_requests?: Array<{ number?: number; html_url?: string }> };
repository?: { name?: string; owner?: { login?: string } };
} | null;
const pr = p?.pull_request ?? p?.check_run?.pull_requests?.[0];
- const number = pr?.number ?? p?.number;
+ const number = pr?.number ?? p?.number;
const owner = p?.repository?.owner?.login;
const repo = p?.repository?.name;
- if (!number || !owner || !repo) return undefined;
+ if (!Number.isInteger(number) || number <= 0) return undefined;
+ if (typeof owner !== 'string' || typeof repo !== 'string') return undefined;
+ if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) return undefined;
return { owner, repo, number, url: pr?.html_url ?? `https://github.com/${owner}/${repo}/pull/${number}` };
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@review/agent.ts` around lines 72 - 84, readPr currently returns a Pr using
loose truthy checks; add runtime validation to ensure number is a positive
integer and owner/repo are non-empty safe strings (no whitespace, shell-special
chars, or path separators) before returning. Update the readPr function to (1)
validate number via Number.isInteger(number) && number > 0, (2) validate owner
and repo with a simple whitelist regex (e.g. /^[A-Za-z0-9_.-]+$/) and non-empty
checks, and (3) return undefined if any check fails so malformed payloads never
reach ctx.sandbox.exec; keep references to the existing properties
(p.pull_request, p.check_run.pull_requests[0], p.number,
p.repository.owner.login, p.repository.name) when locating fields.
| const releases: Release[] = []; | ||
| for (const artist of await followedArtists(token)) { | ||
| for (const r of await latestReleases(token, artist)) { | ||
| if (r.date > since) releases.push(r); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Spotify album objects, what values can release_date_precisiontake, and how shouldrelease_datebe interpreted foryearandmonth precision?
💡 Result:
For Spotify album objects, the release_date_precision field can take one of three string values: "year", "month", or "day" [1][2]. The release_date field should be interpreted based on the corresponding precision value provided [1][3]: - "year": The release_date string contains only the year (e.g., "1981") [4][5]. - "month": The release_date string is formatted as "YYYY-MM" (e.g., "1981-12") [4][5]. - "day": The release_date string is a complete date in "YYYY-MM-DD" format (e.g., "1981-12-15") [3]. In cases where the release date is imprecise, the release_date_precision field acts as a qualifier to indicate which portion of the release_date string is accurate [3].
Citations:
- 1: https://developer.spotify.com/documentation/web-api/reference/get-track
- 2: https://developer.spotify.com/documentation/web-api/reference/get-new-releases
- 3: https://developer.spotify.com/documentation/web-api/concepts/api-calls
- 4: https://developer.spotify.com/documentation/web-api/reference/get-an-album
- 5: https://developer.spotify.com/documentation/web-api/reference/search
Fix Spotify release-date filtering to respect release_date_precision
File: spotify-releases/agent.ts (line 31)
Line 31 compares r.date to since as raw strings, but Spotify’s album release_date is only a full YYYY-MM-DD when release_date_precision is day. For month and year, Spotify returns release_date as YYYY-MM or YYYY respectively, so lexical comparisons can incorrectly drop valid releases. The mapping also needs to capture release_date_precision (lines 47-54).
💡 Proposed fix
interface Release {
name: string;
artist: string;
date: string;
+ datePrecision: 'day' | 'month' | 'year';
url: string;
}
@@
- if (r.date > since) releases.push(r);
+ if (normalizeReleaseDate(r.date, r.datePrecision) > since) releases.push(r);
@@
const data = (await spotify(token, `/artists/${artist.id}/albums?include_groups=album,single&limit=5`)) as {
- items?: Array<{ name: string; release_date: string; external_urls: { spotify: string } }>;
+ items?: Array<{
+ name: string;
+ release_date: string;
+ release_date_precision: 'day' | 'month' | 'year';
+ external_urls: { spotify: string };
+ }>;
};
return (data.items ?? []).map((a) => ({
name: a.name,
artist: artist.name,
date: a.release_date,
+ datePrecision: a.release_date_precision,
url: a.external_urls.spotify
}));
}
+
+function normalizeReleaseDate(date: string, precision: 'day' | 'month' | 'year'): string {
+ if (precision === 'year') return `${date}-12-31`;
+ if (precision === 'month') return `${date}-31`;
+ return date;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (r.date > since) releases.push(r); | |
| interface Release { | |
| name: string; | |
| artist: string; | |
| date: string; | |
| datePrecision: 'day' | 'month' | 'year'; | |
| url: string; | |
| } | |
| // ... existing code ... | |
| if (normalizeReleaseDate(r.date, r.datePrecision) > since) releases.push(r); | |
| // ... existing code ... | |
| const data = (await spotify(token, `/artists/${artist.id}/albums?include_groups=album,single&limit=5`)) as { | |
| items?: Array<{ | |
| name: string; | |
| release_date: string; | |
| release_date_precision: 'day' | 'month' | 'year'; | |
| external_urls: { spotify: string }; | |
| }>; | |
| }; | |
| return (data.items ?? []).map((a) => ({ | |
| name: a.name, | |
| artist: artist.name, | |
| date: a.release_date, | |
| datePrecision: a.release_date_precision, | |
| url: a.external_urls.spotify | |
| })); | |
| } | |
| function normalizeReleaseDate(date: string, precision: 'day' | 'month' | 'year'): string { | |
| if (precision === 'year') return `${date}-12-31`; | |
| if (precision === 'month') return `${date}-31`; | |
| return date; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@spotify-releases/agent.ts` at line 31, The code compares r.date (a Spotify
release_date string) to since directly, which fails for precision values "month"
or "year"; update the mapping that creates r (the release object) to include
r.release_date_precision, and change the filter so it normalizes/parses r.date
according to r.release_date_precision (treat "year" as YYYY-01-01 and "month" as
YYYY-MM-01 or use Date.UTC) into a Date (or ISO full date) before comparing to
the since Date; ensure the comparison uses the parsed Date (e.g.,
releaseDate.getTime()) rather than raw strings so releases with precision
"month" or "year" are correctly included.
| async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> { | ||
| const data = (await spotify(token, '/me/following?type=artist&limit=50')) as { | ||
| artists?: { items?: Array<{ id: string; name: string }> }; | ||
| }; | ||
| return data.artists?.items ?? []; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Spotify Web API /me/following?type=artist, what is the pagination mechanism (cursor/after), and does limit=50 return only the first page unless paginated?
💡 Result:
The Spotify Web API endpoint /me/following?type=artist uses a cursor-based pagination mechanism [1][2]. In this system: - The response includes a cursors object containing an "after" key, which provides the cursor value needed to fetch the next page of results [1][3]. - The API response also includes a "next" field, which is a URL string that provides the complete endpoint call required to retrieve the subsequent page [1][2]. If there are no further items, this value is null [1][2]. Regarding the "limit=50" parameter: - The limit parameter defines the maximum number of items to return in a single request, with a valid range of 1 to 50 (defaulting to 20 if not specified) [1][2]. - Requesting limit=50 does not automatically retrieve all followed artists; it simply sets the page size to the maximum allowed value [1][2]. To retrieve the complete list of followed artists, you must implement pagination by performing subsequent requests using the cursor value provided in the "after" field of the previous response, or by following the "next" URL [1][4]. If you do not perform these subsequent requests, you will receive only the first "page" (the first 50 results) [1][4].
Citations:
- 1: https://developer.spotify.com/documentation/web-api/reference/get-followed
- 2: https://spotifyweb.apidog.io/project-347255/api-3543089
- 3: https://apifox.com/apidoc/project-406112/api-5365333
- 4: https://community.spotify.com/t5/Spotify-for-Developers/Problems-with-quot-Get-Followed-Artists-quot-API-getting-next/td-p/5513680
Paginate followed artists to avoid missing updates for users with >50 follows.
followedArtists() fetches only /me/following?type=artist&limit=50 and never follows Spotify’s cursor pagination (cursors.after / next), so artists beyond the first 50 are never checked.
💡 Proposed fix
async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> {
- const data = (await spotify(token, '/me/following?type=artist&limit=50')) as {
- artists?: { items?: Array<{ id: string; name: string }>;
- };
- return data.artists?.items ?? [];
+ const all: Array<{ id: string; name: string }> = [];
+ let after: string | undefined;
+
+ do {
+ const page = (await spotify(
+ token,
+ `/me/following?type=artist&limit=50${after ? `&after=${encodeURIComponent(after)}` : ''}`
+ )) as {
+ artists?: {
+ items?: Array<{ id: string; name: string }>;
+ cursors?: { after?: string };
+ };
+ };
+
+ const items = page.artists?.items ?? [];
+ all.push(...items);
+ after = page.artists?.cursors?.after;
+ } while (after);
+
+ return all;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> { | |
| const data = (await spotify(token, '/me/following?type=artist&limit=50')) as { | |
| artists?: { items?: Array<{ id: string; name: string }> }; | |
| }; | |
| return data.artists?.items ?? []; | |
| } | |
| async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> { | |
| const all: Array<{ id: string; name: string }> = []; | |
| let after: string | undefined; | |
| do { | |
| const page = (await spotify( | |
| token, | |
| `/me/following?type=artist&limit=50${after ? `&after=${encodeURIComponent(after)}` : ''}` | |
| )) as { | |
| artists?: { | |
| items?: Array<{ id: string; name: string }>; | |
| cursors?: { after?: string }; | |
| }; | |
| }; | |
| const items = page.artists?.items ?? []; | |
| all.push(...items); | |
| after = page.artists?.cursors?.after; | |
| } while (after); | |
| return all; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@spotify-releases/agent.ts` around lines 39 - 44, The followedArtists function
currently fetches only the first page (/me/following?type=artist&limit=50) and
misses additional pages; update followedArtists to loop/paginate using the
Spotify pagination fields (response.artists.next or
response.artists.cursors.after) by repeatedly calling spotify(token,
nextUrlOrPath) until no next is returned, accumulating items from each page into
a single array and returning that array; be sure to handle missing artists or
items safely (artists?.items) and preserve the same return type Array<{ id:
string; name: string }>.
| async function latestVersion(pkg: string): Promise<string | undefined> { | ||
| const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`); | ||
| if (!res.ok) return undefined; | ||
| return ((await res.json()) as { version?: string }).version; | ||
| } |
There was a problem hiding this comment.
Isolate per-package fetch failures so one network error doesn’t kill the run.
fetch can throw; currently that aborts the entire scan. Catch/log per package and continue.
💡 Proposed fix
- for (const pkg of vendors) {
- const version = await latestVersion(pkg);
+ for (const pkg of vendors) {
+ const version = await latestVersion(ctx, pkg);
if (!version) continue;
current[pkg] = version;
@@
-async function latestVersion(pkg: string): Promise<string | undefined> {
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`);
- if (!res.ok) return undefined;
- return ((await res.json()) as { version?: string }).version;
+async function latestVersion(ctx: WorkforceCtx, pkg: string): Promise<string | undefined> {
+ try {
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`);
+ if (!res.ok) {
+ ctx.log('warn', 'vendor-monitor.latest-version.non-ok', { pkg, status: res.status });
+ return undefined;
+ }
+ return ((await res.json()) as { version?: string }).version;
+ } catch (error) {
+ ctx.log('warn', 'vendor-monitor.latest-version.error', { pkg, error: String(error) });
+ return undefined;
+ }
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@vendor-monitor/agent.ts` around lines 39 - 43, The latestVersion function
currently lets fetch/json throw and abort the whole scan; wrap the network call
and JSON parsing in a try/catch inside latestVersion so per-package failures are
caught, log the error (e.g., console.error or the existing logger) including the
package name and error, and return undefined on failure so the scan continues
for other packages; ensure you still encodeURIComponent(pkg) and keep the same
return type (Promise<string|undefined>).
What
Implements all six agents described in the repo's READMEs — each as a typed
persona.ts(triggers + run config) and anagent.tshandler — plus a project scaffold (package.json/tsconfig.json/package-lock.json) and a README listing them.file.created)issue.create(labelled) /comment.createDesign
definePersonafrom@agentworkforce/persona-kit, so eachtriggers[].onautocompletes the provider's catalog events and is linted at deploy.ctxclients; no direct API calls or tokens.ctx.sandbox.cwdvia relayfile, so handlers hand the work toctx.harness.runrather thangit clone. Thelinearagent additionally parses a repo named in the issue (github.com/owner/repoorowner/repo) and points the agent there.reviewandlinearrun on thecodexharness;granolaonclaude; the schedule-only agents use a light model (or none).Verification
npm run typecheck(tsc over all six agents) passes against@agentworkforce/runtime+persona-kit3.0.24.🤖 Generated with Claude Code
Summary by cubic
Implements six proactive showcase agents with typed personas and runtime handlers, plus a scaffold and README. Agents use VFS-backed integrations and materialized GitHub workspaces (no
git clone);granolanow auto-resolves the Linear team, and we hardened prompts (nogit/gh), event handling, and the review/merge flow for safety and reliability.granola: on storagefile.createdfor Granola notes → classify prospect calls → create Linear issue → open GitHub PR and comment link; auto-resolves the Linear team from synced teams.hn-monitor: 2×/day schedule → scan HN by topics → Slack digest with dedupe via durable memory.linear: on Linearissue.create(labelagentrelay) andcomment.create→ implement → open GitHub PR → comment PR link; comment path triggers only onMENTIONand skips its own replies; supports explicitgithub.com/owner/repoparsing from issue text.review: on PR/open/update/review/comments/CI → review, auto-fix, resolve CI/conflicts, post Slack updates when ready, merge on approval (honors optionalAPPROVERSallowlist).spotify-releases: daily schedule → new releases from followed artists → Slack DM; tracks last check in memory.vendor-monitor: weekday AM → watch npm packages for version bumps → team Slack post; persists seen versions.Written for commit 090833b. Summary will update on new commits. Review in cubic