suemo 0.1.6 → 0.1.7
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/README.md +23 -0
- package/package.json +1 -1
- package/skills/suemo/SKILL.md +26 -1
- package/skills/suemo/references/agents-snippet.md +20 -9
- package/skills/suemo/references/cli-reference.md +7 -2
- package/skills/suemo/references/core-workflow.md +38 -10
- package/skills/suemo/references/manual-test-plan.md +17 -6
- package/skills/suemo/references/mcp-reference.md +9 -1
- package/skills/suemo/references/schema-retention-longevity.md +1 -1
- package/skills/suemo/references/sync-local-vps.md +1 -1
- package/src/AGENTS.md +84 -104
- package/src/cli/commands/believe.ts +3 -0
- package/src/cli/commands/init.ts +27 -1
- package/src/cli/index.ts +20 -0
- package/src/mcp/dispatch.ts +14 -4
- package/src/mcp/stdio.ts +3 -2
- package/src/opencode/plugin.ts +105 -0
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
suemo gives AI agents a memory that survives across sessions, models, and runtimes. Write observations from a Telegram bot, query them from OpenCode, consolidate overnight — all agents share one source of truth in SurrealDB.
|
|
13
13
|
|
|
14
|
+
Canonical behavioral specification: **[`SPEC.md`](./SPEC.md)**.
|
|
15
|
+
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
## Features
|
|
@@ -445,6 +447,27 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
|
|
|
445
447
|
|
|
446
448
|
Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
|
|
447
449
|
|
|
450
|
+
### Memory field semantics (quick reference)
|
|
451
|
+
|
|
452
|
+
- `content`: canonical memory payload.
|
|
453
|
+
- `summary`: optional condensed text for compact retrieval/consolidation. It does **not** replace `content`.
|
|
454
|
+
- `source`: optional provenance label (free-form string), e.g. `prompt-capture`, `consolidation:nrem`.
|
|
455
|
+
- `prompt_source`: optional pointer to the prompt memory record that originated a derived memory.
|
|
456
|
+
- `fsrs_next_review`: optional review timestamp; currently set on `recall()` (not on every ingest).
|
|
457
|
+
|
|
458
|
+
### Episode `memory_ids`
|
|
459
|
+
|
|
460
|
+
`episode.memory_ids` stores memory IDs attached to an open session, used for session reconstruction.
|
|
461
|
+
|
|
462
|
+
If you rarely see it populated, the typical reason is missing `sessionId` on write calls.
|
|
463
|
+
Current behavior: only write paths invoked with `sessionId` append to `memory_ids`.
|
|
464
|
+
`believe` now supports session linkage too:
|
|
465
|
+
|
|
466
|
+
- CLI: `suemo believe "..." --session <sessionId>`
|
|
467
|
+
- MCP: `believe({ content, sessionId, ... })`
|
|
468
|
+
|
|
469
|
+
See `SPEC.md` for full normative semantics and hardening targets.
|
|
470
|
+
|
|
448
471
|
### Scope and longevity notes
|
|
449
472
|
|
|
450
473
|
- Default inferred project scope now uses nearest `<projectDir>/suemo.json` with `main.projectDir` defaulting to `.ua`.
|
package/package.json
CHANGED
package/skills/suemo/SKILL.md
CHANGED
|
@@ -3,13 +3,38 @@ name: suemo
|
|
|
3
3
|
description: OpenCode-focused persistent memory workflow for suemo with CLI/MCP parity and versioned references.
|
|
4
4
|
license: GPL-3.0-only
|
|
5
5
|
compatibility: opencode
|
|
6
|
-
version: 0.1.
|
|
6
|
+
version: 0.1.7
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# suemo skill
|
|
10
10
|
|
|
11
11
|
Use suemo to persist technical context across sessions with minimal, project-scoped memory.
|
|
12
12
|
|
|
13
|
+
## Strict defaults (v0.1.7)
|
|
14
|
+
|
|
15
|
+
- Always run pre-checks before implementation:
|
|
16
|
+
- `goal_list({ scope })`
|
|
17
|
+
- `query({ input: "recent work on <topic>", scope })`
|
|
18
|
+
- `context({ scope, limit: 20 })`
|
|
19
|
+
- `observe.kind` accepts only: `observation | belief | question | hypothesis | goal`
|
|
20
|
+
- For non-trivial writes, prefer structured content:
|
|
21
|
+
- `**What**`, `**Why**`, `**Where**`, `**Learned**`
|
|
22
|
+
- End meaningful tasks with:
|
|
23
|
+
- `episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })`
|
|
24
|
+
- `goal_resolve({ goalId })` when done
|
|
25
|
+
- `query({ input: "most recent observations", scope })`
|
|
26
|
+
- `health()`
|
|
27
|
+
|
|
28
|
+
## OpenCode plugin support
|
|
29
|
+
|
|
30
|
+
`suemo init opencode` now installs three things:
|
|
31
|
+
|
|
32
|
+
1. MCP config entry for suemo
|
|
33
|
+
2. AGENTS.md suemo block
|
|
34
|
+
3. OpenCode plugin at `~/.config/opencode/plugins/suemo.ts`
|
|
35
|
+
|
|
36
|
+
The plugin injects strict suemo protocol into system prompt and adds compaction checkpoint reminders.
|
|
37
|
+
|
|
13
38
|
## Quick use
|
|
14
39
|
|
|
15
40
|
- CLI latest skill: `suemo skill`
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agents-snippet
|
|
3
3
|
description: AGENTS.md snippet optimized for suemo skill discovery and usage.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# AGENTS.md snippet
|
|
8
8
|
|
|
9
9
|
```md
|
|
10
|
-
## suemo-first memory workflow
|
|
10
|
+
## suemo-first memory workflow (strict)
|
|
11
11
|
|
|
12
12
|
Before significant work:
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- suemo_goal_list({ scope: "<active-scope>" })
|
|
15
|
+
- suemo_query({ input: "recent work on <topic>", scope: "<active-scope>" })
|
|
16
|
+
- suemo_context({ scope: "<active-scope>", limit: 20 })
|
|
17
|
+
- suemo_episode_start({ sessionId: "<session-id>" }) for new tasks
|
|
18
|
+
|
|
19
|
+
Schema guardrails:
|
|
20
|
+
|
|
21
|
+
- suemo_observe.kind must be one of: observation | belief | question | hypothesis | goal
|
|
22
|
+
- Never use discovery/architecture/bugfix as kind
|
|
15
23
|
|
|
16
24
|
If you need latest usage docs:
|
|
17
25
|
|
|
@@ -21,16 +29,19 @@ If you need latest usage docs:
|
|
|
21
29
|
|
|
22
30
|
During work:
|
|
23
31
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
32
|
+
- suemo_observe({ content: "**What**: ...\n**Why**: ...\n**Where**: ...\n**Learned**: ...", kind: "observation" })
|
|
33
|
+
- suemo_believe("...") for uncertain interpretations
|
|
34
|
+
- suemo_goal_set("...") for long tasks
|
|
27
35
|
|
|
28
36
|
After completion:
|
|
29
37
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
38
|
+
- suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })
|
|
39
|
+
- suemo_goal_resolve({ goalId }) if applicable
|
|
40
|
+
- suemo_query({ input: "most recent observations", scope: "<active-scope>" })
|
|
41
|
+
- suemo_health()
|
|
32
42
|
|
|
33
43
|
After compaction/reset:
|
|
34
44
|
|
|
35
|
-
-
|
|
45
|
+
- suemo_session_context_set({ sessionId, summary: "<compacted summary>" })
|
|
46
|
+
- suemo_context({ scope: "<project-id>", limit: 20 })
|
|
36
47
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cli-reference
|
|
3
3
|
description: CLI command reference for suemo v0.0.6 including skill access.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# CLI reference
|
|
@@ -14,10 +14,15 @@ Core:
|
|
|
14
14
|
- `suemo serve [--stdio|--dev]`
|
|
15
15
|
- `suemo skill [reference]`
|
|
16
16
|
|
|
17
|
+
Aliases (deprecated, still accepted with warning):
|
|
18
|
+
|
|
19
|
+
- `suemo skills` → use `suemo skill`
|
|
20
|
+
- `suemo init surrealdb` → use `suemo init surreal`
|
|
21
|
+
|
|
17
22
|
Memory:
|
|
18
23
|
|
|
19
24
|
- `suemo observe <content>`
|
|
20
|
-
- `suemo believe <content
|
|
25
|
+
- `suemo believe <content> [--session <sessionId>]`
|
|
21
26
|
- `suemo query <input>`
|
|
22
27
|
- `suemo recall <nodeId>`
|
|
23
28
|
- `suemo wander`
|
|
@@ -1,40 +1,68 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: core-workflow
|
|
3
3
|
description: Canonical suemo operating loop for OpenCode agents.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Core workflow
|
|
8
8
|
|
|
9
|
-
1. Start with
|
|
9
|
+
1. Start with strict pre-check:
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
|
|
12
|
+
goal_list({ scope: '<project-scope>' })
|
|
13
|
+
query({ input: 'recent work on <topic>', scope: '<project-scope>' })
|
|
14
|
+
context({ scope: '<project-scope>', limit: 20 })
|
|
15
|
+
episode_start({ sessionId: '<session-id>' }) // when beginning new task
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
2. During work, persist concrete discoveries:
|
|
16
19
|
|
|
17
20
|
```ts
|
|
18
|
-
observe(
|
|
19
|
-
|
|
21
|
+
observe({
|
|
22
|
+
content: '**What**: ...\n**Why**: ...\n**Where**: ...\n**Learned**: ...',
|
|
23
|
+
kind: 'observation',
|
|
24
|
+
scope: '<project-scope>',
|
|
25
|
+
})
|
|
26
|
+
believe({ content: '...', scope: '<project-scope>' })
|
|
20
27
|
```
|
|
21
28
|
|
|
22
|
-
3.
|
|
29
|
+
3. Kind schema is strict:
|
|
23
30
|
|
|
24
31
|
```ts
|
|
25
|
-
|
|
32
|
+
// valid only:
|
|
33
|
+
// observation | belief | question | hypothesis | goal
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
4.
|
|
36
|
+
4. For long tasks:
|
|
29
37
|
|
|
30
38
|
```ts
|
|
39
|
+
goal_set({ content: '...', scope: '<project-scope>' })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. If compaction/reset happened:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
session_context_set({
|
|
46
|
+
sessionId: '<session-id>',
|
|
47
|
+
summary: '<compacted summary>',
|
|
48
|
+
})
|
|
31
49
|
context({ scope: '<project-id>', limit: 20 })
|
|
32
50
|
```
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
6. Close session with structured fields:
|
|
35
53
|
|
|
36
54
|
```ts
|
|
37
|
-
episode_end(
|
|
55
|
+
episode_end({
|
|
56
|
+
sessionId: '<session-id>',
|
|
57
|
+
summary: '...',
|
|
58
|
+
goal: '...',
|
|
59
|
+
discoveries: ['...'],
|
|
60
|
+
accomplished: ['...'],
|
|
61
|
+
files_changed: ['path/to/file'],
|
|
62
|
+
})
|
|
63
|
+
goal_resolve({ goalId: '<goal-id>' }) // if completed
|
|
64
|
+
query({ input: 'most recent observations', scope: '<project-scope>' })
|
|
65
|
+
health()
|
|
38
66
|
```
|
|
39
67
|
|
|
40
68
|
Prefer project-scoped memory. Let suemo infer scope from nearest `{projectDir}/suemo.json` (recommended `main.projectDir = '.ua'`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: manual-test-plan
|
|
3
3
|
description: Comprehensive manual test matrix for suemo features and commands.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Manual test plan
|
|
@@ -35,10 +35,11 @@ Expected:
|
|
|
35
35
|
|
|
36
36
|
1. `episode_start`
|
|
37
37
|
2. add observations with `sessionId`
|
|
38
|
-
3. `
|
|
39
|
-
4. `
|
|
40
|
-
5. `
|
|
41
|
-
6.
|
|
38
|
+
3. add belief with `sessionId` (`suemo believe "..." --session <id>` or MCP `believe({ sessionId })`)
|
|
39
|
+
4. `session_context_set`
|
|
40
|
+
5. `context`
|
|
41
|
+
6. `episode_end` with structured fields
|
|
42
|
+
7. verify episode fields persisted
|
|
42
43
|
|
|
43
44
|
## E. Goals
|
|
44
45
|
|
|
@@ -66,7 +67,17 @@ Expected:
|
|
|
66
67
|
2. `suemo skill core-workflow`
|
|
67
68
|
3. MCP `skill({})`, `skill({ reference: "cli-reference" })`
|
|
68
69
|
|
|
69
|
-
## I.
|
|
70
|
+
## I. OpenCode init + plugin install
|
|
71
|
+
|
|
72
|
+
1. `suemo init opencode --dry-run`
|
|
73
|
+
2. `suemo init opencode`
|
|
74
|
+
3. Verify files:
|
|
75
|
+
- `~/.config/opencode/opencode.jsonc` or `.json` contains `mcp.suemo`
|
|
76
|
+
- `~/.config/opencode/AGENTS.md` has `<!-- SUEMO:START --> ... <!-- SUEMO:END -->`
|
|
77
|
+
- `~/.config/opencode/plugins/suemo.ts` exists
|
|
78
|
+
4. Validate plugin appears under OpenCode loaded plugins
|
|
79
|
+
|
|
80
|
+
## J. Output modes
|
|
70
81
|
|
|
71
82
|
Repeat representative commands under:
|
|
72
83
|
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-reference
|
|
3
3
|
description: MCP tool reference for suemo v0.0.6.
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# MCP tools
|
|
8
8
|
|
|
9
|
+
Strictness notes (v0.1.7):
|
|
10
|
+
|
|
11
|
+
- `observe.kind` is strict enum: `observation | belief | question | hypothesis | goal`
|
|
12
|
+
- `episode_end` supports optional structured fields: `summary`, `goal`, `discoveries`, `accomplished`, `files_changed`
|
|
13
|
+
- For reliable agent adoption, pair `episode_end` with `goal_resolve` and post-check `query("most recent observations")`
|
|
14
|
+
|
|
9
15
|
Write/mutate:
|
|
10
16
|
|
|
11
17
|
- `observe`
|
|
@@ -42,5 +48,7 @@ Maintenance:
|
|
|
42
48
|
Notes:
|
|
43
49
|
|
|
44
50
|
- Optional `scope` defaults to inferred project id.
|
|
51
|
+
- `believe` accepts optional `sessionId` to attach created belief memories to an open episode.
|
|
45
52
|
- `update` re-embeds when `content` changes.
|
|
46
53
|
- `episode_end` accepts structured fields (`goal`, `discoveries`, `accomplished`, `files_changed`).
|
|
54
|
+
- `skill` returns inline references; use `skill({ reference: "core-workflow" })` for canonical loop.
|
package/src/AGENTS.md
CHANGED
|
@@ -1,150 +1,130 @@
|
|
|
1
|
-
|
|
1
|
+
**CRITICAL:** This protocol is mandatory. Start each new task with the checklist below. End each completed task with `suemo_episode_end` + `suemo_goal_resolve` (if applicable). Do not skip.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 1) Pre-Session Checklist (required)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Before implementation:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [ ] `suemo_goal_list({ scope: "<active-scope>" })`
|
|
8
|
+
- [ ] `suemo_query({ input: "recent work on <topic>", scope: "<active-scope>" })`
|
|
9
|
+
- [ ] `suemo_context({ scope: "<active-scope>", limit: 20 })`
|
|
10
|
+
- [ ] Resolve abandoned goals if any
|
|
11
|
+
- [ ] `suemo_episode_start({ sessionId: "<session-id>" })` for new tasks
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
- MCP: `skill({})`
|
|
13
|
+
Proceed only after all checks pass.
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
---
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
## 2) Strict schema rules (required)
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
### `suemo_observe` kind is strict
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Valid `kind` values only:
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
- `observation`
|
|
24
|
+
- `belief`
|
|
25
|
+
- `question`
|
|
26
|
+
- `hypothesis`
|
|
27
|
+
- `goal`
|
|
21
28
|
|
|
22
|
-
###
|
|
29
|
+
### `suemo_episode_end` payload shape
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
goal_list(scope: "<active-scope>")
|
|
26
|
-
query("recent work on [topic]", scope: "<active-scope>")
|
|
27
|
-
```
|
|
31
|
+
Minimum schema requirement: `sessionId`.
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
Required by protocol when finishing meaningful work:
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
- `summary`
|
|
36
|
+
- `goal`
|
|
37
|
+
- `discoveries` (array)
|
|
38
|
+
- `accomplished` (array)
|
|
39
|
+
- `files_changed` (array)
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
Use exact field names above.
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
observe("[fact you learned or confirmed]")
|
|
37
|
-
believe("[interpretation or conclusion that might change]")
|
|
38
|
-
```
|
|
43
|
+
---
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
## 3) During work
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
Persist important information immediately (not only at the end).
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
Use this structure for non-trivial observations:
|
|
45
50
|
|
|
51
|
+
```md
|
|
52
|
+
**What**: <what changed>
|
|
53
|
+
**Why**: <reason>
|
|
54
|
+
**Where**: <files/paths>
|
|
55
|
+
**Learned**: <gotchas/decisions>
|
|
46
56
|
```
|
|
47
|
-
episode_end(<session_id>, summary="<one sentence of what was accomplished>")
|
|
48
|
-
goal_resolve(<goal_id>) # only if the goal is fully done
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
If the session is interrupted and you never reach this step, that is acceptable. Incomplete episodes are better than missing observations mid-session.
|
|
52
|
-
|
|
53
|
-
## observe vs believe
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
Guidance:
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
- Facts → `suemo_observe`
|
|
61
|
+
- Interpretations/uncertainty → `suemo_believe`
|
|
62
|
+
- Longer tasks → `suemo_goal_set`
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
---
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
## 4) After completion
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
Run in order:
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
1. `suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })`
|
|
71
|
+
2. `suemo_goal_resolve({ goalId })` when done
|
|
72
|
+
3. `suemo_query({ input: "most recent observations", scope })`
|
|
73
|
+
4. `suemo_health()`
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
- Config or flags that took effort to figure out
|
|
70
|
-
- Errors encountered and how they were resolved
|
|
71
|
-
- Decisions made and the reasoning behind them
|
|
72
|
-
- Constraints or preferences the user stated
|
|
75
|
+
---
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
**Skip:** `"[library] supports [feature]"` ← already in the docs
|
|
77
|
+
## 5) Querying best practices
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- Transient state: current cursor position, open file, in-progress thought
|
|
80
|
-
- Exact code snippets from files — observe your _understanding_ of them instead
|
|
81
|
-
- Timestamps — suemo sets `valid_from` automatically
|
|
82
|
-
- Uncertain things stated as facts — use `kind: "hypothesis"` instead
|
|
83
|
-
|
|
84
|
-
## Querying
|
|
85
|
-
|
|
86
|
-
suemo uses hybrid retrieval (vector + BM25 + graph) with score gates.
|
|
87
|
-
Natural language often works, but broad prompts can still be noisy.
|
|
88
|
-
|
|
89
|
-
Use scoped, specific queries first:
|
|
79
|
+
Prefer specific, scoped queries:
|
|
90
80
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
```ts
|
|
82
|
+
suemo_query({
|
|
83
|
+
input: 'past decisions about auth middleware',
|
|
84
|
+
scope: '<project-id>',
|
|
85
|
+
})
|
|
86
|
+
suemo_query({
|
|
87
|
+
input: '"SURREAL_CAPS_ALLOW_FUNC" init fix',
|
|
88
|
+
scope: '<project-id>',
|
|
89
|
+
})
|
|
94
90
|
```
|
|
95
91
|
|
|
96
|
-
|
|
92
|
+
When weak ranking is suspected, run `--explain` from CLI to inspect scoring.
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
query('"SURREAL_CAPS_ALLOW_FUNC" init fix', scope: "<active-scope>")
|
|
100
|
-
```
|
|
94
|
+
---
|
|
101
95
|
|
|
102
|
-
|
|
96
|
+
## 6) Anti-patterns
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
- ❌ Storing transient state (cursor position, temporary thoughts)
|
|
99
|
+
- ❌ Storing raw code snippets instead of distilled understanding
|
|
100
|
+
- ❌ Querying on every small step
|
|
101
|
+
- ❌ Skipping `suemo_episode_end`
|
|
102
|
+
- ❌ Forgetting `suemo_goal_resolve`
|
|
103
|
+
- ❌ Using non-schema `kind` values
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
- Use `recall(<nodeId>)` when you have a node ID and want it plus neighbours
|
|
105
|
+
---
|
|
111
106
|
|
|
112
|
-
|
|
107
|
+
## 7) Compaction/reset handling
|
|
113
108
|
|
|
114
|
-
|
|
109
|
+
Immediately:
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
1. `suemo_session_context_set({ sessionId, summary: "<compacted summary>" })`
|
|
112
|
+
2. `suemo_context({ scope: "<project-id>", limit: 20 })`
|
|
113
|
+
3. Continue work
|
|
118
114
|
|
|
119
|
-
|
|
115
|
+
---
|
|
120
116
|
|
|
121
|
-
##
|
|
117
|
+
## 8) Documentation shortcuts
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
```bash
|
|
120
|
+
suemo skill
|
|
121
|
+
suemo skill core-workflow
|
|
122
|
+
suemo skill mcp-reference
|
|
123
|
+
suemo skill manual-test-plan
|
|
127
124
|
```
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
goal_resolve(<id>)
|
|
133
|
-
```
|
|
126
|
+
Load docs once per session unless requirements changed.
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## What suemo handles automatically
|
|
138
|
-
|
|
139
|
-
- **Timestamps** — `valid_from` on write, `valid_until` on contradiction or invalidation
|
|
140
|
-
- **Deduplication** — identical writes merge; near-duplicates create a new linked node
|
|
141
|
-
- **Consolidation** — runs on a schedule; no manual trigger needed
|
|
142
|
-
- **Embeddings** — computed server-side via `fn::embed()`; never pass vectors yourself
|
|
143
|
-
|
|
144
|
-
## Health check
|
|
145
|
-
|
|
146
|
-
```
|
|
147
|
-
suemo health
|
|
148
|
-
```
|
|
128
|
+
---
|
|
149
129
|
|
|
150
|
-
|
|
130
|
+
_Version: 0.1.7 | Updated: 2026-03-23 | Summary: stricter schema + explicit completion contract_
|
|
@@ -12,6 +12,7 @@ export const believeCmd = app.sub('believe')
|
|
|
12
12
|
.flags({
|
|
13
13
|
scope: { type: 'string', short: 's', description: 'Scope label' },
|
|
14
14
|
confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
|
|
15
|
+
session: { type: 'string', description: 'Session ID (attach to open episode)' },
|
|
15
16
|
json: { type: 'boolean', description: 'Output JSON result' },
|
|
16
17
|
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
17
18
|
})
|
|
@@ -32,12 +33,14 @@ export const believeCmd = app.sub('believe')
|
|
|
32
33
|
hasScope: Boolean(resolvedScope),
|
|
33
34
|
scope: resolvedScope,
|
|
34
35
|
confidence: flags.confidence,
|
|
36
|
+
hasSession: Boolean(flags.session),
|
|
35
37
|
contentLength: args.content.length,
|
|
36
38
|
})
|
|
37
39
|
const { node, contradicted } = await believe(db, {
|
|
38
40
|
content: args.content,
|
|
39
41
|
scope: resolvedScope,
|
|
40
42
|
confidence: flags.confidence,
|
|
43
|
+
sessionId: flags.session,
|
|
41
44
|
}, config)
|
|
42
45
|
if (outputMode === 'json') {
|
|
43
46
|
const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -16,6 +16,8 @@ import OPENCODE_AGENTS_SNIPPET_TEXT from '@/src/AGENTS.md' with { type: 'text' }
|
|
|
16
16
|
import template from '@/src/config.template' with { type: 'text' }
|
|
17
17
|
import FASTEMBED_SCRIPT_TEXT from '@/src/embedding/fastembed-server.py' with { type: 'text' }
|
|
18
18
|
|
|
19
|
+
const OPENCODE_PLUGIN_TEXT = readFileSync(new URL('../../opencode/plugin.ts', import.meta.url), 'utf-8')
|
|
20
|
+
|
|
19
21
|
interface InitFlags {
|
|
20
22
|
debug?: boolean | undefined
|
|
21
23
|
config?: string | undefined
|
|
@@ -253,6 +255,7 @@ type ServiceManager =
|
|
|
253
255
|
type OpenCodeConfigMode = 'mcp'
|
|
254
256
|
type OpenCodeConfigStatus = 'added' | 'already-present' | 'unchanged'
|
|
255
257
|
type OpenCodeAgentsStatus = 'added' | 'updated' | 'unchanged'
|
|
258
|
+
type OpenCodePluginStatus = 'added' | 'updated' | 'unchanged'
|
|
256
259
|
|
|
257
260
|
interface OpenCodeConfigResolution {
|
|
258
261
|
path: string
|
|
@@ -268,6 +271,8 @@ interface OpenCodeInitResult {
|
|
|
268
271
|
configMode: OpenCodeConfigMode
|
|
269
272
|
agentsPath: string
|
|
270
273
|
agentsStatus: OpenCodeAgentsStatus
|
|
274
|
+
pluginPath: string
|
|
275
|
+
pluginStatus: OpenCodePluginStatus
|
|
271
276
|
dryRun: boolean
|
|
272
277
|
}
|
|
273
278
|
|
|
@@ -594,6 +599,18 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
|
|
|
594
599
|
const existingAgents = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf-8') : ''
|
|
595
600
|
const agentsResult = upsertAgentsSnippet(existingAgents)
|
|
596
601
|
|
|
602
|
+
const pluginsDir = join(openCodeDir, 'plugins')
|
|
603
|
+
const pluginPath = join(pluginsDir, 'suemo.ts')
|
|
604
|
+
const nextPluginText = `${OPENCODE_PLUGIN_TEXT.trimEnd()}\n`
|
|
605
|
+
const pluginExists = existsSync(pluginPath)
|
|
606
|
+
const existingPlugin = pluginExists ? readFileSync(pluginPath, 'utf-8') : ''
|
|
607
|
+
let pluginStatus: OpenCodePluginStatus = 'unchanged'
|
|
608
|
+
if (!pluginExists) {
|
|
609
|
+
pluginStatus = 'added'
|
|
610
|
+
} else if (existingPlugin !== nextPluginText) {
|
|
611
|
+
pluginStatus = 'updated'
|
|
612
|
+
}
|
|
613
|
+
|
|
597
614
|
if (!dryRun) {
|
|
598
615
|
mkdirSync(openCodeDir, { recursive: true })
|
|
599
616
|
if (!configResolution.existed || configWriteResult.changed) {
|
|
@@ -602,6 +619,10 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
|
|
|
602
619
|
if (!existsSync(agentsPath) || agentsResult.content !== existingAgents) {
|
|
603
620
|
writeFileSync(agentsPath, agentsResult.content, 'utf-8')
|
|
604
621
|
}
|
|
622
|
+
mkdirSync(pluginsDir, { recursive: true })
|
|
623
|
+
if (pluginStatus !== 'unchanged') {
|
|
624
|
+
writeFileSync(pluginPath, nextPluginText, 'utf-8')
|
|
625
|
+
}
|
|
605
626
|
}
|
|
606
627
|
|
|
607
628
|
return {
|
|
@@ -612,6 +633,8 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
|
|
|
612
633
|
configMode: configWriteResult.mode,
|
|
613
634
|
agentsPath,
|
|
614
635
|
agentsStatus: agentsResult.status,
|
|
636
|
+
pluginPath,
|
|
637
|
+
pluginStatus,
|
|
615
638
|
dryRun,
|
|
616
639
|
}
|
|
617
640
|
}
|
|
@@ -1129,7 +1152,7 @@ const initSchemaCmd = init.sub('schema')
|
|
|
1129
1152
|
})
|
|
1130
1153
|
|
|
1131
1154
|
const initOpenCodeCmd = init.sub('opencode')
|
|
1132
|
-
.meta({ description: 'Initialize OpenCode MCP config + AGENTS.md suemo
|
|
1155
|
+
.meta({ description: 'Initialize OpenCode MCP config + AGENTS.md snippet + suemo plugin' })
|
|
1133
1156
|
.flags({
|
|
1134
1157
|
'dry-run': { type: 'boolean', description: 'Show changes without writing files', default: false },
|
|
1135
1158
|
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
@@ -1160,6 +1183,8 @@ const initOpenCodeCmd = init.sub('opencode')
|
|
|
1160
1183
|
configMode: result.configMode,
|
|
1161
1184
|
agentsPath: result.agentsPath,
|
|
1162
1185
|
agentsStatus: result.agentsStatus,
|
|
1186
|
+
pluginPath: result.pluginPath,
|
|
1187
|
+
pluginStatus: result.pluginStatus,
|
|
1163
1188
|
dryRun: result.dryRun,
|
|
1164
1189
|
},
|
|
1165
1190
|
suemoVersion: npmVersion,
|
|
@@ -1173,6 +1198,7 @@ const initOpenCodeCmd = init.sub('opencode')
|
|
|
1173
1198
|
console.log(`OpenCode config: ${result.configPath} (${result.configFormat})`)
|
|
1174
1199
|
console.log(`MCP entry: ${result.configStatus} (${result.configMode})`)
|
|
1175
1200
|
console.log(`AGENTS.md: ${result.agentsStatus} at ${result.agentsPath}`)
|
|
1201
|
+
console.log(`Plugin: ${result.pluginStatus} at ${result.pluginPath}`)
|
|
1176
1202
|
if (dryRun) {
|
|
1177
1203
|
console.log('Dry-run mode: no files were written.')
|
|
1178
1204
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -20,6 +20,26 @@ import { helpPlugin, versionPlugin } from '@crustjs/plugins'
|
|
|
20
20
|
|
|
21
21
|
import packageJson from '@/package.json' with { type: 'json' }
|
|
22
22
|
|
|
23
|
+
function applyDeprecatedAliases(argv: string[]): string[] {
|
|
24
|
+
const next = [...argv]
|
|
25
|
+
|
|
26
|
+
if (next[0] === 'skills') {
|
|
27
|
+
next[0] = 'skill'
|
|
28
|
+
console.warn('[suemo] Deprecated alias `skills` detected. Use `skill` instead.')
|
|
29
|
+
return next
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (next[0] === 'init' && next[1] === 'surrealdb') {
|
|
33
|
+
next[1] = 'surreal'
|
|
34
|
+
console.warn('[suemo] Deprecated alias `init surrealdb` detected. Use `init surreal` instead.')
|
|
35
|
+
return next
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return next
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process.argv = [...process.argv.slice(0, 2), ...applyDeprecatedAliases(process.argv.slice(2))]
|
|
42
|
+
|
|
23
43
|
await app
|
|
24
44
|
.use(versionPlugin(packageJson.version ?? '0.0.0'))
|
|
25
45
|
.use(helpPlugin())
|
package/src/mcp/dispatch.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
upsertByKey,
|
|
17
17
|
} from '@/src/memory/write.ts'
|
|
18
18
|
import { listSkillReferences, readSkillDoc, readSkillReference } from '@/src/skill/catalog.ts'
|
|
19
|
-
import { ObserveInputSchema, QueryInputSchema } from '@/src/types.ts'
|
|
19
|
+
import { MemoryKindSchema, ObserveInputSchema, QueryInputSchema } from '@/src/types.ts'
|
|
20
20
|
import type { Surreal } from 'surrealdb'
|
|
21
21
|
import { z } from 'zod'
|
|
22
22
|
|
|
@@ -49,6 +49,7 @@ export async function handleToolCall(
|
|
|
49
49
|
): Promise<unknown> {
|
|
50
50
|
log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
|
|
51
51
|
const inferredScope = inferProjectScope(process.cwd(), config)
|
|
52
|
+
const validKinds = MemoryKindSchema.options.join(', ')
|
|
52
53
|
|
|
53
54
|
const maybeTriggerMutationSync = (): void => {
|
|
54
55
|
if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
|
|
@@ -59,7 +60,15 @@ export async function handleToolCall(
|
|
|
59
60
|
|
|
60
61
|
switch (method) {
|
|
61
62
|
case 'observe': {
|
|
62
|
-
const
|
|
63
|
+
const parsedObserve = ObserveInputSchema.safeParse(params)
|
|
64
|
+
if (!parsedObserve.success) {
|
|
65
|
+
const invalidKindIssue = parsedObserve.error.issues.find((issue) => issue.path[0] === 'kind')
|
|
66
|
+
if (invalidKindIssue && typeof params.kind === 'string') {
|
|
67
|
+
throw new Error(`Invalid observe.kind "${params.kind}". Valid kinds: ${validKinds}`)
|
|
68
|
+
}
|
|
69
|
+
throw parsedObserve.error
|
|
70
|
+
}
|
|
71
|
+
const parsed = parsedObserve.data
|
|
63
72
|
const result = await observe(db, {
|
|
64
73
|
...parsed,
|
|
65
74
|
scope: parsed.scope?.trim() || inferredScope,
|
|
@@ -74,6 +83,7 @@ export async function handleToolCall(
|
|
|
74
83
|
content: z.string(),
|
|
75
84
|
scope: z.string().optional(),
|
|
76
85
|
confidence: z.number().optional(),
|
|
86
|
+
sessionId: z.string().optional(),
|
|
77
87
|
})
|
|
78
88
|
.parse(params)
|
|
79
89
|
const result = await believe(db, {
|
|
@@ -225,7 +235,7 @@ export async function handleToolCall(
|
|
|
225
235
|
confidence: z.number().optional(),
|
|
226
236
|
source: z.string().optional(),
|
|
227
237
|
sessionId: z.string().optional(),
|
|
228
|
-
kind:
|
|
238
|
+
kind: MemoryKindSchema.optional(),
|
|
229
239
|
})
|
|
230
240
|
.parse(params)
|
|
231
241
|
const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
|
|
@@ -262,7 +272,7 @@ export async function handleToolCall(
|
|
|
262
272
|
.object({
|
|
263
273
|
nodeId: z.string(),
|
|
264
274
|
content: z.string().optional(),
|
|
265
|
-
kind:
|
|
275
|
+
kind: MemoryKindSchema.optional(),
|
|
266
276
|
tags: z.array(z.string()).optional(),
|
|
267
277
|
scope: z.string().nullable().optional(),
|
|
268
278
|
source: z.string().nullable().optional(),
|
package/src/mcp/stdio.ts
CHANGED
|
@@ -31,12 +31,12 @@ interface StdioServerOptions {
|
|
|
31
31
|
const MCP_TOOLS: McpToolDefinition[] = [
|
|
32
32
|
{
|
|
33
33
|
name: 'observe',
|
|
34
|
-
description: 'Store
|
|
34
|
+
description: 'Store memory node (kind must be observation|belief|question|hypothesis|goal)',
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
properties: {
|
|
38
38
|
content: { type: 'string' },
|
|
39
|
-
kind: { type: 'string' },
|
|
39
|
+
kind: { type: 'string', enum: ['observation', 'belief', 'question', 'hypothesis', 'goal'] },
|
|
40
40
|
tags: { type: 'array', items: { type: 'string' } },
|
|
41
41
|
scope: { type: 'string' },
|
|
42
42
|
source: { type: 'string' },
|
|
@@ -55,6 +55,7 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
55
55
|
content: { type: 'string' },
|
|
56
56
|
scope: { type: 'string' },
|
|
57
57
|
confidence: { type: 'number' },
|
|
58
|
+
sessionId: { type: 'string' },
|
|
58
59
|
},
|
|
59
60
|
required: ['content'],
|
|
60
61
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* suemo OpenCode plugin (v0.1.7)
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Inject strict, always-on suemo memory workflow instructions.
|
|
6
|
+
* - Preserve compacted-session signal by requiring immediate suemo checkpointing.
|
|
7
|
+
*
|
|
8
|
+
* This module is copied by `suemo init opencode` to:
|
|
9
|
+
* ~/.config/opencode/plugins/suemo.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface SystemTransformOutput {
|
|
13
|
+
system: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SessionCompactingInput {
|
|
17
|
+
sessionID?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SessionCompactingOutput {
|
|
21
|
+
context: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MEMORY_INSTRUCTIONS = `## Suemo Persistent Memory — Protocol (v0.1.7)
|
|
25
|
+
|
|
26
|
+
You have access to suemo persistent memory tools (commonly prefixed as \`suemo_*\`).
|
|
27
|
+
|
|
28
|
+
### REQUIRED START-OF-TASK CHECKLIST
|
|
29
|
+
|
|
30
|
+
Before implementation:
|
|
31
|
+
1. \`suemo_goal_list({ scope })\`
|
|
32
|
+
2. \`suemo_query({ input: "recent work on <topic>", scope })\`
|
|
33
|
+
3. \`suemo_context({ scope, limit: 20 })\`
|
|
34
|
+
4. If task is new: \`suemo_episode_start({ sessionId })\`
|
|
35
|
+
|
|
36
|
+
### REQUIRED WRITE TRIGGERS
|
|
37
|
+
|
|
38
|
+
Call \`suemo_observe\` immediately after:
|
|
39
|
+
- bug fix completed
|
|
40
|
+
- architecture/design decision made
|
|
41
|
+
- non-obvious discovery
|
|
42
|
+
- config/environment change
|
|
43
|
+
- user preference/constraint discovered
|
|
44
|
+
|
|
45
|
+
Use this strict content template for non-trivial writes:
|
|
46
|
+
|
|
47
|
+
\`\`\`
|
|
48
|
+
**What**: ...
|
|
49
|
+
**Why**: ...
|
|
50
|
+
**Where**: path/to/file
|
|
51
|
+
**Learned**: ...
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
### SCHEMA GUARDRAILS
|
|
55
|
+
|
|
56
|
+
For \`suemo_observe\`, \`kind\` MUST be one of:
|
|
57
|
+
- \`observation\`
|
|
58
|
+
- \`belief\`
|
|
59
|
+
- \`question\`
|
|
60
|
+
- \`hypothesis\`
|
|
61
|
+
- \`goal\`
|
|
62
|
+
|
|
63
|
+
### END-OF-TASK CHECKLIST
|
|
64
|
+
|
|
65
|
+
Before declaring done:
|
|
66
|
+
1. \`suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })\`
|
|
67
|
+
2. \`suemo_goal_resolve({ goalId })\` for completed goals
|
|
68
|
+
3. \`suemo_query({ input: "most recent observations", scope })\`
|
|
69
|
+
4. \`suemo_health()\`
|
|
70
|
+
|
|
71
|
+
### COMPACTION/RESET RULE
|
|
72
|
+
|
|
73
|
+
If context is compacted/reset, FIRST:
|
|
74
|
+
1. Save checkpoint with \`suemo_session_context_set({ sessionId, summary })\`
|
|
75
|
+
2. Recover via \`suemo_context({ scope, limit: 20 })\`
|
|
76
|
+
3. Then continue work
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
export const Suemo = async () => {
|
|
80
|
+
return {
|
|
81
|
+
'experimental.chat.system.transform': async (_input: unknown, output: SystemTransformOutput) => {
|
|
82
|
+
if (output.system.length > 0) {
|
|
83
|
+
output.system[output.system.length - 1] += `\n\n${MEMORY_INSTRUCTIONS}`
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
output.system.push(MEMORY_INSTRUCTIONS)
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
'experimental.session.compacting': async (
|
|
90
|
+
input: SessionCompactingInput,
|
|
91
|
+
output: SessionCompactingOutput,
|
|
92
|
+
) => {
|
|
93
|
+
const sessionId = input.sessionID ?? '<current-session-id>'
|
|
94
|
+
output.context.push(
|
|
95
|
+
[
|
|
96
|
+
'FIRST ACTION REQUIRED:',
|
|
97
|
+
`Call suemo_session_context_set({ sessionId: "${sessionId}", summary: "<compacted summary>" }) before any other work.`,
|
|
98
|
+
'Then call suemo_context({ scope: "<project-scope>", limit: 20 }) to recover continuity.',
|
|
99
|
+
].join(' '),
|
|
100
|
+
)
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default Suemo
|