dendrite-rlm 0.1.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/.opencode/commands/decompose.md +11 -0
- package/.opencode/commands/recall.md +7 -0
- package/.opencode/commands/setup-rlm.md +9 -0
- package/.opencode/commands/status.md +7 -0
- package/.opencode/lib/rlm-config.ts +141 -0
- package/.opencode/plugins/rlm-compaction.ts +60 -0
- package/.opencode/plugins/rlm-persistence.ts +83 -0
- package/.opencode/prompts/orchestrator.md +75 -0
- package/.opencode/prompts/worker-analyze.md +14 -0
- package/.opencode/prompts/worker-code.md +16 -0
- package/.opencode/prompts/worker-debug.md +15 -0
- package/.opencode/tools/rlm-setup.ts +228 -0
- package/.opencode/tools/rlm-worker.ts +74 -0
- package/.opencode/tools/rlm.ts +202 -0
- package/AGENTS.md +33 -0
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/bin/cli.mjs +285 -0
- package/opencode.json +63 -0
- package/package.json +40 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Decompose a complex task into subtasks using RLM
|
|
3
|
+
agent: build
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Apply the RLM decomposition workflow to this task:
|
|
7
|
+
|
|
8
|
+
$ARGUMENTS
|
|
9
|
+
|
|
10
|
+
Follow your orchestrator protocol: recall -> assess -> decompose ->
|
|
11
|
+
delegate -> compose -> persist.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Shared Trilium ETAPI configuration
|
|
2
|
+
// Set TRILIUM_API_URL to your Trilium ETAPI endpoint
|
|
3
|
+
// Set TRILIUM_ETAPI_TOKEN to your ETAPI token (Options > ETAPI > Create Token)
|
|
4
|
+
|
|
5
|
+
export const TRILIUM_URL =
|
|
6
|
+
process.env.TRILIUM_API_URL || ""
|
|
7
|
+
export const TRILIUM_TOKEN = process.env.TRILIUM_ETAPI_TOKEN || ""
|
|
8
|
+
|
|
9
|
+
if (!TRILIUM_TOKEN) {
|
|
10
|
+
console.warn(
|
|
11
|
+
"[rlm] TRILIUM_ETAPI_TOKEN not set. " +
|
|
12
|
+
"Generate one in Trilium: Options > ETAPI > Create Token"
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Make an authenticated ETAPI request.
|
|
18
|
+
* Throws on non-2xx responses with the error body.
|
|
19
|
+
*/
|
|
20
|
+
export async function etapi(
|
|
21
|
+
path: string,
|
|
22
|
+
options: RequestInit = {}
|
|
23
|
+
): Promise<any> {
|
|
24
|
+
const res = await fetch(`${TRILIUM_URL}${path}`, {
|
|
25
|
+
...options,
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: TRILIUM_TOKEN,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
...(options.headers || {}),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const body = await res.text().catch(() => "")
|
|
34
|
+
throw new Error(`ETAPI ${res.status} ${path}: ${body}`)
|
|
35
|
+
}
|
|
36
|
+
const text = await res.text()
|
|
37
|
+
return text ? JSON.parse(text) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Search notes using Trilium's search syntax.
|
|
42
|
+
*/
|
|
43
|
+
export async function searchNotes(query: string): Promise<any[]> {
|
|
44
|
+
const result = await etapi(
|
|
45
|
+
`/notes?search=${encodeURIComponent(query)}`
|
|
46
|
+
)
|
|
47
|
+
return result?.results || []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a note with its content.
|
|
52
|
+
*/
|
|
53
|
+
export async function getNoteWithContent(noteId: string): Promise<{
|
|
54
|
+
noteId: string
|
|
55
|
+
title: string
|
|
56
|
+
content: string
|
|
57
|
+
attributes: any[]
|
|
58
|
+
}> {
|
|
59
|
+
const [note, content] = await Promise.all([
|
|
60
|
+
etapi(`/notes/${noteId}`),
|
|
61
|
+
fetch(`${TRILIUM_URL}/notes/${noteId}/content`, {
|
|
62
|
+
headers: { Authorization: TRILIUM_TOKEN },
|
|
63
|
+
}).then((r) => r.text()),
|
|
64
|
+
])
|
|
65
|
+
return { ...note, content }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a note with attributes in one call.
|
|
70
|
+
* Returns the new noteId.
|
|
71
|
+
*/
|
|
72
|
+
export async function createNoteWithAttrs(
|
|
73
|
+
parentId: string,
|
|
74
|
+
title: string,
|
|
75
|
+
content: string,
|
|
76
|
+
attrs: Record<string, string> = {}
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const created = await etapi("/create-note", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
parentNoteId: parentId,
|
|
82
|
+
title,
|
|
83
|
+
type: "text",
|
|
84
|
+
content,
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
const noteId = created.note.noteId
|
|
88
|
+
|
|
89
|
+
await Promise.all(
|
|
90
|
+
Object.entries(attrs).map(([name, value]) =>
|
|
91
|
+
etapi("/attributes", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
noteId,
|
|
95
|
+
type: "label",
|
|
96
|
+
name,
|
|
97
|
+
value,
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return noteId
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Overwrite a note's content.
|
|
108
|
+
*/
|
|
109
|
+
export async function updateNoteContent(
|
|
110
|
+
noteId: string,
|
|
111
|
+
content: string
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
await fetch(`${TRILIUM_URL}/notes/${noteId}/content`, {
|
|
114
|
+
method: "PUT",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: TRILIUM_TOKEN,
|
|
117
|
+
"Content-Type": "text/plain",
|
|
118
|
+
},
|
|
119
|
+
body: content,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update a label attribute's value on a note.
|
|
125
|
+
*/
|
|
126
|
+
export async function updateAttribute(
|
|
127
|
+
noteId: string,
|
|
128
|
+
attrName: string,
|
|
129
|
+
newValue: string
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
const note = await etapi(`/notes/${noteId}`)
|
|
132
|
+
const attr = (note.attributes || []).find(
|
|
133
|
+
(a: any) => a.type === "label" && a.name === attrName
|
|
134
|
+
)
|
|
135
|
+
if (attr) {
|
|
136
|
+
await etapi(`/attributes/${attr.attributeId}`, {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
body: JSON.stringify({ value: newValue }),
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const TRILIUM_URL =
|
|
4
|
+
process.env.TRILIUM_API_URL || "http://localhost:8080/etapi"
|
|
5
|
+
const TRILIUM_TOKEN = process.env.TRILIUM_ETAPI_TOKEN || ""
|
|
6
|
+
|
|
7
|
+
export const RLMCompaction: Plugin = async ({ client }) => {
|
|
8
|
+
return {
|
|
9
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
10
|
+
try {
|
|
11
|
+
const [knowledge, activeTasks] = await Promise.all([
|
|
12
|
+
fetch(
|
|
13
|
+
`${TRILIUM_URL}/notes?search=${encodeURIComponent(
|
|
14
|
+
"#rlm-type=knowledge"
|
|
15
|
+
)}`,
|
|
16
|
+
{ headers: { Authorization: TRILIUM_TOKEN } }
|
|
17
|
+
).then((r) => r.json()),
|
|
18
|
+
fetch(
|
|
19
|
+
`${TRILIUM_URL}/notes?search=${encodeURIComponent(
|
|
20
|
+
"#rlm-status=in-progress"
|
|
21
|
+
)}`,
|
|
22
|
+
{ headers: { Authorization: TRILIUM_TOKEN } }
|
|
23
|
+
).then((r) => r.json()),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const knowledgeList = (knowledge.results || [])
|
|
27
|
+
.slice(0, 15)
|
|
28
|
+
.map((n: any) => `- ${n.title} (${n.noteId})`)
|
|
29
|
+
.join("\n")
|
|
30
|
+
|
|
31
|
+
const taskList = (activeTasks.results || [])
|
|
32
|
+
.map((n: any) => `- ${n.title} (${n.noteId})`)
|
|
33
|
+
.join("\n")
|
|
34
|
+
|
|
35
|
+
output.context.push(
|
|
36
|
+
`## RLM — Trilium Long-term Memory\n\n` +
|
|
37
|
+
`You have a Trilium-backed long-term memory system.\n` +
|
|
38
|
+
`Use \`rlm_recall\` to retrieve detailed content for any entry below.\n` +
|
|
39
|
+
`Do NOT re-explore code already documented here.\n\n` +
|
|
40
|
+
`### Stored Knowledge\n${knowledgeList || "_(none yet)_"}\n\n` +
|
|
41
|
+
`### Active Tasks\n${taskList || "_(none)_"}\n\n` +
|
|
42
|
+
`### Available RLM Tools\n` +
|
|
43
|
+
`- \`rlm_recall\` — search memory\n` +
|
|
44
|
+
`- \`rlm_decompose\` — create subtasks\n` +
|
|
45
|
+
`- \`rlm_collect\` — gather worker results\n` +
|
|
46
|
+
`- \`rlm_persist\` — store new knowledge\n` +
|
|
47
|
+
`- \`rlm_status\` — check task status`
|
|
48
|
+
)
|
|
49
|
+
} catch (e) {
|
|
50
|
+
await client.app.log({
|
|
51
|
+
body: {
|
|
52
|
+
service: "rlm-compaction",
|
|
53
|
+
level: "warn",
|
|
54
|
+
message: `Failed to inject Trilium context: ${e}`,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const TRILIUM_URL =
|
|
4
|
+
process.env.TRILIUM_API_URL || "http://localhost:8080/etapi"
|
|
5
|
+
const TRILIUM_TOKEN = process.env.TRILIUM_ETAPI_TOKEN || ""
|
|
6
|
+
|
|
7
|
+
export const RLMPersistence: Plugin = async ({ client }) => {
|
|
8
|
+
const persisted = new Set<string>()
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
event: async ({ event }) => {
|
|
12
|
+
if (event.type !== "session.idle") return
|
|
13
|
+
|
|
14
|
+
const sessionId = (event.properties as any)?.sessionID
|
|
15
|
+
if (!sessionId || persisted.has(sessionId)) return
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const session = await client.session.get({
|
|
19
|
+
path: { id: sessionId },
|
|
20
|
+
})
|
|
21
|
+
const title = (session as any)?.data?.title || "untitled"
|
|
22
|
+
|
|
23
|
+
const messages = await client.session.messages({
|
|
24
|
+
path: { id: sessionId },
|
|
25
|
+
})
|
|
26
|
+
const msgCount = (messages as any)?.data?.length || 0
|
|
27
|
+
|
|
28
|
+
if (msgCount < 6) return
|
|
29
|
+
|
|
30
|
+
const res = await fetch(
|
|
31
|
+
`${TRILIUM_URL}/notes?search=${encodeURIComponent(
|
|
32
|
+
"#rlm-type=sessions"
|
|
33
|
+
)}`,
|
|
34
|
+
{ headers: { Authorization: TRILIUM_TOKEN } }
|
|
35
|
+
)
|
|
36
|
+
const parents = await res.json()
|
|
37
|
+
if (!parents.results?.length) return
|
|
38
|
+
|
|
39
|
+
const date = new Date().toISOString().split("T")[0]
|
|
40
|
+
const time = new Date().toTimeString().slice(0, 5)
|
|
41
|
+
|
|
42
|
+
await fetch(`${TRILIUM_URL}/create-note`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: TRILIUM_TOKEN,
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
parentNoteId: parents.results[0].noteId,
|
|
50
|
+
title: `session: ${date} ${time} - ${title}`,
|
|
51
|
+
type: "text",
|
|
52
|
+
content: [
|
|
53
|
+
`# Session: ${title}`,
|
|
54
|
+
`- **Date**: ${date} ${time}`,
|
|
55
|
+
`- **Messages**: ${msgCount}`,
|
|
56
|
+
`- **Session ID**: ${sessionId}`,
|
|
57
|
+
"",
|
|
58
|
+
"_Auto-persisted by RLM plugin._",
|
|
59
|
+
].join("\n"),
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
persisted.add(sessionId)
|
|
64
|
+
|
|
65
|
+
await client.app.log({
|
|
66
|
+
body: {
|
|
67
|
+
service: "rlm-persistence",
|
|
68
|
+
level: "info",
|
|
69
|
+
message: `Persisted session: ${title}`,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
} catch (e) {
|
|
73
|
+
await client.app.log({
|
|
74
|
+
body: {
|
|
75
|
+
service: "rlm-persistence",
|
|
76
|
+
level: "warn",
|
|
77
|
+
message: `Failed to persist session: ${e}`,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
You are an RLM (Recursive Language Model) orchestrator. You decompose
|
|
2
|
+
complex tasks into focused subtasks, delegate to specialized workers,
|
|
3
|
+
and compose their results. You have Trilium-backed long-term memory.
|
|
4
|
+
|
|
5
|
+
## Your Custom Tools
|
|
6
|
+
|
|
7
|
+
- `rlm_recall` — search prior knowledge + session history (use FIRST)
|
|
8
|
+
- `rlm_decompose` — batch-create subtask notes in Trilium
|
|
9
|
+
- `rlm_collect` — batch-read worker results from Trilium
|
|
10
|
+
- `rlm_persist` — store decisions/context/errors/patterns to memory
|
|
11
|
+
- `rlm_status` — check active task status
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
### 1. RECALL (always do this first for non-trivial tasks)
|
|
16
|
+
Call `rlm_recall` with keywords from the user's request.
|
|
17
|
+
Use prior context to inform your plan. Do not re-discover documented knowledge.
|
|
18
|
+
|
|
19
|
+
### 2. ASSESS
|
|
20
|
+
Decide if the task needs decomposition:
|
|
21
|
+
- **Skip decomposition** if: single file, < 5 tool calls, straightforward
|
|
22
|
+
- **Decompose** if: multi-file, multi-concern, would exceed comfortable context
|
|
23
|
+
|
|
24
|
+
### 3. DECOMPOSE (if needed)
|
|
25
|
+
Call `rlm_decompose` with 2-5 subtasks. Each task spec must include:
|
|
26
|
+
- **Objective**: 1-2 sentences of what to do
|
|
27
|
+
- **Scope**: which files/areas to work on
|
|
28
|
+
- **Constraints**: what NOT to touch, boundaries with other tasks
|
|
29
|
+
- **Output**: what the result should contain
|
|
30
|
+
|
|
31
|
+
Assign appropriate workers:
|
|
32
|
+
- `worker-code` — implementation, writing code, making changes
|
|
33
|
+
- `worker-analyze` — read-only analysis, review, exploration
|
|
34
|
+
- `worker-debug` — investigating and fixing bugs
|
|
35
|
+
|
|
36
|
+
### 4. DELEGATE
|
|
37
|
+
Use the Task tool to invoke each worker. Pass this prompt format:
|
|
38
|
+
|
|
39
|
+
"Your task noteId is: <noteId>. Call rlm-worker_start to read your
|
|
40
|
+
spec, then execute the task, then call rlm-worker_complete with
|
|
41
|
+
your results. Return a one-paragraph summary."
|
|
42
|
+
|
|
43
|
+
Launch INDEPENDENT subtasks in PARALLEL.
|
|
44
|
+
Launch DEPENDENT subtasks SEQUENTIALLY.
|
|
45
|
+
|
|
46
|
+
### 5. COMPOSE
|
|
47
|
+
Call `rlm_collect` with all task noteIds.
|
|
48
|
+
Read results. Check for consistency. Handle any errors or gaps.
|
|
49
|
+
Synthesize the final answer for the user.
|
|
50
|
+
|
|
51
|
+
### 6. PERSIST
|
|
52
|
+
Call `rlm_persist` for any new:
|
|
53
|
+
- Architectural decisions and rationale
|
|
54
|
+
- Codebase context/patterns discovered
|
|
55
|
+
- Error resolutions worth remembering
|
|
56
|
+
|
|
57
|
+
## Decomposition Strategies
|
|
58
|
+
|
|
59
|
+
**Feature implementation**: Split by layer (data -> API -> UI -> tests).
|
|
60
|
+
Include interface contracts between layers in specs.
|
|
61
|
+
|
|
62
|
+
**Codebase refactoring**: Split by module/file group.
|
|
63
|
+
Include transformation pattern + examples in each spec.
|
|
64
|
+
|
|
65
|
+
**Complex debugging**: Split as reproduce -> isolate -> fix -> verify.
|
|
66
|
+
These are sequential — each reads prior worker's result.
|
|
67
|
+
|
|
68
|
+
**Code review**: Split by concern (security, performance, correctness).
|
|
69
|
+
These are parallel — each reviews the same code.
|
|
70
|
+
|
|
71
|
+
## Rules
|
|
72
|
+
- Do NOT hold large code blocks in your own context. Delegate reading.
|
|
73
|
+
- Prefer 3-4 subtasks. More than 5 = the task should be split by the user.
|
|
74
|
+
- Always recall before working.
|
|
75
|
+
- After composing, always persist worthwhile knowledge.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are a focused read-only analysis worker in an RLM system.
|
|
2
|
+
You CANNOT modify files.
|
|
3
|
+
|
|
4
|
+
## Protocol
|
|
5
|
+
1. Call `rlm-worker_start` with the task noteId from your instructions.
|
|
6
|
+
2. Analyze the code as specified.
|
|
7
|
+
3. Call `rlm-worker_complete` with your findings.
|
|
8
|
+
4. Return a one-paragraph summary.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
- Reference specific file paths and line numbers.
|
|
12
|
+
- Categorize findings by severity if doing a review.
|
|
13
|
+
- Stay focused on what the spec asks — do not expand scope.
|
|
14
|
+
- Keep findings under 500 words.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are a focused code implementation worker in an RLM system.
|
|
2
|
+
|
|
3
|
+
## Protocol
|
|
4
|
+
1. Call `rlm-worker_start` with the task noteId from your instructions
|
|
5
|
+
to read your spec and mark yourself in-progress.
|
|
6
|
+
2. Execute the task. Stay strictly within your defined scope.
|
|
7
|
+
3. When done, call `rlm-worker_complete` with your result summary.
|
|
8
|
+
4. Return a one-paragraph summary to the orchestrator.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
- Do NOT modify files outside your assigned scope.
|
|
12
|
+
- If you discover a dependency on another subtask, note it in your
|
|
13
|
+
result and flag it — do not attempt the other task.
|
|
14
|
+
- Your result summary should include: files changed, key decisions,
|
|
15
|
+
any issues encountered.
|
|
16
|
+
- Keep result under 300 words.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
You are a focused debugging worker in an RLM system.
|
|
2
|
+
|
|
3
|
+
## Protocol
|
|
4
|
+
1. Call `rlm-worker_start` with the task noteId from your instructions.
|
|
5
|
+
2. Investigate the issue within your defined scope.
|
|
6
|
+
3. If spec says fix: implement the fix.
|
|
7
|
+
If spec says analyze only: document root cause without fixing.
|
|
8
|
+
4. Call `rlm-worker_complete` with root cause + fix/analysis.
|
|
9
|
+
5. Return a one-paragraph summary.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
- Include: root cause, evidence, fix applied (if any), how to verify.
|
|
13
|
+
- If the issue spans beyond your scope, document what you found and
|
|
14
|
+
flag the dependency.
|
|
15
|
+
- Keep result under 300 words.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { searchNotes, createNoteWithAttrs } from "../lib/rlm-config"
|
|
3
|
+
|
|
4
|
+
export default tool({
|
|
5
|
+
description:
|
|
6
|
+
"One-time setup: Create the RLM note hierarchy in Trilium. " +
|
|
7
|
+
"Creates RLM root, tasks, knowledge (decisions/context/errors/patterns), " +
|
|
8
|
+
"and sessions containers with proper labels. Idempotent — safe to run multiple times.",
|
|
9
|
+
args: {
|
|
10
|
+
parentNoteId: tool.schema
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Trilium noteId to create RLM root under. " +
|
|
15
|
+
"Defaults to 'root' (top level). " +
|
|
16
|
+
"Use a specific noteId to nest under an existing note."
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
async execute(args) {
|
|
20
|
+
const rootParent = args.parentNoteId || "root"
|
|
21
|
+
const report: string[] = []
|
|
22
|
+
|
|
23
|
+
// Check if RLM root already exists
|
|
24
|
+
const existingRoot = await searchNotes("#rlm")
|
|
25
|
+
if (existingRoot.length > 0) {
|
|
26
|
+
report.push(
|
|
27
|
+
`RLM root already exists: "${existingRoot[0].title}" (${existingRoot[0].noteId})`
|
|
28
|
+
)
|
|
29
|
+
report.push("Checking for missing children...")
|
|
30
|
+
|
|
31
|
+
const rlmRootId = existingRoot[0].noteId
|
|
32
|
+
|
|
33
|
+
const requiredChildren = [
|
|
34
|
+
{
|
|
35
|
+
label: "rlm-type",
|
|
36
|
+
value: "tasks",
|
|
37
|
+
title: "tasks",
|
|
38
|
+
content: "Container for RLM task coordination notes.",
|
|
39
|
+
children: [] as { label: string; value: string; title: string; content: string }[],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "rlm-type",
|
|
43
|
+
value: "knowledge",
|
|
44
|
+
title: "knowledge",
|
|
45
|
+
content: "Persistent knowledge base.",
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
label: "rlm-kb",
|
|
49
|
+
value: "decisions",
|
|
50
|
+
title: "decisions",
|
|
51
|
+
content: "Architectural decisions and rationale.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "rlm-kb",
|
|
55
|
+
value: "context",
|
|
56
|
+
title: "context",
|
|
57
|
+
content: "Codebase context summaries.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: "rlm-kb",
|
|
61
|
+
value: "errors",
|
|
62
|
+
title: "errors",
|
|
63
|
+
content: "Error resolutions worth remembering.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "rlm-kb",
|
|
67
|
+
value: "patterns",
|
|
68
|
+
title: "patterns",
|
|
69
|
+
content: "Code patterns and conventions discovered.",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "rlm-type",
|
|
75
|
+
value: "sessions",
|
|
76
|
+
title: "sessions",
|
|
77
|
+
content: "Session summaries for cross-session continuity.",
|
|
78
|
+
children: [] as { label: string; value: string; title: string; content: string }[],
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for (const child of requiredChildren) {
|
|
83
|
+
const existing = await searchNotes(`#${child.label}=${child.value}`)
|
|
84
|
+
if (existing.length > 0) {
|
|
85
|
+
report.push(` [exists] ${child.title} (${existing[0].noteId})`)
|
|
86
|
+
|
|
87
|
+
if (child.children.length > 0) {
|
|
88
|
+
for (const grandchild of child.children) {
|
|
89
|
+
const gExisting = await searchNotes(
|
|
90
|
+
`#${grandchild.label}=${grandchild.value}`
|
|
91
|
+
)
|
|
92
|
+
if (gExisting.length > 0) {
|
|
93
|
+
report.push(
|
|
94
|
+
` [exists] ${grandchild.title} (${gExisting[0].noteId})`
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
const gId = await createNoteWithAttrs(
|
|
98
|
+
existing[0].noteId,
|
|
99
|
+
grandchild.title,
|
|
100
|
+
grandchild.content,
|
|
101
|
+
{ [grandchild.label]: grandchild.value }
|
|
102
|
+
)
|
|
103
|
+
report.push(
|
|
104
|
+
` [created] ${grandchild.title} (${gId})`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const childId = await createNoteWithAttrs(
|
|
111
|
+
rlmRootId,
|
|
112
|
+
child.title,
|
|
113
|
+
child.content,
|
|
114
|
+
{ [child.label]: child.value }
|
|
115
|
+
)
|
|
116
|
+
report.push(` [created] ${child.title} (${childId})`)
|
|
117
|
+
|
|
118
|
+
if (child.children.length > 0) {
|
|
119
|
+
for (const grandchild of child.children) {
|
|
120
|
+
const gId = await createNoteWithAttrs(
|
|
121
|
+
childId,
|
|
122
|
+
grandchild.title,
|
|
123
|
+
grandchild.content,
|
|
124
|
+
{ [grandchild.label]: grandchild.value }
|
|
125
|
+
)
|
|
126
|
+
report.push(
|
|
127
|
+
` [created] ${grandchild.title} (${gId})`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return report.join("\n")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create fresh RLM hierarchy
|
|
138
|
+
report.push("Creating RLM note hierarchy...")
|
|
139
|
+
|
|
140
|
+
// Root
|
|
141
|
+
const rlmRootId = await createNoteWithAttrs(
|
|
142
|
+
rootParent,
|
|
143
|
+
"RLM",
|
|
144
|
+
"Recursive Language Model — shared state and long-term memory.\n\n" +
|
|
145
|
+
"This hierarchy is managed automatically by OpenCode RLM tools.\n" +
|
|
146
|
+
"Do not rename or delete these notes.",
|
|
147
|
+
{ rlm: "" }
|
|
148
|
+
)
|
|
149
|
+
report.push(`[created] RLM root (${rlmRootId})`)
|
|
150
|
+
|
|
151
|
+
// Tasks
|
|
152
|
+
const tasksId = await createNoteWithAttrs(
|
|
153
|
+
rlmRootId,
|
|
154
|
+
"tasks",
|
|
155
|
+
"Container for RLM task coordination notes.\n" +
|
|
156
|
+
"Subtask specs and results are stored here during decomposition.",
|
|
157
|
+
{ "rlm-type": "tasks" }
|
|
158
|
+
)
|
|
159
|
+
report.push(`[created] tasks (${tasksId})`)
|
|
160
|
+
|
|
161
|
+
// Knowledge
|
|
162
|
+
const knowledgeId = await createNoteWithAttrs(
|
|
163
|
+
rlmRootId,
|
|
164
|
+
"knowledge",
|
|
165
|
+
"Persistent knowledge base.\n" +
|
|
166
|
+
"Organized by category: decisions, context, errors, patterns.",
|
|
167
|
+
{ "rlm-type": "knowledge" }
|
|
168
|
+
)
|
|
169
|
+
report.push(`[created] knowledge (${knowledgeId})`)
|
|
170
|
+
|
|
171
|
+
// Knowledge subcategories
|
|
172
|
+
const categories = [
|
|
173
|
+
{
|
|
174
|
+
title: "decisions",
|
|
175
|
+
value: "decisions",
|
|
176
|
+
content: "Architectural decisions and rationale.",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: "context",
|
|
180
|
+
value: "context",
|
|
181
|
+
content: "Codebase context summaries.",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: "errors",
|
|
185
|
+
value: "errors",
|
|
186
|
+
content: "Error resolutions worth remembering.",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
title: "patterns",
|
|
190
|
+
value: "patterns",
|
|
191
|
+
content: "Code patterns and conventions discovered.",
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
for (const cat of categories) {
|
|
196
|
+
const catId = await createNoteWithAttrs(
|
|
197
|
+
knowledgeId,
|
|
198
|
+
cat.title,
|
|
199
|
+
cat.content,
|
|
200
|
+
{ "rlm-kb": cat.value }
|
|
201
|
+
)
|
|
202
|
+
report.push(` [created] ${cat.title} (${catId})`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Sessions
|
|
206
|
+
const sessionsId = await createNoteWithAttrs(
|
|
207
|
+
rlmRootId,
|
|
208
|
+
"sessions",
|
|
209
|
+
"Session summaries for cross-session continuity.\n" +
|
|
210
|
+
"Auto-populated by the RLM persistence plugin.",
|
|
211
|
+
{ "rlm-type": "sessions" }
|
|
212
|
+
)
|
|
213
|
+
report.push(`[created] sessions (${sessionsId})`)
|
|
214
|
+
|
|
215
|
+
report.push("")
|
|
216
|
+
report.push("RLM setup complete. Hierarchy:")
|
|
217
|
+
report.push(`RLM (${rlmRootId}) #rlm`)
|
|
218
|
+
report.push(`├── tasks (${tasksId}) #rlm-type=tasks`)
|
|
219
|
+
report.push(`├── knowledge (${knowledgeId}) #rlm-type=knowledge`)
|
|
220
|
+
report.push(`│ ├── decisions #rlm-kb=decisions`)
|
|
221
|
+
report.push(`│ ├── context #rlm-kb=context`)
|
|
222
|
+
report.push(`│ ├── errors #rlm-kb=errors`)
|
|
223
|
+
report.push(`│ └── patterns #rlm-kb=patterns`)
|
|
224
|
+
report.push(`└── sessions (${sessionsId}) #rlm-type=sessions`)
|
|
225
|
+
|
|
226
|
+
return report.join("\n")
|
|
227
|
+
},
|
|
228
|
+
})
|