@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,4 +1,3 @@
1
- import KnownStore from "../../agent/KnownStore.js";
2
1
  import msg from "../../agent/messages.js";
3
2
  import RummyContext from "../../hooks/RummyContext.js";
4
3
  import File from "../file/file.js";
@@ -67,7 +66,7 @@ export default class Rpc {
67
66
  const row = await ctx.db.upsert_model.get({
68
67
  alias: params.alias,
69
68
  actual: params.actual,
70
- context_length: params.contextLength || null,
69
+ context_length: params.contextLength ?? null,
71
70
  });
72
71
  return { id: row.id, alias: params.alias };
73
72
  },
@@ -90,15 +89,15 @@ export default class Rpc {
90
89
 
91
90
  // --- Entry operations (same dispatch as model) ---
92
91
 
93
- r.register("read", {
92
+ // Override: get has persist flag for file constraint management
93
+ r.register("get", {
94
94
  handler: async (params, ctx) => {
95
95
  if (!params.path) throw new Error("path is required");
96
96
 
97
97
  if (params.persist) {
98
98
  const visibility = params.readonly ? "readonly" : "active";
99
- return File.activate(
99
+ await File.setConstraint(
100
100
  ctx.db,
101
- ctx.projectAgent.entries,
102
101
  ctx.projectId,
103
102
  params.path,
104
103
  visibility,
@@ -115,105 +114,50 @@ export default class Rpc {
115
114
  description: "Promote entry to full state.",
116
115
  params: {
117
116
  path: "string — file path or glob pattern",
118
- run: "string? — run alias (required without persist)",
119
- persist: "boolean? — create file constraint",
117
+ run: "string — run alias",
118
+ persist: "boolean? — also create file constraint",
120
119
  readonly: "boolean? — with persist, set readonly instead of active",
121
120
  },
122
121
  requiresInit: true,
123
122
  });
124
123
 
124
+ // store is not a tool — it manages file constraints
125
125
  r.register("store", {
126
126
  handler: async (params, ctx) => {
127
127
  if (!params.path) throw new Error("path is required");
128
128
 
129
129
  if (params.clear) {
130
- return File.drop(ctx.db, ctx.projectId, params.path);
130
+ await File.dropConstraint(ctx.db, ctx.projectId, params.path);
131
+ return { status: "ok" };
131
132
  }
132
133
  if (params.persist) {
133
- if (params.ignore) {
134
- return File.ignore(
135
- ctx.db,
136
- ctx.projectAgent.entries,
137
- ctx.projectId,
138
- params.path,
139
- );
140
- }
141
- return File.drop(ctx.db, ctx.projectId, params.path);
134
+ const visibility = params.ignore ? "ignore" : "active";
135
+ await File.setConstraint(
136
+ ctx.db,
137
+ ctx.projectId,
138
+ params.path,
139
+ visibility,
140
+ );
142
141
  }
143
142
 
144
143
  if (!params.run) throw new Error("run is required");
145
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
146
- await dispatchTool(hooks, rummy, "store", params.path, "", {
147
- path: params.path,
148
- });
144
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
145
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
146
+ const store = ctx.projectAgent.entries;
147
+ await store.demoteByPattern(runRow.id, params.path, null);
149
148
  return { status: "ok" };
150
149
  },
151
150
  description: "Demote entry to stored state.",
152
151
  params: {
153
152
  path: "string — file path or glob pattern",
154
153
  run: "string? — run alias (required without persist)",
155
- persist: "boolean? — create file constraint",
154
+ persist: "boolean? — also create file constraint",
156
155
  ignore: "boolean? — with persist, exclude from scan",
157
156
  clear: "boolean? — remove existing constraint",
158
157
  },
159
158
  requiresInit: true,
160
159
  });
161
160
 
162
- r.register("write", {
163
- handler: async (params, ctx) => {
164
- if (!params.path) throw new Error("path is required");
165
- if (!params.run) throw new Error("run is required");
166
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
167
-
168
- const scheme = KnownStore.scheme(params.path);
169
- if (scheme) {
170
- await rummy.set({
171
- path: params.path,
172
- body: params.body || "",
173
- state: params.state || "full",
174
- attributes: params.attributes,
175
- });
176
- } else {
177
- await dispatchTool(
178
- hooks,
179
- rummy,
180
- "set",
181
- params.path,
182
- params.body || "",
183
- { path: params.path, ...params.attributes },
184
- );
185
- }
186
- return { status: "ok" };
187
- },
188
- description: "Create or update an entry.",
189
- params: {
190
- run: "string — run alias",
191
- path: "string — entry path",
192
- body: "string? — entry content",
193
- state: "string? — entry state (default: full)",
194
- attributes: "object? — JSON attributes",
195
- },
196
- requiresInit: true,
197
- });
198
-
199
- r.register("delete", {
200
- handler: async (params, ctx) => {
201
- if (!params.path) throw new Error("path is required");
202
- if (!params.run) throw new Error("run is required");
203
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
204
- await dispatchTool(hooks, rummy, "rm", params.path, "", {
205
- path: params.path,
206
- });
207
- return { status: "ok" };
208
- },
209
- description: "Remove an entry.",
210
- params: {
211
- run: "string — run alias",
212
- path: "string — entry path",
213
- },
214
- requiresInit: true,
215
- });
216
-
217
161
  r.register("getEntries", {
218
162
  handler: async (params, ctx) => {
219
163
  let run;
@@ -225,14 +169,15 @@ export default class Rpc {
225
169
  if (!run) return [];
226
170
  const entries = await ctx.projectAgent.entries.getEntriesByPattern(
227
171
  run.id,
228
- params.pattern || "*",
229
- params.body || null,
230
- { limit: params.limit, offset: params.offset },
172
+ params.pattern ?? "*",
173
+ params.body ?? null,
174
+ { limit: params.limit ?? null, offset: params.offset ?? null },
231
175
  );
232
176
  return entries.map((e) => ({
233
177
  path: e.path,
234
178
  scheme: e.scheme,
235
- state: e.state,
179
+ status: e.status,
180
+ fidelity: e.fidelity,
236
181
  tokens: e.tokens_full,
237
182
  }));
238
183
  },
@@ -256,7 +201,7 @@ export default class Rpc {
256
201
  const runRow = await ctx.db.create_run.get({
257
202
  project_id: ctx.projectId,
258
203
  parent_run_id: null,
259
- model: params.model,
204
+ model: params.model ?? null,
260
205
  alias,
261
206
  temperature: params.temperature ?? null,
262
207
  persona: params.persona ?? null,
@@ -283,10 +228,12 @@ export default class Rpc {
283
228
  params.prompt,
284
229
  params.run,
285
230
  {
286
- temperature: params.temperature,
287
- persona: params.persona,
231
+ temperature: params.temperature ?? null,
232
+ persona: params.persona ?? null,
288
233
  contextLimit: params.contextLimit,
289
- noContext: params.noContext,
234
+ noRepo: params.noRepo,
235
+ noInteraction: params.noInteraction,
236
+ noWeb: params.noWeb,
290
237
  fork: params.fork,
291
238
  },
292
239
  );
@@ -300,7 +247,9 @@ export default class Rpc {
300
247
  temperature: "number?",
301
248
  persona: "string?",
302
249
  contextLimit: "number?",
303
- noContext: "boolean?",
250
+ noRepo: "boolean?",
251
+ noInteraction: "boolean? — disable ask_user tool",
252
+ noWeb: "boolean? — disable search and URL fetch",
304
253
  fork: "boolean?",
305
254
  },
306
255
  requiresInit: true,
@@ -315,10 +264,12 @@ export default class Rpc {
315
264
  params.prompt,
316
265
  params.run,
317
266
  {
318
- temperature: params.temperature,
319
- persona: params.persona,
267
+ temperature: params.temperature ?? null,
268
+ persona: params.persona ?? null,
320
269
  contextLimit: params.contextLimit,
321
- noContext: params.noContext,
270
+ noRepo: params.noRepo,
271
+ noInteraction: params.noInteraction,
272
+ noWeb: params.noWeb,
322
273
  fork: params.fork,
323
274
  },
324
275
  );
@@ -332,7 +283,9 @@ export default class Rpc {
332
283
  temperature: "number?",
333
284
  persona: "string?",
334
285
  contextLimit: "number?",
335
- noContext: "boolean?",
286
+ noRepo: "boolean?",
287
+ noInteraction: "boolean? — disable ask_user tool",
288
+ noWeb: "boolean? — disable search and URL fetch",
336
289
  fork: "boolean?",
337
290
  },
338
291
  requiresInit: true,
@@ -358,7 +311,7 @@ export default class Rpc {
358
311
  ctx.projectAgent.abortRun(runRow.id);
359
312
  await ctx.db.update_run_status.run({
360
313
  id: runRow.id,
361
- status: "aborted",
314
+ status: 499,
362
315
  });
363
316
  return { status: "ok" };
364
317
  },
@@ -447,7 +400,7 @@ export default class Rpc {
447
400
  run: row.alias,
448
401
  status: row.status,
449
402
  turn: row.turn,
450
- summary: row.summary || "",
403
+ summary: row.summary,
451
404
  created: row.created_at,
452
405
  }));
453
406
  },
@@ -512,8 +465,8 @@ export default class Rpc {
512
465
  };
513
466
  }),
514
467
  },
515
- last_user_prompt: promptRow?.body || "",
516
- last_summary: summaryRow?.body || "",
468
+ last_user_prompt: promptRow?.body,
469
+ last_summary: summaryRow?.body,
517
470
  };
518
471
  },
519
472
  description: "Full run detail.",
@@ -527,6 +480,10 @@ export default class Rpc {
527
480
  r.registerNotification("run/progress", "Turn status.");
528
481
  r.registerNotification("ui/render", "Streaming output.");
529
482
  r.registerNotification("ui/notify", "Toast notification.");
483
+
484
+ // Auto-dispatch: any registered tool is callable via RPC.
485
+ // Checked at request time — no timing dependency on plugin load order.
486
+ r.setToolFallback(hooks, buildRunContext, dispatchTool);
530
487
  }
531
488
  }
532
489
 
@@ -547,7 +504,7 @@ async function buildRunContext(hooks, ctx, runAlias) {
547
504
  sequence: runRow.next_turn,
548
505
  runId: runRow.id,
549
506
  turnId: null,
550
- noContext: false,
507
+ noRepo: false,
551
508
  contextSize: null,
552
509
  systemPrompt: "",
553
510
  loopPrompt: "",
@@ -558,25 +515,24 @@ async function buildRunContext(hooks, ctx, runAlias) {
558
515
 
559
516
  async function dispatchTool(hooks, rummy, scheme, path, body, attributes) {
560
517
  const store = rummy.entries;
561
- const resultPath = await store.dedup(rummy.runId, scheme, path || "");
562
-
563
- await store.upsert(
518
+ const resultPath = await store.dedup(
564
519
  rummy.runId,
520
+ scheme,
521
+ path,
565
522
  rummy.sequence,
566
- resultPath,
567
- body || "",
568
- "full",
569
- {
570
- attributes: attributes || null,
571
- },
572
523
  );
573
524
 
525
+ await store.upsert(rummy.runId, rummy.sequence, resultPath, body, 200, {
526
+ attributes: attributes,
527
+ loopId: rummy.loopId,
528
+ });
529
+
574
530
  const entry = {
575
531
  scheme,
576
532
  path: resultPath,
577
- body: body || "",
578
- attributes: attributes || {},
579
- state: "full",
533
+ body: body,
534
+ attributes: attributes,
535
+ status: 200,
580
536
  resultPath,
581
537
  };
582
538
 
@@ -1,32 +1,33 @@
1
1
  # set
2
2
 
3
- Writes or edits entry content. Handles new files, full overwrites, SEARCH/REPLACE edits, and pattern updates.
3
+ Writes or edits entry content. Handles new files, full overwrites,
4
+ SEARCH/REPLACE edits, and pattern updates.
4
5
 
5
6
  ## Files
6
7
 
7
8
  - **set.js** — Plugin registration and edit dispatch logic.
8
- - **HeuristicMatcher.js** — Fuzzy SEARCH/REPLACE matching. Handles whitespace/indentation differences and escaped characters when literal match fails.
9
+ - **HeuristicMatcher.js** — Fuzzy SEARCH/REPLACE matching.
9
10
  - **HeuristicMatcher.test.js** — Tests for HeuristicMatcher.
10
11
 
11
12
  ## Registration
12
13
 
13
14
  - **Tool**: `set`
14
- - **Modes**: ask, act
15
- - **Category**: act
16
- - **Handler**: Routes to different paths based on attributes:
15
+ - **Category**: `logging`
16
+ - **Handler**: Routes based on attributes:
17
17
  - `blocks` or `search` — SEARCH/REPLACE edit via `processEdit`.
18
18
  - `preview` — pattern preview (dry run).
19
- - K/V path — direct upsert at `full` state.
20
- - File path — produces `proposed` entry with udiff patch.
19
+ - Scheme path — direct upsert at status 200.
20
+ - File path — produces status 202 (proposed) with unified diff patch.
21
21
  - Glob/filter — bulk update via `updateBodyByPattern`.
22
22
 
23
23
  ## Projection
24
24
 
25
- Shows `set {file}` with token delta (`before→after tokens`). Includes the merge conflict block when a SEARCH/REPLACE was performed.
25
+ Shows `set {file}` with token delta (`before→after tokens`). Includes
26
+ the merge conflict block when a SEARCH/REPLACE was performed.
26
27
 
27
28
  ## Behavior
28
29
 
29
- - **Literal match first**: SEARCH text is matched literally against the entry body.
30
- - **Heuristic fallback**: On literal failure, `HeuristicMatcher.matchAndPatch` attempts fuzzy matching with warnings.
31
- - **Patch generation**: `generatePatch` produces unified diff format for client display.
32
- - File writes are always `proposed`; K/V writes resolve immediately.
30
+ - **Literal match first**: SEARCH text is matched literally.
31
+ - **Heuristic fallback**: On literal failure, fuzzy matching with warnings.
32
+ - **Patch generation**: `generatePatch` produces unified diff for client display.
33
+ - File writes are always status 202 (proposed); scheme writes resolve immediately.
@@ -1,7 +1,9 @@
1
- import { readFileSync } from "node:fs";
2
1
  import KnownStore from "../../agent/KnownStore.js";
3
2
  import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
4
3
  import { storePatternResult } from "../helpers.js";
4
+ import docs from "./setDoc.js";
5
+
6
+ const VALID_FIDELITY = { archive: 1, summary: 1, index: 1, full: 1 };
5
7
 
6
8
  // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
7
9
  export default class Set {
@@ -9,23 +11,74 @@ export default class Set {
9
11
 
10
12
  constructor(core) {
11
13
  this.#core = core;
12
- core.registerScheme({
13
- validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
14
- });
14
+ core.registerScheme();
15
15
  core.on("handler", this.handler.bind(this));
16
16
  core.on("full", this.full.bind(this));
17
17
  core.on("summary", this.summary.bind(this));
18
18
  core.on("turn.proposing", this.#materializeRevisions.bind(this));
19
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
20
- core.filter("instructions.toolDocs", async (content) =>
21
- content ? `${content}\n\n${docs}` : docs,
22
- );
19
+ core.filter("instructions.toolDocs", async (docsMap) => {
20
+ docsMap.set = docs;
21
+ return docsMap;
22
+ });
23
23
  }
24
24
 
25
25
  async handler(entry, rummy) {
26
- const { entries: store, sequence: turn, runId } = rummy;
26
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
27
27
  const attrs = entry.attributes;
28
28
 
29
+ // Fidelity control: <set path="..." fidelity="archive"/>
30
+ const fidelityAttr = VALID_FIDELITY[attrs.fidelity] ? attrs.fidelity : null;
31
+ if (fidelityAttr && attrs.path) {
32
+ const target = attrs.path;
33
+ const rawSummary =
34
+ typeof attrs.summary === "string" ? attrs.summary : null;
35
+ const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
36
+ const matches = await store.getEntriesByPattern(
37
+ runId,
38
+ target,
39
+ attrs.body,
40
+ );
41
+ if (entry.body) {
42
+ // Write content directly at specified fidelity
43
+ const entryAttrs = summaryText ? { summary: summaryText } : null;
44
+ for (const match of matches) {
45
+ await store.upsert(runId, turn, match.path, entry.body, 200, {
46
+ fidelity: fidelityAttr,
47
+ attributes: entryAttrs,
48
+ loopId,
49
+ });
50
+ }
51
+ if (matches.length === 0) {
52
+ await store.upsert(runId, turn, target, entry.body, 200, {
53
+ fidelity: fidelityAttr,
54
+ attributes: entryAttrs,
55
+ loopId,
56
+ });
57
+ }
58
+ } else {
59
+ // No body — change fidelity, attach summary if provided
60
+ for (const match of matches) {
61
+ await store.setFidelity(runId, match.path, fidelityAttr);
62
+ if (summaryText) {
63
+ await store.setAttributes(runId, match.path, {
64
+ summary: summaryText,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ const label =
70
+ fidelityAttr === "archive" ? "archived" : `set to ${fidelityAttr}`;
71
+ const body =
72
+ matches.length > 0
73
+ ? `${matches.map((m) => m.path).join(", ")} ${label}`
74
+ : `${target} not found`;
75
+ await store.upsert(runId, turn, entry.resultPath, body, 200, {
76
+ fidelity: "archive",
77
+ loopId,
78
+ });
79
+ return;
80
+ }
81
+
29
82
  if (attrs.blocks || attrs.search != null) {
30
83
  await this.#processEdit(rummy, entry, attrs);
31
84
  return;
@@ -45,7 +98,7 @@ export default class Set {
45
98
  attrs.path,
46
99
  attrs.body,
47
100
  matches,
48
- true,
101
+ { preview: true, loopId },
49
102
  );
50
103
  return;
51
104
  }
@@ -57,8 +110,9 @@ export default class Set {
57
110
  if (scheme === null) {
58
111
  const udiff = generatePatch(target, "", entry.body || "");
59
112
  const merge = `<<<<<<< SEARCH\n=======\n${entry.body || ""}\n>>>>>>> REPLACE`;
60
- await store.upsert(runId, turn, entry.resultPath, "", "proposed", {
113
+ await store.upsert(runId, turn, entry.resultPath, "", 202, {
61
114
  attributes: { file: target, patch: udiff, merge },
115
+ loopId,
62
116
  });
63
117
  } else if (attrs.filter || target.includes("*")) {
64
118
  const matches = await store.getEntriesByPattern(
@@ -80,9 +134,10 @@ export default class Set {
80
134
  target,
81
135
  attrs.filter,
82
136
  matches,
137
+ { loopId },
83
138
  );
84
139
  } else {
85
- await store.upsert(runId, turn, target, entry.body, "full");
140
+ await store.upsert(runId, turn, target, entry.body, 200, { loopId });
86
141
  }
87
142
  }
88
143
 
@@ -103,13 +158,14 @@ export default class Set {
103
158
  }
104
159
 
105
160
  async #processEdit(rummy, entry, attrs) {
106
- const { entries: store, sequence: turn, runId } = rummy;
161
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
107
162
  const target = attrs.path;
108
163
  const matches = await store.getEntriesByPattern(runId, target, attrs.body);
109
164
 
110
165
  if (matches.length === 0) {
111
- await store.upsert(runId, turn, entry.resultPath, "", "error", {
166
+ await store.upsert(runId, turn, entry.resultPath, "", 404, {
112
167
  attributes: { file: target, error: `${target} not found in context` },
168
+ loopId,
113
169
  });
114
170
  return;
115
171
  }
@@ -121,8 +177,9 @@ export default class Set {
121
177
  const existingAttrs = await rummy.getAttributes(canonicalPath);
122
178
  const revisions = existingAttrs?.revisions || [];
123
179
  revisions.push(revision);
124
- await store.upsert(runId, turn, canonicalPath, "", "full", {
180
+ await store.upsert(runId, turn, canonicalPath, "", 200, {
125
181
  attributes: { file: match.path, revisions },
182
+ loopId,
126
183
  });
127
184
  if (KnownStore.normalizePath(entry.resultPath) !== canonicalPath) {
128
185
  await store.remove(runId, entry.resultPath);
@@ -133,7 +190,7 @@ export default class Set {
133
190
  const { patch, searchText, replaceText, warning, error } =
134
191
  Set.#applyRevision(match.body, attrs);
135
192
 
136
- const state = error ? "error" : "pass";
193
+ const status = error ? 409 : 200;
137
194
  const resultPath = `set://${match.path}`;
138
195
  const udiff = patch ? generatePatch(match.path, match.body, patch) : null;
139
196
  const merge =
@@ -143,7 +200,7 @@ export default class Set {
143
200
  const beforeTokens = match.tokens_full || 0;
144
201
  const afterTokens = patch ? (patch.length / 4) | 0 : beforeTokens;
145
202
 
146
- await store.upsert(runId, turn, resultPath, match.body, state, {
203
+ await store.upsert(runId, turn, resultPath, match.body, status, {
147
204
  attributes: {
148
205
  file: match.path,
149
206
  patch: udiff,
@@ -153,16 +210,19 @@ export default class Set {
153
210
  warning,
154
211
  error,
155
212
  },
213
+ loopId,
156
214
  });
157
215
 
158
- if (state === "pass" && patch) {
159
- await store.upsert(runId, turn, match.path, patch, match.state);
216
+ if (status === 200 && patch) {
217
+ await store.upsert(runId, turn, match.path, patch, match.status, {
218
+ loopId,
219
+ });
160
220
  }
161
221
  }
162
222
  }
163
223
 
164
224
  async #materializeRevisions({ rummy }) {
165
- const { entries: store, sequence: turn, runId } = rummy;
225
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
166
226
  const setEntries = await store.getEntriesByPattern(runId, "set://*");
167
227
 
168
228
  for (const entry of setEntries) {
@@ -198,7 +258,7 @@ export default class Set {
198
258
  }
199
259
  }
200
260
 
201
- const state = lastError ? "error" : "proposed";
261
+ const state = lastError ? 409 : 202;
202
262
  const udiff =
203
263
  current !== original
204
264
  ? generatePatch(filePath, original, current)
@@ -217,6 +277,7 @@ export default class Set {
217
277
  warning: lastWarning,
218
278
  error: lastError,
219
279
  },
280
+ loopId,
220
281
  });
221
282
  }
222
283
  }
@@ -0,0 +1,45 @@
1
+ // Tool doc for <set>. 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 + body = edit content
6
+ ['## <set path="[path/to/file]">[edit]</set> - Edit a file or entry'],
7
+
8
+ // --- Examples: sed, SEARCH/REPLACE, fidelity control
9
+ [
10
+ 'Example: <set path="src/config.js">s/port = 3000/port = 8080/g</set>',
11
+ "Sed syntax: most common edit pattern. Shows s/old/new/ with g flag.",
12
+ ],
13
+ [
14
+ `Example: <set path="src/app.js"><<<<<<< SEARCH
15
+ // TODO: add error handling
16
+ =======
17
+ // error handler configured
18
+ >>>>>>> REPLACE</set>`,
19
+ "SEARCH/REPLACE block: literal match and replace. Use when sed escaping is complex.",
20
+ ],
21
+ [
22
+ 'Example: <set path="known://plan" stored summary="Migration plan for Q2"/>',
23
+ "Fidelity + summary: archive an entry while preserving a description. Lifecycle endpoint.",
24
+ ],
25
+
26
+ // --- Constraints
27
+ [
28
+ '* `fidelity="..."`: `archive`, `summary`, `index`, `full`',
29
+ "Fidelity control. Archive removes from context but preserves for retrieval.",
30
+ ],
31
+ [
32
+ '* `summary="..."` (<= 80 chars) persists across fidelity changes',
33
+ "Model-authored descriptions survive demotion. No janitorial pass needed.",
34
+ ],
35
+ [
36
+ "* YOU MUST NOT use <sh/> or <env/> to read, create, or edit files",
37
+ "Forces file operations through set/get. Prevents untracked mutations.",
38
+ ],
39
+ [
40
+ "* Editing: s/old/new/ sed patterns and literal SEARCH/REPLACE blocks",
41
+ "Both syntaxes supported. Hedberg normalizes either form.",
42
+ ],
43
+ ];
44
+
45
+ export default LINES.map(([text]) => text).join("\n");
@@ -5,9 +5,8 @@ Proposes shell command execution for client approval.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `sh`
8
- - **Modes**: act only
9
- - **Category**: act
10
- - **Handler**: Upserts the entry as `proposed` state. The client must approve execution.
8
+ - **Category**: `logging`
9
+ - **Handler**: Upserts the entry at status 202 (proposed). The client must approve execution.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows `sh {command}` followed by the entry body.
15
14
 
16
15
  ## Behavior
17
16
 
18
- All shell commands require client-side approval — nothing executes server-side. Act mode only; blocked in ask mode.
17
+ All shell commands require client-side approval — nothing executes
18
+ server-side. Act mode only; excluded in ask mode by `resolveForLoop`.
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./shDoc.js";
2
2
 
3
3
  export default class Sh {
4
4
  #core;
@@ -9,16 +9,17 @@ export default class Sh {
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.sh = 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, "proposed", {
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