name: clean description: 智能代码清理,具备主线检测、陈旧工件发现和安全执行功能。支持定向清理和确认。 argument-hint: “[–dry-run] [–focus=<area>]”
工作流清理命令
概述
基于证据的智能清理命令。通过主线分析系统性地识别陈旧工件,发现漂移,并安全移除未使用的会话、文档和死代码。
核心工作流:检测主线 → 发现漂移 → 确认 → 暂存 → 执行
目标清理
焦点区域:$FOCUS(如果未指定,则为整个项目) 模式:$ARGUMENTS
--dry-run:预览清理而不执行--focus:焦点区域(模块或路径)
执行流程
阶段 0:初始化
├─ 解析参数(--dry-run, FOCUS)
├─ 设置会话文件夹
└─ 初始化工具函数
阶段 1:主线检测
├─ 分析 git 历史(30 天)
├─ 识别核心模块(高提交频率)
├─ 映射活跃与陈旧分支
└─ 构建主线配置文件
阶段 2:漂移发现(子代理)
├─ 使用 cli-explore-agent 角色启动代理
├─ 扫描工作流会话中的孤立工件
├─ 识别偏离主线的文档
├─ 检测死代码和未使用的导出
└─ 生成清理清单
阶段 3:确认
├─ 验证清单模式
├─ 按类别显示清理摘要
├─ ASK_USER:选择类别和风险级别
└─ 如果是 --dry-run,则退出
阶段 4:执行
├─ 验证路径(安全检查)
├─ 暂存删除(移动到 .trash)
├─ 更新清单
├─ 永久删除
└─ 报告结果
实现
阶段 0:初始化
步骤 0:确定项目根目录
检测项目根目录,确保 .workflow/ 产物位置正确:
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
优先通过 git 获取仓库根目录;非 git 项目回退到 pwd 取当前绝对路径。
存储为 {projectRoot},后续所有 .workflow/ 路径必须以此为前缀。
const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()
// 解析参数
const args = "$ARGUMENTS"
const isDryRun = args.includes('--dry-run')
const focusMatch = args.match(/FOCUS="([^"]+)"/)
const focusArea = focusMatch ? focusMatch[1] : "$FOCUS" !== "$" + "FOCUS" ? "$FOCUS" : null
// 会话设置
const dateStr = getUtc8ISOString().substring(0, 10)
const sessionId = `clean-${dateStr}`
const sessionFolder = `${projectRoot}/.workflow/.clean/${sessionId}`
const trashFolder = `${sessionFolder}/.trash`
const projectRoot = bash('git rev-parse --show-toplevel 2>/dev/null || pwd').trim()
bash(`mkdir -p ${sessionFolder}`)
bash(`mkdir -p ${trashFolder}`)
// 工具函数
function fileExists(p) {
try { return bash(`test -f "${p}" && echo "yes"`).includes('yes') } catch { return false }
}
function dirExists(p) {
try { return bash(`test -d "${p}" && echo "yes"`).includes('yes') } catch { return false }
}
function validatePath(targetPath) {
if (targetPath.includes('..')) return { valid: false, reason: '路径遍历' }
const allowed = ['.workflow/', '.claude/rules/tech/', 'src/']
const dangerous = [/^\//, /^C:\\Windows/i, /node_modules/, /\.git$/]
if (!allowed.some(p => targetPath.startsWith(p))) {
return { valid: false, reason: '超出允许目录' }
}
if (dangerous.some(p => p.test(targetPath))) {
return { valid: false, reason: '危险模式' }
}
return { valid: true }
}
阶段 1:主线检测
// 检查 git 仓库
const isGitRepo = bash('git rev-parse --git-dir 2>/dev/null && echo "yes"').includes('yes')
let mainlineProfile = {
coreModules: [],
activeFiles: [],
activeBranches: [],
staleThreshold: { sessions: 7, branches: 30, documents: 14 },
isGitRepo,
timestamp: getUtc8ISOString()
}
if (isGitRepo) {
// 按目录的提交频率(最近 30 天)
const freq = bash('git log --since="30 days ago" --name-only --pretty=format: | grep -v "^$" | cut -d/ -f1-2 | sort | uniq -c | sort -rn | head -20')
// 解析核心模块(>5 次提交)
mainlineProfile.coreModules = freq.trim().split('
')
.map(l => l.trim().match(/^(\d+)\s+(.+)$/))
.filter(m => m && parseInt(m[1]) >= 5)
.map(m => m[2])
// 最近分支
const branches = bash('git for-each-ref --sort=-committerdate refs/heads/ --format="%(refname:short)" | head -10')
mainlineProfile.activeBranches = branches.trim().split('
').filter(Boolean)
}
Write(`${sessionFolder}/mainline-profile.json`, JSON.stringify(mainlineProfile, null, 2))
阶段 2:漂移发现
let exploreAgent = null
try {
// 启动 cli-explore-agent
exploreAgent = spawn_agent({
message: `
## 任务分配
### 强制初始步骤
1. 阅读:~/.codex/agents/cli-explore-agent.md
2. 阅读:${projectRoot}/.workflow/project-tech.json(如果存在)
## 任务目标
发现陈旧工件以进行清理。
## 上下文
- 会话:${sessionFolder}
- 焦点:${focusArea || '整个项目'}
## 发现类别
### 1. 陈旧工作流会话
扫描:${projectRoot}/.workflow/active/WFS-*, ${projectRoot}/.workflow/archives/WFS-*, ${projectRoot}/.workflow/.lite-plan/*, ${projectRoot}/.workflow/.debug/DBG-*
标准:无修改 >7 天 + 无相关 git 提交
### 2. 漂移文档
扫描:.claude/rules/tech/*, ${projectRoot}/.workflow/.scratchpad/*
标准:>30% 破损引用指向不存在的文件
### 3. 死代码
扫描:未使用的导出、孤立文件(未被任何地方导入)
标准:导入图中无导入者
## 输出
写入:${sessionFolder}/cleanup-manifest.json
格式:
{
"generated_at": "ISO",
"discoveries": {
"stale_sessions": [{ "path": "...", "age_days": N, "reason": "...", "risk": "low|medium|high" }],
"drifted_documents": [{ "path": "...", "drift_percentage": N, "reason": "...", "risk": "..." }],
"dead_code": [{ "path": "...", "type": "orphan_file", "reason": "...", "risk": "..." }]
},
"summary": { "total_items": N, "by_category": {...}, "by_risk": {...} }
}
`
})
// 等待并处理超时
let result = wait({ ids: [exploreAgent], timeout_ms: 600000 })
if (result.timed_out) {
send_input({ id: exploreAgent, message: '现在完成并写入 cleanup-manifest.json。' })
result = wait({ ids: [exploreAgent], timeout_ms: 300000 })
if (result.timed_out) throw new Error('代理超时')
}
if (!fileExists(`${sessionFolder}/cleanup-manifest.json`)) {
throw new Error('清单未生成')
}
} finally {
if (exploreAgent) close_agent({ id: exploreAgent })
}
阶段 3:确认
// 加载并验证清单
const manifest = JSON.parse(Read(`${sessionFolder}/cleanup-manifest.json`))
// 显示摘要
console.log(`
## 清理发现报告
| 类别 | 数量 | 风险 |
|----------|-------|------|
| 会话 | ${manifest.summary.by_category.stale_sessions} | ${getRiskSummary('sessions')} |
| 文档 | ${manifest.summary.by_category.drifted_documents} | ${getRiskSummary('documents')} |
| 死代码 | ${manifest.summary.by_category.dead_code} | ${getRiskSummary('code')} |
**总计**:${manifest.summary.total_items} 个项目
`)
// 干运行退出
if (isDryRun) {
console.log(`
**干运行模式**:未进行任何更改。
清单:${sessionFolder}/cleanup-manifest.json
`)
return
}
// 用户确认
const selection = ASK_USER([
{
id: "categories", type: "multi-select",
prompt: "清理哪些类别?",
options: [
{ label: "会话", description: `${manifest.summary.by_category.stale_sessions} 个陈旧会话` },
{ label: "文档", description: `${manifest.summary.by_category.drifted_documents} 个漂移文档` },
{ label: "死代码", description: `${manifest.summary.by_category.dead_code} 个未使用文件` }
]
},
{
id: "risk", type: "select",
prompt: "风险级别?",
options: [
{ label: "仅低风险", description: "最安全(推荐)" },
{ label: "低 + 中风险", description: "包括可能未使用的" },
{ label: "全部", description: "激进" }
]
}
]) // 阻塞(等待用户响应)
阶段 4:执行
const riskFilter = {
'仅低风险': ['low'],
'低 + 中风险': ['low', 'medium'],
'全部': ['low', 'medium', 'high']
}[selection.risk]
// 收集要清理的项目
const items = []
if (selection.categories.includes('会话')) {
items.push(...manifest.discoveries.stale_sessions.filter(s => riskFilter.includes(s.risk)))
}
if (selection.categories.includes('文档')) {
items.push(...manifest.discoveries.drifted_documents.filter(d => riskFilter.includes(d.risk)))
}
if (selection.categories.includes('死代码')) {
items.push(...manifest.discoveries.dead_code.filter(c => riskFilter.includes(c.risk)))
}
const results = { staged: [], deleted: [], failed: [], skipped: [] }
// 验证并暂存
for (const item of items) {
const validation = validatePath(item.path)
if (!validation.valid) {
results.skipped.push({ path: item.path, reason: validation.reason })
continue
}
if (!fileExists(item.path) && !dirExists(item.path)) {
results.skipped.push({ path: item.path, reason: '未找到' })
continue
}
try {
const trashTarget = `${trashFolder}/${item.path.replace(/\//g, '_')}`
bash(`mv "${item.path}" "${trashTarget}"`)
results.staged.push({ path: item.path, trashPath: trashTarget })
} catch (e) {
results.failed.push({ path: item.path, error: e.message })
}
}
// 永久删除
for (const staged of results.staged) {
try {
bash(`rm -rf "${staged.trashPath}"`)
results.deleted.push(staged.path)
} catch (e) {
console.error(`失败:${staged.path}`)
}
}
// 清理空垃圾文件夹
bash(`rmdir "${trashFolder}" 2>/dev/null || true`)
// 报告
console.log(`
## 清理完成
**已删除**:${results.deleted.length}
**失败**:${results.failed.length}
**跳过**:${results.skipped.length}
### 已删除
${results.deleted.map(p => `- ${p}`).join('
') || '(无)'}
${results.failed.length > 0 ? `### 失败
${results.failed.map(f => `- ${f.path}: ${f.error}`).join('
')}` : ''}
报告:${sessionFolder}/cleanup-report.json
`)
Write(`${sessionFolder}/cleanup-report.json`, JSON.stringify({
timestamp: getUtc8ISOString(),
results,
summary: {
deleted: results.deleted.length,
failed: results.failed.length,
skipped: results.skipped.length
}
}, null, 2))
会话文件夹
{projectRoot}/.workflow/.clean/clean-{YYYY-MM-DD}/
├── mainline-profile.json # Git 历史分析
├── cleanup-manifest.json # 发现结果
├── cleanup-report.json # 执行结果
└── .trash/ # 暂存区域(临时)
风险级别
| 风险 | 描述 | 示例 |
|---|---|---|
| 低 | 安全删除 | 空会话、草稿文件 |
| 中 | 可能未使用 | 孤立文件、旧存档 |
| 高 | 可能有依赖项 | 有导入的文件 |
安全功能
| 功能 | 保护 |
|---|---|
| 路径验证 | 白名单目录,拒绝遍历 |
| 暂存删除 | 在永久删除前移动到 .trash |
| 危险模式 | 阻止系统目录、node_modules、.git |
迭代流程
首次调用 (/prompts:clean):
├─ 从 git 历史检测主线
├─ 通过子代理发现陈旧工件
├─ 显示摘要,等待用户选择
└─ 使用暂存执行清理
干运行 (/prompts:clean --dry-run):
├─ 除执行外的所有阶段
└─ 保存清单供审查
定向 (/prompts:clean FOCUS="auth"):
└─ 发现限于指定区域
错误处理
| 情况 | 操作 |
|---|---|
| 无 git 仓库 | 仅使用文件时间戳 |
| 代理超时 | 重试一次并提示,然后中止 |
| 路径验证失败 | 跳过项目,报告原因 |
| 清单解析错误 | 中止并报错 |
| 空发现 | 报告“代码库已清理” |
现在执行清理工作流,焦点:$FOCUS