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 +10 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/index.ts +107 -0
- package/memory.ts +99 -0
- package/package.json +37 -0
- package/types.ts +44 -0
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
|
+
}
|