Skip to content

Commit 374a1e1

Browse files
author
catlog22
committed
fix: robust session resume for team-lifecycle with state reconciliation
Resolve task execution order disruption after pause/resume by adding 9-step resume flow: audit TaskList, reconcile session vs TaskList state, rebuild dependency chains, create missing tasks via TASK_METADATA lookup, and kick first actionable worker to break resume deadlock. Also add Phase 1.5 Resume Artifact Check to worker Task Lifecycle in SKILL.md to prevent duplicate artifact generation on resumed tasks.
1 parent 2e01852 commit 374a1e1

2 files changed

Lines changed: 263 additions & 14 deletions

File tree

.claude/skills/team-lifecycle/SKILL.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,25 @@ if (myTasks.length === 0) return // idle
135135
const task = TaskGet({ taskId: myTasks[0].id })
136136
TaskUpdate({ taskId: task.id, status: 'in_progress' })
137137

138+
// Phase 1.5: Resume Artifact Check (防止重复产出)
139+
// 当 session 从暂停恢复时,coordinator 已将 in_progress 任务重置为 pending。
140+
// Worker 在开始工作前,必须检查该任务的输出产物是否已存在。
141+
// 如果产物已存在且内容完整:
142+
// → 直接跳到 Phase 5 报告完成(避免覆盖上次成果)
143+
// 如果产物存在但不完整(如文件为空或缺少关键 section):
144+
// → 正常执行 Phase 2-4(基于已有产物继续,而非从头开始)
145+
// 如果产物不存在:
146+
// → 正常执行 Phase 2-4
147+
//
148+
// 每个 role 检查自己的输出路径:
149+
// analyst → sessionFolder/spec/discovery-context.json
150+
// writer → sessionFolder/spec/{product-brief.md | requirements/ | architecture/ | epics/}
151+
// discussant → sessionFolder/discussions/discuss-NNN-*.md
152+
// planner → sessionFolder/plan/plan.json
153+
// executor → git diff (已提交的代码变更)
154+
// tester → test pass rate
155+
// reviewer → sessionFolder/spec/readiness-report.md (quality) 或 review findings (code)
156+
138157
// Phase 2-4: Role-specific (see roles/{role}.md)
139158

140159
// Phase 5: Report + Loop
@@ -193,10 +212,18 @@ Coordinator supports `--resume` / `--continue` flags to resume interrupted sessi
193212

194213
1. Scans `.workflow/.team/TLS-*/team-session.json` for `status: "active"` or `"paused"`
195214
2. Multiple matches → `AskUserQuestion` for user selection
196-
3. Loads session state: `teamName`, `mode`, `sessionFolder`, `completed_tasks`
197-
4. Rebuilds team (`TeamCreate` + worker spawns)
198-
5. Creates only uncompleted tasks in the task chain
199-
6. Jumps to Phase 4 coordination loop
215+
3. **Audit TaskList** — 获取当前所有任务的真实状态
216+
4. **Reconcile** — 双向同步 session.completed_tasks ↔ TaskList 状态:
217+
- session 已完成但 TaskList 未标记 → 修正 TaskList 为 completed
218+
- TaskList 已完成但 session 未记录 → 补录到 session
219+
- in_progress 状态(暂停中断)→ 重置为 pending
220+
5. Determines remaining pipeline from reconciled state
221+
6. Rebuilds team (`TeamCreate` + worker spawns for needed roles only)
222+
7. Creates missing tasks with correct `blockedBy` dependency chain (uses `TASK_METADATA` lookup)
223+
8. Verifies dependency chain integrity for existing tasks
224+
9. Updates session file with reconciled state + current_phase
225+
10. **Kick** — 向首个可执行任务的 worker 发送 `task_unblocked` 消息,打破 resume 死锁
226+
11. Jumps to Phase 4 coordination loop
200227

201228
## Coordinator Spawn Template
202229

.claude/skills/team-lifecycle/roles/coordinator.md

Lines changed: 232 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,247 @@ if (isResume) {
6767
const mode = resumedSession.mode
6868
const sessionFolder = `.workflow/.team/${resumedSession.session_id}`
6969
const taskDescription = resumedSession.topic
70+
const executionMethod = resumedSession.user_preferences?.execution_method || 'Auto'
71+
const codeReviewTool = resumedSession.user_preferences?.code_review || 'Skip'
72+
73+
// ============================================================
74+
// Pipeline Constants
75+
// ============================================================
76+
const SPEC_CHAIN = [
77+
'RESEARCH-001', 'DISCUSS-001', 'DRAFT-001', 'DISCUSS-002',
78+
'DRAFT-002', 'DISCUSS-003', 'DRAFT-003', 'DISCUSS-004',
79+
'DRAFT-004', 'DISCUSS-005', 'QUALITY-001', 'DISCUSS-006'
80+
]
81+
const IMPL_CHAIN = ['PLAN-001', 'IMPL-001', 'TEST-001', 'REVIEW-001']
82+
83+
// Task metadata: prefix → { subject, owner, description template, activeForm }
84+
const TASK_METADATA = {
85+
'RESEARCH-001': { owner: 'analyst', subject: 'RESEARCH-001: 主题发现与上下文研究', activeForm: '研究中',
86+
desc: () => `${taskDescription}\n\nSession: ${sessionFolder}\n输出: ${sessionFolder}/spec/spec-config.json + spec/discovery-context.json` },
87+
'DISCUSS-001': { owner: 'discussant', subject: 'DISCUSS-001: 研究结果讨论 - 范围确认与方向调整', activeForm: '讨论范围中',
88+
desc: () => `讨论 RESEARCH-001 的发现结果\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/discovery-context.json\n输出: ${sessionFolder}/discussions/discuss-001-scope.md` },
89+
'DRAFT-001': { owner: 'writer', subject: 'DRAFT-001: 撰写 Product Brief', activeForm: '撰写 Brief 中',
90+
desc: () => `基于研究和讨论共识撰写产品简报\n\nSession: ${sessionFolder}\n输入: discovery-context.json + discuss-001-scope.md\n输出: ${sessionFolder}/spec/product-brief.md` },
91+
'DISCUSS-002': { owner: 'discussant', subject: 'DISCUSS-002: Product Brief 多视角评审', activeForm: '评审 Brief 中',
92+
desc: () => `评审 Product Brief 文档\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/product-brief.md\n输出: ${sessionFolder}/discussions/discuss-002-brief.md` },
93+
'DRAFT-002': { owner: 'writer', subject: 'DRAFT-002: 撰写 Requirements/PRD', activeForm: '撰写 PRD 中',
94+
desc: () => `基于 Brief 和讨论反馈撰写需求文档\n\nSession: ${sessionFolder}\n输入: product-brief.md + discuss-002-brief.md\n输出: ${sessionFolder}/spec/requirements/` },
95+
'DISCUSS-003': { owner: 'discussant', subject: 'DISCUSS-003: 需求完整性与优先级讨论', activeForm: '讨论需求中',
96+
desc: () => `讨论 PRD 需求完整性\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/requirements/_index.md\n输出: ${sessionFolder}/discussions/discuss-003-requirements.md` },
97+
'DRAFT-003': { owner: 'writer', subject: 'DRAFT-003: 撰写 Architecture Document', activeForm: '撰写架构中',
98+
desc: () => `基于需求和讨论反馈撰写架构文档\n\nSession: ${sessionFolder}\n输入: requirements/ + discuss-003-requirements.md\n输出: ${sessionFolder}/spec/architecture/` },
99+
'DISCUSS-004': { owner: 'discussant', subject: 'DISCUSS-004: 架构决策与技术可行性讨论', activeForm: '讨论架构中',
100+
desc: () => `讨论架构设计合理性\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/architecture/_index.md\n输出: ${sessionFolder}/discussions/discuss-004-architecture.md` },
101+
'DRAFT-004': { owner: 'writer', subject: 'DRAFT-004: 撰写 Epics & Stories', activeForm: '撰写 Epics 中',
102+
desc: () => `基于架构和讨论反馈撰写史诗和用户故事\n\nSession: ${sessionFolder}\n输入: architecture/ + discuss-004-architecture.md\n输出: ${sessionFolder}/spec/epics/` },
103+
'DISCUSS-005': { owner: 'discussant', subject: 'DISCUSS-005: 执行计划与MVP范围讨论', activeForm: '讨论执行计划中',
104+
desc: () => `讨论执行计划就绪性\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/epics/_index.md\n输出: ${sessionFolder}/discussions/discuss-005-epics.md` },
105+
'QUALITY-001': { owner: 'reviewer', subject: 'QUALITY-001: 规格就绪度检查', activeForm: '质量检查中',
106+
desc: () => `全文档交叉验证和质量评分\n\nSession: ${sessionFolder}\n输入: 全部文档\n输出: ${sessionFolder}/spec/readiness-report.md + spec/spec-summary.md` },
107+
'DISCUSS-006': { owner: 'discussant', subject: 'DISCUSS-006: 最终签收与交付确认', activeForm: '最终签收讨论中',
108+
desc: () => `最终讨论和签收\n\nSession: ${sessionFolder}\n输入: ${sessionFolder}/spec/readiness-report.md\n输出: ${sessionFolder}/discussions/discuss-006-final.md` },
109+
'PLAN-001': { owner: 'planner', subject: 'PLAN-001: 探索和规划实现', activeForm: '规划中',
110+
desc: () => `${taskDescription}\n\nSession: ${sessionFolder}\n写入: ${sessionFolder}/plan/` },
111+
'IMPL-001': { owner: 'executor', subject: 'IMPL-001: 实现已批准的计划', activeForm: '实现中',
112+
desc: () => `${taskDescription}\n\nSession: ${sessionFolder}\nPlan: ${sessionFolder}/plan/plan.json\nexecution_method: ${executionMethod}\ncode_review: ${codeReviewTool}` },
113+
'TEST-001': { owner: 'tester', subject: 'TEST-001: 测试修复循环', activeForm: '测试中',
114+
desc: () => taskDescription },
115+
'REVIEW-001': { owner: 'reviewer', subject: 'REVIEW-001: 代码审查与需求验证', activeForm: '审查中',
116+
desc: () => `${taskDescription}\n\nSession: ${sessionFolder}\nPlan: ${sessionFolder}/plan/plan.json` }
117+
}
118+
119+
// Pipeline dependency: prefix → predecessor prefix (special: TEST-001 & REVIEW-001 both depend on IMPL-001)
120+
function getPredecessor(prefix, pipeline) {
121+
if (prefix === 'TEST-001' || prefix === 'REVIEW-001') return 'IMPL-001'
122+
const idx = pipeline.indexOf(prefix)
123+
return idx > 0 ? pipeline[idx - 1] : null
124+
}
125+
126+
// ============================================================
127+
// Step 1: Audit TaskList — 审计当前任务清单状态
128+
// ============================================================
129+
const allTasks = TaskList()
130+
const pipeline = mode === 'spec-only' ? SPEC_CHAIN
131+
: mode === 'impl-only' ? IMPL_CHAIN
132+
: [...SPEC_CHAIN, ...IMPL_CHAIN]
133+
const sessionCompleted = new Set(resumedSession.completed_tasks || [])
134+
135+
// Build prefix → task mapping from existing TaskList
136+
const existingByPrefix = {}
137+
allTasks.forEach(t => {
138+
const prefixMatch = t.subject.match(/^([A-Z]+-\d+)/)
139+
if (prefixMatch) existingByPrefix[prefixMatch[1]] = t
140+
})
141+
142+
// ============================================================
143+
// Step 2: Reconcile — 同步 session 与 TaskList 状态
144+
// ============================================================
145+
const reconciledCompleted = new Set(sessionCompleted)
146+
const statusFixes = []
147+
148+
for (const prefix of pipeline) {
149+
const existing = existingByPrefix[prefix]
150+
if (!existing) continue
151+
152+
// Case A: session 记录已完成,但 TaskList 状态不是 completed → 修正 TaskList
153+
if (sessionCompleted.has(prefix) && existing.status !== 'completed') {
154+
TaskUpdate({ taskId: existing.id, status: 'completed' })
155+
statusFixes.push(`${prefix}: ${existing.status} → completed (sync from session)`)
156+
}
157+
158+
// Case B: TaskList 已 completed,但 session 未记录 → 补录 session
159+
if (existing.status === 'completed' && !sessionCompleted.has(prefix)) {
160+
reconciledCompleted.add(prefix)
161+
statusFixes.push(`${prefix}: completed (sync to session)`)
162+
}
163+
164+
// Case C: TaskList 是 in_progress(暂停时可能中断)→ 重置为 pending
165+
if (existing.status === 'in_progress' && !sessionCompleted.has(prefix)) {
166+
TaskUpdate({ taskId: existing.id, status: 'pending' })
167+
statusFixes.push(`${prefix}: in_progress → pending (reset for retry)`)
168+
}
169+
}
170+
171+
// Update session with reconciled completed_tasks
172+
resumedSession.completed_tasks = [...reconciledCompleted]
70173

71-
// Rebuild team
174+
// ============================================================
175+
// Step 3: Determine remaining pipeline — 确定剩余任务顺序
176+
// ============================================================
177+
const remainingPipeline = pipeline.filter(p => !reconciledCompleted.has(p))
178+
179+
// ============================================================
180+
// Step 4: Rebuild team + Spawn workers — 重建团队
181+
// ============================================================
72182
TeamCreate({ team_name: teamName })
73-
// Spawn workers based on mode (see Phase 2)
74183

75-
// Update session status
184+
// Determine which worker roles are needed based on remaining tasks
185+
const neededRoles = new Set()
186+
remainingPipeline.forEach(prefix => {
187+
const meta = TASK_METADATA[prefix]
188+
if (meta) neededRoles.add(meta.owner)
189+
})
190+
191+
// Spawn only needed workers using Phase 2 spawn template (see SKILL.md Coordinator Spawn Template)
192+
// Each worker is spawned with prompt that:
193+
// 1. Identifies their role
194+
// 2. Instructs to call Skill(skill="team-lifecycle", args="--role=<name>")
195+
// 3. Includes session context: taskDescription, sessionFolder, constraints
196+
// 4. Instructs immediate TaskList polling on startup
197+
neededRoles.forEach(role => {
198+
// → Use SKILL.md Coordinator Spawn Template for each role
199+
// → Worker prompt includes: "Session: ${sessionFolder}", "需求: ${taskDescription}"
200+
})
201+
202+
// ============================================================
203+
// Step 5: Create missing tasks with correct dependencies
204+
// ============================================================
205+
// In a new conversation, TaskList is EMPTY — all remaining tasks must be created.
206+
// In a same-conversation resume, some tasks may already exist.
207+
const missingPrefixes = remainingPipeline.filter(p => !existingByPrefix[p])
208+
209+
for (const prefix of missingPrefixes) {
210+
const meta = TASK_METADATA[prefix]
211+
if (!meta) continue
212+
213+
// Create task
214+
const newTask = TaskCreate({
215+
subject: meta.subject,
216+
description: meta.desc(),
217+
activeForm: meta.activeForm
218+
})
219+
TaskUpdate({ taskId: newTask.id, owner: meta.owner })
220+
221+
// Register in existingByPrefix for dependency wiring
222+
existingByPrefix[prefix] = { id: newTask.id, status: 'pending', blockedBy: [] }
223+
224+
// Wire dependency: find predecessor
225+
const predPrefix = getPredecessor(prefix, pipeline)
226+
if (predPrefix && !reconciledCompleted.has(predPrefix)) {
227+
const predTask = existingByPrefix[predPrefix]
228+
if (predTask) {
229+
TaskUpdate({ taskId: newTask.id, addBlockedBy: [predTask.id] })
230+
}
231+
}
232+
233+
statusFixes.push(`${prefix}: created (missing in TaskList)`)
234+
}
235+
236+
// ============================================================
237+
// Step 6: Verify dependency chain integrity for existing tasks
238+
// ============================================================
239+
for (const prefix of remainingPipeline) {
240+
// Skip tasks we just created (already wired)
241+
if (missingPrefixes.includes(prefix)) continue
242+
const task = existingByPrefix[prefix]
243+
if (!task || task.status === 'completed') continue
244+
245+
const predPrefix = getPredecessor(prefix, pipeline)
246+
if (!predPrefix || reconciledCompleted.has(predPrefix)) continue
247+
248+
const predTask = existingByPrefix[predPrefix]
249+
if (predTask && task.blockedBy && !task.blockedBy.includes(predTask.id)) {
250+
TaskUpdate({ taskId: task.id, addBlockedBy: [predTask.id] })
251+
statusFixes.push(`${prefix}: added missing blockedBy → ${predPrefix}`)
252+
}
253+
}
254+
255+
// ============================================================
256+
// Step 7: Update session file — 写入恢复状态
257+
// ============================================================
76258
resumedSession.status = 'active'
77259
resumedSession.resumed_at = new Date().toISOString()
78260
resumedSession.updated_at = new Date().toISOString()
261+
if (remainingPipeline.length > 0) {
262+
const firstRemaining = remainingPipeline[0]
263+
if (/^(RESEARCH|DISCUSS|DRAFT|QUALITY)/.test(firstRemaining)) {
264+
resumedSession.current_phase = 'spec'
265+
} else if (firstRemaining.startsWith('PLAN')) {
266+
resumedSession.current_phase = 'plan'
267+
} else {
268+
resumedSession.current_phase = 'impl'
269+
}
270+
}
79271
Write(`${sessionFolder}/team-session.json`, JSON.stringify(resumedSession, null, 2))
80272

81-
// Create only uncompleted tasks from pipeline
82-
const completedTasks = new Set(resumedSession.completed_tasks || [])
83-
const pipeline = resumedSession.mode === 'spec-only' ? SPEC_CHAIN
84-
: resumedSession.mode === 'impl-only' ? IMPL_CHAIN
85-
: [...SPEC_CHAIN, ...IMPL_CHAIN]
86-
const remainingTasks = pipeline.filter(t => !completedTasks.has(t))
273+
// ============================================================
274+
// Step 8: Report reconciliation — 输出恢复摘要
275+
// ============================================================
276+
// Output to user:
277+
// - Session: {session_id} resumed
278+
// - Completed: {reconciledCompleted.size}/{pipeline.length} tasks
279+
// - Remaining: {remainingPipeline.join(' → ')}
280+
// - Status fixes: {statusFixes.length} corrections applied
281+
// - Next task: {remainingPipeline[0]}
282+
// - Workers spawned: {[...neededRoles].join(', ')}
283+
284+
// ============================================================
285+
// Step 9: Kick — 通知首个可执行任务的 worker 启动
286+
// ============================================================
287+
// 解决 resume 后的死锁:coordinator 等 worker 消息 ↔ worker 等任务
288+
// 找到第一个 pending + blockedBy 为空的任务,向其 owner 发送 task_unblocked
289+
const firstActionable = remainingPipeline.find(prefix => {
290+
const task = existingByPrefix[prefix]
291+
return task && task.status === 'pending' && (!task.blockedBy || task.blockedBy.length === 0)
292+
})
293+
294+
if (firstActionable) {
295+
const meta = TASK_METADATA[firstActionable]
296+
mcp__ccw-tools__team_msg({
297+
operation: "log", team: teamName,
298+
from: "coordinator", to: meta.owner,
299+
type: "task_unblocked",
300+
summary: `Resume: ${firstActionable} is ready for execution`
301+
})
302+
SendMessage({
303+
type: "message",
304+
recipient: meta.owner,
305+
content: `Session 已恢复。你的任务 ${firstActionable} 已就绪,请立即执行 TaskList 检查并开始工作。`,
306+
summary: `Resume kick: ${firstActionable}`
307+
})
308+
}
87309

88-
// → Skip to Phase 3 with remainingTasks, then Phase 4 coordination loop
310+
// → Skip to Phase 4 coordination loop
89311
}
90312
}
91313
```

0 commit comments

Comments
 (0)