@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
@@ -5,9 +5,8 @@ Copies an entry from one path to another within the K/V store.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `cp`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Reads source body, writes to destination. K/V destinations resolve immediately (`pass`); file destinations produce a `proposed` entry.
8
+ - **Category**: `logging`
9
+ - **Handler**: Reads source body, writes to destination. Scheme destinations resolve immediately (status 200); file destinations produce status 202 (proposed).
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,6 @@ Shows `cp {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 whether the destination is a K/V path or a file path.
17
+ Warns if the destination already exists and will be overwritten. Uses
18
+ `KnownStore.scheme()` to determine whether the destination is a scheme
19
+ path or a file path.
@@ -1,26 +1,28 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
2
+ import docs from "./cpDoc.js";
3
3
 
4
4
  export default class Cp {
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.cp = 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,13 +36,15 @@ export default class Cp {
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: false, warning },
41
+ loopId,
39
42
  });
40
43
  } else {
41
- await store.upsert(runId, turn, to, source, "full");
42
- await store.upsert(runId, turn, entry.resultPath, body, "pass", {
44
+ await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
45
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
43
46
  attributes: { from: path, to, isMove: false, warning },
47
+ loopId,
44
48
  });
45
49
  }
46
50
  }
@@ -0,0 +1,29 @@
1
+ // Tool doc for <cp>. 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
+ ['## <cp path="[source]">[destination]</cp> - Copy a file or entry'],
7
+
8
+ // --- Examples: single copy, glob batch, cross-scheme
9
+ [
10
+ 'Example: <cp path="src/config.js">src/config.backup.js</cp>',
11
+ "Simple file copy. Path = source, body = destination.",
12
+ ],
13
+ [
14
+ 'Example: <cp path="known://plan_*">known://archive_</cp>',
15
+ "Glob batch copy across known entries. Shows pattern operations on cp.",
16
+ ],
17
+
18
+ // --- Constraints
19
+ [
20
+ "* Source path accepts globs: `src/*.js`, `known://draft_*`",
21
+ "Pattern support. Distributes glob teaching beyond get.",
22
+ ],
23
+ [
24
+ "* Use `preview` to check matches before bulk copy",
25
+ "Safety pattern consistent with get and rm preview.",
26
+ ],
27
+ ];
28
+
29
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,7 +1,7 @@
1
1
  # current
2
2
 
3
3
  Renders the `<current>` section of the user message — the active loop's
4
- model responses, tool results, and agent warnings.
4
+ tool results and lifecycle signals.
5
5
 
6
6
  ## Registration
7
7
 
@@ -9,6 +9,6 @@ model responses, tool results, and agent warnings.
9
9
 
10
10
  ## Behavior
11
11
 
12
- Filters turn_context rows where `category` is `result` or `structural`
13
- and `source_turn >= loopStartTurn`. Renders each entry chronologically
14
- with status symbols. Empty on the first turn of a loop.
12
+ Filters turn_context rows where `category === "logging"` and
13
+ `source_turn >= loopStartTurn`. Renders each entry chronologically
14
+ with turn number and status. Empty on the first turn of a loop.
@@ -8,33 +8,35 @@ export default class Current {
8
8
 
9
9
  async assembleCurrent(content, ctx) {
10
10
  const entries = ctx.rows.filter(
11
- (r) =>
12
- (r.category === "result" || r.category === "structural") &&
13
- r.source_turn >= ctx.loopStartTurn,
11
+ (r) => r.category === "logging" && r.source_turn >= ctx.loopStartTurn,
14
12
  );
15
13
  if (entries.length === 0) return content;
16
14
 
17
15
  const lines = await Promise.all(
18
- entries.map((e) => renderToolTag(e, "full", this.#core)),
16
+ entries.map((e) => renderToolTag(e, this.#core)),
19
17
  );
20
18
  return `${content}<current>\n${lines.join("\n")}\n</current>\n`;
21
19
  }
22
20
  }
23
21
 
24
- async function renderToolTag(entry, fidelity, core) {
22
+ async function renderToolTag(entry, core) {
25
23
  const attrs =
26
24
  typeof entry.attributes === "string"
27
25
  ? JSON.parse(entry.attributes)
28
26
  : entry.attributes;
29
27
 
30
- const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
31
- const status = entry.state ? ` status="${entry.state}"` : "";
28
+ const target = attrs?.path || attrs?.file || attrs?.command || "";
29
+ const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
30
+ const status = entry.status ? ` status="${entry.status}"` : "";
31
+ const summary =
32
+ typeof attrs?.summary === "string"
33
+ ? ` summary="${attrs.summary.slice(0, 80)}"`
34
+ : "";
32
35
 
33
36
  let body;
34
37
  try {
35
38
  body = await core.hooks.tools.view(entry.scheme, {
36
39
  ...entry,
37
- fidelity,
38
40
  attributes: attrs,
39
41
  });
40
42
  } catch {
@@ -42,7 +44,7 @@ async function renderToolTag(entry, fidelity, core) {
42
44
  }
43
45
 
44
46
  if (body) {
45
- return `<tool path="${path}"${status}>${body}</tool>`;
47
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}>${body}</${entry.scheme}>`;
46
48
  }
47
- return `<tool path="${path}"${status}/>`;
49
+ return `<${entry.scheme} path="${target}"${turn}${status}${summary}/>`;
48
50
  }
@@ -1,18 +1,13 @@
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
 
11
- -- PREP: get_promoted_token_total
12
- SELECT COALESCE(SUM(ke.tokens), 0) AS total
13
- FROM known_entries AS ke
14
- JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
15
- WHERE
16
- ke.run_id = :run_id
17
- AND ke.state IN ('full', 'summary')
18
- AND s.model_visible = 1;
13
+
@@ -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;
@@ -32,15 +37,10 @@ WHERE run_id = :run_id AND turn = :turn;
32
37
  -- PREP: get_turn_distribution
33
38
  SELECT
34
39
  CASE category
35
- WHEN 'file' THEN 'files'
36
- WHEN 'file_symbols' THEN 'files'
37
- WHEN 'file_index' THEN 'keys'
38
- WHEN 'known' THEN 'known'
39
- WHEN 'known_index' THEN 'keys'
40
- WHEN 'unknown' THEN 'history'
41
- WHEN 'result' THEN 'history'
42
- WHEN 'prompt' THEN 'system'
43
- WHEN 'system' THEN 'system'
40
+ WHEN 'data' THEN 'data'
41
+ WHEN 'logging' THEN 'logging'
42
+ WHEN 'unknown' THEN 'unknown'
43
+ WHEN 'prompt' THEN 'prompt'
44
44
  ELSE 'system'
45
45
  END AS bucket,
46
46
  COALESCE(SUM(tokens), 0) AS tokens,
@@ -1,13 +1,12 @@
1
1
  # env
2
2
 
3
- Stores environment/context information as a pass-through entry.
3
+ Runs an exploratory shell command and records the output.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `env`
8
- - **Modes**: ask, act
9
- - **Category**: ask
10
- - **Handler**: Upserts the entry body as `pass` state with original attributes preserved.
8
+ - **Category**: `logging`
9
+ - **Handler**: Upserts the entry at status 202 (proposed) with original attributes preserved.
11
10
 
12
11
  ## Projection
13
12
 
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./envDoc.js";
2
2
 
3
3
  export default class Env {
4
4
  #core;
@@ -9,16 +9,17 @@ export default class Env {
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.env = docs;
14
+ return docsMap;
15
+ });
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
 
@@ -0,0 +1,29 @@
1
+ // Tool doc for <env>. 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
+ ["## <env>[command]</env> - Run an exploratory shell command"],
7
+
8
+ // --- Examples: version check and git status — safe, read-only commands
9
+ [
10
+ "Example: <env>npm --version</env>",
11
+ "Version check. Safe, no side effects.",
12
+ ],
13
+ [
14
+ "Example: <env>git log --oneline -5</env>",
15
+ "Git history. Shows env for read-only investigation.",
16
+ ],
17
+
18
+ // --- Constraints: hard boundaries
19
+ [
20
+ '* YOU MUST NOT use <env/> to read or list files — use <get path="*" preview/> instead',
21
+ "Prevents cat/ls through shell. Forces file access through get for proper tracking.",
22
+ ],
23
+ [
24
+ "* YOU MUST use <sh/> for commands with side effects",
25
+ "Separates exploration from action. env = observe, sh = mutate.",
26
+ ],
27
+ ];
28
+
29
+ export default LINES.map(([text]) => text).join("\n");
@@ -2,24 +2,21 @@
2
2
 
3
3
  Owns file-related projections and file constraint management.
4
4
 
5
- ## Files
6
-
7
- - **file.js** — Plugin registration, projection hooks, and constraint CRUD (activate, ignore, drop).
8
- - **FileScanner.js** — Scans project directories for file entries.
9
- - **GitProvider.js** — Git integration for file discovery and status.
10
- - **ProjectContext.js** — Builds project-level context from scanned files.
11
- - **FsProvider.js** — Filesystem abstraction for file reading/writing.
12
-
13
5
  ## Registration
14
6
 
15
- - **Projections**: Registers `onProject` handlers for schemes: `file`, `known`, `skill`, `ask`, `act`, `progress`. All project the entry body directly.
7
+ - **Schemes**: `file` (bare paths), `http`, `https` all `category: "data"`
8
+ - **Views**: `full` and `summary` for file scheme. Default identity views
9
+ for `http`/`https` (overridden by rummy.web when installed).
16
10
  - **No tool handler** — file operations are dispatched through `set`, `get`, `rm`, etc.
17
11
 
18
12
  ## File Constraints
19
13
 
20
- Static methods `activate`, `ignore`, and `drop` manage per-project file constraints in the database. Constraints control file visibility across all runs:
14
+ Static methods `setConstraint` and `dropConstraint` manage per-project
15
+ file constraints in the database. Constraints are project-level config
16
+ (backbone), not tool dispatch. See SPEC.md §2.3.
21
17
 
22
- - `active` / `readonly` — always promoted into context.
18
+ - `active` / `readonly` — promoted into context.
23
19
  - `ignore` — excluded from scans; demotes existing entries.
24
20
 
25
- Paths are normalized to project-relative when absolute.
21
+ Entry promotion/demotion from constraints goes through the standard
22
+ tool handler chain via `dispatchTool`.
@@ -1,48 +1,45 @@
1
1
  import { isAbsolute, relative } from "node:path";
2
2
 
3
+ /**
4
+ * File plugin: projections and constraints for filesystem entries.
5
+ *
6
+ * Bare file paths (src/app.js) have scheme=NULL in the DB because
7
+ * schemeOf() only recognizes "://" patterns. The schemes table has
8
+ * a "file" entry so v_model_context can JOIN via COALESCE(scheme, 'file').
9
+ * This is the one exception to "every scheme has a plugin owner" —
10
+ * the file plugin owns the NULL scheme through the "file" registry entry.
11
+ */
3
12
  export default class File {
4
13
  #core;
5
14
 
6
15
  constructor(core) {
7
16
  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
- });
17
+ // "file" scheme covers bare paths (scheme IS NULL in DB)
18
+ core.registerScheme({ category: "data" });
19
+ core.registerScheme({ name: "http", category: "data" });
20
+ core.registerScheme({ name: "https", category: "data" });
25
21
  core.on("full", this.full.bind(this));
26
-
27
- // Register identity projections for schemes that just pass through body
28
- for (const scheme of ["known", "skill", "ask", "act", "progress"]) {
29
- core.hooks.tools.onView(scheme, (entry) => entry.body);
30
- }
22
+ core.on("summary", this.summary.bind(this));
23
+ // Default identity views for http/https rummy.web overrides these
24
+ core.hooks.tools.onView("http", (entry) => entry.body);
25
+ core.hooks.tools.onView("https", (entry) => entry.body);
31
26
  }
32
27
 
33
28
  full(entry) {
34
29
  return entry.body;
35
30
  }
36
31
 
37
- static async activate(
38
- db,
39
- knownStore,
40
- projectId,
41
- pattern,
42
- visibility = "active",
43
- ) {
32
+ summary() {
33
+ return "";
34
+ }
35
+
36
+ /**
37
+ * Set a project-level file constraint. Backbone operation —
38
+ * constraints are project config, not tool dispatch.
39
+ */
40
+ static async setConstraint(db, projectId, pattern, visibility = "active") {
44
41
  const path = await normalizePath(db, projectId, pattern);
45
- if (!path) return { status: "ok" };
42
+ if (!path) return null;
46
43
 
47
44
  await db.upsert_file_constraint.run({
48
45
  project_id: projectId,
@@ -50,30 +47,22 @@ export default class File {
50
47
  visibility,
51
48
  });
52
49
 
53
- if (visibility === "ignore") {
54
- const runs = await db.get_all_runs.all({ project_id: projectId });
55
- for (const run of runs) {
56
- await knownStore.demoteByPattern(run.id, path, null);
57
- }
58
- }
59
-
60
- return { status: "ok" };
61
- }
62
-
63
- static async ignore(db, knownStore, projectId, pattern) {
64
- return File.activate(db, knownStore, projectId, pattern, "ignore");
50
+ return path;
65
51
  }
66
52
 
67
- static async drop(db, projectId, pattern) {
53
+ /**
54
+ * Remove a project-level file constraint.
55
+ */
56
+ static async dropConstraint(db, projectId, pattern) {
68
57
  const path = await normalizePath(db, projectId, pattern);
69
- if (!path) return { status: "ok" };
58
+ if (!path) return null;
70
59
 
71
60
  await db.delete_file_constraint.run({
72
61
  project_id: projectId,
73
62
  pattern: path,
74
63
  });
75
64
 
76
- return { status: "ok" };
65
+ return path;
77
66
  }
78
67
  }
79
68
 
@@ -5,8 +5,7 @@ Retrieves and promotes entries by path or glob pattern.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `get`
8
- - **Modes**: ask, act
9
- - **Category**: ask
8
+ - **Category**: `logging`
10
9
  - **Handler**: Fetches matching entries via `getEntriesByPattern`, promotes them with `promoteByPattern`, and records the result.
11
10
 
12
11
  ## Projection
@@ -17,3 +16,4 @@ Shows `get {path}` followed by the entry body.
17
16
 
18
17
  - Pattern queries (globs or body filters) produce a summary of matched paths.
19
18
  - Exact path queries report the path and token count, or "not found".
19
+ - Budget check: rejects with 413 if incoming tokens exceed remaining context.
@@ -1,28 +1,42 @@
1
- import { readFileSync } from "node:fs";
1
+ import KnownStore from "../../agent/KnownStore.js";
2
2
  import { storePatternResult } from "../helpers.js";
3
+ import docs from "./getDoc.js";
3
4
 
4
5
  export default class Get {
5
6
  #core;
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));
13
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
14
- core.filter("instructions.toolDocs", async (content) =>
15
- content ? `${content}\n\n${docs}` : docs,
16
- );
14
+ core.filter("instructions.toolDocs", async (docsMap) => {
15
+ docsMap.get = docs;
16
+ return docsMap;
17
+ });
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
+
39
+ await store.promoteByPattern(runId, normalized, bodyFilter, turn);
26
40
 
27
41
  if (isPattern) {
28
42
  await storePatternResult(
@@ -33,13 +47,16 @@ export default class Get {
33
47
  target,
34
48
  bodyFilter,
35
49
  matches,
50
+ { loopId },
36
51
  );
37
52
  } else {
38
53
  const total = matches.reduce((s, m) => s + m.tokens_full, 0);
39
54
  const paths = matches.map((m) => m.path).join(", ");
40
55
  const body =
41
56
  matches.length > 0 ? `${paths} ${total} tokens` : `${target} not found`;
42
- await store.upsert(runId, turn, entry.resultPath, body, "read");
57
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
58
+ loopId,
59
+ });
43
60
  }
44
61
  }
45
62
 
@@ -0,0 +1,41 @@
1
+ // Tool doc for <get>. 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-form is the primary invocation (simplest)
6
+ ["## <get>[path/to/file]</get> - Load a file or entry into context"],
7
+
8
+ // --- Examples: 3 examples covering single file, known recall, and content search
9
+ [
10
+ "Example: <get>src/app.js</get>",
11
+ "Simplest form. Body = path. Teaches that get is the default read tool.",
12
+ ],
13
+ [
14
+ 'Example: <get path="known://*">auth</get>',
15
+ "Keyword recall: glob in path, search term in body. Cross-scheme hedberg pattern.",
16
+ ],
17
+ [
18
+ 'Example: <get path="src/**/*.js" preview>TODO</get>',
19
+ "Full pattern: recursive glob + preview + content filter. Shows all 3 features at once.",
20
+ ],
21
+
22
+ // --- Constraints: RFC-style. Each prevents a specific failure mode.
23
+ [
24
+ "* Paths accept globs: `src/**/*.js`, `known://api_*`",
25
+ "Reinforces picomatch patterns work everywhere, not just in examples.",
26
+ ],
27
+ [
28
+ "* `preview` shows matches without loading into context",
29
+ "Budget-awareness. Without this, models load everything and blow context.",
30
+ ],
31
+ [
32
+ "* Body text filters results by content match",
33
+ "Generalizes examples 2-3. Body = filter, not just path.",
34
+ ],
35
+ [
36
+ '* Use <set path="..." fidelity="index"/> to archive loaded content',
37
+ "Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
38
+ ],
39
+ ];
40
+
41
+ export default LINES.map(([text]) => text).join("\n");
@@ -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,7 +1,6 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { parseEditContent } from "./edits.js";
3
2
  import HeuristicMatcher, { generatePatch } from "./matcher.js";
4
- import { normalizeAttrs } from "./normalize.js";
3
+ import { normalizeAttrs, parseJsonEdit } from "./normalize.js";
5
4
  import { hedmatch, hedsearch } from "./patterns.js";
6
5
  import { parseSed } from "./sed.js";
7
6
 
@@ -30,14 +29,13 @@ export default class Hedberg {
30
29
  replace: Hedberg.replace,
31
30
  parseSed,
32
31
  parseEdits: parseEditContent,
32
+ parseJsonEdit,
33
33
  normalizeAttrs,
34
34
  generatePatch,
35
35
  };
36
36
 
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
- );
37
+ // Patterns documentation distributed to individual tool docs.
38
+ // Hedberg has no model-facing docs of its own.
41
39
  }
42
40
 
43
41
  /**
@@ -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