@comfanion/workflow 4.38.1-dev.1 → 4.38.1-dev.12

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.1",
3
+ "version": "4.38.1-dev.12",
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.1",
3
- "buildDate": "2026-01-27T01:03:19.105Z",
2
+ "version": "4.38.1-dev.12",
3
+ "buildDate": "2026-01-27T11:55:26.654Z",
4
4
  "files": [
5
5
  "config.yaml",
6
6
  "FLOW.yaml",
@@ -6,7 +6,7 @@ temperature: 0.1 # Low temperature for precise analysis
6
6
  #model: openai/gpt-5.2-codex # Best at finding bugs and security issues
7
7
  model: anthropic/claude-sonnet-4-5 # Best at finding bugs and security issues
8
8
 
9
- # Tools - Read-only for review (no writes)
9
+ # Tools - Read-only for code, but CAN write review findings to story/epic files
10
10
  tools:
11
11
  read: true
12
12
  glob: true
@@ -18,12 +18,14 @@ tools:
18
18
  bash: true # For running tests
19
19
  todowrite: false # Reviewer doesn't manage todos
20
20
  todoread: true
21
- edit: false # Reviewer doesn't edit code
22
- write: false # Reviewer doesn't write files
21
+ edit: true # To append ## Review section to story/epic files
22
+ write: false # Reviewer doesn't write new files
23
23
 
24
- # Permissions - read-only analysis
24
+ # Permissions - read-only for code, write ONLY to story/epic docs
25
25
  permission:
26
- edit: deny # Reviewer only reports, doesn't fix
26
+ edit:
27
+ "docs/sprint-artifacts/**/*.md": allow # Story and epic files
28
+ "*": deny # Everything else read-only
27
29
  bash:
28
30
  "*": deny
29
31
  # Tests
@@ -116,10 +118,17 @@ permission:
116
118
  <action>If failures → include in review report as HIGH priority</action>
117
119
  </phase>
118
120
 
119
- <phase name="6. Report">
120
- <action>Categorize issues: High/Medium/Low</action>
121
- <action>Provide specific fixes for each issue</action>
122
- <action>Return verdict: APPROVE | CHANGES_REQUESTED | BLOCKED</action>
121
+ <phase name="6. Write to Story File">
122
+ <action>Append `### Review #N` block to the story file's `## Review` section (see code-review skill for format)</action>
123
+ <action>Determine N by counting existing `### Review #` blocks + 1</action>
124
+ <action>Include: verdict, summary, test/lint results, action items with file:line</action>
125
+ <critical>NEVER overwrite previous reviews — always APPEND. History is preserved for analytics.</critical>
126
+ </phase>
127
+
128
+ <phase name="7. Return Summary to Caller">
129
+ <action>Return SHORT summary so calling agent does NOT re-read the story file</action>
130
+ <action>Format: verdict + action items list (caller uses this directly)</action>
131
+ <critical>Caller (@dev) uses YOUR output, not the file. Keep it actionable.</critical>
123
132
  </phase>
124
133
  </workflow>
125
134
 
@@ -220,38 +229,33 @@ permission:
220
229
  </category>
221
230
  </review_checklist>
222
231
 
223
- <output_format>
224
- ## Code Review: {{story_title}}
225
-
226
- **Reviewer:** @reviewer (Marcus)
227
- **Date:** {{date}}
228
- **Model:** GPT-5.2 Codex
229
-
230
- ### Verdict: {{APPROVE | CHANGES_REQUESTED | BLOCKED}}
231
-
232
- ### Summary
233
- {{1-2 sentence summary}}
234
-
235
- ### Issues Found
236
-
237
- #### HIGH Priority (Must Fix)
238
- - **[Security]** `path/file.ts:42` - {{issue}}
239
- - **Fix:** {{specific fix}}
240
-
241
- #### MEDIUM Priority (Should Fix)
242
- - **[Performance]** `path/file.ts:100` - {{issue}}
243
- - **Fix:** {{specific fix}}
244
-
245
- #### LOW Priority (Nice to Have)
246
- - **[Style]** `path/file.ts:15` - {{issue}}
247
-
248
- ### What's Good
249
- - {{positive feedback}}
250
-
251
- ### Action Items
252
- - [ ] [HIGH] Fix {{issue}}
253
- - [ ] [MED] Add {{test/improvement}}
254
- </output_format>
232
+ <output_format hint="TWO outputs: file + return summary">
233
+
234
+ <file_output hint="Appended to story file ## Review section — full details for analytics">
235
+ ### Review #{{N}} — {{YYYY-MM-DD}}
236
+ **Verdict:** {{APPROVE | CHANGES_REQUESTED | BLOCKED}}
237
+ **Reviewer:** @reviewer (Marcus)
238
+ **Summary:** {{1-2 sentences}}
239
+ **Tests:** {{PASS | FAIL details}}
240
+ **Lint:** {{PASS | FAIL — details}}
241
+ #### Action Items (if CHANGES_REQUESTED/BLOCKED)
242
+ - [ ] [HIGH] `path/file.ts:42` — {{issue}} Fix: {{fix}}
243
+ - [ ] [MED] `path/file.ts:100` — {{issue}} → Fix: {{fix}}
244
+ #### What's Good (if APPROVE)
245
+ - {{positive feedback}}
246
+ </file_output>
247
+
248
+ <return_summary hint="Returned to calling agent — short, actionable, NO re-read needed">
249
+ **VERDICT: {{APPROVE | CHANGES_REQUESTED | BLOCKED}}**
250
+ {{IF CHANGES_REQUESTED or BLOCKED:}}
251
+ Action items:
252
+ - [HIGH] `path/file.ts:42` — {{issue}} → {{fix}}
253
+ - [MED] `path/file.ts:100` — {{issue}} → {{fix}}
254
+ {{IF APPROVE:}}
255
+ All good. No issues found.
256
+ </return_summary>
257
+
258
+ </output_format>
255
259
 
256
260
  </agent>
257
261
 
@@ -269,4 +273,7 @@ permission:
269
273
  - Make architecture decisions (→ @architect)
270
274
  - Write documentation (→ @pm)
271
275
 
276
+ **What I Write:**
277
+ - `## Review` section in story files (append history: Review #1, #2, ...)
278
+
272
279
  **My Model:** GPT-5.2 Codex (best at finding bugs)
@@ -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,85 +321,113 @@ 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
- // First, try to find epic state file
398
+ let storyPath: string | null = null
399
+
400
+ // First, try epic state file for story path
310
401
  const epicState = await getActiveEpicState()
311
- if (epicState) {
312
- // Parse epic state to get current story
313
- const storyPathMatch = epicState.content.match(/next_action:\s*["']?Execute\s+(.+?)["']?$/m)
314
- if (storyPathMatch) {
315
- const storyFileName = storyPathMatch[1]
316
- // Find story file
317
- const sprintMatch = epicState.statePath.match(/sprint-(\d+)/)
318
- if (sprintMatch) {
319
- const storyPath = `docs/sprint-artifacts/sprint-${sprintMatch[1]}/stories/${storyFileName}`
320
- const storyContent = await readFile(join(directory, storyPath), "utf-8")
321
-
322
- const titleMatch = storyContent.match(/^#\s+(.+)/m)
323
- const statusMatch = storyContent.match(/\*\*Status:\*\*\s*(\w+)/i)
324
-
325
- const completedTasks: string[] = []
326
- const pendingTasks: string[] = []
327
- let currentTask: string | null = null
328
-
329
- // Parse tasks with more detail
330
- const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+?)(?=\n|$)/g
331
- let match
332
- while ((match = taskRegex.exec(storyContent)) !== null) {
333
- const [, checked, taskId, taskName] = match
334
- const taskInfo = `T${taskId}: ${taskName.trim()}`
335
- if (checked === "x") {
336
- completedTasks.push(taskInfo)
337
- } else {
338
- if (!currentTask) currentTask = taskInfo
339
- pendingTasks.push(taskInfo)
340
- }
341
- }
342
-
343
- // Parse acceptance criteria
344
- const acceptanceCriteria: string[] = []
345
- const acSection = storyContent.match(/## Acceptance Criteria[\s\S]*?(?=##|$)/i)
346
- if (acSection) {
347
- const acRegex = /- \[([ x])\]\s+(.+?)(?=\n|$)/g
348
- while ((match = acRegex.exec(acSection[0])) !== null) {
349
- const [, checked, criteria] = match
350
- acceptanceCriteria.push(`${checked === "x" ? "✅" : "⬜"} ${criteria.trim()}`)
351
- }
352
- }
402
+ if (epicState?.nextStoryPath) {
403
+ storyPath = epicState.nextStoryPath
404
+ }
353
405
 
354
- return {
355
- path: storyPath,
356
- title: titleMatch?.[1] || "Unknown Story",
357
- status: statusMatch?.[1] || "unknown",
358
- currentTask,
359
- completedTasks,
360
- pendingTasks,
361
- acceptanceCriteria,
362
- fullContent: storyContent
363
- }
406
+ // Fallback: try sprint-status.yaml
407
+ if (!storyPath) {
408
+ try {
409
+ const sprintStatusPath = join(directory, "docs", "sprint-artifacts", "sprint-status.yaml")
410
+ const content = await readFile(sprintStatusPath, "utf-8")
411
+ const inProgressMatch = content.match(/status:\s*in-progress[\s\S]*?path:\s*["']?([^"'\n]+)["']?/i)
412
+ if (inProgressMatch) {
413
+ storyPath = inProgressMatch[1]
364
414
  }
415
+ } catch {
416
+ // No sprint-status.yaml
365
417
  }
366
418
  }
367
-
368
- // Fallback: try old sprint-status.yaml format
369
- const sprintStatusPath = join(directory, "docs", "sprint-artifacts", "sprint-status.yaml")
370
- const content = await readFile(sprintStatusPath, "utf-8")
371
-
372
- const inProgressMatch = content.match(/status:\s*in-progress[\s\S]*?path:\s*["']?([^"'\n]+)["']?/i)
373
- if (!inProgressMatch) return null
374
419
 
375
- const storyPath = inProgressMatch[1]
420
+ if (!storyPath) return null
421
+
422
+ // Parse story file
376
423
  const storyContent = await readFile(join(directory, storyPath), "utf-8")
377
-
378
424
  const titleMatch = storyContent.match(/^#\s+(.+)/m)
379
425
  const statusMatch = storyContent.match(/\*\*Status:\*\*\s*(\w+)/i)
380
-
426
+
381
427
  const completedTasks: string[] = []
382
428
  const pendingTasks: string[] = []
383
429
  let currentTask: string | null = null
384
-
385
- // Parse tasks with more detail
430
+
386
431
  const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+?)(?=\n|$)/g
387
432
  let match
388
433
  while ((match = taskRegex.exec(storyContent)) !== null) {
@@ -395,8 +440,7 @@ DO NOT skip this step. DO NOT ask user what to do. Just read these files first.`
395
440
  pendingTasks.push(taskInfo)
396
441
  }
397
442
  }
398
-
399
- // Parse acceptance criteria
443
+
400
444
  const acceptanceCriteria: string[] = []
401
445
  const acSection = storyContent.match(/## Acceptance Criteria[\s\S]*?(?=##|$)/i)
402
446
  if (acSection) {
@@ -478,31 +522,118 @@ DO NOT skip this step. DO NOT ask user what to do. Just read these files first.`
478
522
  }
479
523
 
480
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
+
481
528
  const [todos, story] = await Promise.all([
482
529
  getTodoList(),
483
- getActiveStory()
530
+ // If session state has story path, use it; otherwise parse files
531
+ sessionState?.story?.file
532
+ ? readStoryFromPath(sessionState.story.file)
533
+ : getActiveStory()
484
534
  ])
485
535
 
486
536
  const epicState = await getActiveEpicState()
487
537
  const relevantFiles = await getRelevantFiles(agent, story)
488
- 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)
489
541
 
490
- 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
+ }
491
592
  }
492
593
 
493
594
  async function formatDevContext(ctx: SessionContext): Promise<string> {
494
595
  const sections: string[] = []
495
-
496
- // Check if we're in epic workflow
497
- const epicState = await getActiveEpicState()
498
-
499
- if (epicState) {
500
- // Epic/Sprint workflow mode - show epic progress
501
- const progress = epicState.totalStories > 0
502
- ? ((epicState.completedCount / epicState.totalStories) * 100).toFixed(0)
503
- : 0
596
+ const ss = ctx.sessionState
597
+
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
504
635
 
505
- sections.push(`## 🎯 Epic Workflow: ${epicState.epicTitle}
636
+ sections.push(`## 🎯 Epic Workflow: ${epicState.epicTitle}
506
637
 
507
638
  **Epic ID:** ${epicState.epicId}
508
639
  **Epic State:** \`${epicState.statePath}\` ← READ THIS FIRST
@@ -525,7 +656,10 @@ ${epicState.nextStoryPath ? `**Next Story:** \`${epicState.nextStoryPath}\` ←
525
656
  💡 **Note:** If this is part of /dev-sprint, after epic completes:
526
657
  1. Update sprint-status.yaml (mark epic done)
527
658
  2. Continue to next epic automatically`)
528
- } else if (ctx.story) {
659
+ }
660
+ }
661
+
662
+ if (!ss && ctx.story) {
529
663
  // Regular story mode
530
664
  const s = ctx.story
531
665
  const total = s.completedTasks.length + s.pendingTasks.length
@@ -777,6 +911,74 @@ This is /dev-epic autopilot mode. Execute stories sequentially until epic done.`
777
911
  4. Update todo/story when task complete`
778
912
  }
779
913
 
914
+ function buildBriefing(agent: string | null, ss: SessionState | null, ctx: SessionContext, readCommands: string): string {
915
+ const lines: string[] = []
916
+
917
+ // 1. WHO you are
918
+ if (agent) {
919
+ lines.push(`You are @${agent} (→ .opencode/agents/${agent}.md).`)
920
+ }
921
+
922
+ // 2. WHAT you are doing
923
+ if (ss) {
924
+ const cmd = ss.command || "unknown command"
925
+ if (ss.epic) {
926
+ lines.push(`You are executing ${cmd} ${ss.epic.id}: ${ss.epic.title}.`)
927
+ } else if (ss.story) {
928
+ lines.push(`You are executing ${cmd} ${ss.story.id}: ${ss.story.title}.`)
929
+ } else {
930
+ lines.push(`You are executing ${cmd}.`)
931
+ }
932
+ } else if (ctx.activeCommand) {
933
+ lines.push(`You are executing ${ctx.activeCommand}.`)
934
+ }
935
+
936
+ // 3. WHERE you stopped
937
+ if (ss?.story) {
938
+ const task = ss.story.current_task || "review"
939
+ lines.push(`You were on story ${ss.story.id}: ${ss.story.title}, task ${task}.`)
940
+ if (ss.story.completed_tasks.length > 0) {
941
+ lines.push(`Completed: ${ss.story.completed_tasks.join(", ")}.`)
942
+ }
943
+ if (ss.story.pending_tasks.length > 0) {
944
+ lines.push(`Remaining: ${ss.story.pending_tasks.join(", ")}.`)
945
+ }
946
+ } else if (ss?.epic) {
947
+ lines.push(`Epic progress: ${ss.epic.progress}.`)
948
+ } else if (ctx.story) {
949
+ lines.push(`You were on story: ${ctx.story.title}, task ${ctx.story.currentTask || "review"}.`)
950
+ }
951
+
952
+ // 4. WHAT to do next
953
+ if (ss?.next_action) {
954
+ lines.push(`\nNext action: ${ss.next_action}`)
955
+ }
956
+
957
+ // 5. READ these files
958
+ lines.push(`\n${readCommands}`)
959
+
960
+ // 6. KEY DECISIONS (if any)
961
+ if (ss?.key_decisions && ss.key_decisions.length > 0) {
962
+ lines.push(`\nKey decisions from your session:`)
963
+ for (const d of ss.key_decisions) {
964
+ lines.push(`- ${d}`)
965
+ }
966
+ }
967
+
968
+ // 7. TODO status (brief)
969
+ if (ctx.todos.length > 0) {
970
+ const inProgress = ctx.todos.filter(t => t.status === "in_progress")
971
+ const pending = ctx.todos.filter(t => t.status === "pending")
972
+ const completed = ctx.todos.filter(t => t.status === "completed")
973
+ lines.push(`\nTODO: ${completed.length} done, ${inProgress.length} in progress, ${pending.length} pending.`)
974
+ }
975
+
976
+ // 8. RULES
977
+ lines.push(`\nDO NOT ask user what to do. Read files above, then resume automatically.`)
978
+
979
+ return lines.join("\n")
980
+ }
981
+
780
982
  return {
781
983
  // Track active agent from chat messages
782
984
  "chat.message": async (input, output) => {
@@ -831,26 +1033,12 @@ This is /dev-epic autopilot mode. Execute stories sequentially until epic done.`
831
1033
 
832
1034
  const context = await formatContext(ctx)
833
1035
  const instructions = await formatInstructions(ctx)
834
- const readCommands = await generateReadCommands(agent, ctx.story, ctx.activeCommand)
835
-
836
- // Agent identity reminder
837
- const agentIdentity = agent
838
- ? `You are @${agent} (.opencode/agents/${agent}.md). Load your persona and continue.`
839
- : "You are an AI assistant helping with this project."
840
-
841
- output.context.push(`# Session Continuation
842
-
843
- ${agentIdentity}
844
-
845
- ${readCommands}
846
-
847
- ---
848
-
849
- ${context}
850
-
851
- ---
1036
+ const readCommands = await generateReadCommands(agent, ctx.story, ctx.activeCommand, ctx.sessionState)
852
1037
 
853
- ${instructions}`)
1038
+ // Build agentic briefing
1039
+ const ss = ctx.sessionState
1040
+ const briefing = buildBriefing(agent, ss, ctx, readCommands)
1041
+ output.context.push(briefing)
854
1042
 
855
1043
  await log(directory, ` -> output.context pushed (${output.context.length} items)`)
856
1044
  await log(directory, `=== COMPACTION DONE ===`)