Skip to content

feat: implement the six showcase proactive agents#1

Merged
khaliqgant merged 4 commits into
mainfrom
feat/showcase-proactive-agents
May 26, 2026
Merged

feat: implement the six showcase proactive agents#1
khaliqgant merged 4 commits into
mainfrom
feat/showcase-proactive-agents

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented May 26, 2026

What

Implements all six agents described in the repo's READMEs — each as a typed persona.ts (triggers + run config) and an agent.ts handler — plus a project scaffold (package.json / tsconfig.json / package-lock.json) and a README listing them.

Agent Fires on Does
granola new Granola note (Nango sync → file.created) detect prospect call → Linear issue → GitHub PR
hn-monitor schedule (2×/day) HN topic scan → Slack digest
linear Linear issue.create (labelled) / comment.create implement → GitHub PR → comment link
review GitHub PR opened / synchronize / review / review-comment / check_run review, fix (own + other bots'), resolve CI/conflicts, DM when ready, merge on approval
spotify-releases schedule (daily) new releases from followed artists → Slack DM
vendor-monitor schedule (weekday AM) npm vendor version bumps → team Slack

Design

  • Typed authoringdefinePersona from @agentworkforce/persona-kit, so each triggers[].on autocompletes the provider's catalog events and is linted at deploy.
  • VFS-backed integrations — Slack/Linear/GitHub/Granola via the typed ctx clients; no direct API calls or tokens.
  • No clone — for code-touching agents the cloud materializes the repo into ctx.sandbox.cwd via relayfile, so handlers hand the work to ctx.harness.run rather than git clone. The linear agent additionally parses a repo named in the issue (github.com/owner/repo or owner/repo) and points the agent there.
  • review and linear run on the codex harness; granola on claude; the schedule-only agents use a light model (or none).

Verification

  • npm run typecheck (tsc over all six agents) passes against @agentworkforce/runtime + persona-kit 3.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); granola now auto-resolves the Linear team, and we hardened prompts (no git/gh), event handling, and the review/merge flow for safety and reliability.

  • New Features
    • granola: on storage file.created for 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 Linear issue.create (label agentrelay) and comment.create → implement → open GitHub PR → comment PR link; comment path triggers only on MENTION and skips its own replies; supports explicit github.com/owner/repo parsing 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 optional APPROVERS allowlist).
    • 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

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Review Change Stack

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55fa756c-990e-40c4-994f-b568480d4972

📥 Commits

Reviewing files that changed from the base of the PR and between 40d472b and 090833b.

📒 Files selected for processing (5)
  • granola/agent.ts
  • linear/agent.ts
  • linear/persona.ts
  • review/agent.ts
  • review/persona.ts
📝 Walkthrough

Walkthrough

This 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.

Changes

Agent Workforce Agents

Layer / File(s) Summary
Project foundation and documentation
.gitignore, package.json, tsconfig.json, README.md
Standard Node.js ES module scaffold with TypeScript strict mode enabled. Build scripts compile personas and typecheck. README documents agent purpose, persona/handler architecture, local execution, and deployment instructions.
Linear implementer webhook agent
linear/agent.ts, linear/persona.ts
Webhook handler listens for Linear issues labeled agentrelay and comment creation events. For each event, fetches the issue, extracts optional GitHub repo target from issue text, runs a coding harness to implement and open a PR, then comments the PR URL back onto the Linear issue. Persona configures codex harness with gpt-5.5 and high reasoning.
Granola prospect classifier agent
granola/agent.ts, granola/persona.ts
File-event handler triggered when note files are synced to /granola/notes/. Reads note transcript, asks LLM to classify as prospect or non-prospect. For prospect calls, resolves target Linear team (from LINEAR_TEAM_ID env or by auto-detecting from materialized team JSON), creates a Linear issue, runs coding harness to open implementation PR, and comments PR URL onto the issue. Persona uses Claude Sonnet with optional team disambiguation input.
PR review automation agent
review/agent.ts, review/persona.ts
Multi-trigger webhook handler for GitHub PR events and check runs. Merges approved PRs via squash, ignores successful CI completions, and otherwise runs a review-and-fix harness to address failures. Optionally notifies a configured Slack user of readiness/merge status. Persona configures codex harness with gpt-5.5, high reasoning, and 2400s timeout.
Scheduled information agents
hn-monitor/agent.ts, hn-monitor/persona.ts, spotify-releases/agent.ts, spotify-releases/persona.ts, vendor-monitor/agent.ts, vendor-monitor/persona.ts
Three cron-triggered agents that poll external services and post Slack summaries: HN Monitor runs twice daily, fetches Hacker News front-page stories via Algolia API, filters by configured keywords, generates LLM digest, and persists seen story IDs. Spotify Monitor runs daily, checks followed artists for new releases, and sends DM with new releases to configured Slack user. Vendor Monitor runs weekday mornings, checks npm registry for package version updates, and posts changes to Slack channel. All three use durable workspace memory with TTL for seen-state tracking.

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

🐰 Six new agents hop into the fray,
Linear issues fixed, notes classified today,
PR reviews and Slack feeds, Spotify's delight,
Vendor updates tracked through each cron'd night—
A workforce of bots, all dressed to impress!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement the six showcase proactive agents' directly and clearly summarizes the main change—implementing six agents as typed personas and handlers.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing what each agent does, design decisions, and verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/showcase-proactive-agents

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread review/agent.ts Outdated
Comment on lines +80 to +83
const number = pr?.number ?? p?.number;
const owner = p?.repository?.owner?.login;
const repo = p?.repository?.name;
if (!number || !owner || !repo) return undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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.

Suggested change
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;

Comment thread linear/agent.ts
Comment on lines +54 to +59
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];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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;
}

Comment thread spotify-releases/agent.ts
Comment on lines +27 to +33
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);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 });
    }
  }

Comment thread granola/agent.ts
Comment on lines +78 to +82
try {
return JSON.parse((await ctx.llm.complete(prompt, { maxTokens: 400 })).trim()) as Ask;
} catch {
return { isProspect: false, title: '', summary: '' };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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: '' };
  }

Comment thread vendor-monitor/agent.ts
Comment on lines +39 to +43
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
  }
}

Comment thread hn-monitor/agent.ts
Comment on lines +42 to +51
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
}));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread linear/agent.ts Outdated
Comment thread linear/persona.ts Outdated
linear: {
triggers: [
{ on: 'issue.create', match: 'agentrelay' }, // only issues labelled "agentrelay"
{ on: 'comment.create' } // …or a new comment on an issue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread vendor-monitor/agent.ts

/** 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`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread granola/persona.ts
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' } }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread review/agent.ts Outdated
Comment thread review/agent.ts Outdated
Comment thread spotify-releases/agent.ts
}

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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread spotify-releases/agent.ts
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread spotify-releases/agent.ts
});

async function followedArtists(token: string): Promise<Array<{ id: string; name: string }>> {
const data = (await spotify(token, '/me/following?type=artist&limit=50')) as {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread granola/agent.ts
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`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread review/persona.ts Outdated
khaliqgant and others added 2 commits May 26, 2026 15:45
…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>
@khaliqgant
Copy link
Copy Markdown
Member Author

Thanks for the review — addressed in the latest commits:

Prompts / flow

  • linear, granola: implement comprehensively (dropped "small"), and open the PR via the GitHub integration, not gh/git — the repo is a tarball mount with no .git.
  • linear readIssueId (cubic/gemini P1): for comment.create, data.id is the comment id — now prefer data.issueId / data.issue.id and only fall back to data.id.
  • linear comment.create (cubic P1 + your note): now fires only on an @-mention of the agent (configurable MENTION, default @agentrelay) and never on its own reply.
  • linear parseRepo (gemini): restricted to explicit github.com/owner/repo URLs, so phrases like client/server no longer false-match.

Robustness

  • hn-monitor / vendor-monitor: fetch wrapped in try/catch so an outage doesn't crash the run.
  • spotify-releases: artists fetched in parallel via Promise.allSettled.
  • granola: strip ```json fences before JSON.parse.

review

  • command injection (gemini security): the PR number is validated as an integer before interpolation.
  • readiness (cubic): exact READY sentinel on the last line (no longer matches "NOT READY").
  • notification + approver (cubic + your note): posts to a Slack channel referencing the PR author instead of DMing a fixed user; honors approvals only from an optional APPROVERS allowlist.

Deliberately left

  • granola your-org/your-repo (cubic P2): intentional placeholder — it's the repo you configure for your deployment.
  • gh pr merge in review: kept because there's no merge method on the github client yet. Everything else in review avoids gh; happy to switch merge over once the integration exposes it.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0dad795 and 40d472b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • .gitignore
  • README.md
  • granola/agent.ts
  • granola/persona.ts
  • hn-monitor/agent.ts
  • hn-monitor/persona.ts
  • linear/agent.ts
  • linear/persona.ts
  • package.json
  • review/agent.ts
  • review/persona.ts
  • spotify-releases/agent.ts
  • spotify-releases/persona.ts
  • tsconfig.json
  • vendor-monitor/agent.ts
  • vendor-monitor/persona.ts

Comment thread granola/agent.ts
Comment on lines +69 to +81
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: '' };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment thread granola/agent.ts
Comment on lines +104 to +107
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`
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment thread linear/agent.ts Outdated
Comment thread linear/persona.ts Outdated
Comment on lines +20 to +22
{ on: 'issue.create', match: 'agentrelay' }, // only issues labelled "agentrelay"
{ on: 'comment.create' } // …or a new comment on an issue
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread review/agent.ts Outdated
Comment thread review/agent.ts Outdated
Comment on lines +72 to +84
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}` };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread spotify-releases/agent.ts
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


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.

Suggested change
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.

Comment thread spotify-releases/agent.ts
Comment on lines +39 to +44
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 ?? [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


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.

Suggested change
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 }>.

Comment thread vendor-monitor/agent.ts
Comment on lines +39 to +43
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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>).

@khaliqgant khaliqgant merged commit a2bba47 into main May 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant