关于 clean
clean 是一条工作流命令,通过分析仓库活动和发现偏移来识别过期项目产物。它先展示结果供用户选择,支持干运行预览,并在删除前暂存所选内容。
主要功能
- 01基于 Git 历史识别主线
- 02发现过期会话、文档和代码
- 03按风险级别分类清理项
- 04支持干运行预览
- 05删除前暂存至回收区
- 06路径校验与危险目录保护
适用场景
- 01清理长期未使用的工作流会话
- 02查找并处理孤立文档或归档
- 03检查特定模块中的过期文件
- 04执行删除前预览清理清单
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ catlog22/ claude-code-workflow --skill clean安装后在 Claude Code 中输入/clean确认已加载。
详细介绍
工作流清理命令
概述
基于证据的智能清理命令。通过主线分析系统地识别过期产物,发现偏离,并安全地移除未使用的会话、文档和无效代码。
核心工作流:检测主线 → 发现偏离 → 确认 → 暂存 → 执行
清理目标
关注范围:$FOCUS(如果未指定,则为整个项目) 模式:$ARGUMENTS
--dry-run:预览清理操作,不执行--focus:关注范围(模块或路径)
执行流程
Phase 0: Initialization
├─ Parse arguments (--dry-run, FOCUS)
├─ Setup session folder
└─ Initialize utility functions
Phase 1: Mainline Detection
├─ Analyze git history (30 days)
├─ Identify core modules (high commit frequency)
├─ Map active vs stale branches
└─ Build mainline profile
Phase 2: Drift Discovery (Subagent)
├─ spawn_agent with cli-explore-agent role
├─ Scan workflow sessions for orphaned artifacts
├─ Identify documents drifted from mainline
├─ Detect dead code and unused exports
└─ Generate cleanup manifest
Phase 3: Confirmation
├─ Validate manifest schema
├─ Display cleanup summary by category
├─ request_user_input: Select categories and risk level
└─ Dry-run exit if --dry-run
Phase 4: Execution
├─ Validate paths (security check)
├─ Stage deletion (move to .trash)
├─ Update manifests
├─ Permanent deletion
└─ Report results
实现
阶段 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()
// Parse arguments
const args = "$ARGUMENTS"
const isDryRun = args.includes('--dry-run')
const focusMatch = args.match(/FOCUS="([^"]+)"/)
const focusArea = focusMatch ? focusMatch[1] : "$FOCUS" !== "$" + "FOCUS" ? "$FOCUS" : null
// Session setup
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}`)
// Utility functions
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: 'Path traversal' }
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: 'Outside allowed directories' }
}
if (dangerous.some(p => p.test(targetPath))) {
return { valid: false, reason: 'Dangerous pattern' }
}
return { valid: true }
}
阶段 1:主线检测
// Check git repository
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) {
// Commit frequency by directory (last 30 days)
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')
// Parse core modules (>5 commits)
mainlineProfile.coreModules = freq.trim().split('\n')
.map(l => l.trim().match(/^(\d+)\s+(.+)$/))
.filter(m => m && parseInt(m[1]) >= 5)
.map(m => m[2])
// Recent branches
const branches = bash('git for-each-ref --sort=-committerdate refs/heads/ --format="%(refname:short)" | head -10')
mainlineProfile.activeBranches = branches.trim().split('\n').filter(Boolean)
}
Write(`${sessionFolder}/mainline-profile.json`, JSON.stringify(mainlineProfile, null, 2))
阶段 2:偏离发现
let exploreAgent = null
try {
// Launch cli-explore-agent
exploreAgent = spawn_agent({
agent_type: "cli_explore_agent",
message: `
## TASK ASSIGNMENT
### MANDATORY FIRST STEPS
1. Read: ${projectRoot}/.workflow/project-tech.json (if exists)
## Task Objective
Discover stale artifacts for cleanup.
## Context
- Session: ${sessionFolder}
- Focus: ${focusArea || 'entire project'}
## Discovery Categories
### 1. Stale Workflow Sessions
Scan: ${projectRoot}/.workflow/active/WFS-*, ${projectRoot}/.workflow/archives/WFS-*, ${projectRoot}/.workflow/.lite-plan/*, ${projectRoot}/.workflow/.debug/DBG-*
Criteria: No modification >7 days + no related git commits
### 2. Drifted Documents
Scan: .claude/rules/tech/*, ${projectRoot}/.workflow/.scratchpad/*
Criteria: >30% broken references to non-existent files
### 3. Dead Code
Scan: Unused exports, orphan files (not imported anywhere)
Criteria: No importers in import graph
## Output
Write to: ${sessionFolder}/cleanup-manifest.json
Format:
{
"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": {...} }
}
`
})
// Wait with timeout handling (4-step cascade)
let result = wait_agent({ timeout_ms: 1800000 })
if (result.timed_out) {
// Status probe
followup_task({ target: exploreAgent, message: "STATUS_CHECK: Report current progress, findings so far, and estimated remaining work." })
const status = wait_agent({ timeout_ms: 180000 }) // 3 min
if (status.timed_out) {
// Force finalize
followup_task({ target: exploreAgent, message: "FINALIZE: Output all current findings immediately. Time limit reached.", interrupt: true })
const forced = wait_agent({ timeout_ms: 180000 }) // 3 min
if (forced.timed_out) {
close_agent({ target: exploreAgent })
throw new Error('Agent timeout')
}
}
}
if (!fileExists(`${sessionFolder}/cleanup-manifest.json`)) {
throw new Error('Manifest not generated')
}
} finally {
if (exploreAgent) close_agent({ target: exploreAgent })
}
阶段 3:确认
// Load and validate manifest
const manifest = JSON.parse(Read(`${sessionFolder}/cleanup-manifest.json`))
// Display summary
console.log(`
## Cleanup Discovery Report
| Category | Count | Risk |
|----------|-------|------|
| Sessions | ${manifest.summary.by_category.stale_sessions} | ${getRiskSummary('sessions')} |
| Documents | ${manifest.summary.by_category.drifted_documents} | ${getRiskSummary('documents')} |
| Dead Code | ${manifest.summary.by_category.dead_code} | ${getRiskSummary('code')} |
**Total**: ${manifest.summary.total_items} items
`)
// Dry-run exit
if (isDryRun) {
console.log(`
**Dry-run mode**: No changes made.
Manifest: ${sessionFolder}/cleanup-manifest.json
`)
return
}
// User confirmation
const selection = functions.request_user_input({
questions: [
{
header: "清理类别",
id: "categories",
question: "Which categories to clean?",
options: [
{ label: "Sessions", description: `${manifest.summary.by_category.stale_sessions} stale sessions` },
{ label: "Documents", description: `${manifest.summary.by_category.drifted_documents} drifted docs` },
{ label: "Dead Code", description: `${manifest.summary.by_category.dead_code} unused files` }
]
},
{
header: "风险级别",
id: "risk",
question: "Risk level?",
options: [
{ label: "Low only(Recommended)", description: "Safest — only clearly stale items" },
{ label: "Low + Medium", description: "Includes likely unused" },
{ label: "All", description: "Aggressive" }
]
}
]
}) // BLOCKS (wait for user response)
阶段 4:执行
const selectedCategory = selection.answers.categories.answers[0]
const selectedRisk = selection.answers.risk.answers[0]
const riskFilter = {
'Low only(Recommended)': ['low'],
'Low + Medium': ['low', 'medium'],
'All': ['low', 'medium', 'high']
}[selectedRisk]
// Collect items to clean
const items = []
if (selectedCategory === 'Sessions') {
items.push(...manifest.discoveries.stale_sessions.filter(s => riskFilter.includes(s.risk)))
}
if (selectedCategory === 'Documents') {
items.push(...manifest.discoveries.drifted_documents.filter(d => riskFilter.includes(d.risk)))
}
if (selectedCategory === 'Dead Code') {
items.push(...manifest.discoveries.dead_code.filter(c => riskFilter.includes(c.risk)))
}
const results = { staged: [], deleted: [], failed: [], skipped: [] }
// Validate and stage
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: 'Not found' })
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 })
}
}
// Permanent deletion
for (const staged of results.staged) {
try {
bash(`rm -rf "${staged.trashPath}"`)
results.deleted.push(staged.path)
} catch (e) {
console.error(`Failed: ${staged.path}`)
}
}
// Cleanup empty trash
bash(`rmdir "${trashFolder}" 2>/dev/null || true`)
// Report
console.log(`
## Cleanup Complete
**Deleted**: ${results.deleted.length}
**Failed**: ${results.failed.length}
**Skipped**: ${results.skipped.length}
### Deleted
${results.deleted.map(p => `- ${p}`).join('\n') || '(none)'}
${results.failed.length > 0 ? `### Failed\n${results.failed.map(f => `- ${f.path}: ${f.error}`).join('\n')}` : ''}
Report: ${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 history analysis
├── cleanup-manifest.json # Discovery results
├── cleanup-report.json # Execution results
└── .trash/ # Staging area (temporary)
风险等级
| 风险 | 说明 | 示例 |
|---|---|---|
| 低 | 可安全删除 | 空会话、临时文件 |
| 中 | 可能未使用 | 孤立文件、旧归档 |
| 高 | 可能存在依赖 | 有一些导入项的文件 |
安全功能
| 功能 | 防护措施 |
|---|---|
| 路径验证 | 使用目录白名单,拒绝路径穿越 |
| 暂存删除 | 永久删除前先移至 .trash |
| 危险模式 | 禁止操作系统目录、node_modules、.git |
迭代流程
First Call (/prompts:clean):
├─ Detect mainline from git history
├─ Discover stale artifacts via subagent
├─ Display summary, await user selection
└─ Execute cleanup with staging
Dry-Run (/prompts:clean --dry-run):
├─ All phases except execution
└─ Manifest saved for review
Focused (/prompts:clean FOCUS="auth"):
└─ Discovery limited to specified area
错误处理
| 情况 | 操作 |
|---|---|
| 没有 git 仓库 | 仅使用文件时间戳 |
| Agent 超时 | 使用提示词重试一次,然后中止 |
| 路径验证失败 | 跳过项目并报告原因 |
| 清单解析错误 | 报错并中止 |
| 未发现内容 | 报告“代码库干净” |
现在执行清理工作流,关注范围:$FOCUS
评论
支持 Markdown