@possumtech/rummy 0.2.7 → 0.3.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.
Files changed (119) hide show
  1. package/.env.example +12 -3
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +454 -197
  4. package/SPEC.md +284 -93
  5. package/migrations/001_initial_schema.sql +57 -70
  6. package/package.json +16 -10
  7. package/service.js +1 -1
  8. package/src/agent/AgentLoop.js +254 -70
  9. package/src/agent/ContextAssembler.js +18 -4
  10. package/src/agent/KnownStore.js +156 -23
  11. package/src/agent/ProjectAgent.js +5 -4
  12. package/src/agent/ResponseHealer.js +21 -1
  13. package/src/agent/TurnExecutor.js +393 -115
  14. package/src/agent/XmlParser.js +92 -39
  15. package/src/agent/known_checks.sql +5 -4
  16. package/src/agent/known_queries.sql +4 -3
  17. package/src/agent/known_store.sql +45 -15
  18. package/src/agent/loops.sql +63 -0
  19. package/src/agent/runs.sql +7 -7
  20. package/src/agent/schemes.sql +5 -2
  21. package/src/agent/tokens.js +6 -21
  22. package/src/agent/turns.sql +13 -4
  23. package/src/hooks/Hooks.js +18 -0
  24. package/src/hooks/PluginContext.js +14 -10
  25. package/src/hooks/RummyContext.js +30 -10
  26. package/src/hooks/ToolRegistry.js +83 -19
  27. package/src/llm/LlmProvider.js +27 -8
  28. package/src/llm/OpenAiClient.js +20 -0
  29. package/src/llm/OpenRouterClient.js +24 -2
  30. package/src/llm/XaiClient.js +47 -2
  31. package/src/plugins/ask_user/README.md +4 -4
  32. package/src/plugins/ask_user/ask_user.js +8 -7
  33. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  34. package/src/plugins/budget/BudgetGuard.js +74 -0
  35. package/src/plugins/budget/README.md +43 -0
  36. package/src/plugins/budget/budget.js +79 -0
  37. package/src/plugins/cp/README.md +5 -4
  38. package/src/plugins/cp/cp.js +16 -12
  39. package/src/plugins/cp/cpDoc.js +29 -0
  40. package/src/plugins/current/README.md +4 -4
  41. package/src/plugins/current/current.js +12 -10
  42. package/src/plugins/engine/engine.sql +5 -10
  43. package/src/plugins/engine/turn_context.sql +13 -13
  44. package/src/plugins/env/README.md +3 -4
  45. package/src/plugins/env/env.js +8 -7
  46. package/src/plugins/env/envDoc.js +29 -0
  47. package/src/plugins/file/README.md +9 -12
  48. package/src/plugins/file/file.js +34 -45
  49. package/src/plugins/get/README.md +2 -2
  50. package/src/plugins/get/get.js +28 -11
  51. package/src/plugins/get/getDoc.js +41 -0
  52. package/src/plugins/hedberg/docs.md +0 -9
  53. package/src/plugins/hedberg/hedberg.js +4 -6
  54. package/src/plugins/hedberg/matcher.js +1 -1
  55. package/src/plugins/hedberg/normalize.js +28 -0
  56. package/src/plugins/hedberg/patterns.js +31 -33
  57. package/src/plugins/hedberg/sed.js +17 -10
  58. package/src/plugins/helpers.js +2 -2
  59. package/src/plugins/index.js +93 -28
  60. package/src/plugins/instructions/README.md +6 -2
  61. package/src/plugins/instructions/instructions.js +21 -5
  62. package/src/plugins/instructions/preamble.md +9 -5
  63. package/src/plugins/known/README.md +10 -7
  64. package/src/plugins/known/known.js +33 -23
  65. package/src/plugins/known/knownDoc.js +33 -0
  66. package/src/plugins/mv/README.md +5 -4
  67. package/src/plugins/mv/mv.js +16 -12
  68. package/src/plugins/mv/mvDoc.js +31 -0
  69. package/src/plugins/persona/persona.js +78 -0
  70. package/src/plugins/previous/README.md +2 -2
  71. package/src/plugins/previous/previous.js +12 -8
  72. package/src/plugins/progress/progress.js +44 -12
  73. package/src/plugins/prompt/README.md +5 -5
  74. package/src/plugins/prompt/prompt.js +23 -19
  75. package/src/plugins/rm/README.md +4 -4
  76. package/src/plugins/rm/rm.js +29 -12
  77. package/src/plugins/rm/rmDoc.js +30 -0
  78. package/src/plugins/rpc/README.md +15 -28
  79. package/src/plugins/rpc/rpc.js +63 -107
  80. package/src/plugins/set/README.md +13 -12
  81. package/src/plugins/set/set.js +82 -21
  82. package/src/plugins/set/setDoc.js +45 -0
  83. package/src/plugins/sh/README.md +4 -4
  84. package/src/plugins/sh/sh.js +8 -7
  85. package/src/plugins/sh/shDoc.js +29 -0
  86. package/src/plugins/{skills/skills.js → skill/skill.js} +12 -54
  87. package/src/plugins/summarize/README.md +6 -5
  88. package/src/plugins/summarize/summarize.js +7 -6
  89. package/src/plugins/summarize/summarizeDoc.js +33 -0
  90. package/src/plugins/telemetry/telemetry.js +20 -8
  91. package/src/plugins/think/README.md +20 -0
  92. package/src/plugins/think/think.js +5 -0
  93. package/src/plugins/unknown/README.md +5 -5
  94. package/src/plugins/unknown/unknown.js +11 -8
  95. package/src/plugins/unknown/unknownDoc.js +31 -0
  96. package/src/plugins/update/README.md +3 -8
  97. package/src/plugins/update/update.js +7 -6
  98. package/src/plugins/update/updateDoc.js +33 -0
  99. package/src/server/ClientConnection.js +3 -5
  100. package/src/server/RpcRegistry.js +52 -4
  101. package/src/sql/v_model_context.sql +31 -39
  102. package/src/sql/v_run_log.sql +3 -3
  103. package/src/agent/prompt_queue.sql +0 -39
  104. package/src/plugins/ask_user/docs.md +0 -2
  105. package/src/plugins/cp/docs.md +0 -2
  106. package/src/plugins/env/docs.md +0 -2
  107. package/src/plugins/get/docs.md +0 -6
  108. package/src/plugins/known/docs.md +0 -3
  109. package/src/plugins/mv/docs.md +0 -2
  110. package/src/plugins/rm/docs.md +0 -4
  111. package/src/plugins/set/docs.md +0 -4
  112. package/src/plugins/sh/docs.md +0 -2
  113. package/src/plugins/skills/README.md +0 -25
  114. package/src/plugins/store/README.md +0 -20
  115. package/src/plugins/store/docs.md +0 -5
  116. package/src/plugins/store/store.js +0 -52
  117. package/src/plugins/summarize/docs.md +0 -4
  118. package/src/plugins/unknown/docs.md +0 -5
  119. package/src/plugins/update/docs.md +0 -4
package/PLUGINS.md CHANGED
@@ -1,11 +1,73 @@
1
1
  # PLUGINS.md — Plugin Development Guide
2
2
 
3
- ## Plugin Contract
3
+ Every `<tag>` the model sees is a plugin. Every scheme is registered by
4
+ its owner. Every operation — model, client, plugin — flows through the
5
+ same tool handler. No exceptions without documentation in EXCEPTIONS.md.
4
6
 
5
- A plugin is a directory under `src/plugins/` containing a `.js` file that
6
- exports a default class. The class name matches the file name. The
7
- constructor receives `core` (a PluginContext) the plugin's complete
8
- interface with the system.
7
+ ## §0 Quickstart
8
+
9
+ A complete tool plugin in four parts: register, handle, render, document.
10
+
11
+ ```js
12
+ // src/plugins/ping/ping.js
13
+ import docs from "./pingDoc.js";
14
+
15
+ export default class Ping {
16
+ #core;
17
+
18
+ constructor(core) {
19
+ this.#core = core;
20
+ core.ensureTool();
21
+ core.registerScheme({ category: "logging" });
22
+ core.on("handler", this.handler.bind(this));
23
+ core.on("full", this.full.bind(this));
24
+ core.filter("instructions.toolDocs", async (docsMap) => {
25
+ docsMap.ping = docs;
26
+ return docsMap;
27
+ });
28
+ }
29
+
30
+ async handler(entry, rummy) {
31
+ const now = new Date().toISOString();
32
+ await rummy.set({
33
+ path: entry.resultPath,
34
+ body: `pong ${now}`,
35
+ status: 200,
36
+ attributes: { path: entry.path },
37
+ });
38
+ }
39
+
40
+ full(entry) {
41
+ return entry.body;
42
+ }
43
+ }
44
+ ```
45
+
46
+ ```js
47
+ // src/plugins/ping/pingDoc.js
48
+ const LINES = [
49
+ ["## ping",
50
+ "Header — model sees this as the tool name"],
51
+ ["<ping/>",
52
+ "Simplest invocation — no path, no body"],
53
+ ["* Returns server timestamp",
54
+ "One-line description of what the tool does"],
55
+ ];
56
+ export default LINES.map(([text]) => text).join("\n");
57
+ ```
58
+
59
+ Install external plugins via npm + env var:
60
+
61
+ ```env
62
+ RUMMY_PLUGIN_PING=@myorg/rummy.ping
63
+ ```
64
+
65
+ ## §1 Plugin Contract
66
+
67
+ A plugin is a directory under `src/plugins/` containing a `.js` file
68
+ that exports a default class. The class name matches the file name.
69
+ The constructor receives `core` (a PluginContext) — the plugin's
70
+ complete interface with the system.
9
71
 
10
72
  ```js
11
73
  export default class MyTool {
@@ -13,8 +75,15 @@ export default class MyTool {
13
75
 
14
76
  constructor(core) {
15
77
  this.#core = core;
78
+ core.ensureTool();
79
+ core.registerScheme({ category: "logging" });
16
80
  core.on("handler", this.handler.bind(this));
17
81
  core.on("full", this.full.bind(this));
82
+ core.on("summary", this.summary.bind(this));
83
+ core.filter("instructions.toolDocs", async (docsMap) => {
84
+ docsMap.mytool = docs;
85
+ return docsMap;
86
+ });
18
87
  }
19
88
 
20
89
  async handler(entry, rummy) {
@@ -22,13 +91,17 @@ export default class MyTool {
22
91
  }
23
92
 
24
93
  full(entry) {
25
- // What the model sees at full fidelity
26
- return `# mytool ${entry.path}\n${entry.body}`;
94
+ return entry.body;
95
+ }
96
+
97
+ summary(entry) {
98
+ return entry.body;
27
99
  }
28
100
  }
29
101
  ```
30
102
 
31
103
  File naming: `src/plugins/mytool/mytool.js`. Class name = file name.
104
+ Tool docs: `src/plugins/mytool/mytoolDoc.js` (annotated line arrays).
32
105
 
33
106
  External plugins install via npm and load via `RUMMY_PLUGIN_*` env vars:
34
107
 
@@ -37,11 +110,11 @@ RUMMY_PLUGIN_WEB=@possumtech/rummy.web
37
110
  RUMMY_PLUGIN_REPO=@possumtech/rummy.repo
38
111
  ```
39
112
 
40
- ## Unified API
113
+ ## §2 Unified API
41
114
 
42
- The model, the client, and plugins all use the same interface. Each tier
43
- is a superset of the one below. `name` (model) = `method` (client) =
44
- method name (plugin). The params shape is the same at every tier.
115
+ The model, the client, and plugins all use the same interface. Each
116
+ tier is a superset of the one below. `name` (model) = `method` (client)
117
+ = method name (plugin). The params shape is the same at every tier.
45
118
 
46
119
  ```
47
120
  Model: <rm path="file.txt"/> → { name: "rm", path: "file.txt" }
@@ -49,37 +122,121 @@ Client: { method: "rm", params: { path: "file.txt" } }
49
122
  Plugin: rummy.rm({ path: "file.txt" })
50
123
  ```
51
124
 
52
- ## Registration
125
+ All three tiers go through the same tool handler. Budget enforcement
126
+ applies equally. A client `get` is subject to the same budget check
127
+ as a model `<get>`.
53
128
 
54
- All registration happens in the constructor via `core.on()` and
55
- `core.filter()`. No static methods. No direct hook manipulation.
129
+ ## §3 Registration
56
130
 
57
- ### core.on(event, callback, priority?)
131
+ All registration happens in the constructor via `core.on()`,
132
+ `core.filter()`, `core.ensureTool()`, and `core.registerScheme()`.
58
133
 
59
- | Event | Purpose |
60
- |-------|---------|
61
- | `"handler"` | Tool handler — called when model/client invokes this tool |
62
- | `"full"` | Full projection — what the model sees at full fidelity |
63
- | `"summary"` | Summary projection — condensed view under token pressure |
64
- | `"docs"` | Tool documentation — included in model prompt |
65
- | `"turn"` | Turn processor — runs before context materialization |
66
- | `"entry.created"` | Entry created during dispatch |
67
- | `"entry.changed"` | File entries changed on disk |
68
- | Any `"dotted.name"` | Resolves to the matching hook in the hook tree |
134
+ ### §3.1 core.ensureTool()
69
135
 
70
- ### core.filter(name, callback, priority?)
136
+ Declares this plugin as a model-facing tool. Required for the tool
137
+ to appear in the model's tool list. Called automatically by
138
+ `core.on("handler", ...)` but must be called explicitly for tools
139
+ without handlers (e.g., `summarize`, `update`, `unknown`).
71
140
 
72
- | Filter | Purpose |
73
- |--------|---------|
74
- | `"assembly.system"` | Contribute to system message |
75
- | `"assembly.user"` | Contribute to user message |
76
- | `"llm.messages"` | Transform final messages before LLM call |
77
- | `"llm.response"` | Transform LLM response |
78
- | Any `"dotted.name"` | Resolves to the matching filter in the hook tree |
141
+ ### §3.2 core.registerScheme(config?)
142
+
143
+ Registers this plugin's scheme in the database. Called once in the
144
+ constructor.
79
145
 
80
- ### handler(entry, rummy)
146
+ ```js
147
+ core.registerScheme({
148
+ modelVisible: 1, // 1 or 0 — appears in v_model_context
149
+ category: "logging", // "data", "logging", "unknown", "prompt"
150
+ });
151
+ ```
81
152
 
82
- The handler receives the parsed command entry and a per-turn RummyContext:
153
+ All fields optional. `core.registerScheme()` with no args gives a
154
+ sensible result-type scheme.
155
+
156
+ ### §3.3 core.on(event, callback, priority?)
157
+
158
+ | Event | Payload | Purpose |
159
+ |-------|---------|---------|
160
+ | `"handler"` | `(entry, rummy)` | Tool handler — called when model/client invokes this tool |
161
+ | `"full"` | `(entry)` | Full fidelity projection — what the model sees at full |
162
+ | `"summary"` | `(entry)` | Summary fidelity projection — what the model sees at summary |
163
+ | `"turn.started"` | `(ctx)` | Turn beginning — write prompt/progress/instructions entries |
164
+ | `"turn.response"` | `(result, rummy)` | LLM responded — write audit entries, commit usage |
165
+ | `"turn.proposing"` | `(rummy)` | All dispatches done — materialize file edit proposals |
166
+ | `"entry.created"` | `({ runId, path, scheme })` | Entry created during dispatch |
167
+ | `"entry.changed"` | `({ runId, path, changeType })` | Entry content, fidelity, or status modified |
168
+ | Any `"dotted.name"` | varies | Resolves to the matching hook in the hook tree |
169
+
170
+ ```js
171
+ // One-liner examples
172
+ core.on("handler", async (entry, rummy) => { /* tool logic */ });
173
+ core.on("full", (entry) => entry.body);
174
+ core.on("summary", (entry) => entry.body?.slice(0, 200));
175
+ core.on("turn.started", async (ctx) => { /* write entries */ });
176
+ core.on("turn.response", async (result, rummy) => { /* audit */ });
177
+ core.on("entry.changed", ({ runId, path, changeType }) => { /* react */ });
178
+ ```
179
+
180
+ ### §3.4 core.filter(name, callback, priority?)
181
+
182
+ | Filter | Signature | Purpose |
183
+ |--------|-----------|---------|
184
+ | `"instructions.toolDocs"` | `(docsMap) → docsMap` | Add tool documentation (docsMap pattern) |
185
+ | `"assembly.system"` | `(content, ctx) → content` | Contribute to system message |
186
+ | `"assembly.user"` | `(content, ctx) → content` | Contribute to user message |
187
+ | `"llm.messages"` | `(messages) → messages` | Transform final messages before LLM call |
188
+ | `"llm.response"` | `(response) → response` | Transform LLM response |
189
+ | Any `"dotted.name"` | varies | Resolves to the matching filter in the hook tree |
190
+
191
+ ```js
192
+ // One-liner examples
193
+ core.filter("assembly.system", async (content, ctx) => {
194
+ return `${content}\n<mytag>${myData}</mytag>`;
195
+ }, 400);
196
+ core.filter("assembly.user", async (content, ctx) => {
197
+ return `${content}\n<status>${myStatus}</status>`;
198
+ }, 150);
199
+ core.filter("instructions.toolDocs", async (docsMap) => {
200
+ docsMap.mytool = docs;
201
+ return docsMap;
202
+ });
203
+ ```
204
+
205
+ The `ctx` object passed to assembly filters:
206
+
207
+ ```js
208
+ ctx = {
209
+ rows, // turn_context rows (materialized entries)
210
+ loopStartTurn, // First turn of current loop
211
+ type, // "ask" or "act"
212
+ tools, // Set of active tool names
213
+ contextSize, // Model context window size
214
+ lastContextTokens, // Assembled tokens from previous turn
215
+ }
216
+ ```
217
+
218
+ ### §3.5 Tool Docs
219
+
220
+ Each tool plugin has a `*Doc.js` file with annotated line arrays.
221
+ Text goes to the model. Rationale stays in source. Registered via
222
+ the `instructions.toolDocs` filter using the docsMap pattern:
223
+
224
+ ```js
225
+ import docs from "./mytoolDoc.js";
226
+
227
+ core.filter("instructions.toolDocs", async (docsMap) => {
228
+ docsMap.mytool = docs;
229
+ return docsMap;
230
+ });
231
+ ```
232
+
233
+ The instructions plugin filters by the active tool set — tools
234
+ excluded by mode or flags are automatically omitted from the docs.
235
+
236
+ ### §3.6 handler(entry, rummy)
237
+
238
+ The handler receives the parsed command entry and a per-turn
239
+ RummyContext:
83
240
 
84
241
  ```js
85
242
  entry = {
@@ -87,216 +244,316 @@ entry = {
87
244
  path, // Entry path ("set://src/app.js")
88
245
  body, // Tag body text
89
246
  attributes, // Parsed tag attributes
90
- state, // Current state
91
247
  resultPath, // Where to write the result
92
248
  }
93
249
  ```
94
250
 
95
- Multiple handlers per scheme. Lower priority runs first. Return `false`
96
- to stop the chain.
251
+ Multiple handlers per scheme. Lower priority runs first. Return
252
+ `false` to stop the chain.
97
253
 
98
- ### full(entry) / summary(entry)
254
+ ### §3.7 full(entry) / summary(entry)
99
255
 
100
- Returns the string the model sees for this tool's entries at the given
101
- fidelity. Called during materialization. Every tool MUST register `full`.
102
- `summary` is optional — if unregistered, the entry is invisible at summary
103
- fidelity (no fallback).
256
+ Returns the string the model sees for this tool's entries at the
257
+ given fidelity. Every tool MUST register `full`. `summary` is
258
+ optional — if unregistered, falls back to `attributes.summary`
259
+ (model-authored keyword description) or empty string.
104
260
 
105
- ## Two Objects
261
+ At summary fidelity, `attributes.summary` is prepended above the
262
+ plugin's summary output automatically by ToolRegistry.view().
263
+
264
+ ## §4 Two Objects
106
265
 
107
266
  Plugins interact with two objects at different scopes:
108
267
 
109
- **PluginContext** (`this.#core`) — startup-scoped. Created once per plugin.
110
- Used for registration (`on()`, `filter()`), database access, store queries.
111
- Lives for the lifetime of the service. This is `rummy.core` the
112
- plugin-only tier that clients cannot reach.
268
+ **PluginContext** (`core`) — startup-scoped. Created once per plugin.
269
+ Used for registration (`on()`, `filter()`, `registerScheme()`,
270
+ `ensureTool()`). Available as `this.#core` throughout the plugin's
271
+ lifetime.
113
272
 
114
- **RummyContext** (`rummy` argument) — turn-scoped. Passed to handlers
115
- per-invocation. Has tool verbs, per-turn state (runId, turn, mode).
273
+ **RummyContext** (`rummy`) — turn-scoped. Passed to handlers per
274
+ invocation. Has tool verbs, per-turn state, database access.
116
275
 
117
- ### Tool Verbs (available on both objects)
276
+ ### §4.1 Tool Verbs (on RummyContext)
118
277
 
119
278
  | Method | Effect |
120
279
  |--------|--------|
121
- | `rummy.set({ path, body, state, attributes })` | Create/update entry |
122
- | `rummy.get({ path })` | Promote to full state |
123
- | `rummy.store({ path })` | Demote to stored state |
280
+ | `rummy.set({ path, body, status, fidelity, attributes })` | Create/update entry |
281
+ | `rummy.get({ path })` | Promote to full fidelity |
124
282
  | `rummy.rm({ path })` | Delete permanently |
125
283
  | `rummy.mv({ path, to })` | Move entry |
126
284
  | `rummy.cp({ path, to })` | Copy entry |
127
285
 
128
- ### Query Methods
286
+ ### §4.2 Query Methods
129
287
 
130
288
  | Method | Returns |
131
289
  |--------|---------|
132
- | `rummy.getEntry(path)` | Full entry object |
133
290
  | `rummy.getBody(path)` | Body text or null |
134
- | `rummy.getState(path)` | State string or null |
291
+ | `rummy.getState(path)` | Status code or null |
135
292
  | `rummy.getAttributes(path)` | Parsed attributes `{}` |
136
293
  | `rummy.getEntries(pattern, body?)` | Array of matching entries |
137
294
 
138
- ### Properties
295
+ ### §4.3 Properties
139
296
 
140
- | Property | Type |
141
- |----------|------|
142
- | `rummy.name` | Plugin name (PluginContext only) |
143
- | `rummy.entries` | KnownStore instance |
144
- | `rummy.db` | Database |
145
- | `rummy.runId` | Current run ID (RummyContext only) |
146
- | `rummy.projectId` | Current project ID |
147
- | `rummy.sequence` | Current turn number (RummyContext only) |
297
+ | Property | Type | Scope |
298
+ |----------|------|-------|
299
+ | `rummy.entries` | KnownStore instance | Both |
300
+ | `rummy.db` | Database | Both |
301
+ | `rummy.runId` | Current run ID | RummyContext |
302
+ | `rummy.projectId` | Current project ID | Both |
303
+ | `rummy.sequence` | Current turn number | RummyContext |
304
+ | `rummy.contextSize` | Model context window | RummyContext |
305
+ | `rummy.noRepo` | Skip filesystem scanning | RummyContext |
148
306
 
149
- ## Events & Filters
307
+ ## §5 Tool Display Order
150
308
 
151
- **Events** are fire-and-forget. All handlers run. Return values ignored.
309
+ Tools are presented to the model in priority order:
310
+ gather → reason → act → communicate.
152
311
 
153
- ```js
154
- hooks.entry.changed.on(async ({ rummy, runId, turn, paths }) => {
155
- // React to file changes
156
- }, priority);
157
- ```
158
-
159
- **Filters** transform data through a chain. Each handler receives the value
160
- and context, returns the (possibly modified) value.
161
-
162
- ```js
163
- hooks.llm.messages.addFilter(async (messages, context) => {
164
- return [{ role: "system", content: "Extra" }, ...messages];
165
- }, priority);
166
- ```
312
+ Defined in `ToolRegistry.TOOL_ORDER`. The `resolveForLoop(mode, flags)`
313
+ method handles all exclusions through one mechanism:
167
314
 
168
- Lower priority runs first. All hooks are async.
315
+ | Flag | Excludes |
316
+ |------|----------|
317
+ | `mode === "ask"` | `sh` |
318
+ | `noInteraction` | `ask_user` |
319
+ | `noWeb` | `search` |
169
320
 
170
- ### Project Lifecycle
321
+ ## §6 Hedberg
171
322
 
172
- | Hook | Type | Payload | When |
173
- |------|------|---------|------|
174
- | `project.init.started` | event | `{ projectName, projectRoot }` | Before project DB upsert |
175
- | `project.init.completed` | event | `{ projectId, projectRoot, db }` | After project created |
323
+ The hedberg plugin exposes pattern matching and interpretation
324
+ utilities on `core.hooks.hedberg` for all plugins to use:
176
325
 
177
- ### RPC Pipeline
178
-
179
- | Hook | Type | Payload | When |
180
- |------|------|---------|------|
181
- | `socket.message.raw` | filter | Raw buffer | Before JSON parse |
182
- | `rpc.request` | filter | `{ method, params, id }` | Before handler lookup |
183
- | `rpc.started` | event | `{ method, params, id, projectId }` | Before handler execution |
184
- | `rpc.response.result` | filter | `result, { method, id }` | Before sending response |
185
- | `rpc.completed` | event | `{ method, id, result }` | After response sent |
186
- | `rpc.error` | event | `{ id, error }` | On handler error |
326
+ ```js
327
+ const { match, search, replace, parseSed, parseEdits,
328
+ normalizeAttrs, generatePatch } = core.hooks.hedberg;
329
+ ```
187
330
 
188
- ### Run Lifecycle
331
+ | Method | Purpose |
332
+ |--------|---------|
333
+ | `match(pattern, string)` | Full-string pattern match (glob, regex, literal) |
334
+ | `search(pattern, string)` | Substring search |
335
+ | `replace(body, search, replacement, opts?)` | Apply replacement |
336
+ | `parseSed(input)` | Parse sed syntax (any delimiter) |
337
+ | `parseEdits(content)` | Detect edit format (merge conflict, udiff, sed) |
338
+ | `normalizeAttrs(attrs)` | Heal model attribute names |
339
+ | `generatePatch(path, old, new)` | Generate unified diff |
189
340
 
190
- | Hook | Type | Payload | When |
191
- |------|------|---------|------|
192
- | `ask.started` | event | `{ projectId, model, prompt, run }` | Run requested in ask mode |
193
- | `act.started` | event | `{ projectId, model, prompt, run }` | Run requested in act mode |
194
- | `run.config` | filter | Config object, `{ projectId }` | Before run config applied |
195
- | `run.progress` | event | `{ run, turn, status }` | Status change (thinking, processing) |
196
- | `run.state` | event | `{ run, turn, status, summary, history, unknowns, proposed, telemetry }` | After each turn — full state snapshot |
197
- | `run.step.completed` | event | `{ run, turn, flags }` | Turn resolved, no proposals pending |
198
- | `ask.completed` | event | `{ projectId, run, status, turn }` | Ask run finished |
199
- | `act.completed` | event | `{ projectId, run, status, turn }` | Act run finished |
341
+ ## §7 Events & Filters
200
342
 
201
- ### Turn Pipeline
343
+ **Events** are fire-and-forget. All handlers run. Return values ignored.
344
+ **Filters** transform data through a chain. Lower priority runs first.
345
+ All hooks are async.
346
+
347
+ ### §7.1 Project Lifecycle
348
+
349
+ | Hook | Type | When |
350
+ |------|------|------|
351
+ | `project.init.started` | event | Before project DB upsert |
352
+ | `project.init.completed` | event | After project created |
353
+
354
+ ### §7.2 Run & Loop Lifecycle
355
+
356
+ | Hook | Type | When |
357
+ |------|------|------|
358
+ | `run.created` | event | Run just created in DB |
359
+ | `ask.started` | event | Run requested in ask mode |
360
+ | `act.started` | event | Run requested in act mode |
361
+ | `loop.started` | event | Loop execution beginning |
362
+ | `run.config` | filter | Before run config applied |
363
+ | `run.progress` | event | Status change (thinking, processing) |
364
+ | `run.state` | event | After each turn — full state snapshot |
365
+ | `run.step.completed` | event | Turn resolved, no proposals pending |
366
+ | `loop.completed` | event | Loop execution finished (any exit path) |
367
+ | `ask.completed` | event | Ask run finished |
368
+ | `act.completed` | event | Act run finished |
369
+
370
+ ### §7.3 Turn Pipeline
202
371
 
203
372
  Hooks fire in this order every turn:
204
373
 
205
- | Hook | Type | Payload | When |
206
- |------|------|---------|------|
207
- | `entry.changed` | event | `{ rummy, runId, turn, paths }` | Files changed on disk since last turn |
208
- | `onTurn` | processor | `(rummy)` | Plugin turn setup, before context assembly |
209
- | `assembly.system` | filter | `(content, { rows, loopStartTurn, type, tools })` | Build system message plugins append sections |
210
- | `assembly.user` | filter | `(content, { rows, loopStartTurn, type, tools })` | Build user message plugins append sections |
211
- | `llm.messages` | filter | `messages[], { model, projectId, runId }` | Before LLM call — modify final messages |
212
- | `llm.request.started` | event | `{ model, turn }` | LLM call about to fire |
213
- | `llm.response` | filter | `response, { model, projectId, runId }` | Raw LLM response normalize, transform |
214
- | `llm.request.completed` | event | `{ model, turn, usage }` | LLM call finished |
215
- | `tools.dispatch` | handler | `(entry, rummy)` | Per command handler chain executes |
216
- | `entry.created` | event | `{ scheme, path, body, attributes, state, resultPath }` | After each command dispatched |
217
- | `turn.proposing` | event | `{ rummy, recorded }` | All dispatches done — materialize proposals |
218
-
219
- ### Client Notifications
220
-
221
- | Hook | Type | Payload | When |
222
- |------|------|---------|------|
223
- | `ui.render` | event | `{ text, append }` | Text for client display |
224
- | `ui.notify` | event | `{ text, level }` | Status notification |
225
-
226
- ## RPC Registration
374
+ | # | Hook | Type | When |
375
+ |---|------|------|------|
376
+ | 1 | `turn.started` | event | Plugins write prompt/progress/instructions entries |
377
+ | 2 | `context.materialized` | event | turn_context populated from v_model_context |
378
+ | 3 | `assembly.system` | filter | Build system message from entries |
379
+ | 4 | `assembly.user` | filter | Build user message from entries |
380
+ | 5 | `budget.enforce` | hook | Measure assembled tokens, 413 if over |
381
+ | 6 | `llm.messages` | filter | Transform messages before LLM call |
382
+ | 7 | `llm.request.started` | event | LLM call about to fire |
383
+ | 8 | `llm.response` | filter | Transform raw LLM response |
384
+ | 9 | `llm.request.completed` | event | LLM call finished |
385
+ | 10 | `turn.response` | event | Plugins write audit entries |
386
+ | 11 | `entry.recording` | filter | Before each entry is stored (validate/transform) |
387
+ | 12 | `tool.before` | event | Before tool handler dispatch |
388
+ | 13 | Tool handler dispatch | — | Lifecycle always, actions sequential |
389
+ | 14 | `tool.after` | event | After tool handler dispatch |
390
+ | 15 | `entry.created` | event | After each new entry dispatched |
391
+ | 16 | `entry.changed` | event | After entry content, fidelity, or status modified |
392
+ | 17 | `turn.proposing` | event | All dispatches done materialize proposals |
393
+ | 18 | `turn.completed` | event | Turn fully resolved with final status |
394
+
395
+ ### §7.4 Entry Events
396
+
397
+ | Hook | Type | When |
398
+ |------|------|------|
399
+ | `entry.recording` | filter | Before entry stored. Return `{ status: 4xx }` to reject. |
400
+ | `entry.created` | event | New entry added during dispatch |
401
+ | `entry.changed` | event | Entry content, fidelity, or status modified |
402
+
403
+ `entry.recording` is a filter — plugins can validate, transform, or
404
+ reject entries before they hit the store. Payload:
405
+ `{ scheme, path, body, attributes, status }`. Return the object
406
+ (modified or not). Set `status >= 400` to reject.
407
+
408
+ `entry.changed` fires on any mutation to an existing entry — body
409
+ update, fidelity change, status change, attribute update. Payload:
410
+ `{ runId, path, changeType }`. Subscribers include the budget plugin
411
+ (remeasure context) and the repo plugin (detect file changes on disk).
412
+
413
+ ### §7.5 Budget
414
+
415
+ | Hook | Type | When |
416
+ |------|------|------|
417
+ | `budget.enforce` | hook | After assembly, before LLM call. Returns 413 if over context limit. |
418
+
419
+ The budget plugin measures `countTokens()` on assembled messages —
420
+ the actual content being sent to the LLM. No estimates, no DB token
421
+ math. The assembled message IS the measurement.
422
+
423
+ **DB tokens vs assembled tokens:** The `tokens` column on entries is
424
+ strictly for DISPLAY — showing token counts in `<knowns>` tags so
425
+ the model can reason about entry sizes. It is NEVER used for budget
426
+ decisions. Budget math uses only assembled message token counts.
427
+ These are two separate numbers that must never be conflated.
428
+
429
+ ### §7.6 Client Notifications
430
+
431
+ | Hook | Type | When |
432
+ |------|------|------|
433
+ | `ui.render` | event | Text for client display |
434
+ | `ui.notify` | event | Status notification |
435
+
436
+ ## §8 Entry Lifecycle
437
+
438
+ Every entry follows the same lifecycle regardless of origin:
439
+
440
+ 1. **Created** — `known_entries` row with scheme, path, body, status
441
+ 2. **Dispatched** — tool handler chain executes
442
+ 3. **Status set** — handler sets 200, 202, 400, 413, etc.
443
+ 4. **Materialized** — `v_model_context` projects into `turn_context`
444
+ 5. **Assembled** — filter chain renders into system/user messages
445
+ 6. **Visible** — model sees the entry in its context
446
+
447
+ Entries at `archive` fidelity skip steps 4-6 (invisible to model).
448
+ Entries at `index` fidelity render as path-only tags (no body).
449
+ Entries at `summary` fidelity render with `attributes.summary`
450
+ prepended above the plugin's summary view output.
451
+
452
+ ## §9 Bundled Plugins
227
453
 
228
- ```js
229
- hooks.rpc.registry.register("myMethod", {
230
- handler: async (params, ctx) => {
231
- // ctx.projectAgent, ctx.db, ctx.projectId, ctx.projectRoot
232
- return { result: "value" };
233
- },
234
- description: "What this method does",
235
- params: { arg1: "description" },
236
- requiresInit: true,
237
- longRunning: true, // for methods that call the model
238
- });
239
- ```
240
-
241
- ## Hedberg Pattern Library
242
-
243
- Available in JS and SQL. Five pattern types, auto-detected:
454
+ | Plugin | Type | Description |
455
+ |--------|------|-------------|
456
+ | `get` | Core tool | Load file/entry into context |
457
+ | `set` | Core tool | Edit file/entry, fidelity control |
458
+ | `known` | Core tool + Assembly | Save knowledge, render `<knowns>` section |
459
+ | `rm` | Core tool | Delete permanently |
460
+ | `mv` | Core tool | Move entry |
461
+ | `cp` | Core tool | Copy entry |
462
+ | `sh` | Core tool | Shell command (act mode only) |
463
+ | `env` | Core tool | Exploratory command |
464
+ | `ask_user` | Core tool | Ask the user |
465
+ | `search` | Core tool | Web search (via external plugin) |
466
+ | `summarize` | Structural | Signal completion |
467
+ | `update` | Structural | Signal continued work |
468
+ | `unknown` | Structural + Assembly | Register unknowns, render `<unknowns>` |
469
+ | `previous` | Assembly | Render `<previous>` loop history |
470
+ | `current` | Assembly | Render `<current>` active loop work |
471
+ | `progress` | Assembly | Render `<progress>` telemetry + warnings |
472
+ | `prompt` | Assembly | Render `<prompt mode="ask|act">` tag |
473
+ | `hedberg` | Utility | Pattern matching, interpretation, normalization |
474
+ | `instructions` | Internal | Preamble + tool docs + persona assembly |
475
+ | `file` | Internal | File entry projections and constraints |
476
+ | `rpc` | Internal | RPC method registration |
477
+ | `telemetry` | Internal | Audit entries, usage stats, reasoning_content |
478
+ | `budget` | Internal | Context ceiling enforcement (413), panic mode, BudgetGuard |
479
+ | `think` | Internal | Model reasoning tag (`model_visible = 0`) |
480
+ | `mcp` | Core tool | Model Context Protocol server management |
481
+
482
+ Removed: `crunch` (dead code, replaced by model-owned context management),
483
+ `store` (merged into `set` fidelity attributes).
484
+
485
+ ## §10 External Plugins
244
486
 
245
- | Syntax | Type | Example |
246
- |--------|------|---------|
247
- | `s/old/new/flags` | Sed replace | `s/3000/8080/g` |
248
- | `/pattern/flags` | Regex | `/\d+/g` |
249
- | `$.path` | JSONPath | `$.config.port` |
250
- | `//element` | XPath | `//div[@class]` |
251
- | `*glob*` | Glob | `src/**/*.js` |
252
- | Everything else | Literal | `port = 3000` |
487
+ | Plugin | Package | Description |
488
+ |--------|---------|-------------|
489
+ | Repo | `@possumtech/rummy.repo` | Git-aware file scanning and symbol extraction |
490
+ | Web | `@possumtech/rummy.web` | Web search and URL fetching via searxng |
253
491
 
254
- JS API:
492
+ Loaded via `RUMMY_PLUGIN_*` env vars. External plugins have access
493
+ to the same PluginContext API as bundled plugins.
255
494
 
256
- ```js
257
- import { hedmatch, hedsearch, hedreplace } from "./sql/functions/hedberg.js";
495
+ ## §11 RPC Methods
258
496
 
259
- hedmatch(pattern, string) // boolean (full string match)
260
- hedsearch(pattern, string) // { found, match, index }
261
- hedreplace(pattern, replacement, string) // → new string or null
262
- ```
497
+ Client-facing JSON-RPC 2.0 over WebSocket. All tool methods go through
498
+ the same handler chain as model commands.
263
499
 
264
- SQL functions: `hedmatch()`, `hedsearch()`, `hedreplace()`.
500
+ ### §11.1 Wire Format
265
501
 
266
- ## Bundled Plugins
502
+ ```json
503
+ // Request
504
+ { "jsonrpc": "2.0", "id": 1, "method": "get", "params": { "path": "src/app.js", "run": "my_run" } }
267
505
 
268
- Each plugin has its own README at `src/plugins/{name}/README.md`.
506
+ // Success response
507
+ { "jsonrpc": "2.0", "id": 1, "result": { "path": "src/app.js", "status": 200 } }
269
508
 
270
- | Plugin | Type | Description |
271
- |--------|------|-------------|
272
- | [`get`](src/plugins/get/) | Core tool | Load file/entry into context |
273
- | [`set`](src/plugins/set/) | Core tool | Edit file/entry |
274
- | [`known`](src/plugins/known/) | Core tool | Save knowledge, render `<known>` |
275
- | [`store`](src/plugins/store/) | Core tool | Remove from context |
276
- | [`rm`](src/plugins/rm/) | Core tool | Delete permanently |
277
- | [`mv`](src/plugins/mv/) | Core tool | Move entry |
278
- | [`cp`](src/plugins/cp/) | Core tool | Copy entry |
279
- | [`sh`](src/plugins/sh/) | Core tool | Shell command |
280
- | [`env`](src/plugins/env/) | Core tool | Exploratory command |
281
- | [`ask_user`](src/plugins/ask_user/) | Core tool | Ask the user |
282
- | [`summarize`](src/plugins/summarize/) | Structural | Signal completion |
283
- | [`update`](src/plugins/update/) | Structural | Signal continued work |
284
- | [`unknown`](src/plugins/unknown/) | Structural | Register unknowns, render `<unknowns>` |
285
- | [`previous`](src/plugins/previous/) | Assembly | Render `<previous>` loop history |
286
- | [`current`](src/plugins/current/) | Assembly | Render `<current>` active loop work |
287
- | [`progress`](src/plugins/progress/) | Assembly | Render `<progress>` bridge text |
288
- | [`prompt`](src/plugins/prompt/) | Assembly | Render `<ask>`/`<act>` prompt tag |
289
- | [`instructions`](src/plugins/instructions/) | Internal | System prompt assembly |
290
- | [`file`](src/plugins/file/) | Internal | File projections, constraints, scanning |
291
- | [`rpc`](src/plugins/rpc/) | Internal | RPC method registration |
292
- | [`skills`](src/plugins/skills/) | Internal | Skill/persona management |
293
- | [`telemetry`](src/plugins/telemetry/) | Internal | Debug logging |
294
-
295
- ## External Plugins
509
+ // Error response
510
+ { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Missing required param: path" } }
296
511
 
297
- | Plugin | Package | Description |
298
- |--------|---------|-------------|
299
- | Web | `@possumtech/rummy.web` | Search and URL fetching |
300
- | Repo | `@possumtech/rummy.repo` | Symbol extraction |
512
+ // Notification (server client, no id)
513
+ { "jsonrpc": "2.0", "method": "run/state", "params": { "run": "my_run", "status": 200 } }
514
+ ```
301
515
 
302
- Loaded via `RUMMY_PLUGIN_*` env vars. Graceful failure if not installed.
516
+ ### §11.2 Tool Methods (Unified API)
517
+
518
+ | Method | Params | Notes |
519
+ |--------|--------|-------|
520
+ | `get` | `{ path, run, persist?, readonly? }` | `persist` also sets file constraint |
521
+ | `set` | `{ path, body, run, attributes? }` | All entries go through handler chain |
522
+ | `rm` | `{ path, run }` | |
523
+ | `mv` | `{ path, to, run }` | |
524
+ | `cp` | `{ path, to, run }` | |
525
+ | `store` | `{ path, run?, persist?, ignore?, clear? }` | File constraints only — not a model tool |
526
+ | `getEntries` | `{ pattern?, body?, run?, limit?, offset? }` | Query entries |
527
+
528
+ ### §11.3 Run Management
529
+
530
+ | Method | Params | Notes |
531
+ |--------|--------|-------|
532
+ | `startRun` | `{ model, temperature?, persona?, contextLimit? }` | Create run without prompt |
533
+ | `ask` | `{ model, prompt, run?, noInteraction?, noWeb?, noRepo? }` | |
534
+ | `act` | `{ model, prompt, run?, noInteraction?, noWeb?, noRepo? }` | |
535
+ | `run/resolve` | `{ run, resolution }` | Accept/reject proposals |
536
+ | `run/abort` | `{ run }` | Cancel active run |
537
+ | `run/config` | `{ run, contextLimit?, persona?, model? }` | Update run settings |
538
+ | `run/rename` | `{ run, name }` | Change run alias |
539
+ | `run/inject` | `{ run, message }` | Inject message into active turn |
540
+
541
+ ### §11.4 Project Management
542
+
543
+ | Method | Params | Notes |
544
+ |--------|--------|-------|
545
+ | `init` | `{ name, projectRoot }` | Initialize project |
546
+ | `addModel` | `{ alias, actual, contextLength? }` | Register model |
547
+ | `removeModel` | `{ alias }` | Remove model |
548
+ | `getRuns` | `{ limit?, offset? }` | List runs |
549
+ | `getRun` | `{ run }` | Get single run details |
550
+ | `getModels` | `{}` | List models |
551
+
552
+ ### §11.5 Notifications (server → client)
553
+
554
+ | Method | Payload |
555
+ |--------|---------|
556
+ | `run/state` | `{ run, status, turn, entries, ... }` |
557
+ | `run/progress` | `{ run, status }` |
558
+ | `ui/render` | `{ text }` |
559
+ | `ui/notify` | `{ message }` |