opencodekit 0.22.0 → 0.23.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +4 -25
  3. package/dist/template/.opencode/.template-manifest.json +115 -188
  4. package/dist/template/.opencode/AGENTS.md +21 -4
  5. package/dist/template/.opencode/README.md +1 -1
  6. package/dist/template/.opencode/agent/build.md +155 -13
  7. package/dist/template/.opencode/agent/plan.md +7 -16
  8. package/dist/template/.opencode/agent/scout.md +2 -2
  9. package/dist/template/.opencode/artifacts/.active +1 -0
  10. package/dist/template/.opencode/artifacts/example/plan.md +12 -0
  11. package/dist/template/.opencode/artifacts/example/progress.md +4 -0
  12. package/dist/template/.opencode/artifacts/example/research.md +4 -0
  13. package/dist/template/.opencode/artifacts/example/spec.md +16 -0
  14. package/dist/template/.opencode/artifacts/todo.md +5 -0
  15. package/dist/template/.opencode/artifacts/verify.log +4 -0
  16. package/dist/template/.opencode/command/clarify.md +6 -8
  17. package/dist/template/.opencode/command/create.md +29 -71
  18. package/dist/template/.opencode/command/design.md +1 -2
  19. package/dist/template/.opencode/command/explore.md +3 -4
  20. package/dist/template/.opencode/command/fix.md +0 -1
  21. package/dist/template/.opencode/command/init.md +1 -4
  22. package/dist/template/.opencode/command/plan.md +30 -60
  23. package/dist/template/.opencode/command/pr.md +10 -28
  24. package/dist/template/.opencode/command/refactor.md +0 -1
  25. package/dist/template/.opencode/command/research.md +7 -29
  26. package/dist/template/.opencode/command/review-codebase.md +6 -13
  27. package/dist/template/.opencode/command/ship.md +136 -78
  28. package/dist/template/.opencode/command/ui-review.md +2 -4
  29. package/dist/template/.opencode/command/verify.md +15 -23
  30. package/dist/template/.opencode/dcp-prompts/overrides/compress-range.md +89 -0
  31. package/dist/template/.opencode/dcp.jsonc +96 -96
  32. package/dist/template/.opencode/memory/README.md +1 -1
  33. package/dist/template/.opencode/memory/_templates/prd.md +1 -1
  34. package/dist/template/.opencode/memory/_templates/roadmap.md +1 -1
  35. package/dist/template/.opencode/memory/_templates/state.md +1 -1
  36. package/dist/template/.opencode/memory/project/gotchas.md +3 -3
  37. package/dist/template/.opencode/memory/project/project.md +2 -2
  38. package/dist/template/.opencode/memory/project/roadmap.md +1 -1
  39. package/dist/template/.opencode/memory/project/state.md +2 -2
  40. package/dist/template/.opencode/memory/project/tech-stack.md +2 -2
  41. package/dist/template/.opencode/opencode.json +112 -152
  42. package/dist/template/.opencode/plugin/README.md +11 -1
  43. package/dist/template/.opencode/plugin/session-summary.ts +542 -0
  44. package/dist/template/.opencode/skill/brainstorming/SKILL.md +1 -1
  45. package/dist/template/.opencode/skill/context-engineering/SKILL.md +1 -1
  46. package/dist/template/.opencode/skill/development-lifecycle/SKILL.md +26 -45
  47. package/dist/template/.opencode/skill/gemini-large-context/SKILL.md +4 -4
  48. package/dist/template/.opencode/skill/opensrc/references/example-workflow.md +1 -1
  49. package/dist/template/.opencode/skill/subagent-driven-development/SKILL.md +1 -1
  50. package/dist/template/.opencode/skill/using-git-worktrees/SKILL.md +6 -6
  51. package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +6 -6
  52. package/dist/template/.opencode/skill/verification-before-completion/references/VERIFICATION_PROTOCOL.md +5 -5
  53. package/package.json +76 -76
  54. package/dist/template/.opencode/plans/1768385996691-silent-wizard.md +0 -247
  55. package/dist/template/.opencode/plans/1770006237537-mighty-otter.md +0 -418
  56. package/dist/template/.opencode/plans/1770006913647-glowing-forest.md +0 -170
  57. package/dist/template/.opencode/plans/1770013678126-witty-planet.md +0 -278
  58. package/dist/template/.opencode/plans/1770112267595-shiny-rocket.md +0 -258
  59. package/dist/template/.opencode/plans/swarm-protocol.md +0 -123
  60. package/dist/template/.opencode/skill/beads/SKILL.md +0 -182
  61. package/dist/template/.opencode/skill/beads/references/BEST_PRACTICES.md +0 -27
  62. package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +0 -219
  63. package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +0 -124
  64. package/dist/template/.opencode/skill/beads/references/EXAMPLES.md +0 -45
  65. package/dist/template/.opencode/skill/beads/references/FILE_CLAIMING.md +0 -101
  66. package/dist/template/.opencode/skill/beads/references/GIT_SYNC.md +0 -25
  67. package/dist/template/.opencode/skill/beads/references/HIERARCHY.md +0 -71
  68. package/dist/template/.opencode/skill/beads/references/MULTI_AGENT.md +0 -40
  69. package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +0 -177
  70. package/dist/template/.opencode/skill/beads/references/SESSION_PROTOCOL.md +0 -61
  71. package/dist/template/.opencode/skill/beads/references/TASK_CREATION.md +0 -38
  72. package/dist/template/.opencode/skill/beads/references/TROUBLESHOOTING.md +0 -38
  73. package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +0 -226
@@ -11,6 +11,7 @@ plugin/
11
11
  ├── copilot-auth.ts # GitHub Copilot provider/auth integration
12
12
  ├── prompt-leverage.ts # Prompt pre-processing with structured execution framing
13
13
  ├── rtk.ts # Optional RTK command-output compression hook
14
+ ├── session-summary.ts # Structured persistent session summary (artifact trail, decisions, anchored merge)
14
15
  ├── skill-mcp.ts # Skill-scoped MCP bridge (skill_mcp tools)
15
16
  └── lib/
16
17
  ├── memory-tools.ts # 6 core memory tools (observation, search, get, read, update, timeline)
@@ -41,7 +42,7 @@ plugin/
41
42
  - Curates observations from distillations via pattern matching
42
43
  - Injects relevant knowledge into system prompt (BM25 _ recency _ confidence scoring)
43
44
  - Manages context window via messages.transform (token budget enforcement)
44
- - Merges compaction logic (beads, handoffs, project memory, knowledge)
45
+ - Merges compaction logic (plans, handoffs, project memory, knowledge)
45
46
  - Provides 3 tools: observation, memory-search, memory-admin
46
47
 
47
48
  - `sessions.ts`
@@ -69,6 +70,15 @@ plugin/
69
70
  - Rewrites low-risk `bash`/`shell` commands through `rtk rewrite`
70
71
  - Keeps an idempotency guard for symlinked global/project config double-loading
71
72
 
73
+ - `session-summary.ts`
74
+ - Maintains a structured, incrementally-updated session summary that survives DCP compression cycles
75
+ - **File-artifact tracking**: intercepts `read`, `edit`, `write`, `srcwalk_read` via `tool.execute.before` to track which files were read, modified, or created
76
+ - **Decision capture**: auto-tracks `observation(type:decision)` calls to log decisions with rationale
77
+ - **Anchored merge**: persists summary to `.opencode/state/session-summary.md` before compaction, merges incrementally rather than regenerating from scratch
78
+ - **Context injection**: injects structured `<session_summary>` block into system prompt via `experimental.chat.system.transform`
79
+ - **Intent guessing**: captures session intent from the first user message
80
+ - Inspired by Factory.ai's anchored iterative summarization research
81
+
72
82
  ## Notes
73
83
 
74
84
  - OpenCode auto-discovers every `.ts` file in `plugin/` as a plugin — keep helper modules in `lib/`
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Session Summary Plugin — Structured Persistent Context
3
+ *
4
+ * Maintains a structured, incrementally-updated session summary that survives
5
+ * DCP compression cycles ("anchored iterative summarization" — inspired by
6
+ * Factory.ai's approach). Tracks:
7
+ *
8
+ * 1. File artifact trail — which files were read, modified, or created
9
+ * 2. Decisions — what was decided and why (rationale + alternatives)
10
+ * 3. Session intent and state — what we're doing and where we are
11
+ * 4. Continuation — next steps to resume work without re-fetching
12
+ *
13
+ * On each system.transform, the summary is injected into context.
14
+ * On compaction, the summary is persisted to disk so it survives the cycle.
15
+ * The anchored design means we merge new information incrementally rather
16
+ * than regenerating from scratch (avoiding semantic drift per Factory's research).
17
+ *
18
+ * Persistence: .opencode/state/session-summary.md
19
+ * Hooks: tool.execute.before, experimental.chat.system.transform, experimental.session.compacting
20
+ *
21
+ * Inspired by: https://factory.ai/news/evaluating-compression
22
+ */
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import type { Plugin } from "@opencode-ai/plugin";
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ interface Decision {
32
+ what: string;
33
+ rationale: string;
34
+ }
35
+
36
+ interface SessionSummaryData {
37
+ intent: string;
38
+ state: "exploring" | "implementing" | "verifying" | "done" | "unknown";
39
+ files: {
40
+ modified: Map<string, string>; // path → what changed
41
+ created: Set<string>; // paths
42
+ read: Map<string, string>; // path → why examined / key finding
43
+ };
44
+ decisions: Decision[];
45
+ nextSteps: string[];
46
+ }
47
+
48
+ // ============================================================================
49
+ // Constants
50
+ // ============================================================================
51
+
52
+ /** Max artifact entries before we start evicting oldest reads */
53
+ const MAX_READS = 30;
54
+ const MAX_MODIFIED = 20;
55
+ const MAX_CREATED = 10;
56
+ const MAX_DECISIONS = 10;
57
+ const MAX_NEXT_STEPS = 8;
58
+ /** Target summary size in chars (~400 tokens * ~4 chars/token) */
59
+ const MAX_SUMMARY_CHARS = 1600;
60
+
61
+ // ============================================================================
62
+ // Helpers
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Extract a short change description from edit tool args.
67
+ */
68
+ function extractEditDetail(args: Record<string, unknown>): string {
69
+ const oldStr = String(args.oldString ?? "").trim();
70
+ const newStr = String(args.newString ?? "").trim();
71
+ if (!oldStr || !newStr) return "Modified";
72
+
73
+ // If old is much longer than new, it's a deletion/truncation
74
+ if (oldStr.length > newStr.length * 3) return "Truncated/reduced content";
75
+ // If new is much longer than old, it's an addition
76
+ if (newStr.length > oldStr.length * 3) return "Expanded content";
77
+ // Single-line change: show the first line
78
+ const oldLine = oldStr.split("\n")[0]?.trim() ?? "";
79
+ const newLine = newStr.split("\n")[0]?.trim() ?? "";
80
+ if (oldLine && newLine && oldLine !== newLine) {
81
+ const maxLen = 60;
82
+ const shortOld = oldLine.length > maxLen ? `${oldLine.slice(0, maxLen)}…` : oldLine;
83
+ const shortNew = newLine.length > maxLen ? `${newLine.slice(0, maxLen)}…` : newLine;
84
+ return `"${shortOld}" → "${shortNew}"`;
85
+ }
86
+ return "Modified";
87
+ }
88
+
89
+ /**
90
+ * Normalize file path: strip leading ./ and cwd prefix.
91
+ */
92
+ function normalizePath(filePath: string, cwd: string): string {
93
+ let normalized = filePath.startsWith("./") ? filePath.slice(2) : filePath;
94
+
95
+ // Strip path:line or path:start-end suffix (from srcwalk_read path:line format).
96
+ // e.g., "src/app.ts:44-89" → "src/app.ts"
97
+ normalized = normalized.replace(/:\d+(-\d+)?$/, "");
98
+
99
+ // If it's an absolute path, try to make it relative to cwd
100
+ if (path.isAbsolute(normalized)) {
101
+ const relative = path.relative(cwd, normalized);
102
+ // If relative doesn't start with .., it's inside cwd
103
+ if (!relative.startsWith("..")) return relative;
104
+ // Otherwise keep absolute but shortened
105
+ return normalized;
106
+ }
107
+ return normalized;
108
+ }
109
+
110
+ /**
111
+ * Format the summary for context injection (compact markdown).
112
+ * Uses XML-like block for easy delimiting in system prompt.
113
+ */
114
+ function formatSummary(s: SessionSummaryData): string {
115
+ const lines: string[] = [`intent: ${s.intent}`, `state: ${s.state}`, ""];
116
+
117
+ // Files section
118
+ const fileParts: string[] = [];
119
+
120
+ if (s.files.created.size > 0) {
121
+ fileParts.push(`created: ${[...s.files.created].map((p) => `\`${p}\``).join(", ")}`);
122
+ }
123
+
124
+ if (s.files.modified.size > 0) {
125
+ for (const [p, detail] of s.files.modified) {
126
+ fileParts.push(`modified: \`${p}\` — ${detail}`);
127
+ }
128
+ }
129
+
130
+ if (s.files.read.size > 0) {
131
+ // Only include reads that have a reason, plus a summary count
132
+ const readsWithReason = [...s.files.read.entries()].filter(([, r]) => r.length > 0);
133
+ if (readsWithReason.length > 0) {
134
+ for (const [p, reason] of readsWithReason) {
135
+ fileParts.push(`read: \`${p}\` — ${reason}`);
136
+ }
137
+ }
138
+ // Always note total read count
139
+ const extraReads = s.files.read.size - readsWithReason.length;
140
+ if (extraReads > 0) {
141
+ fileParts.push(`read: ${extraReads} more files (no specific notes)`);
142
+ }
143
+ }
144
+
145
+ if (fileParts.length > 0) {
146
+ lines.push("== files ==");
147
+ lines.push(...fileParts);
148
+ lines.push("");
149
+ }
150
+
151
+ // Decisions section
152
+ if (s.decisions.length > 0) {
153
+ lines.push("== decisions ==");
154
+ for (const d of s.decisions) {
155
+ const maxWhat = 120;
156
+ const what = d.what.length > maxWhat ? `${d.what.slice(0, maxWhat)}…` : d.what;
157
+ const maxRat = 200;
158
+ const rationale =
159
+ d.rationale.length > maxRat ? `${d.rationale.slice(0, maxRat)}…` : d.rationale;
160
+ lines.push(`- ${what} | ${rationale}`);
161
+ }
162
+ lines.push("");
163
+ }
164
+
165
+ // Next steps
166
+ if (s.nextSteps.length > 0) {
167
+ lines.push("== next ==");
168
+ for (const step of s.nextSteps) {
169
+ lines.push(`- ${step}`);
170
+ }
171
+ lines.push("");
172
+ }
173
+
174
+ let result = lines.join("\n").trim();
175
+
176
+ // Trim to max chars at line boundary
177
+ if (result.length > MAX_SUMMARY_CHARS) {
178
+ result = result.slice(0, MAX_SUMMARY_CHARS);
179
+ const lastNewline = result.lastIndexOf("\n");
180
+ if (lastNewline > 0) result = result.slice(0, lastNewline);
181
+ result += "\n… (summary truncated)";
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * Serialize summary to compact line-based format for disk persistence.
189
+ * Each section uses a single-letter prefix for parseability.
190
+ */
191
+ function serializeSummary(s: SessionSummaryData): string {
192
+ const lines: string[] = [];
193
+ lines.push(`I: ${s.intent}`);
194
+ lines.push(`S: ${s.state}`);
195
+
196
+ for (const p of s.files.created) {
197
+ lines.push(`C: ${p}`);
198
+ }
199
+ for (const [p, d] of s.files.modified) {
200
+ const detail = d.replace(/\n/g, " ");
201
+ if (detail) {
202
+ lines.push(`M: ${p} | ${detail}`);
203
+ } else {
204
+ lines.push(`M: ${p}`);
205
+ }
206
+ }
207
+ for (const [p, r] of s.files.read) {
208
+ const reason = r.replace(/\n/g, " ");
209
+ if (reason) {
210
+ lines.push(`R: ${p} | ${reason}`);
211
+ } else {
212
+ lines.push(`R: ${p}`);
213
+ }
214
+ }
215
+ for (const d of s.decisions) {
216
+ const what = d.what.replace(/\n/g, " ");
217
+ const rat = d.rationale.replace(/\n/g, " ");
218
+ if (rat) {
219
+ lines.push(`D: ${what} | ${rat}`);
220
+ } else {
221
+ lines.push(`D: ${what}`);
222
+ }
223
+ }
224
+ for (const step of s.nextSteps) {
225
+ lines.push(`N: ${step}`);
226
+ }
227
+
228
+ return lines.join("\n");
229
+ }
230
+
231
+ /**
232
+ * Parse the serialized format back into a SessionSummaryData.
233
+ */
234
+ function deserializeSummary(text: string): SessionSummaryData {
235
+ const summary: SessionSummaryData = {
236
+ intent: "",
237
+ state: "unknown",
238
+ files: {
239
+ modified: new Map(),
240
+ created: new Set(),
241
+ read: new Map(),
242
+ },
243
+ decisions: [],
244
+ nextSteps: [],
245
+ };
246
+
247
+ for (const line of text.split("\n")) {
248
+ const trimmed = line.trim();
249
+ if (!trimmed || trimmed.length < 3) continue;
250
+
251
+ const prefix = trimmed[0];
252
+ const content = trimmed.slice(2).trim();
253
+
254
+ if (!content) continue;
255
+
256
+ switch (prefix) {
257
+ case "I":
258
+ summary.intent = content;
259
+ break;
260
+ case "S":
261
+ if (["exploring", "implementing", "verifying", "done", "unknown"].includes(content)) {
262
+ summary.state = content as SessionSummaryData["state"];
263
+ }
264
+ break;
265
+ case "C":
266
+ summary.files.created.add(content);
267
+ break;
268
+ case "M": {
269
+ const pipeIdx = content.indexOf(" | ");
270
+ if (pipeIdx > 0) {
271
+ summary.files.modified.set(content.slice(0, pipeIdx), content.slice(pipeIdx + 3).trim());
272
+ } else {
273
+ summary.files.modified.set(content, "Modified");
274
+ }
275
+ break;
276
+ }
277
+ case "R": {
278
+ const pipeIdx = content.indexOf(" | ");
279
+ if (pipeIdx > 0) {
280
+ summary.files.read.set(content.slice(0, pipeIdx), content.slice(pipeIdx + 3).trim());
281
+ } else {
282
+ summary.files.read.set(content, "");
283
+ }
284
+ break;
285
+ }
286
+ case "D": {
287
+ const pipeIdx = content.indexOf(" | ");
288
+ if (pipeIdx > 0) {
289
+ summary.decisions.push({
290
+ what: content.slice(0, pipeIdx),
291
+ rationale: content.slice(pipeIdx + 3).trim(),
292
+ });
293
+ } else {
294
+ summary.decisions.push({ what: content, rationale: "" });
295
+ }
296
+ break;
297
+ }
298
+ case "N":
299
+ summary.nextSteps.push(content);
300
+ break;
301
+ }
302
+ }
303
+
304
+ return summary;
305
+ }
306
+
307
+ // ============================================================================
308
+ // Enforce max sizes — evict oldest entries
309
+ // ============================================================================
310
+
311
+ function enforceLimits(summary: SessionSummaryData): void {
312
+ // Reads: keep newest (Map preserves insertion order)
313
+ if (summary.files.read.size > MAX_READS) {
314
+ const entries = [...summary.files.read.entries()];
315
+ summary.files.read = new Map(entries.slice(entries.length - MAX_READS));
316
+ }
317
+ if (summary.files.modified.size > MAX_MODIFIED) {
318
+ const entries = [...summary.files.modified.entries()];
319
+ summary.files.modified = new Map(entries.slice(entries.length - MAX_MODIFIED));
320
+ }
321
+ if (summary.files.created.size > MAX_CREATED) {
322
+ summary.files.created = new Set([...summary.files.created].slice(-MAX_CREATED));
323
+ }
324
+ if (summary.decisions.length > MAX_DECISIONS) {
325
+ summary.decisions = summary.decisions.slice(-MAX_DECISIONS);
326
+ }
327
+ if (summary.nextSteps.length > MAX_NEXT_STEPS) {
328
+ summary.nextSteps = summary.nextSteps.slice(-MAX_NEXT_STEPS);
329
+ }
330
+ }
331
+
332
+ // ============================================================================
333
+ // Helper: addRead with dedup and reason preservation
334
+ // ============================================================================
335
+
336
+ function addRead(summary: SessionSummaryData, filePath: string, reason = ""): void {
337
+ // If already tracked as read, update reason only if new one is non-empty
338
+ if (summary.files.read.has(filePath) && !reason) return;
339
+ // Re-insert to update insertion order (move to "newest")
340
+ summary.files.read.delete(filePath);
341
+ summary.files.read.set(filePath, reason);
342
+ }
343
+
344
+ function addModified(summary: SessionSummaryData, filePath: string, detail: string): void {
345
+ summary.files.modified.set(filePath, detail);
346
+ // Remove from created if it was tracked as created
347
+ summary.files.created.delete(filePath);
348
+ }
349
+
350
+ function addDecision(summary: SessionSummaryData, what: string, rationale: string): void {
351
+ summary.decisions.push({ what, rationale });
352
+ }
353
+
354
+ function addCreated(summary: SessionSummaryData, filePath: string): void {
355
+ summary.files.created.add(filePath);
356
+ }
357
+
358
+ // ============================================================================
359
+ // Persistence
360
+ // ============================================================================
361
+
362
+ function ensureDir(dir: string): void {
363
+ if (!fs.existsSync(dir)) {
364
+ fs.mkdirSync(dir, { recursive: true });
365
+ }
366
+ }
367
+
368
+ function loadSummary(filePath: string): SessionSummaryData {
369
+ try {
370
+ if (fs.existsSync(filePath)) {
371
+ const text = fs.readFileSync(filePath, "utf-8").trim();
372
+ if (text) return deserializeSummary(text);
373
+ }
374
+ } catch {
375
+ /* Corrupted or missing — start fresh */
376
+ }
377
+ return {
378
+ intent: "",
379
+ state: "unknown",
380
+ files: { modified: new Map(), created: new Set(), read: new Map() },
381
+ decisions: [],
382
+ nextSteps: [],
383
+ };
384
+ }
385
+
386
+ function saveSummary(filePath: string, summary: SessionSummaryData): void {
387
+ try {
388
+ ensureDir(path.dirname(filePath));
389
+ fs.writeFileSync(filePath, serializeSummary(summary), "utf-8");
390
+ } catch {
391
+ /* Non-fatal — summary is best-effort */
392
+ }
393
+ }
394
+
395
+ // ============================================================================
396
+ // Plugin Export
397
+ // ============================================================================
398
+
399
+ export const SessionSummaryPlugin: Plugin = async ({ client, directory }) => {
400
+ const cwd = process.cwd();
401
+ const stateDir = path.join(directory, ".opencode", "state");
402
+ const summaryPath = path.join(stateDir, "session-summary.md");
403
+
404
+ // Load persisted summary (survives compaction)
405
+ const summary = loadSummary(summaryPath);
406
+
407
+ // Helper to log
408
+ const log = async (message: string, level: "info" | "warn" = "info") => {
409
+ try {
410
+ await client.app.log({
411
+ body: { service: "session-summary", level, message },
412
+ });
413
+ } catch {
414
+ /* Best-effort */
415
+ }
416
+ };
417
+
418
+ // Attempt to guess intent from the first user message we see
419
+ let intentGuessed = summary.intent.length > 0;
420
+
421
+ return {
422
+ // ================================================================
423
+ // File-Artifact Instrumentation
424
+ // Intercept tool calls before execution to track file operations.
425
+ // ================================================================
426
+ "tool.execute.before": async (input, output) => {
427
+ const tool = input.tool?.toLowerCase() ?? "";
428
+ const args = (output.args as Record<string, unknown>) ?? {};
429
+ const filePath = String(args.filePath ?? args.path ?? "").trim();
430
+
431
+ if (!filePath) return;
432
+
433
+ const normalized = normalizePath(filePath, cwd);
434
+
435
+ switch (tool) {
436
+ case "read":
437
+ addRead(summary, normalized);
438
+ break;
439
+ case "edit":
440
+ addModified(summary, normalized, extractEditDetail(args));
441
+ break;
442
+ case "write": {
443
+ // Distinguish create from overwrite
444
+ const absolutePath = path.isAbsolute(normalized)
445
+ ? normalized
446
+ : path.join(cwd, normalized);
447
+ if (!fs.existsSync(absolutePath)) {
448
+ addCreated(summary, normalized);
449
+ }
450
+ addModified(summary, normalized, "Written/created");
451
+ break;
452
+ }
453
+ case "srcwalk_read":
454
+ addRead(summary, normalized, "Code navigation");
455
+ break;
456
+ case "grep":
457
+ case "srcwalk_search":
458
+ case "glob":
459
+ case "srcwalk_files":
460
+ // Search tools — not tracking individual files
461
+ break;
462
+ }
463
+ },
464
+
465
+ // ================================================================
466
+ // Context Injection
467
+ // Inject the structured summary into the system prompt on every
468
+ // system.transform cycle, so it's always available after DCP
469
+ // compression clears older conversation spans.
470
+ // ================================================================
471
+ "experimental.chat.system.transform": async (_input, output) => {
472
+ // If summary is empty, don't waste tokens
473
+ const hasContent =
474
+ summary.intent ||
475
+ summary.files.modified.size > 0 ||
476
+ summary.files.created.size > 0 ||
477
+ summary.decisions.length > 0;
478
+
479
+ if (!hasContent) return;
480
+
481
+ const formatted = formatSummary(summary);
482
+ output.system.push(`\n<session_summary>\n${formatted}\n</session_summary>`);
483
+ },
484
+
485
+ // ================================================================
486
+ // Compaction Anchor
487
+ // Before DCP compression fires:
488
+ // 1. Persist the summary to disk (so it survives the compression)
489
+ // 2. Instruct the summarizer to preserve the summary's data
490
+ // ================================================================
491
+ "experimental.session.compacting": async (_input, output) => {
492
+ // Persist current summary state
493
+ enforceLimits(summary);
494
+ saveSummary(summaryPath, summary);
495
+ await log("Session summary persisted for compaction");
496
+
497
+ // Add instructions to the compaction prompt
498
+ const existingPrompt = output.prompt ?? "";
499
+ output.prompt = `${existingPrompt}
500
+
501
+ <session_summary_anchor>
502
+ The session artifact trail is tracked in .opencode/state/session-summary.md.
503
+ Preserve all file paths, decisions, and next steps noted there.
504
+ Include the updated summary in your compression output.
505
+ </session_summary_anchor>`;
506
+ },
507
+
508
+ // ================================================================
509
+ // Generic Event Handler
510
+ // Capture session intent from first user message.
511
+ // Also hook observation(type:decision) to auto-track decisions.
512
+ // ================================================================
513
+ event: async (input: unknown) => {
514
+ const ev = (input as { event?: { type?: string; properties?: Record<string, unknown> } })
515
+ ?.event;
516
+ if (!ev?.type) return;
517
+
518
+ // Capture session intent from first substantive user message
519
+ if (!intentGuessed && ev.type === "message.updated") {
520
+ const props = ev.properties as Record<string, unknown> | undefined;
521
+ const content = props?.content as string | undefined;
522
+ if (content && content.length > 10 && content.length < 500) {
523
+ summary.intent = content.slice(0, 200);
524
+ intentGuessed = true;
525
+ }
526
+ }
527
+
528
+ // Track decisions from observation tool
529
+ if (ev.type === "tool.execute.after") {
530
+ const props = ev.properties as Record<string, unknown> | undefined;
531
+ if (props?.tool === "observation") {
532
+ const args = props?.args as Record<string, unknown> | undefined;
533
+ if (args?.type === "decision" && args?.title) {
534
+ addDecision(summary, String(args.title), String(args.narrative ?? args.content ?? ""));
535
+ }
536
+ }
537
+ }
538
+ },
539
+ };
540
+ };
541
+
542
+ export default SessionSummaryPlugin;
@@ -57,7 +57,7 @@ Start by understanding the current project context, then ask questions one at a
57
57
 
58
58
  **Documentation:**
59
59
 
60
- - Write the validated design to `.beads/artifacts/<bead-id>/design.md`
60
+ - Write the validated design to `.opencode/artifacts/<slug>/design.md`
61
61
  - Use template from `.opencode/memory/_templates/design.md`
62
62
  - Use elements-of-style:writing-clearly-and-concisely skill if available
63
63
  - Commit the design document to git
@@ -169,7 +169,7 @@ These files are the project's invariant layer. Always available, never stale:
169
169
  These are created fresh per task and cleaned up after:
170
170
 
171
171
  ```
172
- .beads/artifacts/<task-id>/
172
+ .opencode/artifacts/<slug>/
173
173
  ├── delegation.md # Task-specific instructions
174
174
  ├── spec.md # Technical requirements
175
175
  └── progress.txt # Task state (append-only)