@possumtech/rummy 2.1.0 → 2.2.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.
- package/.env.example +40 -15
- package/.xai.key +1 -0
- package/PLUGINS.md +169 -53
- package/README.md +38 -32
- package/SPEC.md +366 -179
- package/bin/digest.js +1097 -0
- package/biome/no-fallbacks.grit +2 -2
- package/gemini.key +1 -0
- package/lang/en.json +10 -1
- package/migrations/001_initial_schema.sql +9 -2
- package/package.json +19 -8
- package/service.js +1 -0
- package/src/agent/AgentLoop.js +76 -26
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/Entries.js +238 -60
- package/src/agent/ProjectAgent.js +44 -0
- package/src/agent/TurnExecutor.js +99 -30
- package/src/agent/XmlParser.js +206 -111
- package/src/agent/errors.js +35 -0
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +3 -42
- package/src/agent/materializeContext.js +30 -1
- package/src/agent/runs.sql +8 -18
- package/src/agent/tokens.js +0 -1
- package/src/agent/turns.sql +1 -0
- package/src/hooks/Hooks.js +26 -0
- package/src/hooks/RummyContext.js +12 -1
- package/src/lib/hedberg/README.md +60 -0
- package/src/lib/hedberg/hedberg.js +60 -0
- package/src/lib/hedberg/marker.js +158 -0
- package/src/{plugins → lib}/hedberg/matcher.js +1 -2
- package/src/llm/LlmProvider.js +41 -3
- package/src/llm/openaiStream.js +17 -0
- package/src/plugins/ask_user/ask_user.js +12 -2
- package/src/plugins/ask_user/ask_userDoc.md +1 -5
- package/src/plugins/budget/README.md +29 -24
- package/src/plugins/budget/budget.js +166 -110
- package/src/plugins/cli/README.md +3 -4
- package/src/plugins/cli/cli.js +31 -5
- package/src/plugins/cloudflare/cloudflare.js +136 -0
- package/src/plugins/cp/cp.js +41 -4
- package/src/plugins/cp/cpDoc.md +5 -6
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/README.md +5 -4
- package/src/plugins/env/env.js +7 -4
- package/src/plugins/env/envDoc.md +7 -8
- package/src/plugins/error/error.js +56 -15
- package/src/plugins/file/README.md +12 -3
- package/src/plugins/file/file.js +2 -2
- package/src/plugins/get/get.js +59 -36
- package/src/plugins/get/getDoc.md +10 -34
- package/src/plugins/google/google.js +115 -0
- package/src/plugins/hedberg/hedberg.js +13 -56
- package/src/plugins/helpers.js +66 -12
- package/src/plugins/index.js +1 -2
- package/src/plugins/instructions/README.md +44 -47
- package/src/plugins/instructions/instructions-system.md +44 -0
- package/src/plugins/instructions/instructions-user.md +53 -0
- package/src/plugins/instructions/instructions.js +58 -189
- package/src/plugins/known/README.md +6 -7
- package/src/plugins/known/known.js +24 -30
- package/src/plugins/log/log.js +41 -32
- package/src/plugins/mv/mv.js +40 -1
- package/src/plugins/mv/mvDoc.md +1 -8
- package/src/plugins/ollama/ollama.js +4 -3
- package/src/plugins/openai/openai.js +4 -3
- package/src/plugins/openrouter/openrouter.js +14 -4
- package/src/plugins/persona/README.md +11 -13
- package/src/plugins/persona/default.md +29 -0
- package/src/plugins/persona/persona.js +10 -66
- package/src/plugins/policy/policy.js +23 -22
- package/src/plugins/prompt/README.md +37 -27
- package/src/plugins/prompt/prompt.js +13 -19
- package/src/plugins/rm/rm.js +18 -0
- package/src/plugins/rm/rmDoc.md +5 -6
- package/src/plugins/rpc/rpc.js +3 -3
- package/src/plugins/set/set.js +205 -323
- package/src/plugins/set/setDoc.md +47 -17
- package/src/plugins/sh/README.md +6 -5
- package/src/plugins/sh/sh.js +8 -5
- package/src/plugins/sh/shDoc.md +7 -8
- package/src/plugins/skill/README.md +37 -14
- package/src/plugins/skill/skill.js +200 -101
- package/src/plugins/skill/skillDoc.js +3 -0
- package/src/plugins/skill/skillDoc.md +9 -0
- package/src/plugins/stream/README.md +7 -6
- package/src/plugins/stream/finalize.js +100 -0
- package/src/plugins/stream/stream.js +13 -45
- package/src/plugins/telemetry/telemetry.js +27 -4
- package/src/plugins/think/think.js +2 -3
- package/src/plugins/think/thinkDoc.md +2 -4
- package/src/plugins/unknown/README.md +1 -1
- package/src/plugins/unknown/unknown.js +17 -19
- package/src/plugins/update/update.js +4 -51
- package/src/plugins/update/updateDoc.md +21 -6
- package/src/plugins/xai/xai.js +68 -102
- package/src/plugins/yolo/yolo.js +102 -75
- package/src/sql/functions/hedmatch.js +1 -1
- package/src/sql/functions/hedreplace.js +1 -1
- package/src/sql/functions/hedsearch.js +1 -1
- package/src/sql/functions/slugify.js +16 -2
- package/BENCH_ENVIRONMENT.md +0 -230
- package/CLIENT_INTERFACE.md +0 -396
- package/last_run.txt +0 -5617
- package/scriptify/ask_run.js +0 -77
- package/scriptify/cache_probe.js +0 -66
- package/scriptify/cache_probe_grok.js +0 -74
- package/src/agent/budget.js +0 -33
- package/src/agent/config.js +0 -38
- package/src/plugins/hedberg/README.md +0 -71
- package/src/plugins/hedberg/docs.md +0 -0
- package/src/plugins/hedberg/edits.js +0 -55
- package/src/plugins/hedberg/normalize.js +0 -17
- package/src/plugins/hedberg/sed.js +0 -49
- package/src/plugins/instructions/instructions.md +0 -34
- package/src/plugins/instructions/instructions_104.md +0 -8
- package/src/plugins/instructions/instructions_105.md +0 -39
- package/src/plugins/instructions/instructions_106.md +0 -22
- package/src/plugins/instructions/instructions_107.md +0 -17
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/known/knownDoc.js +0 -3
- package/src/plugins/known/knownDoc.md +0 -8
- package/src/plugins/unknown/unknownDoc.js +0 -3
- package/src/plugins/unknown/unknownDoc.md +0 -11
- package/turns/cli_1777462658211/turn_001.txt +0 -772
- package/turns/cli_1777462658211/turn_002.txt +0 -606
- package/turns/cli_1777462658211/turn_003.txt +0 -667
- package/turns/cli_1777462658211/turn_004.txt +0 -297
- package/turns/cli_1777462658211/turn_005.txt +0 -301
- package/turns/cli_1777462658211/turn_006.txt +0 -262
- package/turns/cli_1777465095132/turn_001.txt +0 -715
- package/turns/cli_1777465095132/turn_002.txt +0 -236
- package/turns/cli_1777465095132/turn_003.txt +0 -287
- package/turns/cli_1777465095132/turn_004.txt +0 -694
- package/turns/cli_1777465095132/turn_005.txt +0 -422
- package/turns/cli_1777465095132/turn_006.txt +0 -365
- package/turns/cli_1777465095132/turn_007.txt +0 -885
- package/turns/cli_1777465095132/turn_008.txt +0 -1277
- package/turns/cli_1777465095132/turn_009.txt +0 -736
- /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
package/src/plugins/set/set.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Entries from "../../agent/Entries.js";
|
|
2
2
|
import { countTokens } from "../../agent/tokens.js";
|
|
3
|
+
import Hedberg, { generatePatch } from "../../lib/hedberg/hedberg.js";
|
|
3
4
|
import File from "../file/file.js";
|
|
4
|
-
import
|
|
5
|
-
import { storePatternResult } from "../helpers.js";
|
|
5
|
+
import { SUMMARY_MAX_CHARS, storePatternResult } from "../helpers.js";
|
|
6
6
|
import docs from "./setDoc.js";
|
|
7
7
|
|
|
8
8
|
const VALID_VISIBILITY = { archived: 1, summarized: 1, visible: 1 };
|
|
@@ -13,6 +13,18 @@ function isSetProposal(path) {
|
|
|
13
13
|
return m?.[1] === "set";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Cap the size of the current-body context surfaced on conflict. Big
|
|
17
|
+
// enough for typical known:// entries (plans, notes) and a useful slice
|
|
18
|
+
// of files; small enough that a 100k-line file doesn't blow the budget
|
|
19
|
+
// on every conflict. The model can `<get>` the path for the full body.
|
|
20
|
+
const CONFLICT_FEEDBACK_MAX_CHARS = 4000;
|
|
21
|
+
function truncateForFeedback(body) {
|
|
22
|
+
if (body == null) return null;
|
|
23
|
+
if (body.length <= CONFLICT_FEEDBACK_MAX_CHARS) return body;
|
|
24
|
+
const head = body.slice(0, CONFLICT_FEEDBACK_MAX_CHARS);
|
|
25
|
+
return `${head}\n[truncated; ${body.length - CONFLICT_FEEDBACK_MAX_CHARS} more chars — <get> the path for full body]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
|
|
17
29
|
export default class Set {
|
|
18
30
|
#core;
|
|
@@ -23,13 +35,15 @@ export default class Set {
|
|
|
23
35
|
core.on("handler", this.handler.bind(this));
|
|
24
36
|
core.on("visible", this.full.bind(this));
|
|
25
37
|
core.on("summarized", this.summary.bind(this));
|
|
26
|
-
core.on("proposal.prepare", this.#materializeRevisions.bind(this));
|
|
27
38
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
28
39
|
docsMap.set = docs;
|
|
29
40
|
return docsMap;
|
|
30
41
|
});
|
|
31
42
|
core.filter("proposal.accepting", this.#vetoReadonly.bind(this));
|
|
32
43
|
core.filter("proposal.content", this.#preferExistingBody.bind(this));
|
|
44
|
+
// Materialization is shape-coupled (attrs.path + attrs.patched), not
|
|
45
|
+
// path-coupled. Any plugin emitting a proposal in that shape
|
|
46
|
+
// (set, cp, future tools) gets fs materialization for free.
|
|
33
47
|
core.on("proposal.accepted", this.#materializeFile.bind(this));
|
|
34
48
|
}
|
|
35
49
|
|
|
@@ -58,35 +72,30 @@ export default class Set {
|
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
async #materializeFile(ctx) {
|
|
61
|
-
if (!isSetProposal(ctx.path)) return;
|
|
62
75
|
const { attrs, runId, projectId, projectRoot, db, entries } = ctx;
|
|
63
|
-
|
|
76
|
+
// Shape gate, not path gate: any accepted proposal whose
|
|
77
|
+
// attributes describe a file materialization (target path +
|
|
78
|
+
// authoritative patched body) lands a fresh file body and writes
|
|
79
|
+
// to disk. Lets cp/set/future tools share one materializer.
|
|
80
|
+
if (!attrs?.path || attrs?.patched == null) return;
|
|
64
81
|
|
|
65
82
|
const existing = await entries.getBody(runId, attrs.path);
|
|
66
83
|
const isNewFile = existing === null;
|
|
67
|
-
const
|
|
68
|
-
const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
|
|
69
|
-
let patched = fileBody;
|
|
70
|
-
for (const block of blocks) {
|
|
71
|
-
const m = block.match(
|
|
72
|
-
/<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
|
|
73
|
-
);
|
|
74
|
-
if (!m) continue;
|
|
75
|
-
if (m[1] === "") {
|
|
76
|
-
patched = m[2];
|
|
77
|
-
} else {
|
|
78
|
-
patched = patched.replace(m[1], m[2]);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
84
|
+
const patched = attrs.patched;
|
|
81
85
|
const turn = (await db.get_run_by_id.get({ id: runId })).next_turn;
|
|
82
|
-
//
|
|
86
|
+
// Visibility precedence: explicit attrs.visibility (mv/cp pass
|
|
87
|
+
// the model's tag attribute through) > current entry visibility
|
|
88
|
+
// (preserves an earlier <get>'s promotion) > scheme default.
|
|
83
89
|
const existingState = await entries.getState(runId, attrs.path);
|
|
90
|
+
const visibility = attrs.visibility
|
|
91
|
+
? attrs.visibility
|
|
92
|
+
: existingState?.visibility;
|
|
84
93
|
await entries.set({
|
|
85
94
|
runId,
|
|
86
95
|
turn,
|
|
87
96
|
path: attrs.path,
|
|
88
97
|
body: patched,
|
|
89
|
-
visibility
|
|
98
|
+
visibility,
|
|
90
99
|
});
|
|
91
100
|
if (projectRoot) {
|
|
92
101
|
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
@@ -98,7 +107,7 @@ export default class Set {
|
|
|
98
107
|
await writeFile(targetPath, patched);
|
|
99
108
|
}
|
|
100
109
|
if (isNewFile && projectId) {
|
|
101
|
-
await File.setConstraint(db, projectId, attrs.path, "
|
|
110
|
+
await File.setConstraint(db, projectId, attrs.path, "add");
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
@@ -108,8 +117,27 @@ export default class Set {
|
|
|
108
117
|
const visibilityAttr = VALID_VISIBILITY[attrs.visibility]
|
|
109
118
|
? attrs.visibility
|
|
110
119
|
: null;
|
|
111
|
-
const
|
|
112
|
-
const
|
|
120
|
+
const rawTags = typeof attrs.tags === "string" ? attrs.tags : null;
|
|
121
|
+
const tagsText = rawTags ? rawTags.slice(0, 80) : null;
|
|
122
|
+
|
|
123
|
+
// log:// is the immutable record of what happened. Visibility/metadata
|
|
124
|
+
// updates are fine (no body); rewriting the body destroys history.
|
|
125
|
+
// Models reach for this when the Demote example pattern primes
|
|
126
|
+
// `<set ... visibility="summarized">` and they tack on a body line —
|
|
127
|
+
// 405 here teaches the shape that's actually allowed.
|
|
128
|
+
if (attrs.path?.startsWith("log://") && entry.body) {
|
|
129
|
+
await store.set({
|
|
130
|
+
runId,
|
|
131
|
+
turn,
|
|
132
|
+
loopId,
|
|
133
|
+
path: entry.resultPath,
|
|
134
|
+
body: `log:// is immutable. To demote: <set path="${attrs.path}" visibility="summarized"/> (no body).`,
|
|
135
|
+
state: "failed",
|
|
136
|
+
outcome: "method_not_allowed",
|
|
137
|
+
attributes: { path: attrs.path },
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
113
141
|
|
|
114
142
|
// Reject invalid visibility on body-less set; otherwise a typo silently wipes the body.
|
|
115
143
|
if (
|
|
@@ -123,7 +151,7 @@ export default class Set {
|
|
|
123
151
|
turn,
|
|
124
152
|
loopId,
|
|
125
153
|
path: entry.resultPath,
|
|
126
|
-
body: `Invalid visibility "${attrs.visibility}"
|
|
154
|
+
body: `Invalid visibility "${attrs.visibility}"`,
|
|
127
155
|
state: "failed",
|
|
128
156
|
outcome: "validation",
|
|
129
157
|
attributes: { path: attrs.path },
|
|
@@ -131,6 +159,48 @@ export default class Set {
|
|
|
131
159
|
return;
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
// Refuse parse-error edits (e.g., malformed sed). Without this the
|
|
163
|
+
// XmlParser would have either silently produced a corrupted edit
|
|
164
|
+
// or fallen through to body-replace, overwriting the target with
|
|
165
|
+
// the literal sed text. Surfacing the error gives the model a
|
|
166
|
+
// concrete signal it can adapt to.
|
|
167
|
+
if (attrs.error) {
|
|
168
|
+
await store.set({
|
|
169
|
+
runId,
|
|
170
|
+
turn,
|
|
171
|
+
loopId,
|
|
172
|
+
path: entry.resultPath,
|
|
173
|
+
body: attrs.error,
|
|
174
|
+
state: "failed",
|
|
175
|
+
outcome: "validation",
|
|
176
|
+
attributes: { path: attrs.path, error: attrs.error },
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Manifest: universal preview gate. Fires before any operational
|
|
182
|
+
// branch so visibility flips, SEARCH/REPLACE edits, sed substitutions,
|
|
183
|
+
// pattern writes, and direct writes all support
|
|
184
|
+
// "list-without-doing" with the same flag.
|
|
185
|
+
if (attrs.manifest !== undefined && attrs.path) {
|
|
186
|
+
const matches = await store.getEntriesByPattern(
|
|
187
|
+
runId,
|
|
188
|
+
attrs.path,
|
|
189
|
+
attrs.body,
|
|
190
|
+
);
|
|
191
|
+
await storePatternResult(
|
|
192
|
+
store,
|
|
193
|
+
runId,
|
|
194
|
+
turn,
|
|
195
|
+
"set",
|
|
196
|
+
attrs.path,
|
|
197
|
+
attrs.body,
|
|
198
|
+
matches,
|
|
199
|
+
{ manifest: true, loopId, attributes: { path: attrs.path } },
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
134
204
|
// Pure visibility/metadata change — no body content
|
|
135
205
|
if (!entry.body && visibilityAttr && attrs.path) {
|
|
136
206
|
const target = attrs.path;
|
|
@@ -158,12 +228,12 @@ export default class Set {
|
|
|
158
228
|
path: match.path,
|
|
159
229
|
visibility: visibilityAttr,
|
|
160
230
|
});
|
|
161
|
-
if (
|
|
231
|
+
if (tagsText) {
|
|
162
232
|
await store.set({
|
|
163
233
|
runId: runId,
|
|
164
234
|
path: match.path,
|
|
165
235
|
attributes: {
|
|
166
|
-
|
|
236
|
+
tags: tagsText,
|
|
167
237
|
},
|
|
168
238
|
});
|
|
169
239
|
}
|
|
@@ -181,42 +251,68 @@ export default class Set {
|
|
|
181
251
|
return;
|
|
182
252
|
}
|
|
183
253
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
254
|
+
// Build the new content. Either from the marker-parsed operation
|
|
255
|
+
// list (NEW / PREPEND / APPEND / REPLACE / DELETE / SEARCH+REPLACE)
|
|
256
|
+
// or from the plain body (full-replace shorthand).
|
|
257
|
+
const target = attrs.path;
|
|
258
|
+
if (!target) return;
|
|
259
|
+
let newContent;
|
|
260
|
+
if (attrs.operations) {
|
|
261
|
+
const existing = await store.getBody(runId, target);
|
|
262
|
+
const requiresExisting = attrs.operations.some(
|
|
263
|
+
(op) => op.op === "search_replace" || op.op === "delete",
|
|
193
264
|
);
|
|
194
|
-
|
|
195
|
-
store
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
265
|
+
if (requiresExisting && existing === null) {
|
|
266
|
+
await store.set({
|
|
267
|
+
runId,
|
|
268
|
+
turn,
|
|
269
|
+
loopId,
|
|
270
|
+
path: entry.resultPath,
|
|
271
|
+
body: `${target} not found in context`,
|
|
272
|
+
state: "failed",
|
|
273
|
+
outcome: "not_found",
|
|
274
|
+
attributes: {
|
|
275
|
+
path: target,
|
|
276
|
+
error: `${target} not found in context`,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const result = Set.#applyOperations(
|
|
282
|
+
existing == null ? "" : existing,
|
|
283
|
+
attrs.operations,
|
|
203
284
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
285
|
+
if (result.error) {
|
|
286
|
+
await store.set({
|
|
287
|
+
runId,
|
|
288
|
+
turn,
|
|
289
|
+
loopId,
|
|
290
|
+
path: entry.resultPath,
|
|
291
|
+
body: existing == null ? "" : existing,
|
|
292
|
+
state: "failed",
|
|
293
|
+
outcome: "conflict",
|
|
294
|
+
attributes: {
|
|
295
|
+
path: target,
|
|
296
|
+
error: result.error,
|
|
297
|
+
attempted: result.attempted,
|
|
298
|
+
currentBody: truncateForFeedback(existing),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
newContent = result.body;
|
|
304
|
+
} else if (entry.body) {
|
|
305
|
+
newContent = entry.body;
|
|
306
|
+
}
|
|
209
307
|
|
|
308
|
+
if (newContent !== undefined) {
|
|
210
309
|
const scheme = Entries.scheme(target);
|
|
211
310
|
if (scheme === null) {
|
|
212
|
-
// File write —
|
|
311
|
+
// File write — emit a "proposed" entry; #materializeFile
|
|
312
|
+
// writes to disk on accept.
|
|
213
313
|
const existing = await store.getBody(runId, target);
|
|
214
|
-
const oldContent = existing
|
|
215
|
-
const newContent = entry.body;
|
|
314
|
+
const oldContent = existing == null ? "" : existing;
|
|
216
315
|
const udiff = generatePatch(target, oldContent, newContent);
|
|
217
|
-
const merge = oldContent
|
|
218
|
-
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
219
|
-
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
220
316
|
const beforeTokens = oldContent ? countTokens(oldContent) : 0;
|
|
221
317
|
const afterTokens = countTokens(newContent);
|
|
222
318
|
await store.set({
|
|
@@ -228,15 +324,17 @@ export default class Set {
|
|
|
228
324
|
attributes: {
|
|
229
325
|
path: target,
|
|
230
326
|
patch: udiff,
|
|
231
|
-
|
|
327
|
+
patched: newContent,
|
|
232
328
|
beforeTokens,
|
|
233
329
|
afterTokens,
|
|
234
|
-
|
|
330
|
+
tags: tagsText,
|
|
235
331
|
},
|
|
236
332
|
loopId,
|
|
237
333
|
});
|
|
238
334
|
} else if (attrs.filter || target.includes("*")) {
|
|
239
|
-
// Pattern update
|
|
335
|
+
// Pattern body-update: write the same body to every matching
|
|
336
|
+
// entry. Operations don't apply here (this is a bulk
|
|
337
|
+
// metadata-flavored body assignment).
|
|
240
338
|
const matches = await store.getEntriesByPattern(
|
|
241
339
|
runId,
|
|
242
340
|
target,
|
|
@@ -245,7 +343,7 @@ export default class Set {
|
|
|
245
343
|
await store.set({
|
|
246
344
|
runId: runId,
|
|
247
345
|
path: target,
|
|
248
|
-
body:
|
|
346
|
+
body: newContent,
|
|
249
347
|
bodyFilter: attrs.filter === undefined ? null : attrs.filter,
|
|
250
348
|
});
|
|
251
349
|
await storePatternResult(
|
|
@@ -261,12 +359,8 @@ export default class Set {
|
|
|
261
359
|
} else {
|
|
262
360
|
// Direct scheme write; same diff-against-existing shape as file writes.
|
|
263
361
|
const existing = await store.getBody(runId, target);
|
|
264
|
-
const oldContent = existing
|
|
265
|
-
const newContent = entry.body;
|
|
362
|
+
const oldContent = existing == null ? "" : existing;
|
|
266
363
|
const udiff = generatePatch(target, oldContent, newContent);
|
|
267
|
-
const merge = oldContent
|
|
268
|
-
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
269
|
-
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
270
364
|
const beforeTokens = oldContent ? countTokens(oldContent) : 0;
|
|
271
365
|
const afterTokens = countTokens(newContent);
|
|
272
366
|
|
|
@@ -276,9 +370,8 @@ export default class Set {
|
|
|
276
370
|
path: target,
|
|
277
371
|
body: newContent,
|
|
278
372
|
state: "resolved",
|
|
279
|
-
// Scheme writes default visible; the model wrote it.
|
|
280
373
|
visibility: visibilityAttr ? visibilityAttr : "visible",
|
|
281
|
-
attributes:
|
|
374
|
+
attributes: tagsText ? { tags: tagsText } : null,
|
|
282
375
|
loopId,
|
|
283
376
|
});
|
|
284
377
|
await store.set({
|
|
@@ -291,10 +384,9 @@ export default class Set {
|
|
|
291
384
|
attributes: {
|
|
292
385
|
path: target,
|
|
293
386
|
patch: udiff,
|
|
294
|
-
merge,
|
|
295
387
|
beforeTokens,
|
|
296
388
|
afterTokens,
|
|
297
|
-
|
|
389
|
+
tags: tagsText,
|
|
298
390
|
},
|
|
299
391
|
});
|
|
300
392
|
}
|
|
@@ -311,11 +403,11 @@ export default class Set {
|
|
|
311
403
|
visibility: visibilityAttr,
|
|
312
404
|
});
|
|
313
405
|
}
|
|
314
|
-
if (
|
|
406
|
+
if (tagsText) {
|
|
315
407
|
await store.set({
|
|
316
408
|
runId: runId,
|
|
317
409
|
path: target,
|
|
318
|
-
attributes: {
|
|
410
|
+
attributes: { tags: tagsText },
|
|
319
411
|
});
|
|
320
412
|
}
|
|
321
413
|
}
|
|
@@ -324,270 +416,60 @@ export default class Set {
|
|
|
324
416
|
full(entry) {
|
|
325
417
|
const attrs = entry.attributes;
|
|
326
418
|
const target = attrs.path || entry.path;
|
|
327
|
-
if (attrs.error)
|
|
419
|
+
if (attrs.error) {
|
|
420
|
+
const lines = [`# set ${target}`, attrs.error];
|
|
421
|
+
if (attrs.attempted) {
|
|
422
|
+
lines.push("", "--- attempted ---", attrs.attempted);
|
|
423
|
+
}
|
|
424
|
+
if (attrs.currentBody != null) {
|
|
425
|
+
lines.push("", `--- current body of ${target} ---`, attrs.currentBody);
|
|
426
|
+
}
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
328
429
|
const tokens =
|
|
329
430
|
attrs.beforeTokens != null
|
|
330
431
|
? ` ${attrs.beforeTokens}→${attrs.afterTokens} tokens`
|
|
331
432
|
: "";
|
|
332
|
-
if (!attrs.
|
|
333
|
-
return `# set ${target}${tokens}\n${attrs.
|
|
433
|
+
if (!attrs.patch) return `# set ${target}${tokens}`;
|
|
434
|
+
return `# set ${target}${tokens}\n${attrs.patch}`;
|
|
334
435
|
}
|
|
335
436
|
|
|
336
437
|
summary(entry) {
|
|
337
438
|
if (!entry.body) return "";
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return flat.length <= 80 ? flat : `${flat.slice(0, 77)}...`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async #processEdit(rummy, entry, attrs) {
|
|
347
|
-
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
348
|
-
const target = attrs.path;
|
|
349
|
-
const matches = await store.getEntriesByPattern(runId, target, attrs.body);
|
|
350
|
-
|
|
351
|
-
if (matches.length === 0) {
|
|
352
|
-
await store.set({
|
|
353
|
-
runId,
|
|
354
|
-
turn,
|
|
355
|
-
path: entry.resultPath,
|
|
356
|
-
body: "",
|
|
357
|
-
state: "failed",
|
|
358
|
-
outcome: "not_found",
|
|
359
|
-
attributes: { path: target, error: `${target} not found in context` },
|
|
360
|
-
loopId,
|
|
361
|
-
});
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
for (const match of matches) {
|
|
366
|
-
if (match.scheme === null) {
|
|
367
|
-
// Bare file: apply edit immediately so log carries before/after merge.
|
|
368
|
-
const canonicalPath = `set://${match.path}`;
|
|
369
|
-
const revision = Set.#buildRevision(attrs);
|
|
370
|
-
const existingAttrs = await rummy.getAttributes(canonicalPath);
|
|
371
|
-
const revisions = existingAttrs?.revisions
|
|
372
|
-
? existingAttrs.revisions
|
|
373
|
-
: [];
|
|
374
|
-
revisions.push(revision);
|
|
375
|
-
await store.set({
|
|
376
|
-
runId,
|
|
377
|
-
turn,
|
|
378
|
-
path: canonicalPath,
|
|
379
|
-
body: "",
|
|
380
|
-
state: "resolved",
|
|
381
|
-
attributes: { path: match.path, revisions },
|
|
382
|
-
loopId,
|
|
383
|
-
});
|
|
384
|
-
const { patch, searchText, replaceText, warning, error } =
|
|
385
|
-
Set.#applyRevision(match.body, attrs);
|
|
386
|
-
const merge =
|
|
387
|
-
searchText != null
|
|
388
|
-
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
389
|
-
: null;
|
|
390
|
-
const beforeTokens = match.tokens;
|
|
391
|
-
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
392
|
-
const logState = error ? "failed" : "resolved";
|
|
393
|
-
await store.set({
|
|
394
|
-
runId,
|
|
395
|
-
turn,
|
|
396
|
-
path: entry.resultPath,
|
|
397
|
-
body: merge ?? (patch || `edit to ${match.path}`),
|
|
398
|
-
state: logState,
|
|
399
|
-
outcome: error ? "conflict" : null,
|
|
400
|
-
attributes: {
|
|
401
|
-
path: match.path,
|
|
402
|
-
merge,
|
|
403
|
-
beforeTokens,
|
|
404
|
-
afterTokens,
|
|
405
|
-
warning,
|
|
406
|
-
error,
|
|
407
|
-
},
|
|
408
|
-
loopId,
|
|
409
|
-
});
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const { patch, searchText, replaceText, warning, error } =
|
|
414
|
-
Set.#applyRevision(match.body, attrs);
|
|
415
|
-
|
|
416
|
-
const state = error ? "failed" : "resolved";
|
|
417
|
-
const outcome = error ? "conflict" : null;
|
|
418
|
-
const udiff = patch ? generatePatch(match.path, match.body, patch) : null;
|
|
419
|
-
const merge =
|
|
420
|
-
searchText != null
|
|
421
|
-
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
422
|
-
: null;
|
|
423
|
-
const beforeTokens = match.tokens;
|
|
424
|
-
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
425
|
-
|
|
426
|
-
// Log entry at log://turn_N/set/<target> records the action.
|
|
427
|
-
await store.set({
|
|
428
|
-
runId,
|
|
429
|
-
turn,
|
|
430
|
-
path: entry.resultPath,
|
|
431
|
-
body: patch ?? match.body,
|
|
432
|
-
state,
|
|
433
|
-
outcome,
|
|
434
|
-
attributes: {
|
|
435
|
-
path: match.path,
|
|
436
|
-
patch: udiff,
|
|
437
|
-
merge,
|
|
438
|
-
beforeTokens,
|
|
439
|
-
afterTokens,
|
|
440
|
-
warning,
|
|
441
|
-
error,
|
|
442
|
-
},
|
|
443
|
-
loopId,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
if (state === "resolved" && patch) {
|
|
447
|
-
await store.set({
|
|
448
|
-
runId,
|
|
449
|
-
turn,
|
|
450
|
-
path: match.path,
|
|
451
|
-
body: patch,
|
|
452
|
-
state: match.state,
|
|
453
|
-
loopId,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
}
|
|
439
|
+
// Contract: summarized projections are ≤ SUMMARY_MAX_CHARS. The
|
|
440
|
+
// merge body for an edit can be many KB; truncate. The model
|
|
441
|
+
// reads the full body via promotion to visible if it needs the
|
|
442
|
+
// edit's exact content.
|
|
443
|
+
return entry.body.slice(0, SUMMARY_MAX_CHARS);
|
|
457
444
|
}
|
|
458
445
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const mergeBlocks = [];
|
|
477
|
-
let lastError = null;
|
|
478
|
-
let lastWarning = null;
|
|
479
|
-
|
|
480
|
-
for (const rev of attrs.revisions) {
|
|
481
|
-
if (!rev) continue;
|
|
482
|
-
const { patch, searchText, replaceText, warning, error } =
|
|
483
|
-
Set.#applyRevision(current, rev);
|
|
484
|
-
|
|
485
|
-
if (error) lastError = error;
|
|
486
|
-
else if (patch) current = patch;
|
|
487
|
-
if (warning) lastWarning = warning;
|
|
488
|
-
|
|
489
|
-
if (searchText != null) {
|
|
490
|
-
mergeBlocks.push(
|
|
491
|
-
`<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`,
|
|
492
|
-
);
|
|
446
|
+
// Walk the parsed marker operation list against a starting body, returning
|
|
447
|
+
// the final body or the first error. SEARCH/REPLACE and DELETE go through
|
|
448
|
+
// Hedberg.replace (fuzzy whitespace match); NEW/REPLACE/PREPEND/APPEND
|
|
449
|
+
// are direct string operations.
|
|
450
|
+
static #applyOperations(currentBody, operations) {
|
|
451
|
+
let body = currentBody;
|
|
452
|
+
for (const op of operations) {
|
|
453
|
+
if (op.op === "new" || op.op === "replace") {
|
|
454
|
+
body = op.content;
|
|
455
|
+
} else if (op.op === "append") {
|
|
456
|
+
body = body + op.content;
|
|
457
|
+
} else if (op.op === "prepend") {
|
|
458
|
+
body = op.content + body;
|
|
459
|
+
} else if (op.op === "delete") {
|
|
460
|
+
const result = Hedberg.replace(body, op.content, "");
|
|
461
|
+
if (result.error) {
|
|
462
|
+
return { body, error: result.error, attempted: op.content };
|
|
493
463
|
}
|
|
464
|
+
body = result.patch;
|
|
465
|
+
} else if (op.op === "search_replace") {
|
|
466
|
+
const result = Hedberg.replace(body, op.search, op.replace);
|
|
467
|
+
if (result.error) {
|
|
468
|
+
return { body, error: result.error, attempted: op.search };
|
|
469
|
+
}
|
|
470
|
+
body = result.patch;
|
|
494
471
|
}
|
|
495
|
-
|
|
496
|
-
const state = lastError ? "failed" : "proposed";
|
|
497
|
-
const outcome = lastError ? "conflict" : null;
|
|
498
|
-
const udiff =
|
|
499
|
-
current !== original
|
|
500
|
-
? generatePatch(entryPath, original, current)
|
|
501
|
-
: null;
|
|
502
|
-
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
503
|
-
const beforeTokens = targetEntry[0].tokens;
|
|
504
|
-
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
505
|
-
|
|
506
|
-
await store.set({
|
|
507
|
-
runId,
|
|
508
|
-
turn,
|
|
509
|
-
path: entry.path,
|
|
510
|
-
body: current,
|
|
511
|
-
state,
|
|
512
|
-
outcome,
|
|
513
|
-
attributes: {
|
|
514
|
-
path: entryPath,
|
|
515
|
-
patch: udiff,
|
|
516
|
-
merge,
|
|
517
|
-
beforeTokens,
|
|
518
|
-
afterTokens,
|
|
519
|
-
warning: lastWarning,
|
|
520
|
-
error: lastError,
|
|
521
|
-
},
|
|
522
|
-
loopId,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Missing `replace` = delete the match; normalize to empty string.
|
|
528
|
-
static #resolveReplace(attrs) {
|
|
529
|
-
return attrs.replace === undefined ? "" : attrs.replace;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
static #buildRevision(attrs) {
|
|
533
|
-
if (attrs.search != null) {
|
|
534
|
-
return { search: attrs.search, replace: Set.#resolveReplace(attrs) };
|
|
535
|
-
}
|
|
536
|
-
if (attrs.blocks?.length > 0) {
|
|
537
|
-
return { blocks: attrs.blocks };
|
|
538
|
-
}
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
static #applyRevision(body, attrs) {
|
|
543
|
-
if (attrs.search != null) {
|
|
544
|
-
return Hedberg.replace(body, attrs.search, Set.#resolveReplace(attrs), {
|
|
545
|
-
sed: attrs.sed,
|
|
546
|
-
flags: attrs.flags,
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
if (attrs.blocks?.length > 0 && attrs.blocks[0].search === null) {
|
|
550
|
-
return {
|
|
551
|
-
patch: attrs.blocks[0].replace,
|
|
552
|
-
searchText: null,
|
|
553
|
-
replaceText: attrs.blocks[0].replace,
|
|
554
|
-
warning: null,
|
|
555
|
-
error: null,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
if (body && attrs.blocks?.length > 0) {
|
|
559
|
-
if (attrs.blocks.length === 1) {
|
|
560
|
-
const block = attrs.blocks[0];
|
|
561
|
-
return Hedberg.replace(body, block.search, block.replace, {
|
|
562
|
-
sed: block.sed,
|
|
563
|
-
flags: block.flags,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
let current = body;
|
|
567
|
-
let lastWarning = null;
|
|
568
|
-
for (const block of attrs.blocks) {
|
|
569
|
-
const result = Hedberg.replace(current, block.search, block.replace, {
|
|
570
|
-
sed: block.sed,
|
|
571
|
-
flags: block.flags,
|
|
572
|
-
});
|
|
573
|
-
if (result.error) return result;
|
|
574
|
-
if (result.warning) lastWarning = result.warning;
|
|
575
|
-
if (result.patch) current = result.patch;
|
|
576
|
-
}
|
|
577
|
-
return {
|
|
578
|
-
patch: current !== body ? current : null,
|
|
579
|
-
searchText: null,
|
|
580
|
-
replaceText: null,
|
|
581
|
-
warning: lastWarning,
|
|
582
|
-
error: null,
|
|
583
|
-
};
|
|
584
472
|
}
|
|
585
|
-
return {
|
|
586
|
-
patch: null,
|
|
587
|
-
searchText: null,
|
|
588
|
-
replaceText: null,
|
|
589
|
-
warning: null,
|
|
590
|
-
error: null,
|
|
591
|
-
};
|
|
473
|
+
return { body, error: null, attempted: null };
|
|
592
474
|
}
|
|
593
475
|
}
|