照测试会话那条链路补一份 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
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import type { HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest'
|
||
import type { IssueColumn } from '../../gitea/types'
|
||
import { LOCAL_STATE_FIELDS } from '../../issues/localState'
|
||
|
||
export function canStartHandoff(issue: {
|
||
source?: 'gitea' | 'youtrack'
|
||
column: IssueColumn
|
||
handoffAttachmentId?: string
|
||
assignees?: string[]
|
||
}, me: string | undefined): boolean {
|
||
const assignees = issue.assignees ?? []
|
||
const mine = assignees.length === 0 || (me !== undefined && assignees.includes(me))
|
||
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId && mine
|
||
}
|
||
|
||
export function canAcceptHandoff(issue: {
|
||
source?: 'gitea' | 'youtrack'
|
||
handoffAttachmentId?: string
|
||
assignees?: string[]
|
||
}, me: string | undefined): boolean {
|
||
return issue.source !== 'youtrack'
|
||
&& !!issue.handoffAttachmentId
|
||
&& me !== undefined
|
||
&& (issue.assignees ?? []).includes(me)
|
||
}
|
||
|
||
/** 移交前 worktree 必须「干净 + 全部 push」,否则接管方从远端重建会丢工作。 */
|
||
export function checkWorktreePushed(input: {
|
||
statusPorcelain: string
|
||
localHead: string
|
||
remoteHead: string
|
||
}): { ok: true } | { ok: false, reason: string } {
|
||
if (input.statusPorcelain.trim().length > 0)
|
||
return { ok: false, reason: '工作区有未提交改动,请先提交并 push' }
|
||
if (input.localHead.trim() !== input.remoteHead.trim())
|
||
return { ok: false, reason: '本地分支与远端不一致,请先 push' }
|
||
return { ok: true }
|
||
}
|
||
|
||
/**
|
||
* 共享墓碑:只写 handoffAttachmentId / handoffFrom 两个共享字段。本机
|
||
* sid/profile/worktreePath 墓碑要等本机清理跑完(成功指派后)才落盘,
|
||
* 见 handoffLocalTombstones —— 否则④/⑤失败回滚时本机字段已经被清空,
|
||
* 用户会话恢复不了。
|
||
*/
|
||
export function handoffStartedStateExtra(attachmentId: number, from: string): Record<string, unknown> {
|
||
return { handoffAttachmentId: String(attachmentId), handoffFrom: from }
|
||
}
|
||
|
||
/**
|
||
* 本机墓碑:移交流程最后一步(本机清理跑完之后)才调用。worktree 删除失败时
|
||
* 保留 worktreePath(keepWorktreePath),别把用户还留在磁盘上的 worktree
|
||
* 从 state 里抹掉。
|
||
*/
|
||
export function handoffLocalTombstones(opts: { keepWorktreePath: boolean }): Record<string, string> {
|
||
const extra: Record<string, string> = {}
|
||
for (const f of LOCAL_STATE_FIELDS) {
|
||
if (f === 'worktreePath' && opts.keepWorktreePath)
|
||
continue
|
||
extra[f] = ''
|
||
}
|
||
return extra
|
||
}
|
||
|
||
export function handoffStartedUiPatch(attachmentId: number, from: string, to: string, opts: { keepWorktreePath: boolean }): Record<string, unknown> {
|
||
return {
|
||
handoffAttachmentId: String(attachmentId),
|
||
handoffFrom: from,
|
||
assignees: [to],
|
||
sessionId: null,
|
||
implementSessionId: null,
|
||
reviewSessionId: null,
|
||
testSessionId: null,
|
||
prototypeSessionId: null,
|
||
profilePath: null,
|
||
brainstormProfilePath: null,
|
||
testProfilePath: null,
|
||
prototypeProfilePath: null,
|
||
prDiffFile: null,
|
||
// worktree 没删掉时(opts.keepWorktreePath)不碰这两个字段,让 UI 继续显示孤儿 worktree。
|
||
...(opts.keepWorktreePath ? {} : { worktreePath: null, worktreeExists: false }),
|
||
}
|
||
}
|
||
|
||
function definedEntries(o: Record<string, any>): Record<string, string> {
|
||
const out: Record<string, string> = {}
|
||
for (const [k, v] of Object.entries(o)) {
|
||
if (v)
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
export function handoffAcceptedStateExtra(local: {
|
||
sessions: HandoffSessions
|
||
profiles: HandoffProfiles
|
||
worktreePath?: string
|
||
}): Record<string, unknown> {
|
||
return {
|
||
handoffAttachmentId: '',
|
||
handoffFrom: '',
|
||
...definedEntries(local.sessions),
|
||
...definedEntries(local.profiles),
|
||
...(local.worktreePath ? { worktreePath: local.worktreePath } : {}),
|
||
}
|
||
}
|
||
|
||
export function handoffAcceptedUiPatch(local: {
|
||
sessions: HandoffSessions
|
||
profiles: HandoffProfiles
|
||
worktreePath?: string
|
||
worktreeExists: boolean
|
||
reviewSessionFileExists?: boolean
|
||
}): Record<string, unknown> {
|
||
return {
|
||
handoffAttachmentId: null,
|
||
handoffFrom: null,
|
||
...definedEntries(local.sessions),
|
||
...definedEntries(local.profiles),
|
||
// 显式 null 而非省略:省略会让 webview 侧留着接管前的旧 worktreePath,
|
||
// 与 worktreeExists: false 对不上。
|
||
worktreePath: local.worktreePath ?? null,
|
||
worktreeExists: local.worktreeExists,
|
||
...(local.reviewSessionFileExists !== undefined ? { reviewSessionFileExists: local.reviewSessionFileExists } : {}),
|
||
}
|
||
}
|