SLOPSHOPPER

gem-upgrade

Research Ruby gem upgrades and their application impact.

guardtoolagents
v0.1.0no licenseupdated 2026-10-11andreimaxim/claude-gem-security-review-plugin
A shopper browsing a rack in a slop shop
README

Gem upgrade research

A Claude Code mod that gives the main agent one tool for researching a Ruby gem version change. It does not select CVE fixes, update dependencies, or open pull requests.

Try it

Requires Claude Code with mods support. Tested with version 2.1.296. From the application you want to inspect, load this plugin by its absolute path:

claude --plugin-dir /path/to/claude-gem-security-review-plugin

Ask Claude to research a specific upgrade, for example:

Research upgrading faraday from 2.14.2 to 2.14.4 and explain the application impact.

The tool mcp__gem-upgrade__research takes three required strings:

{"gem": "faraday", "currentVersion": "2.14.2", "requestedVersion": "2.14.4"}

It starts a researcher in the current application's directory, using Opus 5.5 with low reasoning effort. The researcher can read files and search the web, but has no shell, editing, or delegation tools. Its prompt is in prompts/research.md.

Report delivery

The tool returns a launch acknowledgement. When the researcher finishes, the mod submits its labelled report as a plugin-authored follow-up message. This wakes the main agent so it can continue without another user prompt. Research failures are labelled as failures; cancelled runs do not wake the main agent.

This is asynchronous because a mod-created subagent runs in the background and a hook cannot wait on its own promise for more than ten seconds. Claude Code 2.1.296 does not wake the main agent for these workers' native completion notifications, so the mod forwards their completion events explicitly. It does not add polling or a second agent process.

For unattended use, keep a streaming-input session open until the main agent answers the report. Pass --input-format stream-json, --output-format stream-json, and --verbose, and close stdin only after the follow-up result. Interactive sessions already stay open. One-shot claude -p and claude plugin eval can exit before the completion hook queues its message in 2.1.296; output streaming alone does not prevent that shutdown race.

The output can contain an initial result with a waiting message and a later result with the main agent's answer. The raw worker report is also in the completed task_notification event's summary. Add --forward-subagent-text to retain worker text and thinking alongside tool calls for evaluation. Native plugin eval can capture worker reports, but is not a reliable check of main-agent continuation.

The Agent SDK can load the mod with its plugins option. Give the main agent a fixed dispatcher systemPrompt; the researcher uses its own whole prompt from prompts/research.md, not that main-agent prompt. The evaluator's SDK driver keeps streaming input open and checks the exact worker-ID-labelled report returned by the main session. See the evaluator workflow.

Local checks

claude plugin test /path/to/claude-gem-security-review-plugin
claude plugin validate /path/to/claude-gem-security-review-plugin --strict

These checks cover input and launch failures, report forwarding, and cancellation, not research accuracy. Evaluate research with real sessions and inspect their source-linked reports and traces.

Evaluations

evals/README.md describes the Ruby evaluation workflow and evals/BASELINE.md defines its Rails upgrade baseline. The application is margin-notes. Evaluation commands use the existing application directory in the Orb and require the pinned revision in evals/fixture_source.json. Do not expose evals/ to the research worker.

Source 1 files
hooks/register.ts 66 lines
1import type { Register } from 'claude-code'
2
3export const register: Register = (on) => {
4  on('session.start', async ($, e, next) => {
5    await $.agent.register({
6      name: 'researcher',
7      description: 'Research a Ruby gem version change and its application impact.',
8      prompt: await $.fs.read(`${$.plugin.root}/prompts/research.md`),
9      model: 'claude-opus-5-5',
10      effort: 'low',
11      tools: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
12    })
13    await $.tool.register({
14      name: 'research',
15      description: 'Research a Ruby gem upgrade before applying it. Starts a read-only subagent whose source-linked report of release changes, application impact, required adaptations, and validation needs arrives in a follow-up message. Wait for that report before making upgrade decisions.',
16      inputSchema: {
17        type: 'object',
18        properties: {
19          gem: { type: 'string', minLength: 1, description: 'Ruby gem name.' },
20          currentVersion: { type: 'string', minLength: 1, description: 'Exact current gem version.' },
21          requestedVersion: { type: 'string', minLength: 1, description: 'Exact version to research upgrading to.' },
22        },
23        required: ['gem', 'currentVersion', 'requestedVersion'],
24        additionalProperties: false,
25      },
26    })
27    return next(e)
28  })
29
30  on('agent.offer', { agent: 'gem-upgrade:researcher' }, () => ({ isOffered: false }))
31
32  on('tool.call', { tool: 'mcp__gem-upgrade__research' }, async ($, e) => {
33    const { gem, currentVersion, requestedVersion } = e
34    if (![gem, currentVersion, requestedVersion].every(value => typeof value === 'string' && value.trim())) {
35      return { deny: 'Supply gem, currentVersion, and requestedVersion as nonempty strings.' }
36    }
37    const spawned = await $.agent.spawn({
38      subagentType: 'gem-upgrade:researcher',
39      description: `Research ${gem} ${currentVersion} → ${requestedVersion}`,
40      prompt: `Gem: ${gem}\nCurrent version: ${currentVersion}\nRequested version: ${requestedVersion}`,
41    })
42    if (spawned.deny || !spawned.agentId) {
43      return { deny: spawned.deny || 'The research subagent did not start.' }
44    }
45    return {
46      result: `Research started for ${gem} ${currentVersion} → ${requestedVersion} (agent ${spawned.agentId}). The report will arrive in a follow-up message; this acknowledgement is not a research result.`,
47    }
48  }).catch(($, e, next) => ({ deny: `Research tool failed: ${next.error.message ?? next.error.kind}` }))
49
50  on('turn.complete', async ($, e, next) => {
51    if (!e.agentId || e.reason === 'aborted') return next(e)
52
53    const agent = (await $.agent.list()).find(agent =>
54      agent.id === e.agentId && agent.type === 'gem-upgrade:researcher' && agent.spawnedBy === $.plugin.name)
55    if (!agent) return next(e)
56
57    const outcome = e.reason === 'answer' && e.answer.trim()
58      ? 'completed'
59      : `failed (${e.reason}); no completed report`
60    // Submission waits for the main loop to be idle; do not hold completion open.
61    void $.prompt.submit({ text: `${agent.description} (agent ${agent.id}) ${outcome}.\n\n${e.answer}` })
62      .catch(error => $.ui.log(`Could not deliver research report: ${String(error)}`))
63    return next(e)
64  })
65}
66