@comfanion/workflow 4.38.1-dev.8 → 4.38.1

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.
@@ -26,8 +26,12 @@ let logFilePath: string | null = null
26
26
  // Log to file only
27
27
  function logFile(msg: string): void {
28
28
  if (logFilePath) {
29
- const timestamp = new Date().toISOString().slice(11, 19)
30
- fsSync.appendFileSync(logFilePath, `${timestamp} ${msg}\n`)
29
+ try {
30
+ const timestamp = new Date().toISOString().slice(11, 19)
31
+ fsSync.appendFileSync(logFilePath, `${timestamp} ${msg}\n`)
32
+ } catch {
33
+ // Ignore write errors (e.g., ENOENT if log directory was removed)
34
+ }
31
35
  }
32
36
  }
33
37
 
@@ -183,6 +187,7 @@ async function loadConfig(projectRoot: string): Promise<VectorizerConfig> {
183
187
  exclude = excludeMatch[1].match(/-\s+(.+)/g)?.map(m => m.replace(/^-\s+/, '').trim()) || DEFAULT_CONFIG.exclude
184
188
  }
185
189
 
190
+ // TODO(BACKLOG): parse vectorizer.indexes from config.yaml to support custom extensions
186
191
  return { enabled, auto_index, debounce_ms, indexes: DEFAULT_CONFIG.indexes, exclude }
187
192
  } catch (e) {
188
193
  debug(`Failed to load config: ${(e as Error).message}`)
@@ -201,7 +206,11 @@ function getIndexForFile(filePath: string, config: VectorizerConfig): string | n
201
206
  }
202
207
 
203
208
  function isExcluded(relativePath: string, config: VectorizerConfig): boolean {
204
- return config.exclude.some(pattern => relativePath.startsWith(pattern))
209
+ const norm = relativePath.replace(/\\/g, '/')
210
+ return config.exclude.some(pattern => {
211
+ const p = pattern.replace(/\\/g, '/').replace(/\/+$/, '')
212
+ return norm === p || norm.startsWith(`${p}/`) || norm.includes(`/${p}/`)
213
+ })
205
214
  }
206
215
 
207
216
  async function isVectorizerInstalled(projectRoot: string): Promise<boolean> {
@@ -258,20 +267,23 @@ async function ensureIndexOnSessionStart(
258
267
  for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
259
268
  if (!indexConfig.enabled) continue
260
269
  const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
261
- const indexExists = await hasIndex(projectRoot, indexName)
262
-
263
- if (!indexExists) {
264
- const health = await indexer.checkHealth(config.exclude)
265
- totalExpectedFiles += health.expectedCount
266
- needsWork = true
267
- } else {
268
- const health = await indexer.checkHealth(config.exclude)
269
- if (health.needsReindex) {
270
+ try {
271
+ const indexExists = await hasIndex(projectRoot, indexName)
272
+
273
+ if (!indexExists) {
274
+ const health = await indexer.checkHealth(config.exclude)
270
275
  totalExpectedFiles += health.expectedCount
271
276
  needsWork = true
277
+ } else {
278
+ const health = await indexer.checkHealth(config.exclude)
279
+ if (health.needsReindex) {
280
+ totalExpectedFiles += health.expectedCount
281
+ needsWork = true
282
+ }
272
283
  }
284
+ } finally {
285
+ await indexer.unloadModel()
273
286
  }
274
- await indexer.unloadModel()
275
287
  }
276
288
 
277
289
  // Notify about work to do
@@ -287,47 +299,48 @@ async function ensureIndexOnSessionStart(
287
299
  const startTime = Date.now()
288
300
 
289
301
  const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
290
-
291
- if (!indexExists) {
292
- log(`Creating "${indexName}" index...`)
293
- const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
294
- if (indexed % 10 === 0 || indexed === total) {
295
- logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
296
- }
297
- }, config.exclude)
298
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
299
- log(`"${indexName}": done ${stats.indexed} files (${elapsed}s)`)
300
- totalFiles += stats.indexed
301
- action = 'created'
302
- } else {
303
- const health = await indexer.checkHealth(config.exclude)
304
-
305
- if (health.needsReindex) {
306
- log(`Rebuilding "${indexName}" (${health.reason}: ${health.currentCount} vs ${health.expectedCount} files)...`)
302
+ try {
303
+ if (!indexExists) {
304
+ log(`Creating "${indexName}" index...`)
307
305
  const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
308
306
  if (indexed % 10 === 0 || indexed === total) {
309
307
  logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
310
308
  }
311
309
  }, config.exclude)
312
310
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
313
- log(`"${indexName}": rebuilt ${stats.indexed} files (${elapsed}s)`)
311
+ log(`"${indexName}": done ${stats.indexed} files (${elapsed}s)`)
314
312
  totalFiles += stats.indexed
315
- action = 'rebuilt'
313
+ action = 'created'
316
314
  } else {
317
- log(`Freshening "${indexName}"...`)
318
- const stats = await indexer.freshen()
319
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
315
+ const health = await indexer.checkHealth(config.exclude)
320
316
 
321
- if (stats.updated > 0 || stats.deleted > 0) {
322
- log(`"${indexName}": +${stats.updated} -${stats.deleted} (${elapsed}s)`)
323
- action = 'freshened'
317
+ if (health.needsReindex) {
318
+ log(`Rebuilding "${indexName}" (${health.reason}: ${health.currentCount} vs ${health.expectedCount} files)...`)
319
+ const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
320
+ if (indexed % 10 === 0 || indexed === total) {
321
+ logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
322
+ }
323
+ }, config.exclude)
324
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
325
+ log(`"${indexName}": rebuilt ${stats.indexed} files (${elapsed}s)`)
326
+ totalFiles += stats.indexed
327
+ action = 'rebuilt'
324
328
  } else {
325
- log(`"${indexName}": fresh (${elapsed}s)`)
329
+ log(`Freshening "${indexName}"...`)
330
+ const stats = await indexer.freshen()
331
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
332
+
333
+ if (stats.updated > 0 || stats.deleted > 0) {
334
+ log(`"${indexName}": +${stats.updated} -${stats.deleted} (${elapsed}s)`)
335
+ action = 'freshened'
336
+ } else {
337
+ log(`"${indexName}": fresh (${elapsed}s)`)
338
+ }
326
339
  }
327
340
  }
341
+ } finally {
342
+ await indexer.unloadModel()
328
343
  }
329
-
330
- await indexer.unloadModel()
331
344
  }
332
345
 
333
346
  elapsedSeconds = (Date.now() - overallStart) / 1000
@@ -364,21 +377,22 @@ async function processPendingFiles(projectRoot: string, config: VectorizerConfig
364
377
 
365
378
  for (const [indexName, files] of filesToProcess.entries()) {
366
379
  const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
367
-
368
- for (const filePath of files) {
369
- try {
370
- const wasIndexed = await indexer.indexSingleFile(filePath)
371
- if (wasIndexed) {
372
- log(`Reindexed: ${path.relative(projectRoot, filePath)} → ${indexName}`)
373
- } else {
374
- logFile(`Skipped (unchanged): ${path.relative(projectRoot, filePath)}`)
380
+ try {
381
+ for (const filePath of files) {
382
+ try {
383
+ const wasIndexed = await indexer.indexSingleFile(filePath)
384
+ if (wasIndexed) {
385
+ log(`Reindexed: ${path.relative(projectRoot, filePath)} → ${indexName}`)
386
+ } else {
387
+ logFile(`Skipped (unchanged): ${path.relative(projectRoot, filePath)}`)
388
+ }
389
+ } catch (e) {
390
+ log(`Error reindexing ${path.relative(projectRoot, filePath)}: ${(e as Error).message}`)
375
391
  }
376
- } catch (e) {
377
- log(`Error reindexing ${path.relative(projectRoot, filePath)}: ${(e as Error).message}`)
378
392
  }
393
+ } finally {
394
+ await indexer.unloadModel()
379
395
  }
380
-
381
- await indexer.unloadModel()
382
396
  }
383
397
  } catch (e) {
384
398
  debug(`Fatal: ${(e as Error).message}`)
@@ -409,7 +423,12 @@ export const FileIndexerPlugin: Plugin = async ({ directory, client }) => {
409
423
 
410
424
  // Setup log file
411
425
  logFilePath = path.join(directory, '.opencode', 'indexer.log')
412
- fsSync.writeFileSync(logFilePath, '') // Clear old log
426
+ try {
427
+ fsSync.writeFileSync(logFilePath, '') // Clear old log
428
+ } catch {
429
+ if (DEBUG) console.log(`[file-indexer] log init failed`)
430
+ logFilePath = null // Disable file logging if can't write
431
+ }
413
432
 
414
433
  log(`Plugin ACTIVE`)
415
434
 
@@ -449,6 +468,11 @@ export const FileIndexerPlugin: Plugin = async ({ directory, client }) => {
449
468
  function queueFileForIndexing(filePath: string): void {
450
469
  const relativePath = path.relative(directory, filePath)
451
470
 
471
+ // Reject paths outside project directory
472
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
473
+ return
474
+ }
475
+
452
476
  // Check exclusions from config
453
477
  if (isExcluded(relativePath, config)) {
454
478
  return
@@ -468,6 +492,7 @@ export const FileIndexerPlugin: Plugin = async ({ directory, client }) => {
468
492
  await processPendingFiles(directory, config)
469
493
  } else {
470
494
  debug(`Vectorizer not installed`)
495
+ pendingFiles.clear() // Prevent unbounded growth when vectorizer is not installed
471
496
  }
472
497
  }, config.debounce_ms + 100)
473
498
  }
@@ -49,23 +49,34 @@ async function getLocalVersion(directory: string): Promise<string | null> {
49
49
 
50
50
  async function getLatestVersion(): Promise<string | null> {
51
51
  return new Promise((resolve) => {
52
- const timeout = setTimeout(() => resolve(null), 5000) // 5s timeout
53
-
54
- https.get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, (res) => {
52
+ let settled = false
53
+ const done = (value: string | null) => {
54
+ if (settled) return
55
+ settled = true
56
+ clearTimeout(timeout)
57
+ resolve(value)
58
+ }
59
+
60
+ const timeout = setTimeout(() => {
61
+ done(null)
62
+ req.destroy() // Destroy socket on timeout to prevent leak
63
+ }, 5000)
64
+
65
+ const req = https.get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, (res) => {
55
66
  let data = ''
56
- res.on('data', chunk => data += chunk)
67
+ res.on('data', chunk => {
68
+ if (!settled) data += chunk // Stop accumulating after settled
69
+ })
57
70
  res.on('end', () => {
58
- clearTimeout(timeout)
59
71
  try {
60
72
  const json = JSON.parse(data)
61
- resolve(json.version || null)
73
+ done(json.version || null)
62
74
  } catch {
63
- resolve(null)
75
+ done(null)
64
76
  }
65
77
  })
66
78
  }).on('error', () => {
67
- clearTimeout(timeout)
68
- resolve(null)
79
+ done(null)
69
80
  })
70
81
  })
71
82
  }
@@ -88,8 +99,10 @@ async function saveCache(directory: string, cache: VersionCache): Promise<void>
88
99
  }
89
100
 
90
101
  function compareVersions(local: string, latest: string): number {
91
- const localParts = local.split('.').map(Number)
92
- const latestParts = latest.split('.').map(Number)
102
+ // Strip pre-release suffix (e.g., "4.38.1-beta.1" → "4.38.1")
103
+ const strip = (v: string) => v.replace(/-.*$/, '')
104
+ const localParts = strip(local).split('.').map(Number)
105
+ const latestParts = strip(latest).split('.').map(Number)
93
106
 
94
107
  for (let i = 0; i < 3; i++) {
95
108
  const l = localParts[i] || 0
@@ -111,14 +124,34 @@ async function getLanguage(directory: string): Promise<'en' | 'uk' | 'ru'> {
111
124
  try {
112
125
  const configPath = path.join(directory, '.opencode', 'config.yaml')
113
126
  const config = await fs.readFile(configPath, 'utf8')
114
- if (config.includes('communication_language: Ukrainian') || config.includes('communication_language: uk')) return 'uk'
115
- if (config.includes('communication_language: Russian') || config.includes('communication_language: ru')) return 'ru'
127
+ const match = config.match(/communication_language:\s*["']?(\w+)["']?/i)
128
+ const lang = match?.[1]?.toLowerCase()
129
+ if (lang === 'ukrainian' || lang === 'uk') return 'uk'
130
+ if (lang === 'russian' || lang === 'ru') return 'ru'
116
131
  } catch {}
117
132
  return 'en'
118
133
  }
119
134
 
135
+ async function loadVersionCheckConfig(directory: string): Promise<{ enabled: boolean; checkInterval: number }> {
136
+ try {
137
+ const configPath = path.join(directory, '.opencode', 'config.yaml')
138
+ const content = await fs.readFile(configPath, 'utf8')
139
+ const section = content.match(/version_check:\s*\n([\s\S]*?)(?=\n[a-z_]+:|$)/i)
140
+ if (!section) return { enabled: true, checkInterval: 60 * 60 * 1000 }
141
+ const enabledMatch = section[1].match(/^\s+enabled:\s*(true|false)/m)
142
+ const intervalMatch = section[1].match(/^\s+check_interval:\s*(\d+)/m)
143
+ return {
144
+ enabled: enabledMatch ? enabledMatch[1] === 'true' : true,
145
+ checkInterval: intervalMatch ? parseInt(intervalMatch[1]) : 60 * 60 * 1000,
146
+ }
147
+ } catch {
148
+ return { enabled: true, checkInterval: 60 * 60 * 1000 }
149
+ }
150
+ }
151
+
120
152
  export const VersionCheckPlugin: Plugin = async ({ directory, client }) => {
121
- const CHECK_INTERVAL = 60 * 60 * 1000 // 1 hour (you release often! 🚀)
153
+ const vcConfig = await loadVersionCheckConfig(directory)
154
+ const CHECK_INTERVAL = vcConfig.checkInterval
122
155
 
123
156
  const toast = async (message: string, variant: 'info' | 'success' | 'error' = 'info') => {
124
157
  try {
@@ -128,6 +161,14 @@ export const VersionCheckPlugin: Plugin = async ({ directory, client }) => {
128
161
 
129
162
  log(`Plugin loaded`)
130
163
 
164
+ // Respect config enabled flag
165
+ if (!vcConfig.enabled) {
166
+ log(`Plugin DISABLED by config`)
167
+ return {
168
+ event: async () => {},
169
+ }
170
+ }
171
+
131
172
  // Run check after short delay (let TUI initialize)
132
173
  setTimeout(async () => {
133
174
  try {
@@ -16,6 +16,75 @@ How to perform thorough code reviews for implemented stories.
16
16
 
17
17
  Ensure code quality, correctness, and adherence to project standards before merging.
18
18
 
19
+ ---
20
+
21
+ <workflow name="code-review">
22
+
23
+ <phase name="1-prepare" title="Preparation">
24
+ <action>Read the story file completely</action>
25
+ <action>Identify all acceptance criteria</action>
26
+ <action>Load docs/coding-standards/*.md for coding standards</action>
27
+ <action>Use search() to find similar patterns in codebase to compare against</action>
28
+ <action>Use search() in docs for architecture requirements</action>
29
+ <action>Review File List section for changed files</action>
30
+ </phase>
31
+
32
+ <phase name="2-security" title="Security Analysis (HIGH Priority)">
33
+ <critical>Security issues are ALWAYS high priority</critical>
34
+ <check>No hardcoded secrets, API keys, passwords</check>
35
+ <check>All user inputs validated and sanitized</check>
36
+ <check>Parameterized queries (no SQL injection)</check>
37
+ <check>Auth required on protected endpoints</check>
38
+ <check>Authorization checks before data access</check>
39
+ <check>Sensitive data not logged</check>
40
+ <check>Error messages don't leak internal details</check>
41
+ </phase>
42
+
43
+ <phase name="3-correctness" title="Correctness Analysis (HIGH Priority)">
44
+ <check>All acceptance criteria satisfied</check>
45
+ <check>Edge cases handled</check>
46
+ <check>Error scenarios have proper handling</check>
47
+ <check>No obvious logic errors</check>
48
+ <check>No race conditions</check>
49
+ </phase>
50
+
51
+ <phase name="4-tests" title="Testing Review (HIGH Priority)">
52
+ <check>Unit tests exist for new code</check>
53
+ <check>Tests cover happy path and errors</check>
54
+ <check>No flaky tests</check>
55
+ <check>Test names are descriptive</check>
56
+ <check>Run test suite: go test / npm test / pytest / cargo test</check>
57
+ <check>If failures → include in review report as HIGH priority</check>
58
+ </phase>
59
+
60
+ <phase name="5-quality" title="Code Quality (MEDIUM Priority)">
61
+ <check>Follows project architecture</check>
62
+ <check>Clear naming conventions</check>
63
+ <check>No code duplication</check>
64
+ <check>Functions are focused and small</check>
65
+ <check>Proper error wrapping</check>
66
+ <check>No N+1 query issues</check>
67
+ <check>Run linter: golangci-lint / eslint / ruff / cargo clippy</check>
68
+ </phase>
69
+
70
+ <phase name="6-write-file" title="Write Findings to Story File">
71
+ <critical>MANDATORY: Append review to story file for history/analytics</critical>
72
+ <step n="1">Read story file's ## Review section</step>
73
+ <step n="2">Count existing ### Review #N blocks → your review is N+1</step>
74
+ <step n="3">Append ### Review #N block with format below</step>
75
+ <step n="4">NEVER overwrite previous reviews — always APPEND</step>
76
+ </phase>
77
+
78
+ <phase name="7-return-summary" title="Return Summary to Caller">
79
+ <critical>Caller (@dev) uses YOUR output, not the file. Keep it actionable.</critical>
80
+ <step n="1">Return SHORT summary: verdict + action items</step>
81
+ <step n="2">Caller will use this directly without re-reading story file</step>
82
+ </phase>
83
+
84
+ </workflow>
85
+
86
+ ---
87
+
19
88
  ## Review Process
20
89
 
21
90
  ### 1. Preparation
@@ -101,38 +170,82 @@ For each AC in the story:
101
170
 
102
171
  All criteria met. Code is ready to merge.
103
172
 
104
- ```markdown
105
- ### Review Outcome: Approve
106
-
107
- All acceptance criteria satisfied. Code follows project standards.
108
- Ready for merge.
109
- ```
110
-
111
173
  ### 🔄 Changes Requested
112
174
 
113
175
  Issues found that need addressing.
114
176
 
177
+ ### ❌ Blocked
178
+
179
+ Major issues that prevent approval.
180
+
181
+ ## Write Findings to Story File (MANDATORY)
182
+
183
+ After completing the review, **append** your findings to the story file's `## Review` section.
184
+ Each review round is a separate `### Review #N` block. NEVER overwrite previous reviews — always append.
185
+
186
+ **How to determine review number:**
187
+ 1. Read the story file's `## Review` section
188
+ 2. Count existing `### Review #N` blocks
189
+ 3. Your review is `N + 1` (or `#1` if none exist)
190
+
191
+ **Format to append at the end of the story file:**
192
+
115
193
  ```markdown
116
- ### Review Outcome: Changes Requested
194
+ ### Review #{{N}} {{YYYY-MM-DD}}
117
195
 
118
- **Action Items:**
119
- - [ ] [High] Fix missing error handling in X
120
- - [ ] [Med] Add unit test for edge case Y
121
- - [ ] [Low] Improve variable naming in Z
196
+ **Verdict:** {{APPROVE | CHANGES_REQUESTED | BLOCKED}}
197
+ **Reviewer:** @reviewer (Marcus)
198
+
199
+ **Summary:** {{1-2 sentences}}
200
+
201
+ **Tests:** {{PASS | FAIL — details}}
202
+ **Lint:** {{PASS | FAIL — details}}
203
+
204
+ {{IF issues found:}}
205
+ #### Action Items
206
+ - [ ] [HIGH] `path/file.ts:42` — {{issue}} → Fix: {{specific fix}}
207
+ - [ ] [MED] `path/file.ts:100` — {{issue}} → Fix: {{specific fix}}
208
+ - [ ] [LOW] `path/file.ts:15` — {{issue}}
209
+
210
+ {{IF approve:}}
211
+ #### What's Good
212
+ - {{positive feedback}}
122
213
  ```
123
214
 
124
- ### Blocked
215
+ **Example first review with issues:**
125
216
 
126
- Major issues that prevent approval.
217
+ ```markdown
218
+ ### Review #1 — 2026-01-27
219
+
220
+ **Verdict:** CHANGES_REQUESTED
221
+ **Reviewer:** @reviewer (Marcus)
222
+
223
+ **Summary:** Missing error handling in CreateUser handler, no test for duplicate email.
224
+
225
+ **Tests:** PASS (12/12)
226
+ **Lint:** PASS
227
+
228
+ #### Action Items
229
+ - [ ] [HIGH] `internal/user/handler.go:42` — No error handling for DB timeout → Fix: wrap with domain error
230
+ - [ ] [MED] `internal/user/handler_test.go` — Missing duplicate email test → Fix: add TestCreateUser_DuplicateEmail
231
+ ```
232
+
233
+ **Example — second review after fixes:**
127
234
 
128
235
  ```markdown
129
- ### Review Outcome: Blocked
236
+ ### Review #2 — 2026-01-27
237
+
238
+ **Verdict:** APPROVE
239
+ **Reviewer:** @reviewer (Marcus)
130
240
 
131
- **Blocking Issues:**
132
- 1. Security vulnerability in authentication flow
133
- 2. Missing critical test coverage
241
+ **Summary:** All issues from Review #1 fixed. Error handling added, test coverage complete.
134
242
 
135
- Cannot proceed until blocking issues resolved.
243
+ **Tests:** PASS (14/14)
244
+ **Lint:** PASS
245
+
246
+ #### What's Good
247
+ - Clean error wrapping with domain errors
248
+ - Good test coverage for edge cases
136
249
  ```
137
250
 
138
251
  ## Severity Levels
@@ -164,39 +277,53 @@ func foo() error { ... }
164
277
 
165
278
  ## Updating Story File
166
279
 
167
- After review, add to story file:
280
+ **MANDATORY:** Use the format from "Write Findings to Story File" section above.
281
+ Append `### Review #N` block to the `## Review` section at the end of the story file.
282
+ NEVER overwrite previous reviews — history must be preserved for analytics.
168
283
 
169
- ```markdown
170
- ## Senior Developer Review (AI)
284
+ ## Return Summary to Caller (MANDATORY)
171
285
 
172
- ### Review Date
173
- 2024-01-15
286
+ After writing to story file, return a SHORT summary to the calling agent (@dev).
287
+ This prevents the caller from re-reading the story file.
174
288
 
175
- ### Review Outcome
176
- Changes Requested
289
+ **Format:**
177
290
 
178
- ### Action Items
179
- - [ ] [High] Add error handling to CreateUser handler
180
- - [ ] [Med] Add unit test for duplicate email validation
181
- - [ ] [Low] Rename 'x' to 'userCount'
291
+ ```
292
+ **VERDICT: {{APPROVE | CHANGES_REQUESTED | BLOCKED}}**
293
+
294
+ {{IF CHANGES_REQUESTED or BLOCKED:}}
295
+ Action items:
296
+ - [HIGH] `path/file.ts:42` — {{issue}} → {{fix}}
297
+ - [MED] `path/file.ts:100` — {{issue}} → {{fix}}
182
298
 
183
- ### Detailed Comments
184
- [Include detailed review comments here]
299
+ {{IF APPROVE:}}
300
+ All good. No issues found.
185
301
  ```
186
302
 
187
- If changes requested, also add:
303
+ **Example Changes Requested:**
188
304
 
189
- ```markdown
190
- ### Review Follow-ups (AI)
305
+ ```
306
+ **VERDICT: CHANGES_REQUESTED**
191
307
 
192
- - [ ] [AI-Review] [High] Add error handling to CreateUser handler
193
- - [ ] [AI-Review] [Med] Add unit test for duplicate email validation
308
+ Action items:
309
+ - [HIGH] `internal/user/handler.go:42` No error handling for DB timeout → wrap with domain error
310
+ - [MED] `internal/user/handler_test.go` — Missing duplicate email test → add TestCreateUser_DuplicateEmail
194
311
  ```
195
312
 
313
+ **Example — Approve:**
314
+
315
+ ```
316
+ **VERDICT: APPROVE**
317
+
318
+ All good. No issues found. Clean error wrapping, good test coverage.
319
+ ```
320
+
321
+ ---
322
+
196
323
  ## Best Practices
197
324
 
198
325
  1. **Be specific** - Point to exact file and line
199
326
  2. **Suggest solutions** - Don't just criticize
200
327
  3. **Prioritize** - Focus on important issues first
201
328
  4. **Be constructive** - Phrase feedback positively
202
- 5. **Use different LLM** - For fresh perspective
329
+ 5. **Use search()** - Find similar patterns before reviewing
@@ -78,27 +78,34 @@ metadata:
78
78
  </phase>
79
79
 
80
80
  <phase name="3-loop" title="Story Execution Loop">
81
+ <critical>Status flow: in_progress → review → done. NEVER mark done before review!</critical>
81
82
  <for-each item="story" in="pending_stories">
82
83
 
83
84
  <action name="execute-story">
84
85
  Follow /dev-story logic:
85
- - Create nested TODO for tasks
86
- - Implement all tasks (RED/GREEN/REFACTOR)
87
- - Clear task TODO when done
86
+ - Read ONE story file
87
+ - Execute tasks ONE BY ONE (or parallel if independent)
88
+ - NEVER delegate entire story to @coder in one prompt
89
+ - After each task: verify, mark done, next task
88
90
  </action>
89
91
 
90
- <action name="mark-done">
91
- Mark story as completed in epic TODO
92
+ <action name="story-to-review">
93
+ All tasks done set story status: review
94
+ Mark story TODO as "review" (NOT "done" yet!)
92
95
  </action>
93
96
 
94
- <action name="review">
95
- Mark "Review Story" as in_progress
96
- Invoke @reviewer
97
+ <action name="review-story">
98
+ Invoke @reviewer on story code.
99
+ Reviewer does TWO things:
100
+ 1. WRITES findings to story file (## Review → ### Review #N) — for history
101
+ 2. RETURNS summary to you — use THIS, do NOT re-read story file
97
102
  <if condition="CHANGES_REQUESTED">
98
- Add fix tasks re-execute → re-review (max 3 attempts)
103
+ Use reviewer's returned action items directly.
104
+ Create fix tasks from action items → execute → re-review (max 3 attempts).
99
105
  </if>
100
106
  <if condition="APPROVED">
101
- Mark "Review Story" as completed
107
+ Set story status: done
108
+ Mark story TODO as completed
102
109
  </if>
103
110
  </action>
104
111
 
@@ -111,20 +118,22 @@ metadata:
111
118
 
112
119
  <action name="compact">
113
120
  Mark next story as in_progress in TODO
114
- Wait for auto-compaction
115
- Plugin reads TODO + state → resume
121
+ Wait for auto-compaction → resume
116
122
  </action>
117
123
 
118
124
  </for-each>
119
125
  </phase>
120
126
 
121
127
  <phase name="4-finalize" title="Finalize Epic">
122
- <step n="1">Run epic integration tests (mark in TODO)</step>
123
- <step n="2">Verify all AC from epic file (mark in TODO)</step>
124
- <step n="3">Set state: status="done"</step>
125
- <step n="4">Clear epic TODO list</step>
126
- <step n="5">Update .opencode/session-state.yaml (next epic or done)</step>
127
- <step n="6">Report completion with summary</step>
128
+ <critical>Epic also goes through review before done!</critical>
129
+ <step n="1">All stories done set epic status: review</step>
130
+ <step n="2">Run epic integration tests (mark in TODO)</step>
131
+ <step n="3">Verify all AC from epic file (mark in TODO)</step>
132
+ <step n="4">If tests fail fix → re-test</step>
133
+ <step n="5">All passed set epic status: done</step>
134
+ <step n="6">Clear epic TODO list</step>
135
+ <step n="7">Update .opencode/session-state.yaml (next epic or done)</step>
136
+ <step n="8">Report completion with summary</step>
128
137
  </phase>
129
138
 
130
139
  </workflow>
@@ -170,6 +179,11 @@ This file survives compaction and tells the agent where to resume.
170
179
  <rules>
171
180
  <do>Create clean TODO list for each epic</do>
172
181
  <do>Update epic state file BEFORE compaction</do>
182
+ <do>Execute stories IN ORDER as planned in epic file</do>
183
+ <do>Execute tasks within story ONE BY ONE (or parallel if independent)</do>
173
184
  <dont>Ask user for confirmation between stories — TODO is your guide</dont>
174
185
  <dont>Proceed to next story if review fails — enter fix loop</dont>
186
+ <dont>Reorder, skip, merge, or "optimize" story execution order</dont>
187
+ <dont>Combine tasks from different stories into one batch</dont>
188
+ <dont>Delegate entire story to @coder in one prompt — task by task only</dont>
175
189
  </rules>