@possumtech/rummy 0.5.0 → 2.0.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 (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
package/PLUGINS.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Every `<tag>` the model sees is a plugin. Every scheme is registered by
4
4
  its owner. Every operation — model, client, plugin — flows through the
5
- same tool handler. No exceptions without documentation in EXCEPTIONS.md.
5
+ same tool handler. Exceptions to that discipline must justify themselves
6
+ in the architecture spec (SPEC.md).
6
7
 
7
- ## §0 Quickstart
8
+ ## Quickstart
8
9
 
9
10
  A complete tool plugin in four parts: register, handle, render, document.
10
11
 
@@ -20,7 +21,8 @@ export default class Ping {
20
21
  core.ensureTool();
21
22
  core.registerScheme({ category: "logging" });
22
23
  core.on("handler", this.handler.bind(this));
23
- core.on("full", this.full.bind(this));
24
+ core.on("visible", this.full.bind(this));
25
+ core.on("summarized", this.summary.bind(this));
24
26
  core.filter("instructions.toolDocs", async (docsMap) => {
25
27
  docsMap.ping = docs;
26
28
  return docsMap;
@@ -32,14 +34,13 @@ export default class Ping {
32
34
  await rummy.set({
33
35
  path: entry.resultPath,
34
36
  body: `pong ${now}`,
35
- status: 200,
37
+ state: "resolved",
36
38
  attributes: { path: entry.path },
37
39
  });
38
40
  }
39
41
 
40
- full(entry) {
41
- return entry.body;
42
- }
42
+ full(entry) { return entry.body; }
43
+ summary(entry) { return ""; }
43
44
  }
44
45
  ```
45
46
 
@@ -62,7 +63,7 @@ Install external plugins via npm + env var:
62
63
  RUMMY_PLUGIN_PING=@myorg/rummy.ping
63
64
  ```
64
65
 
65
- ## §1 Plugin Contract
66
+ ## Plugin Contract {#plugins_contract}
66
67
 
67
68
  A plugin is a directory under `src/plugins/` containing a `.js` file
68
69
  that exports a default class. The class name matches the file name.
@@ -78,8 +79,8 @@ export default class MyTool {
78
79
  core.ensureTool();
79
80
  core.registerScheme({ category: "logging" });
80
81
  core.on("handler", this.handler.bind(this));
81
- core.on("full", this.full.bind(this));
82
- core.on("summary", this.summary.bind(this));
82
+ core.on("visible", this.full.bind(this));
83
+ core.on("summarized", this.summary.bind(this));
83
84
  core.filter("instructions.toolDocs", async (docsMap) => {
84
85
  docsMap.mytool = docs;
85
86
  return docsMap;
@@ -90,13 +91,8 @@ export default class MyTool {
90
91
  // What the tool does (rummy is per-turn RummyContext)
91
92
  }
92
93
 
93
- full(entry) {
94
- return entry.body;
95
- }
96
-
97
- summary(entry) {
98
- return entry.body;
99
- }
94
+ full(entry) { return entry.body; }
95
+ summary(entry) { return entry.body; }
100
96
  }
101
97
  ```
102
98
 
@@ -110,74 +106,106 @@ RUMMY_PLUGIN_WEB=@possumtech/rummy.web
110
106
  RUMMY_PLUGIN_REPO=@possumtech/rummy.repo
111
107
  ```
112
108
 
113
- ## §2 Unified API
109
+ ## Unified API {#plugins_unified_api}
114
110
 
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.
111
+ Three tiers share the tool vocabulary, but the invocation shape and
112
+ dispatch path differ.
118
113
 
119
114
  ```
120
- Model: <rm path="file.txt"/> → { name: "rm", path: "file.txt" }
121
- Client: { method: "rm", params: { path: "file.txt" } }
122
- Plugin: rummy.rm({ path: "file.txt" })
115
+ Model: <rm path="file.txt"/> → { name: "rm", path: "file.txt" }
116
+ TurnExecutor.#record()
117
+ hooks.tools.dispatch("rm", entry, rummy)
118
+ Client: { method: "rm", params: {...} } → rpc.js #dispatchRm(...)
119
+ → Entries.rm({...})
120
+ Plugin: rummy.rm(path) / rummy.set({...}) → Entries.set / Entries.rm
121
+ → (Entries also fires entry events)
123
122
  ```
124
123
 
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>`.
124
+ Three surfaces, one grammar (see [surfaces](SPEC.md#surfaces)). The model dispatches through
125
+ the handler chain (`TurnExecutor.#record()` `hooks.tools.dispatch`
126
+ policy filter → turn-scoped recording → abort cascade → budget
127
+ lifecycle around it). The client primitives (`set`/`get`/`rm`/`cp`/
128
+ `mv`/`update` RPCs) talk directly to Entries — `writer: "client"`
129
+ on every call, permissions enforced per-scheme. Plugins use
130
+ RummyContext verbs; the `rummy.entries` accessor is a Proxy that
131
+ auto-binds `writer: rummy.writer` on every write, so a plugin writing
132
+ on behalf of the model gets `writer: "model"` without opt-in.
133
+
134
+ Plugin code wanting full handler semantics (policy filter, proposal
135
+ flow, turn recording) calls `hooks.tools.dispatch` directly instead
136
+ of going through a primitive.
137
+
138
+ Verb signatures vary. See [plugins_rummy_verbs](#plugins_rummy_verbs).
128
139
 
129
- ## §3 Registration
140
+ ## Registration {#plugins_registration}
130
141
 
131
142
  All registration happens in the constructor via `core.on()`,
132
143
  `core.filter()`, `core.ensureTool()`, and `core.registerScheme()`.
133
144
 
134
- ### §3.1 core.ensureTool()
145
+ ### core.ensureTool() {#plugins_ensure_tool}
135
146
 
136
147
  Declares this plugin as a model-facing tool. Required for the tool
137
148
  to appear in the model's tool list. Called automatically by
138
149
  `core.on("handler", ...)` but must be called explicitly for tools
139
- without handlers (e.g., `summarize`, `update`, `unknown`).
150
+ without handlers (e.g., `update`, `unknown`).
140
151
 
141
- ### §3.2 core.registerScheme(config?)
152
+ ### core.registerScheme(config?) {#plugins_register_scheme}
142
153
 
143
154
  Registers this plugin's scheme in the database. Called once in the
144
155
  constructor.
145
156
 
146
157
  ```js
147
158
  core.registerScheme({
148
- modelVisible: 1, // 1 or 0 — appears in v_model_context
149
- category: "logging", // "data", "logging", "unknown", "prompt"
159
+ name: "mytool", // defaults to plugin name
160
+ modelVisible: 1, // 1 or 0 — appears in v_model_context
161
+ category: "logging", // "data" | "logging" | "unknown" | "prompt"
162
+ scope: "run", // "run" | "project" | "global" — default scope
163
+ writableBy: ["model", "plugin"], // subset of: system | plugin | client | model
150
164
  });
151
165
  ```
152
166
 
153
167
  All fields optional. `core.registerScheme()` with no args gives a
154
- sensible result-type scheme.
168
+ sensible result-type scheme (logging category, run scope, writable by
169
+ model + plugin).
155
170
 
156
- ### §3.3 core.on(event, callback, priority?)
171
+ `scope` determines where entries at this scheme land (see
172
+ [entries](SPEC.md#entries) / [physical_layout](SPEC.md#physical_layout)).
173
+ `writableBy` is enforced at `Entries.set` — writes from a writer
174
+ not in the list throw a typed `PermissionError` (importable from
175
+ `src/agent/errors.js`). The four writer tiers
176
+ (see [writer_tiers](SPEC.md#writer_tiers)) form
177
+ a strict hierarchy: **system > plugin > client > model**. Each tier
178
+ is a superset of what's below.
179
+
180
+ ### core.on(event, callback, priority?) {#plugins_on}
157
181
 
158
182
  | Event | Payload | Purpose |
159
183
  |-------|---------|---------|
160
184
  | `"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 |
185
+ | `"visible"` | `(entry)` | Visible-visibility projection — body shown in `<knowns>` / `<performed>` |
186
+ | `"summarized"` | `(entry)` | Summarized-visibility projection — path + summary only (body hidden) |
187
+ | `"turn.started"` | `({rummy, mode, prompt, loopIteration, isContinuation})` | Turn beginning — plugins write prompt/instructions entries |
188
+ | `"turn.response"` | `({rummy, turn, result, responseMessage, content, commands, ...})` | LLM responded — write audit entries, commit usage |
189
+ | `"proposal.prepare"` | `({rummy, recorded})` | Tool dispatched — materialize proposals (e.g. file edit 202 revisions) |
190
+ | `"proposal.pending"` | `({projectId, run, proposed})` | Proposal awaits client resolution |
191
+ | `"turn.completed"` | `(turnResult)` | Turn resolved full turnResult |
192
+ | `"entry.created"` | `(entry)` | Entry created during dispatch |
193
+ | `"entry.changed"` | `({runId, path, changeType})` | Entry content, visibility, or status modified |
194
+ | `"run.state"` | `({projectId, run, turn, status, summary, history, unknowns, telemetry})` | Incremental client-facing state push (wire-layer `status` HTTP code stays; DB stores the 5-value state enum) |
195
+ | `"error.log"` | `({runId, turn, loopId, message})` | Runtime error — creates an `error://` entry |
196
+ | Any `"dotted.name"` | varies | Resolves to the matching hook in `src/hooks/Hooks.js` |
169
197
 
170
198
  ```js
171
199
  // One-liner examples
172
200
  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 */ });
201
+ core.on("visible", (entry) => entry.body);
202
+ core.on("summarized", (entry) => entry.attributes?.summary || "");
203
+ core.on("turn.started", async ({ rummy, mode }) => { /* write entries */ });
204
+ core.on("turn.response", async ({ rummy, result }) => { /* audit */ });
177
205
  core.on("entry.changed", ({ runId, path, changeType }) => { /* react */ });
178
206
  ```
179
207
 
180
- ### §3.4 core.filter(name, callback, priority?)
208
+ ### core.filter(name, callback, priority?) {#plugins_filter}
181
209
 
182
210
  | Filter | Signature | Purpose |
183
211
  |--------|-----------|---------|
@@ -186,6 +214,7 @@ core.on("entry.changed", ({ runId, path, changeType }) => { /* react */ });
186
214
  | `"assembly.user"` | `(content, ctx) → content` | Contribute to user message |
187
215
  | `"llm.messages"` | `(messages) → messages` | Transform final messages before LLM call |
188
216
  | `"llm.response"` | `(response) → response` | Transform LLM response |
217
+ | `"llm.reasoning"` | `(reasoning, {commands}) → reasoning` | Contribute to `reasoning_content` (the think plugin subscribes here to merge `<think>` tag bodies) |
189
218
  | Any `"dotted.name"` | varies | Resolves to the matching filter in the hook tree |
190
219
 
191
220
  ```js
@@ -209,13 +238,15 @@ ctx = {
209
238
  rows, // turn_context rows (materialized entries)
210
239
  loopStartTurn, // First turn of current loop
211
240
  type, // "ask" or "act"
212
- tools, // Set of active tool names
241
+ toolSet, // Set<string> of active tool names for this loop
213
242
  contextSize, // Model context window size
214
- lastContextTokens, // Assembled tokens from previous turn
243
+ lastContextTokens, // Actual API tokens from the prior turn (0 on turn 1)
244
+ demoted, // Mutable array — plugins push paths they summarized
245
+ turn, // Current turn number
215
246
  }
216
247
  ```
217
248
 
218
- ### §3.5 Tool Docs
249
+ ### Tool Docs {#plugins_tool_docs}
219
250
 
220
251
  Each tool plugin has a `*Doc.js` file with annotated line arrays.
221
252
  Text goes to the model. Rationale stays in source. Registered via
@@ -233,7 +264,7 @@ core.filter("instructions.toolDocs", async (docsMap) => {
233
264
  The instructions plugin filters by the active tool set — tools
234
265
  excluded by mode or flags are automatically omitted from the docs.
235
266
 
236
- ### §3.6 handler(entry, rummy)
267
+ ### handler(entry, rummy) {#plugins_handler}
237
268
 
238
269
  The handler receives the parsed command entry and a per-turn
239
270
  RummyContext:
@@ -251,17 +282,17 @@ entry = {
251
282
  Multiple handlers per scheme. Lower priority runs first. Return
252
283
  `false` to stop the chain.
253
284
 
254
- ### §3.7 full(entry) / summary(entry)
285
+ ### full(entry) / summary(entry) {#plugins_views}
255
286
 
256
287
  Returns the string the model sees for this tool's entries at the
257
- given fidelity. Every tool MUST register `full`. `summary` is
288
+ given visibility. Every tool MUST register `full`. `summary` is
258
289
  optional — if unregistered, falls back to `attributes.summary`
259
290
  (model-authored keyword description) or empty string.
260
291
 
261
- At summary fidelity, `attributes.summary` is prepended above the
292
+ At summary visibility, `attributes.summary` is prepended above the
262
293
  plugin's summary output automatically by ToolRegistry.view().
263
294
 
264
- ## §4 Two Objects
295
+ ## Two Objects {#plugins_two_objects}
265
296
 
266
297
  Plugins interact with two objects at different scopes:
267
298
 
@@ -273,59 +304,76 @@ lifetime.
273
304
  **RummyContext** (`rummy`) — turn-scoped. Passed to handlers per
274
305
  invocation. Has tool verbs, per-turn state, database access.
275
306
 
276
- ### §4.1 Tool Verbs (on RummyContext)
307
+ ### Tool Verbs (on RummyContext) {#plugins_rummy_verbs}
308
+
309
+ Convenience wrappers that bind `runId`, `turn`, `loopId` from context
310
+ and delegate to Entries. Signatures vary per verb. For full
311
+ handler-chain semantics (policy filtering, proposal flow, abort
312
+ cascade), call `rummy.hooks.tools.dispatch(scheme, entry, rummy)`
313
+ instead.
277
314
 
278
315
  | Method | Effect |
279
316
  |--------|--------|
280
- | `rummy.set({ path, body, status, fidelity, attributes })` | Create/update entry |
281
- | `rummy.get({ path })` | Promote to full fidelity |
282
- | `rummy.rm({ path })` | Delete permanently |
283
- | `rummy.mv({ path, to })` | Move entry |
284
- | `rummy.cp({ path, to })` | Copy entry |
317
+ | `rummy.set({ path?, body?, state?, visibility?, outcome?, attributes? })` | Create/update entry. If `path` omitted, slugifies from body/summary. State defaults to `"resolved"`. |
318
+ | `rummy.get(path)` | Promote entries matching a pattern (default visibility `"visible"`). |
319
+ | `rummy.rm(path)` | Remove entry's view. |
320
+ | `rummy.mv(from, to)` | Rename entry. |
321
+ | `rummy.cp(from, to)` | Copy entry to a new path. |
322
+ | `rummy.update(body, { status?, attributes? })` | Write the once-per-turn lifecycle signal to `update://<slug>`. |
285
323
 
286
- ### §4.2 Query Methods
324
+ ### Query Methods {#plugins_rummy_queries}
287
325
 
288
326
  | Method | Returns |
289
327
  |--------|---------|
290
328
  | `rummy.getBody(path)` | Body text or null |
291
- | `rummy.getState(path)` | Status code or null |
292
- | `rummy.getAttributes(path)` | Parsed attributes `{}` |
293
- | `rummy.getEntries(pattern, body?)` | Array of matching entries |
329
+ | `rummy.getState(path)` | Categorical state (`"proposed"` \| `"streaming"` \| `"resolved"` \| `"failed"` \| `"cancelled"`) or null |
330
+ | `rummy.getOutcome(path)` | Outcome string (populated when state ∈ {failed, cancelled}) or null |
331
+ | `rummy.getAttributes(path)` | Parsed attributes `{}` or null |
332
+ | `rummy.getEntry(path)` | First matching entry or null |
333
+ | `rummy.getEntries(pattern, bodyFilter?)` | Array of matching entries |
334
+ | `rummy.setAttributes(path, attrs)` | Merge attributes via json_patch |
294
335
 
295
- ### §4.3 Properties
336
+ ### Properties {#plugins_rummy_properties}
296
337
 
297
- | Property | Type | Scope |
338
+ | Property | Type | Notes |
298
339
  |----------|------|-------|
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
340
+ | `rummy.entries` | Entries proxy | Write calls auto-carry `writer: rummy.writer`. Read-through for reads + internal ops. |
341
+ | `rummy.db` | SqlRite db | Prefer `entries` for plugin-facing data access |
342
+ | `rummy.hooks` | Hook registry | |
343
+ | `rummy.runId` | number | Current run |
344
+ | `rummy.projectId` | number | |
345
+ | `rummy.sequence` | number | Current turn number |
346
+ | `rummy.loopId` / `rummy.turnId` | number | |
347
+ | `rummy.type` | `"ask"` \| `"act"` | Current mode |
348
+ | `rummy.toolSet` | Set<string> \| null | Active tool list for this loop |
349
+ | `rummy.contextSize` | number \| null | Model context window |
350
+ | `rummy.systemPrompt` / `rummy.loopPrompt` | string | |
351
+ | `rummy.noRepo` / `rummy.noInteraction` / `rummy.noWeb` | boolean | Loop flags |
352
+ | `rummy.writer` | `"system"` \| `"plugin"` \| `"client"` \| `"model"` | Default `"model"` in handler dispatch. The Proxy on `rummy.entries` binds this to every write for permission checks (see [writer_tiers](SPEC.md#writer_tiers)). |
353
+
354
+ ## Tool Display Order {#plugins_display_order}
308
355
 
309
356
  Tools are presented to the model in priority order:
310
357
  gather → reason → act → communicate.
311
358
 
312
- Defined in `ToolRegistry.TOOL_ORDER`. The `resolveForLoop(mode, flags)`
313
- method handles all exclusions through one mechanism:
359
+ Defined in `ToolRegistry.TOOL_ORDER`. `resolveForLoop(mode, flags)`
360
+ handles all exclusions:
314
361
 
315
- | Flag | Excludes |
316
- |------|----------|
362
+ | Condition | Excludes |
363
+ |-----------|----------|
317
364
  | `mode === "ask"` | `sh` |
318
- | `noInteraction` | `ask_user` |
319
- | `noWeb` | `search` |
365
+ | `noInteraction` flag | `ask_user` |
366
+ | `noWeb` flag | `search` |
367
+ | `noProposals` flag | `ask_user`, `env`, `sh` |
320
368
 
321
- ## §6 Hedberg
369
+ ## Hedberg {#plugins_hedberg}
322
370
 
323
371
  The hedberg plugin exposes pattern matching and interpretation
324
372
  utilities on `core.hooks.hedberg` for all plugins to use:
325
373
 
326
374
  ```js
327
375
  const { match, search, replace, parseSed, parseEdits,
328
- normalizeAttrs, generatePatch } = core.hooks.hedberg;
376
+ generatePatch } = core.hooks.hedberg;
329
377
  ```
330
378
 
331
379
  | Method | Purpose |
@@ -335,23 +383,22 @@ const { match, search, replace, parseSed, parseEdits,
335
383
  | `replace(body, search, replacement, opts?)` | Apply replacement |
336
384
  | `parseSed(input)` | Parse sed syntax (any delimiter) |
337
385
  | `parseEdits(content)` | Detect edit format (merge conflict, udiff, sed) |
338
- | `normalizeAttrs(attrs)` | Heal model attribute names |
339
386
  | `generatePatch(path, old, new)` | Generate unified diff |
340
387
 
341
- ## §7 Events & Filters
388
+ ## Events & Filters {#plugins_events_overview}
342
389
 
343
390
  **Events** are fire-and-forget. All handlers run. Return values ignored.
344
391
  **Filters** transform data through a chain. Lower priority runs first.
345
392
  All hooks are async.
346
393
 
347
- ### §7.1 Project Lifecycle
394
+ ### Project Lifecycle {#plugins_project_lifecycle}
348
395
 
349
396
  | Hook | Type | When |
350
397
  |------|------|------|
351
398
  | `project.init.started` | event | Before project DB upsert |
352
399
  | `project.init.completed` | event | After project created |
353
400
 
354
- ### §7.2 Run & Loop Lifecycle
401
+ ### Run & Loop Lifecycle {#plugins_run_loop_lifecycle}
355
402
 
356
403
  | Hook | Type | When |
357
404
  |------|------|------|
@@ -360,129 +407,224 @@ All hooks are async.
360
407
  | `act.started` | event | Run requested in act mode |
361
408
  | `loop.started` | event | Loop execution beginning |
362
409
  | `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 |
410
+ | `run.progress` | event | Transient turn activity (`thinking` / `processing` / `retrying`) |
411
+ | `run.state` | event | Turn conclusion, per-command incremental, or terminal run close — full state snapshot (status, history, unknowns, telemetry) |
412
+ | `run.step.completed` | event | Turn verdict resolved (post-healer, pre-close) |
413
+ | `loop.completed` | event | Loop exit fires from `finally`, guaranteed on every exit path |
414
+ | `ask.completed` | event | Ask-mode run finished |
415
+ | `act.completed` | event | Act-mode run finished |
416
+ | `proposal.prepare` | event | Per recorded entry — plugins materialize proposals (e.g. set plugin turns search/replace revisions into 202 entries) |
417
+ | `proposal.pending` | event | A materialized proposal awaits client resolution |
369
418
 
370
- ### §7.3 Turn Pipeline
419
+ ### Turn Pipeline {#plugins_turn_pipeline}
371
420
 
372
421
  Hooks fire in this order every turn:
373
422
 
374
423
  | # | Hook | Type | When |
375
424
  |---|------|------|------|
376
- | 1 | `turn.started` | event | Plugins write prompt/progress/instructions entries |
425
+ | 1 | `turn.started` | event | Plugins write prompt/instructions entries |
377
426
  | 2 | `context.materialized` | event | turn_context populated from v_model_context |
378
427
  | 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 |
428
+ | 4 | `assembly.user` | filter | Build user message (prompt plugin adds `<prompt tokensFree tokenUsage>`) |
429
+ | 5 | `budget.enforce` | call | Measure assembled tokens; if over and it's turn 1, demote prompt, re-materialize, re-check; still over → 413 |
381
430
  | 6 | `llm.messages` | filter | Transform messages before LLM call |
382
431
  | 7 | `llm.request.started` | event | LLM call about to fire |
383
432
  | 8 | `llm.response` | filter | Transform raw LLM response |
384
433
  | 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
434
+ | 10 | `turn.response` | event | Plugins write audit entries (telemetry) |
435
+ | 11 | `entry.recording` | filter | Per command, during `#record()`. Returning an entry with `state: "failed"` (or `"cancelled"`) rejects it. |
436
+ | 12 | Per recorded entry (sequential, abort-on-failure): | | |
437
+ | | `tool.before` | event | Before handler dispatch |
438
+ | | `tools.dispatch` | | Scheme's registered handler runs |
439
+ | | `tool.after` | event | Handler finished |
440
+ | | `entry.created` | event | Entry written to store |
441
+ | | `run.state` | event | Incremental state push to connected clients |
442
+ | | `proposal.prepare` | event | This entry's dispatch may have created proposals (e.g. set → 202 revisions) |
443
+ | | `proposal.pending` | event | Per each materialized proposal — client is notified, dispatch awaits resolution |
444
+ | 13 | `budget.postDispatch` | call | Re-materialize + check. If over ceiling → Turn Demotion (visibility=summarized on turn's visible rows) + emit 413 error. |
445
+ | 14 | `hooks.update.resolve` | call | Update plugin classifies this turn's `<update>` (terminal/continuation, override-to-continuation if actions failed, heal from raw content if missing) |
446
+ | 15 | `turn.completed` | event | Turn fully resolved with final status |
447
+
448
+ `entry.changed` fires asynchronously from mutation points — not
449
+ pipeline-ordered. Subscribe when you need to react to any entry
450
+ modification (used by budget remeasurement and file-on-disk detection).
451
+
452
+ ### Entry Events {#plugins_entry_events}
396
453
 
397
454
  | Hook | Type | When |
398
455
  |------|------|------|
399
- | `entry.recording` | filter | Before entry stored. Return `{ status: 4xx }` to reject. |
456
+ | `entry.recording` | filter | Before entry stored. Return `{ state: "failed", outcome }` to reject. |
400
457
  | `entry.created` | event | New entry added during dispatch |
401
- | `entry.changed` | event | Entry content, fidelity, or status modified |
458
+ | `entry.changed` | event | Entry content, visibility, or state modified |
402
459
 
403
460
  `entry.recording` is a filter — plugins can validate, transform, or
404
461
  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.
462
+ `{ scheme, path, body, attributes, state, outcome }`. Second arg is
463
+ a context bag: `{ store, runId, turn, loopId, mode }`. Return the
464
+ entry object (modified or not). Set `state: "failed"` with an
465
+ `outcome` string (e.g. `"permission"`, `"validation"`) to reject —
466
+ the policy plugin uses this pattern for ask-mode rejections.
407
467
 
408
468
  `entry.changed` fires on any mutation to an existing entry — body
409
- update, fidelity change, status change, attribute update. Payload:
469
+ update, visibility change, state change, attribute update. Payload:
410
470
  `{ runId, path, changeType }`. Subscribers include the budget plugin
411
471
  (remeasure context) and the repo plugin (detect file changes on disk).
412
472
 
413
- ### §7.5 Budget
473
+ ### Budget {#plugins_budget}
414
474
 
415
475
  | Hook | Type | When |
416
476
  |------|------|------|
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
477
+ | `hooks.budget.enforce` | method | Pre-LLM ceiling check. On first-turn 413 Prompt Demotion + re-check. |
478
+ | `hooks.budget.postDispatch` | method | Post-dispatch re-check. On 413 → Turn Demotion + 413 `error://` entry via `hooks.error.log.emit`. |
479
+
480
+ The budget plugin measures tokens on the assembled messages the
481
+ actual content being sent to the LLM. No estimates at the ceiling,
482
+ no SQL token sums. The assembled message IS the measurement. When
483
+ turn 2+ information is available, `budget.enforce` prefers the actual
484
+ API-reported token count (`turns.context_tokens` from the prior
485
+ turn) over re-measuring the assembled string.
486
+
487
+ **DB tokens vs assembled tokens:** The `tokens` column on `entries`
488
+ is strictly for DISPLAY — showing token costs in `<knowns>` tags so
425
489
  the model can reason about entry sizes. It is NEVER used for budget
426
490
  decisions. Budget math uses only assembled message token counts.
427
- These are two separate numbers that must never be conflated.
491
+ These are two separate numbers that must never be conflated. See
492
+ See [budget_enforcement](SPEC.md#budget_enforcement) for the three-measure table.
428
493
 
429
- ### §7.6 Client Notifications
494
+ ### Client Notifications {#plugins_client_notifications}
430
495
 
431
496
  | Hook | Type | When |
432
497
  |------|------|------|
433
498
  | `ui.render` | event | Text for client display |
434
499
  | `ui.notify` | event | Status notification |
435
500
 
436
- ## §8 Entry Lifecycle
501
+ ## Entry Lifecycle {#plugins_entry_lifecycle}
437
502
 
438
503
  Every entry follows the same lifecycle regardless of origin:
439
504
 
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
505
+ 1. **Created** — `entries` row (content) + `run_views` row (per-run
506
+ projection) via the two-prep upsert flow (see [physical_layout](SPEC.md#physical_layout)).
507
+ 2. **Dispatched** — tool handler chain executes.
508
+ 3. **State set** — handler sets `state` (`"proposed"` \| `"streaming"`
509
+ \| `"resolved"` \| `"failed"` \| `"cancelled"`) + optional
510
+ `outcome` string on the `run_views` row. State is view-side; body
511
+ is content-side. (See [entries](SPEC.md#entries).)
512
+ 4. **Materialized** — `v_model_context` joins entries + run_views,
513
+ projects into `turn_context`.
514
+ 5. **Assembled** — filter chain renders into system/user messages.
515
+ Model-facing tags carry `status="NNN"` (HTTP code) via
516
+ `src/agent/httpStatus.js`'s state-to-HTTP mapping — the model's
517
+ vocabulary is HTTP; the DB is categorical.
518
+ 6. **Visible** — model sees the entry in its context.
519
+
520
+ Entries at `visibility = 'archived'` skip steps 4–6 (invisible to
521
+ model, discoverable via pattern search). Entries at `visibility =
522
+ 'summarized'` render with `attributes.summary` (model-authored keyword
523
+ description) prepended above the plugin's `summarized` view output —
524
+ the body is hidden; promoting with `<get>` brings it back.
525
+
526
+ **Per-plugin visibility projection reference.** Each plugin chooses
527
+ what its `visible` / `summarized` view hooks return. Renderers trust
528
+ the projected body — they do NOT re-check `entry.visibility`.
529
+
530
+ | Plugin | Category | `visible` body | `summarized` body | Notes |
531
+ |--------|----------|-----------------|----------------|-------|
532
+ | `known` | data | `entry.body` | `""` | Tag's `summary` attr carries the keywords at summarized visibility |
533
+ | `unknown` | unknown | `entry.body` | `""` | Same pattern as known |
534
+ | `prompt` | prompt | `entry.body` | 500-char truncation with `[truncated — promote to see the complete prompt]` marker | |
535
+ | `budget` | logging | `entry.body` | `entry.body` | Feedback signal — kept visible |
536
+ | `update` | logging | `# update\n${entry.body}` | same as visible | Already 80-char capped by tool doc rule |
537
+ | `get` / `set` / `rm` / `cp` / `mv` / `sh` / `env` / `search` | logging | result body | `""` | Just the self-closing tag at summarized |
538
+ | `skill` | data | `entry.body` | `""` | Same as known |
539
+ | `file` (bare paths) | data | `entry.body` | `""` | Same as known |
540
+
541
+ Plugins providing only a `visible` hook fall back to
542
+ `attributes.summary` (model-authored keyword description) at summarized;
543
+ the renderer inserts it automatically. Plugins providing neither
544
+ default to empty body — the tag still renders with its attributes so
545
+ the model can pattern-match the path.
546
+
547
+ ### Streaming Entries {#plugins_streaming_entries}
548
+
549
+ Producers whose output arrives over time (shell commands, web fetches,
550
+ log tails, file watches) use the **streaming entry pattern**. The
551
+ lifecycle extends beyond 202→200:
446
552
 
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.
553
+ ```
554
+ state: "proposed" (user decision pending)
555
+ accept state: "resolved" (log entry: action happened)
556
+ + state: "streaming" data entries (one per channel, growing)
557
+ → "resolved" / "failed" on completion
558
+ ```
451
559
 
452
- ## §9 Bundled Plugins
560
+ **Producer plugin contract:**
561
+
562
+ 1. On dispatch, create a **proposal entry** at `{scheme}://turn_N/{slug}`
563
+ with `state: "proposed"`, category=logging. Body empty;
564
+ `summary=command` attr.
565
+ 2. On user accept (client sends `set { state: "resolved" }` on the
566
+ proposal path), `AgentLoop.resolve()` transitions the proposal
567
+ entry to `state: "resolved"` (it becomes the **log entry**) and
568
+ creates **data entries** at `{path}_1`, `{path}_2`, etc. with
569
+ `state: "streaming"`, category=data, visibility=summarized, empty body.
570
+ 3. Producer/client calls `stream { run, path, channel, chunk }` RPC
571
+ to append chunks to the appropriate channel.
572
+ 4. When the producer is done, `stream/completed { run, path, exit_code? }`
573
+ transitions all `{path}_*` data entries to a terminal state
574
+ (`"resolved"` on exit_code=0 or omitted; `"failed"` with outcome
575
+ `"exit:N"` otherwise) and rewrites the log entry body with final
576
+ stats. For client-initiated cancellation, the client calls
577
+ `stream/aborted { run, path, reason? }` instead — transitions
578
+ channels to `state: "cancelled"` with outcome=reason.
579
+
580
+ **Channel numbering:** Unix file descriptor convention — `_1` is the
581
+ primary stream (stdout for shell, body for fetch, lines for tail);
582
+ `_2` is alternate/error (stderr, redirects, anomalies); `_3`+ for
583
+ additional producer-specific streams.
584
+
585
+ **The `stream` plugin** owns the RPC infrastructure. Producer plugins
586
+ only need to:
587
+ - Create the proposal entry on dispatch (status=202)
588
+ - Rely on `AgentLoop.resolve()` to create data channels on accept
589
+ - Let clients/external producers call `stream`, `stream/completed`,
590
+ and `stream/aborted`
591
+
592
+ No scheme registration or tooldoc for the stream plugin itself — it's
593
+ pure RPC plumbing shared across all streaming producers.
594
+
595
+ ## Bundled Plugins
453
596
 
454
597
  | Plugin | Type | Description |
455
598
  |--------|------|-------------|
456
599
  | `get` | Core tool | Load file/entry into context |
457
- | `set` | Core tool | Edit file/entry, fidelity control |
600
+ | `set` | Core tool | Edit file/entry, visibility control |
458
601
  | `known` | Core tool + Assembly | Save knowledge, render `<knowns>` section |
459
602
  | `rm` | Core tool | Delete permanently |
460
603
  | `mv` | Core tool | Move entry |
461
604
  | `cp` | Core tool | Copy entry |
462
- | `sh` | Core tool | Shell command (act mode only) |
463
- | `env` | Core tool | Exploratory command |
605
+ | `sh` | Core tool | Shell command (act mode only). Streaming producer — see [plugins_streaming_entries](#plugins_streaming_entries) |
606
+ | `env` | Core tool | Exploratory command. Streaming producer — see §8.1 |
607
+ | `stream` | Internal | Generic streaming-entry RPC (`stream`, `stream/completed`, `stream/aborted`, `stream/cancel`) for sh/env and future producers |
464
608
  | `ask_user` | Core tool | Ask the user |
465
609
  | `search` | Core tool | Web search (via external plugin) |
466
- | `summarize` | Structural | Signal completion |
467
- | `update` | Structural | Signal continued work |
610
+ | `update` | Structural | Status report + lifecycle signal. `status="200\|204\|422"` terminates; `status="102"` continues. Exposes `hooks.update.resolve` for TurnExecutor. |
468
611
  | `unknown` | Structural + Assembly | Register unknowns, render `<unknowns>` |
469
612
  | `previous` | Assembly | Render `<previous>` loop history |
470
613
  | `performed` | Assembly | Render `<performed>` active loop work |
471
- | `progress` | Assembly | Render `<progress>` telemetry + warnings |
472
- | `prompt` | Assembly | Render `<prompt mode="ask|act">` tag |
614
+ | `prompt` | Assembly | Render `<prompt mode="ask\|act" tokensFree="N" tokenUsage="M">` tag |
473
615
  | `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 |
616
+ | `instructions` | Internal | Preamble + tool docs + persona assembly; exposes `hooks.instructions.resolveSystemPrompt` |
617
+ | `file` | Internal | File entry projections and constraints (`scheme IS NULL`) |
618
+ | `rpc` | Internal | RPC method registration + tool-fallback dispatch |
477
619
  | `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).
620
+ | `budget` | Internal | Context ceiling enforcement: Prompt Demotion (pre-LLM first-turn 413) + Turn Demotion (post-dispatch). Exposes `hooks.budget.enforce` / `hooks.budget.postDispatch`. |
621
+ | `policy` | Internal | Ask-mode per-invocation rejections via `entry.recording` filter |
622
+ | `error` | Internal | `error.log` hook `error://` entries |
623
+ | `think` | Tool | Private reasoning tag; contributes to `reasoning_content` via the `llm.reasoning` filter |
624
+ | `openai` / `ollama` / `xai` / `openrouter` | LLM provider | Register with `hooks.llm.providers`; handle `{prefix}/...` model aliases. Silently inert if their env isn't configured. |
625
+ | `persona` / `skill` | Internal | Runtime persona/skill management via RPC |
484
626
 
485
- ## §10 External Plugins
627
+ ## External Plugins
486
628
 
487
629
  | Plugin | Package | Description |
488
630
  |--------|---------|-------------|
@@ -492,68 +634,121 @@ Removed: `crunch` (dead code, replaced by model-owned context management),
492
634
  Loaded via `RUMMY_PLUGIN_*` env vars. External plugins have access
493
635
  to the same PluginContext API as bundled plugins.
494
636
 
495
- ## §11 RPC Methods
637
+ ## RPC Methods {#plugins_rpc}
496
638
 
497
- Client-facing JSON-RPC 2.0 over WebSocket. All tool methods go through
498
- the same handler chain as model commands.
639
+ Client-facing JSON-RPC 2.0 over WebSocket. Protocol version **2.0.0**.
640
+ The client surface is a thin projection of the plugin API (SPEC §0.3):
641
+ the six primitives match the plugin's `rummy.set` / `rummy.get` / etc.
642
+ exactly, plus a connection handshake and a few config verbs.
499
643
 
500
- ### §11.1 Wire Format
644
+ ### Wire Format {#plugins_rpc_wire_format}
501
645
 
502
646
  ```json
503
647
  // Request
504
- { "jsonrpc": "2.0", "id": 1, "method": "get", "params": { "path": "src/app.js", "run": "my_run" } }
648
+ { "jsonrpc": "2.0", "id": 1, "method": "set", "params": { "run": "my_run", "path": "known://fact", "body": "...", "state": "resolved" } }
505
649
 
506
650
  // Success response
507
- { "jsonrpc": "2.0", "id": 1, "result": { "path": "src/app.js", "status": 200 } }
651
+ { "jsonrpc": "2.0", "id": 1, "result": { "ok": true } }
508
652
 
509
653
  // Error response
510
- { "jsonrpc": "2.0", "id": 1, "error": { "code": -32600, "message": "Missing required param: path" } }
654
+ { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "set: path is required" } }
511
655
 
512
656
  // Notification (server → client, no id)
513
- { "jsonrpc": "2.0", "method": "run/state", "params": { "run": "my_run", "status": 200 } }
657
+ { "jsonrpc": "2.0", "method": "run/state", "params": { "run": "my_run", "turn": 3, "status": 200, ... } }
514
658
  ```
515
659
 
516
- ### §11.2 Tool Methods (Unified API)
660
+ ### Connection Handshake {#plugins_rpc_handshake}
661
+
662
+ First call every client makes. Establishes project identity and
663
+ enforces protocol-version compatibility.
517
664
 
518
665
  | Method | Params | Notes |
519
666
  |--------|--------|-------|
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 |
667
+ | `rummy/hello` | `{ name, projectRoot, configPath?, clientVersion? }` | Returns `{ rummyVersion, projectId, projectRoot }`. Server rejects MAJOR mismatch with a protocol-mismatch error. |
527
668
 
528
- ### §11.3 Run Management
669
+ ### Primitives (see [primitives](SPEC.md#primitives)) {#plugins_rpc_primitives}
670
+
671
+ Six verbs. Object-args matching the entry grammar. Writer is fixed to
672
+ `"client"` server-side; permissions enforced per-scheme via the
673
+ scheme's `writable_by`.
529
674
 
530
675
  | Method | Params | Notes |
531
676
  |--------|--------|-------|
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 |
677
+ | `set` | `{ run, path, body?, state?, visibility?, outcome?, attributes?, append?, pattern?, bodyFilter? }` | Wide semantic: write content, change visibility/state, merge attributes, append (streaming), pattern update. Writing to `run://<alias>` starts or cancels a run (see §11.4). State transitions on proposed entries route through `AgentLoop.resolve()` for scheme-specific side effects. |
678
+ | `get` | `{ run, path, bodyFilter?, visibility? }` | Promote an entry (or pattern) to visible visibility. |
679
+ | `rm` | `{ run, path, bodyFilter? }` | Remove entry's view. |
680
+ | `cp` | `{ run, from, to, visibility? }` | Copy entry to new path. |
681
+ | `mv` | `{ run, from, to, visibility? }` | Rename entry. |
682
+ | `update` | `{ run, body, status?, attributes? }` | Write the once-per-turn lifecycle signal to `update://<slug>`. |
683
+
684
+ ### Run Lifecycle via Primitives {#plugins_rpc_run_lifecycle}
685
+
686
+ Runs are addressable as `run://<alias>` entries (SPEC §0.5). The
687
+ client manipulates run lifecycle via ordinary `set` calls:
540
688
 
541
- ### §11.4 Project Management
689
+ | Action | Call |
690
+ |--------|------|
691
+ | Start a run (named) | `set { path: "run://<alias>", body: <prompt>, attributes: { model, mode?, persona?, temperature?, contextLimit?, noRepo?, noInteraction?, noWeb?, noProposals? } }` |
692
+ | Start a run (anonymous) | `set { path: "run://", body: <prompt>, attributes: { model, ... } }` — server synthesizes alias as `${model}_${unixEpochMs}` and returns it in the response |
693
+ | Cancel a run | `set { path: "run://<alias>", state: "cancelled" }` |
694
+ | Inject continuation | `set { path: "run://<alias>", body: <message> }` on an existing run |
695
+ | Accept a proposal | `set { run, path: "<entry>", state: "resolved", body?: <output> }` |
696
+ | Reject a proposal | `set { run, path: "<entry>", state: "cancelled", body?: <reason> }` |
697
+
698
+ Starting a new run is fire-and-forget: server returns `{ ok: true, alias }`
699
+ immediately; client watches the run's state transitions via the
700
+ `run/state` notification (and the `run://` entry itself).
701
+
702
+ ### Config & Query Methods {#plugins_rpc_queries}
703
+
704
+ Not every server capability fits the entry grammar. These are
705
+ dedicated verbs with 1:1 plugin-API equivalents.
542
706
 
543
707
  | Method | Params | Notes |
544
708
  |--------|--------|-------|
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 |
709
+ | `ping` | | Liveness check |
710
+ | `discover` | | Return the live RPC catalog |
711
+ | `getModels` / `addModel` / `removeModel` | (see rpc.js) | Model aliases |
712
+ | `getRuns` / `getRun` | `{ limit?, offset? }` / `{ run }` | Run listing and detail |
713
+ | `getEntries` | `{ run, pattern?, scheme?, state?, visibility?, bodyFilter? }` | Read-only entry query. Returns `[{path, scheme, state, visibility, attributes, turn, tokens}]`. No promotion side-effect. Pair with `get` primitive (which is a write verb). |
714
+ | `file/constraint` | `{ pattern, visibility }` | Project-scoped: set overlay. `visibility ∈ {active, readonly, ignore}`. Patterns can be globs. `readonly` is enforced on `set://` accept in `AgentLoop.resolve()`. |
715
+ | `file/drop` | `{ pattern }` | Project-scoped: remove overlay row. |
716
+ | `getConstraints` | — | Project-scoped: returns `[{pattern, visibility}]`. |
717
+ | `skill/add` / `skill/remove` / `getSkills` / `listSkills` | | Skill management |
718
+ | `persona/set` / `listPersonas` | | Persona management |
719
+ | `stream` / `stream/completed` / `stream/aborted` / `stream/cancel` | | Streaming RPC (§8.1) |
720
+
721
+ **Why file constraints are typed RPCs and not `set` entries:** they
722
+ are project-scoped (no `run`), persist across runs, and `readonly`
723
+ requires enforcement server-side on `set://` accept. Every `set`
724
+ primitive call requires a run alias; constraints don't have one. The
725
+ typed verbs match the capability's actual shape rather than contorting
726
+ the grammar.
727
+
728
+ ### Notifications (server → client) {#plugins_rpc_notifications}
551
729
 
552
- ### §11.5 Notifications (server → client)
553
-
554
- | Method | Payload |
730
+ | Method | Purpose |
555
731
  |--------|---------|
556
- | `run/state` | `{ run, status, turn, entries, ... }` |
557
- | `run/progress` | `{ run, status }` |
558
- | `ui/render` | `{ text }` |
559
- | `ui/notify` | `{ message }` |
732
+ | `run/state` | Incremental state push per tool dispatch |
733
+ | `run/proposal` | A proposed entry awaits client resolution |
734
+ | `stream/cancelled` | Server-initiated streaming cancellation |
735
+ | `ui/render` | Streaming UI output |
736
+ | `ui/notify` | Toast notification |
737
+
738
+ ### Retired Methods (2.0.0)
739
+
740
+ Protocol 1.x shipped many methods that collapsed into the primitive
741
+ grammar. Clients migrating from 1.x need to replace the following:
742
+
743
+ | 1.x method | Replacement |
744
+ |------------|-------------|
745
+ | `init` | `rummy/hello` |
746
+ | `ask` / `act` / `startRun` | `set { path: "run://<alias>", body: <prompt>, attributes: { model, mode, ... } }` |
747
+ | `run/resolve` | `set { run, path, state, body? }` |
748
+ | `run/abort` / `run/cancel` | `set { path: "run://<alias>", state: "cancelled" }` |
749
+ | `run/rename` | `mv { run, from: "run://<old>", to: "run://<new>" }` |
750
+ | `run/inject` | `set { path: "run://<alias>", body: <message> }` on an existing run |
751
+ | `run/config` | `set { path: "run://<alias>", attributes: { ... } }` |
752
+ | `store` (demote) | `set { run, path, visibility: "summarized", pattern: true }` |
753
+ | `getEntries` | Kept as §11.5 typed helper — now filter-capable (scheme/state/visibility). Pairs with the `get` write primitive. |
754
+ | `get { persist }` / `store { persist, clear, ignore }` (file constraints) | `file/constraint { pattern, visibility }` and `file/drop { pattern }`. Project-scoped helpers in §11.5 with real server enforcement for `readonly`. |