feat(vscode): worktree 改建到 ~/Sources/worktree/<项目>/<slug>,接线 superpowers.worktreeDirectory 配置

实施流程不再硬编码 .claude/worktrees/<hash>:
- worktree 目录名改用 spec/plan 的 slug(specs|plans 后那一级目录名),分支改为 feature/<slug>
- 目录走 superpowers.worktreeDirectory 模板(package.json 新注册配置),支持 $project_root/$project_name/$feature_name 占位符及开头 ~ 展开,默认 ~/Sources/worktree/$project_name/$feature_name
- worktree 现位于工作区外:创建前 mkdir -p 父目录,state/webview 改存绝对路径(下游消费方已兼容 isAbsolute)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 02:37:16 +08:00
co-authored by Claude Opus 4.7
parent 495e961e53
commit 11ea6b345c
2 changed files with 65 additions and 7 deletions
+10
View File
@@ -93,6 +93,16 @@
"group": "navigation"
}
]
},
"configuration": {
"title": "Superpowers",
"properties": {
"superpowers.worktreeDirectory": {
"type": "string",
"default": "~/Sources/worktree/$project_name/$feature_name",
"markdownDescription": "运行 Plan 时创建 worktree 的目录模板。支持占位符 `$project_root`(工作区根目录的绝对路径)、`$project_name`(工作区目录名)、`$feature_name`spec/plan 的 slug)。开头的 `~` 会展开为当前用户主目录。默认会把 worktree 建到 `~/Sources/worktree/<项目名>/<slug>`。"
}
}
}
},
"scripts": {
+55 -7
View File
@@ -558,6 +558,48 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
return true
}
/**
* Derive a human-readable slug from a spec/plan file path.
*
* spec/plan live as directories on disk
* (docs/superpowers/{specs,plans}/<slug>/{spec,plan}.md), so the slug is the
* path segment right after the `specs`/`plans` segment — that's the name the
* user sees in the tree. Falls back to the parent dir name, then to a stable
* 8-char sha256 of the path so the worktree dir name is never empty. Paths
* come from gitea body comments and use `/` separators.
*/
function deriveSlug(planFile: string): string {
const segments = planFile.split('/').filter(Boolean)
const anchor = segments.findIndex(s => s === 'plans' || s === 'specs')
if (anchor >= 0 && segments[anchor + 1]) {
return segments[anchor + 1]
}
const parent = path.basename(path.dirname(planFile))
if (parent && parent !== '.' && parent !== '/') {
return parent
}
return createHash('sha256').update(planFile).digest('hex').slice(0, 8)
}
/**
* Expand the `superpowers.worktreeDirectory` template into an ABSOLUTE path.
* Placeholders: $project_root (workspace abs path), $project_name (its
* basename), $feature_name (the slug). A leading `~` is expanded here because
* git/execFile run without a shell and won't do tilde expansion themselves.
* The result is resolved to absolute since the worktree now lives outside the
* workspace and is stored/compared as an absolute path downstream.
*/
function resolveWorktreeDir(template: string, workspaceRoot: string, slug: string): string {
let p = template
.split('$project_name').join(path.basename(workspaceRoot))
.split('$project_root').join(workspaceRoot)
.split('$feature_name').join(slug)
if (p.startsWith('~')) {
p = path.join(os.homedir(), p.slice(1))
}
return path.resolve(p)
}
/**
* Kick off the end-to-end "implement this plan" flow:
* 1. Detect repo + token, compute a stable feature_name from the plan path.
@@ -639,10 +681,13 @@ export async function handleImplement(
return
}
const feature = createHash('sha256').update(planFile).digest('hex').slice(0, 8)
const branch = `feature/${feature}`
const relativeWorktreePath = `.claude/worktrees/${feature}`
const worktreePath = path.join(workspaceRoot, relativeWorktreePath)
// worktree 目录名用 spec/plan 的 slug(可读、与 spec 目录同名),不再用 planFile 的 hash。
const slug = deriveSlug(planFile)
const branch = `feature/${slug}`
// 目录走 superpowers.worktreeDirectory 模板(设置里可改),默认 ~/Sources/worktree/<项目>/<slug>。
const worktreeTemplate = workspace.getConfiguration('superpowers').get<string>('worktreeDirectory')
|| '~/Sources/worktree/$project_name/$feature_name'
const worktreePath = resolveWorktreeDir(worktreeTemplate, workspaceRoot, slug)
// Pre-flight: refuse if either the directory or the branch already
// exists, since `git worktree add -b` would fail and we'd have to
@@ -677,7 +722,7 @@ export async function handleImplement(
}
if (worktreeExists || branchExists) {
void window.showErrorMessage(
`feature ${feature} 的 worktree 或分支已存在,请先清理`,
`feature ${slug} 的 worktree 或分支已存在,请先清理`,
)
return
}
@@ -713,6 +758,9 @@ export async function handleImplement(
// Create the worktree before anything else, so a worktree-add failure
// doesn't leave half-written state behind.
try {
// worktree 现在建在工作区外(~/Sources/worktree/<项目>/...),父目录可能不存在;
// git worktree add 不会创建多层父目录,先补齐。
await fsp.mkdir(path.dirname(worktreePath), { recursive: true })
await createWorktree({ workspaceRoot, worktreePath, branch })
}
catch (err) {
@@ -759,7 +807,7 @@ export async function handleImplement(
await panel.mergeIssueState(issueNumber, {
column: 'in-progress',
branch,
worktreePath: relativeWorktreePath,
worktreePath,
implementStatus: 'running',
})
panel.postMessage({
@@ -768,7 +816,7 @@ export async function handleImplement(
patch: {
column: 'in-progress',
branch,
worktreePath: relativeWorktreePath,
worktreePath,
implementStatus: 'running',
worktreeExists: true,
},