@possumtech/rummy 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.env.example +12 -3
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +454 -197
  4. package/SPEC.md +284 -93
  5. package/migrations/001_initial_schema.sql +57 -70
  6. package/package.json +16 -10
  7. package/service.js +1 -1
  8. package/src/agent/AgentLoop.js +254 -70
  9. package/src/agent/ContextAssembler.js +18 -4
  10. package/src/agent/KnownStore.js +156 -23
  11. package/src/agent/ProjectAgent.js +5 -4
  12. package/src/agent/ResponseHealer.js +21 -1
  13. package/src/agent/TurnExecutor.js +393 -115
  14. package/src/agent/XmlParser.js +92 -39
  15. package/src/agent/known_checks.sql +5 -4
  16. package/src/agent/known_queries.sql +4 -3
  17. package/src/agent/known_store.sql +45 -15
  18. package/src/agent/loops.sql +63 -0
  19. package/src/agent/runs.sql +7 -7
  20. package/src/agent/schemes.sql +5 -2
  21. package/src/agent/tokens.js +6 -21
  22. package/src/agent/turns.sql +13 -4
  23. package/src/hooks/Hooks.js +18 -0
  24. package/src/hooks/PluginContext.js +14 -10
  25. package/src/hooks/RummyContext.js +30 -10
  26. package/src/hooks/ToolRegistry.js +83 -19
  27. package/src/llm/LlmProvider.js +27 -8
  28. package/src/llm/OpenAiClient.js +20 -0
  29. package/src/llm/OpenRouterClient.js +24 -2
  30. package/src/llm/XaiClient.js +47 -2
  31. package/src/plugins/ask_user/README.md +4 -4
  32. package/src/plugins/ask_user/ask_user.js +8 -7
  33. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  34. package/src/plugins/budget/BudgetGuard.js +74 -0
  35. package/src/plugins/budget/README.md +43 -0
  36. package/src/plugins/budget/budget.js +79 -0
  37. package/src/plugins/cp/README.md +5 -4
  38. package/src/plugins/cp/cp.js +16 -12
  39. package/src/plugins/cp/cpDoc.js +29 -0
  40. package/src/plugins/current/README.md +4 -4
  41. package/src/plugins/current/current.js +12 -10
  42. package/src/plugins/engine/engine.sql +5 -10
  43. package/src/plugins/engine/turn_context.sql +13 -13
  44. package/src/plugins/env/README.md +3 -4
  45. package/src/plugins/env/env.js +8 -7
  46. package/src/plugins/env/envDoc.js +29 -0
  47. package/src/plugins/file/README.md +9 -12
  48. package/src/plugins/file/file.js +34 -45
  49. package/src/plugins/get/README.md +2 -2
  50. package/src/plugins/get/get.js +28 -11
  51. package/src/plugins/get/getDoc.js +41 -0
  52. package/src/plugins/hedberg/docs.md +0 -9
  53. package/src/plugins/hedberg/hedberg.js +4 -6
  54. package/src/plugins/hedberg/matcher.js +1 -1
  55. package/src/plugins/hedberg/normalize.js +28 -0
  56. package/src/plugins/hedberg/patterns.js +31 -33
  57. package/src/plugins/hedberg/sed.js +17 -10
  58. package/src/plugins/helpers.js +2 -2
  59. package/src/plugins/index.js +93 -28
  60. package/src/plugins/instructions/README.md +6 -2
  61. package/src/plugins/instructions/instructions.js +21 -5
  62. package/src/plugins/instructions/preamble.md +9 -5
  63. package/src/plugins/known/README.md +10 -7
  64. package/src/plugins/known/known.js +33 -23
  65. package/src/plugins/known/knownDoc.js +33 -0
  66. package/src/plugins/mv/README.md +5 -4
  67. package/src/plugins/mv/mv.js +16 -12
  68. package/src/plugins/mv/mvDoc.js +31 -0
  69. package/src/plugins/persona/persona.js +78 -0
  70. package/src/plugins/previous/README.md +2 -2
  71. package/src/plugins/previous/previous.js +12 -8
  72. package/src/plugins/progress/progress.js +44 -12
  73. package/src/plugins/prompt/README.md +5 -5
  74. package/src/plugins/prompt/prompt.js +23 -19
  75. package/src/plugins/rm/README.md +4 -4
  76. package/src/plugins/rm/rm.js +29 -12
  77. package/src/plugins/rm/rmDoc.js +30 -0
  78. package/src/plugins/rpc/README.md +15 -28
  79. package/src/plugins/rpc/rpc.js +63 -107
  80. package/src/plugins/set/README.md +13 -12
  81. package/src/plugins/set/set.js +82 -21
  82. package/src/plugins/set/setDoc.js +45 -0
  83. package/src/plugins/sh/README.md +4 -4
  84. package/src/plugins/sh/sh.js +8 -7
  85. package/src/plugins/sh/shDoc.js +29 -0
  86. package/src/plugins/{skills/skills.js → skill/skill.js} +12 -54
  87. package/src/plugins/summarize/README.md +6 -5
  88. package/src/plugins/summarize/summarize.js +7 -6
  89. package/src/plugins/summarize/summarizeDoc.js +33 -0
  90. package/src/plugins/telemetry/telemetry.js +20 -8
  91. package/src/plugins/think/README.md +20 -0
  92. package/src/plugins/think/think.js +5 -0
  93. package/src/plugins/unknown/README.md +5 -5
  94. package/src/plugins/unknown/unknown.js +11 -8
  95. package/src/plugins/unknown/unknownDoc.js +31 -0
  96. package/src/plugins/update/README.md +3 -8
  97. package/src/plugins/update/update.js +7 -6
  98. package/src/plugins/update/updateDoc.js +33 -0
  99. package/src/server/ClientConnection.js +3 -5
  100. package/src/server/RpcRegistry.js +52 -4
  101. package/src/sql/v_model_context.sql +31 -39
  102. package/src/sql/v_run_log.sql +3 -3
  103. package/src/agent/prompt_queue.sql +0 -39
  104. package/src/plugins/ask_user/docs.md +0 -2
  105. package/src/plugins/cp/docs.md +0 -2
  106. package/src/plugins/env/docs.md +0 -2
  107. package/src/plugins/get/docs.md +0 -6
  108. package/src/plugins/known/docs.md +0 -3
  109. package/src/plugins/mv/docs.md +0 -2
  110. package/src/plugins/rm/docs.md +0 -4
  111. package/src/plugins/set/docs.md +0 -4
  112. package/src/plugins/sh/docs.md +0 -2
  113. package/src/plugins/skills/README.md +0 -25
  114. package/src/plugins/store/README.md +0 -20
  115. package/src/plugins/store/docs.md +0 -5
  116. package/src/plugins/store/store.js +0 -52
  117. package/src/plugins/summarize/docs.md +0 -4
  118. package/src/plugins/unknown/docs.md +0 -5
  119. package/src/plugins/update/docs.md +0 -4
@@ -19,6 +19,8 @@ const KNOWN_ATTRS = new Set([
19
19
  "results",
20
20
  "command",
21
21
  "warn",
22
+ "summary",
23
+ "fidelity",
22
24
  ]);
23
25
 
24
26
  export function normalizeAttrs(attrs) {
@@ -37,5 +39,31 @@ export function normalizeAttrs(attrs) {
37
39
  }
38
40
  }
39
41
  if ("preview" in out) out.preview = true;
42
+ // summary="..." is the description text, not a fidelity flag
43
+ if ("summary" in out && !out.summary) out.summary = true;
44
+ // file:// prefix — strip silently, bare paths are the convention
45
+ if (out.path?.startsWith("file://")) out.path = out.path.slice(7);
40
46
  return out;
41
47
  }
48
+
49
+ /**
50
+ * Parse JSON-style edit from body content.
51
+ * Accepts: {"search":"old","replace":"new"} and {search="old",replace="new"}
52
+ * Returns { search, replace } or null.
53
+ */
54
+ export function parseJsonEdit(text) {
55
+ const trimmed = text.trim();
56
+ if (!trimmed.startsWith("{") || !/search/.test(trimmed)) return null;
57
+ try {
58
+ const json = JSON.parse(trimmed);
59
+ if (json.search != null)
60
+ return { search: json.search, replace: json.replace ?? "" };
61
+ } catch {
62
+ const searchMatch = trimmed.match(/search\s*=\s*"([^"]*)"/);
63
+ const replaceMatch = trimmed.match(/replace\s*=\s*"([^"]*)"/);
64
+ if (searchMatch) {
65
+ return { search: searchMatch[1], replace: replaceMatch?.[1] ?? "" };
66
+ }
67
+ }
68
+ return null;
69
+ }
@@ -1,4 +1,6 @@
1
- import { JSDOM } from "jsdom";
1
+ import { DOMParser } from "@xmldom/xmldom";
2
+ import picomatch from "picomatch";
3
+ import xpath from "xpath";
2
4
 
3
5
  export const deterministic = true;
4
6
 
@@ -130,26 +132,7 @@ function detect(pattern) {
130
132
 
131
133
  // --- Compilation ---
132
134
 
133
- function globToRegex(glob) {
134
- let result = "";
135
- for (let i = 0; i < glob.length; i++) {
136
- const c = glob[i];
137
- if (c === "*") result += ".*";
138
- else if (c === "?") result += ".";
139
- else if (c === "[") {
140
- const close = glob.indexOf("]", i + 1);
141
- if (close === -1) {
142
- result += "\\[";
143
- continue;
144
- }
145
- result += glob.slice(i, close + 1);
146
- i = close;
147
- } else if (/[.+^${}()|\\]/.test(c)) {
148
- result += `\\${c}`;
149
- } else result += c;
150
- }
151
- return result;
152
- }
135
+ // Glob matching delegated to picomatch (standard, battle-tested).
153
136
 
154
137
  function parseRegex(pattern) {
155
138
  const lastSlash = pattern.lastIndexOf("/");
@@ -213,12 +196,28 @@ function compile(pattern) {
213
196
  switch (type) {
214
197
  case "literal":
215
198
  return { type, pattern };
216
- case "glob":
217
- return {
218
- type,
219
- anchoredRe: new RegExp(`^${globToRegex(pattern)}$`),
220
- searchRe: new RegExp(globToRegex(pattern)),
221
- };
199
+ case "glob": {
200
+ const escaped = pattern.replace(/([()])/g, "\\$1");
201
+ // Scheme paths have no directory structure — * matches everything
202
+ const opts = escaped.includes("://")
203
+ ? {
204
+ dot: true,
205
+ nobrace: true,
206
+ noextglob: true,
207
+ bash: false,
208
+ regex: true,
209
+ }
210
+ : { dot: true, nobrace: true, noextglob: true };
211
+
212
+ // For scheme paths, convert single * after :// to ** so it crosses "/"
213
+ const prepared = escaped.includes("://")
214
+ ? escaped.replace(/:\/\/\*(?!\*)/, "://**")
215
+ : escaped;
216
+
217
+ const isMatch = picomatch(prepared, opts);
218
+ const picoRe = picomatch.makeRe(prepared, opts);
219
+ return { type, isMatch, searchRe: picoRe };
220
+ }
222
221
  case "regex": {
223
222
  const { body, flags } = parseRegex(pattern);
224
223
  return {
@@ -250,11 +249,10 @@ function compile(pattern) {
250
249
 
251
250
  function evalXpath(expr, string) {
252
251
  try {
253
- const dom = new JSDOM(string, { contentType: "text/xml" });
254
- const doc = dom.window.document;
255
- const result = doc.evaluate(expr, doc, null, 0, null);
256
- const node = result.iterateNext();
257
- if (!node) return null;
252
+ const doc = new DOMParser().parseFromString(string, "text/xml");
253
+ const nodes = xpath.select(expr, doc);
254
+ if (!nodes || nodes.length === 0) return null;
255
+ const node = nodes[0];
258
256
  return { match: node.textContent, node };
259
257
  } catch {
260
258
  return null;
@@ -332,7 +330,7 @@ export function hedmatch(pattern, string) {
332
330
  case "literal":
333
331
  return string === compiled.pattern;
334
332
  case "glob":
335
- return compiled.anchoredRe.test(string);
333
+ return compiled.isMatch(string);
336
334
  case "regex":
337
335
  return compiled.re.test(string);
338
336
  case "sed":
@@ -5,14 +5,15 @@
5
5
  * - Flag extraction (g, i, m, s, v)
6
6
  */
7
7
 
8
- function splitSed(str) {
8
+ function splitSed(str, delim) {
9
9
  const parts = [];
10
10
  let current = "";
11
+ const escaped = `\\${delim}`;
11
12
  for (let i = 0; i < str.length; i++) {
12
13
  if (str[i] === "\\" && i + 1 < str.length) {
13
14
  current += str[i] + str[i + 1];
14
15
  i++;
15
- } else if (str[i] === "/") {
16
+ } else if (str[i] === delim) {
16
17
  parts.push(current);
17
18
  current = "";
18
19
  } else {
@@ -20,26 +21,32 @@ function splitSed(str) {
20
21
  }
21
22
  }
22
23
  parts.push(current);
23
- return parts;
24
+ return { parts, escaped };
24
25
  }
25
26
 
26
27
  export function parseSed(input) {
27
- if (!input.startsWith("s/")) return null;
28
+ // Sed allows any non-alphanumeric delimiter: s/old/new/, s|old|new|, s#old#new#
29
+ const match = input.match(/^s([^\w\s])/);
30
+ if (!match) return null;
28
31
 
32
+ const delim = match[1];
29
33
  const blocks = [];
30
34
  let remaining = input;
31
- while (remaining.startsWith("s/")) {
32
- const parts = splitSed(remaining.slice(2));
35
+ const prefix = `s${delim}`;
36
+
37
+ while (remaining.startsWith(prefix)) {
38
+ const { parts, escaped } = splitSed(remaining.slice(2), delim);
33
39
  if (parts.length < 2) break;
34
40
  const flags = (parts[2] || "").match(/^[gimsv]*/)?.[0] || "";
41
+ const unesc = (s) => s.replaceAll(escaped, delim);
35
42
  blocks.push({
36
- search: parts[0].replaceAll("\\/", "/"),
37
- replace: parts[1].replaceAll("\\/", "/"),
43
+ search: unesc(parts[0]),
44
+ replace: unesc(parts[1]),
38
45
  flags,
39
46
  sed: true,
40
47
  });
41
- const rest = parts.slice(2).join("/");
42
- const next = rest.indexOf("s/");
48
+ const rest = parts.slice(2).join(delim);
49
+ const next = rest.indexOf(prefix);
43
50
  remaining = next >= 0 ? rest.slice(next) : "";
44
51
  }
45
52
 
@@ -10,7 +10,7 @@ export async function storePatternResult(
10
10
  path,
11
11
  bodyFilter,
12
12
  matches,
13
- preview = false,
13
+ { preview = false, loopId = null } = {},
14
14
  ) {
15
15
  const slug = await store.slugPath(runId, scheme, path);
16
16
  const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
@@ -18,5 +18,5 @@ export async function storePatternResult(
18
18
  const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
19
19
  const prefix = preview ? "PREVIEW " : "";
20
20
  const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
21
- await store.upsert(runId, turn, slug, body, "pattern");
21
+ await store.upsert(runId, turn, slug, body, 200, { loopId });
22
22
  }
@@ -1,9 +1,16 @@
1
+ import { execSync } from "node:child_process";
1
2
  import { existsSync } from "node:fs";
2
3
  import { readdir, stat } from "node:fs/promises";
3
- import { basename, join } from "node:path";
4
+ import { basename, isAbsolute, join } from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import PluginContext from "../hooks/PluginContext.js";
6
7
 
8
+ let globalPrefix;
9
+ function getGlobalPrefix() {
10
+ globalPrefix ??= execSync("npm prefix -g", { encoding: "utf8" }).trim();
11
+ return globalPrefix;
12
+ }
13
+
7
14
  const instances = new Map();
8
15
 
9
16
  /**
@@ -23,10 +30,6 @@ export async function registerPlugins(dirs = [], hooks) {
23
30
  const AUDIT_SCHEMES = [
24
31
  "instructions",
25
32
  "system",
26
- "prompt",
27
- "ask",
28
- "act",
29
- "progress",
30
33
  "reasoning",
31
34
  "model",
32
35
  "error",
@@ -35,20 +38,26 @@ const AUDIT_SCHEMES = [
35
38
  "content",
36
39
  ];
37
40
 
41
+ const PROMPT_SCHEMES = ["prompt", "progress"];
42
+
38
43
  /**
39
44
  * After DB is ready, inject db and store into all PluginContext instances,
40
45
  * upsert declared schemes, and bootstrap audit schemes.
41
46
  */
42
47
  export async function initPlugins(db, store, hooks) {
43
48
  for (const name of AUDIT_SCHEMES) {
44
- const scheme = {
49
+ await db.upsert_scheme.run({
45
50
  name,
46
- fidelity: ["ask", "act", "progress"].includes(name) ? "full" : "null",
47
- model_visible: ["ask", "act", "progress"].includes(name) ? 1 : 0,
48
- valid_states: JSON.stringify(["info"]),
51
+ model_visible: 0,
49
52
  category: "audit",
50
- };
51
- await db.upsert_scheme.run(scheme);
53
+ });
54
+ }
55
+ for (const name of PROMPT_SCHEMES) {
56
+ await db.upsert_scheme.run({
57
+ name,
58
+ model_visible: 1,
59
+ category: "prompt",
60
+ });
52
61
  }
53
62
 
54
63
  for (const ctx of instances.values()) {
@@ -66,25 +75,37 @@ export async function initPlugins(db, store, hooks) {
66
75
  for (const s of ctx.schemes) registered.add(s.name);
67
76
  }
68
77
  for (const name of AUDIT_SCHEMES) registered.add(name);
78
+ for (const name of PROMPT_SCHEMES) registered.add(name);
69
79
 
70
80
  for (const toolName of hooks.tools.names) {
71
81
  if (registered.has(toolName)) continue;
72
82
  await db.upsert_scheme.run({
73
83
  name: toolName,
74
- fidelity: "full",
75
84
  model_visible: 1,
76
- valid_states: JSON.stringify([
77
- "full",
78
- "proposed",
79
- "pass",
80
- "rejected",
81
- "error",
82
- "info",
83
- ]),
84
- category: "result",
85
+ category: "logging",
85
86
  });
86
87
  }
87
88
  }
89
+
90
+ if (store) store.loadSchemes(db);
91
+ }
92
+
93
+ function resolvePlugin(packageName) {
94
+ // Check local node_modules first, then global
95
+ const localDir = join(process.cwd(), "node_modules", packageName);
96
+ if (existsSync(join(localDir, "package.json"))) return localDir;
97
+ const globalDir = join(getGlobalPrefix(), "lib", "node_modules", packageName);
98
+ if (existsSync(join(globalDir, "package.json"))) return globalDir;
99
+ throw new Error(`Package '${packageName}' not found locally or globally`);
100
+ }
101
+
102
+ async function importPlugin(packageName) {
103
+ const dir = resolvePlugin(packageName);
104
+ const pkg = JSON.parse(
105
+ (await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
106
+ );
107
+ const entry = pkg.exports?.["."] || pkg.main || "index.js";
108
+ return import(pathToFileURL(join(dir, entry)).href);
88
109
  }
89
110
 
90
111
  async function loadEnvPlugins(hooks) {
@@ -92,9 +113,20 @@ async function loadEnvPlugins(hooks) {
92
113
  if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
93
114
  const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
94
115
  try {
95
- const { default: Plugin } = await import(value);
116
+ const importPromise = isAbsolute(value)
117
+ ? importAbsolute(value)
118
+ : importPlugin(value);
119
+ const { default: Plugin } = await withTimeout(
120
+ importPromise,
121
+ PLUGIN_LOAD_TIMEOUT,
122
+ `Plugin import timed out: ${value}`,
123
+ );
96
124
  if (typeof Plugin?.register === "function") {
97
- await Plugin.register(hooks);
125
+ await withTimeout(
126
+ Plugin.register(hooks),
127
+ PLUGIN_LOAD_TIMEOUT,
128
+ `Plugin register timed out: ${value}`,
129
+ );
98
130
  } else if (typeof Plugin === "function") {
99
131
  const ctx = new PluginContext(name, hooks);
100
132
  new Plugin(ctx);
@@ -107,6 +139,19 @@ async function loadEnvPlugins(hooks) {
107
139
  }
108
140
  }
109
141
 
142
+ async function importAbsolute(dir) {
143
+ const pkgPath = join(dir, "package.json");
144
+ if (!existsSync(pkgPath)) {
145
+ // Bare .js file
146
+ return import(pathToFileURL(dir).href);
147
+ }
148
+ const pkg = JSON.parse(
149
+ (await import("node:fs")).readFileSync(pkgPath, "utf8"),
150
+ );
151
+ const entry = pkg.exports?.["."] || pkg.main || "index.js";
152
+ return import(pathToFileURL(join(dir, entry)).href);
153
+ }
154
+
110
155
  async function scanDir(dir, hooks, isRoot = false) {
111
156
  if (!existsSync(dir)) return;
112
157
 
@@ -154,18 +199,29 @@ async function scanDir(dir, hooks, isRoot = false) {
154
199
  await loadPlugin(fullPath, hooks);
155
200
  }
156
201
  } else if (stats.isDirectory()) {
202
+ if (existsSync(join(fullPath, "DISABLED"))) continue;
157
203
  await scanDir(fullPath, hooks, false);
158
204
  }
159
205
  }
160
206
  }
161
207
 
208
+ const PLUGIN_LOAD_TIMEOUT = 10000;
209
+
162
210
  async function loadPlugin(filePath, hooks) {
163
211
  try {
164
212
  const url = pathToFileURL(filePath).href;
165
- const { default: Plugin } = await import(url);
213
+ const { default: Plugin } = await withTimeout(
214
+ import(url),
215
+ PLUGIN_LOAD_TIMEOUT,
216
+ `Plugin import timed out: ${filePath}`,
217
+ );
166
218
 
167
219
  if (typeof Plugin?.register === "function") {
168
- await Plugin.register(hooks);
220
+ await withTimeout(
221
+ Plugin.register(hooks),
222
+ PLUGIN_LOAD_TIMEOUT,
223
+ `Plugin register timed out: ${filePath}`,
224
+ );
169
225
  } else if (typeof Plugin === "function") {
170
226
  const name = basename(filePath, ".js");
171
227
  const ctx = new PluginContext(name, hooks);
@@ -173,8 +229,17 @@ async function loadPlugin(filePath, hooks) {
173
229
  instances.set(name, ctx);
174
230
  }
175
231
  } catch (err) {
176
- if (process.env.RUMMY_DEBUG === "true") {
177
- console.error(`[RUMMY] Plugin load failed at ${filePath}:`, err);
178
- }
232
+ console.warn(
233
+ `[RUMMY] Plugin load failed: ${basename(filePath)} — ${err.message}`,
234
+ );
179
235
  }
180
236
  }
237
+
238
+ function withTimeout(promise, ms, message) {
239
+ return Promise.race([
240
+ promise,
241
+ new Promise((_, reject) =>
242
+ setTimeout(() => reject(new Error(message)), ms),
243
+ ),
244
+ ]);
245
+ }
@@ -4,8 +4,12 @@ Projects the system prompt instructions into model context.
4
4
 
5
5
  ## Registration
6
6
 
7
- - **Projection**: `onProject("instructions", ...)` — no tool handler.
7
+ - **View**: `full` — renders preamble + tool docs + persona.
8
+ - **Event**: `turn.started` — writes `instructions://system` entry.
9
+ - **Filter**: `instructions.toolDocs` — gathers docs from all tool plugins.
8
10
 
9
11
  ## Behavior
10
12
 
11
- Replaces the `[%TOOLS%]` placeholder in the prompt body with the `tools` attribute. Appends tool descriptions and persona text when present in attributes.
13
+ Replaces the `[%TOOLS%]` placeholder in the preamble with the active
14
+ tool list. Appends tool descriptions gathered via the `toolDocs` filter
15
+ and persona text when present in attributes.
@@ -17,20 +17,36 @@ export default class Instructions {
17
17
  async onTurnStarted({ rummy }) {
18
18
  const { entries: store, sequence: turn, runId } = rummy;
19
19
  const runRow = await rummy.db.get_run_by_id.get({ id: runId });
20
- await store.upsert(runId, turn, "instructions://system", "", "info", {
21
- attributes: { persona: runRow?.persona || null },
20
+ const toolSet = rummy.toolSet
21
+ ? [...rummy.toolSet]
22
+ : this.#core.hooks.tools.names;
23
+ await store.upsert(runId, turn, "instructions://system", "", 200, {
24
+ attributes: {
25
+ persona: runRow?.persona || null,
26
+ toolSet,
27
+ },
22
28
  });
23
29
  }
24
30
 
25
31
  async full(entry) {
26
32
  const attrs = entry.attributes;
27
- const tools = this.#core.hooks.tools.names.join(", ");
33
+ const activeTools = attrs.toolSet
34
+ ? new Set(attrs.toolSet)
35
+ : new Set(this.#core.hooks.tools.names);
36
+ const sorted = this.#core.hooks.tools.names.filter((n) =>
37
+ activeTools.has(n),
38
+ );
39
+ const tools = sorted.join(", ");
28
40
  let prompt = preamble.replace("[%TOOLS%]", tools);
29
41
  const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
30
- "",
31
42
  {},
43
+ { toolSet: activeTools },
32
44
  );
33
- if (toolDocs) prompt += `\n\n${toolDocs}`;
45
+ const docsText = sorted
46
+ .filter((key) => toolDocs[key])
47
+ .map((key) => toolDocs[key])
48
+ .join("\n\n");
49
+ if (docsText) prompt += `\n\n${docsText}`;
34
50
  if (attrs.persona) prompt += `\n\n## Persona\n\n${attrs.persona}`;
35
51
  return prompt;
36
52
  }
@@ -1,12 +1,16 @@
1
- You are an assistant. You gather information, then either answer questions or take action.
1
+ You are an assistant. YOU MUST gather information, then YOU MAY either answer questions or take action.
2
2
 
3
3
  # Response Rules
4
4
 
5
- * You must register unknowns with <unknown>(thing I don't know yet)</unknown> before acting.
6
- * Save known information with <known>(thing I know now)</known>.
7
- * Respond with Tool Commands. You may use multiple tools in your response.
5
+ Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use multiple tools in your response.
6
+ Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
7
+ Required: YOU MUST register all unknowns with <unknown>(specific thing I need to learn)</unknown>.
8
+ Required: YOU MUST register all new information, decisions, and plans with <known>(specific information, ideas, or plans)</known>.
9
+ Required: YOU MUST conclude every turn with EITHER <update/> if still working OR <summarize/> if done. Never both.
10
+ Required: Path and summary information is approximate. YOU MUST use <get> to verify before acting on summarized content.
11
+ Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
12
+ Info: Your context is limited but your storage is not. Organize and categorize your information, ideas, plans, and history to optimize your context.
8
13
 
9
14
  # Tool Commands
10
15
 
11
16
  Tools: [%TOOLS%]
12
- Required: Either `<update/>` if still working or `<summarize/>` if done. Never both.
@@ -1,18 +1,21 @@
1
1
  # known
2
2
 
3
- Writes arbitrary key/value entries into the store at full fidelity.
3
+ Writes knowledge entries into the store at full fidelity.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `known`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Upserts the entry body at the target path with `full` state.
8
+ - **Category**: `data`
9
+ - **Handler**: Upserts the entry body at the target path with status 200.
10
+ - **Filter**: `assembly.system` at priority 100 renders `<knowns>` section.
11
11
 
12
12
  ## Projection
13
13
 
14
- Shows `known {path}` followed by the entry body.
14
+ Shows `# known {path}` followed by the entry body.
15
15
 
16
- ## Behavior
16
+ ## Assembly
17
17
 
18
- The target path defaults to `entry.resultPath` but can be overridden via `attrs.path`. Used by the model to persist structured notes and context.
18
+ Filters turn_context rows where `category === "data"`. Renders all
19
+ data entries (files, knowledge, skills, URLs) into the `<knowns>` section
20
+ of the system message. Third-party plugins that register with
21
+ `category: "data"` automatically appear here.
@@ -1,28 +1,24 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./knownDoc.js";
2
2
 
3
3
  export default class Known {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({
9
- fidelity: "turn",
10
- validStates: ["full", "stored"],
11
- category: "knowledge",
12
- });
8
+ core.registerScheme({ category: "data" });
13
9
  core.on("handler", this.handler.bind(this));
14
10
  core.on("full", this.full.bind(this));
15
11
  core.filter("assembly.system", this.assembleKnown.bind(this), 100);
16
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
17
- core.filter("instructions.toolDocs", async (content) =>
18
- content ? `${content}\n\n${docs}` : docs,
19
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.known = docs;
14
+ return docsMap;
15
+ });
20
16
  }
21
17
 
22
18
  async handler(entry, rummy) {
23
19
  const { entries: store, sequence: turn, runId } = rummy;
24
20
  const target = entry.attributes.path || entry.resultPath;
25
- await store.upsert(runId, turn, target, entry.body, "full");
21
+ await store.upsert(runId, turn, target, entry.body, 200);
26
22
  }
27
23
 
28
24
  full(entry) {
@@ -30,28 +26,42 @@ export default class Known {
30
26
  }
31
27
 
32
28
  async assembleKnown(content, ctx) {
33
- const entries = ctx.rows.filter(
34
- (r) =>
35
- r.category === "file" ||
36
- r.category === "file_index" ||
37
- r.category === "known" ||
38
- r.category === "known_index",
39
- );
29
+ const entries = ctx.rows.filter((r) => r.category === "data");
40
30
  if (entries.length === 0) return content;
41
31
 
42
32
  // Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
43
- const lines = entries.map((e) => renderKnownTag(e));
33
+ const demotedSet = new Set(ctx.demoted || []);
34
+ const panic = ctx.type === "panic";
35
+ const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
44
36
  return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
45
37
  }
46
38
  }
47
39
 
48
- function renderKnownTag(entry) {
40
+ function renderKnownTag(entry, demotedSet, panic = false) {
41
+ const tag = entry.scheme || "file";
42
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
49
43
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
50
- const state = entry.state ? ` state="${entry.state}"` : "";
44
+ const status = entry.status ? ` status="${entry.status}"` : "";
45
+ const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
46
+ const flag = demotedSet?.has(entry.path) ? " demoted" : "";
47
+
48
+ // Panic mode: index-only view so context fits in LLM window
49
+ if (panic) {
50
+ return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
51
+ }
52
+
53
+ const attrs =
54
+ typeof entry.attributes === "string"
55
+ ? JSON.parse(entry.attributes)
56
+ : entry.attributes;
57
+ const summary =
58
+ typeof attrs?.summary === "string"
59
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
60
+ : "";
51
61
 
52
62
  if (entry.body) {
53
- return `<known path="${entry.path}"${state}${tokens}>${entry.body}</known>`;
63
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}>${entry.body}</${tag}>`;
54
64
  }
55
65
 
56
- return `<known path="${entry.path}"${state}${tokens}/>`;
66
+ return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}/>`;
57
67
  }
@@ -0,0 +1,33 @@
1
+ // Tool doc for <known/>. 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: body = the information to save
6
+ [
7
+ "## <known>[specific information, ideas, or plans]</known> - Sort and save what you learn for later recall",
8
+ ],
9
+ // --- Examples: summary-with-keywords first (teaches the right pattern)
10
+ [
11
+ 'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
12
+ "Primary pattern: comma-separated keywords in summary. Path auto-generated from summary as known://hedberg/comedian/death/2005. Keywords become searchable path segments.",
13
+ ],
14
+ [
15
+ 'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
16
+ "Explicit path form: slashed path=category/key, summary=keywords. For when the model wants direct control over taxonomy.",
17
+ ],
18
+ // --- Lifecycle
19
+ [
20
+ '* Recall with <get path="known://people/*">keyword</get>',
21
+ "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
22
+ ],
23
+ [
24
+ "* `summary` keywords survive compression — write keywords you'll search for later",
25
+ "Teaches WHY summaries matter. Keywords become the path AND the compressed view.",
26
+ ],
27
+ [
28
+ "* YOU MUST sort and save all new information, ideas, and plans in their own <known> entries",
29
+ "Critical behavioral constraint. 'new' prevents re-saving known facts.",
30
+ ],
31
+ ];
32
+
33
+ export default LINES.map(([text]) => text).join("\n");
@@ -5,9 +5,8 @@ Moves (renames) an entry from one path to another within the K/V store.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `mv`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Reads source body, writes to destination, removes source. K/V destinations resolve immediately (`pass`); file destinations produce a `proposed` entry.
8
+ - **Category**: `logging`
9
+ - **Handler**: Reads source body, writes to destination, removes source. Scheme destinations resolve immediately (status 200); file destinations produce status 202 (proposed).
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,6 @@ Shows `mv {from} {to}`.
15
14
 
16
15
  ## Behavior
17
16
 
18
- Warns if the destination already exists and will be overwritten. Uses `KnownStore.scheme()` to determine K/V vs file paths. Source entry is removed on successful K/V moves.
17
+ Warns if the destination already exists and will be overwritten. Uses
18
+ `KnownStore.scheme()` to determine scheme vs file paths. Source entry
19
+ is removed on successful scheme moves.