@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/workflow",
3
- "version": "4.38.1-dev.3",
3
+ "version": "4.38.1-dev.5",
4
4
  "description": "Initialize OpenCode Workflow system for AI-assisted development with semantic code search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "4.38.1-dev.3",
3
- "buildDate": "2026-01-27T10:14:50.120Z",
2
+ "version": "4.38.1-dev.5",
3
+ "buildDate": "2026-01-27T10:42:36.948Z",
4
4
  "files": [
5
5
  "config.yaml",
6
6
  "FLOW.yaml",
@@ -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 epic state file if in epic workflow
193
+ // For dev/coder: add session-state.yaml as priority read
183
194
  if ((agentKey === "dev" || agentKey === "coder")) {
184
- const epicState = await getActiveEpicState()
185
- if (epicState) {
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 if active
191
- if (story) {
192
- filesToRead.unshift(story.path) // Story first!
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
- getActiveStory()
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
- const activeCommand = await detectActiveCommand(todos, epicState)
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
- sections.push(`## 🎯 Epic Workflow: ${epicState.epicTitle}
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
- } else if (ctx.story) {
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
- [ ] Story 1: [Title]
48
- [ ] Review Story 1
49
- [ ] Story 2: [Title]
50
- [ ] Review Story 2
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
- [ ] Epic integration tests
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">Report completion with summary</step>
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
- [ ] Epic 1: [Title]
50
- [ ] Story 1-1: [Title]
51
- [ ] Review Story 1-1
52
- [ ] Story 1-2: [Title]
53
- [ ] Review Story 1-2
54
- [ ] Review Epic 1
55
- [ ] Epic 2: [Title]
56
- [ ] Story 2-1: [Title]
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">Report completion with summary + metrics</step>
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">