@possumtech/rummy 0.4.0 → 2.0.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.
- package/.env.example +21 -4
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -331
- package/src/agent/ContextAssembler.js +4 -2
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -379
- package/src/agent/XmlParser.js +242 -67
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -118
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +35 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +140 -37
- package/src/hooks/ToolRegistry.js +36 -35
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -23
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -23
- package/src/plugins/budget/budget.js +261 -69
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +31 -13
- package/src/plugins/cp/cpDoc.js +2 -23
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +47 -8
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -7
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +125 -49
- package/src/plugins/get/getDoc.js +2 -43
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +43 -3
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +126 -12
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +77 -45
- package/src/plugins/known/knownDoc.js +2 -29
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +57 -24
- package/src/plugins/mv/mvDoc.js +2 -29
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +63 -18
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +58 -14
- package/src/plugins/rm/rmDoc.js +2 -24
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -77
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +52 -8
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -17
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +148 -74
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +14 -1
- package/src/plugins/think/thinkDoc.js +2 -17
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +56 -21
- package/src/plugins/unknown/unknownDoc.js +2 -25
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -27
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/src/agent/KnownStore.js +0 -338
- package/src/agent/ResponseHealer.js +0 -188
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -37
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -60
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -26
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -28
package/src/plugins/set/set.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Entries from "../../agent/Entries.js";
|
|
2
2
|
import { countTokens } from "../../agent/tokens.js";
|
|
3
|
+
import File from "../file/file.js";
|
|
3
4
|
import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
|
|
4
5
|
import { storePatternResult } from "../helpers.js";
|
|
5
6
|
import docs from "./setDoc.js";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const VALID_VISIBILITY = { archived: 1, summarized: 1, visible: 1 };
|
|
9
|
+
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
10
|
+
|
|
11
|
+
function isSetProposal(path) {
|
|
12
|
+
const m = LOG_ACTION_RE.exec(path);
|
|
13
|
+
return m?.[1] === "set";
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
|
|
10
17
|
export default class Set {
|
|
@@ -14,25 +21,121 @@ export default class Set {
|
|
|
14
21
|
this.#core = core;
|
|
15
22
|
core.registerScheme();
|
|
16
23
|
core.on("handler", this.handler.bind(this));
|
|
17
|
-
core.on("
|
|
18
|
-
core.on("
|
|
19
|
-
core.on("
|
|
24
|
+
core.on("visible", this.full.bind(this));
|
|
25
|
+
core.on("summarized", this.summary.bind(this));
|
|
26
|
+
core.on("proposal.prepare", this.#materializeRevisions.bind(this));
|
|
20
27
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
21
28
|
docsMap.set = docs;
|
|
22
29
|
return docsMap;
|
|
23
30
|
});
|
|
31
|
+
core.filter("proposal.accepting", this.#vetoReadonly.bind(this));
|
|
32
|
+
core.filter("proposal.content", this.#preferExistingBody.bind(this));
|
|
33
|
+
core.on("proposal.accepted", this.#materializeFile.bind(this));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #vetoReadonly(current, ctx) {
|
|
37
|
+
if (current) return current;
|
|
38
|
+
if (!isSetProposal(ctx.path)) return current;
|
|
39
|
+
if (!ctx.attrs?.path) return current;
|
|
40
|
+
const blocked = await File.isReadonly(
|
|
41
|
+
ctx.db,
|
|
42
|
+
ctx.projectId,
|
|
43
|
+
ctx.attrs.path,
|
|
44
|
+
);
|
|
45
|
+
if (!blocked) return current;
|
|
46
|
+
return {
|
|
47
|
+
allow: false,
|
|
48
|
+
outcome: "readonly",
|
|
49
|
+
body: `refused: ${ctx.attrs.path} is readonly`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async #preferExistingBody(defaultBody, ctx) {
|
|
54
|
+
if (!isSetProposal(ctx.path)) return defaultBody;
|
|
55
|
+
const existing = await ctx.entries.getBody(ctx.runId, ctx.path);
|
|
56
|
+
if (existing) return existing;
|
|
57
|
+
return defaultBody;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #materializeFile(ctx) {
|
|
61
|
+
if (!isSetProposal(ctx.path)) return;
|
|
62
|
+
const { attrs, runId, projectId, projectRoot, db, entries } = ctx;
|
|
63
|
+
if (!attrs?.path || !attrs?.merge) return;
|
|
64
|
+
|
|
65
|
+
const existing = await entries.getBody(runId, attrs.path);
|
|
66
|
+
const isNewFile = existing === null;
|
|
67
|
+
const fileBody = isNewFile ? "" : existing;
|
|
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
|
+
}
|
|
81
|
+
const turn = (await db.get_run_by_id.get({ id: runId })).next_turn;
|
|
82
|
+
// Preserve the file entry's current visibility — a <get>
|
|
83
|
+
// earlier in the run may have promoted it. Updating the
|
|
84
|
+
// body without specifying visibility falls through to
|
|
85
|
+
// the data-category default ("summarized") and wipes
|
|
86
|
+
// the promotion, making the model re-get the file next
|
|
87
|
+
// turn (then cycle-strike out).
|
|
88
|
+
const existingState = await entries.getState(runId, attrs.path);
|
|
89
|
+
await entries.set({
|
|
90
|
+
runId,
|
|
91
|
+
turn,
|
|
92
|
+
path: attrs.path,
|
|
93
|
+
body: patched,
|
|
94
|
+
visibility: existingState?.visibility,
|
|
95
|
+
});
|
|
96
|
+
if (projectRoot) {
|
|
97
|
+
const { writeFile } = await import("node:fs/promises");
|
|
98
|
+
const { join } = await import("node:path");
|
|
99
|
+
await writeFile(join(projectRoot, attrs.path), patched).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
if (isNewFile && projectId) {
|
|
102
|
+
await File.setConstraint(db, projectId, attrs.path, "active");
|
|
103
|
+
}
|
|
24
104
|
}
|
|
25
105
|
|
|
26
106
|
async handler(entry, rummy) {
|
|
27
107
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
28
108
|
const attrs = entry.attributes;
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
const visibilityAttr = VALID_VISIBILITY[attrs.visibility]
|
|
110
|
+
? attrs.visibility
|
|
111
|
+
: null;
|
|
112
|
+
const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
|
|
32
113
|
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
33
114
|
|
|
34
|
-
//
|
|
35
|
-
|
|
115
|
+
// Invalid visibility value on a body-less set: reject with an
|
|
116
|
+
// error instead of falling through to the write path. Without
|
|
117
|
+
// this guard, a typo like visibility="promoted" (pre-migration
|
|
118
|
+
// terminology) silently body-wiped the target — the fidelity
|
|
119
|
+
// regression that cost us multiple demo runs.
|
|
120
|
+
if (
|
|
121
|
+
!entry.body &&
|
|
122
|
+
attrs.path &&
|
|
123
|
+
attrs.visibility !== undefined &&
|
|
124
|
+
!visibilityAttr
|
|
125
|
+
) {
|
|
126
|
+
await rummy.hooks.error.log.emit({
|
|
127
|
+
store,
|
|
128
|
+
runId,
|
|
129
|
+
turn,
|
|
130
|
+
loopId,
|
|
131
|
+
message: `Invalid visibility "${attrs.visibility}" on <set path="${attrs.path}"/>. Use visibility="visible|summarized|archived".`,
|
|
132
|
+
status: 400,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Pure visibility/metadata change — no body content
|
|
138
|
+
if (!entry.body && visibilityAttr && attrs.path) {
|
|
36
139
|
const target = attrs.path;
|
|
37
140
|
const matches = await store.getEntriesByPattern(
|
|
38
141
|
runId,
|
|
@@ -40,34 +143,44 @@ export default class Set {
|
|
|
40
143
|
attrs.body,
|
|
41
144
|
);
|
|
42
145
|
if (matches.length === 0) {
|
|
43
|
-
await store.
|
|
146
|
+
await store.set({
|
|
44
147
|
runId,
|
|
45
148
|
turn,
|
|
46
|
-
entry.resultPath,
|
|
47
|
-
`${target} not found`,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
149
|
+
path: entry.resultPath,
|
|
150
|
+
body: `${target} not found`,
|
|
151
|
+
state: "failed",
|
|
152
|
+
outcome: "not_found",
|
|
153
|
+
visibility: "archived",
|
|
154
|
+
loopId,
|
|
155
|
+
});
|
|
51
156
|
return;
|
|
52
157
|
}
|
|
53
158
|
for (const match of matches) {
|
|
54
|
-
await store.
|
|
159
|
+
await store.set({
|
|
160
|
+
runId: runId,
|
|
161
|
+
path: match.path,
|
|
162
|
+
visibility: visibilityAttr,
|
|
163
|
+
});
|
|
55
164
|
if (summaryText) {
|
|
56
|
-
await store.
|
|
57
|
-
|
|
165
|
+
await store.set({
|
|
166
|
+
runId: runId,
|
|
167
|
+
path: match.path,
|
|
168
|
+
attributes: {
|
|
169
|
+
summary: summaryText,
|
|
170
|
+
},
|
|
58
171
|
});
|
|
59
172
|
}
|
|
60
173
|
}
|
|
61
|
-
const label =
|
|
62
|
-
|
|
63
|
-
await store.upsert(
|
|
174
|
+
const label = `set to ${visibilityAttr}`;
|
|
175
|
+
await store.set({
|
|
64
176
|
runId,
|
|
65
177
|
turn,
|
|
66
|
-
entry.resultPath,
|
|
67
|
-
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
178
|
+
path: entry.resultPath,
|
|
179
|
+
body: `${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
180
|
+
state: "resolved",
|
|
181
|
+
visibility: "archived",
|
|
182
|
+
loopId,
|
|
183
|
+
});
|
|
71
184
|
return;
|
|
72
185
|
}
|
|
73
186
|
|
|
@@ -97,18 +210,32 @@ export default class Set {
|
|
|
97
210
|
const target = attrs.path;
|
|
98
211
|
if (!target) return;
|
|
99
212
|
|
|
100
|
-
const scheme =
|
|
213
|
+
const scheme = Entries.scheme(target);
|
|
101
214
|
if (scheme === null) {
|
|
102
215
|
// File write — diff against existing content
|
|
103
216
|
const existing = await store.getBody(runId, target);
|
|
104
|
-
const oldContent = existing
|
|
105
|
-
const newContent = entry.body
|
|
217
|
+
const oldContent = existing === null ? "" : existing;
|
|
218
|
+
const newContent = entry.body;
|
|
106
219
|
const udiff = generatePatch(target, oldContent, newContent);
|
|
107
220
|
const merge = oldContent
|
|
108
221
|
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
109
222
|
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
110
|
-
|
|
111
|
-
|
|
223
|
+
const beforeTokens = oldContent ? countTokens(oldContent) : 0;
|
|
224
|
+
const afterTokens = countTokens(newContent);
|
|
225
|
+
await store.set({
|
|
226
|
+
runId,
|
|
227
|
+
turn,
|
|
228
|
+
path: entry.resultPath,
|
|
229
|
+
body: newContent,
|
|
230
|
+
state: "proposed",
|
|
231
|
+
attributes: {
|
|
232
|
+
path: target,
|
|
233
|
+
patch: udiff,
|
|
234
|
+
merge,
|
|
235
|
+
beforeTokens,
|
|
236
|
+
afterTokens,
|
|
237
|
+
summary: summaryText,
|
|
238
|
+
},
|
|
112
239
|
loopId,
|
|
113
240
|
});
|
|
114
241
|
} else if (attrs.filter || target.includes("*")) {
|
|
@@ -118,12 +245,12 @@ export default class Set {
|
|
|
118
245
|
target,
|
|
119
246
|
attrs.filter,
|
|
120
247
|
);
|
|
121
|
-
await store.
|
|
122
|
-
runId,
|
|
123
|
-
target,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
248
|
+
await store.set({
|
|
249
|
+
runId: runId,
|
|
250
|
+
path: target,
|
|
251
|
+
body: entry.body,
|
|
252
|
+
bodyFilter: attrs.filter === undefined ? null : attrs.filter,
|
|
253
|
+
});
|
|
127
254
|
await storePatternResult(
|
|
128
255
|
store,
|
|
129
256
|
runId,
|
|
@@ -135,42 +262,91 @@ export default class Set {
|
|
|
135
262
|
{ loopId },
|
|
136
263
|
);
|
|
137
264
|
} else {
|
|
138
|
-
// Direct scheme write
|
|
139
|
-
|
|
140
|
-
|
|
265
|
+
// Direct scheme write (known://, unknown://, etc.)
|
|
266
|
+
// Same result shape as file writes — diff against existing.
|
|
267
|
+
const existing = await store.getBody(runId, target);
|
|
268
|
+
const oldContent = existing === null ? "" : existing;
|
|
269
|
+
const newContent = entry.body;
|
|
270
|
+
const udiff = generatePatch(target, oldContent, newContent);
|
|
271
|
+
const merge = oldContent
|
|
272
|
+
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
273
|
+
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
274
|
+
const beforeTokens = oldContent ? countTokens(oldContent) : 0;
|
|
275
|
+
const afterTokens = countTokens(newContent);
|
|
276
|
+
|
|
277
|
+
await store.set({
|
|
278
|
+
runId,
|
|
279
|
+
turn,
|
|
280
|
+
path: target,
|
|
281
|
+
body: newContent,
|
|
282
|
+
state: "resolved",
|
|
283
|
+
// Scheme writes default to promoted — the model wrote it, so
|
|
284
|
+
// it's material unless they explicitly demote/archive.
|
|
285
|
+
visibility: visibilityAttr ? visibilityAttr : "visible",
|
|
141
286
|
attributes: summaryText ? { summary: summaryText } : null,
|
|
142
287
|
loopId,
|
|
143
288
|
});
|
|
289
|
+
await store.set({
|
|
290
|
+
runId,
|
|
291
|
+
turn,
|
|
292
|
+
path: entry.resultPath,
|
|
293
|
+
body: newContent,
|
|
294
|
+
state: "resolved",
|
|
295
|
+
loopId,
|
|
296
|
+
attributes: {
|
|
297
|
+
path: target,
|
|
298
|
+
patch: udiff,
|
|
299
|
+
merge,
|
|
300
|
+
beforeTokens,
|
|
301
|
+
afterTokens,
|
|
302
|
+
summary: summaryText,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
144
305
|
}
|
|
145
306
|
}
|
|
146
307
|
|
|
147
|
-
// Apply
|
|
148
|
-
if (
|
|
308
|
+
// Apply visibility after all write operations
|
|
309
|
+
if (visibilityAttr && attrs.path) {
|
|
149
310
|
const target = attrs.path;
|
|
150
|
-
const scheme =
|
|
311
|
+
const scheme = Entries.scheme(target);
|
|
151
312
|
if (scheme !== null) {
|
|
152
|
-
await store.
|
|
313
|
+
await store.set({
|
|
314
|
+
runId: runId,
|
|
315
|
+
path: target,
|
|
316
|
+
visibility: visibilityAttr,
|
|
317
|
+
});
|
|
153
318
|
}
|
|
154
319
|
if (summaryText) {
|
|
155
|
-
await store.
|
|
320
|
+
await store.set({
|
|
321
|
+
runId: runId,
|
|
322
|
+
path: target,
|
|
323
|
+
attributes: { summary: summaryText },
|
|
324
|
+
});
|
|
156
325
|
}
|
|
157
326
|
}
|
|
158
327
|
}
|
|
159
328
|
|
|
160
329
|
full(entry) {
|
|
161
330
|
const attrs = entry.attributes;
|
|
162
|
-
const
|
|
163
|
-
if (attrs.error) return `# set ${
|
|
331
|
+
const target = attrs.path || entry.path;
|
|
332
|
+
if (attrs.error) return `# set ${target}\n${attrs.error}`;
|
|
164
333
|
const tokens =
|
|
165
334
|
attrs.beforeTokens != null
|
|
166
335
|
? ` ${attrs.beforeTokens}→${attrs.afterTokens} tokens`
|
|
167
336
|
: "";
|
|
168
|
-
if (!attrs.merge) return `# set ${
|
|
169
|
-
return `# set ${
|
|
337
|
+
if (!attrs.merge) return `# set ${target}${tokens}`;
|
|
338
|
+
return `# set ${target}${tokens}\n${attrs.merge}`;
|
|
170
339
|
}
|
|
171
340
|
|
|
172
341
|
summary(entry) {
|
|
173
|
-
|
|
342
|
+
if (!entry.body) return "";
|
|
343
|
+
// Preserve SEARCH/REPLACE merge blocks intact — truncating them
|
|
344
|
+
// drops the before/after the model needs to recognize its edit.
|
|
345
|
+
if (/<<<<<<< SEARCH[\s\S]*>>>>>>> REPLACE/.test(entry.body)) {
|
|
346
|
+
return entry.body;
|
|
347
|
+
}
|
|
348
|
+
const flat = entry.body.replace(/\s+/g, " ").trim();
|
|
349
|
+
return flat.length <= 80 ? flat : `${flat.slice(0, 77)}...`;
|
|
174
350
|
}
|
|
175
351
|
|
|
176
352
|
async #processEdit(rummy, entry, attrs) {
|
|
@@ -179,8 +355,14 @@ export default class Set {
|
|
|
179
355
|
const matches = await store.getEntriesByPattern(runId, target, attrs.body);
|
|
180
356
|
|
|
181
357
|
if (matches.length === 0) {
|
|
182
|
-
await store.
|
|
183
|
-
|
|
358
|
+
await store.set({
|
|
359
|
+
runId,
|
|
360
|
+
turn,
|
|
361
|
+
path: entry.resultPath,
|
|
362
|
+
body: "",
|
|
363
|
+
state: "failed",
|
|
364
|
+
outcome: "not_found",
|
|
365
|
+
attributes: { path: target, error: `${target} not found in context` },
|
|
184
366
|
loopId,
|
|
185
367
|
});
|
|
186
368
|
return;
|
|
@@ -188,37 +370,78 @@ export default class Set {
|
|
|
188
370
|
|
|
189
371
|
for (const match of matches) {
|
|
190
372
|
if (match.scheme === null) {
|
|
373
|
+
// Bare file path — apply the edit immediately against the
|
|
374
|
+
// match body so the log carries a concrete before/after
|
|
375
|
+
// merge. #materializeRevisions still runs at turn-end to
|
|
376
|
+
// consolidate the set:// proposal for client acceptance.
|
|
191
377
|
const canonicalPath = `set://${match.path}`;
|
|
192
378
|
const revision = Set.#buildRevision(attrs);
|
|
193
379
|
const existingAttrs = await rummy.getAttributes(canonicalPath);
|
|
194
|
-
const revisions = existingAttrs?.revisions
|
|
380
|
+
const revisions = existingAttrs?.revisions
|
|
381
|
+
? existingAttrs.revisions
|
|
382
|
+
: [];
|
|
195
383
|
revisions.push(revision);
|
|
196
|
-
await store.
|
|
197
|
-
|
|
384
|
+
await store.set({
|
|
385
|
+
runId,
|
|
386
|
+
turn,
|
|
387
|
+
path: canonicalPath,
|
|
388
|
+
body: "",
|
|
389
|
+
state: "resolved",
|
|
390
|
+
attributes: { path: match.path, revisions },
|
|
391
|
+
loopId,
|
|
392
|
+
});
|
|
393
|
+
const { patch, searchText, replaceText, warning, error } =
|
|
394
|
+
Set.#applyRevision(match.body, attrs);
|
|
395
|
+
const merge =
|
|
396
|
+
searchText != null
|
|
397
|
+
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
398
|
+
: null;
|
|
399
|
+
const beforeTokens = match.tokens;
|
|
400
|
+
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
401
|
+
const logState = error ? "failed" : "resolved";
|
|
402
|
+
await store.set({
|
|
403
|
+
runId,
|
|
404
|
+
turn,
|
|
405
|
+
path: entry.resultPath,
|
|
406
|
+
body: merge ?? (patch || `edit to ${match.path}`),
|
|
407
|
+
state: logState,
|
|
408
|
+
outcome: error ? "conflict" : null,
|
|
409
|
+
attributes: {
|
|
410
|
+
path: match.path,
|
|
411
|
+
merge,
|
|
412
|
+
beforeTokens,
|
|
413
|
+
afterTokens,
|
|
414
|
+
warning,
|
|
415
|
+
error,
|
|
416
|
+
},
|
|
198
417
|
loopId,
|
|
199
418
|
});
|
|
200
|
-
if (KnownStore.normalizePath(entry.resultPath) !== canonicalPath) {
|
|
201
|
-
await store.remove(runId, entry.resultPath);
|
|
202
|
-
}
|
|
203
419
|
return;
|
|
204
420
|
}
|
|
205
421
|
|
|
206
422
|
const { patch, searchText, replaceText, warning, error } =
|
|
207
423
|
Set.#applyRevision(match.body, attrs);
|
|
208
424
|
|
|
209
|
-
const
|
|
210
|
-
const
|
|
425
|
+
const state = error ? "failed" : "resolved";
|
|
426
|
+
const outcome = error ? "conflict" : null;
|
|
211
427
|
const udiff = patch ? generatePatch(match.path, match.body, patch) : null;
|
|
212
428
|
const merge =
|
|
213
429
|
searchText != null
|
|
214
430
|
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
215
431
|
: null;
|
|
216
|
-
const beforeTokens = match.tokens
|
|
432
|
+
const beforeTokens = match.tokens;
|
|
217
433
|
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
218
434
|
|
|
219
|
-
|
|
435
|
+
// Log entry at log://turn_N/set/<target> records the action.
|
|
436
|
+
await store.set({
|
|
437
|
+
runId,
|
|
438
|
+
turn,
|
|
439
|
+
path: entry.resultPath,
|
|
440
|
+
body: patch ?? match.body,
|
|
441
|
+
state,
|
|
442
|
+
outcome,
|
|
220
443
|
attributes: {
|
|
221
|
-
|
|
444
|
+
path: match.path,
|
|
222
445
|
patch: udiff,
|
|
223
446
|
merge,
|
|
224
447
|
beforeTokens,
|
|
@@ -229,8 +452,13 @@ export default class Set {
|
|
|
229
452
|
loopId,
|
|
230
453
|
});
|
|
231
454
|
|
|
232
|
-
if (
|
|
233
|
-
await store.
|
|
455
|
+
if (state === "resolved" && patch) {
|
|
456
|
+
await store.set({
|
|
457
|
+
runId,
|
|
458
|
+
turn,
|
|
459
|
+
path: match.path,
|
|
460
|
+
body: patch,
|
|
461
|
+
state: match.state,
|
|
234
462
|
loopId,
|
|
235
463
|
});
|
|
236
464
|
}
|
|
@@ -248,11 +476,11 @@ export default class Set {
|
|
|
248
476
|
: entry.attributes;
|
|
249
477
|
if (!attrs?.revisions?.length) continue;
|
|
250
478
|
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
479
|
+
const entryPath = attrs.path;
|
|
480
|
+
const targetEntry = await store.getEntriesByPattern(runId, entryPath);
|
|
481
|
+
if (targetEntry.length === 0) continue;
|
|
254
482
|
|
|
255
|
-
const original =
|
|
483
|
+
const original = targetEntry[0].body;
|
|
256
484
|
let current = original;
|
|
257
485
|
const mergeBlocks = [];
|
|
258
486
|
let lastError = null;
|
|
@@ -274,18 +502,25 @@ export default class Set {
|
|
|
274
502
|
}
|
|
275
503
|
}
|
|
276
504
|
|
|
277
|
-
const state = lastError ?
|
|
505
|
+
const state = lastError ? "failed" : "proposed";
|
|
506
|
+
const outcome = lastError ? "conflict" : null;
|
|
278
507
|
const udiff =
|
|
279
508
|
current !== original
|
|
280
|
-
? generatePatch(
|
|
509
|
+
? generatePatch(entryPath, original, current)
|
|
281
510
|
: null;
|
|
282
511
|
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
283
|
-
const beforeTokens =
|
|
512
|
+
const beforeTokens = targetEntry[0].tokens;
|
|
284
513
|
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
285
514
|
|
|
286
|
-
await store.
|
|
515
|
+
await store.set({
|
|
516
|
+
runId,
|
|
517
|
+
turn,
|
|
518
|
+
path: entry.path,
|
|
519
|
+
body: current,
|
|
520
|
+
state,
|
|
521
|
+
outcome,
|
|
287
522
|
attributes: {
|
|
288
|
-
|
|
523
|
+
path: entryPath,
|
|
289
524
|
patch: udiff,
|
|
290
525
|
merge,
|
|
291
526
|
beforeTokens,
|
|
@@ -298,9 +533,15 @@ export default class Set {
|
|
|
298
533
|
}
|
|
299
534
|
}
|
|
300
535
|
|
|
536
|
+
// `replace` attr is optional in search/replace form — absence means
|
|
537
|
+
// "delete the match"; normalize to empty string at this boundary.
|
|
538
|
+
static #resolveReplace(attrs) {
|
|
539
|
+
return attrs.replace === undefined ? "" : attrs.replace;
|
|
540
|
+
}
|
|
541
|
+
|
|
301
542
|
static #buildRevision(attrs) {
|
|
302
543
|
if (attrs.search != null) {
|
|
303
|
-
return { search: attrs.search, replace: attrs
|
|
544
|
+
return { search: attrs.search, replace: Set.#resolveReplace(attrs) };
|
|
304
545
|
}
|
|
305
546
|
if (attrs.blocks?.length > 0) {
|
|
306
547
|
return { blocks: attrs.blocks };
|
|
@@ -310,7 +551,7 @@ export default class Set {
|
|
|
310
551
|
|
|
311
552
|
static #applyRevision(body, attrs) {
|
|
312
553
|
if (attrs.search != null) {
|
|
313
|
-
return Hedberg.replace(body, attrs.search, attrs
|
|
554
|
+
return Hedberg.replace(body, attrs.search, Set.#resolveReplace(attrs), {
|
|
314
555
|
sed: attrs.sed,
|
|
315
556
|
flags: attrs.flags,
|
|
316
557
|
});
|
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// Text goes to the model. Rationale stays in source.
|
|
3
|
-
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
-
const LINES = [
|
|
5
|
-
[
|
|
6
|
-
'## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry',
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
'Example: <set path="known://project/milestones" fidelity="summary" summary="milestone,deadline,2026"/>',
|
|
10
|
-
"Fidelity control first — most unique capability of set.",
|
|
11
|
-
],
|
|
12
|
-
[
|
|
13
|
-
`Example: <set path="src/app.js">
|
|
14
|
-
<<<<<<< SEARCH
|
|
15
|
-
old text
|
|
16
|
-
=======
|
|
17
|
-
new text
|
|
18
|
-
>>>>>>> REPLACE
|
|
19
|
-
</set>`,
|
|
20
|
-
"SEARCH/REPLACE block — primary edit pattern for existing files.",
|
|
21
|
-
],
|
|
22
|
-
[
|
|
23
|
-
'Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/host = 127.0.0.1/host = localhost/g;</set>',
|
|
24
|
-
"Sed syntax: chained s/old/new/ patterns with semicolons.",
|
|
25
|
-
],
|
|
26
|
-
[
|
|
27
|
-
'Example: <set path="example.md">Full file content here</set>',
|
|
28
|
-
"Create: body contents are entire file.",
|
|
29
|
-
],
|
|
30
|
-
[
|
|
31
|
-
"* YOU MUST NOT use <sh/> or <env/> to list, create, read, or edit files. Use the Tool Commands.",
|
|
32
|
-
"Forces file operations through set/get.",
|
|
33
|
-
],
|
|
34
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
35
2
|
|
|
36
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "setDoc.md");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry
|
|
2
|
+
|
|
3
|
+
Example: <set path="known://project/milestones" visibility="summarized" summary="milestone,deadline,2026"/>
|
|
4
|
+
<!-- Visibility control first — most unique capability of set. -->
|
|
5
|
+
|
|
6
|
+
Example: <set path="src/app.js">
|
|
7
|
+
<<<<<<< SEARCH
|
|
8
|
+
old text
|
|
9
|
+
=======
|
|
10
|
+
new text
|
|
11
|
+
>>>>>>> REPLACE
|
|
12
|
+
</set>
|
|
13
|
+
<!-- SEARCH/REPLACE block — primary edit pattern for existing files. -->
|
|
14
|
+
|
|
15
|
+
Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/We're almost done/We're done./g;</set>
|
|
16
|
+
<!-- Sed syntax: chained s/old/new/ patterns with semicolons. -->
|
|
17
|
+
|
|
18
|
+
Example: <set path="example.md">Full file content here</set>
|
|
19
|
+
<!-- Create: body contents are entire file. -->
|
|
20
|
+
|
|
21
|
+
* YOU MUST NOT use <sh></sh> or <env></env> to list, create, read, or edit files — use <get></get> and <set></set>
|
|
22
|
+
<!-- Reinforces at the decision point — model reading setDoc for file ops sees the prohibition here, not just buried in shDoc/envDoc which it may not be reading. -->
|
package/src/plugins/sh/README.md
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
|
-
# sh
|
|
1
|
+
# sh {#sh_plugin}
|
|
2
2
|
|
|
3
|
-
Proposes shell command execution for client approval.
|
|
3
|
+
Proposes shell command execution for client approval. Streaming
|
|
4
|
+
producer: the actual stdout/stderr arrive as separate data entries
|
|
5
|
+
after the proposal is accepted.
|
|
4
6
|
|
|
5
7
|
## Registration
|
|
6
8
|
|
|
7
9
|
- **Tool**: `sh`
|
|
8
|
-
- **
|
|
9
|
-
- **Handler**: Upserts the entry at status 202 (proposed). The
|
|
10
|
+
- **Scheme**: `sh` — `category: "data"` (channels only; see below)
|
|
11
|
+
- **Handler**: Upserts the proposal entry at status 202 (proposed). The
|
|
12
|
+
client must approve execution.
|
|
13
|
+
|
|
14
|
+
## Two namespaces per invocation
|
|
15
|
+
|
|
16
|
+
A single `<sh>` emission produces entries in two namespaces — one audit
|
|
17
|
+
record, one data payload:
|
|
18
|
+
|
|
19
|
+
- **Log entry**: `log://turn_N/sh/{slug}` — scheme=`log`, category=`logging`.
|
|
20
|
+
This is the proposal the client sees and resolves. On accept, body is
|
|
21
|
+
rewritten to `ran '{cmd}' (in progress). Output: {dataBase}_1, {dataBase}_2`
|
|
22
|
+
and finalized by `stream/completed` with exit code + duration. Renders
|
|
23
|
+
inside the `<log>` block as `<sh>`.
|
|
24
|
+
- **Data channels**: `sh://turn_N/{slug}_1` (stdout), `sh://turn_N/{slug}_2`
|
|
25
|
+
(stderr) — scheme=`sh`, category=`data`. Created at status=102 on
|
|
26
|
+
proposal acceptance, grow via the `stream` RPC, transition to 200/500
|
|
27
|
+
via `stream/completed`. Render inside the `<context>` block as `<sh>`.
|
|
28
|
+
|
|
29
|
+
The `sh` scheme exists **only** for the data channels. The proposal/log
|
|
30
|
+
entry itself is in the unified `log://` namespace along with every
|
|
31
|
+
other action record. See [scheme_category_split](#scheme_category_split).
|
|
10
32
|
|
|
11
33
|
## Projection
|
|
12
34
|
|
|
13
|
-
|
|
35
|
+
- **Visible**: `# sh {command}\n{body}` (channel body is the captured stream).
|
|
36
|
+
- **Summarized**: empty (the command + path are already shown via attrs).
|
|
14
37
|
|
|
15
38
|
## Behavior
|
|
16
39
|
|