@possumtech/rummy 0.3.0 → 0.4.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 (65) hide show
  1. package/.env.example +13 -1
  2. package/PLUGINS.md +1 -1
  3. package/README.md +5 -1
  4. package/SPEC.md +211 -54
  5. package/migrations/001_initial_schema.sql +3 -4
  6. package/package.json +7 -3
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +183 -238
  9. package/src/agent/ContextAssembler.js +2 -0
  10. package/src/agent/KnownStore.js +36 -85
  11. package/src/agent/ResponseHealer.js +65 -31
  12. package/src/agent/TurnExecutor.js +284 -382
  13. package/src/agent/XmlParser.js +28 -4
  14. package/src/agent/known_queries.sql +1 -1
  15. package/src/agent/known_store.sql +32 -34
  16. package/src/agent/runs.sql +2 -2
  17. package/src/agent/tokens.js +1 -0
  18. package/src/agent/turns.sql +5 -0
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +2 -4
  21. package/src/hooks/ToolRegistry.js +8 -13
  22. package/src/plugins/ask_user/ask_userDoc.js +3 -8
  23. package/src/plugins/budget/README.md +26 -30
  24. package/src/plugins/budget/budget.js +69 -36
  25. package/src/plugins/budget/recovery.js +47 -0
  26. package/src/plugins/cp/cp.js +1 -1
  27. package/src/plugins/cp/cpDoc.js +5 -10
  28. package/src/plugins/env/envDoc.js +3 -8
  29. package/src/plugins/get/get.js +70 -2
  30. package/src/plugins/get/getDoc.js +19 -16
  31. package/src/plugins/hedberg/matcher.js +10 -29
  32. package/src/plugins/helpers.js +2 -2
  33. package/src/plugins/instructions/instructions.js +3 -2
  34. package/src/plugins/instructions/preamble.md +33 -12
  35. package/src/plugins/known/known.js +66 -17
  36. package/src/plugins/known/knownDoc.js +7 -10
  37. package/src/plugins/mv/mv.js +18 -1
  38. package/src/plugins/mv/mvDoc.js +9 -10
  39. package/src/plugins/{current → performed}/README.md +4 -3
  40. package/src/plugins/{current/current.js → performed/performed.js} +15 -20
  41. package/src/plugins/policy/policy.js +47 -0
  42. package/src/plugins/previous/README.md +2 -1
  43. package/src/plugins/previous/previous.js +31 -25
  44. package/src/plugins/progress/README.md +1 -2
  45. package/src/plugins/progress/progress.js +10 -60
  46. package/src/plugins/prompt/prompt.js +10 -8
  47. package/src/plugins/rm/rm.js +27 -15
  48. package/src/plugins/rm/rmDoc.js +6 -11
  49. package/src/plugins/rpc/rpc.js +3 -1
  50. package/src/plugins/set/set.js +125 -92
  51. package/src/plugins/set/setDoc.js +28 -37
  52. package/src/plugins/sh/shDoc.js +2 -7
  53. package/src/plugins/summarize/summarize.js +7 -0
  54. package/src/plugins/summarize/summarizeDoc.js +6 -11
  55. package/src/plugins/telemetry/telemetry.js +14 -9
  56. package/src/plugins/think/think.js +12 -0
  57. package/src/plugins/think/thinkDoc.js +18 -0
  58. package/src/plugins/unknown/README.md +2 -1
  59. package/src/plugins/unknown/unknown.js +26 -4
  60. package/src/plugins/unknown/unknownDoc.js +9 -14
  61. package/src/plugins/update/update.js +7 -0
  62. package/src/plugins/update/updateDoc.js +6 -11
  63. package/src/server/ClientConnection.js +69 -45
  64. package/src/sql/v_model_context.sql +7 -17
  65. package/src/plugins/budget/BudgetGuard.js +0 -74
@@ -2,27 +2,22 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: path attr = source, body = destination
6
5
  ['## <cp path="[source]">[destination]</cp> - Copy a file or entry'],
7
-
8
- // --- Examples: single copy, glob batch, cross-scheme
9
6
  [
10
7
  'Example: <cp path="src/config.js">src/config.backup.js</cp>',
11
8
  "Simple file copy. Path = source, body = destination.",
12
9
  ],
13
10
  [
14
11
  'Example: <cp path="known://plan_*">known://archive_</cp>',
15
- "Glob batch copy across known entries. Shows pattern operations on cp.",
12
+ "Glob batch copy across known entries.",
16
13
  ],
17
-
18
- // --- Constraints
19
14
  [
20
- "* Source path accepts globs: `src/*.js`, `known://draft_*`",
21
- "Pattern support. Distributes glob teaching beyond get.",
15
+ "* Source path accepts patterns: `src/*.js`, `known://draft_*`",
16
+ "Pattern support consistent with get/rm.",
22
17
  ],
23
18
  [
24
- "* Use `preview` to check matches before bulk copy",
25
- "Safety pattern consistent with get and rm preview.",
19
+ "* Use `preview` to check matches before pattern-based bulk copy",
20
+ "Safety pattern consistent with rm.",
26
21
  ],
27
22
  ];
28
23
 
@@ -2,10 +2,7 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax
6
5
  ["## <env>[command]</env> - Run an exploratory shell command"],
7
-
8
- // --- Examples: version check and git status — safe, read-only commands
9
6
  [
10
7
  "Example: <env>npm --version</env>",
11
8
  "Version check. Safe, no side effects.",
@@ -14,15 +11,13 @@ const LINES = [
14
11
  "Example: <env>git log --oneline -5</env>",
15
12
  "Git history. Shows env for read-only investigation.",
16
13
  ],
17
-
18
- // --- Constraints: hard boundaries
19
14
  [
20
15
  '* 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.",
16
+ "Prevents cat/ls through shell. Forces file access through get.",
22
17
  ],
23
18
  [
24
- "* YOU MUST use <sh/> for commands with side effects",
25
- "Separates exploration from action. env = observe, sh = mutate.",
19
+ "* YOU MUST NOT use <env/> for commands with side effects",
20
+ "Separates exploration from action. env = observe only.",
26
21
  ],
27
22
  ];
28
23
 
@@ -30,13 +30,79 @@ export default class Get {
30
30
  const normalized = KnownStore.normalizePath(target);
31
31
  const bodyFilter = entry.attributes.body || null;
32
32
  const isPattern = bodyFilter || normalized.includes("*");
33
+
34
+ const line =
35
+ entry.attributes.line != null
36
+ ? Math.max(1, parseInt(entry.attributes.line, 10))
37
+ : null;
38
+ const limit =
39
+ entry.attributes.limit != null
40
+ ? Math.max(1, parseInt(entry.attributes.limit, 10))
41
+ : null;
42
+
33
43
  const matches = await store.getEntriesByPattern(
34
44
  runId,
35
45
  normalized,
36
46
  bodyFilter,
37
47
  );
38
48
 
49
+ // Partial read — no fidelity promotion, returns a line slice as the log item.
50
+ if (line !== null || limit !== null) {
51
+ if (isPattern) {
52
+ await store.upsert(
53
+ runId,
54
+ turn,
55
+ entry.resultPath,
56
+ "line/limit requires a single path, not a glob or body filter",
57
+ 400,
58
+ { loopId },
59
+ );
60
+ return;
61
+ }
62
+ if (matches.length === 0) {
63
+ await store.upsert(
64
+ runId,
65
+ turn,
66
+ entry.resultPath,
67
+ `${target} not found`,
68
+ 200,
69
+ { loopId },
70
+ );
71
+ return;
72
+ }
73
+ const allLines = matches[0].body.split("\n");
74
+ const total = allLines.length;
75
+ const startLine = line ?? 1;
76
+ const startIdx = startLine - 1;
77
+ const endIdx = limit !== null ? Math.min(startIdx + limit, total) : total;
78
+ const slice = allLines.slice(startIdx, endIdx).join("\n");
79
+ const endLine = endIdx;
80
+ const header = `[lines ${startLine}–${endLine} / ${total} total]`;
81
+ await store.upsert(
82
+ runId,
83
+ turn,
84
+ entry.resultPath,
85
+ `${header}\n${slice}`,
86
+ 200,
87
+ { loopId },
88
+ );
89
+ return;
90
+ }
91
+
92
+ const VALID_FIDELITY = {
93
+ summary: 1,
94
+ full: 1,
95
+ archive: 1,
96
+ };
97
+ const fidelityAttr = VALID_FIDELITY[entry.attributes.fidelity]
98
+ ? entry.attributes.fidelity
99
+ : null;
100
+
39
101
  await store.promoteByPattern(runId, normalized, bodyFilter, turn);
102
+ if (fidelityAttr) {
103
+ for (const match of matches)
104
+ await store.setFidelity(runId, match.path, fidelityAttr);
105
+ }
40
106
 
41
107
  if (isPattern) {
42
108
  await storePatternResult(
@@ -50,10 +116,12 @@ export default class Get {
50
116
  { loopId },
51
117
  );
52
118
  } else {
53
- const total = matches.reduce((s, m) => s + m.tokens_full, 0);
119
+ const total = matches.reduce((s, m) => s + m.tokens, 0);
54
120
  const paths = matches.map((m) => m.path).join(", ");
55
121
  const body =
56
- matches.length > 0 ? `${paths} ${total} tokens` : `${target} not found`;
122
+ matches.length > 0
123
+ ? `${paths} promoted to full (${total} tokens)`
124
+ : `${target} not found`;
57
125
  await store.upsert(runId, turn, entry.resultPath, body, 200, {
58
126
  loopId,
59
127
  });
@@ -2,39 +2,42 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: body-form is the primary invocation (simplest)
6
5
  ["## <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
6
  [
10
7
  "Example: <get>src/app.js</get>",
11
- "Simplest form. Body = path. Teaches that get is the default read tool.",
8
+ "Simplest form. Body = path.",
12
9
  ],
13
10
  [
14
11
  'Example: <get path="known://*">auth</get>',
15
- "Keyword recall: glob in path, search term in body. Cross-scheme hedberg pattern.",
12
+ "Keyword recall: glob in path, search term in body.",
16
13
  ],
17
14
  [
18
- 'Example: <get path="src/**/*.js" preview>TODO</get>',
19
- "Full pattern: recursive glob + preview + content filter. Shows all 3 features at once.",
15
+ 'Example: <get path="src/**/*.js" preview>authentication</get>',
16
+ "Full pattern: recursive glob + preview + content filter.",
17
+ ],
18
+ [
19
+ 'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
20
+ "Partial read. Returns lines 644–723 without promoting.",
20
21
  ],
21
-
22
- // --- Constraints: RFC-style. Each prevents a specific failure mode.
23
22
  [
24
- "* Paths accept globs: `src/**/*.js`, `known://api_*`",
25
- "Reinforces picomatch patterns work everywhere, not just in examples.",
23
+ "* Paths accept patterns: `src/**/*.js`, `known://api_*`",
24
+ "Reinforces picomatch patterns work everywhere.",
26
25
  ],
27
26
  [
28
- "* `preview` shows matches without loading into context",
29
- "Budget-awareness. Without this, models load everything and blow context.",
27
+ "* `preview` lists matches without loading into context",
28
+ "Budget-awareness. Preview avoids promotion.",
30
29
  ],
31
30
  [
32
31
  "* Body text filters results by content match",
33
- "Generalizes examples 2-3. Body = filter, not just path.",
32
+ "Body = filter, not just path.",
33
+ ],
34
+ [
35
+ "* `line` and `limit` read a slice without promoting — patterns not allowed",
36
+ "Partial read is safe: context budget unaffected.",
34
37
  ],
35
38
  [
36
- '* Use <set path="..." fidelity="index"/> to archive loaded content',
37
- "Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
39
+ '* Use <set path="src/file.txt" fidelity="summary"/> when the content is irrelevant to save tokens.',
40
+ "Cross-tool lifecycle: get promotes, set demotes.",
38
41
  ],
39
42
  ];
40
43
 
@@ -1,34 +1,15 @@
1
- import { execSync } from "node:child_process";
2
- import { unlinkSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
1
+ import { createTwoFilesPatch } from "diff";
5
2
 
6
3
  export function generatePatch(filePath, oldContent, newContent) {
7
- const id = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
8
- const oldPath = join(tmpdir(), `rummy_diff_old_${id}`);
9
- const newPath = join(tmpdir(), `rummy_diff_new_${id}`);
10
-
11
- try {
12
- writeFileSync(oldPath, oldContent);
13
- writeFileSync(newPath, newContent);
14
-
15
- const result = execSync(
16
- `diff -u --label "${filePath}\told" --label "${filePath}\tnew" "${oldPath}" "${newPath}"`,
17
- { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
18
- );
19
- return result;
20
- } catch (err) {
21
- // diff exits 1 when files differ — that's the success case
22
- if (err.stdout) return err.stdout;
23
- return "";
24
- } finally {
25
- try {
26
- unlinkSync(oldPath);
27
- } catch {}
28
- try {
29
- unlinkSync(newPath);
30
- } catch {}
31
- }
4
+ return createTwoFilesPatch(
5
+ `${filePath}\told`,
6
+ `${filePath}\tnew`,
7
+ oldContent,
8
+ newContent,
9
+ "",
10
+ "",
11
+ { context: 3 },
12
+ );
32
13
  }
33
14
 
34
15
  export default class HeuristicMatcher {
@@ -14,8 +14,8 @@ export async function storePatternResult(
14
14
  ) {
15
15
  const slug = await store.slugPath(runId, scheme, path);
16
16
  const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
17
- const total = matches.reduce((s, m) => s + m.tokens_full, 0);
18
- const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
17
+ const total = matches.reduce((s, m) => s + m.tokens, 0);
18
+ const listing = matches.map((m) => `${m.path} (${m.tokens})`).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
21
  await store.upsert(runId, turn, slug, body, 200, { loopId });
@@ -37,7 +37,6 @@ export default class Instructions {
37
37
  activeTools.has(n),
38
38
  );
39
39
  const tools = sorted.join(", ");
40
- let prompt = preamble.replace("[%TOOLS%]", tools);
41
40
  const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
42
41
  {},
43
42
  { toolSet: activeTools },
@@ -46,7 +45,9 @@ export default class Instructions {
46
45
  .filter((key) => toolDocs[key])
47
46
  .map((key) => toolDocs[key])
48
47
  .join("\n\n");
49
- if (docsText) prompt += `\n\n${docsText}`;
48
+ let prompt = preamble
49
+ .replace("[%TOOLS%]", tools)
50
+ .replace("[%TOOLDOCS%]", docsText);
50
51
  if (attrs.persona) prompt += `\n\n## Persona\n\n${attrs.persona}`;
51
52
  return prompt;
52
53
  }
@@ -1,16 +1,37 @@
1
- You are an assistant. YOU MUST gather information, then YOU MAY either answer questions or take action.
2
-
3
- # Response Rules
4
-
5
- Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use multiple tools in your response.
6
- Optional: YOU MAY think in an optional <think></think> tag before using any other Tool Commands.
7
- Required: YOU MUST register all unknowns with <unknown>(specific thing I need to learn)</unknown>.
8
- Required: YOU MUST register all new information, decisions, and plans with <known>(specific information, ideas, or plans)</known>.
9
- Required: YOU MUST conclude every turn with EITHER <update/> if still working OR <summarize/> if done. Never both.
10
- Required: Path and summary information is approximate. YOU MUST use <get> to verify before acting on summarized content.
11
- Info: When information conflicts, later turns are more likely to be relevant and correct than earlier turns.
12
- Info: Your context is limited but your storage is not. Organize and categorize your information, ideas, plans, and history to optimize your context.
1
+ You are a folksonomic knowledgebase assistant. YOU MUST discern what you don't know into unknowns, then extract and organize your findings into navigable and searchable knowns, then YOU MAY answer questions and/or perform actions.
13
2
 
14
3
  # Tool Commands
15
4
 
16
5
  Tools: [%TOOLS%]
6
+
7
+ # Tool Rules
8
+
9
+ ## Response Rules
10
+ Required: YOU MUST respond with Tool Commands in the XML format. YOU MAY use up to 12 tools in your response.
11
+ Required: YOU MUST register all unknowns with <unknown>[specific thing I need to learn]</unknown>.
12
+ Required: YOU MUST register all new facts, decisions, and plans with <known path="topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known>.
13
+
14
+ ## Folksonomic Memory Management
15
+ * Write paths with navigable hierarchies and summaries with searchable tags.
16
+ * When new facts, decisions, and plans appear, set them as <known/> entries with navigable hierarchies and summaries with searchable tags to improve your folksonomic knowledgebase.
17
+ * When new questions emerge, use pattern matching operations to optimize the fidelity and relevance of your knowledgebase.
18
+ * The turn attribute can be helpful for discerning what's fresh or stale, prefer more recent information if conflicts exist.
19
+ * YOU MUST promote all relevant entries and demote all irrelevant entries before acting or answering. Use body pattern search (Example: <get path="known://*">John Doe</get>) to recall archived entries when needed.
20
+ * Logging entries in <previous/> can also be demoted to optimize context.
21
+
22
+ ## Fidelity Management
23
+ * full: Entire contents are shown (consumes token budget)
24
+ * summary: Only path and summary are shown. (<= 80 chars, saves token budget)
25
+ * archive: Archived in an unlimited archive. Entries can be recalled with path recall or pattern search. (use caution)
26
+
27
+ ## Token Budget Management
28
+ * Entries contain a "fidelity" and a "token" attribute to enable token budget management and context optimization.
29
+ * Set relevant entries to "full" and irrelevant entries to "summary" to optimize context.
30
+ * The less irrelevant information in your context, the better.
31
+
32
+ ## Response Termination
33
+ Required: YOU MUST conclude every turn with EITHER <update></update> if still working OR <summarize></summarize> if done. Never both.
34
+
35
+ # Tool Usage
36
+
37
+ [%TOOLDOCS%]
@@ -1,5 +1,8 @@
1
+ import { countTokens } from "../../agent/tokens.js";
1
2
  import docs from "./knownDoc.js";
2
3
 
4
+ const MAX_ENTRY_TOKENS = Number(process.env.RUMMY_MAX_ENTRY_TOKENS) || 512;
5
+
3
6
  export default class Known {
4
7
  #core;
5
8
 
@@ -8,6 +11,7 @@ export default class Known {
8
11
  core.registerScheme({ category: "data" });
9
12
  core.on("handler", this.handler.bind(this));
10
13
  core.on("full", this.full.bind(this));
14
+ core.on("summary", this.summary.bind(this));
11
15
  core.filter("assembly.system", this.assembleKnown.bind(this), 100);
12
16
  core.filter("instructions.toolDocs", async (docsMap) => {
13
17
  docsMap.known = docs;
@@ -16,28 +20,78 @@ export default class Known {
16
20
  }
17
21
 
18
22
  async handler(entry, rummy) {
19
- const { entries: store, sequence: turn, runId } = rummy;
20
- const target = entry.attributes.path || entry.resultPath;
21
- await store.upsert(runId, turn, target, entry.body, 200);
23
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
24
+ if (!entry.body) return;
25
+
26
+ // Size gate
27
+ const entryTokens = countTokens(entry.body);
28
+ if (entryTokens > MAX_ENTRY_TOKENS) {
29
+ const rejectPath = await store.slugPath(runId, "known", entry.body);
30
+ await store.upsert(
31
+ runId,
32
+ turn,
33
+ rejectPath,
34
+ `Entry too large (${entryTokens} tokens, max ${MAX_ENTRY_TOKENS}). Sort the information, ideas, or plans carefully into multiple entries.`,
35
+ 413,
36
+ { loopId },
37
+ );
38
+ return;
39
+ }
40
+
41
+ // Resolve path: explicit or auto-generated slug
42
+ let knownPath = entry.attributes?.path || null;
43
+ if (knownPath && !knownPath.includes("://")) {
44
+ knownPath = `known://${knownPath}`;
45
+ }
46
+ if (!knownPath) {
47
+ knownPath = await store.slugPath(
48
+ runId,
49
+ "known",
50
+ entry.body,
51
+ entry.attributes?.summary,
52
+ );
53
+ }
54
+
55
+ // Dedup: if path exists, update rather than duplicate
56
+ const existing = await store.getEntriesByPattern(runId, knownPath, null);
57
+ if (existing.length > 0) {
58
+ await store.upsert(
59
+ runId,
60
+ turn,
61
+ existing[0].path,
62
+ entry.body || existing[0].body,
63
+ 200,
64
+ { attributes: entry.attributes, loopId },
65
+ );
66
+ return;
67
+ }
68
+
69
+ await store.upsert(runId, turn, knownPath, entry.body, 200, {
70
+ attributes: entry.attributes,
71
+ loopId,
72
+ });
22
73
  }
23
74
 
24
75
  full(entry) {
25
76
  return `# known ${entry.path}\n${entry.body}`;
26
77
  }
27
78
 
79
+ summary(entry) {
80
+ return this.full(entry);
81
+ }
82
+
28
83
  async assembleKnown(content, ctx) {
29
84
  const entries = ctx.rows.filter((r) => r.category === "data");
30
85
  if (entries.length === 0) return content;
31
86
 
32
- // Rows arrive pre-sorted by SQL: skill → index → summary → full, then by recency
87
+ // Rows arrive pre-sorted by SQL: summary → full, then by recency
33
88
  const demotedSet = new Set(ctx.demoted || []);
34
- const panic = ctx.type === "panic";
35
- const lines = entries.map((e) => renderKnownTag(e, demotedSet, panic));
89
+ const lines = entries.map((e) => renderKnownTag(e, demotedSet));
36
90
  return `${content}\n\n<knowns>\n${lines.join("\n")}\n</knowns>`;
37
91
  }
38
92
  }
39
93
 
40
- function renderKnownTag(entry, demotedSet, panic = false) {
94
+ function renderKnownTag(entry, demotedSet) {
41
95
  const tag = entry.scheme || "file";
42
96
  const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
43
97
  const tokens = entry.tokens ? ` tokens="${entry.tokens}"` : "";
@@ -45,23 +99,18 @@ function renderKnownTag(entry, demotedSet, panic = false) {
45
99
  const fidelity = entry.fidelity ? ` fidelity="${entry.fidelity}"` : "";
46
100
  const flag = demotedSet?.has(entry.path) ? " demoted" : "";
47
101
 
48
- // Panic mode: index-only view so context fits in LLM window
49
- if (panic) {
50
- return `<${tag} path="${entry.path}"${turn}${fidelity}${tokens}/>`;
51
- }
52
-
53
102
  const attrs =
54
103
  typeof entry.attributes === "string"
55
104
  ? JSON.parse(entry.attributes)
56
105
  : entry.attributes;
57
106
  const summary =
58
107
  typeof attrs?.summary === "string"
59
- ? ` summary="${attrs.summary.slice(0, 80)}"`
108
+ ? ` summary="${attrs.summary.replace(/"/g, "'").slice(0, 80)}"`
60
109
  : "";
61
110
 
62
- if (entry.body) {
63
- return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}>${entry.body}</${tag}>`;
111
+ if (entry.fidelity === "archive") return "";
112
+ if (entry.fidelity === "summary") {
113
+ return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}/>`;
64
114
  }
65
-
66
- return `<${tag} path="${entry.path}"${turn}${status}${fidelity}${summary}${tokens}${flag}/>`;
115
+ return `<${tag} path="${entry.path}"${turn}${status}${summary}${fidelity}${tokens}${flag}>${entry.body}</${tag}>`;
67
116
  }
@@ -2,30 +2,27 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: body = the information to save
6
5
  [
7
- "## <known>[specific information, ideas, or plans]</known> - Sort and save what you learn for later recall",
6
+ '## <known path="known://topic/subtopic" summary="keyword,keyword,keyword">[specific facts, decisions, or plans]</known> - Sort and save what you learn for later recall',
8
7
  ],
9
- // --- Examples: summary-with-keywords first (teaches the right pattern)
10
8
  [
11
9
  'Example: <known summary="hedberg,comedian,death,2005">Mitch Hedberg died on March 30, 2005</known>',
12
- "Primary pattern: comma-separated keywords in summary. Path auto-generated from summary as known://hedberg/comedian/death/2005. Keywords become searchable path segments.",
10
+ "Summary-first pattern: comma-separated keywords, path auto-generated.",
13
11
  ],
14
12
  [
15
13
  'Example: <known path="known://people/rumsfeld" summary="defense,secretary,born,1932">Donald Rumsfeld was born in 1932 and served as Secretary of Defense</known>',
16
- "Explicit path form: slashed path=category/key, summary=keywords. For when the model wants direct control over taxonomy.",
14
+ "Explicit path form: slashed path=category/key, summary=keywords.",
17
15
  ],
18
- // --- Lifecycle
19
16
  [
20
17
  '* Recall with <get path="known://people/*">keyword</get>',
21
- "Cross-tool lifecycle: glob by category, filter by keyword. Matches the slashed path convention.",
18
+ "Cross-tool lifecycle: glob by category, filter by keyword.",
22
19
  ],
23
20
  [
24
- "* `summary` keywords survive compression — write keywords you'll search for later",
25
- "Teaches WHY summaries matter. Keywords become the path AND the compressed view.",
21
+ "* YOU SHOULD write `summary` keywords, you can search for them later",
22
+ "Motivates summary writing through self-interest.",
26
23
  ],
27
24
  [
28
- "* YOU MUST sort and save all new information, ideas, and plans in their own <known> entries",
25
+ "* YOU MUST sort and save all new facts, decisions, and plans in their own <known> entries",
29
26
  "Critical behavioral constraint. 'new' prevents re-saving known facts.",
30
27
  ],
31
28
  ];
@@ -19,11 +19,28 @@ export default class Mv {
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 };
22
+ const VALID = { stored: 1, summary: 1, index: 1, full: 1, archive: 1 };
23
23
  const fidelity = VALID[entry.attributes.fidelity]
24
24
  ? entry.attributes.fidelity
25
25
  : undefined;
26
26
 
27
+ // Fidelity-in-place: no destination, change visibility of matched entries
28
+ if (fidelity && !to) {
29
+ const matches = await store.getEntriesByPattern(runId, path);
30
+ for (const match of matches)
31
+ await store.setFidelity(runId, match.path, fidelity);
32
+ const label = fidelity === "archive" ? "archived" : `set to ${fidelity}`;
33
+ await store.upsert(
34
+ runId,
35
+ turn,
36
+ entry.resultPath,
37
+ `${matches.map((m) => m.path).join(", ")} ${label}`,
38
+ 200,
39
+ { fidelity: "archive", loopId },
40
+ );
41
+ return;
42
+ }
43
+
27
44
  const source = await store.getBody(runId, path);
28
45
  if (source === null) return;
29
46
 
@@ -2,29 +2,28 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax: path attr = source, body = destination
6
5
  [
7
6
  '## <mv path="[source]">[destination]</mv> - Move or rename a file or entry',
8
7
  ],
9
-
10
- // --- Examples: entry rename and file move
11
8
  [
12
9
  'Example: <mv path="known://active_task">known://completed_task</mv>',
13
- "Entry rename. Most common mv use case. Shows known:// path convention.",
10
+ "Entry rename. Most common mv use case.",
14
11
  ],
15
12
  [
16
13
  'Example: <mv path="src/old_name.js">src/new_name.js</mv>',
17
- "File rename. Shows that mv works on files too, not just known entries.",
14
+ "File rename.",
15
+ ],
16
+ [
17
+ 'Example: <mv path="known://project/*" fidelity="summary"/>',
18
+ "Batch fidelity change via pattern. No destination = fidelity in place.",
18
19
  ],
19
-
20
- // --- Constraints
21
20
  [
22
- "* Source path accepts globs for batch moves",
21
+ "* Source path accepts patterns for batch moves",
23
22
  "Pattern support consistent with get/cp/rm.",
24
23
  ],
25
24
  [
26
- "* In ask mode, destination MUST be a scheme path (not a file)",
27
- "Mode constraint. Prevents file mutations in ask mode via mv.",
25
+ "* Use `preview` to check matches before pattern-based bulk moves",
26
+ "Safety pattern consistent with rm/cp.",
28
27
  ],
29
28
  ];
30
29
 
@@ -1,6 +1,6 @@
1
- # current
1
+ # performed
2
2
 
3
- Renders the `<current>` section of the user message — the active loop's
3
+ Renders the `<performed>` section of the user message — the active loop's
4
4
  tool results and lifecycle signals.
5
5
 
6
6
  ## Registration
@@ -11,4 +11,5 @@ tool results and lifecycle signals.
11
11
 
12
12
  Filters turn_context rows where `category === "logging"` and
13
13
  `source_turn >= loopStartTurn`. Renders each entry chronologically
14
- with turn number and status. Empty on the first turn of a loop.
14
+ with turn, status, summary, fidelity, and tokens. Empty on the first
15
+ turn of a loop.