@possumtech/rummy 2.0.0 → 2.1.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 (117) hide show
  1. package/.env.example +31 -5
  2. package/BENCH_ENVIRONMENT.md +230 -0
  3. package/CLIENT_INTERFACE.md +396 -0
  4. package/PLUGINS.md +93 -1
  5. package/SPEC.md +389 -28
  6. package/bin/postinstall.js +2 -2
  7. package/bin/rummy.js +2 -2
  8. package/last_run.txt +5617 -0
  9. package/migrations/001_initial_schema.sql +2 -1
  10. package/package.json +13 -9
  11. package/scriptify/ask_run.js +77 -0
  12. package/scriptify/cache_probe.js +66 -0
  13. package/scriptify/cache_probe_grok.js +74 -0
  14. package/service.js +22 -11
  15. package/src/agent/AgentLoop.js +62 -157
  16. package/src/agent/ContextAssembler.js +2 -9
  17. package/src/agent/Entries.js +54 -98
  18. package/src/agent/ProjectAgent.js +4 -11
  19. package/src/agent/TurnExecutor.js +48 -83
  20. package/src/agent/XmlParser.js +247 -273
  21. package/src/agent/budget.js +5 -28
  22. package/src/agent/config.js +38 -0
  23. package/src/agent/errors.js +7 -13
  24. package/src/agent/httpStatus.js +1 -19
  25. package/src/agent/known_queries.sql +1 -1
  26. package/src/agent/known_store.sql +12 -2
  27. package/src/agent/materializeContext.js +15 -18
  28. package/src/agent/pathEncode.js +5 -0
  29. package/src/agent/rummyHome.js +9 -0
  30. package/src/agent/runs.sql +37 -0
  31. package/src/agent/tokens.js +7 -7
  32. package/src/hooks/HookRegistry.js +1 -16
  33. package/src/hooks/Hooks.js +8 -33
  34. package/src/hooks/PluginContext.js +3 -21
  35. package/src/hooks/RpcRegistry.js +1 -4
  36. package/src/hooks/RummyContext.js +6 -16
  37. package/src/hooks/ToolRegistry.js +5 -15
  38. package/src/llm/LlmProvider.js +41 -33
  39. package/src/llm/errors.js +41 -4
  40. package/src/llm/openaiStream.js +125 -0
  41. package/src/llm/retry.js +109 -0
  42. package/src/plugins/budget/budget.js +55 -76
  43. package/src/plugins/cli/README.md +87 -0
  44. package/src/plugins/cli/bin.js +61 -0
  45. package/src/plugins/cli/cli.js +120 -0
  46. package/src/plugins/env/README.md +2 -1
  47. package/src/plugins/env/env.js +4 -6
  48. package/src/plugins/env/envDoc.md +2 -2
  49. package/src/plugins/error/error.js +23 -23
  50. package/src/plugins/file/file.js +2 -22
  51. package/src/plugins/get/get.js +12 -34
  52. package/src/plugins/get/getDoc.md +8 -6
  53. package/src/plugins/hedberg/edits.js +1 -11
  54. package/src/plugins/hedberg/hedberg.js +3 -26
  55. package/src/plugins/hedberg/normalize.js +1 -5
  56. package/src/plugins/hedberg/patterns.js +4 -15
  57. package/src/plugins/hedberg/sed.js +1 -7
  58. package/src/plugins/helpers.js +28 -20
  59. package/src/plugins/index.js +25 -41
  60. package/src/plugins/instructions/README.md +18 -0
  61. package/src/plugins/instructions/instructions.js +97 -38
  62. package/src/plugins/instructions/instructions.md +24 -15
  63. package/src/plugins/instructions/instructions_104.md +5 -4
  64. package/src/plugins/instructions/instructions_105.md +29 -36
  65. package/src/plugins/instructions/instructions_106.md +22 -0
  66. package/src/plugins/instructions/instructions_107.md +17 -0
  67. package/src/plugins/instructions/instructions_108.md +0 -8
  68. package/src/plugins/known/README.md +26 -6
  69. package/src/plugins/known/known.js +37 -34
  70. package/src/plugins/log/README.md +2 -2
  71. package/src/plugins/log/log.js +27 -34
  72. package/src/plugins/ollama/ollama.js +50 -66
  73. package/src/plugins/openai/openai.js +26 -44
  74. package/src/plugins/openrouter/openrouter.js +28 -52
  75. package/src/plugins/policy/README.md +8 -2
  76. package/src/plugins/policy/policy.js +8 -21
  77. package/src/plugins/prompt/README.md +22 -0
  78. package/src/plugins/prompt/prompt.js +14 -16
  79. package/src/plugins/rm/rm.js +5 -2
  80. package/src/plugins/rm/rmDoc.md +4 -4
  81. package/src/plugins/rpc/README.md +2 -1
  82. package/src/plugins/rpc/rpc.js +62 -48
  83. package/src/plugins/set/README.md +5 -1
  84. package/src/plugins/set/set.js +23 -33
  85. package/src/plugins/set/setDoc.md +1 -1
  86. package/src/plugins/sh/README.md +2 -1
  87. package/src/plugins/sh/sh.js +5 -11
  88. package/src/plugins/sh/shDoc.md +2 -2
  89. package/src/plugins/stream/README.md +6 -5
  90. package/src/plugins/stream/stream.js +6 -35
  91. package/src/plugins/telemetry/telemetry.js +26 -19
  92. package/src/plugins/think/think.js +4 -7
  93. package/src/plugins/unknown/unknown.js +8 -13
  94. package/src/plugins/update/update.js +42 -25
  95. package/src/plugins/update/updateDoc.md +3 -3
  96. package/src/plugins/xai/xai.js +30 -20
  97. package/src/plugins/yolo/yolo.js +159 -0
  98. package/src/server/ClientConnection.js +17 -47
  99. package/src/server/SocketServer.js +14 -14
  100. package/src/server/protocol.js +1 -10
  101. package/src/sql/functions/slugify.js +5 -7
  102. package/src/sql/v_model_context.sql +4 -11
  103. package/turns/cli_1777462658211/turn_001.txt +772 -0
  104. package/turns/cli_1777462658211/turn_002.txt +606 -0
  105. package/turns/cli_1777462658211/turn_003.txt +667 -0
  106. package/turns/cli_1777462658211/turn_004.txt +297 -0
  107. package/turns/cli_1777462658211/turn_005.txt +301 -0
  108. package/turns/cli_1777462658211/turn_006.txt +262 -0
  109. package/turns/cli_1777465095132/turn_001.txt +715 -0
  110. package/turns/cli_1777465095132/turn_002.txt +236 -0
  111. package/turns/cli_1777465095132/turn_003.txt +287 -0
  112. package/turns/cli_1777465095132/turn_004.txt +694 -0
  113. package/turns/cli_1777465095132/turn_005.txt +422 -0
  114. package/turns/cli_1777465095132/turn_006.txt +365 -0
  115. package/turns/cli_1777465095132/turn_007.txt +885 -0
  116. package/turns/cli_1777465095132/turn_008.txt +1277 -0
  117. package/turns/cli_1777465095132/turn_009.txt +736 -0
@@ -1,46 +1,23 @@
1
+ import config from "./config.js";
1
2
  import { countTokens } from "./tokens.js";
2
3
 
3
- const CEILING_RATIO = Number(process.env.RUMMY_BUDGET_CEILING);
4
- if (!CEILING_RATIO) throw new Error("RUMMY_BUDGET_CEILING must be set");
4
+ const CEILING_RATIO = config.BUDGET_CEILING;
5
5
 
6
6
  export function ceiling(contextSize) {
7
7
  return Math.floor(contextSize * CEILING_RATIO);
8
8
  }
9
9
 
10
- /**
11
- * Sum assembled-message token counts.
12
- * Used by the budget enforce gate, which has the real messages.
13
- */
10
+ // Sum assembled-message token counts; used by the budget enforce gate.
14
11
  export function measureMessages(messages) {
15
12
  return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
16
13
  }
17
14
 
18
- /**
19
- * Sum projected row body token counts — what's actually in the packet
20
- * for each entry at its current visibility. Used by prompt.js while
21
- * generating the <prompt> tag (before assembly completes).
22
- */
15
+ // Sum projected row body token counts; used by prompt.js pre-assembly.
23
16
  export function measureRows(rows) {
24
17
  return rows.reduce((sum, r) => sum + countTokens(r.body), 0);
25
18
  }
26
19
 
27
- /**
28
- * Single source of truth for budget numbers. Every caller — prompt.js
29
- * generating the <prompt> tag, budget.js enforcing the ceiling,
30
- * AgentLoop emitting telemetry — passes in its own measured totalTokens
31
- * and reads the same object back. No fallbacks: callers produce the
32
- * measurement they have.
33
- *
34
- * Returns:
35
- * ceiling — floor(contextSize × CEILING_RATIO), the hard wall
36
- * totalTokens — echoed back (the full packet size the caller measured)
37
- * tokenUsage — same as totalTokens. Kept under this name for the
38
- * `<prompt tokenUsage="N">` attribute on the wire. Must
39
- * agree with totalTokens so the model's math is honest.
40
- * tokensFree — ceiling − totalTokens (floor 0)
41
- * overflow — max(0, totalTokens − ceiling)
42
- * ok — overflow === 0
43
- */
20
+ // Single source of truth for budget numbers; tokenUsage echoes totalTokens for the wire attribute.
44
21
  export function computeBudget({ contextSize, totalTokens }) {
45
22
  const cap = ceiling(contextSize);
46
23
  const tokensFree = Math.max(0, cap - totalTokens);
@@ -0,0 +1,38 @@
1
+ // Validates required RUMMY_* env at module load; defaults in .env.example.
2
+
3
+ const REQUIRED = {
4
+ BUDGET_CEILING: { env: "RUMMY_BUDGET_CEILING", parse: Number },
5
+ LLM_DEADLINE: { env: "RUMMY_LLM_DEADLINE", parse: Number },
6
+ LLM_MAX_BACKOFF: { env: "RUMMY_LLM_MAX_BACKOFF", parse: Number },
7
+ FETCH_TIMEOUT: { env: "RUMMY_FETCH_TIMEOUT", parse: Number },
8
+ MAX_STRIKES: { env: "RUMMY_MAX_STRIKES", parse: Number },
9
+ MIN_CYCLES: { env: "RUMMY_MIN_CYCLES", parse: Number },
10
+ MAX_CYCLE_PERIOD: { env: "RUMMY_MAX_CYCLE_PERIOD", parse: Number },
11
+ RUN_TIMEOUT: { env: "RUMMY_RUN_TIMEOUT", parse: Number },
12
+ PLUGINS_LOAD_TIMEOUT: { env: "RUMMY_PLUGINS_LOAD_TIMEOUT", parse: Number },
13
+ THINK: { env: "RUMMY_THINK", parse: (v) => v },
14
+ };
15
+
16
+ const config = {};
17
+ const missing = [];
18
+ for (const [key, spec] of Object.entries(REQUIRED)) {
19
+ const raw = process.env[spec.env];
20
+ if (raw === undefined || raw === "") {
21
+ missing.push(spec.env);
22
+ continue;
23
+ }
24
+ const parsed = spec.parse(raw);
25
+ if (typeof parsed === "number" && Number.isNaN(parsed)) {
26
+ missing.push(`${spec.env} (got "${raw}", expected number)`);
27
+ continue;
28
+ }
29
+ config[key] = parsed;
30
+ }
31
+ if (missing.length > 0) {
32
+ throw new Error(
33
+ `RUMMY config missing or invalid: ${missing.join(", ")}. ` +
34
+ "Set in .env, .env.example, or shell env.",
35
+ );
36
+ }
37
+
38
+ export default Object.freeze(config);
@@ -1,21 +1,15 @@
1
- /**
2
- * Typed errors for the agent/Entries layer. Callers catch by type,
3
- * not by regex.
4
- */
5
-
6
- /**
7
- * Thrown when a writer tier isn't permitted to write to a scheme.
8
- * See SPEC writer_tiers: schemes declare writable_by = subset of
9
- * {system, plugin, client, model}. A write from an excluded tier
10
- * rejects with this error.
11
- */
1
+ // Writer tier excluded from scheme.writable_by; see SPEC writer_tiers.
12
2
  export class PermissionError extends Error {
13
3
  constructor(scheme, writer, allowed) {
4
+ // Paths without `://` have a null scheme. Report null verbatim
5
+ // rather than substituting a plausible-sounding "file" — there is
6
+ // no scheme called "file" and the error must reflect actual state.
7
+ const schemeLabel = scheme === null ? "(none)" : scheme;
14
8
  super(
15
- `403: writer "${writer}" not permitted for scheme "${scheme ?? "file"}" (allowed: ${allowed.join(", ")})`,
9
+ `403: writer "${writer}" not permitted for scheme "${schemeLabel}" (allowed: ${allowed.join(", ")})`,
16
10
  );
17
11
  this.name = "PermissionError";
18
- this.scheme = scheme ?? "file";
12
+ this.scheme = scheme;
19
13
  this.writer = writer;
20
14
  this.allowed = [...allowed];
21
15
  }
@@ -1,22 +1,4 @@
1
- /**
2
- * Map the entry-layer (state, outcome) tuple to an HTTP status number for
3
- * model-facing tag rendering.
4
- *
5
- * Model-facing tags still carry `status="NNN"` because the model's
6
- * vocabulary (instructions + tooldocs + training) is HTTP-shaped. The DB
7
- * stores categorical state + textual outcome (see SPEC entries); this helper
8
- * is the one-way translation for rendering.
9
- *
10
- * Outcome strings prefixed with a 3-digit HTTP code (e.g.
11
- * `"overflow:413:..."` or `"permission:403:..."`) extract the code
12
- * verbatim. Otherwise state maps to a canonical HTTP:
13
- *
14
- * resolved → 200
15
- * proposed → 202
16
- * streaming → 102
17
- * cancelled → 499
18
- * failed → 500 (unless outcome carries a code)
19
- */
1
+ // (state, outcome) → HTTP status for model-facing tags; outcome's 3-digit prefix wins.
20
2
  export function stateToStatus(state, outcome = null) {
21
3
  if (outcome) {
22
4
  const match = /(\d{3})/.exec(outcome);
@@ -1,7 +1,7 @@
1
1
  -- PREP: get_known_entries
2
2
  SELECT
3
3
  path, scheme, state, outcome, visibility, body, turn, hash
4
- , attributes, countTokens(body) AS tokens, scope
4
+ , attributes, countTokens(body) AS tokens, scope, loop_id
5
5
  FROM known_entries
6
6
  WHERE run_id = :run_id
7
7
  ORDER BY path;
@@ -223,16 +223,26 @@ WHERE run_id = :run_id AND entry_id IN (
223
223
  );
224
224
 
225
225
  -- PREP: get_entries_by_pattern
226
+ -- Default excludes audit schemes (system://, reasoning://, model://, user://,
227
+ -- assistant://, content://, instructions://) so model-facing tools never leak
228
+ -- internal entries. Internal callers that need them pass include_audit_schemes=1.
229
+ -- :since filters to entries created after a given id; when set, results order
230
+ -- by id (insertion order) for streaming consumers; otherwise by path.
226
231
  SELECT
227
- e.path, e.body, e.scheme, rv.state, rv.outcome, rv.visibility
232
+ e.id, e.path, e.body, e.scheme, rv.state, rv.outcome, rv.visibility, rv.turn
228
233
  , countTokens(e.body) AS tokens, e.attributes
229
234
  FROM run_views AS rv
230
235
  JOIN entries AS e ON e.id = rv.entry_id
236
+ JOIN schemes AS s ON s.name = COALESCE(e.scheme, 'file')
231
237
  WHERE
232
238
  rv.run_id = :run_id
233
239
  AND hedmatch(:path, e.path)
234
240
  AND (:body IS NULL OR hedsearch(:body, e.body))
235
- ORDER BY e.path
241
+ AND (:include_audit_schemes IS NOT NULL OR s.model_visible = 1)
242
+ AND (:since IS NULL OR e.id > :since)
243
+ ORDER BY
244
+ CASE WHEN :since IS NOT NULL THEN e.id ELSE 0 END,
245
+ e.path
236
246
  LIMIT
237
247
  COALESCE(:limit, -1)
238
248
  OFFSET COALESCE(:offset, 0);
@@ -1,11 +1,7 @@
1
1
  import ContextAssembler from "./ContextAssembler.js";
2
- import { countTokens } from "./tokens.js";
2
+ import { countLines, countTokens } from "./tokens.js";
3
3
 
4
- /**
5
- * Rebuild turn_context from v_model_context, then assemble messages.
6
- * Called at turn start and again by the budget plugin when it needs a
7
- * fresh measurement after mutating visibility.
8
- */
4
+ // Rebuild turn_context from v_model_context and assemble messages.
9
5
  export default async function materializeContext({
10
6
  db,
11
7
  hooks,
@@ -16,22 +12,15 @@ export default async function materializeContext({
16
12
  mode,
17
13
  toolSet,
18
14
  contextSize,
19
- demoted,
20
15
  }) {
21
16
  await db.clear_turn_context.run({ run_id: runId, turn });
22
17
  const viewRows = await db.get_model_context.all({ run_id: runId });
23
- // Per-entry token accounting (see SPEC @token_accounting): captured
24
- // here while we still have the raw body, then merged onto rows after
25
- // the read-back roundtrip through turn_context.
18
+ // Per-entry token accounting; merged back after the turn_context roundtrip.
26
19
  const tokenAccounting = new Map();
27
20
  for (const row of viewRows) {
28
- // schemeOf() yields NULL (or "") for bare file paths — translate
29
- // to "file" so the view lookup finds the file scheme handler.
30
21
  const scheme = row.scheme ? row.scheme : "file";
31
22
  const attrs = row.attributes ? JSON.parse(row.attributes) : null;
32
- // Log entries live at log://turn_N/action/slug. Dispatch projection
33
- // to the action plugin's view (set, update, search, etc.) by
34
- // extracting the action segment from the path.
23
+ // Dispatch log entries to their action plugin's view via path segment.
35
24
  let projectionKey = scheme;
36
25
  if (scheme === "log") {
37
26
  const m = row.path.match(/^log:\/\/turn_\d+\/([^/]+)\//);
@@ -54,7 +43,14 @@ export default async function materializeContext({
54
43
  });
55
44
  const vTokens = countTokens(visibleProjection);
56
45
  const sTokens = countTokens(summarizedProjection);
57
- tokenAccounting.set(row.path, { vTokens, sTokens });
46
+ const vLines = countLines(visibleProjection);
47
+ tokenAccounting.set(row.path, {
48
+ vTokens,
49
+ sTokens,
50
+ vLines,
51
+ vBody: visibleProjection,
52
+ sBody: summarizedProjection,
53
+ });
58
54
  const projectedBody =
59
55
  row.visibility === "visible" ? visibleProjection : summarizedProjection;
60
56
  await db.insert_turn_context.run({
@@ -79,9 +75,11 @@ export default async function materializeContext({
79
75
  row.vTokens = t.vTokens;
80
76
  row.sTokens = t.sTokens;
81
77
  row.aTokens = t.vTokens - t.sTokens;
78
+ row.vLines = t.vLines;
79
+ row.vBody = t.vBody;
80
+ row.sBody = t.sBody;
82
81
  }
83
82
  const lastCtx = await db.get_last_context_tokens.get({ run_id: runId });
84
- // First turn of a new run has no prior context.
85
83
  let lastContextTokens = 0;
86
84
  if (lastCtx) lastContextTokens = lastCtx.context_tokens;
87
85
 
@@ -91,7 +89,6 @@ export default async function materializeContext({
91
89
  type: mode,
92
90
  systemPrompt,
93
91
  contextSize,
94
- demoted,
95
92
  toolSet,
96
93
  lastContextTokens,
97
94
  turn,
@@ -0,0 +1,5 @@
1
+ // Single source of truth for path-segment encoding: spaces → _, then URL-encode.
2
+ // Used by slugify (for summary-derived slugs) and Entries (for normalize/dedup/logPath).
3
+ export default function encodeSegment(s) {
4
+ return encodeURIComponent(String(s).replace(/ /g, "_"));
5
+ }
@@ -0,0 +1,9 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ // RUMMY_HOME default per README §Installation; resolved here because
5
+ // entrypoints run before env files load.
6
+ export default function resolveRummyHome() {
7
+ if (process.env.RUMMY_HOME) return process.env.RUMMY_HOME;
8
+ return join(homedir(), ".rummy");
9
+ }
@@ -56,6 +56,24 @@ LIMIT
56
56
  OFFSET
57
57
  COALESCE(:offset, 0);
58
58
 
59
+ -- PREP: get_run_summary
60
+ -- Per-run aggregation across all turns. LEFT JOIN: a run with zero
61
+ -- recorded turns (e.g. signal abort before first turn) returns 0s,
62
+ -- not NULL.
63
+ SELECT
64
+ r.model AS model
65
+ , COUNT(t.id) AS turns
66
+ , COALESCE(SUM(t.cost), 0) AS cost
67
+ , COALESCE(SUM(t.prompt_tokens), 0) AS prompt_tokens
68
+ , COALESCE(SUM(t.cached_tokens), 0) AS cached_tokens
69
+ , COALESCE(SUM(t.completion_tokens), 0) AS completion_tokens
70
+ , COALESCE(SUM(t.reasoning_tokens), 0) AS reasoning_tokens
71
+ , COALESCE(SUM(t.total_tokens), 0) AS total_tokens
72
+ FROM runs AS r
73
+ LEFT JOIN turns AS t ON t.run_id = r.id
74
+ WHERE r.id = :id
75
+ GROUP BY r.id;
76
+
59
77
  -- PREP: rename_run
60
78
  UPDATE runs
61
79
  SET alias = :new_alias
@@ -92,6 +110,25 @@ SELECT
92
110
  FROM run_views
93
111
  WHERE run_id = :parent_run_id;
94
112
 
113
+ -- PREP: archive_prior_prompt_artifacts
114
+ -- Multi-prompt sessions accumulate artifacts from prior prompt cycles
115
+ -- (consumed prompts, their per-turn logs). These pollute the validator's
116
+ -- prior-prompts check on subsequent Deployment landings. Archive all
117
+ -- prior prompt:// entries and prior-turn log:// entries when a new
118
+ -- prompt arrives. Knowns/unknowns/file entries are untouched — they
119
+ -- carry persistent knowledge across cycles. The loop_id IS NULL clause
120
+ -- catches forked-in views from a parent run (per fork_known_entries),
121
+ -- which represent prior cycles' artifacts inherited into a clean child.
122
+ UPDATE run_views
123
+ SET visibility = 'archived'
124
+ WHERE run_id = :run_id
125
+ AND visibility != 'archived'
126
+ AND (turn < :current_turn OR loop_id IS NULL)
127
+ AND entry_id IN (
128
+ SELECT id FROM entries
129
+ WHERE scheme IN ('prompt', 'log')
130
+ );
131
+
95
132
  -- PREP: get_active_runs
96
133
  SELECT r.id
97
134
  FROM runs AS r
@@ -1,10 +1,4 @@
1
- /**
2
- * Token estimation. Conservative character-based approximation.
3
- * RUMMY_TOKEN_DIVISOR controls characters per token.
4
- * No external dependencies. The budget contract is exact.
5
- * contextSize is the ceiling. countTokens is the measurement.
6
- */
7
-
1
+ // Conservative chars/token approximation; RUMMY_TOKEN_DIVISOR controls the divisor.
8
2
  const DIVISOR = Number(process.env.RUMMY_TOKEN_DIVISOR);
9
3
  if (!DIVISOR) throw new Error("RUMMY_TOKEN_DIVISOR must be a non-zero number");
10
4
 
@@ -12,3 +6,9 @@ export function countTokens(text) {
12
6
  if (!text) return 0;
13
7
  return Math.ceil(text.length / DIVISOR);
14
8
  }
9
+
10
+ export function countLines(text) {
11
+ if (!text) return 0;
12
+ const newlines = text.split("\n").length - 1;
13
+ return text.endsWith("\n") ? newlines : newlines + 1;
14
+ }
@@ -1,7 +1,4 @@
1
- /**
2
- * HookRegistry manages a simple, priority-ordered pipeline of processors.
3
- * It also supports basic event emitters for side-effects.
4
- */
1
+ // Priority-ordered processors + filters + events.
5
2
  export default class HookRegistry {
6
3
  #processors = [];
7
4
  #events = new Map();
@@ -12,17 +9,11 @@ export default class HookRegistry {
12
9
  this.#debug = debug;
13
10
  }
14
11
 
15
- /**
16
- * Register a processor for the Turn XML Document.
17
- */
18
12
  onTurn(callback, priority = 10) {
19
13
  this.#processors.push({ callback, priority });
20
14
  this.#processors.sort((a, b) => a.priority - b.priority);
21
15
  }
22
16
 
23
- /**
24
- * Run all registered Turn processors.
25
- */
26
17
  async processTurn(rummy) {
27
18
  for (const p of this.#processors) {
28
19
  const start = performance.now();
@@ -35,9 +26,6 @@ export default class HookRegistry {
35
26
  }
36
27
  }
37
28
 
38
- /**
39
- * Standard WordPress-style Filters for non-DOM data.
40
- */
41
29
  addFilter(tag, callback, priority = 10) {
42
30
  if (!this.#filters.has(tag)) this.#filters.set(tag, []);
43
31
  this.#filters.get(tag).push({ callback, priority });
@@ -54,9 +42,6 @@ export default class HookRegistry {
54
42
  return result;
55
43
  }
56
44
 
57
- /**
58
- * Standard WordPress-style Events for side-effects.
59
- */
60
45
  addEvent(tag, callback, priority = 10) {
61
46
  if (!this.#events.has(tag)) this.#events.set(tag, []);
62
47
  this.#events.get(tag).push({ callback, priority });
@@ -2,10 +2,7 @@ import HookRegistry from "./HookRegistry.js";
2
2
  import RpcRegistry from "./RpcRegistry.js";
3
3
  import ToolRegistry from "./ToolRegistry.js";
4
4
 
5
- /**
6
- * createHooks returns a structured, strictly-typed API for registering
7
- * and emitting hooks, removing the dynamic stringly-typed Proxy magic.
8
- */
5
+ // Strictly-typed hook surface; replaces the previous Proxy magic.
9
6
  export default function createHooks(debug = false) {
10
7
  const registry = new HookRegistry(debug);
11
8
  const tools = new ToolRegistry();
@@ -28,6 +25,10 @@ export default function createHooks(debug = false) {
28
25
  processTurn: registry.processTurn.bind(registry),
29
26
 
30
27
  // Explicit Hook Schema
28
+ boot: {
29
+ // Post-init, pre-accept-connections; one-shot post-init actions subscribe here.
30
+ completed: createEvent("boot.completed"),
31
+ },
31
32
  project: {
32
33
  init: {
33
34
  started: createEvent("project.init.started"),
@@ -43,8 +44,6 @@ export default function createHooks(debug = false) {
43
44
  run: {
44
45
  created: createEvent("run.created"),
45
46
  started: createEvent("run.started"),
46
- progress: createEvent("run.progress"),
47
- state: createEvent("run.state"),
48
47
  config: createFilter("run.config"),
49
48
  step: {
50
49
  completed: createEvent("run.step.completed"),
@@ -59,21 +58,13 @@ export default function createHooks(debug = false) {
59
58
  response: createEvent("turn.response"),
60
59
  completed: createEvent("turn.completed"),
61
60
  },
61
+ // SPEC #resolution covers the proposal hook chain.
62
62
  proposal: {
63
63
  prepare: createEvent("proposal.prepare"),
64
64
  pending: createEvent("proposal.pending"),
65
- // Plugins veto acceptance by returning {allow:false, outcome, body}.
66
- // Used e.g. by set plugin's readonly constraint check.
67
65
  accepting: createFilter("proposal.accepting"),
68
- // Plugins compose the resolved body based on path/action. Default
69
- // is output || "". Used e.g. by set plugin to preserve the
70
- // model's proposed content as the resolved body.
71
66
  content: createFilter("proposal.content"),
72
- // Fires after a proposal resolves with action="accept". Plugins
73
- // perform their side effects (file materialize, unlink, stream
74
- // setup, etc.) here — NOT in AgentLoop.resolve.
75
67
  accepted: createEvent("proposal.accepted"),
76
- // Fires after a proposal resolves with action="error" or "reject".
77
68
  rejected: createEvent("proposal.rejected"),
78
69
  },
79
70
  assembly: {
@@ -98,24 +89,9 @@ export default function createHooks(debug = false) {
98
89
  },
99
90
  messages: createFilter("llm.messages"),
100
91
  response: createFilter("llm.response"),
101
- // Reasoning merge filter. Subscribers contribute per-tag
102
- // reasoning text (e.g. the think plugin's <think>…</think>)
103
- // to the model's reasoning_content field. Fires between parse
104
- // and turn.response.
92
+ // Plugins contribute reasoning text into reasoning_content; fires between parse and turn.response.
105
93
  reasoning: createFilter("llm.reasoning"),
106
- // LLM provider registry. Plugins contribute entries shaped:
107
- // {
108
- // name: string,
109
- // matches: (modelAlias) => boolean,
110
- // completion: (messages, modelAlias, options) => Promise<response>,
111
- // getContextSize: (modelAlias) => Promise<number>,
112
- // }
113
- // Each provider owns a prefix namespace (e.g. "openai/", "ollama/",
114
- // "openrouter/"). LlmProvider picks the first provider whose
115
- // matches() returns true. No catchall — if a model alias doesn't
116
- // match any registered provider, the request fails with a clear
117
- // "no provider registered" error. External plugins add new
118
- // prefixes without namespace collision.
94
+ // Provider entries: { name, matches, completion, getContextSize }.
119
95
  providers: [],
120
96
  },
121
97
  file: {},
@@ -163,7 +139,6 @@ export default function createHooks(debug = false) {
163
139
  agent: {},
164
140
  tools,
165
141
 
166
- // Utility to add raw filters/events directly if needed for tests
167
142
  addFilter: registry.addFilter.bind(registry),
168
143
  applyFilters: registry.applyFilters.bind(registry),
169
144
  addEvent: registry.addEvent.bind(registry),
@@ -1,12 +1,4 @@
1
- /**
2
- * PluginContext is the plugin-only interface to the rummy system.
3
- * Available as `rummy.core` on the per-turn RummyContext, and as the
4
- * direct object passed to plugin constructors at startup.
5
- *
6
- * Carries plugin identity, hook registration, and infrastructure access.
7
- * The unified API (tool verbs, queries) lives on RummyContext.
8
- * This is the tier boundary: clients can't reach core.
9
- */
1
+ // Plugin-only registration interface; tool verbs live on RummyContext. PLUGINS.md.
10
2
  export default class PluginContext {
11
3
  #name;
12
4
  #hooks;
@@ -77,19 +69,12 @@ export default class PluginContext {
77
69
  this.#hooks.tools.ensureTool(this.#name);
78
70
  }
79
71
 
80
- // Mark this plugin's tool as hidden from model-facing tool lists.
81
- // Handler still dispatches if the model emits the tag.
72
+ // Hide from tool lists; handler still dispatches if the model emits the tag.
82
73
  markHidden() {
83
74
  this.#hooks.tools.markHidden(this.#name);
84
75
  }
85
76
 
86
- /**
87
- * Register a named callback for this plugin.
88
- * "handler" registers the tool handler.
89
- * "visible"/"summarized" register visibility projections.
90
- * "docs" sets tool documentation.
91
- * Everything else resolves to a hook event.
92
- */
77
+ // "handler" / "visible" / "summarized" are special; everything else is a hook event name.
93
78
  on(event, callback, priority = 10) {
94
79
  if (event === "handler") {
95
80
  this.#hooks.tools.ensureTool(this.#name);
@@ -104,9 +89,6 @@ export default class PluginContext {
104
89
  if (hook) hook.on(callback, priority);
105
90
  }
106
91
 
107
- /**
108
- * Register a filter callback.
109
- */
110
92
  filter(name, callback, priority = 10) {
111
93
  const hook = this.#resolveFilter(name);
112
94
  if (hook) hook.addFilter(callback, priority);
@@ -26,10 +26,7 @@ export default class RpcRegistry {
26
26
 
27
27
  #toolFallback = null;
28
28
 
29
- /**
30
- * Set a fallback that auto-dispatches any registered tool via RPC.
31
- * Checked at request time — tools registered after this call still work.
32
- */
29
+ // Late-binding tool dispatcher; resolved per request.
33
30
  setToolFallback(hooks, buildRunContext, dispatchTool) {
34
31
  this.#toolFallback = { hooks, buildRunContext, dispatchTool };
35
32
  }
@@ -1,15 +1,6 @@
1
- /**
2
- * RummyContext provides a unified, semantic API for plugins to interact with
3
- * the Turn node tree and core resources like the Database and Project metadata.
4
- */
5
- // Entries write verbs that should automatically carry the caller's
6
- // writer identity. Handler-issued writes on behalf of the model default
7
- // to writer=model; plugin background writes (set via rummy from a hook
8
- // with writer: "plugin" or "system" in ctx) get the context's writer.
1
+ // Per-turn plugin API (see PLUGINS.md); write verbs auto-carry writer identity.
9
2
  const WRITE_VERBS = new Set(["set", "rm", "cp", "mv", "update"]);
10
3
 
11
- // Defaults applied at construction so every plugin-facing getter
12
- // returns a predictable shape without per-access fallbacks.
13
4
  const CONTEXT_DEFAULTS = Object.freeze({
14
5
  hooks: null,
15
6
  activeFiles: [],
@@ -106,6 +97,10 @@ export default class RummyContext {
106
97
  return this.#context.noProposals === true;
107
98
  }
108
99
 
100
+ get yolo() {
101
+ return this.#context.yolo === true;
102
+ }
103
+
109
104
  get toolSet() {
110
105
  return this.#context.toolSet;
111
106
  }
@@ -122,12 +117,7 @@ export default class RummyContext {
122
117
  return this.#context.loopPrompt;
123
118
  }
124
119
 
125
- /**
126
- * Writer identity for Entries permission checks. Defaults to
127
- * 'model' — handlers write on behalf of the model's emitted command.
128
- * Non-handler plugin code (streaming callbacks, background emissions)
129
- * passes `writer: 'plugin'` or `'system'` explicitly.
130
- */
120
+ // Default 'model' (handlers write on the model's behalf); plugins pass writer explicitly.
131
121
  get writer() {
132
122
  return this.#context.writer;
133
123
  }
@@ -1,6 +1,4 @@
1
- // Tool display order: gather → reason → act → communicate.
2
- // Position in the list implies priority to the model.
3
- // `update` is pinned last — it's the turn-closer, not an action.
1
+ // gather → reason → act → communicate; update pinned last (turn-closer).
4
2
  const TOOL_ORDER = [
5
3
  "think",
6
4
  "unknown",
@@ -40,9 +38,7 @@ export default class ToolRegistry {
40
38
  this.#tools.set(scheme, Object.freeze({}));
41
39
  }
42
40
 
43
- // Hidden tools dispatch on direct emission but don't appear in any
44
- // model-facing tool list. Internal schemes (e.g. <known>, <unknown>)
45
- // the model writes via <set path="scheme://..."> instead.
41
+ // Hidden tools dispatch on direct emission but never appear in tool lists.
46
42
  markHidden(scheme) {
47
43
  this.#hidden.add(scheme);
48
44
  }
@@ -82,9 +78,7 @@ export default class ToolRegistry {
82
78
  if (!fn) return "";
83
79
 
84
80
  const body = await fn(entry);
85
- // View handlers MAY return undefined or null to mean "no projected
86
- // body at this visibility" — normalize at this boundary so callers
87
- // get a predictable string.
81
+ // undefined/null = "no projected body at this visibility"; normalize to "".
88
82
  return body == null ? "" : body;
89
83
  }
90
84
 
@@ -106,18 +100,14 @@ export default class ToolRegistry {
106
100
  return sortByPriority([...this.#tools.keys()]);
107
101
  }
108
102
 
109
- // Names advertised to the model registered tools minus hidden ones.
110
- // Use this anywhere a tool list is shown to the model.
103
+ // Registered tools minus hidden; use anywhere a list reaches the model.
111
104
  get advertisedNames() {
112
105
  return sortByPriority(
113
106
  [...this.#tools.keys()].filter((n) => !this.#hidden.has(n)),
114
107
  );
115
108
  }
116
109
 
117
- /**
118
- * Compute the active tool set for a loop.
119
- * All exclusions — mode, flags, hidden — handled here. One mechanism.
120
- */
110
+ // Single source of truth for active-tool exclusions; SPEC #mode_enforcement.
121
111
  resolveForLoop(
122
112
  mode,
123
113
  { noInteraction = false, noWeb = false, noProposals = false } = {},