SLOPSHOPPER

phase-tracker

Tracks and displays the current execution phase (Spec -> Red Test -> Surgical Code -> Verification)

newbandcommand
v0.1.0MITupdated 2026-10-19Covre81/claude-code-mods/phase-tracker
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · phase-tracker
› 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 › /fase ⎿ phase-tracker: Fase ou ação desconhecida: comando vazio ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods

Function-hook mods for Claude Code: shell safety, secret guarding, session telemetry, context panels, and workflow tools. Each mod is an isolated plugin with its own tests, and you enable only the ones you want.

Mods

Security

ModDescription
guardaIntercepts risky Bash commands (rm -rf, git reset --hard, force push, new dependencies) and asks for confirmation explaining the impact.
lixeiraSends rm / Remove-Item targets to the Windows Recycle Bin instead of deleting them permanently.
cofreKeeps API keys out of the conversation: /chave stores them in .env, secret file reads display only variable names, outputs are masked, and pasted keys are quarantined.
atomic-commit-guardBlocks commits that mix top-level directories, violate Conventional Commits, or lack a Co-Authored-By trailer.
universal-audit-logA hook on * that logs every event (tool calls, prompts, turns, fs/http/process calls from other plugins) with secret redaction.

Session & Context

ModDescription
context-monitorContext weather and token usage above the prompt, replay and activity panels, plain-language explanation before each edit, and ai-memory notes.
cache-frioConfirms before submitting a prompt to a large conversation whose prompt cache has gone cold.
session-telemetryTimestamp on each response, TTFT and tok/s per step, and visual notice whenever the responding model switches.
session-time-machine/timemachine: renders the session as a timeline of prompts, tool calls, and turn completions.
turn-inspectorSide pane displaying the session turn-by-turn: every model request and what it carried.
clean-viewCollapses each successful tool call into a single concise line (Edit file +12 −3); errors and in-flight calls retain the full block.
webfetch-cacheServes repeated WebFetch requests (identical URL and prompt) directly from the session cache.
reflexoDetects corrections and constraints in prompts, offering to save them as rules in CLAUDE.md.

Agents & Workflow

ModDescription
agent-flowLive diagram of agent flow: main loop context, subagent tree, and context passed/returned by each agent.
agent-radarOne live line per running subagent above the prompt; /radar opens a pane with messages from each.
delegation-dockStatus line indicator showing in-flight delegations (agy/codex/ollama) and alerting when one returns empty.
phase-trackerTracks and displays the current development phase (Spec → Red Test → Surgical Code → Verification).
next-promptsBand with 3 next-step suggestions generated by a free Ollama cloud model; click one to draft.
git-sidebarLazygit-style side pane: worktrees, local branches, branch switching, and worktree checkout.
wait-quizQuizzes a flashcard in a band above the prompt while Claude is working.
meus-modsCentral hub: quick-access bar above the prompt and /mods command displaying the status of each mod.

Installation

Requires Claude Code with function-hook mods support.

git clone https://github.com/Covre81/claude-code-mods.git ~/.claude/mods

Enable the mods by listing their absolute paths in CLAUDE_CODE_PLUGIN_DIRS inside ~/.claude/settings.json. On Windows, paths are separated by ;:

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "C:/Users/me/.claude/mods/guarda;C:/Users/me/.claude/mods/cofre;C:/Users/me/.claude/mods/session-telemetry"
  }
}

Order matters: the last plugin in the list sits closest to the engine. For instance, lixeira should come after guarda so guarda inspects the original rm -rf first.

Several mods include a command to toggle them on or off without restarting (/webfetch-cache on|off, /radar-toggle, /telemetria…); /mods displays the status of all mods.

Development

Each mod follows the same structure:

<mod>/
  .claude-plugin/plugin.json   manifest
  .claude-plugin/types/        engine types (git-ignored, see below)
  hooks/hooks.json             hooks registered by the mod
  hooks/register.ts(x)         code
  hooks/*.test.ts(x)           tests

Engine types are not committed to the repository. To typecheck and test a mod, copy .claude-plugin/types from an existing mod or generate them with your Claude Code instance.

Tests run directly within Claude Code, not via vitest/jest:

claude plugin test guarda               # single mod
for d in */hooks/hooks.json; do claude plugin test "${d%%/*}"; done   # all mods

Commits follow Conventional Commits, scoped to one mod per commit (enforced by atomic-commit-guard).

Credits

Six mods (agent-flow, git-sidebar, session-time-machine, turn-inspector, universal-audit-log, webfetch-cache) originate from davila7/claude-code-templates, and agent-radar originates from hamzafer/claude-code-mods, both under the MIT License and adapted here. Copyright notices are preserved in THIRD_PARTY_NOTICES.md.

License

MIT

Source 3 files
hooks/register.tsx 107 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { PhaseId } from '../types'
5import { formatBadge, nextPhase, parsePhaseArgs } from './phases'
6
7const isEnabledRef = atom({ plugin: 'phase-tracker', key: 'isEnabled' } as const, true)
8const currentRef = atom({ plugin: 'phase-tracker', key: 'current' } as const, 'idle' as PhaseId)
9const taskRef = atom({ plugin: 'phase-tracker', key: 'task' } as const, null as string | null)
10
11/** Store key of a project's phase: the store is global, so each folder gets its own. */
12export const phaseKey = (cwd: string) => `phase:${cwd.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()}`
13
14// Set at session.start (which re-fires on reload); saves go to the project the phase was loaded from, even after a /cd.
15// ponytail: keyed by the start folder, a session opened in a subfolder is a separate project; walk up to the git root if that bites.
16let projectKey = phaseKey('')
17
18// $.state is per session: phase and task are mirrored to the store and restored at session.start.
19async function savePhase($: EngineInterface) {
20  await $.store.set(projectKey, { current: await read($, currentRef), task: await read($, taskRef) })
21}
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    const saved = await $.store.get('isEnabled').catch(() => undefined)
26    await update($, isEnabledRef, () => (typeof saved === 'boolean' ? saved : isEnabledRef.initial))
27    projectKey = phaseKey(e.cwd)
28    const phase = (await $.store.get(projectKey).catch(() => undefined)) as { current?: PhaseId; task?: string | null } | undefined
29    await update($, currentRef, () => phase?.current ?? 'idle')
30    await update($, taskRef, () => phase?.task ?? null)
31    await $.command.register({
32      name: 'fase',
33      description: 'Acompanha e gerencia a fase da tarefa atual (spec, test, code, verify, done, next, reset)',
34      argumentHint: '<fase|next|reset|on|off> [descricao]',
35    })
36    return next(e)
37  })
38
39  on('command.run', { command: 'fase' }, async ($, e) => {
40    const parsed = parsePhaseArgs(e.args)
41    if (parsed.error) return { text: parsed.error }
42
43    if (parsed.action === 'on' || parsed.action === 'off') {
44      const isEnabled = parsed.action === 'on'
45      await update($, isEnabledRef, () => isEnabled)
46      await $.store.set('isEnabled', isEnabled) // $.state is per session; the store survives a restart
47      return { text: `phase-tracker ${isEnabled ? 'ativado' : 'desativado'}.` }
48    }
49
50    if (parsed.action === 'reset') {
51      await update($, currentRef, () => 'idle')
52      await update($, taskRef, () => null)
53      await savePhase($)
54      return { text: 'Fase resetada para idle.' }
55    }
56
57    if (parsed.action === 'next') {
58      const curr = await read($, currentRef)
59      const next = nextPhase(curr)
60      await update($, currentRef, () => next)
61      await savePhase($)
62      const task = await read($, taskRef)
63      return { text: `Avançado para a fase: ${formatBadge(next, task)}` }
64    }
65
66    if (parsed.phase) {
67      await update($, currentRef, () => parsed.phase!)
68      await update($, taskRef, () => parsed.task ?? null)
69      await savePhase($)
70      return { text: `Fase definida: ${formatBadge(parsed.phase, parsed.task ?? null)}` }
71    }
72
73    const curr = await read($, currentRef)
74    const task = await read($, taskRef)
75    return { text: `Fase atual: ${formatBadge(curr, task) || 'idle'}` }
76  })
77
78  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
79    if (e.props.hasSurvey) return next(e)
80    const isEnabled = await read($, isEnabledRef)
81    if (!isEnabled) return next(e)
82
83    const current = await read($, currentRef)
84    const task = await read($, taskRef)
85    const badge = formatBadge(current, task)
86    if (!badge) return next(e)
87
88    const { Box, Text } = $.ui.resolve(e)
89    const below = await next(e)
90
91    const mine = (
92      <Box width={e.props.bodyColumns} flexDirection="row">
93        <Text color="#4aa3df" bold>
94          {badge}
95        </Text>
96      </Box>
97    )
98
99    return below.type === 'engine' ? mine : (
100      <Box flexDirection="column">
101        {below}
102        {mine}
103      </Box>
104    )
105  })
106}
107
hooks/phases.ts 59 lines
1import type { PhaseId } from '../types'
2
3const ORDER: PhaseId[] = ['idle', 'spec', 'test', 'code', 'verify', 'done']
4
5export function nextPhase(current: PhaseId): PhaseId {
6  const idx = ORDER.indexOf(current)
7  if (idx === -1 || idx === ORDER.length - 1) return 'idle'
8  return ORDER[idx + 1]!
9}
10
11export type ParsedPhase = {
12  phase?: PhaseId
13  task?: string
14  action?: 'next' | 'reset' | 'off' | 'on'
15  error?: string
16}
17
18const PHASES: Record<string, PhaseId> = {
19  spec: 'spec',
20  test: 'test',
21  code: 'code',
22  verify: 'verify',
23  done: 'done',
24}
25
26export function parsePhaseArgs(input: string): ParsedPhase {
27  const trimmed = input.trim()
28  if (!trimmed) return { error: 'Fase ou ação desconhecida: comando vazio' }
29
30  const parts = trimmed.split(/\s+/)
31  const head = parts[0]!.toLowerCase()
32  const task = parts.slice(1).join(' ').trim() || undefined
33
34  if (head === 'next' || head === 'reset' || head === 'off' || head === 'on') {
35    return { action: head }
36  }
37
38  if (head in PHASES) {
39    return { phase: PHASES[head], task }
40  }
41
42  return { error: `Fase ou ação desconhecida: ${head}` }
43}
44
45const LABELS: Record<PhaseId, string> = {
46  idle: '',
47  spec: '[1/4 Spec]',
48  test: '[2/4 Test]',
49  code: '[3/4 Code]',
50  verify: '[4/4 Verify]',
51  done: '[✓ Concluído]',
52}
53
54export function formatBadge(phase: PhaseId, task: string | null): string {
55  if (phase === 'idle') return ''
56  const prefix = LABELS[phase]
57  return task ? `${prefix} ${task}` : prefix
58}
59
types/index.d.ts 13 lines
1export type PhaseId = 'idle' | 'spec' | 'test' | 'code' | 'verify' | 'done'
2
3// The validator wants each key named here, not behind an alias.
4declare module 'claude-code' {
5  interface PluginState {
6    'phase-tracker': {
7      isEnabled: boolean
8      current: PhaseId
9      task: string | null
10    }
11  }
12}
13