@possumtech/rummy 0.5.0 → 2.0.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 +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -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 +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -421
- package/src/agent/XmlParser.js +99 -33
- 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 +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- 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 +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -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 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +306 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- 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 +45 -6
- 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 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- 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 +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +129 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- 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 +64 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +525 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- 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 +50 -6
- 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 -18
- 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 +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +83 -5
- package/src/plugins/update/updateDoc.js +2 -30
- 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/plugins/yolo/yolo.js +192 -0
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- 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 -45
- 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 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
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,24 +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
|
|
109
|
+
const visibilityAttr = VALID_VISIBILITY[attrs.visibility]
|
|
110
|
+
? attrs.visibility
|
|
111
|
+
: null;
|
|
30
112
|
const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
|
|
31
113
|
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
32
114
|
|
|
33
|
-
//
|
|
34
|
-
|
|
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) {
|
|
35
139
|
const target = attrs.path;
|
|
36
140
|
const matches = await store.getEntriesByPattern(
|
|
37
141
|
runId,
|
|
@@ -39,33 +143,44 @@ export default class Set {
|
|
|
39
143
|
attrs.body,
|
|
40
144
|
);
|
|
41
145
|
if (matches.length === 0) {
|
|
42
|
-
await store.
|
|
146
|
+
await store.set({
|
|
43
147
|
runId,
|
|
44
148
|
turn,
|
|
45
|
-
entry.resultPath,
|
|
46
|
-
`${target} not found`,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
149
|
+
path: entry.resultPath,
|
|
150
|
+
body: `${target} not found`,
|
|
151
|
+
state: "failed",
|
|
152
|
+
outcome: "not_found",
|
|
153
|
+
visibility: "archived",
|
|
154
|
+
loopId,
|
|
155
|
+
});
|
|
50
156
|
return;
|
|
51
157
|
}
|
|
52
158
|
for (const match of matches) {
|
|
53
|
-
await store.
|
|
159
|
+
await store.set({
|
|
160
|
+
runId: runId,
|
|
161
|
+
path: match.path,
|
|
162
|
+
visibility: visibilityAttr,
|
|
163
|
+
});
|
|
54
164
|
if (summaryText) {
|
|
55
|
-
await store.
|
|
56
|
-
|
|
165
|
+
await store.set({
|
|
166
|
+
runId: runId,
|
|
167
|
+
path: match.path,
|
|
168
|
+
attributes: {
|
|
169
|
+
summary: summaryText,
|
|
170
|
+
},
|
|
57
171
|
});
|
|
58
172
|
}
|
|
59
173
|
}
|
|
60
|
-
const label = `set to ${
|
|
61
|
-
await store.
|
|
174
|
+
const label = `set to ${visibilityAttr}`;
|
|
175
|
+
await store.set({
|
|
62
176
|
runId,
|
|
63
177
|
turn,
|
|
64
|
-
entry.resultPath,
|
|
65
|
-
`${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
178
|
+
path: entry.resultPath,
|
|
179
|
+
body: `${matches.map((m) => m.path).join(", ")} ${label}`,
|
|
180
|
+
state: "resolved",
|
|
181
|
+
visibility: "archived",
|
|
182
|
+
loopId,
|
|
183
|
+
});
|
|
69
184
|
return;
|
|
70
185
|
}
|
|
71
186
|
|
|
@@ -95,18 +210,32 @@ export default class Set {
|
|
|
95
210
|
const target = attrs.path;
|
|
96
211
|
if (!target) return;
|
|
97
212
|
|
|
98
|
-
const scheme =
|
|
213
|
+
const scheme = Entries.scheme(target);
|
|
99
214
|
if (scheme === null) {
|
|
100
215
|
// File write — diff against existing content
|
|
101
216
|
const existing = await store.getBody(runId, target);
|
|
102
|
-
const oldContent = existing
|
|
103
|
-
const newContent = entry.body
|
|
217
|
+
const oldContent = existing === null ? "" : existing;
|
|
218
|
+
const newContent = entry.body;
|
|
104
219
|
const udiff = generatePatch(target, oldContent, newContent);
|
|
105
220
|
const merge = oldContent
|
|
106
221
|
? `<<<<<<< SEARCH\n${oldContent}\n=======\n${newContent}\n>>>>>>> REPLACE`
|
|
107
222
|
: `<<<<<<< SEARCH\n=======\n${newContent}\n>>>>>>> REPLACE`;
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
},
|
|
110
239
|
loopId,
|
|
111
240
|
});
|
|
112
241
|
} else if (attrs.filter || target.includes("*")) {
|
|
@@ -116,12 +245,12 @@ export default class Set {
|
|
|
116
245
|
target,
|
|
117
246
|
attrs.filter,
|
|
118
247
|
);
|
|
119
|
-
await store.
|
|
120
|
-
runId,
|
|
121
|
-
target,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
248
|
+
await store.set({
|
|
249
|
+
runId: runId,
|
|
250
|
+
path: target,
|
|
251
|
+
body: entry.body,
|
|
252
|
+
bodyFilter: attrs.filter === undefined ? null : attrs.filter,
|
|
253
|
+
});
|
|
125
254
|
await storePatternResult(
|
|
126
255
|
store,
|
|
127
256
|
runId,
|
|
@@ -133,42 +262,91 @@ export default class Set {
|
|
|
133
262
|
{ loopId },
|
|
134
263
|
);
|
|
135
264
|
} else {
|
|
136
|
-
// Direct scheme write
|
|
137
|
-
|
|
138
|
-
|
|
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",
|
|
139
286
|
attributes: summaryText ? { summary: summaryText } : null,
|
|
140
287
|
loopId,
|
|
141
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
|
+
});
|
|
142
305
|
}
|
|
143
306
|
}
|
|
144
307
|
|
|
145
|
-
// Apply
|
|
146
|
-
if (
|
|
308
|
+
// Apply visibility after all write operations
|
|
309
|
+
if (visibilityAttr && attrs.path) {
|
|
147
310
|
const target = attrs.path;
|
|
148
|
-
const scheme =
|
|
311
|
+
const scheme = Entries.scheme(target);
|
|
149
312
|
if (scheme !== null) {
|
|
150
|
-
await store.
|
|
313
|
+
await store.set({
|
|
314
|
+
runId: runId,
|
|
315
|
+
path: target,
|
|
316
|
+
visibility: visibilityAttr,
|
|
317
|
+
});
|
|
151
318
|
}
|
|
152
319
|
if (summaryText) {
|
|
153
|
-
await store.
|
|
320
|
+
await store.set({
|
|
321
|
+
runId: runId,
|
|
322
|
+
path: target,
|
|
323
|
+
attributes: { summary: summaryText },
|
|
324
|
+
});
|
|
154
325
|
}
|
|
155
326
|
}
|
|
156
327
|
}
|
|
157
328
|
|
|
158
329
|
full(entry) {
|
|
159
330
|
const attrs = entry.attributes;
|
|
160
|
-
const
|
|
161
|
-
if (attrs.error) return `# set ${
|
|
331
|
+
const target = attrs.path || entry.path;
|
|
332
|
+
if (attrs.error) return `# set ${target}\n${attrs.error}`;
|
|
162
333
|
const tokens =
|
|
163
334
|
attrs.beforeTokens != null
|
|
164
335
|
? ` ${attrs.beforeTokens}→${attrs.afterTokens} tokens`
|
|
165
336
|
: "";
|
|
166
|
-
if (!attrs.merge) return `# set ${
|
|
167
|
-
return `# set ${
|
|
337
|
+
if (!attrs.merge) return `# set ${target}${tokens}`;
|
|
338
|
+
return `# set ${target}${tokens}\n${attrs.merge}`;
|
|
168
339
|
}
|
|
169
340
|
|
|
170
|
-
summary() {
|
|
171
|
-
return "";
|
|
341
|
+
summary(entry) {
|
|
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)}...`;
|
|
172
350
|
}
|
|
173
351
|
|
|
174
352
|
async #processEdit(rummy, entry, attrs) {
|
|
@@ -177,8 +355,14 @@ export default class Set {
|
|
|
177
355
|
const matches = await store.getEntriesByPattern(runId, target, attrs.body);
|
|
178
356
|
|
|
179
357
|
if (matches.length === 0) {
|
|
180
|
-
await store.
|
|
181
|
-
|
|
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` },
|
|
182
366
|
loopId,
|
|
183
367
|
});
|
|
184
368
|
return;
|
|
@@ -186,37 +370,78 @@ export default class Set {
|
|
|
186
370
|
|
|
187
371
|
for (const match of matches) {
|
|
188
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.
|
|
189
377
|
const canonicalPath = `set://${match.path}`;
|
|
190
378
|
const revision = Set.#buildRevision(attrs);
|
|
191
379
|
const existingAttrs = await rummy.getAttributes(canonicalPath);
|
|
192
|
-
const revisions = existingAttrs?.revisions
|
|
380
|
+
const revisions = existingAttrs?.revisions
|
|
381
|
+
? existingAttrs.revisions
|
|
382
|
+
: [];
|
|
193
383
|
revisions.push(revision);
|
|
194
|
-
await store.
|
|
195
|
-
|
|
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
|
+
},
|
|
196
417
|
loopId,
|
|
197
418
|
});
|
|
198
|
-
if (KnownStore.normalizePath(entry.resultPath) !== canonicalPath) {
|
|
199
|
-
await store.remove(runId, entry.resultPath);
|
|
200
|
-
}
|
|
201
419
|
return;
|
|
202
420
|
}
|
|
203
421
|
|
|
204
422
|
const { patch, searchText, replaceText, warning, error } =
|
|
205
423
|
Set.#applyRevision(match.body, attrs);
|
|
206
424
|
|
|
207
|
-
const
|
|
208
|
-
const
|
|
425
|
+
const state = error ? "failed" : "resolved";
|
|
426
|
+
const outcome = error ? "conflict" : null;
|
|
209
427
|
const udiff = patch ? generatePatch(match.path, match.body, patch) : null;
|
|
210
428
|
const merge =
|
|
211
429
|
searchText != null
|
|
212
430
|
? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
|
|
213
431
|
: null;
|
|
214
|
-
const beforeTokens = match.tokens
|
|
432
|
+
const beforeTokens = match.tokens;
|
|
215
433
|
const afterTokens = patch ? countTokens(patch) : beforeTokens;
|
|
216
434
|
|
|
217
|
-
|
|
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,
|
|
218
443
|
attributes: {
|
|
219
|
-
|
|
444
|
+
path: match.path,
|
|
220
445
|
patch: udiff,
|
|
221
446
|
merge,
|
|
222
447
|
beforeTokens,
|
|
@@ -227,8 +452,13 @@ export default class Set {
|
|
|
227
452
|
loopId,
|
|
228
453
|
});
|
|
229
454
|
|
|
230
|
-
if (
|
|
231
|
-
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,
|
|
232
462
|
loopId,
|
|
233
463
|
});
|
|
234
464
|
}
|
|
@@ -246,11 +476,11 @@ export default class Set {
|
|
|
246
476
|
: entry.attributes;
|
|
247
477
|
if (!attrs?.revisions?.length) continue;
|
|
248
478
|
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
479
|
+
const entryPath = attrs.path;
|
|
480
|
+
const targetEntry = await store.getEntriesByPattern(runId, entryPath);
|
|
481
|
+
if (targetEntry.length === 0) continue;
|
|
252
482
|
|
|
253
|
-
const original =
|
|
483
|
+
const original = targetEntry[0].body;
|
|
254
484
|
let current = original;
|
|
255
485
|
const mergeBlocks = [];
|
|
256
486
|
let lastError = null;
|
|
@@ -272,18 +502,25 @@ export default class Set {
|
|
|
272
502
|
}
|
|
273
503
|
}
|
|
274
504
|
|
|
275
|
-
const state = lastError ?
|
|
505
|
+
const state = lastError ? "failed" : "proposed";
|
|
506
|
+
const outcome = lastError ? "conflict" : null;
|
|
276
507
|
const udiff =
|
|
277
508
|
current !== original
|
|
278
|
-
? generatePatch(
|
|
509
|
+
? generatePatch(entryPath, original, current)
|
|
279
510
|
: null;
|
|
280
511
|
const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
|
|
281
|
-
const beforeTokens =
|
|
512
|
+
const beforeTokens = targetEntry[0].tokens;
|
|
282
513
|
const afterTokens = current ? countTokens(current) : beforeTokens;
|
|
283
514
|
|
|
284
|
-
await store.
|
|
515
|
+
await store.set({
|
|
516
|
+
runId,
|
|
517
|
+
turn,
|
|
518
|
+
path: entry.path,
|
|
519
|
+
body: current,
|
|
520
|
+
state,
|
|
521
|
+
outcome,
|
|
285
522
|
attributes: {
|
|
286
|
-
|
|
523
|
+
path: entryPath,
|
|
287
524
|
patch: udiff,
|
|
288
525
|
merge,
|
|
289
526
|
beforeTokens,
|
|
@@ -296,9 +533,15 @@ export default class Set {
|
|
|
296
533
|
}
|
|
297
534
|
}
|
|
298
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
|
+
|
|
299
542
|
static #buildRevision(attrs) {
|
|
300
543
|
if (attrs.search != null) {
|
|
301
|
-
return { search: attrs.search, replace: attrs
|
|
544
|
+
return { search: attrs.search, replace: Set.#resolveReplace(attrs) };
|
|
302
545
|
}
|
|
303
546
|
if (attrs.blocks?.length > 0) {
|
|
304
547
|
return { blocks: attrs.blocks };
|
|
@@ -308,7 +551,7 @@ export default class Set {
|
|
|
308
551
|
|
|
309
552
|
static #applyRevision(body, attrs) {
|
|
310
553
|
if (attrs.search != null) {
|
|
311
|
-
return Hedberg.replace(body, attrs.search, attrs
|
|
554
|
+
return Hedberg.replace(body, attrs.search, Set.#resolveReplace(attrs), {
|
|
312
555
|
sed: attrs.sed,
|
|
313
556
|
flags: attrs.flags,
|
|
314
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="demoted" 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/We're almost done/We're done./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></sh> or <env></env> to list, create, read, or edit files — use <get></get> and <set></set>",
|
|
32
|
-
"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.",
|
|
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
|
|