@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
@@ -1,26 +1,28 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
2
+ import docs from "./mvDoc.js";
3
3
 
4
4
  export default class Mv {
5
5
  #core;
6
6
 
7
7
  constructor(core) {
8
8
  this.#core = core;
9
- core.registerScheme({
10
- validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
11
- });
9
+ core.registerScheme();
12
10
  core.on("handler", this.handler.bind(this));
13
11
  core.on("full", this.full.bind(this));
14
12
  core.on("summary", this.summary.bind(this));
15
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
16
- core.filter("instructions.toolDocs", async (content) =>
17
- content ? `${content}\n\n${docs}` : docs,
18
- );
13
+ core.filter("instructions.toolDocs", async (docsMap) => {
14
+ docsMap.mv = docs;
15
+ return docsMap;
16
+ });
19
17
  }
20
18
 
21
19
  async handler(entry, rummy) {
22
- const { entries: store, sequence: turn, runId } = rummy;
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
23
21
  const { path, to } = entry.attributes;
22
+ const VALID = { stored: 1, summary: 1, index: 1, full: 1 };
23
+ const fidelity = VALID[entry.attributes.fidelity]
24
+ ? entry.attributes.fidelity
25
+ : undefined;
24
26
 
25
27
  const source = await store.getBody(runId, path);
26
28
  if (source === null) return;
@@ -34,14 +36,16 @@ export default class Mv {
34
36
 
35
37
  const body = `${path} ${to}`;
36
38
  if (destScheme === null) {
37
- await store.upsert(runId, turn, entry.resultPath, body, "proposed", {
39
+ await store.upsert(runId, turn, entry.resultPath, body, 202, {
38
40
  attributes: { from: path, to, isMove: true, warning },
41
+ loopId,
39
42
  });
40
43
  } else {
41
- await store.upsert(runId, turn, to, source, "full");
44
+ await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
42
45
  await store.remove(runId, path);
43
- await store.upsert(runId, turn, entry.resultPath, body, "pass", {
46
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
44
47
  attributes: { from: path, to, isMove: true, warning },
48
+ loopId,
45
49
  });
46
50
  }
47
51
  }
@@ -0,0 +1,31 @@
1
+ // Tool doc for <mv>. 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 = source, body = destination
6
+ [
7
+ '## <mv path="[source]">[destination]</mv> - Move or rename a file or entry',
8
+ ],
9
+
10
+ // --- Examples: entry rename and file move
11
+ [
12
+ 'Example: <mv path="known://active_task">known://completed_task</mv>',
13
+ "Entry rename. Most common mv use case. Shows known:// path convention.",
14
+ ],
15
+ [
16
+ 'Example: <mv path="src/old_name.js">src/new_name.js</mv>',
17
+ "File rename. Shows that mv works on files too, not just known entries.",
18
+ ],
19
+
20
+ // --- Constraints
21
+ [
22
+ "* Source path accepts globs for batch moves",
23
+ "Pattern support consistent with get/cp/rm.",
24
+ ],
25
+ [
26
+ "* In ask mode, destination MUST be a scheme path (not a file)",
27
+ "Mode constraint. Prevents file mutations in ask mode via mv.",
28
+ ],
29
+ ];
30
+
31
+ export default LINES.map(([text]) => text).join("\n");
@@ -0,0 +1,78 @@
1
+ import fs from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export default class Persona {
5
+ #core;
6
+
7
+ constructor(core) {
8
+ this.#core = core;
9
+ const r = core.hooks.rpc.registry;
10
+
11
+ r.register("persona/set", {
12
+ handler: async (params, ctx) => {
13
+ if (!params.run) throw new Error("run is required");
14
+
15
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
16
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
17
+
18
+ let text = params.text;
19
+ if (params.name && !text) {
20
+ text = await loadFile(params.name);
21
+ }
22
+
23
+ await ctx.db.update_run_config.run({
24
+ id: runRow.id,
25
+ temperature: null,
26
+ persona: text || null,
27
+ context_limit: null,
28
+ model: null,
29
+ });
30
+
31
+ return { status: "ok" };
32
+ },
33
+ description:
34
+ "Set persona on a run. Pass name or text. Pass neither to clear.",
35
+ params: {
36
+ run: "string — run alias",
37
+ name: "string? — persona filename (without .md)",
38
+ text: "string? — raw persona text (overrides name)",
39
+ },
40
+ requiresInit: true,
41
+ });
42
+
43
+ r.register("listPersonas", {
44
+ handler: async () => {
45
+ const dir = configDir();
46
+ if (!dir) return [];
47
+ try {
48
+ const files = await fs.readdir(dir);
49
+ return files
50
+ .filter((f) => f.endsWith(".md"))
51
+ .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
52
+ } catch {
53
+ return [];
54
+ }
55
+ },
56
+ description: "List available persona files. Returns [{ name, path }].",
57
+ requiresInit: true,
58
+ });
59
+ }
60
+ }
61
+
62
+ function configDir() {
63
+ const home = process.env.RUMMY_HOME;
64
+ if (home) return join(home, "personas");
65
+ return null;
66
+ }
67
+
68
+ async function loadFile(name) {
69
+ const dir = configDir();
70
+ if (!dir) throw new Error("RUMMY_HOME not configured");
71
+ const path = join(dir, `${name}.md`);
72
+ try {
73
+ return await fs.readFile(path, "utf8");
74
+ } catch (err) {
75
+ if (err.code === "ENOENT") throw new Error(`Not found: ${path}`);
76
+ throw err;
77
+ }
78
+ }
@@ -10,6 +10,6 @@ history from prior ask/act invocations on this run.
10
10
 
11
11
  ## Behavior
12
12
 
13
- Filters turn_context rows where `category` is `result` or `structural`
13
+ Filters turn_context rows where `category` is `logging` or `prompt`
14
14
  and `source_turn < loopStartTurn`. Renders each entry chronologically
15
- with status symbols (✓/✗/·).
15
+ with turn number and status.
@@ -11,32 +11,36 @@ export default class Previous {
11
11
 
12
12
  const entries = ctx.rows.filter(
13
13
  (r) =>
14
- (r.category === "result" || r.category === "structural") &&
14
+ (r.category === "logging" || r.category === "prompt") &&
15
15
  r.source_turn < ctx.loopStartTurn,
16
16
  );
17
17
  if (entries.length === 0) return content;
18
18
 
19
19
  const lines = await Promise.all(
20
- entries.map((e) => renderToolTag(e, "summary", this.#core)),
20
+ entries.map((e) => renderToolTag(e, this.#core)),
21
21
  );
22
22
  return `${content}\n\n<previous>\n${lines.join("\n")}\n</previous>`;
23
23
  }
24
24
  }
25
25
 
26
- async function renderToolTag(entry, fidelity, core) {
26
+ async function renderToolTag(entry, core) {
27
27
  const attrs =
28
28
  typeof entry.attributes === "string"
29
29
  ? JSON.parse(entry.attributes)
30
30
  : entry.attributes;
31
31
 
32
- const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
33
- const status = entry.state ? ` status="${entry.state}"` : "";
32
+ const target = attrs?.path || attrs?.file || attrs?.command || "";
33
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
34
+ const status = entry.status ? ` status="${entry.status}"` : "";
35
+ const summary =
36
+ typeof attrs?.summary === "string"
37
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
38
+ : "";
34
39
 
35
40
  let body;
36
41
  try {
37
42
  body = await core.hooks.tools.view(entry.scheme, {
38
43
  ...entry,
39
- fidelity,
40
44
  attributes: attrs,
41
45
  });
42
46
  } catch {
@@ -44,7 +48,7 @@ async function renderToolTag(entry, fidelity, core) {
44
48
  }
45
49
 
46
50
  if (body) {
47
- return `<tool path="${path}"${status}>${body}</tool>`;
51
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}>${body}</${entry.scheme}>`;
48
52
  }
49
- return `<tool path="${path}"${status}/>`;
53
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}/>`;
50
54
  }
@@ -7,31 +7,63 @@ export default class Progress {
7
7
  }
8
8
 
9
9
  async assembleProgress(content, ctx) {
10
- const usedTokens = ctx.rows.reduce((sum, r) => sum + (r.tokens || 0), 0);
10
+ // Use last turn's real assembled token count when available.
11
+ // Falls back to row token sum (less accurate — missing system prompt overhead).
12
+ const rowTokens = ctx.rows.reduce((sum, r) => sum + (r.tokens || 0), 0);
13
+ const usedTokens = ctx.lastContextTokens || rowTokens;
11
14
  const contextSize = ctx.contextSize || 0;
12
15
  const pct = contextSize ? Math.round((usedTokens / contextSize) * 100) : 0;
13
16
 
17
+ // Fidelity distribution across known/file entries
18
+ const entries = ctx.rows.filter((r) => r.category === "data");
19
+ const fullEntries = entries.filter((r) => r.fidelity === "full");
20
+ const summaryEntries = entries.filter((r) => r.fidelity === "summary");
21
+ const indexEntries = entries.filter((r) => r.fidelity === "index");
22
+ const fullTokens = fullEntries.reduce((s, r) => s + (r.tokens || 0), 0);
23
+ const summaryTokens = summaryEntries.reduce(
24
+ (s, r) => s + (r.tokens || 0),
25
+ 0,
26
+ );
27
+ const indexTokens = indexEntries.reduce((s, r) => s + (r.tokens || 0), 0);
28
+
14
29
  const unknownCount = ctx.rows.filter(
15
30
  (r) => r.category === "unknown",
16
31
  ).length;
17
32
 
18
33
  const hasCurrent = ctx.rows.some(
19
- (r) =>
20
- (r.category === "result" || r.category === "structural") &&
21
- r.source_turn >= ctx.loopStartTurn,
34
+ (r) => r.category === "logging" && r.source_turn >= ctx.loopStartTurn,
22
35
  );
23
36
 
24
37
  const parts = [];
25
38
 
26
- const tokenInfo = contextSize
27
- ? `${usedTokens} of ${contextSize} tokens (${pct}%)`
39
+ const knownCount = entries.length;
40
+ const tokenLine = contextSize
41
+ ? `${usedTokens} of ${contextSize} tokens (${pct}%) · ${knownCount} known${knownCount !== 1 ? "s" : ""} · ${unknownCount} unknown${unknownCount !== 1 ? "s" : ""}`
28
42
  : "";
29
- const unknownInfo =
30
- unknownCount > 0
31
- ? `${unknownCount} unknown${unknownCount > 1 ? "s" : ""} remaining`
32
- : "0 unknowns";
33
- const status = [tokenInfo, unknownInfo].filter(Boolean).join(" · ");
34
- if (status) parts.push(status);
43
+ if (tokenLine) parts.push(tokenLine);
44
+
45
+ // Fidelity distribution
46
+ const fidelityParts = [];
47
+ if (fullEntries.length > 0)
48
+ fidelityParts.push(`${fullEntries.length} full (${fullTokens} tok)`);
49
+ if (summaryEntries.length > 0)
50
+ fidelityParts.push(
51
+ `${summaryEntries.length} summary (${summaryTokens} tok)`,
52
+ );
53
+ if (indexEntries.length > 0)
54
+ fidelityParts.push(`${indexEntries.length} index (${indexTokens} tok)`);
55
+ if (fidelityParts.length > 0)
56
+ parts.push(`Entries: ${fidelityParts.join(" · ")}`);
57
+
58
+ if (pct > 75) {
59
+ parts.push(
60
+ 'Context above 75%. YOU MUST free space: <set fidelity="summary" summary="topic,detail,keyword"/>, <set fidelity="archive"/>, or <rm/>. Target the largest entries.',
61
+ );
62
+ } else if (pct > 50) {
63
+ parts.push(
64
+ 'Context above 50%. You may free space: <set fidelity="summary" summary="topic,detail,keyword"/>, <set fidelity="archive"/>, or <rm/>.',
65
+ );
66
+ }
35
67
 
36
68
  if (hasCurrent) {
37
69
  parts.push(
@@ -1,6 +1,6 @@
1
1
  # prompt
2
2
 
3
- Renders the `<ask>` or `<act>` tag at the end of the user message.
3
+ Renders the `<prompt mode="ask|act">` tag at the end of the user message.
4
4
  Always present on every turn — the model always sees its task.
5
5
 
6
6
  ## Registration
@@ -9,8 +9,8 @@ Always present on every turn — the model always sees its task.
9
9
 
10
10
  ## Behavior
11
11
 
12
- Finds the latest `ask://` or `act://` entry in the turn_context rows.
13
- Renders with `tools` attribute (available tool list) and optional `warn`
14
- attribute in ask mode ("File and system modification prohibited on this
15
- turn."). Falls back to the mode passed by the core if no prompt entry
12
+ Finds the latest `prompt://` entry in the turn_context rows. The mode
13
+ (`ask` or `act`) is stored in `attributes.mode`. Renders with `tools`
14
+ attribute (available tool list) and optional `warn` attribute in ask
15
+ mode. Falls back to the mode passed by the core if no prompt entry
16
16
  exists.
@@ -3,43 +3,47 @@ export default class Prompt {
3
3
 
4
4
  constructor(core) {
5
5
  this.#core = core;
6
+ core.hooks.tools.onView("prompt", (entry) => entry.body);
7
+ core.hooks.tools.onView("progress", (entry) => entry.body);
6
8
  core.on("turn.started", this.onTurnStarted.bind(this));
7
9
  core.filter("assembly.user", this.assemblePrompt.bind(this), 300);
8
10
  }
9
11
 
10
12
  async onTurnStarted({ rummy, mode, prompt, isContinuation }) {
11
- const { entries: store, sequence: turn, runId } = rummy;
13
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
12
14
 
13
15
  if (!isContinuation && prompt) {
14
- await store.upsert(runId, turn, `prompt://${turn}`, "", "info", {
16
+ await store.upsert(runId, turn, `prompt://${turn}`, prompt, 200, {
15
17
  attributes: { mode },
18
+ loopId,
16
19
  });
17
- await store.upsert(runId, turn, `${mode}://${turn}`, prompt, "info");
18
20
  } else {
19
- await store.upsert(
20
- runId,
21
- turn,
22
- `progress://${turn}`,
23
- prompt || "",
24
- "info",
25
- );
21
+ await store.upsert(runId, turn, `progress://${turn}`, prompt || "", 200, {
22
+ loopId,
23
+ });
26
24
  }
27
25
  }
28
26
 
29
27
  async assemblePrompt(content, ctx) {
30
28
  const promptEntry = ctx.rows.findLast(
31
- (r) =>
32
- r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
29
+ (r) => r.category === "prompt" && r.scheme === "prompt",
33
30
  );
34
31
 
35
- const mode = promptEntry?.scheme || ctx.type;
32
+ const attrs =
33
+ typeof promptEntry?.attributes === "string"
34
+ ? JSON.parse(promptEntry.attributes)
35
+ : promptEntry?.attributes;
36
+ const mode = attrs?.mode || ctx.type;
36
37
  const body = promptEntry?.body || "";
37
- const tools = this.#core.hooks.tools.namesForMode(mode).join(", ");
38
- const warn =
39
- mode === "ask"
40
- ? ' warn="File and system modification prohibited on this turn."'
41
- : "";
38
+ const toolNames = ctx.toolSet
39
+ ? [...ctx.toolSet]
40
+ : [...this.#core.hooks.tools.resolveForLoop(mode)];
41
+ const tools = toolNames.join(",");
42
+ let warn = "";
43
+ if (mode === "ask") warn = ' warn="File editing disallowed."';
44
+ if (mode === "panic")
45
+ warn = ' warn="Context overflow. Free space to continue."';
42
46
 
43
- return `${content}<${mode} tools="${tools}"${warn}>${body}</${mode}>`;
47
+ return `${content}<prompt mode="${mode}" tools="${tools}"${warn}>${body}</prompt>`;
44
48
  }
45
49
  }
@@ -5,9 +5,8 @@ Removes entries by path or glob pattern.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `rm`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Matches entries by pattern. K/V entries are removed immediately (`pass`); file entries produce `proposed` state for client approval.
8
+ - **Category**: `logging`
9
+ - **Handler**: Matches entries by pattern. Scheme entries are removed immediately (status 200); file entries produce status 202 (proposed) for client approval.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows `rm {path}`.
15
14
 
16
15
  ## Behavior
17
16
 
18
- Supports glob patterns and body filters via `getEntriesByPattern`. Each matched entry is processed independently.
17
+ Supports glob patterns and body filters via `getEntriesByPattern`. Each
18
+ matched entry is processed independently.
@@ -1,41 +1,58 @@
1
- import { readFileSync } from "node:fs";
1
+ import KnownStore from "../../agent/KnownStore.js";
2
+ import docs from "./rmDoc.js";
2
3
 
3
4
  export default class Rm {
4
5
  #core;
5
6
 
6
7
  constructor(core) {
7
8
  this.#core = core;
8
- core.registerScheme({
9
- validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
10
- });
9
+ core.registerScheme();
11
10
  core.on("handler", this.handler.bind(this));
12
11
  core.on("full", this.full.bind(this));
13
12
  core.on("summary", this.summary.bind(this));
14
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
15
- core.filter("instructions.toolDocs", async (content) =>
16
- content ? `${content}\n\n${docs}` : docs,
17
- );
13
+ core.filter("instructions.toolDocs", async (docsMap) => {
14
+ docsMap.rm = docs;
15
+ return docsMap;
16
+ });
18
17
  }
19
18
 
20
19
  async handler(entry, rummy) {
21
- const { entries: store, sequence: turn, runId } = rummy;
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
22
21
  const target = entry.attributes.path;
22
+ if (!target) {
23
+ await store.upsert(runId, turn, entry.resultPath, "", 400, {
24
+ attributes: { error: "path is required" },
25
+ loopId,
26
+ });
27
+ return;
28
+ }
29
+ const normalized = KnownStore.normalizePath(target);
23
30
  const matches = await store.getEntriesByPattern(
24
31
  runId,
25
- target,
32
+ normalized,
26
33
  entry.attributes.body,
27
34
  );
28
35
 
36
+ if (matches.length === 0) {
37
+ await store.upsert(runId, turn, entry.resultPath, "", 404, {
38
+ attributes: { path: target, error: `${target} not found` },
39
+ loopId,
40
+ });
41
+ return;
42
+ }
43
+
29
44
  for (const match of matches) {
30
45
  const resultPath = `rm://${match.path}`;
31
46
  if (match.scheme === null) {
32
- await store.upsert(runId, turn, resultPath, match.path, "proposed", {
47
+ await store.upsert(runId, turn, resultPath, match.path, 202, {
33
48
  attributes: { path: match.path },
49
+ loopId,
34
50
  });
35
51
  } else {
36
52
  await store.remove(runId, match.path);
37
- await store.upsert(runId, turn, resultPath, match.path, "pass", {
53
+ await store.upsert(runId, turn, resultPath, match.path, 200, {
38
54
  attributes: { path: match.path },
55
+ loopId,
39
56
  });
40
57
  }
41
58
  }
@@ -0,0 +1,30 @@
1
+ // Tool doc for <rm>. 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, self-closing
6
+ ['## <rm path="[path]"/> - Remove a file or entry'],
7
+
8
+ // --- Examples: file, known (with slug path), preview safety
9
+ ['Example: <rm path="src/config.js"/>', "File removal. Simplest form."],
10
+ [
11
+ 'Example: <rm path="known://donald-rumsfeld-was-born-in-1932"/>',
12
+ "Shows the slugified path convention. Model sees these paths in <knowns> section.",
13
+ ],
14
+ [
15
+ 'Example: <rm path="known://temp_*" preview/>',
16
+ "Preview before deleting. Glob pattern. Safety pattern for bulk operations.",
17
+ ],
18
+
19
+ // --- Constraints
20
+ [
21
+ '* Permanent. Prefer <set fidelity="archive"/> to preserve for later retrieval',
22
+ "Nudges toward archive over rm. Archive keeps the key; rm deletes permanently.",
23
+ ],
24
+ [
25
+ "* Paths accept globs — use `preview` to check matches first",
26
+ "Reinforces preview safety pattern. Prevents accidental bulk deletion.",
27
+ ],
28
+ ];
29
+
30
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,45 +1,32 @@
1
1
  # rpc
2
2
 
3
- Registers all core RPC methods and dispatches client operations through the tool handler chain.
3
+ Registers core RPC methods and provides automatic tool dispatch for
4
+ all registered tools.
4
5
 
5
6
  ## Registration
6
7
 
7
- - **No tool handler** — this plugin registers RPC methods on `hooks.rpc.registry`, not tool handlers.
8
+ - **No tool handler** — registers RPC methods on `hooks.rpc.registry`.
9
+ - **Tool fallback** — any registered tool is automatically callable via
10
+ RPC without explicit registration. Third-party plugins get RPC for free.
8
11
 
9
12
  ## RPC Methods
10
13
 
11
14
  ### Protocol
12
- - `ping` — liveness check.
13
- - `discover` — returns method/notification catalog.
14
- - `init` — initialize project (sets projectId, projectRoot, configPath).
15
+ - `ping`, `discover`, `init`
15
16
 
16
17
  ### Models
17
- - `getModels`, `addModel`, `removeModel` — CRUD for model aliases.
18
+ - `getModels`, `addModel`, `removeModel`
18
19
 
19
- ### Entry Operations
20
- - `read` — promote entry to full state, or create persistent file constraint.
21
- - `store` — demote entry to stored state, or manage file constraints (ignore/clear).
22
- - `write` create/update entry. K/V paths write directly; file paths dispatch through `set` handler.
23
- - `delete` — remove entry via `rm` handler dispatch.
20
+ ### Entry Operations (all dispatch through tool handler chain)
21
+ - `get` — promote entry; with `persist` flag, also sets file constraint.
22
+ - `store` — demote entry or manage file constraints (not a model tool).
23
+ - All other registered tools auto-dispatched via tool fallback.
24
24
  - `getEntries` — query entries by glob pattern.
25
25
 
26
26
  ### Runs
27
- - `startRun` — pre-create a run with model/config.
28
- - `ask` non-mutating model query.
29
- - `act` — mutating model directive.
30
- - `run/resolve` — resolve a proposed entry (accept/reject).
31
- - `run/abort` — abort an in-flight run.
32
- - `run/rename` — rename a run alias.
33
- - `run/inject` — inject a message into an idle or active run.
34
- - `run/config` — update run parameters (temperature, persona, context_limit, model).
35
- - `getRuns`, `getRun` — query run list and full run detail.
27
+ - `startRun`, `ask`, `act`
28
+ - `run/resolve`, `run/abort`, `run/rename`, `run/inject`, `run/config`
29
+ - `getRuns`, `getRun`
36
30
 
37
31
  ### Notifications
38
- - `run/state` turn state update with history, unknowns, proposed, telemetry.
39
- - `run/progress` — turn status (thinking/processing).
40
- - `ui/render` — streaming output.
41
- - `ui/notify` — toast notification.
42
-
43
- ## Behavior
44
-
45
- Client operations (read, write, delete, store) build a `RummyContext` for the target run and dispatch through the same handler chain as model operations via `dispatchTool`.
32
+ - `run/state`, `run/progress`, `ui/render`, `ui/notify`