@possumtech/rummy 0.2.7 → 0.2.8

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 (59) hide show
  1. package/.env.example +1 -2
  2. package/PLUGINS.md +105 -82
  3. package/migrations/001_initial_schema.sql +53 -68
  4. package/package.json +4 -6
  5. package/service.js +1 -1
  6. package/src/agent/AgentLoop.js +91 -58
  7. package/src/agent/ContextAssembler.js +2 -2
  8. package/src/agent/KnownStore.js +30 -11
  9. package/src/agent/ProjectAgent.js +1 -3
  10. package/src/agent/TurnExecutor.js +119 -31
  11. package/src/agent/XmlParser.js +20 -0
  12. package/src/agent/known_checks.sql +5 -4
  13. package/src/agent/known_queries.sql +4 -3
  14. package/src/agent/known_store.sql +29 -15
  15. package/src/agent/loops.sql +63 -0
  16. package/src/agent/runs.sql +7 -7
  17. package/src/agent/schemes.sql +2 -2
  18. package/src/agent/turns.sql +3 -3
  19. package/src/hooks/PluginContext.js +1 -10
  20. package/src/hooks/RummyContext.js +16 -8
  21. package/src/plugins/ask_user/ask_user.js +3 -2
  22. package/src/plugins/cp/cp.js +7 -7
  23. package/src/plugins/current/current.js +3 -4
  24. package/src/plugins/engine/engine.sql +5 -3
  25. package/src/plugins/engine/turn_context.sql +9 -4
  26. package/src/plugins/env/docs.md +2 -0
  27. package/src/plugins/env/env.js +3 -2
  28. package/src/plugins/file/file.js +9 -19
  29. package/src/plugins/get/docs.md +7 -3
  30. package/src/plugins/get/get.js +22 -6
  31. package/src/plugins/hedberg/docs.md +0 -9
  32. package/src/plugins/hedberg/hedberg.js +2 -5
  33. package/src/plugins/hedberg/matcher.js +1 -1
  34. package/src/plugins/hedberg/patterns.js +6 -6
  35. package/src/plugins/helpers.js +2 -2
  36. package/src/plugins/index.js +28 -15
  37. package/src/plugins/instructions/instructions.js +1 -1
  38. package/src/plugins/known/known.js +9 -11
  39. package/src/plugins/mv/mv.js +7 -7
  40. package/src/plugins/previous/previous.js +6 -5
  41. package/src/plugins/progress/progress.js +6 -0
  42. package/src/plugins/prompt/prompt.js +9 -10
  43. package/src/plugins/rm/docs.md +3 -1
  44. package/src/plugins/rm/rm.js +24 -7
  45. package/src/plugins/rpc/rpc.js +33 -42
  46. package/src/plugins/set/docs.md +3 -1
  47. package/src/plugins/set/set.js +22 -16
  48. package/src/plugins/sh/sh.js +3 -2
  49. package/src/plugins/skills/skills.js +3 -4
  50. package/src/plugins/store/docs.md +2 -1
  51. package/src/plugins/store/store.js +14 -3
  52. package/src/plugins/summarize/summarize.js +1 -1
  53. package/src/plugins/telemetry/telemetry.js +17 -7
  54. package/src/plugins/unknown/unknown.js +3 -2
  55. package/src/plugins/update/update.js +1 -1
  56. package/src/server/ClientConnection.js +3 -5
  57. package/src/sql/v_model_context.sql +20 -23
  58. package/src/sql/v_run_log.sql +3 -3
  59. package/src/agent/prompt_queue.sql +0 -39
@@ -51,6 +51,10 @@ export default class RummyContext {
51
51
  return this.#context.turnId || null;
52
52
  }
53
53
 
54
+ get loopId() {
55
+ return this.#context.loopId || null;
56
+ }
57
+
54
58
  get noContext() {
55
59
  return this.#context.noContext === true;
56
60
  }
@@ -85,7 +89,7 @@ export default class RummyContext {
85
89
 
86
90
  // --- Tool methods (same operations the model uses) ---
87
91
 
88
- async set({ path, body, state = "full", attributes } = {}) {
92
+ async set({ path, body, status = 200, attributes } = {}) {
89
93
  if (!path) {
90
94
  const slugify = (await import("../sql/functions/slugify.js")).default;
91
95
  const base = slugify(body || "");
@@ -96,8 +100,8 @@ export default class RummyContext {
96
100
  this.sequence,
97
101
  path,
98
102
  body || "",
99
- state,
100
- attributes ? { attributes } : undefined,
103
+ status,
104
+ { attributes, loopId: this.loopId },
101
105
  );
102
106
  return path;
103
107
  }
@@ -117,14 +121,18 @@ export default class RummyContext {
117
121
  async mv(from, to) {
118
122
  const body = await this.entries.getBody(this.runId, from);
119
123
  if (body === null) return;
120
- await this.entries.upsert(this.runId, this.sequence, to, body, "full");
124
+ await this.entries.upsert(this.runId, this.sequence, to, body, 200, {
125
+ loopId: this.loopId,
126
+ });
121
127
  await this.entries.remove(this.runId, from);
122
128
  }
123
129
 
124
130
  async cp(from, to) {
125
131
  const body = await this.entries.getBody(this.runId, from);
126
132
  if (body === null) return;
127
- await this.entries.upsert(this.runId, this.sequence, to, body, "full");
133
+ await this.entries.upsert(this.runId, this.sequence, to, body, 200, {
134
+ loopId: this.loopId,
135
+ });
128
136
  }
129
137
 
130
138
  // --- Plugin-only methods (superset) ---
@@ -137,9 +145,9 @@ export default class RummyContext {
137
145
  return this.entries.getAttributes(this.runId, path);
138
146
  }
139
147
 
140
- async getState(path) {
148
+ async getStatus(path) {
141
149
  const row = await this.entries.getState(this.runId, path);
142
- return row?.state ?? null;
150
+ return row?.status ?? null;
143
151
  }
144
152
 
145
153
  async getEntry(path) {
@@ -161,7 +169,7 @@ export default class RummyContext {
161
169
 
162
170
  async log(message) {
163
171
  const path = `content://${Date.now()}`;
164
- await this.entries.upsert(this.runId, this.sequence, path, message, "info");
172
+ await this.entries.upsert(this.runId, this.sequence, path, message, 200);
165
173
  }
166
174
 
167
175
  // --- Node tree methods ---
@@ -16,7 +16,7 @@ export default class AskUser {
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
19
- const { entries: store, sequence: turn, runId } = rummy;
19
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
20
20
  const { question, options: rawOptions } = entry.attributes;
21
21
 
22
22
  const optionText = rawOptions || entry.body || "";
@@ -28,8 +28,9 @@ export default class AskUser {
28
28
  .filter(Boolean)
29
29
  : [];
30
30
 
31
- await store.upsert(runId, turn, entry.resultPath, entry.body, "proposed", {
31
+ await store.upsert(runId, turn, entry.resultPath, entry.body, 202, {
32
32
  attributes: { question, options },
33
+ loopId,
33
34
  });
34
35
  }
35
36
 
@@ -6,9 +6,7 @@ export default class Cp {
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));
@@ -19,7 +17,7 @@ export default class Cp {
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;
24
22
 
25
23
  const source = await store.getBody(runId, path);
@@ -34,13 +32,15 @@ export default class Cp {
34
32
 
35
33
  const body = `${path} ${to}`;
36
34
  if (destScheme === null) {
37
- await store.upsert(runId, turn, entry.resultPath, body, "proposed", {
35
+ await store.upsert(runId, turn, entry.resultPath, body, 202, {
38
36
  attributes: { from: path, to, isMove: false, warning },
37
+ loopId,
39
38
  });
40
39
  } else {
41
- await store.upsert(runId, turn, to, source, "full");
42
- await store.upsert(runId, turn, entry.resultPath, body, "pass", {
40
+ await store.upsert(runId, turn, to, source, 200, { loopId });
41
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
43
42
  attributes: { from: path, to, isMove: false, warning },
43
+ loopId,
44
44
  });
45
45
  }
46
46
  }
@@ -15,26 +15,25 @@ export default class Current {
15
15
  if (entries.length === 0) return content;
16
16
 
17
17
  const lines = await Promise.all(
18
- entries.map((e) => renderToolTag(e, "full", this.#core)),
18
+ entries.map((e) => renderToolTag(e, this.#core)),
19
19
  );
20
20
  return `${content}<current>\n${lines.join("\n")}\n</current>\n`;
21
21
  }
22
22
  }
23
23
 
24
- async function renderToolTag(entry, fidelity, core) {
24
+ async function renderToolTag(entry, core) {
25
25
  const attrs =
26
26
  typeof entry.attributes === "string"
27
27
  ? JSON.parse(entry.attributes)
28
28
  : entry.attributes;
29
29
 
30
30
  const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
31
- const status = entry.state ? ` status="${entry.state}"` : "";
31
+ const status = entry.status ? ` status="${entry.status}"` : "";
32
32
 
33
33
  let body;
34
34
  try {
35
35
  body = await core.hooks.tools.view(entry.scheme, {
36
36
  ...entry,
37
- fidelity,
38
37
  attributes: attrs,
39
38
  });
40
39
  } catch {
@@ -1,10 +1,12 @@
1
1
  -- PREP: get_promoted_entries
2
- SELECT ke.path, ke.scheme, ke.state, ke.turn, ke.tokens, ke.refs
2
+ SELECT
3
+ ke.path, ke.scheme, ke.status, ke.fidelity, ke.turn
4
+ , ke.tokens, ke.refs
3
5
  FROM known_entries AS ke
4
6
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
5
7
  WHERE
6
8
  ke.run_id = :run_id
7
- AND ke.state IN ('full', 'summary')
9
+ AND ke.fidelity IN ('full', 'summary')
8
10
  AND s.model_visible = 1
9
11
  ORDER BY ke.turn, ke.refs, ke.tokens DESC;
10
12
 
@@ -14,5 +16,5 @@ FROM known_entries AS ke
14
16
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
15
17
  WHERE
16
18
  ke.run_id = :run_id
17
- AND ke.state IN ('full', 'summary')
19
+ AND ke.fidelity IN ('full', 'summary')
18
20
  AND s.model_visible = 1;
@@ -4,22 +4,27 @@ WHERE run_id = :run_id AND turn = :turn;
4
4
 
5
5
  -- PREP: get_model_context
6
6
  SELECT
7
- ordinal, path, scheme, fidelity, state, body, tokens, attributes, category, turn
7
+ ordinal, path, scheme, fidelity, status, body
8
+ , tokens, attributes, category, turn
8
9
  FROM v_model_context
9
10
  WHERE run_id = :run_id
10
11
  ORDER BY ordinal;
11
12
 
12
13
  -- PREP: insert_turn_context
13
14
  INSERT INTO turn_context (
14
- run_id, turn, ordinal, path, fidelity, state, body, tokens, attributes, category, source_turn
15
+ run_id, loop_id, turn, ordinal, path, fidelity, status
16
+ , body, tokens, attributes, category, source_turn
15
17
  )
16
18
  VALUES (
17
- :run_id, :turn, :ordinal, :path, :fidelity, :state, :body, :tokens
19
+ :run_id, :loop_id, :turn, :ordinal, :path, :fidelity
20
+ , :status, :body, :tokens
18
21
  , COALESCE(:attributes, '{}'), :category, :source_turn
19
22
  );
20
23
 
21
24
  -- PREP: get_turn_context
22
- SELECT ordinal, path, scheme, fidelity, state, body, tokens, attributes, category, source_turn
25
+ SELECT
26
+ ordinal, path, scheme, fidelity, status, body
27
+ , tokens, attributes, category, source_turn
23
28
  FROM turn_context
24
29
  WHERE run_id = :run_id AND turn = :turn
25
30
  ORDER BY ordinal;
@@ -1,2 +1,4 @@
1
1
  ## <env>[command]</env> - Run an exploratory shell command
2
2
  Example: <env>npm --version</env>
3
+ * Do not use <env/> to read or list files — use <get path="*" preview/> instead
4
+ * For commands with side effects, use <sh/> instead
@@ -16,9 +16,10 @@ export default class Env {
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
19
- const { entries: store, sequence: turn, runId } = rummy;
20
- await store.upsert(runId, turn, entry.resultPath, entry.body, "pass", {
19
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
20
+ await store.upsert(runId, turn, entry.resultPath, entry.body, 202, {
21
21
  attributes: entry.attributes,
22
+ loopId,
22
23
  });
23
24
  }
24
25
 
@@ -5,23 +5,9 @@ export default class File {
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({
9
- fidelity: "turn",
10
- validStates: ["full", "summary", "index", "stored"],
11
- category: "file",
12
- });
13
- core.registerScheme({
14
- name: "http",
15
- fidelity: "turn",
16
- validStates: ["full", "summary", "stored"],
17
- category: "file",
18
- });
19
- core.registerScheme({
20
- name: "https",
21
- fidelity: "turn",
22
- validStates: ["full", "summary", "stored"],
23
- category: "file",
24
- });
8
+ core.registerScheme({ category: "file" });
9
+ core.registerScheme({ name: "http", category: "file" });
10
+ core.registerScheme({ name: "https", category: "file" });
25
11
  core.on("full", this.full.bind(this));
26
12
 
27
13
  // Register identity projections for schemes that just pass through body
@@ -50,8 +36,12 @@ export default class File {
50
36
  visibility,
51
37
  });
52
38
 
53
- if (visibility === "ignore") {
54
- const runs = await db.get_all_runs.all({ project_id: projectId });
39
+ const runs = await db.get_all_runs.all({ project_id: projectId });
40
+ if (visibility === "active") {
41
+ for (const run of runs) {
42
+ await knownStore.promoteByPattern(run.id, path, null, 0);
43
+ }
44
+ } else if (visibility === "ignore") {
55
45
  for (const run of runs) {
56
46
  await knownStore.demoteByPattern(run.id, path, null);
57
47
  }
@@ -1,6 +1,10 @@
1
1
  ## <get>[path/to/file]</get> - Load a file or entry into context
2
2
  Example: <get>docs/example.txt</get>
3
3
  Example: <get>known://auth_flow</get>
4
- * Entries at state="index" or state="summary" are promoted to full content.
5
- * Use "known://" paths to recall stored information.
6
- * When irrelevant or resolved, use <store/> to remove from context.
4
+ Example: <get path="src/**/*.js" preview/> (list matching files without loading)
5
+ Example: <get path="src/*.js" body="TODO" preview/> (find files containing TODO)
6
+ * Paths accept globs: `src/**/*.js`, `known://api_*`
7
+ * Adding `preview` shows matches without loading into context
8
+ * Use `body` attribute to filter by content
9
+ * Use "known://" paths to recall stored information
10
+ * When irrelevant or resolved, use <store/> to remove from context
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import KnownStore from "../../agent/KnownStore.js";
2
3
  import { storePatternResult } from "../helpers.js";
3
4
 
4
5
  export default class Get {
@@ -6,7 +7,7 @@ export default class Get {
6
7
 
7
8
  constructor(core) {
8
9
  this.#core = core;
9
- core.registerScheme({ validStates: ["full", "read", "pattern"] });
10
+ core.registerScheme();
10
11
  core.on("handler", this.handler.bind(this));
11
12
  core.on("full", this.full.bind(this));
12
13
  core.on("summary", this.summary.bind(this));
@@ -17,12 +18,24 @@ export default class Get {
17
18
  }
18
19
 
19
20
  async handler(entry, rummy) {
20
- const { entries: store, sequence: turn, runId } = rummy;
21
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
21
22
  const target = entry.attributes.path;
23
+ if (!target) {
24
+ await store.upsert(runId, turn, entry.resultPath, "", 400, {
25
+ attributes: { error: "path is required" },
26
+ loopId,
27
+ });
28
+ return;
29
+ }
30
+ const normalized = KnownStore.normalizePath(target);
22
31
  const bodyFilter = entry.attributes.body || null;
23
- const isPattern = bodyFilter || target.includes("*");
24
- const matches = await store.getEntriesByPattern(runId, target, bodyFilter);
25
- await store.promoteByPattern(runId, target, bodyFilter, turn);
32
+ const isPattern = bodyFilter || normalized.includes("*");
33
+ const matches = await store.getEntriesByPattern(
34
+ runId,
35
+ normalized,
36
+ bodyFilter,
37
+ );
38
+ await store.promoteByPattern(runId, normalized, bodyFilter, turn);
26
39
 
27
40
  if (isPattern) {
28
41
  await storePatternResult(
@@ -33,13 +46,16 @@ export default class Get {
33
46
  target,
34
47
  bodyFilter,
35
48
  matches,
49
+ { loopId },
36
50
  );
37
51
  } else {
38
52
  const total = matches.reduce((s, m) => s + m.tokens_full, 0);
39
53
  const paths = matches.map((m) => m.path).join(", ");
40
54
  const body =
41
55
  matches.length > 0 ? `${paths} ${total} tokens` : `${target} not found`;
42
- await store.upsert(runId, turn, entry.resultPath, body, "read");
56
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
57
+ loopId,
58
+ });
43
59
  }
44
60
  }
45
61
 
@@ -1,9 +0,0 @@
1
- # Advanced Patterns
2
- * Paths accept globs: `src/**/*.js`, `known://api_*`
3
- * Body attributes filter by content: `<get path="src/*.js" body="TODO"/>`
4
- * Regex patterns use /slashes/: `<get path="/\.test\.js$/" preview/>`
5
- * Adding `preview` shows matches without making changes
6
- * Chain multiple replacements: `s/old/new/ s/foo/bar/`
7
- Example: <get path="src/**/*.js" body="TODO" preview/> (list js files containing TODO)
8
- Example: <store path="src/**/*.test.js"/> (store all test files)
9
- Example: <rm path="known://temp_*" preview/> (preview which temp entries would be deleted)
@@ -1,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { parseEditContent } from "./edits.js";
3
2
  import HeuristicMatcher, { generatePatch } from "./matcher.js";
4
3
  import { normalizeAttrs } from "./normalize.js";
@@ -34,10 +33,8 @@ export default class Hedberg {
34
33
  generatePatch,
35
34
  };
36
35
 
37
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
38
- core.filter("instructions.toolDocs", async (content) =>
39
- content ? `${content}\n\n${docs}` : docs,
40
- );
36
+ // Patterns documentation distributed to individual tool docs.
37
+ // Hedberg has no model-facing docs of its own.
41
38
  }
42
39
 
43
40
  /**
@@ -120,7 +120,7 @@ export default class HeuristicMatcher {
120
120
  patch: null,
121
121
  warning: null,
122
122
  error:
123
- "Could not find the SEARCH block in the file. Ensure you are providing an exact match of the existing code, without truncating lines with '...'.",
123
+ "SEARCH blocks are matched literally, not as a pattern. Could not find the SEARCH block in the file.",
124
124
  };
125
125
  }
126
126
 
@@ -1,4 +1,5 @@
1
- import { JSDOM } from "jsdom";
1
+ import { DOMParser } from "@xmldom/xmldom";
2
+ import xpath from "xpath";
2
3
 
3
4
  export const deterministic = true;
4
5
 
@@ -250,11 +251,10 @@ function compile(pattern) {
250
251
 
251
252
  function evalXpath(expr, string) {
252
253
  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;
254
+ const doc = new DOMParser().parseFromString(string, "text/xml");
255
+ const nodes = xpath.select(expr, doc);
256
+ if (!nodes || nodes.length === 0) return null;
257
+ const node = nodes[0];
258
258
  return { match: node.textContent, node };
259
259
  } catch {
260
260
  return null;
@@ -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
4
  import { basename, 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
  /**
@@ -41,14 +48,11 @@ const AUDIT_SCHEMES = [
41
48
  */
42
49
  export async function initPlugins(db, store, hooks) {
43
50
  for (const name of AUDIT_SCHEMES) {
44
- const scheme = {
51
+ await db.upsert_scheme.run({
45
52
  name,
46
- fidelity: ["ask", "act", "progress"].includes(name) ? "full" : "null",
47
53
  model_visible: ["ask", "act", "progress"].includes(name) ? 1 : 0,
48
- valid_states: JSON.stringify(["info"]),
49
54
  category: "audit",
50
- };
51
- await db.upsert_scheme.run(scheme);
55
+ });
52
56
  }
53
57
 
54
58
  for (const ctx of instances.values()) {
@@ -71,28 +75,37 @@ export async function initPlugins(db, store, hooks) {
71
75
  if (registered.has(toolName)) continue;
72
76
  await db.upsert_scheme.run({
73
77
  name: toolName,
74
- fidelity: "full",
75
78
  model_visible: 1,
76
- valid_states: JSON.stringify([
77
- "full",
78
- "proposed",
79
- "pass",
80
- "rejected",
81
- "error",
82
- "info",
83
- ]),
84
79
  category: "result",
85
80
  });
86
81
  }
87
82
  }
88
83
  }
89
84
 
85
+ function resolvePlugin(packageName) {
86
+ // Check local node_modules first, then global
87
+ const localDir = join(process.cwd(), "node_modules", packageName);
88
+ if (existsSync(join(localDir, "package.json"))) return localDir;
89
+ const globalDir = join(getGlobalPrefix(), "lib", "node_modules", packageName);
90
+ if (existsSync(join(globalDir, "package.json"))) return globalDir;
91
+ throw new Error(`Package '${packageName}' not found locally or globally`);
92
+ }
93
+
94
+ async function importPlugin(packageName) {
95
+ const dir = resolvePlugin(packageName);
96
+ const pkg = JSON.parse(
97
+ (await import("node:fs")).readFileSync(join(dir, "package.json"), "utf8"),
98
+ );
99
+ const entry = pkg.exports?.["."] || pkg.main || "index.js";
100
+ return import(pathToFileURL(join(dir, entry)).href);
101
+ }
102
+
90
103
  async function loadEnvPlugins(hooks) {
91
104
  for (const [key, value] of Object.entries(process.env)) {
92
105
  if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
93
106
  const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
94
107
  try {
95
- const { default: Plugin } = await import(value);
108
+ const { default: Plugin } = await importPlugin(value);
96
109
  if (typeof Plugin?.register === "function") {
97
110
  await Plugin.register(hooks);
98
111
  } else if (typeof Plugin === "function") {
@@ -17,7 +17,7 @@ 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", {
20
+ await store.upsert(runId, turn, "instructions://system", "", 200, {
21
21
  attributes: { persona: runRow?.persona || null },
22
22
  });
23
23
  }
@@ -5,11 +5,7 @@ export default class Known {
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: "knowledge" });
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);
@@ -22,7 +18,7 @@ export default class Known {
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) {
@@ -40,18 +36,20 @@ export default class Known {
40
36
  if (entries.length === 0) return content;
41
37
 
42
38
  // Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
43
- const lines = entries.map((e) => renderKnownTag(e));
39
+ const demotedSet = new Set(ctx.demoted || []);
40
+ const lines = entries.map((e) => renderKnownTag(e, demotedSet));
44
41
  return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
45
42
  }
46
43
  }
47
44
 
48
- function renderKnownTag(entry) {
45
+ function renderKnownTag(entry, demotedSet) {
49
46
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
50
- const state = entry.state ? ` state="${entry.state}"` : "";
47
+ const status = entry.status ? ` status="${entry.status}"` : "";
48
+ const flag = demotedSet?.has(entry.path) ? " demoted" : "";
51
49
 
52
50
  if (entry.body) {
53
- return `<known path="${entry.path}"${state}${tokens}>${entry.body}</known>`;
51
+ return `<known path="${entry.path}"${status}${tokens}${flag}>${entry.body}</known>`;
54
52
  }
55
53
 
56
- return `<known path="${entry.path}"${state}${tokens}/>`;
54
+ return `<known path="${entry.path}"${status}${tokens}${flag}/>`;
57
55
  }
@@ -6,9 +6,7 @@ export default class Mv {
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));
@@ -19,7 +17,7 @@ export default class Mv {
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;
24
22
 
25
23
  const source = await store.getBody(runId, path);
@@ -34,14 +32,16 @@ export default class Mv {
34
32
 
35
33
  const body = `${path} ${to}`;
36
34
  if (destScheme === null) {
37
- await store.upsert(runId, turn, entry.resultPath, body, "proposed", {
35
+ await store.upsert(runId, turn, entry.resultPath, body, 202, {
38
36
  attributes: { from: path, to, isMove: true, warning },
37
+ loopId,
39
38
  });
40
39
  } else {
41
- await store.upsert(runId, turn, to, source, "full");
40
+ await store.upsert(runId, turn, to, source, 200, { loopId });
42
41
  await store.remove(runId, path);
43
- await store.upsert(runId, turn, entry.resultPath, body, "pass", {
42
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
44
43
  attributes: { from: path, to, isMove: true, warning },
44
+ loopId,
45
45
  });
46
46
  }
47
47
  }
@@ -11,32 +11,33 @@ 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 === "result" ||
15
+ r.category === "structural" ||
16
+ r.category === "prompt") &&
15
17
  r.source_turn < ctx.loopStartTurn,
16
18
  );
17
19
  if (entries.length === 0) return content;
18
20
 
19
21
  const lines = await Promise.all(
20
- entries.map((e) => renderToolTag(e, "summary", this.#core)),
22
+ entries.map((e) => renderToolTag(e, this.#core)),
21
23
  );
22
24
  return `${content}\n\n<previous>\n${lines.join("\n")}\n</previous>`;
23
25
  }
24
26
  }
25
27
 
26
- async function renderToolTag(entry, fidelity, core) {
28
+ async function renderToolTag(entry, core) {
27
29
  const attrs =
28
30
  typeof entry.attributes === "string"
29
31
  ? JSON.parse(entry.attributes)
30
32
  : entry.attributes;
31
33
 
32
34
  const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
33
- const status = entry.state ? ` status="${entry.state}"` : "";
35
+ const status = entry.status ? ` status="${entry.status}"` : "";
34
36
 
35
37
  let body;
36
38
  try {
37
39
  body = await core.hooks.tools.view(entry.scheme, {
38
40
  ...entry,
39
- fidelity,
40
41
  attributes: attrs,
41
42
  });
42
43
  } catch {