opencode-team-memory 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - Initial release
6
+ - `role_memory_save`, `role_memory_load`, `role_memory_clear` tools
7
+ - Compaction hook for automatic context injection
8
+ - Cross-role memory reading support
9
+ - Data versioning (`version` field in context.json)
10
+ - Toggle on/off via `.omo/.team-memory-disabled` marker file
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # opencode-team-memory
2
+
3
+ Persistent role-based memory for OpenCode + Oh-My-OpenCode(OmO) Team Mode.
4
+ Context survives across sessions, Team Mode runs, and compaction.
5
+
6
+ ## Problem
7
+
8
+ OmO Team Mode members are ephemeral — they die when the run ends (default: 120 min).
9
+ Next session, every role starts from zero. No persistent Engineer context, no Tester NG history,
10
+ no Director decision trail.
11
+
12
+ ## Solution
13
+
14
+ Three tools that read/write role context to the filesystem:
15
+
16
+ | Tool | Purpose |
17
+ |---|---|
18
+ | `role_memory_save` | Persist decisions, NG history, scope, active files, handoff target |
19
+ | `role_memory_load` | Restore context from previous sessions (own role or cross-read others) |
20
+ | `role_memory_clear` | Reset memory for a fresh start |
21
+
22
+ Stored per-role at `<project>/.omo/team-memory/{role}/context.json`.
23
+ Override with `OPENCODE_TEAM_MEMORY_DIR` env var.
24
+
25
+ Compaction hook injects compact summaries automatically — long sessions keep context.
26
+
27
+ ## Enable / Disable
28
+
29
+ Toggle without editing config files. Restart `opencode` after switching.
30
+
31
+ **Disable**:
32
+ ```bash
33
+ touch .omo/.team-memory-disabled
34
+ ```
35
+
36
+ **Enable**:
37
+ ```bash
38
+ rm .omo/.team-memory-disabled
39
+ ```
40
+
41
+ Or register as custom commands in `opencode.json`:
42
+
43
+ ```jsonc
44
+ {
45
+ "command": {
46
+ "memory-on": {
47
+ "description": "Enable team memory plugin",
48
+ "command": "rm -f .omo/.team-memory-disabled && echo 'Team memory enabled (restart opencode)'"
49
+ },
50
+ "memory-off": {
51
+ "description": "Disable team memory plugin",
52
+ "command": "touch .omo/.team-memory-disabled && echo 'Team memory disabled (restart opencode)'"
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ Then type `/memory-off` or `/memory-on` in the TUI.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ bun add -d opencode-team-memory
64
+ # or: npm install --save-dev opencode-team-memory
65
+ ```
66
+
67
+ Register in `opencode.json`:
68
+
69
+ ```jsonc
70
+ {
71
+ "plugin": ["opencode-team-memory"]
72
+ }
73
+ ```
74
+
75
+ ## Team Member Prompt Template
76
+
77
+ Add this to each member's prompt in `.omo/teams/{name}/config.json`:
78
+
79
+ ```markdown
80
+ ## Persistent Memory Protocol
81
+
82
+ ### On session start (MANDATORY)
83
+ role_memory_load(role="<your-role>") before ANY work.
84
+ Cross-read other roles as needed (e.g. Tester reads Engineer's memory).
85
+
86
+ ### On session end / handoff (MANDATORY)
87
+ role_memory_save(role="<your-role>", raw="...", previous_decisions=[...])
88
+
89
+ ### Handoff
90
+ Before passing to the next role, set handoff_to="<next-role>" in your save.
91
+ ```
92
+
93
+ ### Role-specific prompts
94
+
95
+ <details>
96
+ <summary>Director (Lead / Sisyphus)</summary>
97
+
98
+ ```
99
+ You are the Director. Load memory first.
100
+ - previous_decisions: carry forward prior judgments
101
+ - excluded_scope: NEVER touch these areas
102
+ - On save: record every decision (adopt/reject/defer + reason)
103
+ - Do NOT dive into implementation details — delegate to Engineer
104
+ ```
105
+ </details>
106
+
107
+ <details>
108
+ <summary>Engineer (deep / hephaestus)</summary>
109
+
110
+ ```
111
+ You are the Engineer. Load memory first.
112
+ - ng_history: check for repeated bugs before coding
113
+ - active_files: be extra careful with these
114
+ - On save: record "what was committed, what changed, unresolved concerns"
115
+ - On handoff to Tester: put "what to test, preconditions, repro steps" in raw
116
+ - On handoff to Director: put "completed scope, known limits, unhandled edge cases" in raw
117
+ - Do NOT make spec decisions alone — confirm with Director via team_send_message
118
+ ```
119
+ </details>
120
+
121
+ <details>
122
+ <summary>Tester (quick / unspecified-low)</summary>
123
+
124
+ ```
125
+ You are the Tester. Load your memory AND Engineer's memory first.
126
+ - ng_history: extract verification points from prior failures
127
+ - On save: record test results (OK/NG), test angles covered
128
+ - On NG: team_send_message to Engineer with "NG item, expected, actual, repro steps"
129
+ AND role_memory_save with ng_history update
130
+ - On OK: team_send_message to Director
131
+ AND role_memory_save with full test results
132
+ - Do NOT fix issues yourself — always bounce back to Engineer
133
+ - Do NOT approve with "probably fine" — verify expected vs actual
134
+ ```
135
+ </details>
136
+
137
+ <details>
138
+ <summary>Designer (visual-engineering + frontend-ui-ux)</summary>
139
+
140
+ ```
141
+ You are the Designer. Load memory first.
142
+ - previous_decisions: recall prior UI choices and rejections
143
+ - ng_history: watch for recurring UX issues
144
+ - On save: record "adopted patterns, rejected alternatives, mobile feel, empty states"
145
+ - On handoff to Director: put "recommended UI, alternatives + tradeoffs, decisions needed" in raw
146
+ - On handoff to Engineer: put "adopted UI pattern, state transitions, edge case displays" in raw
147
+ - Do NOT let implementation difficulty skew UI judgment — Director decides
148
+ - Be specific: "vague discomfort" is not actionable
149
+ ```
150
+ </details>
151
+
152
+ ## Cross-role memory reading
153
+
154
+ Tester should read Engineer's memory for context:
155
+
156
+ ```
157
+ role_memory_load(role="engineer") // what was implemented, preconditions
158
+ role_memory_load(role="tester") // own context
159
+ ```
160
+
161
+ Designer should read Director's memory:
162
+
163
+ ```
164
+ role_memory_load(role="director") // scope, goals, explicit exclusions
165
+ ```
166
+
167
+ ## Environment Variables
168
+
169
+ | Variable | Default | Description |
170
+ |---|---|---|
171
+ | `OPENCODE_TEAM_MEMORY_DIR` | `<project>/.omo/team-memory` | Storage directory. **Warning**: If set globally, multiple projects share the same memory. Use per-project values or keep the default. |
172
+
173
+ ## Stored data structure
174
+
175
+ ```typescript
176
+ interface MemoryEntry {
177
+ version: number // schema version (for future migration)
178
+ role: string
179
+ project: string
180
+ last_updated: string
181
+ previous_decisions: string[] // max 50 entries
182
+ ng_history: string[] // max 50 entries
183
+ confirmed_scope: string[]
184
+ excluded_scope: string[]
185
+ active_files: string[]
186
+ handoff_to: string // target role for next handoff
187
+ raw_entries: string[] // max 50, load returns last 5
188
+ }
189
+ ```
190
+
191
+ ## Troubleshooting
192
+
193
+ **Tools not visible in Team members**
194
+ If OmO Team members can't see `role_memory_*` tools, the plugin tools may not be exposed to subagent sessions.
195
+ Workaround: wrap the same logic in an MCP server. A companion MCP package is planned.
196
+
197
+ **Stale context across runs**
198
+ Run `role_memory_clear(role="engineer")` to wipe and start fresh.
199
+
200
+ ## Versioning
201
+
202
+ - **MAJOR**: Data structure breaking changes (field removal, type change)
203
+ - **MINOR**: New tool, new optional field (backward compatible)
204
+ - **PATCH**: Bug fixes, doc updates
205
+
206
+ The `version` field in `context.json` enables future migration of old data.
207
+
208
+ ## License
209
+
210
+ MIT
package/index.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+ import { readFile, writeFile, mkdir, access } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import { ALL_ROLES, EMPTY_MEMORY, type MemoryEntry, type Role, type SaveInput } from "./types"
5
+ import { merge, format, formatCompact, formatSaveResult } from "./memory"
6
+
7
+ function getMemoryBase(directory: string): string {
8
+ return process.env.OPENCODE_TEAM_MEMORY_DIR || join(directory, ".omo", "team-memory")
9
+ }
10
+
11
+ function getDisabledMarker(directory: string): string {
12
+ return join(directory, ".omo", ".team-memory-disabled")
13
+ }
14
+
15
+ function isENOENT(err: unknown): err is NodeJS.ErrnoException {
16
+ return err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "ENOENT"
17
+ }
18
+
19
+ export const TeamMemoryPlugin: Plugin = async ({ directory }) => {
20
+ const marker = getDisabledMarker(directory)
21
+ try {
22
+ await access(marker)
23
+ return {}
24
+ } catch {
25
+ // marker file does not exist — plugin is enabled
26
+ }
27
+ const base = getMemoryBase(directory)
28
+
29
+ async function load(role: Role): Promise<MemoryEntry | null> {
30
+ try {
31
+ const text = await readFile(join(base, role, "context.json"), "utf-8")
32
+ return JSON.parse(text) as MemoryEntry
33
+ } catch (err: unknown) {
34
+ if (isENOENT(err)) {
35
+ return null
36
+ }
37
+ throw err
38
+ }
39
+ }
40
+
41
+ async function save(input: SaveInput): Promise<MemoryEntry> {
42
+ const dir = join(base, input.role)
43
+ await mkdir(dir, { recursive: true })
44
+ const existing = await load(input.role)
45
+ const merged = merge(existing, input, directory)
46
+ await writeFile(join(dir, "context.json"), JSON.stringify(merged, null, 2))
47
+ return merged
48
+ }
49
+
50
+ return {
51
+ tool: {
52
+ role_memory_save: tool({
53
+ description: "Save persistent role context — survives across sessions and Team Mode runs",
54
+ args: {
55
+ role: tool.schema.enum(ALL_ROLES),
56
+ previous_decisions: tool.schema.array(tool.schema.string()).optional(),
57
+ ng_history: tool.schema.array(tool.schema.string()).optional(),
58
+ confirmed_scope: tool.schema.array(tool.schema.string()).optional(),
59
+ excluded_scope: tool.schema.array(tool.schema.string()).optional(),
60
+ active_files: tool.schema.array(tool.schema.string()).optional(),
61
+ handoff_to: tool.schema.string().optional(),
62
+ raw: tool.schema.string().optional(),
63
+ },
64
+ async execute(args) {
65
+ const m = await save(args as SaveInput)
66
+ return formatSaveResult(m)
67
+ },
68
+ }),
69
+
70
+ role_memory_load: tool({
71
+ description: "Load persistent role context from previous sessions. Call for your own role at session start, or cross-read other roles as needed",
72
+ args: {
73
+ role: tool.schema.enum(ALL_ROLES),
74
+ },
75
+ async execute(args) {
76
+ return format(await load(args.role as Role), args.role as Role)
77
+ },
78
+ }),
79
+
80
+ role_memory_clear: tool({
81
+ description: "Clear all persistent memory for a role — use when starting fresh work",
82
+ args: {
83
+ role: tool.schema.enum(ALL_ROLES),
84
+ },
85
+ async execute(args) {
86
+ const dir = join(base, args.role as Role)
87
+ await mkdir(dir, { recursive: true })
88
+ await writeFile(join(dir, "context.json"), JSON.stringify(EMPTY_MEMORY(args.role as Role, directory), null, 2))
89
+ return `✓ Cleared all memory for '${args.role}'`
90
+ },
91
+ }),
92
+ },
93
+
94
+ "experimental.session.compacting": async (_input, output) => {
95
+ for (const role of ALL_ROLES) {
96
+ try {
97
+ const entry = await load(role)
98
+ if (entry) {
99
+ output.context.push(formatCompact(entry))
100
+ }
101
+ } catch {
102
+ // skip roles with no saved memory
103
+ }
104
+ }
105
+ },
106
+ }
107
+ }
package/memory.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { type MemoryEntry, type SaveInput, CURRENT_VERSION, EMPTY_MEMORY, type Role } from "./types"
2
+
3
+ const MAX_KEEP = 50
4
+ const RAW_LOAD_LIMIT = 5
5
+
6
+ export function merge(existing: MemoryEntry | null, input: SaveInput, project: string): MemoryEntry {
7
+ const now = new Date().toISOString()
8
+
9
+ if (!existing) {
10
+ return {
11
+ ...EMPTY_MEMORY(input.role, project),
12
+ last_updated: now,
13
+ previous_decisions: (input.previous_decisions || []).slice(-MAX_KEEP),
14
+ ng_history: (input.ng_history || []).slice(-MAX_KEEP),
15
+ confirmed_scope: input.confirmed_scope || [],
16
+ excluded_scope: input.excluded_scope || [],
17
+ active_files: input.active_files || [],
18
+ handoff_to: input.handoff_to !== undefined ? input.handoff_to : "",
19
+ raw_entries: input.raw ? [input.raw] : [],
20
+ }
21
+ }
22
+
23
+ const base = migrate(existing)
24
+
25
+ const append = (a: string[], b?: string[]): string[] =>
26
+ [...a, ...(b || [])].slice(-MAX_KEEP)
27
+
28
+ return {
29
+ ...base,
30
+ last_updated: now,
31
+ previous_decisions: append(base.previous_decisions, input.previous_decisions),
32
+ ng_history: append(base.ng_history, input.ng_history),
33
+ confirmed_scope: input.confirmed_scope || base.confirmed_scope,
34
+ excluded_scope: input.excluded_scope || base.excluded_scope,
35
+ active_files: input.active_files || base.active_files,
36
+ handoff_to: input.handoff_to !== undefined ? input.handoff_to : base.handoff_to,
37
+ raw_entries: input.raw
38
+ ? append(base.raw_entries, [input.raw])
39
+ : base.raw_entries,
40
+ }
41
+ }
42
+
43
+ function migrate(entry: MemoryEntry): MemoryEntry {
44
+ if (entry.version >= CURRENT_VERSION) return entry
45
+ return { ...entry, version: CURRENT_VERSION }
46
+ }
47
+
48
+ export function format(entry: MemoryEntry | null, role: Role): string {
49
+ if (!entry) return `No saved context for role '${role}'. This is a fresh start.`
50
+
51
+ const raw = entry.raw_entries.slice(-RAW_LOAD_LIMIT)
52
+
53
+ return [
54
+ `=== PERSISTENT CONTEXT: ${role} ===`,
55
+ `Project: ${entry.project}`,
56
+ `Last updated: ${entry.last_updated}`,
57
+ ``,
58
+ `## Previous Decisions (${entry.previous_decisions.length} total, showing last ${Math.min(entry.previous_decisions.length, RAW_LOAD_LIMIT)})`,
59
+ ...entry.previous_decisions.slice(-RAW_LOAD_LIMIT).map((d, i) => `${i + 1}. ${d}`),
60
+ ``,
61
+ `## NG History (${entry.ng_history.length} total, showing last ${Math.min(entry.ng_history.length, RAW_LOAD_LIMIT)})`,
62
+ ...entry.ng_history.slice(-RAW_LOAD_LIMIT).map((n, i) => `${i + 1}. ${n}`),
63
+ ``,
64
+ `## Confirmed Scope`,
65
+ ...entry.confirmed_scope.map((s) => `- ${s}`),
66
+ ``,
67
+ `## Excluded Scope (DO NOT TOUCH)`,
68
+ ...entry.excluded_scope.map((s) => `- ${s}`),
69
+ ``,
70
+ `## Active Files`,
71
+ ...entry.active_files.map((f) => `- ${f}`),
72
+ ``,
73
+ entry.handoff_to ? `## Handoff Target → ${entry.handoff_to}` : "",
74
+ ``,
75
+ `## Raw Context Entries (${entry.raw_entries.length} total, showing last ${raw.length})`,
76
+ ...raw.map((r, i) => `--- Entry ${entry.raw_entries.length - raw.length + i + 1} ---\n${r}`),
77
+ ].join("\n")
78
+ }
79
+
80
+ export function formatCompact(entry: MemoryEntry | null): string {
81
+ if (!entry) return "(no memory)"
82
+
83
+ return [
84
+ `## Team Memory: ${entry.role}`,
85
+ `Decisions: ${entry.previous_decisions.slice(-3).join("; ") || "none"}`,
86
+ `Recent NG: ${entry.ng_history.slice(-2).join("; ") || "none"}`,
87
+ `Scope: confirmed=[${entry.confirmed_scope.join(", ")}] excluded=[${entry.excluded_scope.join(", ")}]`,
88
+ `Files: ${entry.active_files.join(", ") || "none"}`,
89
+ ].join("\n")
90
+ }
91
+
92
+ export function formatSaveResult(entry: MemoryEntry): string {
93
+ return [
94
+ `✓ Saved context for '${entry.role}'`,
95
+ ` Decisions: ${entry.previous_decisions.length} | NG: ${entry.ng_history.length}`,
96
+ ` Scope: ${entry.confirmed_scope.length} confirmed / ${entry.excluded_scope.length} excluded`,
97
+ entry.handoff_to ? ` Next: → ${entry.handoff_to}` : "",
98
+ ].join("\n")
99
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "opencode-team-memory",
3
+ "version": "1.0.0",
4
+ "description": "Persistent role-based memory for OpenCode/OmO Team Mode — survives across sessions and runs",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "types.ts",
10
+ "memory.ts",
11
+ "README.md",
12
+ "LICENSE",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "bun test",
17
+ "prepublishOnly": "bun test"
18
+ },
19
+ "keywords": [
20
+ "opencode",
21
+ "opencode-plugin",
22
+ "oh-my-opencode",
23
+ "oh-my-openagent",
24
+ "team-mode",
25
+ "multi-agent",
26
+ "memory",
27
+ "persistence"
28
+ ],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/KenKozuma/opencode-team-memory"
33
+ },
34
+ "peerDependencies": {
35
+ "@opencode-ai/plugin": "*"
36
+ }
37
+ }
package/types.ts ADDED
@@ -0,0 +1,44 @@
1
+ export const CURRENT_VERSION = 1
2
+
3
+ export type Role = "engineer" | "tester" | "designer" | "director"
4
+
5
+ export const ALL_ROLES: Role[] = ["engineer", "tester", "designer", "director"]
6
+
7
+ export interface MemoryEntry {
8
+ version: number
9
+ role: Role
10
+ project: string
11
+ last_updated: string
12
+ previous_decisions: string[]
13
+ ng_history: string[]
14
+ confirmed_scope: string[]
15
+ excluded_scope: string[]
16
+ active_files: string[]
17
+ handoff_to: string
18
+ raw_entries: string[]
19
+ }
20
+
21
+ export const EMPTY_MEMORY = (role: Role, project: string): MemoryEntry => ({
22
+ version: CURRENT_VERSION,
23
+ role,
24
+ project,
25
+ last_updated: new Date().toISOString(),
26
+ previous_decisions: [],
27
+ ng_history: [],
28
+ confirmed_scope: [],
29
+ excluded_scope: [],
30
+ active_files: [],
31
+ handoff_to: "",
32
+ raw_entries: [],
33
+ })
34
+
35
+ export interface SaveInput {
36
+ role: Role
37
+ previous_decisions?: string[]
38
+ ng_history?: string[]
39
+ confirmed_scope?: string[]
40
+ excluded_scope?: string[]
41
+ active_files?: string[]
42
+ handoff_to?: string
43
+ raw?: string
44
+ }