@possumtech/rummy 0.2.8 → 0.3.1

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 (114) hide show
  1. package/.env.example +13 -2
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +440 -106
  5. package/migrations/001_initial_schema.sql +5 -3
  6. package/package.json +17 -5
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +252 -55
  9. package/src/agent/ContextAssembler.js +20 -4
  10. package/src/agent/KnownStore.js +82 -25
  11. package/src/agent/ProjectAgent.js +4 -1
  12. package/src/agent/ResponseHealer.js +86 -32
  13. package/src/agent/TurnExecutor.js +542 -207
  14. package/src/agent/XmlParser.js +77 -41
  15. package/src/agent/known_store.sql +68 -4
  16. package/src/agent/schemes.sql +3 -0
  17. package/src/agent/tokens.js +7 -21
  18. package/src/agent/turns.sql +15 -1
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +15 -0
  21. package/src/hooks/PluginContext.js +14 -1
  22. package/src/hooks/RummyContext.js +16 -4
  23. package/src/hooks/ToolRegistry.js +77 -19
  24. package/src/llm/LlmProvider.js +27 -8
  25. package/src/llm/OpenAiClient.js +20 -0
  26. package/src/llm/OpenRouterClient.js +24 -2
  27. package/src/llm/XaiClient.js +47 -2
  28. package/src/plugins/ask_user/README.md +4 -4
  29. package/src/plugins/ask_user/ask_user.js +5 -5
  30. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  31. package/src/plugins/budget/README.md +31 -0
  32. package/src/plugins/budget/budget.js +55 -0
  33. package/src/plugins/cp/README.md +5 -4
  34. package/src/plugins/cp/cp.js +10 -6
  35. package/src/plugins/cp/cpDoc.js +29 -0
  36. package/src/plugins/engine/engine.sql +1 -8
  37. package/src/plugins/engine/turn_context.sql +4 -9
  38. package/src/plugins/env/README.md +3 -4
  39. package/src/plugins/env/env.js +5 -5
  40. package/src/plugins/env/envDoc.js +29 -0
  41. package/src/plugins/file/README.md +9 -12
  42. package/src/plugins/file/file.js +34 -35
  43. package/src/plugins/get/README.md +2 -2
  44. package/src/plugins/get/get.js +77 -6
  45. package/src/plugins/get/getDoc.js +51 -0
  46. package/src/plugins/hedberg/hedberg.js +2 -1
  47. package/src/plugins/hedberg/matcher.js +10 -29
  48. package/src/plugins/hedberg/normalize.js +28 -0
  49. package/src/plugins/hedberg/patterns.js +25 -27
  50. package/src/plugins/hedberg/sed.js +17 -10
  51. package/src/plugins/index.js +66 -14
  52. package/src/plugins/instructions/README.md +6 -2
  53. package/src/plugins/instructions/instructions.js +20 -4
  54. package/src/plugins/instructions/preamble.md +19 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +23 -17
  57. package/src/plugins/known/knownDoc.js +34 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +27 -6
  60. package/src/plugins/mv/mvDoc.js +45 -0
  61. package/src/plugins/performed/README.md +15 -0
  62. package/src/plugins/performed/performed.js +45 -0
  63. package/src/plugins/persona/persona.js +78 -0
  64. package/src/plugins/previous/README.md +3 -2
  65. package/src/plugins/previous/previous.js +33 -24
  66. package/src/plugins/progress/README.md +1 -2
  67. package/src/plugins/progress/progress.js +33 -21
  68. package/src/plugins/prompt/README.md +5 -5
  69. package/src/plugins/prompt/prompt.js +15 -17
  70. package/src/plugins/rm/README.md +4 -4
  71. package/src/plugins/rm/rm.js +32 -20
  72. package/src/plugins/rm/rmDoc.js +30 -0
  73. package/src/plugins/rpc/README.md +15 -28
  74. package/src/plugins/rpc/rpc.js +42 -77
  75. package/src/plugins/set/README.md +13 -12
  76. package/src/plugins/set/set.js +107 -16
  77. package/src/plugins/set/setDoc.js +49 -0
  78. package/src/plugins/sh/README.md +4 -4
  79. package/src/plugins/sh/sh.js +5 -5
  80. package/src/plugins/sh/shDoc.js +29 -0
  81. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  82. package/src/plugins/summarize/README.md +6 -5
  83. package/src/plugins/summarize/summarize.js +7 -6
  84. package/src/plugins/summarize/summarizeDoc.js +33 -0
  85. package/src/plugins/telemetry/telemetry.js +16 -9
  86. package/src/plugins/think/README.md +20 -0
  87. package/src/plugins/think/think.js +5 -0
  88. package/src/plugins/unknown/README.md +6 -5
  89. package/src/plugins/unknown/unknown.js +12 -9
  90. package/src/plugins/unknown/unknownDoc.js +31 -0
  91. package/src/plugins/update/README.md +3 -8
  92. package/src/plugins/update/update.js +7 -6
  93. package/src/plugins/update/updateDoc.js +33 -0
  94. package/src/server/ClientConnection.js +59 -45
  95. package/src/server/RpcRegistry.js +52 -4
  96. package/src/sql/v_model_context.sql +10 -25
  97. package/src/plugins/ask_user/docs.md +0 -2
  98. package/src/plugins/cp/docs.md +0 -2
  99. package/src/plugins/current/README.md +0 -14
  100. package/src/plugins/current/current.js +0 -47
  101. package/src/plugins/env/docs.md +0 -4
  102. package/src/plugins/get/docs.md +0 -10
  103. package/src/plugins/known/docs.md +0 -3
  104. package/src/plugins/mv/docs.md +0 -2
  105. package/src/plugins/rm/docs.md +0 -6
  106. package/src/plugins/set/docs.md +0 -6
  107. package/src/plugins/sh/docs.md +0 -2
  108. package/src/plugins/skills/README.md +0 -25
  109. package/src/plugins/store/README.md +0 -20
  110. package/src/plugins/store/docs.md +0 -6
  111. package/src/plugins/store/store.js +0 -63
  112. package/src/plugins/summarize/docs.md +0 -4
  113. package/src/plugins/unknown/docs.md +0 -5
  114. package/src/plugins/update/docs.md +0 -4
@@ -72,7 +72,29 @@ export default class OpenRouterClient {
72
72
  return data;
73
73
  }
74
74
 
75
- async getContextSize(_model) {
76
- return Number(process.env.RUMMY_CONTEXT_SIZE) || DEFAULT_CONTEXT_SIZE;
75
+ #contextCache = new Map();
76
+
77
+ async getContextSize(model) {
78
+ if (process.env.RUMMY_CONTEXT_SIZE)
79
+ return Number(process.env.RUMMY_CONTEXT_SIZE);
80
+
81
+ if (this.#contextCache.has(model)) return this.#contextCache.get(model);
82
+
83
+ try {
84
+ const res = await fetch(`${this.#baseUrl}/models`, {
85
+ headers: { Authorization: `Bearer ${this.#apiKey}` },
86
+ signal: AbortSignal.timeout(5000),
87
+ });
88
+ if (res.ok) {
89
+ const data = await res.json();
90
+ const entry = data.data?.find((m) => m.id === model);
91
+ if (entry?.context_length) {
92
+ this.#contextCache.set(model, entry.context_length);
93
+ return entry.context_length;
94
+ }
95
+ }
96
+ } catch {}
97
+
98
+ return DEFAULT_CONTEXT_SIZE;
77
99
  }
78
100
  }
@@ -3,6 +3,7 @@ import msg from "../agent/messages.js";
3
3
  export default class XaiClient {
4
4
  #baseUrl;
5
5
  #apiKey;
6
+ #contextCache = new Map();
6
7
 
7
8
  constructor(baseUrl, apiKey) {
8
9
  this.#baseUrl = baseUrl;
@@ -107,7 +108,51 @@ export default class XaiClient {
107
108
  );
108
109
  }
109
110
 
110
- async getContextSize(_model) {
111
- return Number(process.env.RUMMY_CONTEXT_SIZE) || 131072;
111
+ async getContextSize(model) {
112
+ if (this.#contextCache.has(model)) return this.#contextCache.get(model);
113
+
114
+ if (!this.#apiKey) throw new Error(msg("error.xai_api_key_missing"));
115
+
116
+ // Query xAI models endpoint
117
+ const modelsUrl = this.#baseUrl.replace(/\/responses$/, "/models");
118
+ const res = await fetch(modelsUrl, {
119
+ headers: { Authorization: `Bearer ${this.#apiKey}` },
120
+ signal: AbortSignal.timeout(5000),
121
+ });
122
+
123
+ if (res.ok) {
124
+ const data = await res.json();
125
+ const models = data.data || data.models || [];
126
+ const entry = models.find(
127
+ (m) => m.id === model || `${m.id}-latest` === model,
128
+ );
129
+ if (entry?.context_length) {
130
+ this.#contextCache.set(model, entry.context_length);
131
+ return entry.context_length;
132
+ }
133
+ }
134
+
135
+ // Try /v1/language-models for richer metadata
136
+ const langUrl = this.#baseUrl.replace(
137
+ /\/responses$/,
138
+ `/language-models/${model}`,
139
+ );
140
+ const langRes = await fetch(langUrl, {
141
+ headers: { Authorization: `Bearer ${this.#apiKey}` },
142
+ signal: AbortSignal.timeout(5000),
143
+ }).catch(() => null);
144
+
145
+ if (langRes?.ok) {
146
+ const langData = await langRes.json();
147
+ if (langData?.context_length) {
148
+ this.#contextCache.set(model, langData.context_length);
149
+ return langData.context_length;
150
+ }
151
+ }
152
+
153
+ throw new Error(
154
+ `Cannot determine context size for xAI model "${model}". ` +
155
+ "Register the model with addModel(contextLength) or set context_length in the models table.",
156
+ );
112
157
  }
113
158
  }
@@ -5,9 +5,8 @@ Presents a question to the user with optional multiple-choice answers.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `ask_user`
8
- - **Modes**: ask, act
9
- - **Category**: act
10
- - **Handler**: Parses options (semicolon or comma delimited) and upserts a `proposed` entry awaiting user response.
8
+ - **Category**: `logging`
9
+ - **Handler**: Parses options (semicolon or comma delimited) and upserts at status 202 (proposed) awaiting user response.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows the question and answer attributes.
15
14
 
16
15
  ## Behavior
17
16
 
18
- Options are split by semicolons first, falling back to commas. The entry stays in `proposed` state until resolved by the client.
17
+ Options are split by semicolons first, falling back to commas. The entry
18
+ stays at status 202 until resolved by the client via `run/resolve`.
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./ask_userDoc.js";
2
2
 
3
3
  export default class AskUser {
4
4
  #core;
@@ -9,10 +9,10 @@ export default class AskUser {
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.ask_user = docs;
14
+ return docsMap;
15
+ });
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
@@ -0,0 +1,29 @@
1
+ // Tool doc for <ask_user>. 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: question attr + options in body
6
+ ['## <ask_user question="[Question?]">[option1; option2; ...]</ask_user>'],
7
+
8
+ // --- Constraints FIRST: frames correct usage before examples
9
+ [
10
+ "* YOU SHOULD use for decisions, preferences, or approvals the user must make",
11
+ "Positive framing. Shows what ask_user IS for, not just what it isn't.",
12
+ ],
13
+ [
14
+ "* YOU SHOULD use <get> to find information before asking the user",
15
+ "Gentle redirect. Encourages self-sufficiency without forbidding interaction.",
16
+ ],
17
+
18
+ // --- Examples: genuine decision points where user input is valuable
19
+ [
20
+ 'Example: <ask_user question="Which test framework?">Mocha; Jest; Node Native</ask_user>',
21
+ "Preference decision. Model truly cannot know this without asking.",
22
+ ],
23
+ [
24
+ 'Example: <ask_user question="Deploy to staging or production?">staging; production</ask_user>',
25
+ "Consequential action. Shows ask_user for high-stakes choices.",
26
+ ],
27
+ ];
28
+
29
+ export default LINES.map(([text]) => text).join("\n");
@@ -0,0 +1,31 @@
1
+ # budget
2
+
3
+ Context ceiling enforcement.
4
+
5
+ ## Files
6
+
7
+ - **budget.js** — Plugin. Pre-LLM enforce, BudgetGuard activation.
8
+ - **BudgetGuard.js** — Write-layer gate. Installed on KnownStore during
9
+ dispatch. Checks token delta on every upsert, promote, and body update.
10
+
11
+ ## Registration
12
+
13
+ - **Hook**: `hooks.budget.enforce` — pre-LLM ceiling check.
14
+ - **Hook**: `hooks.budget.activate(store, contextSize, assembledTokens)` — install guard.
15
+ - **Hook**: `hooks.budget.deactivate(store)` — remove guard.
16
+
17
+ ## Budget Contract
18
+
19
+ `contextSize` is the ceiling. `countTokens()` is the measurement.
20
+ Over = 413. Under = 200. No margins.
21
+
22
+ ## BudgetGuard
23
+
24
+ Installed on KnownStore by TurnExecutor before dispatch, cleared in
25
+ `finally`. Gates `upsert()`, `promoteByPattern()`, `updateBodyByPattern()`.
26
+
27
+ Exemptions: `status >= 400` (error entries), `model_visible = 0` (audit),
28
+ `fidelity = "archive"` (not in context).
29
+
30
+ On first violation: `BudgetExceeded` thrown, guard trips, all subsequent
31
+ writes fail. TurnExecutor catches per-tool, writes 413 result entry.
@@ -0,0 +1,55 @@
1
+ import { countTokens } from "../../agent/tokens.js";
2
+
3
+ function measureMessages(messages) {
4
+ return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
5
+ }
6
+
7
+ export default class Budget {
8
+ #core;
9
+
10
+ constructor(core) {
11
+ this.#core = core;
12
+ core.registerScheme({
13
+ name: "budget",
14
+ modelVisible: 1,
15
+ category: "logging",
16
+ });
17
+ core.hooks.tools.onView("budget", (entry) => entry.body);
18
+ core.hooks.budget = {
19
+ enforce: this.enforce.bind(this),
20
+ };
21
+ }
22
+
23
+ async enforce({ contextSize, messages, rows, lastPromptTokens = 0 }) {
24
+ if (!contextSize) {
25
+ return { messages, rows, demoted: [], assembledTokens: 0, status: 200 };
26
+ }
27
+
28
+ // Prefer actual prompt_tokens from the last API response — the estimate
29
+ // from measureMessages can be wildly off for structured/XML-heavy content.
30
+ const assembledTokens =
31
+ lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
32
+
33
+ console.warn(
34
+ `[RUMMY] Budget enforce: ${assembledTokens} tokens (${lastPromptTokens > 0 ? "actual" : "estimated"}), ceiling ${contextSize}, ${rows.length} rows`,
35
+ );
36
+
37
+ const ceiling = Math.floor(contextSize * 0.9);
38
+ if (assembledTokens > ceiling) {
39
+ const overflow = assembledTokens - ceiling;
40
+ console.warn(
41
+ `[RUMMY] Budget 413: ${assembledTokens} tokens > ${contextSize} ceiling (${overflow} over)`,
42
+ );
43
+ return {
44
+ messages,
45
+ rows,
46
+ demoted: [],
47
+ assembledTokens,
48
+ status: 413,
49
+ overflow,
50
+ };
51
+ }
52
+
53
+ return { messages, rows, demoted: [], assembledTokens, status: 200 };
54
+ }
55
+ }
@@ -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,5 +1,5 @@
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;
@@ -10,15 +10,19 @@ export default class Cp {
10
10
  core.on("handler", this.handler.bind(this));
11
11
  core.on("full", this.full.bind(this));
12
12
  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
- );
13
+ core.filter("instructions.toolDocs", async (docsMap) => {
14
+ docsMap.cp = docs;
15
+ return docsMap;
16
+ });
17
17
  }
18
18
 
19
19
  async handler(entry, rummy) {
20
20
  const { entries: store, sequence: turn, runId, loopId } = rummy;
21
21
  const { path, to } = entry.attributes;
22
+ const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
23
+ const fidelity = VALID[entry.attributes.fidelity]
24
+ ? entry.attributes.fidelity
25
+ : undefined;
22
26
 
23
27
  const source = await store.getBody(runId, path);
24
28
  if (source === null) return;
@@ -37,7 +41,7 @@ export default class Cp {
37
41
  loopId,
38
42
  });
39
43
  } else {
40
- await store.upsert(runId, turn, to, source, 200, { loopId });
44
+ await store.upsert(runId, turn, to, source, 200, { fidelity, loopId });
41
45
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
42
46
  attributes: { from: path, to, isMove: false, warning },
43
47
  loopId,
@@ -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 patterns: `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");
@@ -10,11 +10,4 @@ WHERE
10
10
  AND s.model_visible = 1
11
11
  ORDER BY ke.turn, ke.refs, ke.tokens DESC;
12
12
 
13
- -- PREP: get_promoted_token_total
14
- SELECT COALESCE(SUM(ke.tokens), 0) AS total
15
- FROM known_entries AS ke
16
- JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
17
- WHERE
18
- ke.run_id = :run_id
19
- AND ke.fidelity IN ('full', 'summary')
20
- AND s.model_visible = 1;
13
+
@@ -37,15 +37,10 @@ WHERE run_id = :run_id AND turn = :turn;
37
37
  -- PREP: get_turn_distribution
38
38
  SELECT
39
39
  CASE category
40
- WHEN 'file' THEN 'files'
41
- WHEN 'file_symbols' THEN 'files'
42
- WHEN 'file_index' THEN 'keys'
43
- WHEN 'known' THEN 'known'
44
- WHEN 'known_index' THEN 'keys'
45
- WHEN 'unknown' THEN 'history'
46
- WHEN 'result' THEN 'history'
47
- WHEN 'prompt' THEN 'system'
48
- WHEN 'system' THEN 'system'
40
+ WHEN 'data' THEN 'data'
41
+ WHEN 'logging' THEN 'logging'
42
+ WHEN 'unknown' THEN 'unknown'
43
+ WHEN 'prompt' THEN 'prompt'
49
44
  ELSE 'system'
50
45
  END AS bucket,
51
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,10 +9,10 @@ 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) {
@@ -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,34 +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({ category: "file" });
9
- core.registerScheme({ name: "http", category: "file" });
10
- core.registerScheme({ name: "https", category: "file" });
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" });
11
21
  core.on("full", this.full.bind(this));
12
-
13
- // Register identity projections for schemes that just pass through body
14
- for (const scheme of ["known", "skill", "ask", "act", "progress"]) {
15
- core.hooks.tools.onView(scheme, (entry) => entry.body);
16
- }
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);
17
26
  }
18
27
 
19
28
  full(entry) {
20
29
  return entry.body;
21
30
  }
22
31
 
23
- static async activate(
24
- db,
25
- knownStore,
26
- projectId,
27
- pattern,
28
- visibility = "active",
29
- ) {
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") {
30
41
  const path = await normalizePath(db, projectId, pattern);
31
- if (!path) return { status: "ok" };
42
+ if (!path) return null;
32
43
 
33
44
  await db.upsert_file_constraint.run({
34
45
  project_id: projectId,
@@ -36,34 +47,22 @@ export default class File {
36
47
  visibility,
37
48
  });
38
49
 
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") {
45
- for (const run of runs) {
46
- await knownStore.demoteByPattern(run.id, path, null);
47
- }
48
- }
49
-
50
- return { status: "ok" };
51
- }
52
-
53
- static async ignore(db, knownStore, projectId, pattern) {
54
- return File.activate(db, knownStore, projectId, pattern, "ignore");
50
+ return path;
55
51
  }
56
52
 
57
- static async drop(db, projectId, pattern) {
53
+ /**
54
+ * Remove a project-level file constraint.
55
+ */
56
+ static async dropConstraint(db, projectId, pattern) {
58
57
  const path = await normalizePath(db, projectId, pattern);
59
- if (!path) return { status: "ok" };
58
+ if (!path) return null;
60
59
 
61
60
  await db.delete_file_constraint.run({
62
61
  project_id: projectId,
63
62
  pattern: path,
64
63
  });
65
64
 
66
- return { status: "ok" };
65
+ return path;
67
66
  }
68
67
  }
69
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.