Files
superwork/vscode/src/cc/handoffManifest.ts
T
cruldra b62e58d739 ✨ feat(vscode): 新增第五种工单会话「原型会话」
照测试会话那条链路补一份 prototype:

- 消息与路由:session/start-prototype、session/resume-prototype、
  issue/update-prototype-profile-path,都进只读门禁
- 会话:handleStartPrototypeSession / handleResumePrototypeSession,
  终端名 issue-N-原型;cwd 有 worktree 用 worktree、没有退主仓库根
  (原 resolveTestSessionCwd 改名 resolveWorktreeOrRootCwd 两种会话共用)
- 不要求有 PR,任何阶段都能起;前置工单锁仍然拦
- 提示词:新增 prompts/prototype.md,设置里可改,占位符 {issueNumber}
- profile:工单级 prototypeProfilePath > 设置里的原型默认 profile >
  内置默认,与其它会话互不回退
- 状态字段 prototypeSessionId / prototypeProfilePath / prototypeTabOpen
  贯通 gitea、youtrack、本机 state 与移交包
- 详情面板加「原型会话id」「原型配置文件」两行,设置面板加对应两项

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-14 10:20:30 +08:00

110 lines
4.0 KiB
TypeScript

export type ClaudeSessionKind = 'brainstorm' | 'implement' | 'test' | 'prototype'
export const CLAUDE_SESSION_FIELDS = [
{ field: 'sessionId', kind: 'brainstorm' },
{ field: 'implementSessionId', kind: 'implement' },
{ field: 'testSessionId', kind: 'test' },
{ field: 'prototypeSessionId', kind: 'prototype' },
] as const satisfies ReadonlyArray<{ field: 'sessionId' | 'implementSessionId' | 'testSessionId' | 'prototypeSessionId', kind: ClaudeSessionKind }>
export interface HandoffSessions {
sessionId?: string
implementSessionId?: string
testSessionId?: string
prototypeSessionId?: string
reviewSessionId?: string
}
export interface HandoffProfiles {
profilePath?: string
brainstormProfilePath?: string
testProfilePath?: string
prototypeProfilePath?: string
}
export interface HandoffManifest {
version: 1
issue: number
from: string
createdAt: string
branch?: string
worktreePath?: string
sessions: HandoffSessions
profiles: HandoffProfiles
claude: Array<{ id: string, kind: ClaudeSessionKind }>
codex: Array<{ id: string, relPath: string }>
}
export const HANDOFF_MANIFEST_FILE = 'handoff.json'
export function handoffAttachmentName(issueNumber: number): string {
return `spx-handoff-issue-${issueNumber}.tgz`
}
const KINDS: ReadonlySet<string> = new Set<ClaudeSessionKind>(['brainstorm', 'implement', 'test', 'prototype'])
// Claude / Codex 会话 id 都是 UUID;顺手挡掉路径穿越(如 `../x`)之类的伪造条目。
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
function optString(v: unknown): string | undefined {
return typeof v === 'string' && v.length > 0 ? v : undefined
}
export function parseHandoffManifest(json: string, expectedIssue: number): HandoffManifest {
const raw = JSON.parse(json) as Record<string, unknown>
if (!raw || typeof raw !== 'object')
throw new Error('handoff.json 不是对象')
if (raw.version !== 1)
throw new Error(`handoff.json version 不支持:${String(raw.version)}`)
if (raw.issue !== expectedIssue)
throw new Error(`handoff.json 属于工单 #${String(raw.issue)},不是 #${expectedIssue}`)
const from = optString(raw.from)
const createdAt = optString(raw.createdAt)
if (!from || !createdAt)
throw new Error('handoff.json 缺少 from / createdAt')
const sessionsRaw = (raw.sessions ?? {}) as Record<string, unknown>
const profilesRaw = (raw.profiles ?? {}) as Record<string, unknown>
const claudeRaw = Array.isArray(raw.claude) ? raw.claude : []
const codexRaw = Array.isArray(raw.codex) ? raw.codex : []
const claude = claudeRaw.map((e) => {
const o = e as Record<string, unknown>
const id = optString(o.id)
const kind = optString(o.kind)
if (!id || !kind || !KINDS.has(kind) || !UUID_RE.test(id))
throw new Error(`handoff.json claude 条目非法:${JSON.stringify(e)}`)
return { id, kind: kind as ClaudeSessionKind }
})
const codex = codexRaw.map((e) => {
const o = e as Record<string, unknown>
const id = optString(o.id)
const relPath = optString(o.relPath)
if (!id || !relPath || relPath.startsWith('/') || relPath.includes('..') || !UUID_RE.test(id))
throw new Error(`handoff.json codex 条目非法:${JSON.stringify(e)}`)
return { id, relPath }
})
const sessions: HandoffSessions = {}
for (const k of ['sessionId', 'implementSessionId', 'testSessionId', 'prototypeSessionId', 'reviewSessionId'] as const) {
const v = optString(sessionsRaw[k])
if (v)
sessions[k] = v
}
const profiles: HandoffProfiles = {}
for (const k of ['profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath'] as const) {
const v = optString(profilesRaw[k])
if (v)
profiles[k] = v
}
return {
version: 1,
issue: expectedIssue,
from,
createdAt,
...(optString(raw.branch) ? { branch: optString(raw.branch) } : {}),
...(optString(raw.worktreePath) ? { worktreePath: optString(raw.worktreePath) } : {}),
sessions,
profiles,
claude,
codex,
}
}