@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.
- package/bin/cli.js +7 -0
- package/package.json +1 -1
- package/src/build-info.json +3 -2
- package/src/opencode/agents/reviewer.md +17 -193
- package/src/opencode/gitignore +28 -0
- package/src/opencode/package.json +5 -1
- package/src/opencode/plugins/__tests__/custom-compaction.test.ts +829 -0
- package/src/opencode/plugins/__tests__/file-indexer.test.ts +425 -0
- package/src/opencode/plugins/__tests__/helpers/mock-ctx.ts +171 -0
- package/src/opencode/plugins/__tests__/leak-stress.test.ts +315 -0
- package/src/opencode/plugins/__tests__/version-check.test.ts +223 -0
- package/src/opencode/plugins/custom-compaction.ts +29 -9
- package/src/opencode/plugins/file-indexer.ts +79 -54
- package/src/opencode/plugins/version-check.ts +55 -14
- package/src/opencode/skills/code-review/SKILL.md +165 -38
- package/src/opencode/skills/dev-epic/SKILL.md +32 -18
- package/src/opencode/skills/dev-sprint/SKILL.md +26 -11
- package/src/opencode/skills/dev-story/SKILL.md +40 -10
- package/src/opencode/skills/prd-writing/SKILL.md +28 -8
- package/src/opencode/skills/prd-writing/template.md +36 -17
- package/src/opencode/skills/story-writing/template.md +8 -0
|
@@ -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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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}":
|
|
311
|
+
log(`"${indexName}": done ${stats.indexed} files (${elapsed}s)`)
|
|
314
312
|
totalFiles += stats.indexed
|
|
315
|
-
action = '
|
|
313
|
+
action = 'created'
|
|
316
314
|
} else {
|
|
317
|
-
|
|
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 (
|
|
322
|
-
log(`"${indexName}"
|
|
323
|
-
|
|
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}"
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 =>
|
|
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
|
-
|
|
73
|
+
done(json.version || null)
|
|
62
74
|
} catch {
|
|
63
|
-
|
|
75
|
+
done(null)
|
|
64
76
|
}
|
|
65
77
|
})
|
|
66
78
|
}).on('error', () => {
|
|
67
|
-
|
|
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
|
-
|
|
92
|
-
const
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
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
|
|
194
|
+
### Review #{{N}} — {{YYYY-MM-DD}}
|
|
117
195
|
|
|
118
|
-
**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
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
|
-
|
|
215
|
+
**Example — first review with issues:**
|
|
125
216
|
|
|
126
|
-
|
|
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
|
|
236
|
+
### Review #2 — 2026-01-27
|
|
237
|
+
|
|
238
|
+
**Verdict:** APPROVE
|
|
239
|
+
**Reviewer:** @reviewer (Marcus)
|
|
130
240
|
|
|
131
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
## Senior Developer Review (AI)
|
|
284
|
+
## Return Summary to Caller (MANDATORY)
|
|
171
285
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
Changes Requested
|
|
289
|
+
**Format:**
|
|
177
290
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
299
|
+
{{IF APPROVE:}}
|
|
300
|
+
All good. No issues found.
|
|
185
301
|
```
|
|
186
302
|
|
|
187
|
-
|
|
303
|
+
**Example — Changes Requested:**
|
|
188
304
|
|
|
189
|
-
```
|
|
190
|
-
|
|
305
|
+
```
|
|
306
|
+
**VERDICT: CHANGES_REQUESTED**
|
|
191
307
|
|
|
192
|
-
|
|
193
|
-
- [
|
|
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
|
|
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
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
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="
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
123
|
-
<step n="
|
|
124
|
-
<step n="
|
|
125
|
-
<step n="
|
|
126
|
-
<step n="
|
|
127
|
-
<step n="
|
|
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>
|