@possumtech/rummy 0.2.8 → 0.3.1

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 (114) hide show
  1. package/.env.example +13 -2
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +440 -106
  5. package/migrations/001_initial_schema.sql +5 -3
  6. package/package.json +17 -5
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +252 -55
  9. package/src/agent/ContextAssembler.js +20 -4
  10. package/src/agent/KnownStore.js +82 -25
  11. package/src/agent/ProjectAgent.js +4 -1
  12. package/src/agent/ResponseHealer.js +86 -32
  13. package/src/agent/TurnExecutor.js +542 -207
  14. package/src/agent/XmlParser.js +77 -41
  15. package/src/agent/known_store.sql +68 -4
  16. package/src/agent/schemes.sql +3 -0
  17. package/src/agent/tokens.js +7 -21
  18. package/src/agent/turns.sql +15 -1
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +15 -0
  21. package/src/hooks/PluginContext.js +14 -1
  22. package/src/hooks/RummyContext.js +16 -4
  23. package/src/hooks/ToolRegistry.js +77 -19
  24. package/src/llm/LlmProvider.js +27 -8
  25. package/src/llm/OpenAiClient.js +20 -0
  26. package/src/llm/OpenRouterClient.js +24 -2
  27. package/src/llm/XaiClient.js +47 -2
  28. package/src/plugins/ask_user/README.md +4 -4
  29. package/src/plugins/ask_user/ask_user.js +5 -5
  30. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  31. package/src/plugins/budget/README.md +31 -0
  32. package/src/plugins/budget/budget.js +55 -0
  33. package/src/plugins/cp/README.md +5 -4
  34. package/src/plugins/cp/cp.js +10 -6
  35. package/src/plugins/cp/cpDoc.js +29 -0
  36. package/src/plugins/engine/engine.sql +1 -8
  37. package/src/plugins/engine/turn_context.sql +4 -9
  38. package/src/plugins/env/README.md +3 -4
  39. package/src/plugins/env/env.js +5 -5
  40. package/src/plugins/env/envDoc.js +29 -0
  41. package/src/plugins/file/README.md +9 -12
  42. package/src/plugins/file/file.js +34 -35
  43. package/src/plugins/get/README.md +2 -2
  44. package/src/plugins/get/get.js +77 -6
  45. package/src/plugins/get/getDoc.js +51 -0
  46. package/src/plugins/hedberg/hedberg.js +2 -1
  47. package/src/plugins/hedberg/matcher.js +10 -29
  48. package/src/plugins/hedberg/normalize.js +28 -0
  49. package/src/plugins/hedberg/patterns.js +25 -27
  50. package/src/plugins/hedberg/sed.js +17 -10
  51. package/src/plugins/index.js +66 -14
  52. package/src/plugins/instructions/README.md +6 -2
  53. package/src/plugins/instructions/instructions.js +20 -4
  54. package/src/plugins/instructions/preamble.md +19 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +23 -17
  57. package/src/plugins/known/knownDoc.js +34 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +27 -6
  60. package/src/plugins/mv/mvDoc.js +45 -0
  61. package/src/plugins/performed/README.md +15 -0
  62. package/src/plugins/performed/performed.js +45 -0
  63. package/src/plugins/persona/persona.js +78 -0
  64. package/src/plugins/previous/README.md +3 -2
  65. package/src/plugins/previous/previous.js +33 -24
  66. package/src/plugins/progress/README.md +1 -2
  67. package/src/plugins/progress/progress.js +33 -21
  68. package/src/plugins/prompt/README.md +5 -5
  69. package/src/plugins/prompt/prompt.js +15 -17
  70. package/src/plugins/rm/README.md +4 -4
  71. package/src/plugins/rm/rm.js +32 -20
  72. package/src/plugins/rm/rmDoc.js +30 -0
  73. package/src/plugins/rpc/README.md +15 -28
  74. package/src/plugins/rpc/rpc.js +42 -77
  75. package/src/plugins/set/README.md +13 -12
  76. package/src/plugins/set/set.js +107 -16
  77. package/src/plugins/set/setDoc.js +49 -0
  78. package/src/plugins/sh/README.md +4 -4
  79. package/src/plugins/sh/sh.js +5 -5
  80. package/src/plugins/sh/shDoc.js +29 -0
  81. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  82. package/src/plugins/summarize/README.md +6 -5
  83. package/src/plugins/summarize/summarize.js +7 -6
  84. package/src/plugins/summarize/summarizeDoc.js +33 -0
  85. package/src/plugins/telemetry/telemetry.js +16 -9
  86. package/src/plugins/think/README.md +20 -0
  87. package/src/plugins/think/think.js +5 -0
  88. package/src/plugins/unknown/README.md +6 -5
  89. package/src/plugins/unknown/unknown.js +12 -9
  90. package/src/plugins/unknown/unknownDoc.js +31 -0
  91. package/src/plugins/update/README.md +3 -8
  92. package/src/plugins/update/update.js +7 -6
  93. package/src/plugins/update/updateDoc.js +33 -0
  94. package/src/server/ClientConnection.js +59 -45
  95. package/src/server/RpcRegistry.js +52 -4
  96. package/src/sql/v_model_context.sql +10 -25
  97. package/src/plugins/ask_user/docs.md +0 -2
  98. package/src/plugins/cp/docs.md +0 -2
  99. package/src/plugins/current/README.md +0 -14
  100. package/src/plugins/current/current.js +0 -47
  101. package/src/plugins/env/docs.md +0 -4
  102. package/src/plugins/get/docs.md +0 -10
  103. package/src/plugins/known/docs.md +0 -3
  104. package/src/plugins/mv/docs.md +0 -2
  105. package/src/plugins/rm/docs.md +0 -6
  106. package/src/plugins/set/docs.md +0 -6
  107. package/src/plugins/sh/docs.md +0 -2
  108. package/src/plugins/skills/README.md +0 -25
  109. package/src/plugins/store/README.md +0 -20
  110. package/src/plugins/store/docs.md +0 -6
  111. package/src/plugins/store/store.js +0 -63
  112. package/src/plugins/summarize/docs.md +0 -4
  113. package/src/plugins/unknown/docs.md +0 -5
  114. package/src/plugins/update/docs.md +0 -4
package/PLUGINS.md CHANGED
@@ -1,28 +1,89 @@
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
+ ```
9
45
 
10
46
  ```js
11
- import { readFileSync } from "node:fs";
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
12
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.
71
+
72
+ ```js
13
73
  export default class MyTool {
14
74
  #core;
15
75
 
16
76
  constructor(core) {
17
77
  this.#core = core;
18
- core.registerScheme();
78
+ core.ensureTool();
79
+ core.registerScheme({ category: "logging" });
19
80
  core.on("handler", this.handler.bind(this));
20
81
  core.on("full", this.full.bind(this));
21
82
  core.on("summary", this.summary.bind(this));
22
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
23
- core.filter("instructions.toolDocs", async (content) =>
24
- content ? `${content}\n\n${docs}` : docs,
25
- );
83
+ core.filter("instructions.toolDocs", async (docsMap) => {
84
+ docsMap.mytool = docs;
85
+ return docsMap;
86
+ });
26
87
  }
27
88
 
28
89
  async handler(entry, rummy) {
@@ -30,7 +91,7 @@ export default class MyTool {
30
91
  }
31
92
 
32
93
  full(entry) {
33
- return `# mytool ${entry.path}\n${entry.body}`;
94
+ return entry.body;
34
95
  }
35
96
 
36
97
  summary(entry) {
@@ -40,7 +101,7 @@ export default class MyTool {
40
101
  ```
41
102
 
42
103
  File naming: `src/plugins/mytool/mytool.js`. Class name = file name.
43
- Tool docs: `src/plugins/mytool/docs.md`.
104
+ Tool docs: `src/plugins/mytool/mytoolDoc.js` (annotated line arrays).
44
105
 
45
106
  External plugins install via npm and load via `RUMMY_PLUGIN_*` env vars:
46
107
 
@@ -49,11 +110,11 @@ RUMMY_PLUGIN_WEB=@possumtech/rummy.web
49
110
  RUMMY_PLUGIN_REPO=@possumtech/rummy.repo
50
111
  ```
51
112
 
52
- ## Unified API
113
+ ## §2 Unified API
53
114
 
54
- The model, the client, and plugins all use the same interface. Each tier
55
- is a superset of the one below. `name` (model) = `method` (client) =
56
- 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.
57
118
 
58
119
  ```
59
120
  Model: <rm path="file.txt"/> → { name: "rm", path: "file.txt" }
@@ -61,68 +122,121 @@ Client: { method: "rm", params: { path: "file.txt" } }
61
122
  Plugin: rummy.rm({ path: "file.txt" })
62
123
  ```
63
124
 
64
- ## 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>`.
128
+
129
+ ## §3 Registration
65
130
 
66
131
  All registration happens in the constructor via `core.on()`,
67
- `core.filter()`, and `core.registerScheme()`.
132
+ `core.filter()`, `core.ensureTool()`, and `core.registerScheme()`.
68
133
 
69
- ### core.registerScheme(config?)
134
+ ### §3.1 core.ensureTool()
135
+
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`).
140
+
141
+ ### §3.2 core.registerScheme(config?)
70
142
 
71
143
  Registers this plugin's scheme in the database. Called once in the
72
- constructor. Defaults are third-party friendly:
144
+ constructor.
73
145
 
74
146
  ```js
75
147
  core.registerScheme({
76
- fidelity: "full", // "full", "turn", or "null"
77
- modelVisible: 1, // 1 or 0
78
- validStates: ["full", "proposed", "pass", "rejected", "error"],
79
- category: "result", // "result", "file", "knowledge", "structural", "audit", "tool"
148
+ modelVisible: 1, // 1 or 0 — appears in v_model_context
149
+ category: "logging", // "data", "logging", "unknown", "prompt"
80
150
  });
81
151
  ```
82
152
 
83
153
  All fields optional. `core.registerScheme()` with no args gives a
84
154
  sensible result-type scheme.
85
155
 
86
- ### core.on(event, callback, priority?)
156
+ ### §3.3 core.on(event, callback, priority?)
87
157
 
88
- | Event | Purpose |
89
- |-------|---------|
90
- | `"handler"` | Tool handler — called when model/client invokes this tool |
91
- | `"full"` | Full fidelity — what the model sees in `<current>` |
92
- | `"summary"` | Summary fidelity — what the model sees in `<previous>` |
93
- | `"turn.started"` | Turn beginning — write prompt/progress/instructions entries |
94
- | `"turn.response"` | LLM responded — write audit entries, commit usage |
95
- | `"turn.proposing"` | All dispatches done — materialize file edit proposals |
96
- | `"entry.created"` | Entry created during dispatch |
97
- | `"entry.changed"` | File entries changed on disk |
98
- | Any `"dotted.name"` | Resolves to the matching hook in the hook tree |
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 |
99
169
 
100
- ### core.filter(name, callback, priority?)
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
+ ```
101
179
 
102
- | Filter | Purpose |
103
- |--------|---------|
104
- | `"instructions.toolDocs"` | Append tool documentation to model prompt |
105
- | `"assembly.system"` | Contribute to system message |
106
- | `"assembly.user"` | Contribute to user message |
107
- | `"llm.messages"` | Transform final messages before LLM call |
108
- | `"llm.response"` | Transform LLM response |
109
- | Any `"dotted.name"` | Resolves to the matching filter in the hook tree |
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 |
110
190
 
111
- ### Tool Docs
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
+ ```
112
204
 
113
- Each tool plugin has a `docs.md` file with model-facing documentation.
114
- Registered via the `instructions.toolDocs` filter in the constructor:
205
+ The `ctx` object passed to assembly filters:
115
206
 
116
207
  ```js
117
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
118
- core.filter("instructions.toolDocs", async (content) =>
119
- content ? `${content}\n\n${docs}` : docs,
120
- );
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
+ }
121
216
  ```
122
217
 
123
- ### handler(entry, rummy)
218
+ ### §3.5 Tool Docs
124
219
 
125
- The handler receives the parsed command entry and a per-turn RummyContext:
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:
126
240
 
127
241
  ```js
128
242
  entry = {
@@ -130,196 +244,316 @@ entry = {
130
244
  path, // Entry path ("set://src/app.js")
131
245
  body, // Tag body text
132
246
  attributes, // Parsed tag attributes
133
- state, // Current state
134
247
  resultPath, // Where to write the result
135
248
  }
136
249
  ```
137
250
 
138
- Multiple handlers per scheme. Lower priority runs first. Return `false`
139
- to stop the chain.
251
+ Multiple handlers per scheme. Lower priority runs first. Return
252
+ `false` to stop the chain.
253
+
254
+ ### §3.7 full(entry) / summary(entry)
140
255
 
141
- ### full(entry) / summary(entry)
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.
142
260
 
143
- Returns the string the model sees for this tool's entries at the given
144
- fidelity. `full` renders in `<current>` (active loop). `summary` renders
145
- in `<previous>` (completed loops). Every tool MUST register `full`.
146
- `summary` is optional — if unregistered, the entry is empty at summary
147
- fidelity.
261
+ At summary fidelity, `attributes.summary` is prepended above the
262
+ plugin's summary output automatically by ToolRegistry.view().
148
263
 
149
- ## Two Objects
264
+ ## §4 Two Objects
150
265
 
151
266
  Plugins interact with two objects at different scopes:
152
267
 
153
- **PluginContext** (`this.#core`) — startup-scoped. Created once per plugin.
154
- Used for registration (`on()`, `filter()`, `registerScheme()`), database
155
- access, store queries. This is `rummy.core` the plugin-only tier that
156
- 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.
157
272
 
158
- **RummyContext** (`rummy` argument) — turn-scoped. Passed to handlers
159
- 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.
160
275
 
161
- ### Tool Verbs (on RummyContext)
276
+ ### §4.1 Tool Verbs (on RummyContext)
162
277
 
163
278
  | Method | Effect |
164
279
  |--------|--------|
165
- | `rummy.set({ path, body, state, attributes })` | Create/update entry |
166
- | `rummy.get({ path })` | Promote to full state |
167
- | `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 |
168
282
  | `rummy.rm({ path })` | Delete permanently |
169
283
  | `rummy.mv({ path, to })` | Move entry |
170
284
  | `rummy.cp({ path, to })` | Copy entry |
171
285
 
172
- ### Query Methods
286
+ ### §4.2 Query Methods
173
287
 
174
288
  | Method | Returns |
175
289
  |--------|---------|
176
- | `rummy.getEntry(path)` | Full entry object |
177
290
  | `rummy.getBody(path)` | Body text or null |
178
- | `rummy.getState(path)` | State string or null |
291
+ | `rummy.getState(path)` | Status code or null |
179
292
  | `rummy.getAttributes(path)` | Parsed attributes `{}` |
180
293
  | `rummy.getEntries(pattern, body?)` | Array of matching entries |
181
294
 
182
- ### Properties
295
+ ### §4.3 Properties
296
+
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 |
306
+
307
+ ## §5 Tool Display Order
183
308
 
184
- | Property | Type |
185
- |----------|------|
186
- | `rummy.name` | Plugin name (PluginContext only) |
187
- | `rummy.entries` | KnownStore instance |
188
- | `rummy.db` | Database |
189
- | `rummy.runId` | Current run ID (RummyContext only) |
190
- | `rummy.projectId` | Current project ID |
191
- | `rummy.sequence` | Current turn number (RummyContext only) |
309
+ Tools are presented to the model in priority order:
310
+ gather → reason → act → communicate.
192
311
 
193
- ## Hedberg
312
+ Defined in `ToolRegistry.TOOL_ORDER`. The `resolveForLoop(mode, flags)`
313
+ method handles all exclusions through one mechanism:
194
314
 
195
- The hedberg plugin exposes pattern matching and interpretation utilities
196
- on `core.hooks.hedberg` for all plugins to use:
315
+ | Flag | Excludes |
316
+ |------|----------|
317
+ | `mode === "ask"` | `sh` |
318
+ | `noInteraction` | `ask_user` |
319
+ | `noWeb` | `search` |
320
+
321
+ ## §6 Hedberg
322
+
323
+ The hedberg plugin exposes pattern matching and interpretation
324
+ utilities on `core.hooks.hedberg` for all plugins to use:
197
325
 
198
326
  ```js
199
- const { match, search, replace, parseSed, parseEdits, normalizeAttrs, generatePatch }
200
- = core.hooks.hedberg;
327
+ const { match, search, replace, parseSed, parseEdits,
328
+ normalizeAttrs, generatePatch } = core.hooks.hedberg;
201
329
  ```
202
330
 
203
331
  | Method | Purpose |
204
332
  |--------|---------|
205
333
  | `match(pattern, string)` | Full-string pattern match (glob, regex, literal) |
206
- | `search(pattern, string)` | Substring search, returns `{ found, match, index }` |
207
- | `replace(body, search, replacement, opts?)` | Apply replacement (sed → literal → heuristic) |
208
- | `parseSed(input)` | Parse sed syntax into `[{ search, replace, flags, sed }]` |
209
- | `parseEdits(content)` | Detect edit format (merge conflict, udiff, Claude XML) |
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) |
210
338
  | `normalizeAttrs(attrs)` | Heal model attribute names |
211
339
  | `generatePatch(path, old, new)` | Generate unified diff |
212
340
 
213
- Pattern types (auto-detected):
214
-
215
- | Syntax | Type | Example |
216
- |--------|------|---------|
217
- | `s/old/new/flags` | Sed replace | `s/3000/8080/g` |
218
- | `/pattern/flags` | Regex | `/\d+/g` |
219
- | `$.path` | JSONPath | `$.config.port` |
220
- | `//element` | XPath | `//div[@class]` |
221
- | `*glob*` | Glob | `src/**/*.js` |
222
- | Everything else | Literal | `port = 3000` |
223
-
224
- ## Events & Filters
341
+ ## §7 Events & Filters
225
342
 
226
343
  **Events** are fire-and-forget. All handlers run. Return values ignored.
227
344
  **Filters** transform data through a chain. Lower priority runs first.
228
345
  All hooks are async.
229
346
 
230
- ### Project Lifecycle
347
+ ### §7.1 Project Lifecycle
231
348
 
232
- | Hook | Type | Payload | When |
233
- |------|------|---------|------|
234
- | `project.init.started` | event | `{ projectName, projectRoot }` | Before project DB upsert |
235
- | `project.init.completed` | event | `{ projectId, projectRoot, db }` | After project created |
349
+ | Hook | Type | When |
350
+ |------|------|------|
351
+ | `project.init.started` | event | Before project DB upsert |
352
+ | `project.init.completed` | event | After project created |
236
353
 
237
- ### RPC Pipeline
354
+ ### §7.2 Run & Loop Lifecycle
238
355
 
239
- | Hook | Type | Payload | When |
240
- |------|------|---------|------|
241
- | `socket.message.raw` | filter | Raw buffer | Before JSON parse |
242
- | `rpc.request` | filter | `{ method, params, id }` | Before handler lookup |
243
- | `rpc.started` | event | `{ method, params, id, projectId }` | Before handler execution |
244
- | `rpc.response.result` | filter | `result, { method, id }` | Before sending response |
245
- | `rpc.completed` | event | `{ method, id, result }` | After response sent |
246
- | `rpc.error` | event | `{ id, error }` | On handler error |
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 |
247
369
 
248
- ### Run Lifecycle
249
-
250
- | Hook | Type | Payload | When |
251
- |------|------|---------|------|
252
- | `ask.started` | event | `{ projectId, model, prompt, run }` | Run requested in ask mode |
253
- | `act.started` | event | `{ projectId, model, prompt, run }` | Run requested in act mode |
254
- | `run.config` | filter | Config object, `{ projectId }` | Before run config applied |
255
- | `run.progress` | event | `{ run, turn, status }` | Status change (thinking, processing) |
256
- | `run.state` | event | `{ run, turn, status, summary, history, unknowns, proposed, telemetry }` | After each turn |
257
- | `run.step.completed` | event | `{ run, turn, flags }` | Turn resolved, no proposals pending |
258
- | `ask.completed` | event | `{ projectId, run, status, turn }` | Ask run finished |
259
- | `act.completed` | event | `{ projectId, run, status, turn }` | Act run finished |
260
-
261
- ### Turn Pipeline
370
+ ### §7.3 Turn Pipeline
262
371
 
263
372
  Hooks fire in this order every turn:
264
373
 
265
- | Hook | Type | Payload | When |
266
- |------|------|---------|------|
267
- | `turn.started` | event | `{ rummy, mode, prompt, isContinuation }` | Plugins write prompt/progress/instructions entries |
268
- | `entry.changed` | event | `{ rummy, runId, turn, paths }` | Files changed on disk (repo plugin) |
269
- | `onTurn` | processor | `(rummy)` | Plugin turn setup, before context assembly |
270
- | `assembly.system` | filter | `(content, { rows, loopStartTurn, type, contextSize })` | Build system message |
271
- | `assembly.user` | filter | `(content, { rows, loopStartTurn, type, contextSize })` | Build user message |
272
- | `llm.messages` | filter | `messages[], { model, projectId, runId }` | Before LLM call |
273
- | `llm.request.started` | event | `{ model, turn }` | LLM call about to fire |
274
- | `llm.response` | filter | `response, { model, projectId, runId }` | Raw LLM response |
275
- | `llm.request.completed` | event | `{ model, turn, usage }` | LLM call finished |
276
- | `turn.response` | event | `{ rummy, turn, result, responseMessage, content, ... }` | Plugins write audit entries |
277
- | `tools.dispatch` | handler | `(entry, rummy)` | Per command handler chain executes |
278
- | `entry.created` | event | `{ scheme, path, body, attributes, state, resultPath }` | After each command dispatched |
279
- | `turn.proposing` | event | `{ rummy, recorded }` | All dispatches done materialize proposals |
280
-
281
- ### Client Notifications
282
-
283
- | Hook | Type | Payload | When |
284
- |------|------|---------|------|
285
- | `ui.render` | event | `{ text, append }` | Text for client display |
286
- | `ui.notify` | event | `{ text, level }` | Status notification |
287
-
288
- ## Bundled Plugins
289
-
290
- Each plugin has its own README at `src/plugins/{name}/README.md`.
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
291
453
 
292
454
  | Plugin | Type | Description |
293
455
  |--------|------|-------------|
294
- | [`get`](src/plugins/get/) | Core tool | Load file/entry into context |
295
- | [`set`](src/plugins/set/) | Core tool | Edit file/entry |
296
- | [`known`](src/plugins/known/) | Core tool + Assembly | Save knowledge, render `<knowns>` section |
297
- | [`store`](src/plugins/store/) | Core tool | Remove from context |
298
- | [`rm`](src/plugins/rm/) | Core tool | Delete permanently |
299
- | [`mv`](src/plugins/mv/) | Core tool | Move entry |
300
- | [`cp`](src/plugins/cp/) | Core tool | Copy entry |
301
- | [`sh`](src/plugins/sh/) | Core tool | Shell command |
302
- | [`env`](src/plugins/env/) | Core tool | Exploratory command |
303
- | [`ask_user`](src/plugins/ask_user/) | Core tool | Ask the user |
304
- | [`summarize`](src/plugins/summarize/) | Structural | Signal completion |
305
- | [`update`](src/plugins/update/) | Structural | Signal continued work |
306
- | [`unknown`](src/plugins/unknown/) | Structural + Assembly | Register unknowns, render `<unknowns>` |
307
- | [`previous`](src/plugins/previous/) | Assembly | Render `<previous>` loop history |
308
- | [`current`](src/plugins/current/) | Assembly | Render `<current>` active loop work |
309
- | [`progress`](src/plugins/progress/) | Assembly | Render `<progress>` telemetry + bridge |
310
- | [`prompt`](src/plugins/prompt/) | Assembly | Render `<ask>`/`<act>` prompt tag |
311
- | [`hedberg`](src/plugins/hedberg/) | Utility | Pattern matching, interpretation, normalization |
312
- | [`instructions`](src/plugins/instructions/) | Internal | Preamble + tool docs + persona assembly |
313
- | [`file`](src/plugins/file/) | Internal | File entry projections and constraints |
314
- | [`rpc`](src/plugins/rpc/) | Internal | RPC method registration |
315
- | [`skills`](src/plugins/skills/) | Internal | Skill/persona management |
316
- | [`telemetry`](src/plugins/telemetry/) | Internal | Audit entries, usage stats, last_run.txt |
317
-
318
- ## External Plugins
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
+ | `performed` | Assembly | Render `<performed>` 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
319
486
 
320
487
  | Plugin | Package | Description |
321
488
  |--------|---------|-------------|
322
489
  | Repo | `@possumtech/rummy.repo` | Git-aware file scanning and symbol extraction |
323
490
  | Web | `@possumtech/rummy.web` | Web search and URL fetching via searxng |
324
491
 
325
- Loaded via `RUMMY_PLUGIN_*` env vars.
492
+ Loaded via `RUMMY_PLUGIN_*` env vars. External plugins have access
493
+ to the same PluginContext API as bundled plugins.
494
+
495
+ ## §11 RPC Methods
496
+
497
+ Client-facing JSON-RPC 2.0 over WebSocket. All tool methods go through
498
+ the same handler chain as model commands.
499
+
500
+ ### §11.1 Wire Format
501
+
502
+ ```json
503
+ // Request
504
+ { "jsonrpc": "2.0", "id": 1, "method": "get", "params": { "path": "src/app.js", "run": "my_run" } }
505
+
506
+ // Success response
507
+ { "jsonrpc": "2.0", "id": 1, "result": { "path": "src/app.js", "status": 200 } }
508
+
509
+ // Error response
510
+ { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Missing required param: path" } }
511
+
512
+ // Notification (server → client, no id)
513
+ { "jsonrpc": "2.0", "method": "run/state", "params": { "run": "my_run", "status": 200 } }
514
+ ```
515
+
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 }` |