@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.
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/opencode/FLOW.yaml +34 -187
- package/src/opencode/agents/dev.md +16 -15
- package/src/opencode/agents/reviewer.md +170 -0
- package/src/opencode/commands/review-story.md +134 -0
- package/src/opencode/config.yaml +5 -0
- package/src/opencode/package.json +1 -1
- package/src/opencode/plugins/custom-compaction.ts +369 -88
- package/src/opencode/skills/coding-standards/template-security.md +325 -0
- package/src/opencode/skills/dev-story/SKILL.md +53 -5
- package/src/opencode/skills/story-writing/template.md +16 -0
package/src/opencode/config.yaml
CHANGED
|
@@ -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
|
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
|
-
*
|
|
32
|
-
* - Tracks
|
|
33
|
-
* -
|
|
34
|
-
* -
|
|
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
|
-
|
|
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(
|
|
212
|
+
completedTasks.push(taskInfo)
|
|
73
213
|
} else {
|
|
74
|
-
if (!currentTask) currentTask =
|
|
75
|
-
pendingTasks.push(
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
if (story) {
|
|
117
|
-
relevantPaths.
|
|
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
|
|
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:
|
|
292
|
+
return { todos, story, relevantFiles, activeAgent: agent }
|
|
131
293
|
}
|
|
132
294
|
|
|
133
|
-
function
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
**In Progress:** ${inProgress.
|
|
144
|
-
**
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
339
|
+
function formatArchitectContext(ctx: SessionContext): string {
|
|
340
|
+
return `## 🏗️ Architecture Session
|
|
154
341
|
|
|
155
|
-
**
|
|
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
|
-
###
|
|
161
|
-
${
|
|
344
|
+
### Critical Files (MUST re-read)
|
|
345
|
+
${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}
|
|
162
346
|
|
|
163
|
-
###
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
474
|
+
// Generic instructions
|
|
475
|
+
return `## Status: IN PROGRESS 🔄
|
|
199
476
|
|
|
200
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
515
|
+
${readCommands}
|
|
230
516
|
|
|
231
517
|
---
|
|
232
518
|
|
|
233
|
-
${
|
|
519
|
+
${context}
|
|
234
520
|
|
|
235
521
|
---
|
|
236
522
|
|
|
237
|
-
|
|
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 }) => {
|