@comfanion/workflow 4.38.3-dev.2 → 4.38.4-dev.0
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 +4 -5
- package/src/opencode/config.yaml +0 -69
- package/src/opencode/gitignore +2 -0
- package/src/opencode/opencode.json +3 -5
- package/src/opencode/vectorizer.yaml +45 -0
- package/src/opencode/plugins/README.md +0 -182
- package/src/opencode/plugins/__tests__/custom-compaction.test.ts +0 -829
- package/src/opencode/plugins/__tests__/file-indexer.test.ts +0 -425
- package/src/opencode/plugins/__tests__/helpers/mock-ctx.ts +0 -171
- package/src/opencode/plugins/__tests__/leak-stress.test.ts +0 -315
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +0 -205
- package/src/opencode/plugins/__tests__/version-check.test.ts +0 -223
- package/src/opencode/plugins/custom-compaction.ts +0 -1080
- package/src/opencode/plugins/file-indexer.ts +0 -516
- package/src/opencode/plugins/usethis-todo-publish.ts +0 -44
- package/src/opencode/plugins/usethis-todo-ui.ts +0 -37
- package/src/opencode/plugins/version-check.ts +0 -230
- package/src/opencode/tools/codeindex.ts +0 -264
- package/src/opencode/tools/search.ts +0 -149
- package/src/opencode/tools/usethis_todo.ts +0 -538
- package/src/vectorizer/index.js +0 -573
- package/src/vectorizer/package.json +0 -16
|
@@ -1,829 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { writeFile } from "fs/promises"
|
|
4
|
-
import { CustomCompactionPlugin } from "../custom-compaction"
|
|
5
|
-
import {
|
|
6
|
-
createMockCtx,
|
|
7
|
-
createTempDir,
|
|
8
|
-
cleanupTempDir,
|
|
9
|
-
FIXTURE_SESSION_STATE,
|
|
10
|
-
FIXTURE_STORY_MD,
|
|
11
|
-
FIXTURE_EPIC_STATE,
|
|
12
|
-
FIXTURE_TODOS,
|
|
13
|
-
} from "./helpers/mock-ctx"
|
|
14
|
-
|
|
15
|
-
// =============================================================================
|
|
16
|
-
// LOCAL REPLICAS of internal functions (not exported from plugin)
|
|
17
|
-
// These mirror the source logic so we can unit-test the algorithms directly.
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
const SERVICE_AGENTS = ["title", "compaction", "summary", "system"]
|
|
21
|
-
|
|
22
|
-
function isRealAgent(agent: string | null): boolean {
|
|
23
|
-
if (!agent) return false
|
|
24
|
-
return !SERVICE_AGENTS.includes(agent.toLowerCase())
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const BASE_FILES = ["CLAUDE.md", "AGENTS.md"]
|
|
28
|
-
|
|
29
|
-
const AGENT_FILES: Record<string, string[]> = {
|
|
30
|
-
dev: [...BASE_FILES, "docs/coding-standards/README.md", "docs/coding-standards/patterns.md"],
|
|
31
|
-
coder: [...BASE_FILES, "docs/coding-standards/patterns.md"],
|
|
32
|
-
architect: [...BASE_FILES, "docs/architecture.md", "docs/prd.md", "docs/coding-standards/README.md", "docs/architecture/adr"],
|
|
33
|
-
pm: [...BASE_FILES, "docs/prd.md", "docs/architecture.md", "docs/sprint-artifacts/sprint-status.yaml", "docs/sprint-artifacts/backlog"],
|
|
34
|
-
analyst: [...BASE_FILES, "docs/requirements/requirements.md", "docs/prd.md"],
|
|
35
|
-
researcher: [...BASE_FILES, "docs/prd.md", "docs/research"],
|
|
36
|
-
crawler: [...BASE_FILES],
|
|
37
|
-
"change-manager": [...BASE_FILES, "docs/prd.md", "docs/architecture.md"],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const DEFAULT_FILES = [...BASE_FILES, "docs/prd.md", "docs/architecture.md"]
|
|
41
|
-
|
|
42
|
-
const MUST_READ_FILES: Record<string, string[]> = {
|
|
43
|
-
dev: ["AGENTS.md", "CLAUDE.md", "docs/coding-standards/README.md"],
|
|
44
|
-
coder: ["AGENTS.md", "CLAUDE.md", "docs/coding-standards/README.md"],
|
|
45
|
-
architect: ["AGENTS.md", "CLAUDE.md", "docs/prd.md", "docs/architecture.md"],
|
|
46
|
-
pm: ["AGENTS.md", "CLAUDE.md", "docs/prd.md"],
|
|
47
|
-
analyst: ["AGENTS.md", "CLAUDE.md", "docs/prd.md"],
|
|
48
|
-
researcher: ["AGENTS.md", "CLAUDE.md"],
|
|
49
|
-
default: ["AGENTS.md", "CLAUDE.md"],
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// =============================================================================
|
|
53
|
-
// UNIT TESTS: isRealAgent (replica)
|
|
54
|
-
// =============================================================================
|
|
55
|
-
describe("isRealAgent", () => {
|
|
56
|
-
it("returns false for null", () => {
|
|
57
|
-
expect(isRealAgent(null)).toBe(false)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it("returns false for service agents", () => {
|
|
61
|
-
for (const agent of SERVICE_AGENTS) {
|
|
62
|
-
expect(isRealAgent(agent)).toBe(false)
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it("returns false for service agents (case-insensitive)", () => {
|
|
67
|
-
expect(isRealAgent("Title")).toBe(false)
|
|
68
|
-
expect(isRealAgent("COMPACTION")).toBe(false)
|
|
69
|
-
expect(isRealAgent("Summary")).toBe(false)
|
|
70
|
-
expect(isRealAgent("SYSTEM")).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it("returns true for real agents", () => {
|
|
74
|
-
expect(isRealAgent("dev")).toBe(true)
|
|
75
|
-
expect(isRealAgent("pm")).toBe(true)
|
|
76
|
-
expect(isRealAgent("architect")).toBe(true)
|
|
77
|
-
expect(isRealAgent("analyst")).toBe(true)
|
|
78
|
-
expect(isRealAgent("researcher")).toBe(true)
|
|
79
|
-
expect(isRealAgent("coder")).toBe(true)
|
|
80
|
-
expect(isRealAgent("reviewer")).toBe(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it("returns true for unknown non-service agents", () => {
|
|
84
|
-
expect(isRealAgent("custom-agent")).toBe(true)
|
|
85
|
-
expect(isRealAgent("myagent")).toBe(true)
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// =============================================================================
|
|
90
|
-
// UNIT TESTS: Constants validation (replicas)
|
|
91
|
-
// =============================================================================
|
|
92
|
-
describe("constants", () => {
|
|
93
|
-
it("SERVICE_AGENTS contains expected entries", () => {
|
|
94
|
-
expect(SERVICE_AGENTS).toContain("title")
|
|
95
|
-
expect(SERVICE_AGENTS).toContain("compaction")
|
|
96
|
-
expect(SERVICE_AGENTS).toContain("summary")
|
|
97
|
-
expect(SERVICE_AGENTS).toContain("system")
|
|
98
|
-
expect(SERVICE_AGENTS.length).toBe(4)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it("BASE_FILES contains CLAUDE.md and AGENTS.md", () => {
|
|
102
|
-
expect(BASE_FILES).toContain("CLAUDE.md")
|
|
103
|
-
expect(BASE_FILES).toContain("AGENTS.md")
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it("AGENT_FILES covers all known agent types", () => {
|
|
107
|
-
const expectedAgents = [
|
|
108
|
-
"dev", "coder", "architect", "pm", "analyst", "researcher", "crawler", "change-manager",
|
|
109
|
-
]
|
|
110
|
-
for (const agent of expectedAgents) {
|
|
111
|
-
expect(AGENT_FILES[agent]).toBeDefined()
|
|
112
|
-
expect(AGENT_FILES[agent].length).toBeGreaterThan(0)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it("all AGENT_FILES include BASE_FILES", () => {
|
|
117
|
-
for (const [_agent, files] of Object.entries(AGENT_FILES)) {
|
|
118
|
-
for (const baseFile of BASE_FILES) {
|
|
119
|
-
expect(files).toContain(baseFile)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it("DEFAULT_FILES includes BASE_FILES + prd + architecture", () => {
|
|
125
|
-
expect(DEFAULT_FILES).toContain("CLAUDE.md")
|
|
126
|
-
expect(DEFAULT_FILES).toContain("AGENTS.md")
|
|
127
|
-
expect(DEFAULT_FILES).toContain("docs/prd.md")
|
|
128
|
-
expect(DEFAULT_FILES).toContain("docs/architecture.md")
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it("MUST_READ_FILES has 'default' key", () => {
|
|
132
|
-
expect(MUST_READ_FILES.default).toBeDefined()
|
|
133
|
-
expect(MUST_READ_FILES.default.length).toBeGreaterThan(0)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it("MUST_READ_FILES dev includes AGENTS.md and CLAUDE.md", () => {
|
|
137
|
-
expect(MUST_READ_FILES.dev).toContain("AGENTS.md")
|
|
138
|
-
expect(MUST_READ_FILES.dev).toContain("CLAUDE.md")
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it("MUST_READ_FILES dev includes coding-standards", () => {
|
|
142
|
-
expect(MUST_READ_FILES.dev).toContain("docs/coding-standards/README.md")
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// =============================================================================
|
|
147
|
-
// INTEGRATION: Plugin hook structure
|
|
148
|
-
// =============================================================================
|
|
149
|
-
describe("CustomCompactionPlugin: hook structure", () => {
|
|
150
|
-
let tempDir: string
|
|
151
|
-
|
|
152
|
-
beforeEach(async () => {
|
|
153
|
-
tempDir = await createTempDir()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
afterEach(async () => {
|
|
157
|
-
await cleanupTempDir(tempDir)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it("returns all required hooks", async () => {
|
|
161
|
-
const ctx = createMockCtx(tempDir)
|
|
162
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
163
|
-
|
|
164
|
-
expect(hooks["chat.message"]).toBeFunction()
|
|
165
|
-
expect(hooks["chat.params"]).toBeFunction()
|
|
166
|
-
expect(hooks["experimental.session.compacting"]).toBeFunction()
|
|
167
|
-
expect(hooks.event).toBeFunction()
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// =============================================================================
|
|
172
|
-
// INTEGRATION: chat.message hook (agent tracking)
|
|
173
|
-
// =============================================================================
|
|
174
|
-
describe("chat.message hook", () => {
|
|
175
|
-
let tempDir: string
|
|
176
|
-
|
|
177
|
-
beforeEach(async () => {
|
|
178
|
-
tempDir = await createTempDir()
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
afterEach(async () => {
|
|
182
|
-
await cleanupTempDir(tempDir)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it("tracks string agent name", async () => {
|
|
186
|
-
const ctx = createMockCtx(tempDir)
|
|
187
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
188
|
-
|
|
189
|
-
await hooks["chat.message"]!(
|
|
190
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
191
|
-
{ message: {}, parts: [] } as any
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
195
|
-
await hooks["experimental.session.compacting"]!(
|
|
196
|
-
{ sessionID: "sess-1" } as any,
|
|
197
|
-
output as any
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
201
|
-
const briefing = output.context.join("\n")
|
|
202
|
-
expect(briefing).toContain("@dev")
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it("tracks object agent with name property", async () => {
|
|
206
|
-
const ctx = createMockCtx(tempDir)
|
|
207
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
208
|
-
|
|
209
|
-
await hooks["chat.message"]!(
|
|
210
|
-
{ agent: { name: "architect" }, sessionID: "sess-1" } as any,
|
|
211
|
-
{ message: {}, parts: [] } as any
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
215
|
-
await hooks["experimental.session.compacting"]!(
|
|
216
|
-
{ sessionID: "sess-1" } as any,
|
|
217
|
-
output as any
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
const briefing = output.context.join("\n")
|
|
221
|
-
expect(briefing).toContain("@architect")
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it("ignores service agents", async () => {
|
|
225
|
-
const ctx = createMockCtx(tempDir)
|
|
226
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
227
|
-
|
|
228
|
-
// First set a real agent
|
|
229
|
-
await hooks["chat.message"]!(
|
|
230
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
231
|
-
{ message: {}, parts: [] } as any
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
// Then send service agent - should NOT override
|
|
235
|
-
await hooks["chat.message"]!(
|
|
236
|
-
{ agent: "compaction", sessionID: "sess-1" } as any,
|
|
237
|
-
{ message: {}, parts: [] } as any
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
241
|
-
await hooks["experimental.session.compacting"]!(
|
|
242
|
-
{ sessionID: "sess-1" } as any,
|
|
243
|
-
output as any
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
const briefing = output.context.join("\n")
|
|
247
|
-
expect(briefing).toContain("@dev")
|
|
248
|
-
expect(briefing).not.toContain("@compaction")
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it("handles null agent without crashing", async () => {
|
|
252
|
-
const ctx = createMockCtx(tempDir)
|
|
253
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
254
|
-
|
|
255
|
-
await hooks["chat.message"]!(
|
|
256
|
-
{ agent: null, sessionID: "sess-1" } as any,
|
|
257
|
-
{ message: {}, parts: [] } as any
|
|
258
|
-
)
|
|
259
|
-
// Should not throw
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it("handles missing agent field", async () => {
|
|
263
|
-
const ctx = createMockCtx(tempDir)
|
|
264
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
265
|
-
|
|
266
|
-
await hooks["chat.message"]!(
|
|
267
|
-
{ sessionID: "sess-1" } as any,
|
|
268
|
-
{ message: {}, parts: [] } as any
|
|
269
|
-
)
|
|
270
|
-
// Should not throw
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
// =============================================================================
|
|
275
|
-
// INTEGRATION: chat.params hook (agent tracking backup)
|
|
276
|
-
// =============================================================================
|
|
277
|
-
describe("chat.params hook", () => {
|
|
278
|
-
let tempDir: string
|
|
279
|
-
|
|
280
|
-
beforeEach(async () => {
|
|
281
|
-
tempDir = await createTempDir()
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
afterEach(async () => {
|
|
285
|
-
await cleanupTempDir(tempDir)
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
it("tracks agent from params as backup", async () => {
|
|
289
|
-
const ctx = createMockCtx(tempDir)
|
|
290
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
291
|
-
|
|
292
|
-
await hooks["chat.params"]!(
|
|
293
|
-
{ agent: "pm", sessionID: "sess-1", model: {}, provider: {}, message: {} } as any,
|
|
294
|
-
{ temperature: 0.3, topP: 1, topK: 0, options: {} } as any
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
298
|
-
await hooks["experimental.session.compacting"]!(
|
|
299
|
-
{ sessionID: "sess-1" } as any,
|
|
300
|
-
output as any
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
const briefing = output.context.join("\n")
|
|
304
|
-
expect(briefing).toContain("@pm")
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it("ignores service agents in params", async () => {
|
|
308
|
-
const ctx = createMockCtx(tempDir)
|
|
309
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
310
|
-
|
|
311
|
-
await hooks["chat.params"]!(
|
|
312
|
-
{ agent: "title", sessionID: "sess-1", model: {}, provider: {}, message: {} } as any,
|
|
313
|
-
{ temperature: 0.3, topP: 1, topK: 0, options: {} } as any
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
317
|
-
await hooks["experimental.session.compacting"]!(
|
|
318
|
-
{ sessionID: "sess-1" } as any,
|
|
319
|
-
output as any
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
323
|
-
const briefing = output.context.join("\n")
|
|
324
|
-
expect(briefing).not.toContain("@title")
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
// =============================================================================
|
|
329
|
-
// INTEGRATION: experimental.session.compacting hook
|
|
330
|
-
// =============================================================================
|
|
331
|
-
describe("compaction hook", () => {
|
|
332
|
-
let tempDir: string
|
|
333
|
-
|
|
334
|
-
beforeEach(async () => {
|
|
335
|
-
tempDir = await createTempDir()
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
afterEach(async () => {
|
|
339
|
-
await cleanupTempDir(tempDir)
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
it("pushes context to output.context array", async () => {
|
|
343
|
-
const ctx = createMockCtx(tempDir)
|
|
344
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
345
|
-
|
|
346
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
347
|
-
await hooks["experimental.session.compacting"]!(
|
|
348
|
-
{ sessionID: "sess-1" } as any,
|
|
349
|
-
output as any
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
353
|
-
expect(typeof output.context[0]).toBe("string")
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it("includes read commands in output", async () => {
|
|
357
|
-
const ctx = createMockCtx(tempDir)
|
|
358
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
359
|
-
|
|
360
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
361
|
-
await hooks["experimental.session.compacting"]!(
|
|
362
|
-
{ sessionID: "sess-1" } as any,
|
|
363
|
-
output as any
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
const fullContext = output.context.join("\n")
|
|
367
|
-
expect(fullContext).toContain("Read")
|
|
368
|
-
expect(fullContext).toContain("AGENTS.md")
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
it("includes DO NOT ask instruction", async () => {
|
|
372
|
-
const ctx = createMockCtx(tempDir)
|
|
373
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
374
|
-
|
|
375
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
376
|
-
await hooks["experimental.session.compacting"]!(
|
|
377
|
-
{ sessionID: "sess-1" } as any,
|
|
378
|
-
output as any
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
const fullContext = output.context.join("\n")
|
|
382
|
-
expect(fullContext).toContain("DO NOT ask user")
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it("includes session state when available", async () => {
|
|
386
|
-
const dir = await createTempDir({
|
|
387
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
388
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
const ctx = createMockCtx(dir)
|
|
392
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
393
|
-
|
|
394
|
-
await hooks["chat.message"]!(
|
|
395
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
396
|
-
{ message: {}, parts: [] } as any
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
400
|
-
await hooks["experimental.session.compacting"]!(
|
|
401
|
-
{ sessionID: "sess-1" } as any,
|
|
402
|
-
output as any
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
const briefing = output.context.join("\n")
|
|
406
|
-
expect(briefing).toContain("AUTH-E01")
|
|
407
|
-
expect(briefing).toContain("AUTH-S01-03")
|
|
408
|
-
expect(briefing).toContain("/dev-epic")
|
|
409
|
-
|
|
410
|
-
await cleanupTempDir(dir)
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
it("formats dev context with story info from session state", async () => {
|
|
414
|
-
const storyPath = "docs/sprint-artifacts/sprint-1/stories/story-01-03-jwt-refresh.md"
|
|
415
|
-
const dir = await createTempDir({
|
|
416
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
417
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
418
|
-
[storyPath]: FIXTURE_STORY_MD,
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
const ctx = createMockCtx(dir)
|
|
422
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
423
|
-
|
|
424
|
-
await hooks["chat.message"]!(
|
|
425
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
426
|
-
{ message: {}, parts: [] } as any
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
430
|
-
await hooks["experimental.session.compacting"]!(
|
|
431
|
-
{ sessionID: "sess-1" } as any,
|
|
432
|
-
output as any
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
const briefing = output.context.join("\n")
|
|
436
|
-
expect(briefing).toContain("T2")
|
|
437
|
-
expect(briefing).toContain("T1")
|
|
438
|
-
expect(briefing).toContain("session-state.yaml")
|
|
439
|
-
|
|
440
|
-
await cleanupTempDir(dir)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
it("pushes context AND instructions (fix #5 verification)", async () => {
|
|
444
|
-
const dir = await createTempDir({
|
|
445
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
446
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
const ctx = createMockCtx(dir)
|
|
450
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
451
|
-
|
|
452
|
-
await hooks["chat.message"]!(
|
|
453
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
454
|
-
{ message: {}, parts: [] } as any
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
458
|
-
await hooks["experimental.session.compacting"]!(
|
|
459
|
-
{ sessionID: "sess-1" } as any,
|
|
460
|
-
output as any
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
// After fix #5: should have briefing + context + instructions = 3 items
|
|
464
|
-
expect(output.context.length).toBeGreaterThanOrEqual(2)
|
|
465
|
-
// The full output should contain both agent-specific context and resume instructions
|
|
466
|
-
const fullContext = output.context.join("\n")
|
|
467
|
-
// Context should have session state info
|
|
468
|
-
expect(fullContext).toContain("Session State")
|
|
469
|
-
// Instructions should have resume protocol
|
|
470
|
-
expect(fullContext).toContain("IN PROGRESS")
|
|
471
|
-
|
|
472
|
-
await cleanupTempDir(dir)
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it("falls back gracefully when no session state exists", async () => {
|
|
476
|
-
const ctx = createMockCtx(tempDir)
|
|
477
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
478
|
-
|
|
479
|
-
await hooks["chat.message"]!(
|
|
480
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
481
|
-
{ message: {}, parts: [] } as any
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
485
|
-
await hooks["experimental.session.compacting"]!(
|
|
486
|
-
{ sessionID: "sess-1" } as any,
|
|
487
|
-
output as any
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
491
|
-
const briefing = output.context.join("\n")
|
|
492
|
-
expect(briefing).toContain("@dev")
|
|
493
|
-
expect(briefing).toContain("Read")
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
it("formats architect-specific context", async () => {
|
|
497
|
-
const ctx = createMockCtx(tempDir)
|
|
498
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
499
|
-
|
|
500
|
-
await hooks["chat.message"]!(
|
|
501
|
-
{ agent: "architect", sessionID: "sess-1" } as any,
|
|
502
|
-
{ message: {}, parts: [] } as any
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
506
|
-
await hooks["experimental.session.compacting"]!(
|
|
507
|
-
{ sessionID: "sess-1" } as any,
|
|
508
|
-
output as any
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
const briefing = output.context.join("\n")
|
|
512
|
-
expect(briefing).toContain("@architect")
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
it("formats PM-specific context", async () => {
|
|
516
|
-
const ctx = createMockCtx(tempDir)
|
|
517
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
518
|
-
|
|
519
|
-
await hooks["chat.message"]!(
|
|
520
|
-
{ agent: "pm", sessionID: "sess-1" } as any,
|
|
521
|
-
{ message: {}, parts: [] } as any
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
525
|
-
await hooks["experimental.session.compacting"]!(
|
|
526
|
-
{ sessionID: "sess-1" } as any,
|
|
527
|
-
output as any
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
const briefing = output.context.join("\n")
|
|
531
|
-
expect(briefing).toContain("@pm")
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
it("handles unknown agent with default context", async () => {
|
|
535
|
-
const ctx = createMockCtx(tempDir)
|
|
536
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
537
|
-
|
|
538
|
-
await hooks["chat.message"]!(
|
|
539
|
-
{ agent: "custom-agent", sessionID: "sess-1" } as any,
|
|
540
|
-
{ message: {}, parts: [] } as any
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
544
|
-
await hooks["experimental.session.compacting"]!(
|
|
545
|
-
{ sessionID: "sess-1" } as any,
|
|
546
|
-
output as any
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
550
|
-
})
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
// =============================================================================
|
|
554
|
-
// INTEGRATION: event hook
|
|
555
|
-
// =============================================================================
|
|
556
|
-
describe("event hook", () => {
|
|
557
|
-
let tempDir: string
|
|
558
|
-
|
|
559
|
-
beforeEach(async () => {
|
|
560
|
-
tempDir = await createTempDir()
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
afterEach(async () => {
|
|
564
|
-
await cleanupTempDir(tempDir)
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
it("handles session.idle event without error", async () => {
|
|
568
|
-
const ctx = createMockCtx(tempDir)
|
|
569
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
570
|
-
|
|
571
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
it("handles unknown event types gracefully", async () => {
|
|
575
|
-
const ctx = createMockCtx(tempDir)
|
|
576
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
577
|
-
|
|
578
|
-
await hooks.event!({ event: { type: "unknown.event" } as any })
|
|
579
|
-
await hooks.event!({ event: { type: "todo.updated" } as any })
|
|
580
|
-
await hooks.event!({ event: { type: "file.edited" } as any })
|
|
581
|
-
})
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
// =============================================================================
|
|
585
|
-
// INTEGRATION: Epic state parsing (via compaction hook)
|
|
586
|
-
// =============================================================================
|
|
587
|
-
describe("epic state parsing", () => {
|
|
588
|
-
it("parses active epic state from sprint directory", async () => {
|
|
589
|
-
const dir = await createTempDir({
|
|
590
|
-
"docs/sprint-artifacts/sprint-1/.sprint-state/epic-01-state.yaml": FIXTURE_EPIC_STATE,
|
|
591
|
-
".opencode/state/todos.json": "[]",
|
|
592
|
-
})
|
|
593
|
-
|
|
594
|
-
const ctx = createMockCtx(dir)
|
|
595
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
596
|
-
|
|
597
|
-
await hooks["chat.message"]!(
|
|
598
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
599
|
-
{ message: {}, parts: [] } as any
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
603
|
-
await hooks["experimental.session.compacting"]!(
|
|
604
|
-
{ sessionID: "sess-1" } as any,
|
|
605
|
-
output as any
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
const briefing = output.context.join("\n")
|
|
609
|
-
expect(briefing).toContain("epic-01-state.yaml")
|
|
610
|
-
expect(briefing).toContain("@dev")
|
|
611
|
-
|
|
612
|
-
await cleanupTempDir(dir)
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
it("prefers session-state.yaml over epic state file", async () => {
|
|
616
|
-
const dir = await createTempDir({
|
|
617
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
618
|
-
"docs/sprint-artifacts/sprint-1/.sprint-state/epic-01-state.yaml": FIXTURE_EPIC_STATE,
|
|
619
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
const ctx = createMockCtx(dir)
|
|
623
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
624
|
-
|
|
625
|
-
await hooks["chat.message"]!(
|
|
626
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
627
|
-
{ message: {}, parts: [] } as any
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
631
|
-
await hooks["experimental.session.compacting"]!(
|
|
632
|
-
{ sessionID: "sess-1" } as any,
|
|
633
|
-
output as any
|
|
634
|
-
)
|
|
635
|
-
|
|
636
|
-
const briefing = output.context.join("\n")
|
|
637
|
-
expect(briefing).toContain("/dev-epic")
|
|
638
|
-
expect(briefing).toContain("AUTH-S01-03")
|
|
639
|
-
|
|
640
|
-
await cleanupTempDir(dir)
|
|
641
|
-
})
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
// =============================================================================
|
|
645
|
-
// INTEGRATION: TODO list in compaction
|
|
646
|
-
// =============================================================================
|
|
647
|
-
describe("TODO list integration", () => {
|
|
648
|
-
let tempDir: string
|
|
649
|
-
|
|
650
|
-
beforeEach(async () => {
|
|
651
|
-
tempDir = await createTempDir()
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
afterEach(async () => {
|
|
655
|
-
await cleanupTempDir(tempDir)
|
|
656
|
-
})
|
|
657
|
-
|
|
658
|
-
it("includes TODO status in compaction output", async () => {
|
|
659
|
-
const dir = await createTempDir({
|
|
660
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
const ctx = createMockCtx(dir)
|
|
664
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
665
|
-
|
|
666
|
-
await hooks["chat.message"]!(
|
|
667
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
668
|
-
{ message: {}, parts: [] } as any
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
672
|
-
await hooks["experimental.session.compacting"]!(
|
|
673
|
-
{ sessionID: "sess-1" } as any,
|
|
674
|
-
output as any
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
const briefing = output.context.join("\n")
|
|
678
|
-
expect(briefing).toContain("TODO")
|
|
679
|
-
expect(briefing).toMatch(/\d+ done/)
|
|
680
|
-
expect(briefing).toMatch(/\d+ in progress/)
|
|
681
|
-
|
|
682
|
-
await cleanupTempDir(dir)
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
it("handles empty TODO list gracefully", async () => {
|
|
686
|
-
const dir = await createTempDir({
|
|
687
|
-
".opencode/state/todos.json": "[]",
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
const ctx = createMockCtx(dir)
|
|
691
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
692
|
-
|
|
693
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
694
|
-
await hooks["experimental.session.compacting"]!(
|
|
695
|
-
{ sessionID: "sess-1" } as any,
|
|
696
|
-
output as any
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
700
|
-
|
|
701
|
-
await cleanupTempDir(dir)
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
it("handles missing TODO file gracefully", async () => {
|
|
705
|
-
const ctx = createMockCtx(tempDir)
|
|
706
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
707
|
-
|
|
708
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
709
|
-
await hooks["experimental.session.compacting"]!(
|
|
710
|
-
{ sessionID: "sess-1" } as any,
|
|
711
|
-
output as any
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
expect(output.context.length).toBeGreaterThan(0)
|
|
715
|
-
})
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
// =============================================================================
|
|
719
|
-
// MEMORY SAFETY
|
|
720
|
-
// =============================================================================
|
|
721
|
-
describe("memory safety", () => {
|
|
722
|
-
let tempDir: string
|
|
723
|
-
|
|
724
|
-
beforeEach(async () => {
|
|
725
|
-
tempDir = await createTempDir()
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
afterEach(async () => {
|
|
729
|
-
await cleanupTempDir(tempDir)
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
it("agent tracking overwrites, doesn't accumulate", async () => {
|
|
733
|
-
const ctx = createMockCtx(tempDir)
|
|
734
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
735
|
-
|
|
736
|
-
// Send 1000 different agents - should only track the latest
|
|
737
|
-
for (let i = 0; i < 1000; i++) {
|
|
738
|
-
await hooks["chat.message"]!(
|
|
739
|
-
{ agent: `agent-${i}`, sessionID: "sess-1" } as any,
|
|
740
|
-
{ message: {}, parts: [] } as any
|
|
741
|
-
)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
745
|
-
await hooks["experimental.session.compacting"]!(
|
|
746
|
-
{ sessionID: "sess-1" } as any,
|
|
747
|
-
output as any
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
const briefing = output.context.join("\n")
|
|
751
|
-
expect(briefing).toContain("@agent-999")
|
|
752
|
-
expect(briefing).not.toContain("@agent-0")
|
|
753
|
-
expect(briefing).not.toContain("@agent-500")
|
|
754
|
-
})
|
|
755
|
-
|
|
756
|
-
it("repeated compaction doesn't leak context", async () => {
|
|
757
|
-
const ctx = createMockCtx(tempDir)
|
|
758
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
759
|
-
|
|
760
|
-
await hooks["chat.message"]!(
|
|
761
|
-
{ agent: "dev", sessionID: "sess-1" } as any,
|
|
762
|
-
{ message: {}, parts: [] } as any
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
// Run compaction 100 times with FRESH output each time
|
|
766
|
-
for (let i = 0; i < 100; i++) {
|
|
767
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
768
|
-
await hooks["experimental.session.compacting"]!(
|
|
769
|
-
{ sessionID: "sess-1" } as any,
|
|
770
|
-
output as any
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
// Each compaction should produce a bounded number of context entries
|
|
774
|
-
// (briefing + context + instructions = up to 3)
|
|
775
|
-
expect(output.context.length).toBeGreaterThanOrEqual(1)
|
|
776
|
-
expect(output.context.length).toBeLessThanOrEqual(3)
|
|
777
|
-
}
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
it("compaction doesn't grow output.context with repeated calls", async () => {
|
|
781
|
-
const ctx = createMockCtx(tempDir)
|
|
782
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
783
|
-
|
|
784
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
785
|
-
|
|
786
|
-
const CALLS = 10
|
|
787
|
-
for (let i = 0; i < CALLS; i++) {
|
|
788
|
-
await hooks["experimental.session.compacting"]!(
|
|
789
|
-
{ sessionID: "sess-1" } as any,
|
|
790
|
-
output as any
|
|
791
|
-
)
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// The plugin pushes up to 3 items per call (briefing + context + instructions)
|
|
795
|
-
const itemsPerCall = output.context.length / CALLS
|
|
796
|
-
expect(itemsPerCall).toBeGreaterThanOrEqual(1)
|
|
797
|
-
expect(itemsPerCall).toBeLessThanOrEqual(3)
|
|
798
|
-
})
|
|
799
|
-
|
|
800
|
-
it("multiple plugin instances don't leak memory", async () => {
|
|
801
|
-
const memBefore = process.memoryUsage().heapUsed
|
|
802
|
-
|
|
803
|
-
for (let i = 0; i < 50; i++) {
|
|
804
|
-
const dir = await createTempDir()
|
|
805
|
-
const ctx = createMockCtx(dir)
|
|
806
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
807
|
-
|
|
808
|
-
await hooks["chat.message"]!(
|
|
809
|
-
{ agent: "dev", sessionID: `sess-${i}` } as any,
|
|
810
|
-
{ message: {}, parts: [] } as any
|
|
811
|
-
)
|
|
812
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
813
|
-
await hooks["experimental.session.compacting"]!(
|
|
814
|
-
{ sessionID: `sess-${i}` } as any,
|
|
815
|
-
output as any
|
|
816
|
-
)
|
|
817
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
818
|
-
|
|
819
|
-
await cleanupTempDir(dir)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if ((globalThis as any).gc) (globalThis as any).gc()
|
|
823
|
-
|
|
824
|
-
const memAfter = process.memoryUsage().heapUsed
|
|
825
|
-
const growthMB = (memAfter - memBefore) / 1024 / 1024
|
|
826
|
-
|
|
827
|
-
expect(growthMB).toBeLessThan(100)
|
|
828
|
-
})
|
|
829
|
-
})
|