diff --git a/docs/superpowers/specs/issue-handoff/spec.md b/docs/superpowers/specs/issue-handoff/spec.md index 3b5342f..2da185c 100644 --- a/docs/superpowers/specs/issue-handoff/spec.md +++ b/docs/superpowers/specs/issue-handoff/spec.md @@ -51,16 +51,14 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有) "sessions": { "sessionId": "…", "implementSessionId": "…", "testSessionId": "…", "reviewSessionId": "…" }, - "profiles": { - "profilePath": "/Users/chw/.claude/profiles/x.json", - "brainstormProfilePath": "…", "testProfilePath": "…" - }, "claude": [ { "id": "", "kind": "brainstorm" | "implement" | "test" } ], "codex": [ { "id": "", "relPath": "2026/08/26/rollout-….jsonl" } ] } ``` -`sessions` / `profiles` 只放存在的键;`claude` / `codex` 只列实际打进包里的文件。profile 路径原样带发送方绝对路径,接管方沿用现有 `resolveProfilePath` 按文件名重映射。 +`sessions` 只放存在的键;`claude` / `codex` 只列实际打进包里的文件。 + +profile(`profilePath` / `brainstormProfilePath` / `testProfilePath` / `prototypeProfilePath`,各类会话用哪份模型配置)是每台机器自己的选择,不进会话包:发送方不打包,接管方不写入,旧包里残留的 `profiles` 解析时直接忽略。接管方的工单没选过 profile 就用自己设置里的默认值。 ## 共享 state JSON 新字段 @@ -89,15 +87,15 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有) 扩展端 `handleHandoffStart(panel, issueNumber, to)` 顺序: -1. 前置:移交存储已配置;workspaceRoot / `detectRepo` / token / `loginForToken` 得 `me`。读 `readIssueState`,取本机字段(四个 sid、三个 profile、`worktreePath`)与共享 `branch`。 +1. 前置:移交存储已配置;workspaceRoot / `detectRepo` / token / `loginForToken` 得 `me`。读 `readIssueState`,取本机的会话 id、`worktreePath` 与共享 `branch`(profile 不取)。 2. **worktree 守卫**(有 `worktreePath` 且目录存在时):`git status --porcelain` 必须为空;`git fetch origin ` 后 `HEAD` 必须等于 `origin/`。否则 toast「有未提交/未推送改动」并终止,什么都不改。 3. 关闭该工单全部终端(`terminalOrigin` 同 issueNumber + 名称匹配 `issue-N-冲突解决`),等 600ms 让 jsonl 刷盘。 4. 在 scratch 临时目录收集会话包:同一卷上用硬链接收文件,跨卷才复制。每个 sid 在 `~/.claude/projects/*/` 里找 `.jsonl`(多份取最新 mtime),连同 `/` 一起拷入 `claude/`(跳过 `subagents/`,子代理对话不移交);`reviewSessionId` 在 `~/.codex/sessions` 里按文件名尾部 uuid 找到 rollout 拷入 `codex/`。找不到的会话不进清单,只 log。写 `handoff.json`,`tar --options xz:compression-level=1 -cJf`。 5. 用阿里云官方 SDK `ali-oss` 的分片上传传到 OSS,带 `x-oss-object-acl: private`;中途失败把已传的分片清掉(abort)。 6. `mergeIssueState(issueNumber, { handoffBundleKey, handoffFrom: me })`。 7. `updateIssueAssignees(… [to])`。 -8. 本机清理(失败只 warn,不回滚——远端移交已生效):`pre-remove` 钩子 → `killProcessesUsingWorktree` → `removeWorktreeDir`;`cleanupFeatureBranch` **不传 remote/token**(只删本地分支,远端分支必须保留给接管方);本机字段全部写 `''` 墓碑。发送方磁盘上的会话文件保留。 -9. `issue/patch { handoffBundleKey, handoffFrom, worktreePath: null, worktreeExists: false, …sid: null }` + 成功 toast。 +8. 本机清理(失败只 warn,不回滚——远端移交已生效):`pre-remove` 钩子 → `killProcessesUsingWorktree` → `removeWorktreeDir`;`cleanupFeatureBranch` **不传 remote/token**(只删本地分支,远端分支必须保留给接管方);本机的会话 id、`worktreePath`、`prDiffFile` 写 `''` 墓碑(worktree 没删掉时保留 `worktreePath`),四个 profile 字段不动,工单以后接回来还是自己原来选的模型配置。发送方磁盘上的会话文件保留。 +9. `issue/patch { handoffBundleKey, handoffFrom, worktreePath: null, worktreeExists: false, …sid: null }` + 成功 toast;patch 里没有 profile 字段。 进度 toast 从核对分支开始,同一条提示随步骤改文案(核对分支、关闭终端、收集会话、`打包 x MB`、`上传 x MB / y MB`、写入状态、指派、清理本机)。打包时按包文件已写出的大小报,上传时每传完一片报一次。移交结束前不自动消失。 @@ -115,7 +113,7 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有) 2. 从 OSS 边下边写到 scratch 临时目录(文件名固定,不拿 key 拼本机路径;连续 60 秒收不到数据就放弃),`tar -xJf`,解析并校验 `handoff.json`(`version === 1`、`issue === issueNumber`)。 3. **重建 worktree**(`manifest.branch ?? state.branch` 存在时):`ensureWorktreeFromRemote({ workspaceRoot, worktreePath, branch })`——`git fetch origin `;该分支已有 live worktree → 直接用;模板路径被垃圾目录占着 → kill + 删;`git worktree add -B origin/`。模板路径用设置 `worktreeDirectory`,slug = `branch` 去掉 `feature/` 前缀。之后跑 `post-create` 钩子。分支不存在于远端 → toast 终止(会话包保留,可重试)。 4. **安装会话**:`brainstorm` 会话装到 `projectsDirFor(workspaceRoot)`,`implement` / `test` 装到 `projectsDirFor(worktreeAbs)`(无 worktree 时也装到 workspaceRoot)。安装前先删本机所有 `~/.claude/projects/*/.jsonl` 与 `/`(避免重复 sid),再拷 jsonl + 目录。codex 文件按 `relPath` 拷到 `~/.codex/sessions/` 下。 -5. 写本机字段:清单里的四个 sid、三个 profile 路径、`worktreePath`。 +5. 写本机字段:清单里的会话 id、`worktreePath`。profile 不写,本机原有的选择和设置里的默认值照旧生效。 6. `mergeIssueState(issueNumber, { handoffBundleKey: '', handoffFrom: '' })`;删 OSS 对象(失败只 warn)。 7. `issue/patch`(sid、worktreePath、worktreeExists、两个 handoff 字段置 null)+ 成功 toast。 diff --git a/vscode/src/cc/handoffBundle.test.ts b/vscode/src/cc/handoffBundle.test.ts index b9105d0..8049c0b 100644 --- a/vscode/src/cc/handoffBundle.test.ts +++ b/vscode/src/cc/handoffBundle.test.ts @@ -51,7 +51,6 @@ describe('stageHandoffBundle', () => { from: 'chw', branch: 'feature/x', sessions: { sessionId: BRAIN, implementSessionId: IMPL, testSessionId: 'missing-0000-4000-8000-000000000000', reviewSessionId: REVIEW }, - profiles: { profilePath: '/Users/chw/p.json' }, }, staging, roots, new Date('2026-08-26T00:00:00Z')) expect(manifest.claude).toEqual([{ id: BRAIN, kind: 'brainstorm' }, { id: IMPL, kind: 'implement' }]) expect(manifest.codex).toEqual([{ id: REVIEW, relPath: path.join('2026', '08', '26', `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`) }]) @@ -63,6 +62,12 @@ describe('stageHandoffBundle', () => { expect(existsSync(path.join(staging, 'codex', manifest.codex[0].relPath))).toBe(true) expect(parseHandoffManifest(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8'), 7)).toEqual(manifest) }) + it('清单不带 profiles:profile 路径是发送方本机的配置,不随会话包移交', async () => { + const staging = tempDir('staging-') + const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { sessionId: BRAIN } }, staging, senderRoots()) + expect(Object.keys(manifest)).not.toContain('profiles') + expect(Object.keys(JSON.parse(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8')))).not.toContain('profiles') + }) }) describe('pack → unpack → install', () => { @@ -74,7 +79,6 @@ describe('pack → unpack → install', () => { from: 'chw', branch: 'feature/x', sessions: { sessionId: BRAIN, implementSessionId: IMPL, reviewSessionId: REVIEW }, - profiles: {}, }, staging, roots) const archive = path.join(tempDir('archive-'), 'issue-7-20261008T010203Z.tar.xz') await packHandoff(staging, archive) @@ -104,7 +108,7 @@ describe('pack → unpack → install', () => { it('无 worktreeProjectsDir 时实施会话也装到工作区目录', async () => { const roots = senderRoots() const staging = tempDir('staging-') - const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { implementSessionId: IMPL }, profiles: {} }, staging, roots) + const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { implementSessionId: IMPL } }, staging, roots) const projectsRoot = tempDir('receiver-projects-') const workspaceProjectsDir = path.join(projectsRoot, '-home-me-proj') await installHandoffBundle(staging, manifest, { projectsRoot, codexRoot: tempDir('receiver-codex-'), workspaceProjectsDir }) @@ -128,7 +132,6 @@ describe('pack → unpack → install', () => { from: 'chw', worktreePath: WORKTREE_FROM, sessions: { implementSessionId: IMPL, reviewSessionId: REVIEW }, - profiles: {}, }, staging, roots) expect(manifest.worktreePath).toBe(WORKTREE_FROM) diff --git a/vscode/src/cc/handoffBundle.ts b/vscode/src/cc/handoffBundle.ts index 41cb769..7f1f4e0 100644 --- a/vscode/src/cc/handoffBundle.ts +++ b/vscode/src/cc/handoffBundle.ts @@ -1,4 +1,4 @@ -import type { HandoffManifest, HandoffProfiles, HandoffSessions } from './handoffManifest' +import type { HandoffManifest, HandoffSessions } from './handoffManifest' import type { ClaudeSessionFiles } from './sessionBundle' import { promises as fsp } from 'node:fs' import * as path from 'node:path' @@ -19,7 +19,6 @@ export interface HandoffSource { branch?: string worktreePath?: string sessions: HandoffSessions - profiles: HandoffProfiles } export interface HandoffRoots { @@ -106,7 +105,6 @@ export async function stageHandoffBundle( ...(src.branch ? { branch: src.branch } : {}), ...(src.worktreePath ? { worktreePath: src.worktreePath } : {}), sessions: src.sessions, - profiles: src.profiles, claude, codex, } diff --git a/vscode/src/cc/handoffManifest.test.ts b/vscode/src/cc/handoffManifest.test.ts index d038da3..189f53c 100644 --- a/vscode/src/cc/handoffManifest.test.ts +++ b/vscode/src/cc/handoffManifest.test.ts @@ -10,7 +10,6 @@ const good = { createdAt: '2026-08-26T00:00:00Z', branch: 'feature/x', sessions: { implementSessionId: 'a' }, - profiles: {}, claude: [{ id: UUID, kind: 'implement' }], codex: [], } @@ -57,4 +56,10 @@ describe('parseHandoffManifest', () => { it('不带 worktreePath → 结果里没有这个 key', () => { expect(parseHandoffManifest(JSON.stringify(good), 42)).not.toHaveProperty('worktreePath') }) + it('旧会话包带 profiles → 丢掉,接管方拿不到发送方机器上的 profile 路径', () => { + const old = { ...good, profiles: { brainstormProfilePath: '/Users/chehongweo/Sources/cruldra-profile/claude-config/profiles/gpt-6.json' } } + const parsed = parseHandoffManifest(JSON.stringify(old), 42) + expect(parsed).not.toHaveProperty('profiles') + expect(parsed).toEqual(good) + }) }) diff --git a/vscode/src/cc/handoffManifest.ts b/vscode/src/cc/handoffManifest.ts index ff651b2..16d87bd 100644 --- a/vscode/src/cc/handoffManifest.ts +++ b/vscode/src/cc/handoffManifest.ts @@ -15,13 +15,6 @@ export interface HandoffSessions { reviewSessionId?: string } -export interface HandoffProfiles { - profilePath?: string - brainstormProfilePath?: string - testProfilePath?: string - prototypeProfilePath?: string -} - export interface HandoffManifest { version: 1 issue: number @@ -30,7 +23,6 @@ export interface HandoffManifest { branch?: string worktreePath?: string sessions: HandoffSessions - profiles: HandoffProfiles claude: Array<{ id: string, kind: ClaudeSessionKind }> codex: Array<{ id: string, relPath: string }> } @@ -68,7 +60,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando if (!from || !createdAt) throw new Error('handoff.json 缺少 from / createdAt') const sessionsRaw = (raw.sessions ?? {}) as Record - const profilesRaw = (raw.profiles ?? {}) as Record const claudeRaw = Array.isArray(raw.claude) ? raw.claude : [] const codexRaw = Array.isArray(raw.codex) ? raw.codex : [] const claude = claudeRaw.map((e) => { @@ -93,12 +84,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando 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, @@ -107,7 +92,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando ...(optString(raw.branch) ? { branch: optString(raw.branch) } : {}), ...(optString(raw.worktreePath) ? { worktreePath: optString(raw.worktreePath) } : {}), sessions, - profiles, claude, codex, } diff --git a/vscode/src/issues/localState.ts b/vscode/src/issues/localState.ts index de56dc6..26e9705 100644 --- a/vscode/src/issues/localState.ts +++ b/vscode/src/issues/localState.ts @@ -12,16 +12,21 @@ import type { Issue } from '../gitea/types' import * as fs from 'node:fs' import { resolveWorktreePath } from '../git/worktree' +/** 工单上选的各类会话用哪份模型配置(profile 文件路径),是这台机器自己的选择,工单移交时既不带走也不清掉。 */ +export const LOCAL_PROFILE_FIELDS = [ + 'profilePath', + 'brainstormProfilePath', + 'testProfilePath', + 'prototypeProfilePath', +] as const + export const LOCAL_STATE_FIELDS = [ 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', - 'profilePath', - 'brainstormProfilePath', - 'testProfilePath', - 'prototypeProfilePath', + ...LOCAL_PROFILE_FIELDS, 'worktreePath', 'prDiffFile', ] as const diff --git a/vscode/src/panel/handlers/handoff.test.ts b/vscode/src/panel/handlers/handoff.test.ts index 39a20d4..5c02af2 100644 --- a/vscode/src/panel/handlers/handoff.test.ts +++ b/vscode/src/panel/handlers/handoff.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { parseHandoffManifest } from '../../cc/handoffManifest' import { canAcceptHandoff, canStartHandoff, @@ -68,6 +69,13 @@ describe('checkWorktreePushed', () => { }) }) +const PROFILE_FIELDS = ['profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath'] as const + +function expectNoProfileFields(payload: Record): void { + for (const f of PROFILE_FIELDS) + expect(payload).not.toHaveProperty(f) +} + describe('payloads', () => { it('移交落盘:只写两个共享字段,本机字段留到本机清理跑完后再墓碑', () => { expect(handoffStartedStateExtra(KEY, 'chw')).toEqual({ @@ -75,17 +83,13 @@ describe('payloads', () => { handoffFrom: 'chw', }) }) - it('本机墓碑:keepWorktreePath=false 时本机字段全清', () => { + it('本机墓碑:keepWorktreePath=false 时清会话 id、worktreePath、prDiffFile', () => { expect(handoffLocalTombstones({ keepWorktreePath: false })).toEqual({ sessionId: '', implementSessionId: '', reviewSessionId: '', testSessionId: '', prototypeSessionId: '', - profilePath: '', - brainstormProfilePath: '', - testProfilePath: '', - prototypeProfilePath: '', worktreePath: '', prDiffFile: '', }) @@ -99,13 +103,13 @@ describe('payloads', () => { reviewSessionId: '', testSessionId: '', prototypeSessionId: '', - profilePath: '', - brainstormProfilePath: '', - testProfilePath: '', - prototypeProfilePath: '', prDiffFile: '', }) }) + it('本机墓碑不清 profile 字段:移交出去再接回来,还是自己原来选的模型配置', () => { + expectNoProfileFields(handoffLocalTombstones({ keepWorktreePath: false })) + expectNoProfileFields(handoffLocalTombstones({ keepWorktreePath: true })) + }) it('移交 UI patch 用 null 清字段并更新 assignees', () => { expect(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: false })).toEqual({ handoffBundleKey: KEY, @@ -116,10 +120,6 @@ describe('payloads', () => { reviewSessionId: null, testSessionId: null, prototypeSessionId: null, - profilePath: null, - brainstormProfilePath: null, - testProfilePath: null, - prototypeProfilePath: null, prDiffFile: null, worktreePath: null, worktreeExists: false, @@ -138,31 +138,43 @@ describe('payloads', () => { reviewSessionId: null, testSessionId: null, prototypeSessionId: null, - profilePath: null, - brainstormProfilePath: null, - testProfilePath: null, - prototypeProfilePath: null, prDiffFile: null, }) }) - it('接管落盘:清两个共享字段,写本机 sid / profile / worktree', () => { + it('移交 UI patch 不碰 profile 字段,卡片上仍显示自己选的模型配置', () => { + expectNoProfileFields(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: false })) + expectNoProfileFields(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: true })) + }) + it('接管落盘:清两个共享字段,写本机 sid / worktree', () => { expect(handoffAcceptedStateExtra({ sessions: { implementSessionId: 'i', reviewSessionId: 'r' }, - profiles: { profilePath: '/p.json' }, worktreePath: '/wt', })).toEqual({ handoffBundleKey: '', handoffFrom: '', implementSessionId: 'i', reviewSessionId: 'r', - profilePath: '/p.json', worktreePath: '/wt', }) }) + it('接管旧会话包:包里带着同事机器上的 profile 路径,本机 state 和 UI 都不写 profile 字段', () => { + const manifest = parseHandoffManifest(JSON.stringify({ + version: 1, + issue: 9, + from: 'chehongweo', + createdAt: '2026-10-08T01:02:03Z', + sessions: { sessionId: 's' }, + profiles: { brainstormProfilePath: '/Users/chehongweo/Sources/cruldra-profile/claude-config/profiles/gpt-6.json' }, + claude: [], + codex: [], + }), 9) + const local = { sessions: manifest.sessions, worktreePath: '/wt' } + expectNoProfileFields(handoffAcceptedStateExtra(local)) + expectNoProfileFields(handoffAcceptedUiPatch({ ...local, worktreeExists: true, missingSessionIds: [] })) + }) it('接管 UI patch', () => { expect(handoffAcceptedUiPatch({ sessions: { sessionId: 's' }, - profiles: {}, worktreePath: '/wt', worktreeExists: true, missingSessionIds: [], @@ -178,7 +190,6 @@ describe('payloads', () => { it('接管 UI patch:无 worktreePath 时显式置 null,不留旧值', () => { expect(handoffAcceptedUiPatch({ sessions: { sessionId: 's' }, - profiles: {}, worktreeExists: false, missingSessionIds: [], })).toEqual({ @@ -193,7 +204,6 @@ describe('payloads', () => { it('接管 UI patch:缺失的会话 id 原样下发,UI 立刻知道哪几行不能 resume', () => { expect(handoffAcceptedUiPatch({ sessions: { sessionId: 's', reviewSessionId: 'r' }, - profiles: {}, worktreeExists: false, missingSessionIds: ['r'], })).toEqual({ diff --git a/vscode/src/panel/handlers/handoff.ts b/vscode/src/panel/handlers/handoff.ts index f1e8b8e..8a7fb25 100644 --- a/vscode/src/panel/handlers/handoff.ts +++ b/vscode/src/panel/handlers/handoff.ts @@ -1,7 +1,7 @@ -import type { HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest' +import type { HandoffSessions } from '../../cc/handoffManifest' import type { IssueColumn } from '../../gitea/types' import { formatByteSize } from '../../cc/handoffArchive' -import { LOCAL_STATE_FIELDS } from '../../issues/localState' +import { LOCAL_PROFILE_FIELDS, LOCAL_STATE_FIELDS } from '../../issues/localState' export function canStartHandoff(issue: { source?: 'gitea' | 'youtrack' @@ -46,30 +46,35 @@ export function checkWorktreePushed(input: { } /** - * 共享字段:只写 handoffBundleKey / handoffFrom 两个。本机 - * sid/profile/worktreePath 墓碑要等本机清理跑完(成功指派后)才落盘, - * 见 handoffLocalTombstones —— 否则④/⑤失败回滚时本机字段已经被清空, - * 用户会话恢复不了。 + * 移交时写进共享 state 的只有 `handoffBundleKey` 和 `handoffFrom` 两个字段,让接管方知道有会话包等着接。 + * + * 本机字段的墓碑要等本机清理跑完(指派成功之后)才落盘,见 [handoffLocalTombstones]:否则写状态或指派失败回滚时, + * 本机字段已经清空,用户的会话就恢复不了。 */ export function handoffStartedStateExtra(bundleKey: string, from: string): Record { return { handoffBundleKey: bundleKey, handoffFrom: from } } /** - * 本机墓碑:移交流程最后一步(本机清理跑完之后)才调用。worktree 删除失败时 - * 保留 worktreePath(keepWorktreePath),别把用户还留在磁盘上的 worktree - * 从 state 里抹掉。 + * 移交给别人后,把本机上会话 id、worktree 路径这些到了接管方就没用的字段写成 `''` 墓碑。 + * + * 移交流程最后一步(本机清理跑完之后)才调用。[LOCAL_PROFILE_FIELDS] 不清:那是用户在这台机器上选的模型配置, + * 工单以后接回来还照旧用。worktree 删除失败时(`keepWorktreePath`)也保留 `worktreePath`, + * 别把用户还留在磁盘上的 worktree 从 state 里抹掉。 */ export function handoffLocalTombstones(opts: { keepWorktreePath: boolean }): Record { + const kept = new Set(LOCAL_PROFILE_FIELDS) + if (opts.keepWorktreePath) + kept.add('worktreePath') const extra: Record = {} for (const f of LOCAL_STATE_FIELDS) { - if (f === 'worktreePath' && opts.keepWorktreePath) - continue - extra[f] = '' + if (!kept.has(f)) + extra[f] = '' } return extra } +/** 移交成功后发给看板的改动:标上待接管、换负责人、清掉本机会话 id 和 worktree,profile 不动,卡片上照旧是本机选的模型配置。 */ export function handoffStartedUiPatch(bundleKey: string, from: string, to: string, opts: { keepWorktreePath: boolean }): Record { return { handoffBundleKey: bundleKey, @@ -80,10 +85,6 @@ export function handoffStartedUiPatch(bundleKey: string, from: string, to: strin 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 }), @@ -99,23 +100,21 @@ function definedEntries(o: Record): Record { return out } +/** 接管后写进 state 的字段:清掉两个共享的移交字段,写入会话 id 和本机 worktree 路径;profile 不写,接管方用自己机器上的模型配置。 */ export function handoffAcceptedStateExtra(local: { sessions: HandoffSessions - profiles: HandoffProfiles worktreePath?: string }): Record { return { handoffBundleKey: '', handoffFrom: '', ...definedEntries(local.sessions), - ...definedEntries(local.profiles), ...(local.worktreePath ? { worktreePath: local.worktreePath } : {}), } } export function handoffAcceptedUiPatch(local: { sessions: HandoffSessions - profiles: HandoffProfiles worktreePath?: string worktreeExists: boolean missingSessionIds: string[] @@ -124,7 +123,6 @@ export function handoffAcceptedUiPatch(local: { handoffBundleKey: null, handoffFrom: null, ...definedEntries(local.sessions), - ...definedEntries(local.profiles), // 显式 null 而非省略:省略会让 webview 侧留着接管前的旧 worktreePath, // 与 worktreeExists: false 对不上。 worktreePath: local.worktreePath ?? null, diff --git a/vscode/src/panel/handlers/handoffFlow.ts b/vscode/src/panel/handlers/handoffFlow.ts index 3409f70..4ba98d3 100644 --- a/vscode/src/panel/handlers/handoffFlow.ts +++ b/vscode/src/panel/handlers/handoffFlow.ts @@ -1,4 +1,4 @@ -import type { HandoffManifest, HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest' +import type { HandoffManifest, HandoffSessions } from '../../cc/handoffManifest' import type { IssueColumn } from '../../gitea/types' import type { HandoffStore } from '../../oss/handoffStore' import type { KanbanWebviewPanel } from '../KanbanPanel' @@ -217,12 +217,6 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber: ...(str(state.prototypeSessionId) ? { prototypeSessionId: str(state.prototypeSessionId) } : {}), ...(str(state.reviewSessionId) ? { reviewSessionId: str(state.reviewSessionId) } : {}), } - const profiles: HandoffProfiles = { - ...(str(state.profilePath) ? { profilePath: str(state.profilePath) } : {}), - ...(str(state.brainstormProfilePath) ? { brainstormProfilePath: str(state.brainstormProfilePath) } : {}), - ...(str(state.testProfilePath) ? { testProfilePath: str(state.testProfilePath) } : {}), - ...(str(state.prototypeProfilePath) ? { prototypeProfilePath: str(state.prototypeProfilePath) } : {}), - } const fail = (message: string): HandoffStartResult => { progress.dismiss() @@ -292,7 +286,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber: // ③ 打包 + 上传 say('收集会话') - const handoffSrc = { issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions, profiles } + const handoffSrc = { issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions } const handoffRoots = { projectsRoot: claudeProjectsRoot(), codexRoot: defaultCodexSessionsDir() } const bundleKey = handoffBundleKey({ owner, repo, issueNumber, at: new Date() }) let manifest: HandoffManifest @@ -372,9 +366,10 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber: await cleanupFeatureBranch({ workspaceRoot, branch, issueNumber, devBranch: settings.devBranch }) } - // ⑦ 本机墓碑:sid/profile/worktreePath 归零。worktree 没删掉时保留 - // worktreePath,别把用户还留在磁盘上的 worktree 从 state 里抹掉。写墓碑 - // 失败只告警——远端移交已经生效,不回滚。 + // ⑦ 本机墓碑:会话 id、worktreePath、prDiffFile 归零,profile 留着——那是 + // 本机选的模型配置,工单接回来还照旧用。worktree 没删掉时保留 worktreePath, + // 别把用户还留在磁盘上的 worktree 从 state 里抹掉。写墓碑失败只告警—— + // 远端移交已经生效,不回滚。 await panel.mergeIssueState(issueNumber, handoffLocalTombstones({ keepWorktreePath: worktreeRemovalFailed })).catch((err) => { const message = err instanceof Error ? err.message : String(err) logger.add({ level: 'warn', source: 'panel', message: `移交 #${issueNumber}:写本机墓碑失败`, details: message }) @@ -583,12 +578,13 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber ...(pathRewrite ? { pathRewrite } : {}), }) - // ④ 本机字段 + 清共享字段;⑤ 删 OSS 上的会话包(失败只 warn) + // ④ 写本机会话 id 和 worktreePath、清共享字段,profile 不碰(用本机自己的 + // 模型配置);⑤ 删 OSS 上的会话包(失败只 warn) // missingSessionIds 是计算字段,board 只在加载时算;会话文件刚装进本机, // 这里按真实文件重算一起下发,UI 不用等下次刷新就能双击 resume。 say('写入状态') const [{ missingSessionIds = [] }] = await annotateMissingSessionIds([manifest.sessions]) - const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs } + const local = { sessions: manifest.sessions, worktreePath: worktreeAbs } await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local)) try { await deleteHandoffBundle(store, bundleKey)