/handoff-compact writes a handoff, compacts with the usual resume instructions and resumes, in one step

Personal, reusable Agent Skills for AI coding agents: Claude Code, Codex, Cursor, and 70+ others. Plus a few Claude Code mods.
| Skill | Description |
|---|---|
field-agent | Dispatch work to one field agent in a herdr tab or worktree. Names the agent, records it in the field ledger, arms a watch loop, and triages each report. Requires herdr >= 0.9.0. |
field-handler | This session becomes the handler: it opens a room of parallel field agents on one theme and steers them itself. Surfaces on its own when work gets complex; never spawns without agreement. Requires herdr >= 0.9.0. |
field-audit | Read each field agent's result, verify it, then close only the panes that are provably finished. Requires herdr >= 0.9.0. |
herdr | The semantics herdr --skill leaves out: agent status, the three agent handles, dispatch and wait idioms, worktree removal order, output shapes. The three field-* skills assume it. |
handoff | Write a post-compact continuation prompt (done, in-flight, next, hard rules) to a new dated file under ~/.claude/handoffs/<repo>/ (outside every repo, so parallel agents never overwrite each other), then hand the user the /compact command. Use when context is running low mid-project or the user says "handoff". |
backlog | Move the current herdr pane into a new tab in the "Backlog" workspace, keeping the tab's name. Requires a herdr pane. |
transcribe | Transcribe a video URL or a local audio/video file to text with yt-dlp and the MacWhisper CLI (mw). macOS only; needs MacWhisper.app with a model downloaded. |
component-system-builder | Stand up a closed-world component + agent-rules system for a platform (Electron, Astro, SwiftUI, …), or retrofit one onto an existing project through an audited, pass-by-pass loop. Makes AI-generated apps consistent by removing choices. |
field-agent harness picker/field-agent lets you pick which agent harness runs the new tab. Name it explicitly, or get a numbered menu:
/field-agent opencode refactor the auth module # explicit harness
/field-agent --harness glm fix the flaky test # explicit, unambiguous form
/field-agent refactor the auth module # no harness → numbered picker (blank = default)
The registry is skills/field-agent/harnesses.json (claude, glm, ds, opencode, pi by default). Edit it to add or change harnesses; if the file is missing or invalid, the skill falls back to an inline default. Each harness launches in its own native auto/yolo flag (--permission-mode auto, --auto, --approve, …).
**The four field-* and herdr skills are one suite. Install them together:**
npx skills add gregbarbosa/skills -s '*' -g -y
That installs into ~/.agents/skills and links every detected agent to it.
If you have PromptScript installed, expect a line like ✗ herdr → PromptScript does not support global skill installation. It is harmless. PromptScript has no global scope; every other agent still installs. Check with npx skills list -g. To avoid the message, name your agents instead, repeating -a because a comma list is rejected:
npx skills add gregbarbosa/skills -s '*' -g -y \
-a claude-code -a pi -a opencode -a hermes-agent -a gemini-cli
The skills packaging format has no group or dependency concept, so the grouping is a convention the skills state and check themselves: field-handler and field-audit look for field-agent's directory on startup, and stop with this command if it is missing.
The vocabulary is borrowed from an intelligence service. You are the handler. Each agent you start is a field agent.
field-agent dispatches one. It names the agent, records it in the ledger (~/.claude/field/ledger.json), and arms a watch loop that reports every state change. field-handler dispatches several on one shared theme and keeps this session steering them. field-audit sweeps up: it reads each result, verifies the work, and closes only the panes that are acknowledged and clean. herdr carries the semantics herdr --skill leaves out, which all three rely on.
The ledger tool field.py has exactly one copy, in skills/field-agent/. field-handler and field-audit call it there. Do not copy it into another skill folder; a second copy goes stale the moment the first one changes.
field-handler: run a room of field agents/field-handler is usually not typed. It surfaces on its own when work is growing complex or splitting into independent strands, offers to open a room, and agrees the scope with you first. On agreement it opens a herdr workspace with one field agent per project. This session becomes the handler, it watches, steers and reports, rather than delegating to a second pane. Their reports arrive as turns here, so the room shares this conversation.
You can also invoke it directly:
/field-handler get the zoho lead projects aligned on one shared match key
Config is skills/field-handler/handler.json (agent model, and layout_threshold: panes side-by-side at or below the threshold, one tab per agent above it). Per-agent harness overrides reuse field-agent's harnesses.json.
Requires herdr >= 0.9.0. The skill checks HERDR_ENV=1 and stops cleanly if you are not inside a herdr-managed pane.
Optional hardening (for yourself only): to make the proactive offer fire more reliably, add one line to your global ~/.claude/CLAUDE.md:
When work is growing complex or splitting into independent strands across projects, proactively offer to open a room with the
field-handlerskill: name the strands, agree scope with me, then spin them up. Never spawn without that conversation.
This is not required for the skill to work and does not travel with the package.
component-system-builder: consistency by removing choicesThe premise is that an agent produces consistent, polished UI when the platform gives it fewer choices, not when the model is smarter. The skill builds six artifacts (a component kit with docs beside source, seed-variable theming, a pattern-table gate, rules skills plus a tiny always-on constraints file, context-economy plumbing, and a scaffold) and places each rule as high on an enforcement ladder as the platform allows (prose → checklists → the gate → closed type surfaces → lint/CI → physically blocked).
Two modes. Green-field starts from a platform scaffold and builds outward. Retrofit runs an audit loop over an existing project, one merge-able commit per pass: inventory, tokens, componentize, build the gate, then mechanical enforcement. Each pass writes its state into docs/system-audit.md, so a cold session can resume from it.
/component-system-builder retrofit a component system onto this Astro site
Three reference files (references/operations.md, references/enforcement.md, references/runtime-discipline.md) load on demand rather than up front.
Requires Node.js (for npx).
# Install all skills, globally, for Claude Code
npx skills add gregbarbosa/skills -g -a claude-code -y
# Install just one skill
npx skills add gregbarbosa/skills -s field-agent -g -a claude-code -y
Drop the -g flag to install into the current project (.claude/skills/) instead of your user directory.
The repo is also a Claude Code plugin marketplace (.claude-plugin/marketplace.json) for mods: function-hook plugins that add commands, bands and status lines inside Claude Code. Mods are Claude Code only, so they install separately from the skills above; npx skills ignores them.
| Mod | What it does |
|---|---|
handoff-compact | /handoff-compact [notes] runs the handoff skill, then, once the file is saved under ~/.claude/handoffs/, shows a band (Handoff saved: <file> · compacting in 10s, with Compact now and Cancel), compacts with the handoff's resume instructions and submits the resume prompt. Typing anything stops the countdown. Needs the handoff skill installed. |
Install from the prompt of a terminal Claude Code session, answering y to add the marketplace and choosing the user scope:
/plugin install handoff-compact --marketplace gregbarbosa/skills
If you already load the mod from a local folder (--plugin-dir or CLAUDE_CODE_PLUGIN_DIRS), remove that entry when you install it here: both copies load otherwise, and each runs its own countdown and compaction.
An installed mod is a copy in the plugin cache, kept per version. To pick up a newer one:
claude plugin marketplace update gregbarbosa
claude plugin update handoff-compact@gregbarbosa
then restart Claude Code. The update compares versions, so a change merged without a version bump in the mod's plugin.json never reaches an installed copy.
To work on a mod, run it from your clone with claude --plugin-dir mods/<name>, then claude plugin validate mods/<name> and claude plugin test mods/<name> before opening a PR. Every PR that changes a mod bumps its version.
git clone https://github.com/gregbarbosa/skills.git
cd skills
npx skills init my-new-skill # scaffolds skills/my-new-skill/SKILL.md
# edit skills/my-new-skill/SKILL.md, then:
git add . && git commit -m "Add my-new-skill" && git push
Pushed changes are live immediately; npx skills add pulls main.
skills/
<name>/SKILL.md
<name>/references/*.md # optional, loaded on demand
<name>/<config>.json # optional
mods/
<name>/.claude-plugin/plugin.json
<name>/hooks/hooks.json, register.tsx, *.test.ts
<name>/types/index.d.ts # the mod's state contract
.claude-plugin/marketplace.json # lists the mods
Each skill is one folder containing a SKILL.md. The skills CLI discovers them automatically, and sibling files and subdirectories install alongside it. Each mod is one plugin folder under mods/, listed in the marketplace file.
hooks/register.tsx 153 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pending } from '../types'
5
6const COUNTDOWN_MS = 10_000
7const isArmed = atom({ plugin: 'handoff-compact', key: 'isArmed' } as const, false)
8const pending = atom({ plugin: 'handoff-compact', key: 'pending' } as const, null)
9const now = atom({ plugin: 'handoff-compact', key: 'now' } as const, 0)
10
11export const isHandoffFile = (path: string) => /\/\.claude\/handoffs\/.+\.md$/.test(path)
12
13export const baseName = (path: string) => path.split('/').pop() ?? path
14
15// The same text the handoff skill tells the user to paste after /compact.
16export const resumeText = (path: string) =>
17 `Read ${path}. Run its Verify first checks; if they pass, set its status to consumed and resume from the first unchecked IN-FLIGHT item; if not, report the drift and wait.`
18
19export const askText = (notes: string) =>
20 [
21 'Run the handoff skill now (Skill tool, skill "handoff").',
22 notes ? `Context from the user for the handoff: ${notes}` : '',
23 'Skip its last step: the handoff-compact mod runs /compact and the resume prompt for you once your turn ends, so end the turn as soon as the file is saved and printed.',
24 ]
25 .filter(Boolean)
26 .join('\n')
27
28// Timers live with this load; a reload drops a running countdown, and the band offers the button instead.
29let timers: { cancel: () => void }[] = []
30
31function stopTimers() {
32 for (const timer of timers) timer.cancel()
33 timers = []
34}
35
36async function compactNow($: EngineInterface) {
37 stopTimers()
38 const held: Pending | null = await read($, pending)
39 if (!held) return
40 await update($, pending, () => ({ ...held, autoAt: null }))
41 try {
42 const done = await $.session.compact({ instructions: resumeText(held.path) })
43 if (done && 'skip' in done && done.skip) {
44 $.ui.toast('Compaction was vetoed by another hook; the handoff is still saved.')
45 return
46 }
47 await update($, pending, () => null)
48 void $.prompt.submit({ text: resumeText(held.path) })
49 } catch (error) {
50 $.ui.toast(`Could not compact yet (${String(error).slice(0, 80)}). Press Compact now when the turn ends.`, {
51 timeoutMs: 8000,
52 })
53 }
54}
55
56export const register: Register = on => {
57 on('session.start', async ($, e, next) => {
58 const started = await next(e)
59 stopTimers()
60 await $.command.register({
61 name: 'handoff-compact',
62 description: 'Write a handoff, compact with its resume instructions, then resume (optional notes for the handoff)',
63 })
64 // A countdown cut off by a reload waits for a press instead.
65 const held = await read($, pending)
66 if (held?.autoAt) await update($, pending, () => ({ ...held, autoAt: null }))
67 return started
68 })
69
70 on('command.run', { command: 'handoff-compact' }, async ($, e) => {
71 stopTimers()
72 await update($, isArmed, () => true)
73 await update($, pending, () => null)
74 // A command may not submit while it answers; the timer submits right after.
75 const ask = askText(e.args.trim())
76 timers.push($.clock.after(0, () => void $.prompt.submit({ text: ask })))
77 return { text: 'Writing the handoff. It compacts 10 seconds after the file is saved; type anything to stop it.' }
78 })
79
80 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
81 const result = await next(e)
82 const path = String((e as { file_path?: string }).file_path ?? '')
83 const failed = ('deny' in result && result.deny) || ('isError' in result && result.isError)
84 if (!e.agentId && !failed && isHandoffFile(path)) {
85 await update($, pending, () => ({ path, autoAt: null }))
86 }
87 return result
88 })
89
90 // Anything the user types takes over: no compaction happens behind their back.
91 on('prompt.submit', async ($, e, next) => {
92 if (e.origin?.kind === 'composer') {
93 const held = await read($, pending)
94 if (held?.autoAt) {
95 stopTimers()
96 await update($, pending, () => ({ ...held, autoAt: null }))
97 }
98 await update($, isArmed, () => false)
99 }
100 return next(e)
101 })
102
103 on('turn.complete', async ($, e, next) => {
104 const done = await next(e)
105 if (e.agentId) return done
106
107 const armed = await read($, isArmed)
108 const held = await read($, pending)
109 if (armed) {
110 await update($, isArmed, () => false)
111 if (!held) {
112 $.ui.toast('No handoff file was written, so nothing was compacted.', { timeoutMs: 8000 })
113 return done
114 }
115 const start = await $.clock.now()
116 await update($, now, () => start)
117 await update($, pending, () => ({ ...held, autoAt: start + COUNTDOWN_MS }))
118 stopTimers()
119 timers.push($.clock.every(1000, () => void $.clock.now().then(t => update($, now, () => t))))
120 timers.push($.clock.after(COUNTDOWN_MS, () => void compactNow($)))
121 }
122 return done
123 })
124
125 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
126 const held = await read($, pending)
127 if (e.props.hasSurvey || !held || e.props.isWorking) return next(e)
128
129 const { Box, Button, Text } = $.ui.resolve(e)
130 const left = held.autoAt ? Math.max(0, Math.ceil((held.autoAt - (await read($, now))) / 1000)) : null
131
132 return (
133 <Box flexDirection="row" paddingX={1}>
134 <Text color="cyan" bold>
135 Handoff saved:{' '}
136 </Text>
137 <Text>{baseName(held.path)}</Text>
138 <Text dimColor>{left === null ? ' ' : ` compacting in ${left}s `}</Text>
139 <Button key="now" label="Compact now" onPress={() => void compactNow($)} />
140 <Text> </Text>
141 <Button
142 key="drop"
143 label={left === null ? 'Dismiss' : 'Cancel'}
144 onPress={async () => {
145 stopTimers()
146 await update($, pending, () => null)
147 }}
148 />
149 </Box>
150 )
151 })
152}
153types/index.d.ts 12 lines1export type Pending = {
2 path: string
3 // When the countdown compacts on its own; null waits for a press.
4 autoAt: number | null
5}
6
7declare module 'claude-code' {
8 interface PluginState {
9 'handoff-compact': { isArmed: boolean; pending: Pending | null; now: number }
10 }
11}
12