@comfanion/workflow 4.36.44 → 4.36.46

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.
@@ -157,6 +157,11 @@ development:
157
157
  # STUB: Interface → Stub Implementation → Test → Full Implementation
158
158
  methodology: tdd
159
159
 
160
+ # Auto-invoke @reviewer after /dev-story completes all tasks
161
+ # When true: story tasks complete → auto /review-story → APPROVE → done
162
+ # When false: story tasks complete → status "review" → manual /review-story
163
+ auto_review: true
164
+
160
165
  # Task structure
161
166
  task:
162
167
  max_hours: 2 # Maximum hours per atomic task
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "@opencode-ai/plugin": "1.1.34"
3
+ "@opencode-ai/plugin": "1.1.35"
4
4
  }
5
5
  }
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
- import { readFile, access } from "fs/promises"
2
+ import { readFile, access, readdir } from "fs/promises"
3
3
  import { join } from "path"
4
4
 
5
5
  interface TaskStatus {
@@ -16,6 +16,8 @@ interface StoryContext {
16
16
  currentTask: string | null
17
17
  completedTasks: string[]
18
18
  pendingTasks: string[]
19
+ acceptanceCriteria: string[]
20
+ fullContent: string
19
21
  }
20
22
 
21
23
  interface SessionContext {
@@ -25,16 +27,152 @@ interface SessionContext {
25
27
  activeAgent: string | null
26
28
  }
27
29
 
30
+ // Base files ALL agents need after compaction (to remember who they are)
31
+ const BASE_FILES = [
32
+ "CLAUDE.md", // Project rules, coding standards
33
+ "AGENTS.md", // Agent personas and how they work
34
+ ]
35
+
36
+ // Agent-specific file priorities (added to BASE_FILES)
37
+ const AGENT_FILES: Record<string, string[]> = {
38
+ dev: [
39
+ ...BASE_FILES,
40
+ "docs/coding-standards/README.md",
41
+ "docs/coding-standards/patterns.md",
42
+ "docs/coding-standards/testing.md",
43
+ "docs/prd.md",
44
+ "docs/architecture.md",
45
+ // story path added dynamically
46
+ ],
47
+ coder: [
48
+ ...BASE_FILES,
49
+ "docs/coding-standards/patterns.md",
50
+ "docs/coding-standards/testing.md",
51
+ ],
52
+ architect: [
53
+ ...BASE_FILES,
54
+ "docs/architecture.md",
55
+ "docs/prd.md",
56
+ "docs/coding-standards/README.md",
57
+ "docs/architecture/adr", // directory
58
+ ],
59
+ pm: [
60
+ ...BASE_FILES,
61
+ "docs/prd.md",
62
+ "docs/architecture.md",
63
+ "docs/sprint-artifacts/sprint-status.yaml",
64
+ "docs/sprint-artifacts/backlog", // directory
65
+ ],
66
+ analyst: [
67
+ ...BASE_FILES,
68
+ "docs/requirements/requirements.md",
69
+ "docs/prd.md",
70
+ ],
71
+ researcher: [
72
+ ...BASE_FILES,
73
+ "docs/prd.md",
74
+ "docs/research", // directory
75
+ ],
76
+ crawler: [
77
+ ...BASE_FILES,
78
+ ],
79
+ "change-manager": [
80
+ ...BASE_FILES,
81
+ "docs/prd.md",
82
+ "docs/architecture.md",
83
+ ],
84
+ }
85
+
86
+ // Default files for unknown agents
87
+ const DEFAULT_FILES = [
88
+ ...BASE_FILES,
89
+ "docs/prd.md",
90
+ "docs/architecture.md",
91
+ ]
92
+
93
+ // Files agent MUST Read after compaction (commands generated)
94
+ const MUST_READ_FILES: Record<string, string[]> = {
95
+ dev: [
96
+ "AGENTS.md",
97
+ "CLAUDE.md",
98
+ "docs/prd.md",
99
+ "docs/architecture.md",
100
+ // story path added dynamically
101
+ ],
102
+ coder: [
103
+ "AGENTS.md",
104
+ "CLAUDE.md",
105
+ "docs/prd.md",
106
+ "docs/architecture.md",
107
+ ],
108
+ architect: [
109
+ "AGENTS.md",
110
+ "CLAUDE.md",
111
+ "docs/prd.md",
112
+ "docs/architecture.md",
113
+ ],
114
+ pm: [
115
+ "AGENTS.md",
116
+ "CLAUDE.md",
117
+ "docs/prd.md",
118
+ "docs/architecture.md",
119
+ ],
120
+ analyst: [
121
+ "AGENTS.md",
122
+ "CLAUDE.md",
123
+ "docs/prd.md",
124
+ ],
125
+ researcher: [
126
+ "AGENTS.md",
127
+ "CLAUDE.md",
128
+ ],
129
+ default: [
130
+ "AGENTS.md",
131
+ "CLAUDE.md",
132
+ "docs/prd.md",
133
+ "docs/architecture.md",
134
+ ],
135
+ }
136
+
28
137
  /**
29
138
  * Custom Compaction Plugin
30
139
  *
31
- * Intelligent context preservation during session compaction:
32
- * - Tracks task/story completion status
33
- * - Preserves relevant documentation files
34
- * - Generates continuation prompts for seamless resumption
140
+ * Agent-aware context preservation during session compaction:
141
+ * - Tracks active agent via chat.message hook
142
+ * - Generates MANDATORY Read commands for critical files
143
+ * - Preserves agent-specific documentation files
144
+ * - Provides detailed story/task info for dev agent
145
+ * - Generates targeted continuation prompts
35
146
  */
36
147
  export const CustomCompactionPlugin: Plugin = async (ctx) => {
37
148
  const { directory } = ctx
149
+
150
+ // Track the last active agent
151
+ let lastActiveAgent: string | null = null
152
+ let lastSessionId: string | null = null
153
+
154
+ /**
155
+ * Generate Read commands that agent MUST execute after compaction
156
+ */
157
+ function generateReadCommands(agent: string | null, story: StoryContext | null): string {
158
+ const agentKey = agent?.toLowerCase() || "default"
159
+ const filesToRead = [...(MUST_READ_FILES[agentKey] || MUST_READ_FILES.default)]
160
+
161
+ // For dev/coder: add story file if active
162
+ if ((agentKey === "dev" || agentKey === "coder") && story) {
163
+ filesToRead.unshift(story.path) // Story first!
164
+ }
165
+
166
+ const commands = filesToRead.map((f, i) => `${i + 1}. Read("${f}")`).join("\n")
167
+
168
+ return `## ⚠️ MANDATORY: Execute these Read commands FIRST
169
+
170
+ Before doing ANYTHING else, you MUST read these files to restore context:
171
+
172
+ ${commands}
173
+
174
+ DO NOT skip this step. DO NOT ask user what to do. Just read these files first.`
175
+ }
38
176
 
39
177
  async function getTodoList(): Promise<TaskStatus[]> {
40
178
  try {
@@ -64,15 +202,28 @@ export const CustomCompactionPlugin: Plugin = async (ctx) => {
64
202
  const pendingTasks: string[] = []
65
203
  let currentTask: string | null = null
66
204
 
67
- const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+)/g
205
+ // Parse tasks with more detail
206
+ const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+?)(?=\n|$)/g
68
207
  let match
69
208
  while ((match = taskRegex.exec(storyContent)) !== null) {
70
209
  const [, checked, taskId, taskName] = match
210
+ const taskInfo = `T${taskId}: ${taskName.trim()}`
71
211
  if (checked === "x") {
72
- completedTasks.push(`T${taskId}: ${taskName}`)
212
+ completedTasks.push(taskInfo)
73
213
  } else {
74
- if (!currentTask) currentTask = `T${taskId}: ${taskName}`
75
- pendingTasks.push(`T${taskId}: ${taskName}`)
214
+ if (!currentTask) currentTask = taskInfo
215
+ pendingTasks.push(taskInfo)
216
+ }
217
+ }
218
+
219
+ // Parse acceptance criteria
220
+ const acceptanceCriteria: string[] = []
221
+ const acSection = storyContent.match(/## Acceptance Criteria[\s\S]*?(?=##|$)/i)
222
+ if (acSection) {
223
+ const acRegex = /- \[([ x])\]\s+(.+?)(?=\n|$)/g
224
+ while ((match = acRegex.exec(acSection[0])) !== null) {
225
+ const [, checked, criteria] = match
226
+ acceptanceCriteria.push(`${checked === "x" ? "✅" : "⬜"} ${criteria.trim()}`)
76
227
  }
77
228
  }
78
229
 
@@ -82,106 +233,214 @@ export const CustomCompactionPlugin: Plugin = async (ctx) => {
82
233
  status: statusMatch?.[1] || "unknown",
83
234
  currentTask,
84
235
  completedTasks,
85
- pendingTasks
236
+ pendingTasks,
237
+ acceptanceCriteria,
238
+ fullContent: storyContent
86
239
  }
87
240
  } catch {
88
241
  return null
89
242
  }
90
243
  }
91
244
 
92
- async function getRelevantFiles(): Promise<string[]> {
245
+ async function getRelevantFiles(agent: string | null, story: StoryContext | null): Promise<string[]> {
93
246
  const relevantPaths: string[] = []
247
+ const agentKey = agent?.toLowerCase() || "default"
248
+ const filesToCheck = AGENT_FILES[agentKey] || DEFAULT_FILES
94
249
 
95
- const criticalFiles = [
96
- "CLAUDE.md",
97
- "AGENTS.md",
98
- "project-context.md",
99
- ".opencode/config.yaml",
100
- "docs/prd.md",
101
- "docs/architecture.md",
102
- "docs/coding-standards/README.md",
103
- "docs/coding-standards/patterns.md"
104
- ]
105
-
106
- for (const filePath of criticalFiles) {
250
+ for (const filePath of filesToCheck) {
107
251
  try {
108
- await access(join(directory, filePath))
109
- relevantPaths.push(filePath)
252
+ const fullPath = join(directory, filePath)
253
+ const stat = await access(fullPath).then(() => true).catch(() => false)
254
+ if (stat) {
255
+ // Check if it's a directory
256
+ try {
257
+ const entries = await readdir(fullPath)
258
+ // Add first 5 files from directory
259
+ for (const entry of entries.slice(0, 5)) {
260
+ if (entry.endsWith('.md') || entry.endsWith('.yaml')) {
261
+ relevantPaths.push(join(filePath, entry))
262
+ }
263
+ }
264
+ } catch {
265
+ // It's a file, add it
266
+ relevantPaths.push(filePath)
267
+ }
268
+ }
110
269
  } catch {
111
- // File doesn't exist, skip
270
+ // File/dir doesn't exist, skip
112
271
  }
113
272
  }
114
273
 
115
- const story = await getActiveStory()
116
- if (story) {
117
- relevantPaths.push(story.path)
274
+ // Always add story path for dev/coder
275
+ if (story && (agentKey === "dev" || agentKey === "coder")) {
276
+ if (!relevantPaths.includes(story.path)) {
277
+ relevantPaths.unshift(story.path) // Add at beginning
278
+ }
118
279
  }
119
280
 
120
281
  return relevantPaths
121
282
  }
122
283
 
123
- async function buildContext(): Promise<SessionContext> {
124
- const [todos, story, relevantFiles] = await Promise.all([
284
+ async function buildContext(agent: string | null): Promise<SessionContext> {
285
+ const [todos, story] = await Promise.all([
125
286
  getTodoList(),
126
- getActiveStory(),
127
- getRelevantFiles()
287
+ getActiveStory()
128
288
  ])
289
+
290
+ const relevantFiles = await getRelevantFiles(agent, story)
129
291
 
130
- return { todos, story, relevantFiles, activeAgent: null }
292
+ return { todos, story, relevantFiles, activeAgent: agent }
131
293
  }
132
294
 
133
- function formatContext(ctx: SessionContext): string {
295
+ function formatDevContext(ctx: SessionContext): string {
134
296
  const sections: string[] = []
297
+
298
+ if (ctx.story) {
299
+ const s = ctx.story
300
+ const total = s.completedTasks.length + s.pendingTasks.length
301
+ const progress = total > 0 ? (s.completedTasks.length / total * 100).toFixed(0) : 0
302
+
303
+ sections.push(`## 🎯 Active Story: ${s.title}
304
+
305
+ **Path:** \`${s.path}\` ← READ THIS FIRST
306
+ **Status:** ${s.status}
307
+ **Progress:** ${progress}% (${s.completedTasks.length}/${total} tasks)
308
+
309
+ ### Current Task (DO THIS NOW)
310
+ \`\`\`
311
+ ${s.currentTask || "All tasks complete - run final tests"}
312
+ \`\`\`
313
+
314
+ ### Task Breakdown
315
+ **Completed:**
316
+ ${s.completedTasks.length > 0 ? s.completedTasks.map(t => `✅ ${t}`).join("\n") : "None yet"}
317
+
318
+ **Remaining:**
319
+ ${s.pendingTasks.length > 0 ? s.pendingTasks.map(t => `⬜ ${t}`).join("\n") : "All done!"}
320
+
321
+ ### Acceptance Criteria
322
+ ${s.acceptanceCriteria.length > 0 ? s.acceptanceCriteria.join("\n") : "Check story file"}`)
323
+ }
135
324
 
136
325
  if (ctx.todos.length > 0) {
137
326
  const inProgress = ctx.todos.filter(t => t.status === "in_progress")
138
- const completed = ctx.todos.filter(t => t.status === "completed")
139
327
  const pending = ctx.todos.filter(t => t.status === "pending")
140
328
 
141
- sections.push(`## Task Status
142
-
143
- **In Progress:** ${inProgress.length > 0 ? inProgress.map(t => t.content).join(", ") : "None"}
144
- **Completed:** ${completed.length > 0 ? completed.map(t => `✅ ${t.content}`).join("\n") : "None"}
145
- **Pending:** ${pending.length > 0 ? pending.map(t => `⬜ ${t.content}`).join("\n") : "None"}`)
329
+ if (inProgress.length > 0 || pending.length > 0) {
330
+ sections.push(`## 📋 Session Tasks
331
+ **In Progress:** ${inProgress.map(t => t.content).join(", ") || "None"}
332
+ **Pending:** ${pending.map(t => t.content).join(", ") || "None"}`)
333
+ }
146
334
  }
147
335
 
148
- if (ctx.story) {
149
- const s = ctx.story
150
- const total = s.completedTasks.length + s.pendingTasks.length
151
- const progress = total > 0 ? (s.completedTasks.length / total * 100).toFixed(0) : 0
336
+ return sections.join("\n\n---\n\n")
337
+ }
152
338
 
153
- sections.push(`## Active Story
339
+ function formatArchitectContext(ctx: SessionContext): string {
340
+ return `## 🏗️ Architecture Session
154
341
 
155
- **Story:** ${s.title}
156
- **Path:** ${s.path}
157
- **Status:** ${s.status}
158
- **Progress:** ${progress}% (${s.completedTasks.length}/${total} tasks)
342
+ **Focus:** System design, ADRs, technical decisions
159
343
 
160
- ### Current Task
161
- ${s.currentTask || "All tasks complete"}
344
+ ### Critical Files (MUST re-read)
345
+ ${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}
162
346
 
163
- ### Completed
164
- ${s.completedTasks.length > 0 ? s.completedTasks.map(t => `✅ ${t}`).join("\n") : "None"}
347
+ ### Resume Actions
348
+ 1. Review docs/architecture.md for current state
349
+ 2. Check docs/architecture/adr/ for recent decisions
350
+ 3. Continue from last architectural discussion`
351
+ }
352
+
353
+ function formatPmContext(ctx: SessionContext): string {
354
+ return `## 📋 PM Session
355
+
356
+ **Focus:** PRD, epics, stories, sprint planning
357
+
358
+ ### Critical Files (MUST re-read)
359
+ ${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}
360
+
361
+ ### Resume Actions
362
+ 1. Check docs/sprint-artifacts/sprint-status.yaml
363
+ 2. Review current sprint progress
364
+ 3. Continue from last planning activity`
365
+ }
366
+
367
+ function formatAnalystContext(ctx: SessionContext): string {
368
+ return `## 📊 Analyst Session
369
+
370
+ **Focus:** Requirements gathering, validation
371
+
372
+ ### Critical Files (MUST re-read)
373
+ ${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}
374
+
375
+ ### Resume Actions
376
+ 1. Review docs/requirements/requirements.md
377
+ 2. Check for pending stakeholder questions
378
+ 3. Continue requirements elicitation`
379
+ }
380
+
381
+ function formatResearcherContext(ctx: SessionContext): string {
382
+ return `## 🔍 Research Session
165
383
 
166
- ### Remaining
167
- ${s.pendingTasks.length > 0 ? s.pendingTasks.map(t => `⬜ ${t}`).join("\n") : "All done!"}`)
384
+ **Focus:** Technical, market, or domain research
385
+
386
+ ### Critical Files (MUST re-read)
387
+ ${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}
388
+
389
+ ### Resume Actions
390
+ 1. Review docs/research/ folder
391
+ 2. Check research objectives
392
+ 3. Continue investigation`
393
+ }
394
+
395
+ function formatGenericContext(ctx: SessionContext): string {
396
+ const sections: string[] = []
397
+
398
+ if (ctx.todos.length > 0) {
399
+ const inProgress = ctx.todos.filter(t => t.status === "in_progress")
400
+ const completed = ctx.todos.filter(t => t.status === "completed")
401
+ const pending = ctx.todos.filter(t => t.status === "pending")
402
+
403
+ sections.push(`## Task Status
404
+ **In Progress:** ${inProgress.length > 0 ? inProgress.map(t => t.content).join(", ") : "None"}
405
+ **Completed:** ${completed.length > 0 ? completed.map(t => `✅ ${t.content}`).join("\n") : "None"}
406
+ **Pending:** ${pending.length > 0 ? pending.map(t => `⬜ ${t.content}`).join("\n") : "None"}`)
168
407
  }
169
408
 
170
409
  if (ctx.relevantFiles.length > 0) {
171
410
  sections.push(`## Critical Files (MUST re-read)
172
-
173
411
  ${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}`)
174
412
  }
175
413
 
176
414
  return sections.join("\n\n---\n\n")
177
415
  }
178
416
 
417
+ function formatContext(ctx: SessionContext): string {
418
+ const agent = ctx.activeAgent?.toLowerCase()
419
+
420
+ switch (agent) {
421
+ case "dev":
422
+ case "coder":
423
+ return formatDevContext(ctx)
424
+ case "architect":
425
+ return formatArchitectContext(ctx)
426
+ case "pm":
427
+ return formatPmContext(ctx)
428
+ case "analyst":
429
+ return formatAnalystContext(ctx)
430
+ case "researcher":
431
+ return formatResearcherContext(ctx)
432
+ default:
433
+ return formatGenericContext(ctx)
434
+ }
435
+ }
436
+
179
437
  function formatInstructions(ctx: SessionContext): string {
438
+ const agent = ctx.activeAgent?.toLowerCase()
180
439
  const hasInProgressTasks = ctx.todos.some(t => t.status === "in_progress")
181
440
  const hasInProgressStory = ctx.story?.status === "in-progress"
182
441
 
183
442
  if (!hasInProgressTasks && !hasInProgressStory) {
184
- return `## Status: COMPLETED
443
+ return `## Status: COMPLETED
185
444
 
186
445
  Previous task was completed successfully.
187
446
 
@@ -191,55 +450,77 @@ Previous task was completed successfully.
191
450
  3. Ask user for next task`
192
451
  }
193
452
 
194
- const instructions = [`## Status: INTERRUPTED
195
-
196
- Session compacted while work in progress.
453
+ // Dev-specific instructions
454
+ if ((agent === "dev" || agent === "coder") && ctx.story) {
455
+ return `## Status: IN PROGRESS 🔄
456
+
457
+ **Active Agent:** @${agent}
458
+ **Story:** ${ctx.story.title}
459
+ **Current Task:** ${ctx.story.currentTask}
460
+
461
+ ### Resume Protocol
462
+ 1. **Read story file:** \`${ctx.story.path}\`
463
+ 2. **Load skill:** \`.opencode/skills/dev-story/SKILL.md\`
464
+ 3. **Run tests first** to see current state
465
+ 4. **Continue task:** ${ctx.story.currentTask}
466
+ 5. **Follow TDD:** Red → Green → Refactor
467
+
468
+ ### DO NOT
469
+ - Start over from scratch
470
+ - Skip reading the story file
471
+ - Ignore existing tests`
472
+ }
197
473
 
198
- **Resume:**`]
474
+ // Generic instructions
475
+ return `## Status: IN PROGRESS 🔄
199
476
 
200
- if (ctx.story?.currentTask) {
201
- instructions.push(`
202
- 1. Read story: \`${ctx.story.path}\`
203
- 2. Current task: ${ctx.story.currentTask}
204
- 3. Load skill: \`.opencode/skills/dev-story/SKILL.md\`
205
- 4. Continue red-green-refactor
206
- 5. Run tests first`)
207
- }
477
+ **Active Agent:** @${ctx.activeAgent || "unknown"}
208
478
 
209
- if (hasInProgressTasks) {
210
- const task = ctx.todos.find(t => t.status === "in_progress")
211
- instructions.push(`
212
- 1. Resume: ${task?.content}
213
- 2. Check previous messages
479
+ ### Resume Protocol
480
+ 1. Read critical files listed above
481
+ 2. Check previous messages for context
214
482
  3. Continue from last action
215
- 4. Update todo when complete`)
216
- }
217
-
218
- return instructions.join("\n")
483
+ 4. Update todo/story when task complete`
219
484
  }
220
485
 
221
486
  return {
487
+ // Track active agent from chat messages
488
+ "chat.message": async (input, output) => {
489
+ if (input.agent) {
490
+ lastActiveAgent = input.agent
491
+ lastSessionId = input.sessionID
492
+ }
493
+ },
494
+
495
+ // Also track from chat params (backup)
496
+ "chat.params": async (input, output) => {
497
+ if (input.agent) {
498
+ lastActiveAgent = input.agent
499
+ }
500
+ },
501
+
222
502
  "experimental.session.compacting": async (input, output) => {
223
- const ctx = await buildContext()
503
+ // Use tracked agent or try to detect from session
504
+ const agent = lastActiveAgent
505
+ const ctx = await buildContext(agent)
506
+ ctx.activeAgent = agent
507
+
224
508
  const context = formatContext(ctx)
225
509
  const instructions = formatInstructions(ctx)
510
+ const readCommands = generateReadCommands(agent, ctx.story)
226
511
 
227
512
  output.context.push(`# Session Continuation
513
+ ${agent ? `**Last Active Agent:** @${agent}` : ""}
228
514
 
229
- ${context}
515
+ ${readCommands}
230
516
 
231
517
  ---
232
518
 
233
- ${instructions}
519
+ ${context}
234
520
 
235
521
  ---
236
522
 
237
- ## On Resume
238
-
239
- 1. **Read critical files** listed above
240
- 2. **Check task/story status**
241
- 3. **Continue from last point** - never start over
242
- 4. **Run tests first** if implementing code`)
523
+ ${instructions}`)
243
524
  },
244
525
 
245
526
  event: async ({ event }) => {