SLOPSHOPPER

session-info

Shows the git repo and branch, kubectl context, model, and effort at the end of the hint line under the prompt

newspinnerpromptprocesstimer
★ 1v0.1.0no licenseupdated 2026-10-12rumstead/ai-agents/mods/session-info
A shopper browsing a rack in a slop shop
README

ai-agents

Personal collection of AI coding subagents, skills, and Claude Code mods, kept in one place so they can be versioned, shared, and reused across AI coding harnesses.

Structure

ai-agents/
├── install.sh        # symlinks agents and skills into their live locations
├── .claude-plugin/   # marketplace.json listing the mods
├── agents/           # *.agent.md subagents
├── skills/           # one folder per skill, each containing a SKILL.md
└── mods/             # one Claude Code plugin of function hooks per folder

Contents

Agents

AgentPurpose
pr-code-qualityReviews idioms, style consistency, design patterns, architectural fit
pr-issue-alignmentVerifies PR changes align with a GitHub issue's requirements
pr-performanceFinds performance bottlenecks, scalability and concurrency risks
pr-reviewComprehensive PR review orchestrating the above

Skills

SkillDescription
code-quality-reviewEvaluate code idioms, style, design patterns, and architectural fit
issue-alignmentVerify PR changes align with a GitHub issue's requirements
performance-reviewFind performance bottlenecks, scalability and concurrency risks

Mods

Claude Code only. Each mod is a plugin whose hooks module runs inside the session.

ModDescription
checkout-guardShows a band when another Claude session is working in the same git checkout and blocks checkout, switch, rebase, pull, merge, stash, reset --hard, clean, and restore there. Subagents started in a worktree are left alone
pr-sessionRenames the session pr-<repo>-<number> after the first PR it mentions, unless you named it yourself. Adds /handoff [note], which writes ~/.claude/handoffs/<repo>/<session>.md for a fresh session to pick up, and /resume-handoff [name or number] to list or load them
session-infoAdds <repo>:<branch> · k8s:<kubectl context> · <model> <effort> to the dim hint line under the prompt, leaving out whatever is unknown. Repo and kube context refresh every 15s and around each turn, effort comes from the request the turn sends

A new mod needs a folder under mods/ and an entry in .claude-plugin/marketplace.json. To check one:

claude plugin validate mods/<name>
claude plugin test mods/<name>

Always-on instructions

A skill whose frontmatter carries applyTo: is personal context (voice, style, standing preferences) rather than a procedure to run on request. Claude Code discovers it by description at run time like any other skill, but Copilot has no on-demand skill discovery, so install.sh also generates an always-on <name>.instructions.md for it in the VS Code prompts directories (local and Remote).

Claude Code only consults such a skill when the task matches its description. Rules that must apply to every reply (no em dashes) belong in ~/.claude/CLAUDE.md, which is loaded into every session.

Portability across harnesses

The canonical source of truth is the files in this repo:

  • agents/*.agent.md — agent prompt bodies with Copilot-style frontmatter
  • skills/*/SKILL.md — reusable skill procedures

The prompt bodies and skills are plain Markdown and transfer across harnesses unchanged. Only the agent frontmatter is harness-specific:

GitHub CopilotClaude CodeWindsurf
Agent file*.agent.md*.md*.md (workflow)
tools field[read, search, web, execute, agent]Read, Grep, Glob, WebFetch, WebSearch, Bash—
Delegationagents: + skill IDsmain-thread subagents + model-invoked skills—

install.sh handles this: Copilot and Windsurf consume the canonical files via symlink, while for Claude Code it generates adapted subagents (rewriting the tools list to Claude Code's PascalCase tool names, dropping Copilot-only keys, and using <name>.md filenames). The comprehensive review workflow includes a direct skill-based fallback for harnesses without nested delegation, and a GitHub CLI fallback for harnesses without GitHub-native tools.

Install

./install.sh

This installs into each harness's live locations:

HarnessAgentsSkills
GitHub Copilot~/Library/Application Support/Code/User/prompts/ on macOS or ~/.config/Code/User/prompts/ elsewhere (VS Code), ~/.config/copilot/agents/ and ~/.copilot/agents/ (CLI) — symlinked~/.copilot/skills/ — symlinked
Windsurf~/.codeium/windsurf/global_workflows/ — symlinked(delivered as workflows)
Claude Code~/.claude/agents/ — generated~/.claude/skills/ — symlinked

For Claude Code it also adds this repo as the rumstead-ai-agents plugin marketplace and installs every mod in it at user scope. The mods are read in place from mods/, so an edit takes effect at the next session start or /reload-plugins.

Plus ~/.config/Code/User/prompts/ and ~/.vscode-server/data/User/prompts/ (generated *.instructions.md for any skill marked applyTo:).

Source 3 files
hooks/register.ts 101 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Info } from '../types'
5import { formatInfo, shortModel } from './format'
6
7const info = atom(
8  { plugin: 'session-info', key: 'info' } as const,
9  { repo: null, kubeContext: null, model: null, effort: null },
10)
11
12const REFRESH_MS = 15_000
13
14async function run($: EngineInterface, argv: string[]): Promise<string | null> {
15  const ran = await $.process.run(argv, { timeoutMs: 5_000 }).catch(() => undefined)
16  const out = ran?.exitCode === 0 ? ran.stdout.trim() : ''
17  return out === '' ? null : out
18}
19
20// `<repo>:<branch>`, the repo named the same from any of its worktrees.
21async function repo($: EngineInterface): Promise<string | null> {
22  const common = await run($, ['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'])
23  if (common === null) return null
24
25  const name = common.replace(/\/\.git\/?$/, '').split('/').filter(Boolean).pop() ?? null
26  const branch = await run($, ['git', 'branch', '--show-current'])
27  return branch === null ? name : `${name}:${branch}`
28}
29
30// The effort the settings give the model, until a request says otherwise.
31async function configuredEffort($: EngineInterface, model: string): Promise<string | null> {
32  const settings = (await $.settings.read()) as {
33    effortLevel?: string
34    modelSettings?: Record<string, { effortLevel?: string }>
35  }
36  return settings.modelSettings?.[model]?.effortLevel ?? settings.effortLevel ?? null
37}
38
39async function refresh($: EngineInterface, options: { effort?: boolean } = {}): Promise<void> {
40  const [repoName, kubeContext, model] = await Promise.all([
41    repo($),
42    run($, ['kubectl', 'config', 'current-context']),
43    $.session.model(),
44  ])
45  const effort = options.effort ? await configuredEffort($, model) : undefined
46
47  await update($, info, (now): Info => ({
48    repo: repoName,
49    kubeContext,
50    model: shortModel(model),
51    effort: effort === undefined ? now.effort : effort,
52  }))
53}
54
55export const register: Register = on => {
56  on('session.start', async ($, e, next) => {
57    const started = await next(e)
58    void refresh($, { effort: true })
59    $.clock.every(REFRESH_MS, () => void refresh($))
60
61    return started
62  })
63
64  on('prompt.submit', async ($, e, next) => {
65    void refresh($)
66
67    return next(e)
68  })
69
70  on('turn.complete', async ($, e, next) => {
71    const done = await next(e)
72    if (e.agentId === undefined) void refresh($)
73
74    return done
75  })
76
77  // /model and /effort change what the next request sends.
78  on('command.run', async ($, e, next) => {
79    const ran = await next(e)
80    if (e.command === 'model' || e.command === 'effort') void refresh($, { effort: true })
81
82    return ran
83  })
84
85  on('turn.step', async function* ($, e, next) {
86    if (e.agentId === undefined) {
87      const effort = e.effort === undefined ? null : String(e.effort)
88      const model = shortModel(e.model)
89      await update($, info, now => ({ ...now, model, effort }))
90    }
91
92    return yield* next(e)
93  })
94
95  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
96    const tail = formatInfo(await read($, info))
97
98    return tail === '' ? next(e) : next({ ...e, props: { ...e.props, tail } })
99  })
100}
101
hooks/format.ts 14 lines
1import type { Info } from '../types'
2
3export function shortModel(model: string): string {
4  return model.replace(/^claude-/, '').replace(/-\d{8}$/, '')
5}
6
7// `argo-cd:master · k8s:admin@talos · opus-5-5 high`, leaving out what is unknown.
8export function formatInfo(info: Info): string {
9  const model = [info.model, info.effort].filter(Boolean).join(' ')
10  return [info.repo, info.kubeContext && `k8s:${info.kubeContext}`, model]
11    .filter(Boolean)
12    .join(' · ')
13}
14
types/index.d.ts 13 lines
1export type Info = {
2  repo: string | null
3  kubeContext: string | null
4  model: string | null
5  effort: string | null
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'session-info': { info: Info }
11  }
12}
13