opencode-team-memory 1.0.2 → 1.2.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 +18 -0
- package/README.md +35 -0
- package/bin/omo-resume +147 -0
- package/index.ts +34 -4
- package/memory.ts +47 -0
- package/package.json +5 -1
- package/types.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
- Add `omo-resume` CLI — auto-restore team context on opencode launch
|
|
6
|
+
- Three modes: `--always`, `--ask` (default), `--never`
|
|
7
|
+
- Reads latest role state from `.omo/team-memory/`
|
|
8
|
+
- Generates continuation prompt and passes to `opencode run`
|
|
9
|
+
|
|
10
|
+
## 1.1.0
|
|
11
|
+
|
|
12
|
+
- Add `formatContinuation()` — generates role-aware continuation prompt
|
|
13
|
+
- Add `experimental.chat.system.transform` hook — injects role context at session start
|
|
14
|
+
- Add Context-Mode compatible JSON metadata to compaction snapshots
|
|
15
|
+
- Sessions now auto-restore role context on restart
|
|
16
|
+
|
|
17
|
+
## 1.0.2
|
|
18
|
+
|
|
19
|
+
- Fix `scripts/preflight.sh` inclusion in npm tarball
|
|
20
|
+
|
|
3
21
|
## 1.0.1
|
|
4
22
|
|
|
5
23
|
- Add `scripts/preflight.sh` — automated installation check
|
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Persistent role-based memory for OpenCode + Oh-My-OpenCode(OmO) Team Mode.
|
|
4
4
|
Context survives across sessions, Team Mode runs, and compaction.
|
|
5
5
|
|
|
6
|
+
**v1.1.0**: Auto-role restoration on session start — Agent recognizes its role without manual reload.
|
|
7
|
+
|
|
6
8
|
## Problem
|
|
7
9
|
|
|
8
10
|
OmO Team Mode members are ephemeral — they die when the run ends (default: 120 min).
|
|
@@ -24,6 +26,39 @@ Override with `OPENCODE_TEAM_MEMORY_DIR` env var.
|
|
|
24
26
|
|
|
25
27
|
Compaction hook injects compact summaries automatically — long sessions keep context.
|
|
26
28
|
|
|
29
|
+
## Auto-Role Restoration (v1.1.0)
|
|
30
|
+
|
|
31
|
+
When opencode starts, the plugin checks saved role memory and injects a continuation prompt into the system prompt. The Agent automatically:
|
|
32
|
+
|
|
33
|
+
1. Recognizes its role (engineer / tester / designer / director)
|
|
34
|
+
2. Reads handoff state (who passed work and where it goes next)
|
|
35
|
+
3. Sees recent NG items and critical decisions
|
|
36
|
+
4. Knows confirmed and excluded scope
|
|
37
|
+
|
|
38
|
+
**No manual `role_memory_load` required.** The session starts with full context.
|
|
39
|
+
|
|
40
|
+
Output looks like:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
## Team Continuation (opencode-team-memory)
|
|
44
|
+
|
|
45
|
+
### Your Role: engineer
|
|
46
|
+
### Handoff → tester
|
|
47
|
+
### Status: ng_count=2
|
|
48
|
+
### Critical Context
|
|
49
|
+
use postgres; adopt JWT; add rate limit
|
|
50
|
+
### Recent NG Items
|
|
51
|
+
login redirect broken
|
|
52
|
+
token expiry too short
|
|
53
|
+
### Confirmed Scope
|
|
54
|
+
- auth module
|
|
55
|
+
|
|
56
|
+
### Instructions
|
|
57
|
+
1. role_memory_load(role="engineer") before starting
|
|
58
|
+
2. Handoff target is 'tester' — prepare output accordingly
|
|
59
|
+
3. Address NG items first
|
|
60
|
+
```
|
|
61
|
+
|
|
27
62
|
## Enable / Disable
|
|
28
63
|
|
|
29
64
|
Toggle without editing config files. Restart `opencode` after switching.
|
package/bin/omo-resume
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# omo-resume — 前回のチーム状態を復元して opencode を起動
|
|
3
|
+
# Usage: omo-resume [--always|--ask|--never]
|
|
4
|
+
|
|
5
|
+
set -eu
|
|
6
|
+
|
|
7
|
+
MEMORY_DIR="${OPENCODE_TEAM_MEMORY_DIR:-$(pwd)/.omo/team-memory}"
|
|
8
|
+
MODE="${OPENCODE_TEAM_CONTINUE:-ask}"
|
|
9
|
+
ROLES="engineer tester designer director"
|
|
10
|
+
|
|
11
|
+
# CLI引数でモード上書き
|
|
12
|
+
case "${1:-}" in
|
|
13
|
+
--always) MODE=always ;;
|
|
14
|
+
--ask) MODE=ask ;;
|
|
15
|
+
--never) MODE=never ;;
|
|
16
|
+
"") ;;
|
|
17
|
+
*) echo "Usage: omo-resume [--always|--ask|--never]" >&2; exit 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
# never モード: 通常起動
|
|
21
|
+
if [ "$MODE" = "never" ]; then
|
|
22
|
+
echo "Team continuation disabled. Starting opencode normally." >&2
|
|
23
|
+
exec opencode
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# メモリ読み取り
|
|
27
|
+
latest_role=""
|
|
28
|
+
latest_time=""
|
|
29
|
+
found=0
|
|
30
|
+
for role in $ROLES; do
|
|
31
|
+
file="$MEMORY_DIR/$role/context.json"
|
|
32
|
+
[ -f "$file" ] || continue
|
|
33
|
+
found=1
|
|
34
|
+
last_updated=$(python3 -c "
|
|
35
|
+
import json,sys
|
|
36
|
+
try:
|
|
37
|
+
d=json.load(open('$file'))
|
|
38
|
+
print(d.get('last_updated',''))
|
|
39
|
+
except: pass
|
|
40
|
+
" 2>/dev/null)
|
|
41
|
+
[ -n "$last_updated" ] || continue
|
|
42
|
+
if [ -z "$latest_time" ] || [ "$last_updated" \> "$latest_time" ]; then
|
|
43
|
+
latest_time="$last_updated"
|
|
44
|
+
latest_role="$role"
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# メモリがない場合: 通常起動
|
|
49
|
+
if [ "$found" -eq 0 ] || [ -z "$latest_role" ]; then
|
|
50
|
+
echo "No previous team context found. Starting opencode normally." >&2
|
|
51
|
+
exec opencode
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 復元プロンプト生成
|
|
55
|
+
prompt=$(python3 -c "
|
|
56
|
+
import json, sys
|
|
57
|
+
|
|
58
|
+
file = '$MEMORY_DIR/$latest_role/context.json'
|
|
59
|
+
with open(file) as f:
|
|
60
|
+
d = json.load(f)
|
|
61
|
+
|
|
62
|
+
decisions = '; '.join(d.get('previous_decisions', [])[-3:]) or 'none'
|
|
63
|
+
ng = d.get('ng_history', [])[-2:]
|
|
64
|
+
handoff = d.get('handoff_to', '')
|
|
65
|
+
confirmed = d.get('confirmed_scope', [])
|
|
66
|
+
excluded = d.get('excluded_scope', [])
|
|
67
|
+
|
|
68
|
+
lines = [
|
|
69
|
+
'Resume team work as $latest_role.',
|
|
70
|
+
'',
|
|
71
|
+
'## Team Continuation (omo-resume)',
|
|
72
|
+
'',
|
|
73
|
+
'### Your Role: $latest_role',
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if handoff:
|
|
77
|
+
lines.append('### Handoff \u2192 ' + handoff)
|
|
78
|
+
|
|
79
|
+
lines.extend([
|
|
80
|
+
'### Status: ng_count=' + str(len(d.get('ng_history', []))),
|
|
81
|
+
'',
|
|
82
|
+
'### Critical Context',
|
|
83
|
+
decisions,
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
if ng:
|
|
87
|
+
lines.append('')
|
|
88
|
+
lines.append('### Recent NG Items')
|
|
89
|
+
for item in ng:
|
|
90
|
+
lines.append('- ' + item)
|
|
91
|
+
|
|
92
|
+
if confirmed:
|
|
93
|
+
lines.append('')
|
|
94
|
+
lines.append('### Confirmed Scope')
|
|
95
|
+
for s in confirmed:
|
|
96
|
+
lines.append('- ' + s)
|
|
97
|
+
|
|
98
|
+
if excluded:
|
|
99
|
+
lines.append('')
|
|
100
|
+
lines.append('### Excluded (DO NOT TOUCH)')
|
|
101
|
+
for s in excluded:
|
|
102
|
+
lines.append('- ' + s)
|
|
103
|
+
|
|
104
|
+
lines.extend([
|
|
105
|
+
'',
|
|
106
|
+
'### Instructions',
|
|
107
|
+
'1. role_memory_load(role=\"$latest_role\") to restore full context',
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
if handoff:
|
|
111
|
+
lines.append(\"2. Handoff target is '\" + handoff + \"'. Prepare accordingly.\")
|
|
112
|
+
if len(d.get('ng_history', [])) > 0:
|
|
113
|
+
lines.append('3. Address NG items before handoff.')
|
|
114
|
+
elif len(d.get('ng_history', [])) > 0:
|
|
115
|
+
lines.append('2. Address NG items first.')
|
|
116
|
+
|
|
117
|
+
lines.append('')
|
|
118
|
+
print('\n'.join(lines))
|
|
119
|
+
" 2>/dev/null)
|
|
120
|
+
|
|
121
|
+
if [ -z "$prompt" ]; then
|
|
122
|
+
echo "Failed to generate continuation prompt. Starting opencode normally." >&2
|
|
123
|
+
exec opencode
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# 復元プロンプトを表示
|
|
127
|
+
echo "" >&2
|
|
128
|
+
echo "========================================" >&2
|
|
129
|
+
echo " Team Continuation" >&2
|
|
130
|
+
echo "========================================" >&2
|
|
131
|
+
echo " Role: $latest_role" >&2
|
|
132
|
+
echo " Updated: $latest_time" >&2
|
|
133
|
+
echo " Mode: $MODE" >&2
|
|
134
|
+
echo "========================================" >&2
|
|
135
|
+
echo "" >&2
|
|
136
|
+
|
|
137
|
+
# ask モード: 確認
|
|
138
|
+
if [ "$MODE" = "ask" ]; then
|
|
139
|
+
printf "Resume with this context? [Y/n] " >&2
|
|
140
|
+
read -r answer
|
|
141
|
+
case "$answer" in
|
|
142
|
+
[Nn]*) echo "Starting fresh session." >&2; exec opencode ;;
|
|
143
|
+
esac
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# always / ask(確認済み): opencode を起動
|
|
147
|
+
exec opencode run "$prompt"
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
|
2
2
|
import { readFile, writeFile, mkdir, access } from "node:fs/promises"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { ALL_ROLES, EMPTY_MEMORY, type MemoryEntry, type Role, type SaveInput } from "./types"
|
|
5
|
-
import { merge, format, formatCompact, formatSaveResult } from "./memory"
|
|
5
|
+
import { merge, format, formatCompact, formatSaveResult, formatContinuation } from "./memory"
|
|
6
6
|
|
|
7
7
|
function getMemoryBase(directory: string): string {
|
|
8
8
|
return process.env.OPENCODE_TEAM_MEMORY_DIR || join(directory, ".omo", "team-memory")
|
|
@@ -95,13 +95,43 @@ export const TeamMemoryPlugin: Plugin = async ({ directory }) => {
|
|
|
95
95
|
for (const role of ALL_ROLES) {
|
|
96
96
|
try {
|
|
97
97
|
const entry = await load(role)
|
|
98
|
-
if (entry)
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (!entry) continue
|
|
99
|
+
|
|
100
|
+
// Human-readable compact summary
|
|
101
|
+
output.context.push(formatCompact(entry))
|
|
102
|
+
|
|
103
|
+
// Context-Mode compatible: structured metadata for FTS5 indexing
|
|
104
|
+
output.context.push(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
source: "opencode-team-memory",
|
|
107
|
+
role: entry.role,
|
|
108
|
+
handoff_to: entry.handoff_to,
|
|
109
|
+
ng_count: entry.ng_history.length,
|
|
110
|
+
decisions: entry.previous_decisions.slice(-3),
|
|
111
|
+
last_updated: entry.last_updated,
|
|
112
|
+
})
|
|
113
|
+
)
|
|
101
114
|
} catch {
|
|
102
115
|
// skip roles with no saved memory
|
|
103
116
|
}
|
|
104
117
|
}
|
|
105
118
|
},
|
|
119
|
+
|
|
120
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
121
|
+
const blocks: string[] = []
|
|
122
|
+
for (const role of ALL_ROLES) {
|
|
123
|
+
try {
|
|
124
|
+
const entry = await load(role)
|
|
125
|
+
if (!entry) continue
|
|
126
|
+
const block = formatContinuation(entry, role)
|
|
127
|
+
if (block) blocks.push(block)
|
|
128
|
+
} catch {
|
|
129
|
+
// skip corrupted files
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (blocks.length > 0) {
|
|
133
|
+
output.context.push(blocks.join("\n\n"))
|
|
134
|
+
}
|
|
135
|
+
},
|
|
106
136
|
}
|
|
107
137
|
}
|
package/memory.ts
CHANGED
|
@@ -97,3 +97,50 @@ export function formatSaveResult(entry: MemoryEntry): string {
|
|
|
97
97
|
entry.handoff_to ? ` Next: → ${entry.handoff_to}` : "",
|
|
98
98
|
].join("\n")
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
export function formatContinuation(entry: MemoryEntry | null, role: Role): string {
|
|
102
|
+
if (!entry) return ""
|
|
103
|
+
|
|
104
|
+
const decisions = entry.previous_decisions.slice(-3).join("; ")
|
|
105
|
+
const ng = entry.ng_history.slice(-2)
|
|
106
|
+
|
|
107
|
+
const lines: string[] = [
|
|
108
|
+
"## Team Continuation (opencode-team-memory)",
|
|
109
|
+
"",
|
|
110
|
+
`### Your Role: ${role}`,
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
if (entry.handoff_to) {
|
|
114
|
+
lines.push(`### Handoff → ${entry.handoff_to}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lines.push(`### Status: ng_count=${entry.ng_history.length}`)
|
|
118
|
+
|
|
119
|
+
if (decisions) {
|
|
120
|
+
lines.push(`### Critical Context\n${decisions}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (ng.length > 0) {
|
|
124
|
+
lines.push(`### Recent NG Items\n${ng.join("\n")}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (entry.confirmed_scope.length > 0) {
|
|
128
|
+
lines.push(`### Confirmed Scope\n${entry.confirmed_scope.map(s => `- ${s}`).join("\n")}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (entry.excluded_scope.length > 0) {
|
|
132
|
+
lines.push(`### Excluded (DO NOT TOUCH)\n${entry.excluded_scope.map(s => `- ${s}`).join("\n")}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push(
|
|
136
|
+
"",
|
|
137
|
+
"### Instructions",
|
|
138
|
+
`1. role_memory_load(role="${role}") before starting`,
|
|
139
|
+
entry.handoff_to ? `2. Handoff target is '${entry.handoff_to}' — prepare output accordingly` : "",
|
|
140
|
+
entry.ng_history.length > 0 ? `${entry.handoff_to ? "3" : "2"}. Address NG items first` : "",
|
|
141
|
+
"0. Resume work based on context above",
|
|
142
|
+
""
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return lines.filter(Boolean).join("\n")
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-team-memory",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Persistent role-based memory for OpenCode/OmO Team Mode — survives across sessions and runs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"omo-resume": "bin/omo-resume"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
11
|
"index.ts",
|
|
9
12
|
"types.ts",
|
|
10
13
|
"memory.ts",
|
|
14
|
+
"bin/",
|
|
11
15
|
"scripts/",
|
|
12
16
|
"README.md",
|
|
13
17
|
"LICENSE",
|
package/types.ts
CHANGED
|
@@ -42,3 +42,14 @@ export interface SaveInput {
|
|
|
42
42
|
handoff_to?: string
|
|
43
43
|
raw?: string
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
export interface ContinueState {
|
|
47
|
+
role: Role
|
|
48
|
+
handoff_to: string
|
|
49
|
+
last_action: string
|
|
50
|
+
ng_count: number
|
|
51
|
+
confirmed_scope: string[]
|
|
52
|
+
excluded_scope: string[]
|
|
53
|
+
decisions_summary: string
|
|
54
|
+
ng_summary: string
|
|
55
|
+
}
|