@possumtech/rummy 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.env.example +11 -1
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +284 -93
  5. package/migrations/001_initial_schema.sql +6 -4
  6. package/package.json +13 -5
  7. package/src/agent/AgentLoop.js +166 -15
  8. package/src/agent/ContextAssembler.js +18 -4
  9. package/src/agent/KnownStore.js +127 -13
  10. package/src/agent/ProjectAgent.js +4 -1
  11. package/src/agent/ResponseHealer.js +21 -1
  12. package/src/agent/TurnExecutor.js +365 -175
  13. package/src/agent/XmlParser.js +72 -39
  14. package/src/agent/known_store.sql +20 -4
  15. package/src/agent/schemes.sql +3 -0
  16. package/src/agent/tokens.js +6 -21
  17. package/src/agent/turns.sql +10 -1
  18. package/src/hooks/Hooks.js +18 -0
  19. package/src/hooks/PluginContext.js +14 -1
  20. package/src/hooks/RummyContext.js +16 -4
  21. package/src/hooks/ToolRegistry.js +83 -19
  22. package/src/llm/LlmProvider.js +27 -8
  23. package/src/llm/OpenAiClient.js +20 -0
  24. package/src/llm/OpenRouterClient.js +24 -2
  25. package/src/llm/XaiClient.js +47 -2
  26. package/src/plugins/ask_user/README.md +4 -4
  27. package/src/plugins/ask_user/ask_user.js +5 -5
  28. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  29. package/src/plugins/budget/BudgetGuard.js +74 -0
  30. package/src/plugins/budget/README.md +43 -0
  31. package/src/plugins/budget/budget.js +79 -0
  32. package/src/plugins/cp/README.md +5 -4
  33. package/src/plugins/cp/cp.js +10 -6
  34. package/src/plugins/cp/cpDoc.js +29 -0
  35. package/src/plugins/current/README.md +4 -4
  36. package/src/plugins/current/current.js +9 -6
  37. package/src/plugins/engine/engine.sql +1 -8
  38. package/src/plugins/engine/turn_context.sql +4 -9
  39. package/src/plugins/env/README.md +3 -4
  40. package/src/plugins/env/env.js +5 -5
  41. package/src/plugins/env/envDoc.js +29 -0
  42. package/src/plugins/file/README.md +9 -12
  43. package/src/plugins/file/file.js +34 -35
  44. package/src/plugins/get/README.md +2 -2
  45. package/src/plugins/get/get.js +6 -5
  46. package/src/plugins/get/getDoc.js +41 -0
  47. package/src/plugins/hedberg/hedberg.js +2 -1
  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 +9 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +29 -17
  57. package/src/plugins/known/knownDoc.js +33 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +10 -6
  60. package/src/plugins/mv/mvDoc.js +31 -0
  61. package/src/plugins/persona/persona.js +78 -0
  62. package/src/plugins/previous/README.md +2 -2
  63. package/src/plugins/previous/previous.js +9 -6
  64. package/src/plugins/progress/progress.js +41 -15
  65. package/src/plugins/prompt/README.md +5 -5
  66. package/src/plugins/prompt/prompt.js +18 -13
  67. package/src/plugins/rm/README.md +4 -4
  68. package/src/plugins/rm/rm.js +5 -5
  69. package/src/plugins/rm/rmDoc.js +30 -0
  70. package/src/plugins/rpc/README.md +15 -28
  71. package/src/plugins/rpc/rpc.js +42 -77
  72. package/src/plugins/set/README.md +13 -12
  73. package/src/plugins/set/set.js +60 -5
  74. package/src/plugins/set/setDoc.js +45 -0
  75. package/src/plugins/sh/README.md +4 -4
  76. package/src/plugins/sh/sh.js +5 -5
  77. package/src/plugins/sh/shDoc.js +29 -0
  78. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  79. package/src/plugins/summarize/README.md +6 -5
  80. package/src/plugins/summarize/summarize.js +7 -6
  81. package/src/plugins/summarize/summarizeDoc.js +33 -0
  82. package/src/plugins/telemetry/telemetry.js +3 -1
  83. package/src/plugins/think/README.md +20 -0
  84. package/src/plugins/think/think.js +5 -0
  85. package/src/plugins/unknown/README.md +5 -5
  86. package/src/plugins/unknown/unknown.js +9 -7
  87. package/src/plugins/unknown/unknownDoc.js +31 -0
  88. package/src/plugins/update/README.md +3 -8
  89. package/src/plugins/update/update.js +7 -6
  90. package/src/plugins/update/updateDoc.js +33 -0
  91. package/src/server/RpcRegistry.js +52 -4
  92. package/src/sql/v_model_context.sql +16 -21
  93. package/src/plugins/ask_user/docs.md +0 -2
  94. package/src/plugins/cp/docs.md +0 -2
  95. package/src/plugins/env/docs.md +0 -4
  96. package/src/plugins/get/docs.md +0 -10
  97. package/src/plugins/known/docs.md +0 -3
  98. package/src/plugins/mv/docs.md +0 -2
  99. package/src/plugins/rm/docs.md +0 -6
  100. package/src/plugins/set/docs.md +0 -6
  101. package/src/plugins/sh/docs.md +0 -2
  102. package/src/plugins/skills/README.md +0 -25
  103. package/src/plugins/store/README.md +0 -20
  104. package/src/plugins/store/docs.md +0 -6
  105. package/src/plugins/store/store.js +0 -63
  106. package/src/plugins/summarize/docs.md +0 -4
  107. package/src/plugins/unknown/docs.md +0 -5
  108. package/src/plugins/update/docs.md +0 -4
@@ -1,4 +1,3 @@
1
- import KnownStore from "../../agent/KnownStore.js";
2
1
  import msg from "../../agent/messages.js";
3
2
  import RummyContext from "../../hooks/RummyContext.js";
4
3
  import File from "../file/file.js";
@@ -90,15 +89,15 @@ export default class Rpc {
90
89
 
91
90
  // --- Entry operations (same dispatch as model) ---
92
91
 
92
+ // Override: get has persist flag for file constraint management
93
93
  r.register("get", {
94
94
  handler: async (params, ctx) => {
95
95
  if (!params.path) throw new Error("path is required");
96
96
 
97
97
  if (params.persist) {
98
98
  const visibility = params.readonly ? "readonly" : "active";
99
- return File.activate(
99
+ await File.setConstraint(
100
100
  ctx.db,
101
- ctx.projectAgent.entries,
102
101
  ctx.projectId,
103
102
  params.path,
104
103
  visibility,
@@ -115,101 +114,50 @@ export default class Rpc {
115
114
  description: "Promote entry to full state.",
116
115
  params: {
117
116
  path: "string — file path or glob pattern",
118
- run: "string? — run alias (required without persist)",
119
- persist: "boolean? — create file constraint",
117
+ run: "string — run alias",
118
+ persist: "boolean? — also create file constraint",
120
119
  readonly: "boolean? — with persist, set readonly instead of active",
121
120
  },
122
121
  requiresInit: true,
123
122
  });
124
123
 
124
+ // store is not a tool — it manages file constraints
125
125
  r.register("store", {
126
126
  handler: async (params, ctx) => {
127
127
  if (!params.path) throw new Error("path is required");
128
128
 
129
129
  if (params.clear) {
130
- return File.drop(ctx.db, ctx.projectId, params.path);
130
+ await File.dropConstraint(ctx.db, ctx.projectId, params.path);
131
+ return { status: "ok" };
131
132
  }
132
133
  if (params.persist) {
133
- if (params.ignore) {
134
- return File.ignore(
135
- ctx.db,
136
- ctx.projectAgent.entries,
137
- ctx.projectId,
138
- params.path,
139
- );
140
- }
141
- return File.drop(ctx.db, ctx.projectId, params.path);
134
+ const visibility = params.ignore ? "ignore" : "active";
135
+ await File.setConstraint(
136
+ ctx.db,
137
+ ctx.projectId,
138
+ params.path,
139
+ visibility,
140
+ );
142
141
  }
143
142
 
144
143
  if (!params.run) throw new Error("run is required");
145
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
146
- await dispatchTool(hooks, rummy, "store", params.path, "", {
147
- path: params.path,
148
- });
144
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
145
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
146
+ const store = ctx.projectAgent.entries;
147
+ await store.demoteByPattern(runRow.id, params.path, null);
149
148
  return { status: "ok" };
150
149
  },
151
150
  description: "Demote entry to stored state.",
152
151
  params: {
153
152
  path: "string — file path or glob pattern",
154
153
  run: "string? — run alias (required without persist)",
155
- persist: "boolean? — create file constraint",
154
+ persist: "boolean? — also create file constraint",
156
155
  ignore: "boolean? — with persist, exclude from scan",
157
156
  clear: "boolean? — remove existing constraint",
158
157
  },
159
158
  requiresInit: true,
160
159
  });
161
160
 
162
- r.register("set", {
163
- handler: async (params, ctx) => {
164
- if (!params.path) throw new Error("path is required");
165
- if (!params.run) throw new Error("run is required");
166
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
167
-
168
- const scheme = KnownStore.scheme(params.path);
169
- if (scheme) {
170
- await rummy.set({
171
- path: params.path,
172
- body: params.body,
173
- status: params.status || 200,
174
- attributes: params.attributes,
175
- });
176
- } else {
177
- await dispatchTool(hooks, rummy, "set", params.path, params.body, {
178
- path: params.path,
179
- ...params.attributes,
180
- });
181
- }
182
- return { status: "ok" };
183
- },
184
- description: "Create or update an entry.",
185
- params: {
186
- run: "string — run alias",
187
- path: "string — entry path",
188
- body: "string? — entry content",
189
- status: "number? — HTTP status code (default: 200)",
190
- attributes: "object? — JSON attributes",
191
- },
192
- requiresInit: true,
193
- });
194
-
195
- r.register("rm", {
196
- handler: async (params, ctx) => {
197
- if (!params.path) throw new Error("path is required");
198
- if (!params.run) throw new Error("run is required");
199
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
200
- await dispatchTool(hooks, rummy, "rm", params.path, "", {
201
- path: params.path,
202
- });
203
- return { status: "ok" };
204
- },
205
- description: "Remove an entry.",
206
- params: {
207
- run: "string — run alias",
208
- path: "string — entry path",
209
- },
210
- requiresInit: true,
211
- });
212
-
213
161
  r.register("getEntries", {
214
162
  handler: async (params, ctx) => {
215
163
  let run;
@@ -283,7 +231,9 @@ export default class Rpc {
283
231
  temperature: params.temperature ?? null,
284
232
  persona: params.persona ?? null,
285
233
  contextLimit: params.contextLimit,
286
- noContext: params.noContext,
234
+ noRepo: params.noRepo,
235
+ noInteraction: params.noInteraction,
236
+ noWeb: params.noWeb,
287
237
  fork: params.fork,
288
238
  },
289
239
  );
@@ -297,7 +247,9 @@ export default class Rpc {
297
247
  temperature: "number?",
298
248
  persona: "string?",
299
249
  contextLimit: "number?",
300
- noContext: "boolean?",
250
+ noRepo: "boolean?",
251
+ noInteraction: "boolean? — disable ask_user tool",
252
+ noWeb: "boolean? — disable search and URL fetch",
301
253
  fork: "boolean?",
302
254
  },
303
255
  requiresInit: true,
@@ -315,7 +267,9 @@ export default class Rpc {
315
267
  temperature: params.temperature ?? null,
316
268
  persona: params.persona ?? null,
317
269
  contextLimit: params.contextLimit,
318
- noContext: params.noContext,
270
+ noRepo: params.noRepo,
271
+ noInteraction: params.noInteraction,
272
+ noWeb: params.noWeb,
319
273
  fork: params.fork,
320
274
  },
321
275
  );
@@ -329,7 +283,9 @@ export default class Rpc {
329
283
  temperature: "number?",
330
284
  persona: "string?",
331
285
  contextLimit: "number?",
332
- noContext: "boolean?",
286
+ noRepo: "boolean?",
287
+ noInteraction: "boolean? — disable ask_user tool",
288
+ noWeb: "boolean? — disable search and URL fetch",
333
289
  fork: "boolean?",
334
290
  },
335
291
  requiresInit: true,
@@ -524,6 +480,10 @@ export default class Rpc {
524
480
  r.registerNotification("run/progress", "Turn status.");
525
481
  r.registerNotification("ui/render", "Streaming output.");
526
482
  r.registerNotification("ui/notify", "Toast notification.");
483
+
484
+ // Auto-dispatch: any registered tool is callable via RPC.
485
+ // Checked at request time — no timing dependency on plugin load order.
486
+ r.setToolFallback(hooks, buildRunContext, dispatchTool);
527
487
  }
528
488
  }
529
489
 
@@ -544,7 +504,7 @@ async function buildRunContext(hooks, ctx, runAlias) {
544
504
  sequence: runRow.next_turn,
545
505
  runId: runRow.id,
546
506
  turnId: null,
547
- noContext: false,
507
+ noRepo: false,
548
508
  contextSize: null,
549
509
  systemPrompt: "",
550
510
  loopPrompt: "",
@@ -555,7 +515,12 @@ async function buildRunContext(hooks, ctx, runAlias) {
555
515
 
556
516
  async function dispatchTool(hooks, rummy, scheme, path, body, attributes) {
557
517
  const store = rummy.entries;
558
- const resultPath = await store.dedup(rummy.runId, scheme, path);
518
+ const resultPath = await store.dedup(
519
+ rummy.runId,
520
+ scheme,
521
+ path,
522
+ rummy.sequence,
523
+ );
559
524
 
560
525
  await store.upsert(rummy.runId, rummy.sequence, resultPath, body, 200, {
561
526
  attributes: attributes,
@@ -1,32 +1,33 @@
1
1
  # set
2
2
 
3
- Writes or edits entry content. Handles new files, full overwrites, SEARCH/REPLACE edits, and pattern updates.
3
+ Writes or edits entry content. Handles new files, full overwrites,
4
+ SEARCH/REPLACE edits, and pattern updates.
4
5
 
5
6
  ## Files
6
7
 
7
8
  - **set.js** — Plugin registration and edit dispatch logic.
8
- - **HeuristicMatcher.js** — Fuzzy SEARCH/REPLACE matching. Handles whitespace/indentation differences and escaped characters when literal match fails.
9
+ - **HeuristicMatcher.js** — Fuzzy SEARCH/REPLACE matching.
9
10
  - **HeuristicMatcher.test.js** — Tests for HeuristicMatcher.
10
11
 
11
12
  ## Registration
12
13
 
13
14
  - **Tool**: `set`
14
- - **Modes**: ask, act
15
- - **Category**: act
16
- - **Handler**: Routes to different paths based on attributes:
15
+ - **Category**: `logging`
16
+ - **Handler**: Routes based on attributes:
17
17
  - `blocks` or `search` — SEARCH/REPLACE edit via `processEdit`.
18
18
  - `preview` — pattern preview (dry run).
19
- - K/V path — direct upsert at `full` state.
20
- - File path — produces `proposed` entry with udiff patch.
19
+ - Scheme path — direct upsert at status 200.
20
+ - File path — produces status 202 (proposed) with unified diff patch.
21
21
  - Glob/filter — bulk update via `updateBodyByPattern`.
22
22
 
23
23
  ## Projection
24
24
 
25
- Shows `set {file}` with token delta (`before→after tokens`). Includes the merge conflict block when a SEARCH/REPLACE was performed.
25
+ Shows `set {file}` with token delta (`before→after tokens`). Includes
26
+ the merge conflict block when a SEARCH/REPLACE was performed.
26
27
 
27
28
  ## Behavior
28
29
 
29
- - **Literal match first**: SEARCH text is matched literally against the entry body.
30
- - **Heuristic fallback**: On literal failure, `HeuristicMatcher.matchAndPatch` attempts fuzzy matching with warnings.
31
- - **Patch generation**: `generatePatch` produces unified diff format for client display.
32
- - File writes are always `proposed`; K/V writes resolve immediately.
30
+ - **Literal match first**: SEARCH text is matched literally.
31
+ - **Heuristic fallback**: On literal failure, fuzzy matching with warnings.
32
+ - **Patch generation**: `generatePatch` produces unified diff for client display.
33
+ - File writes are always status 202 (proposed); scheme writes resolve immediately.
@@ -1,7 +1,9 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
3
2
  import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
4
3
  import { storePatternResult } from "../helpers.js";
4
+ import docs from "./setDoc.js";
5
+
6
+ const VALID_FIDELITY = { archive: 1, summary: 1, index: 1, full: 1 };
5
7
 
6
8
  // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
7
9
  export default class Set {
@@ -14,16 +16,69 @@ export default class Set {
14
16
  core.on("full", this.full.bind(this));
15
17
  core.on("summary", this.summary.bind(this));
16
18
  core.on("turn.proposing", this.#materializeRevisions.bind(this));
17
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
18
- core.filter("instructions.toolDocs", async (content) =>
19
- content ? `${content}\n\n${docs}` : docs,
20
- );
19
+ core.filter("instructions.toolDocs", async (docsMap) => {
20
+ docsMap.set = docs;
21
+ return docsMap;
22
+ });
21
23
  }
22
24
 
23
25
  async handler(entry, rummy) {
24
26
  const { entries: store, sequence: turn, runId, loopId } = rummy;
25
27
  const attrs = entry.attributes;
26
28
 
29
+ // Fidelity control: <set path="..." fidelity="archive"/>
30
+ const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
31
+ if (fidelityAttr && attrs.path) {
32
+ const target = attrs.path;
33
+ const rawSummary =
34
+ typeof attrs.summary === "string" ? attrs.summary : null;
35
+ const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
36
+ const matches = await store.getEntriesByPattern(
37
+ runId,
38
+ target,
39
+ attrs.body,
40
+ );
41
+ if (entry.body) {
42
+ // Write content directly at specified fidelity
43
+ const entryAttrs = summaryText ? { summary: summaryText } : null;
44
+ for (const match of matches) {
45
+ await store.upsert(runId, turn, match.path, entry.body, 200, {
46
+ fidelity: fidelityAttr,
47
+ attributes: entryAttrs,
48
+ loopId,
49
+ });
50
+ }
51
+ if (matches.length === 0) {
52
+ await store.upsert(runId, turn, target, entry.body, 200, {
53
+ fidelity: fidelityAttr,
54
+ attributes: entryAttrs,
55
+ loopId,
56
+ });
57
+ }
58
+ } else {
59
+ // No body — change fidelity, attach summary if provided
60
+ for (const match of matches) {
61
+ await store.setFidelity(runId, match.path, fidelityAttr);
62
+ if (summaryText) {
63
+ await store.setAttributes(runId, match.path, {
64
+ summary: summaryText,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ const label =
70
+ fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
71
+ const body =
72
+ matches.length > 0
73
+ ? `${matches.map((m) => m.path).join(", ")} ${label}`
74
+ : `${target} not found`;
75
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
76
+ fidelity: "archive",
77
+ loopId,
78
+ });
79
+ return;
80
+ }
81
+
27
82
  if (attrs.blocks || attrs.search != null) {
28
83
  await this.#processEdit(rummy, entry, attrs);
29
84
  return;
@@ -0,0 +1,45 @@
1
+ // Tool doc for <set>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax: path attr + body = edit content
6
+ ['## <set path="[path/to/file]">[edit]</set> - Edit a file or entry'],
7
+
8
+ // --- Examples: sed, SEARCH/REPLACE, fidelity control
9
+ [
10
+ 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g</set>',
11
+ "Sed syntax: most common edit pattern. Shows s/old/new/ with g flag.",
12
+ ],
13
+ [
14
+ `Example: <set path="src/app.js"><<<<<<< SEARCH
15
+ // TODO: add error handling
16
+ =======
17
+ // error handler configured
18
+ >>>>>>> REPLACE</set>`,
19
+ "SEARCH/REPLACE block: literal match and replace. Use when sed escaping is complex.",
20
+ ],
21
+ [
22
+ 'Example: <set path="known://plan" stored summary="Migration plan for Q2"/>',
23
+ "Fidelity + summary: archive an entry while preserving a description. Lifecycle endpoint.",
24
+ ],
25
+
26
+ // --- Constraints
27
+ [
28
+ '* `fidelity="..."`: `archive`, `summary`, `index`, `full`',
29
+ "Fidelity control. Archive removes from context but preserves for retrieval.",
30
+ ],
31
+ [
32
+ '* `summary="..."` (<= 80 chars) persists across fidelity changes',
33
+ "Model-authored descriptions survive demotion. No janitorial pass needed.",
34
+ ],
35
+ [
36
+ "* YOU MUST NOT use <sh/> or <env/> to read, create, or edit files",
37
+ "Forces file operations through set/get. Prevents untracked mutations.",
38
+ ],
39
+ [
40
+ "* Editing: s/old/new/ sed patterns and literal SEARCH/REPLACE blocks",
41
+ "Both syntaxes supported. Hedberg normalizes either form.",
42
+ ],
43
+ ];
44
+
45
+ export default LINES.map(([text]) => text).join("\n");
@@ -5,9 +5,8 @@ Proposes shell command execution for client approval.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `sh`
8
- - **Modes**: act only
9
- - **Category**: act
10
- - **Handler**: Upserts the entry as `proposed` state. The client must approve execution.
8
+ - **Category**: `logging`
9
+ - **Handler**: Upserts the entry at status 202 (proposed). The client must approve execution.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows `sh {command}` followed by the entry body.
15
14
 
16
15
  ## Behavior
17
16
 
18
- All shell commands require client-side approval — nothing executes server-side. Act mode only; blocked in ask mode.
17
+ All shell commands require client-side approval — nothing executes
18
+ server-side. Act mode only; excluded in ask mode by `resolveForLoop`.
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./shDoc.js";
2
2
 
3
3
  export default class Sh {
4
4
  #core;
@@ -9,10 +9,10 @@ export default class Sh {
9
9
  core.on("handler", this.handler.bind(this));
10
10
  core.on("full", this.full.bind(this));
11
11
  core.on("summary", this.summary.bind(this));
12
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
- core.filter("instructions.toolDocs", async (content) =>
14
- content ? `${content}\n\n${docs}` : docs,
15
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.sh = docs;
14
+ return docsMap;
15
+ });
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
@@ -0,0 +1,29 @@
1
+ // Tool doc for <sh>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax
6
+ ["## <sh>[command]</sh> - Run a shell command with side effects"],
7
+
8
+ // --- Examples: install and test — real mutations
9
+ [
10
+ "Example: <sh>npm install express</sh>",
11
+ "Package install. Shows a real side-effect command.",
12
+ ],
13
+ [
14
+ "Example: <sh>npm test</sh>",
15
+ "Test execution. Another common side-effect action.",
16
+ ],
17
+
18
+ // --- Constraints
19
+ [
20
+ "* YOU MUST NOT use <sh/> to read, create, or edit files — use <get/> and <set/>",
21
+ "Forces file operations through the entry system. Prevents untracked mutations.",
22
+ ],
23
+ [
24
+ "* YOU MUST use <env/> for commands without side effects",
25
+ "Reinforces the env/sh split. Read = env, mutate = sh.",
26
+ ],
27
+ ];
28
+
29
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,15 +1,17 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
- export default class Skills {
4
+ export default class Skill {
5
5
  #core;
6
6
 
7
7
  constructor(core) {
8
8
  this.#core = core;
9
9
  core.registerScheme({
10
10
  name: "skill",
11
- category: "knowledge",
11
+ category: "data",
12
12
  });
13
+ core.hooks.tools.onView("skill", (entry) => entry.body);
14
+
13
15
  const r = core.hooks.rpc.registry;
14
16
 
15
17
  r.register("skill/add", {
@@ -22,19 +24,12 @@ export default class Skills {
22
24
 
23
25
  const body = await loadFile("skills", params.name);
24
26
  const store = ctx.projectAgent.entries;
25
- await store.upsert(
26
- runRow.id,
27
- runRow.next_turn,
28
- `skill://${params.name}`,
29
- body,
30
- 200,
31
- {
32
- attributes: {
33
- name: params.name,
34
- source: filePath("skills", params.name),
35
- },
27
+ await store.upsert(runRow.id, 0, `skill://${params.name}`, body, 200, {
28
+ attributes: {
29
+ name: params.name,
30
+ source: filePath("skills", params.name),
36
31
  },
37
- );
32
+ });
38
33
 
39
34
  return { status: "ok", skill: params.name };
40
35
  },
@@ -97,43 +92,7 @@ export default class Skills {
97
92
  requiresInit: true,
98
93
  });
99
94
 
100
- r.register("persona/set", {
101
- handler: async (params, ctx) => {
102
- if (!params.run) throw new Error("run is required");
103
-
104
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
105
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
106
-
107
- let text = params.text;
108
- if (params.name && !text) {
109
- text = await loadFile("personas", params.name);
110
- }
111
-
112
- await ctx.db.update_run_config.run({
113
- id: runRow.id,
114
- temperature: null,
115
- persona: text || null,
116
- context_limit: null,
117
- model: null,
118
- });
119
-
120
- return { status: "ok" };
121
- },
122
- description:
123
- "Set persona on a run. Pass name or text. Pass neither to clear.",
124
- params: {
125
- run: "string — run alias",
126
- name: "string? — persona filename (without .md)",
127
- text: "string? — raw persona text (overrides name)",
128
- },
129
- requiresInit: true,
130
- });
131
-
132
- r.register("listPersonas", {
133
- handler: async () => listAvailable("personas"),
134
- description: "List available persona files. Returns [{ name, path }].",
135
- requiresInit: true,
136
- });
95
+ // Persona methods extracted to persona plugin.
137
96
  }
138
97
  }
139
98
 
@@ -1,13 +1,12 @@
1
1
  # summarize
2
2
 
3
- Structural tool for model-generated summaries.
3
+ Lifecycle signal the model declares it has completed the task.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `summarize`
8
- - **Modes**: ask, act
9
- - **Category**: structural
10
- - **Handler**: None — projection only.
8
+ - **Category**: `logging`
9
+ - **Handler**: None — recorded by TurnExecutor as a lifecycle signal.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,6 @@ Shows `summarize` followed by the entry body.
15
14
 
16
15
  ## Behavior
17
16
 
18
- No handler logic. The tool registration exists so the model can emit summary entries that appear in context via projection.
17
+ If the model sends `<summarize>` but actions in the same turn failed,
18
+ TurnExecutor overrides it to `<update>` — the model's assertion that
19
+ it's done is false.
@@ -1,17 +1,18 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./summarizeDoc.js";
2
2
 
3
3
  export default class Summarize {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({ category: "structural" });
8
+ core.ensureTool();
9
+ core.registerScheme({ category: "logging" });
9
10
  core.on("full", this.full.bind(this));
10
11
  core.on("summary", this.summary.bind(this));
11
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
12
- core.filter("instructions.toolDocs", async (content) =>
13
- content ? `${content}\n\n${docs}` : docs,
14
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.summarize = docs;
14
+ return docsMap;
15
+ });
15
16
  }
16
17
 
17
18
  full(entry) {
@@ -0,0 +1,33 @@
1
+ // Tool doc for <summarize>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax
6
+ ["## <summarize>[answer or summary]</summarize> - Signal completion"],
7
+
8
+ // --- Examples: answer and task completion
9
+ [
10
+ "Example: <summarize>The port is 8080</summarize>",
11
+ "Direct answer. Shows summarize as the vehicle for delivering answers.",
12
+ ],
13
+ [
14
+ "Example: <summarize>Installed express, updated config</summarize>",
15
+ "Task summary. Shows summarize for action completion.",
16
+ ],
17
+
18
+ // --- Constraints: RFC-style MUST/MUST NOT
19
+ [
20
+ "* YOU MUST use <summarize> when done — describes the final state",
21
+ "Completion signal. Without this, the loop continues indefinitely.",
22
+ ],
23
+ [
24
+ "* YOU MUST NOT use <summarize> if still working — use <update/> instead",
25
+ "Mutual exclusion with update. Prevents premature completion.",
26
+ ],
27
+ [
28
+ "* YOU MUST keep <summarize> to <= 80 characters",
29
+ "Length cap. Matches the summary attribute constraint. Prevents verbose output.",
30
+ ],
31
+ ];
32
+
33
+ export default LINES.map(([text]) => text).join("\n");
@@ -75,8 +75,8 @@ export default class Telemetry {
75
75
  result,
76
76
  responseMessage,
77
77
  content,
78
- commands,
79
78
  unparsed,
79
+ assembledTokens,
80
80
  systemMsg,
81
81
  userMsg,
82
82
  }) {
@@ -149,6 +149,8 @@ export default class Telemetry {
149
149
  0;
150
150
  await rummy.db.update_turn_stats.run({
151
151
  id: rummy.turnId,
152
+ context_tokens: assembledTokens ?? 0,
153
+ reasoning_content: responseMessage?.reasoning_content || null,
152
154
  prompt_tokens: usage.prompt_tokens ?? 0,
153
155
  cached_tokens: cachedTokens ?? 0,
154
156
  completion_tokens: usage.completion_tokens ?? 0,