Context, 5h and 7d usage above the prompt, each ring coloured by level: green, yellow, orange, red.

Production-grade skills for AI coding agents (OpenClaw, Claude Code, and others).
Built by Fabricio Cavalcante — Software Engineer & AI Specialist.
Plugins are complete workflow bundles — skills + agents + hooks working together as an integrated system. Install a plugin to get the full experience.
| Plugin | Description | Platform |
|---|---|---|
| game-dev | 26 skills, 7 commands, 3 agents, 5 hooks — complete game development workflow with engineering, design, narrative, and infrastructure | Game Dev |
Mods are plugins built on function hooks: they run inside Claude Code, guard tool calls and draw above the prompt. See mods/ for install steps.
| Mod | Description |
|---|---|
| commit-guard | Blocks commits with machine paths or .env files, git add -A, and pushes to main |
| prod-guard | Holds Dokploy MCP writes until you press Proceed |
| bun-only | Rewrites npm, npx, yarn and pnpm to bun |
| done-gate | Shows edited files and the checks still owed |
| worktrees | Shows git worktrees, their changes, and the count |
| usage-rings | Context and plan usage, coloured by level |
| pr-status | The current branch's PR: CI checks and review |
| sql-guard | Holds writes to non-local databases until you press Proceed |
| pino-guard | Blocks pino transports that break bundled logging |
Skills are individual, cherry-pickable modules. Install only what you need.
| Skill | Description | Platform |
|---|---|---|
| claude-agent-sdk | Build production AI agents with the Claude Agent SDK — query(), hooks, subagents, custom tools, MCP servers, sessions, permissions, hosting | Claude Code / API |
| database-performance-tuning | Diagnose and fix slow databases like a DBA — pg_stat_statements, EXPLAIN plans, index design, query rewrites, vacuum and bloat, safe online rollout. Ships a read-only diagnostic script | PostgreSQL / MySQL |
| foundry-vtt-module-dev | Build, extend, and maintain Foundry VTT v14 modules — TypeDataModel, ApplicationV2 (detached windows), ActorSheetV2, hooks, sockets, dice, canvas/PIXI, Scene Levels, Regions as templates, ActiveEffects V2, v13→v14 migration, boilerplate files | Foundry VTT |
| foundry-vtt-system-dev | Build Foundry VTT v14 game systems — system.json documentTypes, Actor/Item TypeDataModel, dice mechanics, combat/initiative, ActiveEffect V2 subtypes, data migration, character sheets, status effects, macros, enrichers | Foundry VTT |
| openclaw-genie | Comprehensive OpenClaw gateway skill — installation, configuration, 38+ channels, memory, tools, hooks, deployment, multi-agent | OpenClaw |
| nuvemshop-sdk | Prepare, audit, implement, homologate, and publish Nuvemshop/Tiendanube apps — OAuth, API, NubeSDK, Nexo, Nimbus, webhooks, and App Store approval | Nuvemshop |
| react-animations | React animation patterns and utilities | React |
| Skill | Description |
|---|---|
| game-design-fundamentals | Core loops, MDA framework, progression curves |
| game-economy-design | Virtual economies, currencies, loot tables |
| level-design | Levels, environments, difficulty curves |
| procedural-gen | Dungeon gen, noise terrain, wave function collapse |
| quest-mission-design | Quests, objectives, quest trees |
| skill-progression-trees | Skill trees, talent trees, unlock graphs |
| ui-ux-game | HUD, menus, onboarding, accessibility |
| Skill | Description |
|---|---|
| betterauth-integration | Auth, OAuth, JWT, RBAC |
| bullmq-game-queues | Job queues, async events, scheduled tasks |
| elevenlabs-sound-music | SFX generation, adaptive music, voice acting |
| game-backend-architecture | Game servers, WebSocket, rooms, tick loops |
| game-state-sync | Client-server sync, rollback netcode, delta compression |
| gameplay-analytics | Retention analytics, funnels, D1/D7/D30 |
| matchmaking-system | Matchmaking, lobbies, ELO, skill-based queues |
| postgres-game-schema | Game DB schemas, Drizzle ORM, inventory |
| redis-game-patterns | Caching, leaderboards, pub/sub, rate limiting |
| stripe-game-payments | In-app purchases, subscriptions, webhooks |
| Skill | Description |
|---|---|
| ci-cd-game | CI/CD pipelines, GitHub Actions, deployment |
| claude-code-game-workflow | Entry point for AI agents on game projects |
| cursor-codex-integration | Cursor IDE, .cursorrules, Codex setup |
| monitoring-game-ops | Monitoring, logging, telemetry, alerts |
| Skill | Description |
|---|---|
| character-design-narrative | Characters, NPCs, dialogue, archetypes |
| quest-narrative-coherence | Narrative coherence check, lore validation |
| story-structure-game | Branching narratives, player agency, story arcs |
| worldbuilding | Lore, factions, geography, world bibles |
All skills follow a flat layout:
skills/<skill-name>/
├── README.md # Overview and installation instructions
├── SKILL.md # Main skill file (YAML frontmatter + body)
├── boilerplate/ # Starter code to copy and customize (optional)
├── templates/ # Document and config templates (optional)
├── references/ # Deep-dive reference files (optional)
└── ARCHITECTURE.md # System design decisions (optional)
SKILL.md is always the entry point — read this firstboilerplate/ files are starter code, copy and customizetemplates/ are document/config templates to fill inARCHITECTURE.md explains system design decisions/plugin marketplace add fcsouza/agent-skills
/plugin install game-dev@fcsouza-agent-skills
See plugins/game-dev/README.md for the full skill index, commands, and agents.
# Install a specific skill
npx skills add fcsouza/agent-skills --skill game-design-fundamentals
# Install all game-dev engineering skills at once
npx skills add fcsouza/agent-skills --skill betterauth-integration --skill postgres-game-schema --skill game-backend-architecture
# List all available skills
npx skills add fcsouza/agent-skills --list
# Plugin (full bundle)
cp -r plugins/game-dev/ .claude/plugins/game-dev/
# Individual skill
cp -r skills/<skill-name>/ .claude/skills/<skill-name>/
# For OpenClaw
cp -r skills/<skill-name>/ ~/.openclaw/workspace/skills/<skill-name>/
GPL-3.0
hooks/register.tsx 224 lines1import type { EngineInterface, Register, Timer } from 'claude-code';
2
3import {
4 type AgentRun,
5 applyProgress,
6 elapsed,
7 isRunning,
8 LINGER_MS,
9 MAX_ROWS,
10 modelLabel,
11 PROGRESS_BRIEF,
12 type ProgressInput,
13 progressBar,
14 sweepBar,
15 visible,
16} from './agents';
17import { colorOf, isNarrow, type Meter, metersOf, ringOf } from './meters';
18
19const REFRESH_MS = 30_000;
20
21let meters: Meter[] = [];
22let timer: Timer | undefined;
23// The action a button started and that has not finished yet; its buttons hide meanwhile.
24let running: 'compact' | 'review' | null = null;
25const PROGRESS_TOOL = 'mcp__usage-rings__progress';
26// Subagents this session started, newest last.
27let agents: AgentRun[] = [];
28// Redraws once a second while a subagent runs, so its clock moves.
29let ticker: Timer | undefined;
30
31function keepTicking($: EngineInterface): void {
32 if (ticker === undefined && isRunning(agents)) {
33 ticker = $.clock.every(1_000, () => {
34 if (!isRunning(agents)) {
35 ticker?.cancel();
36 ticker = undefined;
37 }
38 $.ui.invalidate('ui.render');
39 });
40 }
41}
42
43// Done fills the bar; a reported plan fills it by steps; no plan sweeps while running.
44function barOf(run: AgentRun, now: number): string {
45 if (run.status === 'done') return progressBar(1, 1);
46 if (run.stepTotal !== undefined) return progressBar(run.stepDone ?? 0, run.stepTotal);
47 return run.status === 'running' ? sweepBar(now - run.startedAt) : progressBar(0, 1);
48}
49
50const STATUS_COLOR = { running: '#38bdf8', done: '#22c55e', failed: '#ef4444' } as const;
51
52// `redraw: false` leaves the redraw to the ticker, which runs while any subagent does.
53function updateAgent($: EngineInterface, agentId: string, change: (run: AgentRun) => AgentRun, redraw = true): void {
54 agents = agents.map(run => (run.agentId === agentId ? change(run) : run));
55 if (redraw) $.ui.invalidate('ui.render');
56}
57
58// Runs a slash command as if typed. With `submitOnFailure`, a command the engine will not
59// run directly (a skill) is submitted as the user's prompt instead; /compact never is, so a
60// compaction that fails is not started twice.
61// Work a press starts is cancelled when the press ends, so a timer starts the command in
62// a dispatch of its own that lasts until the command ends.
63function runCommand($: EngineInterface, action: 'compact' | 'review', command: string, args = '', submitOnFailure = false): void {
64 running = action;
65 $.ui.invalidate('ui.render');
66 $.clock.after(1, () =>
67 $.command
68 .run({ command, args })
69 .catch((error: unknown) => {
70 if (!submitOnFailure) throw error;
71 return $.prompt.submit({ text: `/${command}${args === '' ? '' : ` ${args}`}`, asUser: true });
72 })
73 .catch((error: unknown) => $.ui.toast(`/${command} failed: ${error instanceof Error ? error.message : String(error)}`))
74 .finally(() => {
75 running = null;
76 $.ui.invalidate('ui.render');
77 }),
78 );
79}
80
81async function refresh($: EngineInterface): Promise<void> {
82 const [usage, now] = await Promise.all([$.session.usage(), $.clock.now()]);
83 meters = metersOf(usage, now);
84 $.ui.invalidate('ui.render');
85}
86
87export const register: Register = on => {
88 on('session.start', async ($, e, next) => {
89 const started = await next(e);
90 await $.tool
91 .register({
92 name: 'progress',
93 description:
94 'For subagents: report progress on your own task to the bar above the prompt. ' +
95 'Right after reading your task, call it with `total` (your plan in 3-8 steps) and `done: 0`; ' +
96 'call it again as each step finishes. Cheap and silent: it only draws a bar.',
97 inputSchema: {
98 type: 'object',
99 properties: {
100 done: { type: 'integer', minimum: 0, description: 'Steps finished so far.' },
101 total: { type: 'integer', minimum: 1, description: 'Steps planned; may change if the plan changes.' },
102 note: { type: 'string', description: 'The step in progress, a few words.' },
103 },
104 required: ['done'],
105 },
106 // In every agent's tool list, not behind ToolSearch: subagents do not go looking for it.
107 isDeferred: false,
108 })
109 .catch(() => undefined);
110 await refresh($).catch(() => undefined);
111 timer?.cancel();
112 timer = $.clock.every(REFRESH_MS, () => {
113 refresh($).catch(() => undefined);
114 });
115 return started;
116 });
117
118 on('turn.complete', async ($, e, next) => {
119 const done = await next(e);
120 const agentId = e.agentId;
121 if (agentId !== undefined) {
122 const at = await $.clock.now();
123 updateAgent($, agentId, run => ({ ...run, status: e.reason === 'answer' ? 'done' : 'failed', endedAt: at }));
124 // Redraw once the row's linger time is over, so it leaves the band.
125 $.clock.after(LINGER_MS + 50, () => $.ui.invalidate('ui.render'));
126 }
127 await refresh($).catch(() => undefined);
128 return done;
129 });
130
131 on('agent.spawn', async ($, e, next) => {
132 const started = await next(e.prompt.includes(PROGRESS_BRIEF) ? e : { ...e, prompt: e.prompt + PROGRESS_BRIEF });
133 if (started.deny !== undefined || started.agentId === undefined) {
134 return started;
135 }
136 const at = await $.clock.now();
137 const run: AgentRun = {
138 agentId: started.agentId,
139 description: e.description || e.subagentType,
140 model: started.model,
141 status: 'running',
142 startedAt: at,
143 tools: 0,
144 };
145 agents = [...visible(agents, at).filter(other => other.agentId !== run.agentId), run];
146 $.ui.invalidate('ui.render');
147 keepTicking($);
148 return started;
149 }).catch(($, e, next) => next(e));
150
151 // Answers the progress tool itself: nothing runs, the report only moves the subagent's bar.
152 on('tool.call', { tool: PROGRESS_TOOL }, async ($, e) => {
153 const agentId = e.agentId;
154 if (agentId === undefined || !agents.some(run => run.agentId === agentId)) {
155 return { result: 'ignored: only subagents report progress' };
156 }
157 updateAgent($, agentId, run => applyProgress(run, e as ProgressInput));
158 return { result: 'ok' };
159 }).catch(() => ({ result: 'ignored: the progress bar failed' }));
160
161 // Counts each subagent's tool calls: the closest a plain subagent comes to saying how far along it is.
162 on('tool.call', async ($, e, next) => {
163 const agentId = e.agentId;
164 if (agentId !== undefined && e.tool !== PROGRESS_TOOL && agents.some(run => run.agentId === agentId)) {
165 updateAgent($, agentId, run => ({ ...run, tools: run.tools + 1 }), false);
166 }
167 return next(e);
168 }).catch(($, e, next) => next(e));
169
170 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
171 const below = await next(e);
172 if (e.props.hasSurvey) {
173 return below;
174 }
175 const { Box, Button, Text } = $.ui.resolve(e);
176 const canAct = !e.props.isWorking && running === null;
177 const now = await $.clock.now();
178 const shown = visible(agents, now);
179 // Narrow: no reset countdowns, buttons on their own line, shorter agent rows.
180 const narrow = isNarrow(e.viewport?.columns);
181 const buttons = canAct ? (
182 <Box gap={1}>
183 <Button key="compact" label="Compact" onPress={() => runCommand($, 'compact', 'compact')} />
184 <Button key="review" label={narrow ? 'Review' : 'Code review'} onPress={() => runCommand($, 'review', 'code-review', 'high --fix', true)} />
185 </Box>
186 ) : null;
187 return (
188 <Box flexDirection="column">
189 <Box gap={narrow ? 2 : 3} justifyContent="center" flexWrap="wrap">
190 {meters.map(meter => (
191 <Box gap={1}>
192 <Text color={colorOf(meter.percent)}>{ringOf(meter.percent)}</Text>
193 <Text bold color={colorOf(meter.percent)}>
194 {meter.percent}%
195 </Text>
196 <Text dimColor>
197 {meter.label}
198 {meter.resets === undefined || narrow ? '' : ` · ${meter.resets}`}
199 </Text>
200 </Box>
201 ))}
202 {narrow ? null : buttons}
203 </Box>
204 {narrow && buttons !== null ? <Box justifyContent="center">{buttons}</Box> : null}
205 {shown.slice(-MAX_ROWS).map(run => (
206 <Box gap={1}>
207 <Text color={STATUS_COLOR[run.status]}>{run.status === 'running' ? '●' : run.status === 'done' ? '✓' : '✗'}</Text>
208 <Text wrap="truncate-end">{run.description}</Text>
209 <Text color={STATUS_COLOR[run.status]}>{barOf(run, now)}</Text>
210 {run.stepTotal !== undefined ? <Text>{`${run.stepDone ?? 0}/${run.stepTotal}`}</Text> : null}
211 <Text dimColor wrap="truncate-end">
212 {narrow
213 ? `${modelLabel(run.model)} · ${elapsed((run.endedAt ?? now) - run.startedAt)}`
214 : `${run.stepNote !== undefined && run.status === 'running' ? `${run.stepNote} · ` : ''}${modelLabel(run.model)} · ${elapsed((run.endedAt ?? now) - run.startedAt)} · ${run.tools} tool${run.tools === 1 ? '' : 's'}`}
215 </Text>
216 </Box>
217 ))}
218 {shown.length > MAX_ROWS ? <Text dimColor>{`+${shown.length - MAX_ROWS} more`}</Text> : null}
219 {below}
220 </Box>
221 );
222 });
223};
224hooks/agents.ts 83 lines1// The subagents this session runs, as rows under the meters. Pure, so the tests call it directly.
2
3export type AgentStatus = 'running' | 'done' | 'failed';
4
5export type AgentRun = {
6 agentId: string;
7 description: string;
8 model: string;
9 status: AgentStatus;
10 startedAt: number;
11 endedAt?: number;
12 tools: number;
13 // Set once the subagent reports its plan through the progress tool.
14 stepDone?: number;
15 stepTotal?: number;
16 stepNote?: string;
17};
18
19export type ProgressInput = { done?: unknown; total?: unknown; note?: unknown };
20
21// Appended to every subagent's prompt so it reports a plan the band can draw as a real bar.
22export const PROGRESS_BRIEF = `
23
24Progress reporting: right after reading this task, call the \`progress\` tool with \`total\` (your plan in 3-8 steps) and \`done: 0\`, then call it again each time a step finishes, with a few-word \`note\` naming the next step. It only draws a bar for the user; it costs nothing and returns nothing useful. If you have no \`progress\` tool, ignore this paragraph.`;
25
26const BAR_WIDTH = 8;
27
28// How long a finished row stays before it leaves the band.
29export const LINGER_MS = 8_000;
30// Rows drawn at most; the rest are counted on one line.
31export const MAX_ROWS = 8;
32
33/** `claude-haiku-4-5-20251001` → `Haiku 4.5`; anything else as it came. */
34export function modelLabel(model: string): string {
35 const match = /claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d{1,2}))?(?:-|\[|$)/.exec(model);
36 if (match === null) {
37 return model;
38 }
39 const [, family = '', major = '', minor] = match;
40 return `${family[0]?.toUpperCase()}${family.slice(1)} ${major}${minor === undefined ? '' : `.${minor}`}`;
41}
42
43/** 9s, 1m05s, 1h02m. */
44export function elapsed(ms: number): string {
45 const seconds = Math.max(0, Math.floor(ms / 1000));
46 if (seconds < 60) {
47 return `${seconds}s`;
48 }
49 const minutes = Math.floor(seconds / 60);
50 if (minutes < 60) {
51 return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
52 }
53 return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
54}
55
56/** Drops finished rows older than LINGER_MS; running rows always stay. */
57export function visible(runs: readonly AgentRun[], now: number): AgentRun[] {
58 return runs.filter(run => run.status === 'running' || now - (run.endedAt ?? now) < LINGER_MS);
59}
60
61/** Applies a progress report, clamping done to the plan and dropping bad values. */
62export function applyProgress(run: AgentRun, input: ProgressInput): AgentRun {
63 const total = typeof input.total === 'number' && input.total >= 1 ? Math.round(input.total) : run.stepTotal;
64 const doneRaw = typeof input.done === 'number' && input.done >= 0 ? Math.round(input.done) : (run.stepDone ?? 0);
65 const note = typeof input.note === 'string' && input.note.trim() !== '' ? input.note.trim().slice(0, 60) : run.stepNote;
66 return { ...run, stepTotal: total, stepDone: total === undefined ? doneRaw : Math.min(total, doneRaw), stepNote: note };
67}
68
69/** A filled bar for a known plan: ▰▰▰▱▱▱▱▱ */
70export function progressBar(done: number, total: number): string {
71 const filled = total <= 0 ? 0 : Math.round((Math.min(done, total) / total) * BAR_WIDTH);
72 return '▰'.repeat(filled) + '▱'.repeat(BAR_WIDTH - filled);
73}
74
75/** A 3-cell block sweeping across the bar, one cell a second: for a subagent with no plan. */
76export function sweepBar(elapsedMs: number): string {
77 const span = BAR_WIDTH + 2;
78 const head = Math.floor(elapsedMs / 1000) % span;
79 return Array.from({ length: BAR_WIDTH }, (_, i) => (i <= head && i > head - 3 ? '▰' : '▱')).join('');
80}
81
82export const isRunning = (runs: readonly AgentRun[]): boolean => runs.some(run => run.status === 'running');
83hooks/meters.ts 61 lines1// Turns the session's usage into coloured meters. Pure, so the tests call it directly.
2
3export type Meter = { label: string; percent: number; resets?: string };
4export type UsageLike = {
5 context: { percent?: number };
6 rateLimits: readonly { kind: string; percentUsed: number; resetsAt?: string }[];
7};
8
9// Each level's colour: room to spare, worth watching, close, at the limit.
10export const LEVELS = [
11 { below: 50, color: '#22c55e' },
12 { below: 75, color: '#eab308' },
13 { below: 90, color: '#f97316' },
14 { below: Number.POSITIVE_INFINITY, color: '#ef4444' },
15] as const;
16
17export const colorOf = (percent: number): string => LEVELS.find(level => percent < level.below)?.color ?? '#ef4444';
18
19// A ring that fills by quarter: ○ ◔ ◑ ◕ ●
20export const ringOf = (percent: number): string => ['○', '◔', '◑', '◕', '●'][Math.min(4, Math.max(0, Math.round(percent / 25)))] ?? '○';
21
22const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', seven_day_opus: '7d opus', seven_day_sonnet: '7d sonnet' };
23
24export function metersOf(usage: UsageLike, now: number): Meter[] {
25 const meters: Meter[] = [];
26 if (usage.context.percent !== undefined) {
27 meters.push({ label: 'context', percent: clamp(usage.context.percent) });
28 }
29 for (const limit of usage.rateLimits) {
30 const resets = limit.resetsAt === undefined ? undefined : countdown(Date.parse(limit.resetsAt) - now);
31 meters.push({
32 label: LABELS[limit.kind] ?? limit.kind.replaceAll('_', ' '),
33 percent: clamp(limit.percentUsed),
34 ...(resets === undefined ? {} : { resets }),
35 });
36 }
37 return meters;
38}
39
40const clamp = (n: number): number => Math.max(0, Math.min(100, Math.round(n)));
41
42/** 2h05, 47m, or nothing once past. */
43export function countdown(ms: number): string | undefined {
44 if (!Number.isFinite(ms) || ms <= 0) {
45 return undefined;
46 }
47 const minutes = Math.ceil(ms / 60_000);
48 const days = Math.floor(minutes / 1440);
49 if (days > 0) {
50 return `${days}d${Math.floor((minutes % 1440) / 60)}h`;
51 }
52 const hours = Math.floor(minutes / 60);
53 return hours > 0 ? `${hours}h${String(minutes % 60).padStart(2, '0')}` : `${minutes}m`;
54}
55
56// Below this many columns the band switches to its compact form.
57export const NARROW_COLUMNS = 80;
58
59/** Narrow when the surface measured a width under NARROW_COLUMNS; wide when it measured none. */
60export const isNarrow = (columns: number | undefined): boolean => columns !== undefined && columns < NARROW_COLUMNS;
61