@comfanion/workflow 4.38.1-dev.3 → 4.38.1-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/build-info.json
CHANGED
|
@@ -39,9 +39,20 @@ interface StoryContext {
|
|
|
39
39
|
fullContent: string
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
interface SessionState {
|
|
43
|
+
command: string | null // /dev-sprint, /dev-epic, /dev-story
|
|
44
|
+
agent: string | null
|
|
45
|
+
sprint: { number: number; status: string } | null
|
|
46
|
+
epic: { id: string; title: string; file: string; progress: string } | null
|
|
47
|
+
story: { id: string; title: string; file: string; current_task: string | null; completed_tasks: string[]; pending_tasks: string[] } | null
|
|
48
|
+
next_action: string | null
|
|
49
|
+
key_decisions: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
interface SessionContext {
|
|
43
53
|
todos: TaskStatus[]
|
|
44
54
|
story: StoryContext | null
|
|
55
|
+
sessionState: SessionState | null
|
|
45
56
|
relevantFiles: string[]
|
|
46
57
|
activeAgent: string | null
|
|
47
58
|
activeCommand: string | null // /dev-story, /dev-epic, /dev-sprint
|
|
@@ -169,7 +180,7 @@ export const CustomCompactionPlugin: Plugin = async (ctx) => {
|
|
|
169
180
|
/**
|
|
170
181
|
* Generate Read commands that agent MUST execute after compaction
|
|
171
182
|
*/
|
|
172
|
-
async function generateReadCommands(agent: string | null, story: StoryContext | null, activeCommand: string | null): Promise<string> {
|
|
183
|
+
async function generateReadCommands(agent: string | null, story: StoryContext | null, activeCommand: string | null, sessionState: SessionState | null): Promise<string> {
|
|
173
184
|
const agentKey = (typeof agent === 'string' ? agent.toLowerCase() : null) || "default"
|
|
174
185
|
const filesToRead = [...(MUST_READ_FILES[agentKey] || MUST_READ_FILES.default)]
|
|
175
186
|
|
|
@@ -179,17 +190,23 @@ export const CustomCompactionPlugin: Plugin = async (ctx) => {
|
|
|
179
190
|
filesToRead.unshift(`.opencode/commands/${commandFile}`)
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
// For dev/coder: add
|
|
193
|
+
// For dev/coder: add session-state.yaml as priority read
|
|
183
194
|
if ((agentKey === "dev" || agentKey === "coder")) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Epic state file (has all context)
|
|
187
|
-
filesToRead.unshift(epicState.statePath.replace(directory + "/", ""))
|
|
188
|
-
}
|
|
195
|
+
// Session state is most important — has everything
|
|
196
|
+
filesToRead.unshift(".opencode/session-state.yaml")
|
|
189
197
|
|
|
190
|
-
// Then story file
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
// Then story file (from session state or StoryContext)
|
|
199
|
+
const storyFile = sessionState?.story?.file || story?.path
|
|
200
|
+
if (storyFile) {
|
|
201
|
+
filesToRead.unshift(storyFile) // Story first!
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Epic state as backup (only if no session state)
|
|
205
|
+
if (!sessionState) {
|
|
206
|
+
const epicState = await getActiveEpicState()
|
|
207
|
+
if (epicState) {
|
|
208
|
+
filesToRead.unshift(epicState.statePath.replace(directory + "/", ""))
|
|
209
|
+
}
|
|
193
210
|
}
|
|
194
211
|
}
|
|
195
212
|
|
|
@@ -304,6 +321,78 @@ DO NOT skip this step. DO NOT ask user what to do. Just read these files first.`
|
|
|
304
321
|
}
|
|
305
322
|
}
|
|
306
323
|
|
|
324
|
+
/**
|
|
325
|
+
* PRIMARY source: session-state.yaml written by AI agent.
|
|
326
|
+
* Falls back to getActiveEpicState() + getActiveStory() if missing.
|
|
327
|
+
*/
|
|
328
|
+
async function getSessionState(): Promise<SessionState | null> {
|
|
329
|
+
try {
|
|
330
|
+
const statePath = join(directory, ".opencode", "session-state.yaml")
|
|
331
|
+
const content = await readFile(statePath, "utf-8")
|
|
332
|
+
await log(directory, ` session-state.yaml found, parsing...`)
|
|
333
|
+
|
|
334
|
+
// Simple regex YAML parser for flat/nested fields
|
|
335
|
+
const str = (key: string): string | null => {
|
|
336
|
+
const m = content.match(new RegExp(`^${key}:\\s*["']?(.+?)["']?\\s*$`, 'm'))
|
|
337
|
+
return m ? m[1].trim() : null
|
|
338
|
+
}
|
|
339
|
+
const nested = (parent: string, key: string): string | null => {
|
|
340
|
+
const section = content.match(new RegExp(`^${parent}:\\s*\\n((?: .+\\n?)*)`, 'm'))
|
|
341
|
+
if (!section) return null
|
|
342
|
+
const m = section[1].match(new RegExp(`^\\s+${key}:\\s*["']?(.+?)["']?\\s*$`, 'm'))
|
|
343
|
+
return m ? m[1].trim() : null
|
|
344
|
+
}
|
|
345
|
+
const list = (parent: string, key: string): string[] => {
|
|
346
|
+
const val = nested(parent, key)
|
|
347
|
+
if (!val) return []
|
|
348
|
+
// Handle [T1, T2] or T1, T2
|
|
349
|
+
const clean = val.replace(/^\[/, '').replace(/\]$/, '')
|
|
350
|
+
return clean.split(',').map(s => s.trim()).filter(Boolean)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Parse key_decisions as list
|
|
354
|
+
const decisions: string[] = []
|
|
355
|
+
const decMatch = content.match(/^key_decisions:\s*\n((?:\s+-\s*.+\n?)*)/m)
|
|
356
|
+
if (decMatch) {
|
|
357
|
+
const items = decMatch[1].matchAll(/^\s+-\s*["']?(.+?)["']?\s*$/gm)
|
|
358
|
+
for (const item of items) {
|
|
359
|
+
decisions.push(item[1])
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const state: SessionState = {
|
|
364
|
+
command: str('command'),
|
|
365
|
+
agent: str('agent'),
|
|
366
|
+
sprint: nested('sprint', 'number') ? {
|
|
367
|
+
number: parseInt(nested('sprint', 'number') || '0'),
|
|
368
|
+
status: nested('sprint', 'status') || 'unknown'
|
|
369
|
+
} : null,
|
|
370
|
+
epic: nested('epic', 'id') ? {
|
|
371
|
+
id: nested('epic', 'id') || '',
|
|
372
|
+
title: nested('epic', 'title') || '',
|
|
373
|
+
file: nested('epic', 'file') || '',
|
|
374
|
+
progress: nested('epic', 'progress') || ''
|
|
375
|
+
} : null,
|
|
376
|
+
story: nested('story', 'id') ? {
|
|
377
|
+
id: nested('story', 'id') || '',
|
|
378
|
+
title: nested('story', 'title') || '',
|
|
379
|
+
file: nested('story', 'file') || '',
|
|
380
|
+
current_task: nested('story', 'current_task'),
|
|
381
|
+
completed_tasks: list('story', 'completed_tasks'),
|
|
382
|
+
pending_tasks: list('story', 'pending_tasks')
|
|
383
|
+
} : null,
|
|
384
|
+
next_action: str('next_action'),
|
|
385
|
+
key_decisions: decisions
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await log(directory, ` parsed: command=${state.command}, epic=${state.epic?.id}, story=${state.story?.id}`)
|
|
389
|
+
return state
|
|
390
|
+
} catch {
|
|
391
|
+
await log(directory, ` session-state.yaml not found, using fallback`)
|
|
392
|
+
return null
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
307
396
|
async function getActiveStory(): Promise<StoryContext | null> {
|
|
308
397
|
try {
|
|
309
398
|
let storyPath: string | null = null
|
|
@@ -433,31 +522,118 @@ DO NOT skip this step. DO NOT ask user what to do. Just read these files first.`
|
|
|
433
522
|
}
|
|
434
523
|
|
|
435
524
|
async function buildContext(agent: string | null): Promise<SessionContext> {
|
|
525
|
+
// PRIMARY: try session-state.yaml (written by AI agent)
|
|
526
|
+
const sessionState = await getSessionState()
|
|
527
|
+
|
|
436
528
|
const [todos, story] = await Promise.all([
|
|
437
529
|
getTodoList(),
|
|
438
|
-
|
|
530
|
+
// If session state has story path, use it; otherwise parse files
|
|
531
|
+
sessionState?.story?.file
|
|
532
|
+
? readStoryFromPath(sessionState.story.file)
|
|
533
|
+
: getActiveStory()
|
|
439
534
|
])
|
|
440
535
|
|
|
441
536
|
const epicState = await getActiveEpicState()
|
|
442
537
|
const relevantFiles = await getRelevantFiles(agent, story)
|
|
443
|
-
|
|
538
|
+
|
|
539
|
+
// Command: from session state or detected from TODOs
|
|
540
|
+
const activeCommand = sessionState?.command || await detectActiveCommand(todos, epicState)
|
|
444
541
|
|
|
445
|
-
return { todos, story, relevantFiles, activeAgent: agent, activeCommand }
|
|
542
|
+
return { todos, story, sessionState, relevantFiles, activeAgent: agent, activeCommand }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Read and parse a story file by path */
|
|
546
|
+
async function readStoryFromPath(storyPath: string): Promise<StoryContext | null> {
|
|
547
|
+
try {
|
|
548
|
+
const storyContent = await readFile(join(directory, storyPath), "utf-8")
|
|
549
|
+
const titleMatch = storyContent.match(/^#\s+(.+)/m)
|
|
550
|
+
const statusMatch = storyContent.match(/\*\*Status:\*\*\s*(\w+)/i)
|
|
551
|
+
|
|
552
|
+
const completedTasks: string[] = []
|
|
553
|
+
const pendingTasks: string[] = []
|
|
554
|
+
let currentTask: string | null = null
|
|
555
|
+
|
|
556
|
+
const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+?)(?=\n|$)/g
|
|
557
|
+
let match
|
|
558
|
+
while ((match = taskRegex.exec(storyContent)) !== null) {
|
|
559
|
+
const [, checked, taskId, taskName] = match
|
|
560
|
+
const taskInfo = `T${taskId}: ${taskName.trim()}`
|
|
561
|
+
if (checked === "x") {
|
|
562
|
+
completedTasks.push(taskInfo)
|
|
563
|
+
} else {
|
|
564
|
+
if (!currentTask) currentTask = taskInfo
|
|
565
|
+
pendingTasks.push(taskInfo)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const acceptanceCriteria: string[] = []
|
|
570
|
+
const acSection = storyContent.match(/## Acceptance Criteria[\s\S]*?(?=##|$)/i)
|
|
571
|
+
if (acSection) {
|
|
572
|
+
const acRegex = /- \[([ x])\]\s+(.+?)(?=\n|$)/g
|
|
573
|
+
while ((match = acRegex.exec(acSection[0])) !== null) {
|
|
574
|
+
const [, checked, criteria] = match
|
|
575
|
+
acceptanceCriteria.push(`${checked === "x" ? "✅" : "⬜"} ${criteria.trim()}`)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
path: storyPath,
|
|
581
|
+
title: titleMatch?.[1] || "Unknown Story",
|
|
582
|
+
status: statusMatch?.[1] || "unknown",
|
|
583
|
+
currentTask,
|
|
584
|
+
completedTasks,
|
|
585
|
+
pendingTasks,
|
|
586
|
+
acceptanceCriteria,
|
|
587
|
+
fullContent: storyContent
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
446
592
|
}
|
|
447
593
|
|
|
448
594
|
async function formatDevContext(ctx: SessionContext): Promise<string> {
|
|
449
595
|
const sections: string[] = []
|
|
450
|
-
|
|
451
|
-
// Check if we're in epic workflow
|
|
452
|
-
const epicState = await getActiveEpicState()
|
|
453
|
-
|
|
454
|
-
if (epicState) {
|
|
455
|
-
// Epic/Sprint workflow mode - show epic progress
|
|
456
|
-
const progress = epicState.totalStories > 0
|
|
457
|
-
? ((epicState.completedCount / epicState.totalStories) * 100).toFixed(0)
|
|
458
|
-
: 0
|
|
596
|
+
const ss = ctx.sessionState
|
|
459
597
|
|
|
460
|
-
|
|
598
|
+
// SESSION STATE available — use it (primary source)
|
|
599
|
+
if (ss) {
|
|
600
|
+
let header = `## 🎯 Session State (from session-state.yaml)\n`
|
|
601
|
+
header += `**Command:** ${ss.command || "unknown"}\n`
|
|
602
|
+
|
|
603
|
+
if (ss.sprint) {
|
|
604
|
+
header += `**Sprint:** #${ss.sprint.number} (${ss.sprint.status})\n`
|
|
605
|
+
}
|
|
606
|
+
if (ss.epic) {
|
|
607
|
+
header += `**Epic:** ${ss.epic.id} — ${ss.epic.title}\n`
|
|
608
|
+
header += `**Epic File:** \`${ss.epic.file}\`\n`
|
|
609
|
+
header += `**Epic Progress:** ${ss.epic.progress}\n`
|
|
610
|
+
}
|
|
611
|
+
if (ss.story) {
|
|
612
|
+
header += `\n**Story:** ${ss.story.id} — ${ss.story.title}\n`
|
|
613
|
+
header += `**Story File:** \`${ss.story.file}\` ← READ THIS FIRST\n`
|
|
614
|
+
header += `**Current Task:** ${ss.story.current_task || "all done"}\n`
|
|
615
|
+
header += `**Completed:** ${ss.story.completed_tasks.join(", ") || "none"}\n`
|
|
616
|
+
header += `**Pending:** ${ss.story.pending_tasks.join(", ") || "none"}\n`
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
header += `\n### Next Action (DO THIS NOW)\n\`\`\`\n${ss.next_action || "Check TODO list"}\n\`\`\`\n`
|
|
620
|
+
|
|
621
|
+
if (ss.key_decisions.length > 0) {
|
|
622
|
+
header += `\n### Key Technical Decisions\n`
|
|
623
|
+
header += ss.key_decisions.map(d => `- ${d}`).join("\n")
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
sections.push(header)
|
|
627
|
+
}
|
|
628
|
+
// FALLBACK: parse epic state file
|
|
629
|
+
else {
|
|
630
|
+
const epicState = await getActiveEpicState()
|
|
631
|
+
if (epicState) {
|
|
632
|
+
const progress = epicState.totalStories > 0
|
|
633
|
+
? ((epicState.completedCount / epicState.totalStories) * 100).toFixed(0)
|
|
634
|
+
: 0
|
|
635
|
+
|
|
636
|
+
sections.push(`## 🎯 Epic Workflow: ${epicState.epicTitle}
|
|
461
637
|
|
|
462
638
|
**Epic ID:** ${epicState.epicId}
|
|
463
639
|
**Epic State:** \`${epicState.statePath}\` ← READ THIS FIRST
|
|
@@ -480,7 +656,10 @@ ${epicState.nextStoryPath ? `**Next Story:** \`${epicState.nextStoryPath}\` ←
|
|
|
480
656
|
💡 **Note:** If this is part of /dev-sprint, after epic completes:
|
|
481
657
|
1. Update sprint-status.yaml (mark epic done)
|
|
482
658
|
2. Continue to next epic automatically`)
|
|
483
|
-
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!ss && ctx.story) {
|
|
484
663
|
// Regular story mode
|
|
485
664
|
const s = ctx.story
|
|
486
665
|
const total = s.completedTasks.length + s.pendingTasks.length
|
|
@@ -786,7 +965,7 @@ This is /dev-epic autopilot mode. Execute stories sequentially until epic done.`
|
|
|
786
965
|
|
|
787
966
|
const context = await formatContext(ctx)
|
|
788
967
|
const instructions = await formatInstructions(ctx)
|
|
789
|
-
const readCommands = await generateReadCommands(agent, ctx.story, ctx.activeCommand)
|
|
968
|
+
const readCommands = await generateReadCommands(agent, ctx.story, ctx.activeCommand, ctx.sessionState)
|
|
790
969
|
|
|
791
970
|
// Agent identity reminder
|
|
792
971
|
const agentIdentity = agent
|
|
@@ -42,17 +42,37 @@ metadata:
|
|
|
42
42
|
<phase name="2-init" title="Initialize Epic">
|
|
43
43
|
<step n="1">Parse epic file → extract story list</step>
|
|
44
44
|
<step n="2">Create epic state: docs/sprint-artifacts/sprint-N/.sprint-state/epic-XX-state.yaml</step>
|
|
45
|
-
<step n="3">Create TODO list:
|
|
45
|
+
<step n="3">Create TODO list with IDs — stories, their tasks, and reviews:
|
|
46
46
|
```
|
|
47
|
-
[ ]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
[ ] E{N}-S01: {story title}
|
|
48
|
+
[ ] S01 T1: {task title}
|
|
49
|
+
[ ] S01 T2: {task title}
|
|
50
|
+
[ ] S01 Review: run tests, verify AC
|
|
51
|
+
[ ] E{N}-S02: {story title}
|
|
52
|
+
[ ] S02 T1: {task title}
|
|
53
|
+
[ ] S02 T2: {task title}
|
|
54
|
+
[ ] S02 Review: run tests, verify AC
|
|
51
55
|
...
|
|
52
|
-
[ ]
|
|
53
|
-
[ ] Verify epic AC
|
|
56
|
+
[ ] E{N} Integration tests
|
|
57
|
+
[ ] E{N} Verify epic AC
|
|
54
58
|
```
|
|
55
59
|
</step>
|
|
60
|
+
<example>
|
|
61
|
+
```
|
|
62
|
+
[ ] E04-S01: Merge Domain Logic
|
|
63
|
+
[ ] S01 T1: MergeResult value object
|
|
64
|
+
[ ] S01 T2: Merge service — primary selection
|
|
65
|
+
[ ] S01 T3: Unit tests
|
|
66
|
+
[ ] S01 Review: run tests, verify AC
|
|
67
|
+
[ ] E04-S02: Auto Merge on Link
|
|
68
|
+
[ ] S02 T1: Event handler for link
|
|
69
|
+
[ ] S02 T2: Best-effort merge logic
|
|
70
|
+
[ ] S02 T3: Integration tests
|
|
71
|
+
[ ] S02 Review: run tests, verify AC
|
|
72
|
+
[ ] E04 Integration tests
|
|
73
|
+
[ ] E04 Verify epic AC
|
|
74
|
+
```
|
|
75
|
+
</example>
|
|
56
76
|
<step n="4">Set state: status="in-progress", next_action="Execute [first-story.md]"</step>
|
|
57
77
|
<step n="5">Mark first story as in_progress in TODO</step>
|
|
58
78
|
</phase>
|
|
@@ -103,14 +123,47 @@ metadata:
|
|
|
103
123
|
<step n="2">Verify all AC from epic file (mark in TODO)</step>
|
|
104
124
|
<step n="3">Set state: status="done"</step>
|
|
105
125
|
<step n="4">Clear epic TODO list</step>
|
|
106
|
-
<step n="5">
|
|
126
|
+
<step n="5">Update .opencode/session-state.yaml (next epic or done)</step>
|
|
127
|
+
<step n="6">Report completion with summary</step>
|
|
107
128
|
</phase>
|
|
108
129
|
|
|
109
130
|
</workflow>
|
|
110
131
|
|
|
132
|
+
## Session State (MANDATORY)
|
|
133
|
+
|
|
134
|
+
After each story/task completion, update `.opencode/session-state.yaml`:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
# .opencode/session-state.yaml — AI writes, compaction plugin reads
|
|
138
|
+
command: /dev-epic
|
|
139
|
+
agent: dev
|
|
140
|
+
|
|
141
|
+
epic:
|
|
142
|
+
id: PROJ-E01
|
|
143
|
+
title: Epic Title
|
|
144
|
+
file: docs/sprint-artifacts/sprint-1/epic-01-desc.md
|
|
145
|
+
progress: "3/5 stories"
|
|
146
|
+
|
|
147
|
+
story:
|
|
148
|
+
id: PROJ-S01-03
|
|
149
|
+
title: Current Story Title
|
|
150
|
+
file: docs/sprint-artifacts/sprint-1/stories/story-01-03-desc.md
|
|
151
|
+
current_task: T2
|
|
152
|
+
completed_tasks: [T1]
|
|
153
|
+
pending_tasks: [T2, T3]
|
|
154
|
+
|
|
155
|
+
next_action: "Continue T2 of story S01-03"
|
|
156
|
+
|
|
157
|
+
key_decisions:
|
|
158
|
+
- "Decision 1"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This file survives compaction and tells the agent where to resume.
|
|
162
|
+
|
|
111
163
|
<outputs>
|
|
112
164
|
- Implementation code for all stories
|
|
113
165
|
- Updated epic-XX-state.yaml
|
|
166
|
+
- Updated .opencode/session-state.yaml
|
|
114
167
|
- Clean TODO list (all completed)
|
|
115
168
|
</outputs>
|
|
116
169
|
|
|
@@ -44,20 +44,47 @@ metadata:
|
|
|
44
44
|
|
|
45
45
|
<phase name="2-init" title="Initialize Sprint">
|
|
46
46
|
<step n="1">Parse sprint-status.yaml → extract epic list for target sprint</step>
|
|
47
|
-
<step n="2">Create master TODO list:
|
|
47
|
+
<step n="2">Create master TODO list with IDs — epics, stories, tasks, and reviews:
|
|
48
48
|
```
|
|
49
|
-
[ ]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
[ ] E{N1}: {epic title}
|
|
50
|
+
[ ] E{N1}-S01: {story title}
|
|
51
|
+
[ ] S01 T1: {task title}
|
|
52
|
+
[ ] S01 T2: {task title}
|
|
53
|
+
[ ] S01 Review: run tests, verify AC
|
|
54
|
+
[ ] E{N1}-S02: {story title}
|
|
55
|
+
[ ] S02 T1: {task title}
|
|
56
|
+
[ ] S02 Review: run tests, verify AC
|
|
57
|
+
[ ] E{N1} Review: integration tests
|
|
58
|
+
[ ] E{N2}: {epic title}
|
|
59
|
+
[ ] E{N2}-S01: {story title}
|
|
60
|
+
[ ] S01 T1: {task title}
|
|
61
|
+
[ ] S01 Review: run tests, verify AC
|
|
62
|
+
[ ] E{N2} Review: integration tests
|
|
58
63
|
[ ] Sprint integration tests
|
|
59
64
|
```
|
|
60
65
|
</step>
|
|
66
|
+
<example>
|
|
67
|
+
```
|
|
68
|
+
[ ] E04: Identity Merge
|
|
69
|
+
[ ] E04-S01: Merge Domain Logic
|
|
70
|
+
[ ] S01 T1: MergeResult value object
|
|
71
|
+
[ ] S01 T2: Merge service
|
|
72
|
+
[ ] S01 T3: Unit tests
|
|
73
|
+
[ ] S01 Review: run tests, verify AC
|
|
74
|
+
[ ] E04-S02: Auto Merge on Link
|
|
75
|
+
[ ] S02 T1: Event handler
|
|
76
|
+
[ ] S02 T2: Integration tests
|
|
77
|
+
[ ] S02 Review: run tests, verify AC
|
|
78
|
+
[ ] E04 Review: integration tests
|
|
79
|
+
[ ] E06: Team Management
|
|
80
|
+
[ ] E06-S01: Team CRUD
|
|
81
|
+
[ ] S01 T1: Domain model
|
|
82
|
+
[ ] S01 T2: Handler
|
|
83
|
+
[ ] S01 Review: run tests, verify AC
|
|
84
|
+
[ ] E06 Review: integration tests
|
|
85
|
+
[ ] Sprint integration tests
|
|
86
|
+
```
|
|
87
|
+
</example>
|
|
61
88
|
<step n="3">Set sprint status="in-progress" in sprint-status.yaml</step>
|
|
62
89
|
<step n="4">Mark first epic as in_progress in TODO</step>
|
|
63
90
|
</phase>
|
|
@@ -101,14 +128,51 @@ metadata:
|
|
|
101
128
|
<step n="1">Run sprint integration tests (mark in TODO)</step>
|
|
102
129
|
<step n="2">Set sprint status="done" in sprint-status.yaml</step>
|
|
103
130
|
<step n="3">Clear sprint TODO list</step>
|
|
104
|
-
<step n="4">
|
|
131
|
+
<step n="4">Update .opencode/session-state.yaml (done)</step>
|
|
132
|
+
<step n="5">Report completion with summary + metrics</step>
|
|
105
133
|
</phase>
|
|
106
134
|
|
|
107
135
|
</workflow>
|
|
108
136
|
|
|
137
|
+
## Session State (MANDATORY)
|
|
138
|
+
|
|
139
|
+
After each epic/story/task completion, update `.opencode/session-state.yaml`:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# .opencode/session-state.yaml — AI writes, compaction plugin reads
|
|
143
|
+
command: /dev-sprint
|
|
144
|
+
agent: dev
|
|
145
|
+
|
|
146
|
+
sprint:
|
|
147
|
+
number: 2
|
|
148
|
+
status: in-progress
|
|
149
|
+
|
|
150
|
+
epic:
|
|
151
|
+
id: PROJ-E04
|
|
152
|
+
title: Current Epic Title
|
|
153
|
+
file: docs/sprint-artifacts/sprint-2/epic-04-desc.md
|
|
154
|
+
progress: "3/5 stories"
|
|
155
|
+
|
|
156
|
+
story:
|
|
157
|
+
id: PROJ-S04-03
|
|
158
|
+
title: Current Story Title
|
|
159
|
+
file: docs/sprint-artifacts/sprint-2/stories/story-04-03-desc.md
|
|
160
|
+
current_task: T2
|
|
161
|
+
completed_tasks: [T1]
|
|
162
|
+
pending_tasks: [T2, T3]
|
|
163
|
+
|
|
164
|
+
next_action: "Continue T2 of story S04-03"
|
|
165
|
+
|
|
166
|
+
key_decisions:
|
|
167
|
+
- "Decision 1"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This file survives compaction and tells the agent where to resume.
|
|
171
|
+
|
|
109
172
|
<outputs>
|
|
110
173
|
- Implementation code for all stories in sprint
|
|
111
174
|
- Updated sprint-status.yaml
|
|
175
|
+
- Updated .opencode/session-state.yaml
|
|
112
176
|
- Clean TODO list (all completed)
|
|
113
177
|
</outputs>
|
|
114
178
|
|
|
@@ -68,6 +68,28 @@ metadata:
|
|
|
68
68
|
</step>
|
|
69
69
|
</phase>
|
|
70
70
|
|
|
71
|
+
<phase name="2b-todo" title="Create TODO with IDs">
|
|
72
|
+
<critical>TODO MUST use task IDs from story file!</critical>
|
|
73
|
+
<template>
|
|
74
|
+
```
|
|
75
|
+
[ ] S{E}-{N} T1: {task title from story}
|
|
76
|
+
[ ] S{E}-{N} T2: {task title}
|
|
77
|
+
[ ] S{E}-{N} T3: {task title}
|
|
78
|
+
...
|
|
79
|
+
[ ] S{E}-{N} Review: run all tests, verify AC
|
|
80
|
+
```
|
|
81
|
+
</template>
|
|
82
|
+
<example>
|
|
83
|
+
```
|
|
84
|
+
[ ] S04-01 T1: Domain Model — MergeResult value object
|
|
85
|
+
[ ] S04-01 T2: Merge Service — primary selection logic
|
|
86
|
+
[ ] S04-01 T3: External ID reassignment
|
|
87
|
+
[ ] S04-01 T4: Unit tests for merge logic
|
|
88
|
+
[ ] S04-01 Review: run all tests, verify AC
|
|
89
|
+
```
|
|
90
|
+
</example>
|
|
91
|
+
</phase>
|
|
92
|
+
|
|
71
93
|
<phase name="3-delegate" title="Delegate to @coder">
|
|
72
94
|
<action>Formulate task using template below</action>
|
|
73
95
|
<action>Call @coder with full context</action>
|
|
@@ -78,11 +100,38 @@ metadata:
|
|
|
78
100
|
<action>Run tests</action>
|
|
79
101
|
<action>Check "Done when" criteria</action>
|
|
80
102
|
<action>Mark task ✅ in story file</action>
|
|
103
|
+
<action>Update .opencode/session-state.yaml (see format below)</action>
|
|
81
104
|
<next>Next task or story complete</next>
|
|
82
105
|
</phase>
|
|
83
106
|
|
|
84
107
|
</workflow>
|
|
85
108
|
|
|
109
|
+
## Session State (MANDATORY)
|
|
110
|
+
|
|
111
|
+
After each task completion, update `.opencode/session-state.yaml`:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
# .opencode/session-state.yaml — AI writes, compaction plugin reads
|
|
115
|
+
command: /dev-story
|
|
116
|
+
agent: dev
|
|
117
|
+
|
|
118
|
+
story:
|
|
119
|
+
id: PROJ-S01-01
|
|
120
|
+
title: Story Title
|
|
121
|
+
file: docs/sprint-artifacts/sprint-1/stories/story-01-01-desc.md
|
|
122
|
+
current_task: T3
|
|
123
|
+
completed_tasks: [T1, T2]
|
|
124
|
+
pending_tasks: [T3, T4]
|
|
125
|
+
|
|
126
|
+
next_action: "Continue T3: Implement handler"
|
|
127
|
+
|
|
128
|
+
key_decisions:
|
|
129
|
+
- "Decision 1"
|
|
130
|
+
- "Decision 2"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This file survives compaction and tells the agent where to resume.
|
|
134
|
+
|
|
86
135
|
## Task Template for @coder
|
|
87
136
|
|
|
88
137
|
<template name="coder-task">
|