SLOPSHOPPER

Task Workflow

A resumable, worktree-isolated /task workflow for Claude Code

newbandguardcommandtoastprocess
★ 9v0.1.0MITupdated 2026-10-15yelog/.config/task-workflow/adapters/claude
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · task-workflow
› 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 › /task ⎿ task-workflow: 用法:/task <需求描述> task-workflow 状态不可用:invalid task-workflow response: dev ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
task-workflow 状态不可用:invalid task-workflow response: dev
README

.config — yelog 的 dotfiles

macOS 开发环境配置仓库,通过 git + 子模块管理 ~/.config 下 40+ 个工具的配置。

快速开始

git clone --recursive https://github.com/yelog/.config.git ~/.config
~/.config/init.sh

init.sh 在 macOS 上建立符号链接并安装依赖:

  • ~/.ideavimrc → ~/.config/ideavimrc
  • ~/.hammerspoon → ~/.config/hammerspoon
  • ~/.zshrc → ~/.config/zsh/zshrc
  • ~/mcpservers.json → ~/.config/mcp/mcpservers.json
  • brew install zsh 并设为默认 shell

模块总览

编辑器

模块说明
nvim/Neovim 配置,主力开发环境。重点:Java/Spring Cloud 调试(Overseer 服务面板 + DAP)、Maven dashboard、JDTLS、MyBatis 符号导航。见 nvim/README.md
ideavimrcIntelliJ IDEA 的 Vim 模拟配置(已回退到 nvim 工作流,Hyper 快捷键已停用)

终端

模块说明
kitty/日常终端,含自定义布局脚本与主题切换(jb-theme/snazzy)
ghostty/备用终端,含 shader 与 CLAUDE.md
wezterm/备用终端
alacritty/备用终端(toml 与 yml 双版本并存)
tmux/tmux.conf + powerline/tpm/sensible,子模块管理
zsh/zshrc + fzf 增强(fzf.sh、fzf-preview.sh)
starship.toml / neofetch/prompt 与系统信息

窗口管理与快捷键

模块说明
yabai/ + spacebar/ + skhd/平铺窗口管理 + 快捷键(含输入法状态脚本)
aerospace/另一套平铺 WM 配置
hammerspoon/窗口布局(九宫格/分屏)与应用切换(Hyper 键)。见 hammerspoon/README.md
karabiner/Caps 长按 → Hyper(cmd+shift+alt)复杂修改

文件管理

模块说明
ranger/主力文件管理器(rc.conf + rifle + scope.sh)
yazi/备用(toml 全套配置 + 插件)
lf/ / joshuto/备用

输入法

模块说明
rime/万象拼音方案 + macOS ↔ iPhone(Hamster/iCloud)同步。见 rime/README.md

AI 编码

模块说明
opencode/opencode CLI 配置(xrouter/omlx 等 provider)与 skills
.claude/Claude Code 本地设置
codexbar/CodexBar 菜单栏工具的 provider 配置
github-copilot/Copilot 认证与 JetBrains 配置
*.avanterulesavante.nvim 的规则体系(agentic/base/editing 等)

数据库

模块说明
vi-sql/终端 SQL 客户端(PostgreSQL,Vim 键位,keyring 凭据)

其他

模块说明
git/ / gh/git 与 GitHub CLI 配置
zellij/终端多路复用器(CLAUDE.md)
herdr/会话持久化配置
neovide/ ueberzugpp/ warpd/ docs/图形化 nvim、终端预览、鼠标导航、功能设计文档(docs/plans/)

子模块

路径上游
tmux/plugins/tpmtmux-plugins/tpm
tmux/plugins/tmux-powerlineerikw/tmux-powerline
tmux/plugins/tmux-sensibletmux-plugins/tmux-sensible
tmux/vendor/tmux-mem-cpu-loadthewtex/tmux-mem-cpu-load
ranger/plugins/ranger_deviconsalexanderjeurissen/ranger_devicons

文档导航

Source 1 files
hooks/register.ts 253 lines
1type ProcessResult = { exitCode: number; stdout: string; stderr: string }
2type Model = { provider?: string; model: string }
3type CoreResponse = { protocolVersion: number; ok: boolean; data?: any; error?: { code: string; message: string; retryable?: boolean } }
4type ActiveAttempt = {
5  taskId: string
6  attemptId: string
7  token: string
8  stage: string
9  model: Model
10  turnId?: string
11  root: string
12  worktree?: string
13  sessionId: string
14}
15
16const active = new Map<string, ActiveAttempt>()
17const statusText = new Map<string, string>()
18const timers = new Map<string, { cancel(): void }>()
19
20async function corePath($: any): Promise<string> {
21  const configured = await $.env.get('TASK_WORKFLOW_CORE')
22  if (typeof configured === 'string' && configured.trim()) return configured
23  return $.plugin.root.includes('/adapters/claude') ? `${$.plugin.root}/../../dist/cli.js` : `${$.plugin.root}/dist/cli.js`
24}
25
26async function runCore($: any, command: string, payload: Record<string, unknown>): Promise<any> {
27  const result = await $.process.run(['node', await corePath($), command, '--payload', JSON.stringify(payload)], { timeoutMs: 600_000 }) as ProcessResult
28  const raw = result.stdout.trim()
29  if (!raw) throw new Error(result.stderr || `task-workflow core returned no response for ${command}`)
30  let response: CoreResponse
31  try { response = JSON.parse(raw) as CoreResponse } catch { throw new Error(`invalid task-workflow response: ${raw.slice(0, 500)}`) }
32  if (!response.ok) throw new Error(response.error?.message ?? `task-workflow ${command} failed`)
33  return response.data
34}
35
36async function sessionId($: any): Promise<string> { return String(await $.session.id()) }
37async function sessionRoot($: any): Promise<string> { return String(await $.session.cwd()) }
38async function sessionCwd($: any): Promise<string> { return String(await $.session.cwd()) }
39
40function formatSummary(summary: any): string {
41  if (!summary) return '当前会话没有任务。'
42  const error = summary.lastError ? `\n错误:${summary.lastError}` : ''
43  const next = summary.next ? `\n下一步:${summary.next}` : ''
44  return `任务:${summary.name}\n状态:${summary.status}\n阶段:${summary.stage}${summary.reviewPending ? '(收尾审查)' : ''}\n轮次:${summary.round}/${summary.totalRounds}\n模型:${summary.model}\n分支:${summary.taskBranch || '(尚未创建)'} → ${summary.targetBranch}\nworktree:${summary.worktree || '(尚未创建)'}${next}${error}`
45}
46
47function setStatus($: any, summary: any): void {
48  const id = String(summary?.sessionId ?? '')
49  if (!id) return
50  statusText.set(id, summary ? `${summary.name} · ${summary.stage} · ${summary.status}${summary.reviewPending ? ' · review' : ''}${summary.next ? ` · ${summary.next}` : ''}` : '')
51  $.ui.invalidate('ui.render')
52}
53
54function inside(value: string, root: string): boolean {
55  const normalizedValue = value.replace(/\\/g, '/').replace(/\/$/, '')
56  const normalizedRoot = root.replace(/\\/g, '/').replace(/\/$/, '')
57  return normalizedValue === normalizedRoot || normalizedValue.startsWith(`${normalizedRoot}/`)
58}
59
60function writeTool(tool: string): boolean {
61  return ['Edit', 'Write', 'NotebookEdit', 'Bash', 'PowerShell', 'Monitor'].includes(tool)
62}
63
64function supportedModel(model: any): boolean {
65  const value = String(model?.model ?? '')
66  return value === 'codex/gpt-6.1-sol' || value === 'codex/gpt-6-luna' ||
67    value === 'gpt-6.1-sol' || value === 'gpt-6-luna' ||
68    value === 'group/auto-gpt-6-1-sol' || value === 'group/auto-gpt-6-luna' ||
69    value === 'sonnet' || value === 'opus' || value === 'haiku' || value === 'inherit' || value.startsWith('claude-')
70}
71
72function runtimeModel(model: Model): string {
73  if (model.provider === 'magpie' && model.model === 'codex/gpt-6.1-sol') return 'group/auto-gpt-6-1-sol'
74  if (model.provider === 'magpie' && model.model === 'codex/gpt-6-luna') return 'group/auto-gpt-6-luna'
75  // Keep old pinned task snapshots resumable while routing them through magpie.
76  if (model.provider === 'xrouter' && model.model === 'gpt-6.1-sol') return 'group/auto-gpt-6-1-sol'
77  if (model.provider === 'xrouter' && model.model === 'gpt-6-luna') return 'group/auto-gpt-6-luna'
78  if (model.model === 'codex/gpt-6.1-sol') return 'group/auto-gpt-6-1-sol'
79  if (model.model === 'codex/gpt-6-luna') return 'group/auto-gpt-6-luna'
80  if (model.model === 'gpt-6.1-sol') return 'group/auto-gpt-6-1-sol'
81  if (model.model === 'gpt-6-luna') return 'group/auto-gpt-6-luna'
82  return model.model
83}
84
85async function refreshStatus($: any): Promise<any> {
86  try {
87    const id = await sessionId($)
88    const summary = await runCore($, 'status', { root: await sessionRoot($), sessionId: id })
89    setStatus($, { ...summary, sessionId: id })
90    return summary
91  } catch (error) {
92    statusText.set(await sessionId($), `task-workflow 状态不可用:${error instanceof Error ? error.message : String(error)}`)
93    $.ui.invalidate('ui.render')
94    return undefined
95  }
96}
97
98function scheduleNext($: any): void {
99  void sessionId($).then(id => $.clock.after(0, () => { void startNext($, id) }))
100}
101
102async function startNext($: any, id: string): Promise<void> {
103  if (id !== await sessionId($) || active.has(id)) return
104  try {
105    const next = await runCore($, 'next', { root: await sessionRoot($), sessionId: id })
106    setStatus($, { ...next.summary, sessionId: id })
107    if (next.kind === 'done') return
108    if (next.kind === 'integration') {
109      if (!supportedModel(next.model)) throw new Error(`Claude Code cannot use configured model ${next.model?.model ?? '(empty)'}; set executor to sonnet, opus, haiku, or a claude-* model id.`)
110      const attempt: ActiveAttempt = { taskId: next.task, attemptId: next.attemptId, token: next.token, stage: 'integrate', model: next.model, root: await sessionRoot($), sessionId: id }
111      active.set(id, attempt)
112      await $.prompt.submit({ text: next.prompt })
113      return
114    }
115    if (!supportedModel(next.model)) throw new Error(`Claude Code cannot use configured model ${next.model?.model ?? '(empty)'}; set planner/executor to sonnet, opus, haiku, or a claude-* model id.`)
116    const attempt: ActiveAttempt = { taskId: next.task, attemptId: next.attemptId, token: next.token, stage: next.stage, model: next.model, root: await sessionRoot($), worktree: next.summary?.worktree, sessionId: id }
117    active.set(id, attempt)
118    await $.prompt.submit({ text: next.prompt })
119  } catch (error) {
120    active.delete(id)
121    const message = error instanceof Error ? error.message : String(error)
122    statusText.set(id, `task-workflow 暂停:${message}`)
123    $.ui.toast(`任务暂停:${message}`)
124    $.ui.invalidate('ui.render')
125  }
126}
127
128async function pauseAfterAbort($: any, attempt: ActiveAttempt, reason: string): Promise<void> {
129  try {
130    const summary = await runCore($, 'pause', { root: attempt.root, sessionId: attempt.sessionId, reason })
131    setStatus($, { ...summary, sessionId: attempt.sessionId })
132  } catch (error) { $.ui.toast(`任务暂停状态保存失败:${error instanceof Error ? error.message : String(error)}`) }
133}
134
135export function register(on: any): void {
136  on('session.start', async ($: any, e: any, next: any) => {
137    await $.command.register({ name: 'task', description: 'Start a resumable analyze -> plan -> implement -> integrate task', argumentHint: '<requirement>' })
138    await $.command.register({ name: 'task-status', description: 'Show the current task status' })
139    await $.command.register({ name: 'task-resume', description: 'Resume the current paused task', immediate: true })
140    await $.command.register({ name: 'task-cleanup', description: 'List task workflow records; use apply only for explicit maintenance cleanup', argumentHint: '[apply]' })
141    const id = await sessionId($)
142    if (!timers.has(id)) timers.set(id, $.clock.every(3000, () => { void refreshStatus($) }))
143    await refreshStatus($)
144    return next(e)
145  })
146
147  on('session.end', async ($: any, e: any, next: any) => {
148    const id = await sessionId($)
149    timers.get(id)?.cancel()
150    timers.delete(id)
151    active.delete(id)
152    statusText.delete(id)
153    return next(e)
154  })
155
156  on('command.run', { command: 'task' }, async ($: any, e: any) => {
157    if (!e.args?.trim()) return { text: '用法:/task <需求描述>' }
158    const id = await sessionId($)
159    const data = await runCore($, 'init', { root: await sessionRoot($), sessionId: id, requirement: e.args.trim(), allowCommit: true, allowMerge: true, allowCleanup: true })
160    setStatus($, { ...data.summary, sessionId: id })
161    scheduleNext($)
162    return { text: `已创建任务 ${data.summary.name},开始 ${data.summary.stage}。` }
163  })
164
165  on('command.run', { command: 'task-status' }, async ($: any) => {
166    const summary = await refreshStatus($)
167    return { text: formatSummary(summary) }
168  })
169
170  on('command.run', { command: 'task-resume' }, async ($: any) => {
171    const id = await sessionId($)
172    if (active.has(id)) return { text: '任务当前仍在执行中。' }
173    const summary = await runCore($, 'resume', { root: await sessionRoot($), sessionId: id })
174    setStatus($, { ...summary, sessionId: id })
175    scheduleNext($)
176    return { text: `任务已恢复:${summary.name},继续 ${summary.stage}。` }
177  })
178
179  on('command.run', { command: 'task-cleanup' }, async ($: any, e: any) => {
180    const apply = e.args?.trim() === 'apply'
181    if (e.args?.trim() && !apply) return { text: '用法:/task-cleanup [apply];默认只检查,不删除。' }
182    const result = await runCore($, 'cleanup', { root: await sessionRoot($), apply })
183    return { text: result.entries.length ? result.entries.map((entry: any) => `- ${entry.name ?? entry.taskId}: ${entry.reason}`).join('\n') : '没有任务记录。' }
184  })
185
186  on('turn.start', async ($: any, e: any, next: any) => {
187    const attempt = active.get(await sessionId($))
188    if (attempt && !attempt.turnId) attempt.turnId = e.turnId
189    return next(e)
190  })
191
192  on('tool.call', async ($: any, e: any, next: any) => {
193    const attempt = active.get(await sessionId($))
194    if (!attempt) return next(e)
195    const tool = String(e.tool)
196    const cwd = await sessionCwd($)
197    if (attempt.stage === 'implement') {
198      if (tool === 'EnterWorktree') {
199        if (String(e.path ?? '') !== attempt.worktree) return { deny: `EnterWorktree must use the task worktree ${attempt.worktree}.` }
200        return next(e)
201      }
202      if (tool === 'ExitWorktree') return { deny: 'Stay in the task worktree until implementation and its receipt are complete.' }
203      if (writeTool(tool) && (!attempt.worktree || !inside(cwd, attempt.worktree))) return { deny: `Task implementation is locked to ${attempt.worktree}; enter that worktree before writing or running commands.` }
204    }
205    if (attempt.stage === 'integrate' && writeTool(tool)) return { deny: 'Integration is controlled by the task workflow core after ExitWorktree; do not edit or run Git commands in this handoff.' }
206    return next(e)
207  }).catch(async ($: any, e: any, next: any) => {
208    if (next.called) return next(e)
209    return { deny: `task-workflow safety guard failed before ${String(e.tool)} could run: ${next.error?.message ?? 'unknown error'}` }
210  })
211
212  on('turn.step', async function* ($: any, e: any, next: any) {
213    const attempt = active.get(await sessionId($))
214    if (!attempt || e.agentId) return yield* next(e)
215    return yield* next({ ...e, model: runtimeModel(attempt.model) })
216  })
217
218  on('turn.complete', async ($: any, e: any, next: any) => {
219    const id = await sessionId($)
220    const attempt = active.get(id)
221    const result = await next(e)
222    if (!attempt || (attempt.turnId && attempt.turnId !== e.turnId)) return result
223    active.delete(id)
224    if (e.isAborted) {
225      await pauseAfterAbort($, attempt, '用户中断了当前阶段')
226      return result
227    }
228    try {
229      if (attempt.stage === 'integrate') {
230        const summary = await runCore($, 'integrate', { root: await sessionRoot($), cwd: await sessionCwd($), sessionId: id, attemptId: attempt.attemptId, token: attempt.token })
231        setStatus($, { ...summary, sessionId: id })
232        $.ui.toast(`任务 ${summary.name} 已完成并合并。`)
233        return result
234      }
235      const data = await runCore($, 'complete', { root: attempt.root, cwd: await sessionCwd($), sessionId: id, attemptId: attempt.attemptId, token: attempt.token, turnId: e.turnId })
236      setStatus($, { ...data.summary, sessionId: id })
237      scheduleNext($)
238    } catch (error) {
239      const message = error instanceof Error ? error.message : String(error)
240      await pauseAfterAbort($, attempt, message)
241      $.ui.toast(`任务需要恢复:${message}`)
242    }
243    return result
244  })
245
246  on('ui.render', { component: 'AbovePrompt' }, async ($: any, e: any, next: any) => {
247    const text = statusText.get(await sessionId($))
248    if (!text) return next(e)
249    const { Text } = $.ui.resolve(e)
250    return Text({ children: [text], dimColor: true, wrap: 'truncate' })
251  })
252}
253