SLOPSHOPPER

crew-spike

Phase 0 spike for crew: checks the agent-team APIs crew depends on and logs what happens. Not for release.

newguardprompttoolagents
v0.0.1MITupdated 2026-10-15PeterTing/claude-mod/spikes/crew
A shopper browsing a rack in a slop shop
README

claude-mod

Security-reviewed mods for Claude Code, with mods made here to follow: live panes, bands above the prompt, status-line entries, toasts, slash commands, and hooks on tool calls or prompts that change how Claude Code looks and behaves.

繁體中文

This repository is a Claude Code plugin marketplace. It holds mods made here, in mods/, and community mods that passed a security review, each pinned to the exact commit that was reviewed. Any of them installs with one line.

Install a mod

At the prompt of a Claude Code terminal session (2.1.287 or later):

/plugin install <mod> --marketplace PeterTing/claude-mod

For example, /plugin install usage-meter --marketplace PeterTing/claude-mod.

Answer y to add the marketplace, then pick a scope (user scope is the default).

From a shell:

claude plugin install <mod> --marketplace PeterTing/claude-mod

In the desktop app, where /plugin does not run: click + next to the prompt box → Plugins → Add marketplace with PeterTing/claude-mod → Add plugin.

Reviewed community mods

Mods by other authors are not sandboxed: they run with your permissions. Each one listed here was reviewed in full before listing, and installing it from this marketplace gets exactly the reviewed commit. COLLECTION.md has every mod's caveats, and docs/vetting.md explains the review.

ModWhat it doesVerdict
usage-meter5-hour and 7-day plan usage bars above the promptRecommend
context-barContext window as a stacked bar by categoryRecommend
token-weather-usageContext, limits, cache time left and cost in one lineRecommend
flightdeckLive dashboard pane for the model, permission checks and subagentsRecommend
whats-agent-doingA box above the prompt saying what Claude is doing nowRecommend
agent-radarOne live line per running subagentRecommend
filetreeFile tree with git status that highlights files Claude touchesRecommend
prompt-railA rail of your past prompts to reread and jump toRecommend
human-in-the-loopTasks only you can do, waiting in a pane until you answerCaution
prismantisThemed tables, code and Mermaid diagrams in repliesCaution
leakstopMasks or holds secrets before they leakCaution
cache-taxKeeps the prompt cache warm during breaksCaution

Caution means the mod is acceptable but has trade-offs to read first, such as token cost, system-prompt additions or rewritten prompts.

Mods made here

ModWhat it doesKind
zh-replyClaude and its agents reply in Chinese (Traditional by default); a reply in another language is asked for againSystem prompt, agent tasks, messages; sends a rewrite prompt
crewA resident PM and QA work your PRD into features, acceptance criteria and test cases, bring gaps and questions back to you, and remember decisions in the repo. Work in progressSpawns subagents, MCP tools, a pane; writes prd-tracker.json and .crew/; uses Haiku once a day

Repository layout

.claude-plugin/marketplace.json   # marketplace manifest: mods made here + pinned community mods
docs/vetting.md                   # how community mods are reviewed
mods/<mod-name>/
  .claude-plugin/plugin.json      # name, version, description
  hooks/hooks.json                # { "modules": ["./register.tsx"] }
  hooks/register.tsx              # the hooks module
  hooks/*.test.ts                 # behaviour tests
  README.md                       # what it does, screenshots, options
COLLECTION.md                     # reviewed community mods: verdicts, caveats, pinned commits

Develop a mod

claude --plugin-dir mods/<mod-name>      # run Claude Code with the mod loaded (hot-reloads on save)
claude plugin validate mods/<mod-name>   # check the manifest and hooks module
claude plugin test mods/<mod-name>       # run the mod's *.test.ts files
claude plugin validate .                 # check the marketplace manifest

To add a mod: create mods/<mod-name>/, add an entry { "name": "<mod-name>", "source": "./mods/<mod-name>" } to plugins in .claude-plugin/marketplace.json, add a row to the table above, and make sure validate and test both pass.

Contributing

Issues and pull requests are welcome, whether it's a new mod, a fix, or a community mod you'd like added. For a community mod, include the repository URL, the plugin path, the license and one line on what it does; it is listed after it passes the review.

License

MIT for this repository. Community mods are fetched from their authors' repositories and keep their own licenses.

Source 1 files
hooks/register.ts 219 lines
1import type { Register } from 'claude-code'
2
3/**
4 * Phase 0 spike for crew. One headless run walks S1–S8 and writes every
5 * observation to <cwd>/crew-spike.jsonl:
6 *
7 *   S1 a plugin-defined agent type spawned as a named teammate, its model and effort
8 *   S2 the teammate calls this plugin's tool, and the call carries its agentId
9 *   S3 per-request usage (context size) for the teammate's loop
10 *   S4 stopping the teammate with TaskStop and respawning it under the same name
11 *   S5 a delivery to the teammate can carry appended text (memory injection)
12 *   S6 session.compact for an agent loop (logged if it ever fires)
13 *   S7 the teammate's fixed cost: a lean agent against one with default tools and CLAUDE.md
14 *   S8 rewriting effort, then model, on a step, and what that does to the prompt cache
15 */
16
17const MARK = 'MEM-SPIKE-7: the staging database is called aurora-blue.'
18
19const spike = {
20  logPath: '',
21  lines: [] as string[],
22  phase: 'start',
23  started: false,
24  echoId: undefined as string | undefined,
25  echoWakes: 0,
26  crewIds: new Set<string>(),
27  dropNotifications: true,
28}
29
30async function log($: any, kind: string, data: unknown) {
31  spike.lines.push(JSON.stringify({ t: Date.now(), phase: spike.phase, kind, data }))
32  if (spike.logPath !== '') await $.fs.write(spike.logPath, `${spike.lines.join('\n')}\n`)
33}
34
35async function spawnEcho($: any, type: string, prompt: string) {
36  const r = await $.agent.spawn({ subagentType: type, name: 'echo', description: 'crew spike echo', prompt })
37  await log($, 'spawn.result', { type, r })
38  if (r.agentId !== undefined) spike.crewIds.add(r.agentId)
39  return r
40}
41
42async function sendEcho($: any, text: string) {
43  await log($, 'send.result', await $.session.send({ to: 'echo', text }))
44}
45
46export const register: Register = on => {
47  on('session.start', async ($: any, e: any, next: any) => {
48    spike.logPath = `${e.cwd}/crew-spike.jsonl`
49    const tool = await $.tool.register({
50      name: 'note',
51      description: 'Record one short note for the crew spike. Call it exactly as instructed.',
52      inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
53      isDeferred: false,
54    })
55    await log($, 'tool.register', tool)
56    const lean = await $.agent.register({
57      name: 'echo',
58      description: 'Crew spike test agent. Only used by the crew-spike plugin.',
59      prompt: 'You are a test agent for a plugin spike. Do exactly what each message asks, using the note tool when told, then end your turn with the single word the message asks for. Be brief.',
60      tools: ['mcp__crew-spike__note'],
61      model: 'haiku',
62      effort: 'low',
63      omitClaudeMd: true,
64      autoCompactWindow: 100000,
65    })
66    await log($, 'agent.register lean', lean)
67    const full = await $.agent.register({
68      name: 'echo-full',
69      description: 'Crew spike baseline agent with default tools and CLAUDE.md.',
70      prompt: 'You are a test agent for a plugin spike. Reply with the single word the message asks for.',
71      model: 'haiku',
72      effort: 'low',
73    })
74    await log($, 'agent.register full', full)
75    const dev = await $.agent.register({
76      name: 'echo-dev',
77      description: 'Crew spike agent with the tools a working role needs.',
78      prompt: 'You are a test agent for a plugin spike. Reply with the single word the message asks for.',
79      tools: ['Read', 'Edit', 'Write', 'Bash', 'mcp__crew-spike__note'],
80      model: 'haiku',
81      effort: 'low',
82      omitClaudeMd: true,
83    })
84    await log($, 'agent.register dev', dev)
85    return next(e)
86  })
87
88  on('prompt.submit', async ($: any, e: any, next: any) => {
89    const isCrewNotice =
90      e.origin?.kind === 'task-notification' && [...spike.crewIds].some(id => String(e.text).includes(id))
91    await log($, 'prompt.submit', { origin: e.origin, isMeta: e.isMeta, turnId: e.turnId, isCrewNotice, text: String(e.text).slice(0, 160) })
92    if (isCrewNotice && spike.dropNotifications) {
93      return { drop: 'crew: a routine agent update, folded into the digest' }
94    }
95    const r = await next(e)
96    if (!spike.started && e.origin?.kind !== 'plugin' && (await $.env.get('CREW_SPIKE_BASELINE')) === '1') {
97      spike.started = true
98      spike.phase = 'S7'
99      for (const type of ['crew-spike:echo', 'crew-spike:echo-dev', 'crew-spike:echo-full']) {
100        const b = await $.agent.spawn({ subagentType: type, name: type.split(':')[1], description: 'crew spike baseline', prompt: 'Reply with the single word DEV.' })
101        await log($, 'spawn.result', { type, r: b })
102      }
103      return r
104    }
105    if (!spike.started && e.origin?.kind !== 'plugin') {
106      spike.started = true
107      spike.phase = 'S1'
108      const spawned = await spawnEcho($, 'crew-spike:echo', 'Call the note tool with the text "hello from echo". Then reply DONE1.')
109      spike.echoId = spawned.agentId
110    }
111    return r
112  })
113
114  on('agent.spawn', async ($: any, e: any, next: any) => {
115    const r = await next(e)
116    await log($, 'agent.spawn', {
117      origin: next.origin,
118      subagentType: e.subagentType,
119      name: e.name,
120      isTeammate: e.isTeammate,
121      background: e.background,
122      model: e.model,
123      parentModel: e.parentModel,
124      result: r,
125    })
126    return r
127  })
128
129  on('tool.call', { tool: 'mcp__crew-spike__note' }, async ($: any, e: any) => {
130    await log($, 'note', { agentId: e.agentId, input: e.input ?? e })
131    return { result: 'noted' }
132  })
133
134  on('turn.step', async function* ($: any, e: any, next: any) {
135    let input = e
136    if (e.agentId !== undefined && e.agentId === spike.echoId) {
137      if (spike.phase === 'S8-effort') input = { ...e, effort: 'medium' }
138      if (spike.phase === 'S8-model') input = { ...e, model: 'claude-sonnet-5-5' }
139    }
140    await log($, 'step.in', { agentId: e.agentId, model: input.model, effort: input.effort, messageCount: e.messageCount, asked: { model: e.model, effort: e.effort } })
141    const stream = next(input)
142    for await (const chunk of stream) {
143      if (chunk.kind === 'stop') {
144        await log($, 'step.stop', { agentId: e.agentId, stopReason: chunk.stopReason, usage: chunk.usage })
145      }
146      yield chunk
147    }
148    return await stream.result
149  })
150
151  on('session.send', async ($: any, e: any, next: any) => {
152    const r = await next(e)
153    await log($, 'session.send', { to: e.to, origin: e.origin, agentId: e.agentId, text: String(e.text).slice(0, 200), result: r })
154    return r
155  })
156
157  on('session.receive', async ($: any, e: any, next: any) => {
158    const isForEcho = e.agentId !== undefined && e.agentId === spike.echoId
159    const text = spike.phase === 'S5' && isForEcho ? `${e.text}\n\n[crew memory] ${MARK}` : e.text
160    const r = await next({ ...e, text })
161    await log($, 'session.receive', { agentId: e.agentId, origin: e.origin, injected: text !== e.text, text: String(text).slice(0, 300) })
162    return r
163  })
164
165  on('turn.start', async ($: any, e: any, next: any) => {
166    if (e.agentId !== undefined) await log($, 'turn.start', { agentId: e.agentId, text: String(e.text).slice(0, 400) })
167    return next(e)
168  })
169
170  on('session.compact', async ($: any, e: any, next: any) => {
171    await log($, 'session.compact', { agentId: e.agentId, trigger: e.trigger, messages: e.messages?.length })
172    return next(e)
173  })
174
175  on('turn.complete', async ($: any, e: any, next: any) => {
176    const r = await next(e)
177    await log($, 'turn.complete', { agentId: e.agentId, reason: e.reason, usage: e.usage, answer: String(e.answer).slice(0, 300) })
178    if (e.agentId === undefined || e.agentId !== spike.echoId) {
179      if (e.agentId !== undefined && spike.phase === 'S7' && String(e.answer).includes('DEV')) {
180        spike.phase = 'done'
181        await log($, 'agents', await $.agent.list())
182        $.prompt.submit({ text: 'The crew spike is finished. Reply with the single word FINISHED.' }).catch(() => undefined)
183      }
184      return r
185    }
186    spike.echoWakes += 1
187    await log($, 'agents', await $.agent.list())
188    if (spike.echoWakes === 1) {
189      spike.phase = 'S5'
190      await sendEcho($, 'Read the line that starts with [crew memory] in this message, then call the note tool with the name of the staging database it mentions. Then reply DONE2.')
191    } else if (spike.echoWakes === 2) {
192      spike.phase = 'S8-effort'
193      await sendEcho($, 'Call the note tool with the text "effort check". Then reply DONE3.')
194    } else if (spike.echoWakes === 3) {
195      spike.phase = 'S8-model'
196      await sendEcho($, 'Call the note tool with the text "model check". Then reply DONE4.')
197    } else if (spike.echoWakes === 4) {
198      spike.phase = 'S4'
199      const stopped = await $.tool.call({ tool: 'TaskStop', task_id: spike.echoId }).catch((err: unknown) => ({ error: String(err) }))
200      await log($, 'TaskStop', stopped)
201      await log($, 'agents after stop', await $.agent.list())
202      const again = await spawnEcho($, 'crew-spike:echo', 'You were restarted. Call the note tool with the text "respawned". Then reply DONE5.')
203      spike.echoId = again.agentId
204      spike.echoWakes = 4
205      spike.phase = 'S4-respawned'
206    } else {
207      spike.phase = 'S7'
208      await log($, 'agents', await $.agent.list())
209      const full = await $.agent.spawn({ subagentType: 'crew-spike:echo-full', name: 'echo-full', description: 'crew spike baseline', prompt: 'Reply with the single word BASELINE.' })
210      if (full.agentId !== undefined) spike.crewIds.add(full.agentId)
211      await log($, 'spawn.result', { type: 'crew-spike:echo-full', r: full })
212      const dev = await $.agent.spawn({ subagentType: 'crew-spike:echo-dev', name: 'echo-dev', description: 'crew spike dev baseline', prompt: 'Reply with the single word DEV.' })
213      if (dev.agentId !== undefined) spike.crewIds.add(dev.agentId)
214      await log($, 'spawn.result', { type: 'crew-spike:echo-dev', r: dev })
215    }
216    return r
217  })
218}
219