SLOPSHOPPER

pr-pane

Toggle your open GitHub PRs above the prompt; focus with Ctrl+X then Tab, then use arrows and Enter to open one in the browser.

newbandcommandtoaststatusprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · pr-pane
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /prs ⎿ pr-pane: PR list shown above the prompt. ──────────────────────────────────────────────────────────────────────────────────────────────────── GitHub · 0 open JSON Parse error: Unexpected EOF Sort: r: repo s: [status] d: date updated ↓ within groups Ctrl+X then Tab focuses · ↑/↓ or Tab move · Enter opens · r/s/d sort · p/n page · Esc prompt · /prs hides ──────────────────────────────────────────────────────────────────────────────────────────────────── ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
──────────────────────────────────────────────────────────────────────────────────────────────────── GitHub · 0 open JSON Parse error: Unexpected EOF Sort: r: repo s: [status] d: date updated ↓ within groups Ctrl+X then Tab focuses · ↑/↓ or Tab move · Enter opens · r/s/d sort · p/n page · Esc prompt · /prs hides ──────────────────────────────────────────────────────────────────────────────────────────────────── ⟨Claude Code's own drawing⟩
README

asragab-claude-marketplace

A Claude Code plugin marketplace containing plugins for session search/analytics, skill evaluation, Claude Code mod development, and a PR picker mod.

Plugins

PluginVersionDescription
cass0.3.0Cross-agent session search, context, analytics, export, learnings, and session resume powered by CASS CLI
skill-eval1.0.0Automated skill/prompt/tool evaluation and improvement via session log analysis and autoresearch optimization
mod-forge0.2.0Build and verify Claude Code mods in isolated child processes, never in the live session (Claude Code 2.1.287+)
pr-pane0.2.0/prs toggles a band above the prompt with your open GitHub PRs and review state; arrows and Enter open one in the browser (Claude Code 2.1.287+, needs gh)

Installation

1. Add the marketplace

claude plugin marketplace add https://github.com/ASRagab/asragab-claude-marketplace

2. Install plugins

# Install by name (use plugin@marketplace to disambiguate)
claude plugin install cass
claude plugin install skill-eval
claude plugin install mod-forge
claude plugin install pr-pane

Prerequisites

  • Claude Code CLI
  • Bun runtime (required by skill-eval scripts)
  • To run bun run test, first run bun install in plugins/skill-eval/scripts (installs @anthropic-ai/sdk).
  • GitHub CLI (gh), logged in with gh auth login (required by pr-pane)
  • CASS CLI v0.3.0+ (required by cass plugin)

cass

Cross-agent session search, context loading, token analytics, session export, session resume, and learning synthesis powered by CASS (Coding Agent Session Search). Searches across Claude Code, Codex, Cursor, Gemini CLI, Copilot, and 14+ other agents.

Recipes default to token-efficient output (--robot-format toon --fields summary --max-tokens 1600). The plugin's SessionStart hook surfaces the CASS index health, recommended next action, and a CASS_OUTPUT_FORMAT=toon advisory.

Skills

/cass:session-search

Search across all indexed coding agent sessions. Supports lexical (BM25), semantic (vector), and hybrid search modes.

cass search "authentication flow" --mode hybrid --robot-format toon --fields summary --max-tokens 1600 --limit 10
cass search "error" --days 30 --aggregate agent --limit 1 --max-content-length 100 --robot-format toon
/cass:session-context

Load relevant past session context for the current task, file, or project.

cass sessions --current --robot-format toon
cass timeline --since 7d --json --group-by day
/cass:session-analytics

Analyze session history for usage patterns, token consumption, and tool efficiency.

cass analytics tokens --days 7 --group-by day --json
cass analytics tools --limit 20 --json
/cass:session-export

Export sessions to markdown, text, JSON, HTML, or self-contained encrypted HTML.

cass export <session_path> -o conversation.md
cass export-html <session_path> --encrypt --password "secret" --filename report.html
/cass:session-learnings

Extract patterns, recurring issues, and actionable lessons from past sessions.

cass search "error fix bug" --mode hybrid --robot-format toon --fields summary --max-tokens 1600 --limit 20
cass analytics tools --limit 20 --json
/cass:session-resume ★ NEW v0.3.0

Resolve a session into a ready-to-run launch command for its native harness (Claude Code, Codex, OpenCode, pi_agent, Gemini). Triggered by phrases like "resume that session", "pick up where I left off", "continue the X session".

cass resume "$(cass sessions --current --json | jq -r '.sessions[0].path')" --json
/cass:session-maintenance

Diagnose, repair, and maintain CASS installation, index, analytics, and remote sources.

cass health --json
cass doctor --fix
cass index --full --json

skill-eval

A four-stage pipeline for identifying friction in coding agent sessions and iteratively optimizing skills, prompts, and tools. Inspired by Karpathy's autoresearch pattern.

Skills

The stages run sequentially — each consumes the output of the previous stage.

/skill-eval:transcript-extract (M1)

Extract structured events from Claude Code session transcripts into JSONL.

bun scripts/transcript-extract.ts --since 7d -o events.jsonl
/skill-eval:signal-classify (M2)

Classify extracted events into friction, success, noise, and neutral categories using rule-based heuristics. No LLM calls required.

bun scripts/signal-classify.ts -i events.jsonl -o classified.jsonl
bun scripts/signal-classify.ts -i events.jsonl --stats
/skill-eval:target-identify (M3)

Use LLM-as-judge to rank friction clusters by frequency, severity, and improvability.

bun scripts/target-identify.ts -i classified.jsonl --top 5 -o targets.jsonl

Requires ANTHROPIC_API_KEY environment variable.

/skill-eval:autoresearch-loop (M4)

Iteratively generate and evaluate improvements against a target's eval criteria.

bun scripts/autoresearch-loop.ts -t targets.jsonl --max-rounds 20

Requires ANTHROPIC_API_KEY environment variable.

Full Pipeline Example

cd plugins/skill-eval

# 1. Extract events from recent sessions
bun scripts/transcript-extract.ts --since 7d -o events.jsonl

# 2. Classify friction signals
bun scripts/signal-classify.ts -i events.jsonl -o classified.jsonl

# 3. Identify top optimization targets
bun scripts/target-identify.ts -i classified.jsonl --top 5 -o targets.jsonl

# 4. Run autoresearch loop on the top target
bun scripts/autoresearch-loop.ts -t targets.jsonl --max-rounds 10

mod-forge

Build and verify Claude Code mods without touching the session you are working in. Claude Code only, version 2.1.287 or later.

The build-claude-mod skill sends an agent through scripts/run-mod.sh, which runs a mod through five fail-fast stages (validate, test, headless, typecheck, interactive) in separate processes under a harness-owned CLAUDE_CONFIG_DIR. See plugins/mod-forge/README.md for stages, evidence files, and exit codes.

Requires jq. Live stages also need ANTHROPIC_API_KEY, the interactive stage needs tmux, and the type check needs a TypeScript compiler.

# Offline test: no model calls, no API key
bun run test:mod-forge

# Live test: starts real claude children and makes real model calls
# (about $0.01 to $0.03 per headless run). Needs ANTHROPIC_API_KEY and tmux.
bash plugins/mod-forge/tests/live.test.sh

pr-pane

A Claude Code mod for keeping your open pull requests one keystroke away. Claude Code only, version 2.1.287 or later.

/prs toggles a band above the prompt listing your open PRs across repositories. The whole PR description carries its status color: ✗ unresolved comments or requested changes in red, ✓ approved in blue, ● needs review in orange, and ○ draft in dim text. Draft takes precedence; unresolved review threads take precedence over approval. Resolved threads and historical comments do not make a PR red.

Press Ctrl+X, then Tab to focus the band; arrows or Tab move between controls and Enter opens a PR in your browser (open on macOS, xdg-open on Linux). While the band has focus, r sorts by repository, s by status, and d by last-updated date. One sort mode is active at a time; repository/status groups use newest-updated first. The default status order is feedback, approved, needs review, draft. Up to eight rows appear per page; p/n switch pages. If other band output makes the drawing overflow, arrows scroll and Tab still moves focus. Esc returns to the prompt and leaves the band visible; /prs hides it. The band sets no background color and yields while a survey is active.

Dim horizontal rules above and below the PR content visually separate it while preserving the default background. Smaller bands reduce the page size to leave room for the rules.

It needs no configuration. The GitHub user is whoever gh is logged in as. Background polling runs at startup, every 60 seconds, and when you show the band; updates normally appear within a minute plus API latency. The header shows the open PR count and last successful refresh time. A failed refresh keeps the last good list and shows the error in the band. If gh is missing or logged out, it says to run gh auth login. The list includes the 100 most recently updated PRs; the header shows the true total when there are more.

# Run the mod's tests (no network, no API key)
claude plugin test plugins/pr-pane

Uninstallation

claude plugin uninstall cass
claude plugin uninstall skill-eval
claude plugin uninstall mod-forge
claude plugin uninstall pr-pane

# To remove the marketplace itself
claude plugin marketplace remove asragab-claude-marketplace

License

MIT

Source 3 files
hooks/register.tsx 159 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SortBy, View } from '../types'
5import { COLOR, GLYPH, QUERY, SHORT, THREADS_QUERY, counts, parse, row, sorted } from './prs'
6import type { PullRequestNode, ThreadPage } from './prs'
7
8const REFRESH_MS = 60_000
9const view = atom({ plugin: 'pr-pane', key: 'view' } as const, { prs: [], total: 0 } as View)
10const visible = atom({ plugin: 'pr-pane', key: 'visible' } as const, false)
11const sort = atom({ plugin: 'pr-pane', key: 'sort' } as const, 'status' as SortBy)
12const page = atom({ plugin: 'pr-pane', key: 'page' } as const, 0)
13
14const gh = async ($: EngineInterface, args: string[], failure: string): Promise<string> => {
15  const ran = await $.process
16    .run(['gh', ...args], { timeoutMs: 20_000 })
17    .catch(() => undefined)
18  if (!ran) throw new Error('gh could not run (not installed, or no answer in 20s): install the GitHub CLI, then run `gh auth login`')
19  if (ran.exitCode !== 0) throw new Error(`${failure}: ${ran.stderr.trim().split('\n')[0]}`)
20  return ran.stdout
21}
22
23let isBusy = false
24
25const refresh = async ($: EngineInterface): Promise<void> => {
26  if (isBusy) return
27  isBusy = true
28  try {
29    const login = (
30      await gh($, ['api', 'user', '--jq', '.login'], 'gh is not authenticated, run `gh auth login`')
31    ).trim()
32    const out = await gh($, ['api', 'graphql', '-f', `query=${QUERY}`], 'gh could not list PRs')
33    const response = JSON.parse(out)
34    for (const n of response.data.search.nodes as (PullRequestNode | null)[]) {
35      if (!n || n.isDraft || n.reviewDecision === 'CHANGES_REQUESTED') continue
36      let threads = n.reviewThreads
37      while (!threads.nodes.some(t => t?.isResolved === false) && threads.pageInfo.hasNextPage) {
38        const after = threads.pageInfo.endCursor
39        if (!after) throw new Error('GitHub returned an incomplete review thread page')
40        const more = await gh($, ['api', 'graphql', '-f', `query=${THREADS_QUERY}`, '-f', `id=${n.id}`, '-f', `after=${after}`], 'gh could not list review threads')
41        const next = JSON.parse(more).data?.node?.reviewThreads as ThreadPage | undefined
42        if (!next || (next.pageInfo.hasNextPage && next.pageInfo.endCursor === after)) {
43          throw new Error('GitHub returned an incomplete review thread page')
44        }
45        threads = next
46      }
47      n.reviewThreads = threads
48    }
49    const prs = parse(JSON.stringify(response))
50    const count = response.data.search.issueCount as number
51    const updatedAt = await $.clock.now()
52    await update($, view, () => ({ login, prs, total: count, updatedAt }))
53  } catch (err) {
54    const error = err instanceof Error ? err.message : String(err)
55    await update($, view, v => ({ ...v, error }))
56  } finally {
57    isBusy = false
58  }
59}
60
61// ponytail: macOS `open`, then Linux `xdg-open`; no Windows.
62const openUrl = async ($: EngineInterface, url: string): Promise<void> => {
63  if (!url.startsWith('https://')) return
64  for (const bin of ['open', 'xdg-open']) {
65    const ran = await $.process.run([bin, url]).catch(() => undefined)
66    if (ran?.exitCode === 0) return
67  }
68  $.ui.toast(`Could not open ${url}`)
69}
70
71export const register: Register = on => {
72  on('session.start', async ($, e, next) => {
73    const marker = await $.env.get('MOD_FORGE_MARKER')
74    if (marker) await $.fs.write(marker, 'loaded')
75    await $.command.register({ name: 'prs', description: 'Toggle open pull requests above the prompt' })
76    $.ui.status(undefined)
77    const started = await next(e)
78    void refresh($)
79    $.clock.every(REFRESH_MS, () => void refresh($))
80    return started
81  })
82
83  on('command.run', { command: 'prs' }, async $ => {
84    const isVisible = !(await read($, visible))
85    await update($, visible, () => isVisible)
86    if (isVisible) void refresh($)
87    return { text: isVisible ? 'PR list shown above the prompt.' : 'PR list hidden.' }
88  })
89
90  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91    if (e.props.hasSurvey || !(await read($, visible))) return next(e)
92    const theirs = await next(e)
93    const t = $.ui.resolve(e)
94    const { Box, Text, Button } = t
95    const v = await read($, view)
96    const by = await read($, sort)
97    const prs = sorted(v.prs, by)
98    const size = Math.max(1, Math.min(8, e.props.maxRows - 7 - (v.error ? 1 : 0)))
99    const pages = Math.max(1, Math.ceil(prs.length / size))
100    const current = Math.min(await read($, page), pages - 1)
101    const shown = prs.slice(current * size, (current + 1) * size)
102    const movePage = async (to: number) => {
103      await update($, page, () => to)
104      const first = prs[to * size]
105      if (first) void $.ui.focus({ requestId: e.requestId, key: `pr:${first.url}` }).catch(() => undefined)
106    }
107    const changeSort = async (to: SortBy) => {
108      await update($, page, () => 0)
109      await update($, sort, () => to)
110    }
111    const at = v.updatedAt ? new Date(v.updatedAt).toTimeString().slice(0, 5) : undefined
112    const rule = '─'.repeat(Math.max(0, e.props.bodyColumns))
113    return (
114      <Box flexDirection="column">
115        <Text dimColor wrap="truncate-end">{rule}</Text>
116        <Text bold>
117          {v.login ? `@${v.login}` : 'GitHub'} · {v.total} open
118          {v.total > v.prs.length ? ` (showing ${v.prs.length})` : ''}
119          {at ? ` · updated ${at}` : ''}
120        </Text>
121        <Box>
122          {counts(v.prs).map(c => (
123            <Text color={COLOR[c.state]} dimColor={c.state === 'draft'}>
124              {GLYPH[c.state]} {c.n} {SHORT[c.state]}{'   '}
125            </Text>
126          ))}
127        </Box>
128        {v.error && <Text color="red">{v.error}</Text>}
129        {v.prs.length === 0 && !v.error && (
130          <Text dimColor>{v.updatedAt ? 'No open PRs.' : 'Loading...'}</Text>
131        )}
132        <Box columnGap={2}>
133          <Text dimColor>Sort:</Text>
134          {(['repo', 'status', 'date'] as const).map(mode => (
135            <Button key={`sort:${mode}`} plain hotkey={mode[0]} label={by === mode ? `[${mode}]` : mode} onPress={() => changeSort(mode)} />
136          ))}
137          <Text dimColor>{by === 'date' ? 'updated ↓' : 'updated ↓ within groups'}</Text>
138        </Box>
139        {shown.map((p, i) => (
140          <Box columnGap={1}>
141            <Button key={`pr:${p.url}`} plain label="›" autoFocus={i === 0 ? true : undefined} onPress={() => openUrl($, p.url)} />
142            <Text color={COLOR[p.state]} dimColor={p.state === 'draft'} wrap="truncate-end">{row(p, prs)}</Text>
143          </Box>
144        ))}
145        {prs.length > 0 && (
146          <Box columnGap={2}>
147            {current > 0 && <Button key="previous" plain hotkey="p" label="Previous" onPress={() => movePage(current - 1)} />}
148            <Text dimColor>Page {current + 1}/{pages}</Text>
149            {current + 1 < pages && <Button key="next" plain hotkey="n" label="Next" onPress={() => movePage(current + 1)} />}
150          </Box>
151        )}
152        <Text dimColor>Ctrl+X then Tab focuses · ↑/↓ or Tab move · Enter opens · r/s/d sort · p/n page · Esc prompt · /prs hides</Text>
153        <Text dimColor wrap="truncate-end">{rule}</Text>
154        {theirs}
155      </Box>
156    )
157  })
158}
159
hooks/prs.ts 93 lines
1import type { Pr, PrState, SortBy } from '../types'
2
3export const LIMIT = 100  // GraphQL search page maximum
4
5// `gh search prs --json` has no review decision; GraphQL search does.
6export const QUERY = `query{search(query:"is:pr is:open author:@me archived:false sort:updated-desc",type:ISSUE,first:${LIMIT}){issueCount nodes{... on PullRequest{id number title url isDraft reviewDecision updatedAt repository{nameWithOwner} reviewThreads(first:${LIMIT}){nodes{isResolved} pageInfo{hasNextPage endCursor}}}}}}`
7
8export const THREADS_QUERY = `query($id:ID!,$after:String!){node(id:$id){... on PullRequest{reviewThreads(first:${LIMIT},after:$after){nodes{isResolved} pageInfo{hasNextPage endCursor}}}}}`
9
10export const stateOf = (isDraft: boolean, decision: string | null, hasFeedback = false): PrState =>
11  isDraft
12    ? 'draft'
13    : decision === 'CHANGES_REQUESTED' || hasFeedback
14      ? 'has feedback'
15      : decision === 'APPROVED'
16        ? 'approved'
17        : 'needs review'
18
19export type ThreadPage = {
20  nodes: ({ isResolved: boolean } | null)[]
21  pageInfo: { hasNextPage: boolean; endCursor: string | null }
22}
23
24export type PullRequestNode = {
25  id: string
26  number: number
27  title: string
28  url: string
29  isDraft: boolean
30  reviewDecision: string | null
31  updatedAt: string
32  repository: { nameWithOwner: string }
33  reviewThreads: ThreadPage
34}
35
36export const total = (stdout: string): number => JSON.parse(stdout).data.search.issueCount
37
38// Status sorting puts feedback first and drafts last.
39export const ORDER: readonly PrState[] = ['has feedback', 'approved', 'needs review', 'draft']
40
41export const parse = (stdout: string): Pr[] =>
42  (JSON.parse(stdout).data.search.nodes as (PullRequestNode | null)[])
43    .filter((n): n is PullRequestNode => n !== null && !!n.url)
44    .map(n => ({
45      repo: n.repository.nameWithOwner,
46      number: n.number,
47      title: n.title,
48      url: n.url,
49      state: stateOf(n.isDraft, n.reviewDecision, n.reviewThreads?.nodes?.some(t => t?.isResolved === false)),
50      updatedAt: n.updatedAt,
51    }))
52
53export const sorted = (prs: readonly Pr[], by: SortBy): Pr[] =>
54  [...prs].sort((a, b) =>
55    (by === 'repo' ? a.repo.localeCompare(b.repo) : by === 'status' ? ORDER.indexOf(a.state) - ORDER.indexOf(b.state) : 0)
56    || Date.parse(b.updatedAt) - Date.parse(a.updatedAt)
57    || a.repo.localeCompare(b.repo)
58    || a.number - b.number
59    || a.url.localeCompare(b.url),
60  )
61
62export const COLOR: Record<PrState, string | undefined> = {
63  'has feedback': 'red',
64  approved: 'blue',
65  'needs review': '#ffa500',
66  draft: undefined,
67}
68
69export const GLYPH: Record<PrState, string> = {
70  draft: '○',
71  'has feedback': '✗',
72  approved: '✓',
73  'needs review': '●',
74}
75
76export const SHORT: Record<PrState, string> = {
77  draft: 'draft',
78  'has feedback': 'comments/changes',
79  approved: 'approved',
80  'needs review': 'needs review',
81}
82
83export const counts = (prs: readonly Pr[]): { state: PrState; n: number }[] =>
84  ORDER.map(state => ({ state, n: prs.filter(p => p.state === state).length })).filter(c => c.n > 0)
85
86// The glyph carries the state (the header legend names it); the owner is dropped
87// unless two listed repos share a name.
88export const row = (p: Pr, all: readonly Pr[]): string => {
89  const name = p.repo.split('/')[1] ?? p.repo
90  const isAmbiguous = all.some(o => o.repo !== p.repo && o.repo.endsWith(`/${name}`))
91  return `${GLYPH[p.state]} ${isAmbiguous ? p.repo : name}#${p.number} ${p.title}`
92}
93
types/index.d.ts 20 lines
1export type PrState = 'draft' | 'has feedback' | 'approved' | 'needs review'
2
3export type SortBy = 'repo' | 'status' | 'date'
4
5export type Pr = { repo: string; number: number; title: string; url: string; state: PrState; updatedAt: string }
6
7export type View = {
8  login?: string
9  prs: Pr[]
10  total: number
11  updatedAt?: number
12  error?: string
13}
14
15declare module 'claude-code' {
16  interface PluginState {
17    'pr-pane': { view: View; visible: boolean; sort: SortBy; page: number }
18  }
19}
20